diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddd8db..2e03bed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.7.0 + +- Replace deps.sh by cmake/Modules. +- Bump the versions of dependencies. +- Add emscripten build support for CCGEN_ENABLE_JPEG=ON, CCGEN_ENABLE_WEBP=ON + and CCGEN_ENABLE_WEBP2=ON. + ## v0.6.7 - Support images of dimensions below 8 pixels despite some distortion metrics. diff --git a/CMakeLists.txt b/CMakeLists.txt index b70e7cc..d056d84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,9 +16,26 @@ cmake_minimum_required(VERSION 3.20) project( codec-compare-gen LANGUAGES CXX - VERSION 0.6.7) + VERSION 0.7.0) set(CMAKE_CXX_STANDARD 17) +# Options + +option(CCGEN_ENABLE_AVIF "Build with AVIF support (libavif[aom+dav1d])" ON) +option(CCGEN_ENABLE_HEIF "Build with HEIF support (libheif[aom+dav1d])" OFF) +option(CCGEN_ENABLE_JPEG "Build with JPEG support (Turbo, Moz, sjpeg)" ON) +option(CCGEN_ENABLE_JPEG2000 "Build with JPEG 2000 support (OpenJPEG)" OFF) +option(CCGEN_ENABLE_JPEGXL "Build with JPEG XL support (libjxl, jpegli)" ON) +option(CCGEN_ENABLE_WEBP "Build with WebP support (libwebp)" ON) +option(CCGEN_ENABLE_WEBP2 "Build with WebP 2 support (libwebp2) [REQUIRED]" ON) +option(CCGEN_ENABLE_FFV1 "Build with FFV1 support (libavcodec from FFmpeg)" OFF) +option(CCGEN_ENABLE_BASIS "Build with Basis Universal support" OFF) + +option(CCGEN_ENABLE_DSSIM "Build the DSSIM distortion metric binary" ON) + +option(CCGEN_WASM "Build the WASM codec-compare-gen library with emscripten" + OFF) + option(BUILD_SHARED_LIBS "Build the shared codec-compare-gen library" ON) if(BUILD_SHARED_LIBS) set(CCGEN_PREFIX "${CMAKE_SHARED_LIBRARY_PREFIX}") @@ -28,7 +45,14 @@ else() set(CCGEN_SUFFIX "${CMAKE_STATIC_LIBRARY_SUFFIX}") endif() -set(CCGEN_TD "${CMAKE_CURRENT_SOURCE_DIR}/third_party") +option(CCGEN_BUILD_TESTING "Build the tests (requires GoogleTest)" OFF) +if(NOT CCGEN_BUILD_TESTING AND BUILD_TESTING) + # BUILD_TESTING gets disabled somewhere by some dependency. Rely on + # CCGEN_BUILD_TESTING instead but still read BUILD_TESTING. + set(CCGEN_BUILD_TESTING ON) +endif() + +# Library add_library( libccgen OBJECT @@ -82,146 +106,357 @@ endif() # Dependencies -target_compile_definitions(libccgen PRIVATE HAS_WEBP2) -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/libwebp2) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/libwebp2/build) -target_link_libraries( - libccgen ${CCGEN_TD}/libwebp2/build/${CCGEN_PREFIX}webp2${CCGEN_SUFFIX}) -target_link_libraries( - libccgen ${CCGEN_TD}/libwebp2/build/${CCGEN_PREFIX}imageio${CCGEN_SUFFIX}) - -target_compile_definitions(libccgen PRIVATE HAS_AVIF) -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/libavif/include) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/libavif/build) -target_link_libraries( - libccgen ${CCGEN_TD}/libavif/build/${CCGEN_PREFIX}avif${CCGEN_SUFFIX}) - -target_compile_definitions(libccgen PRIVATE HAS_HEIF) -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/libheif/libheif/api) -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/libheif/build) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/libheif/build/libheif) -target_link_libraries( - libccgen ${CCGEN_TD}/libheif/build/libheif/${CCGEN_PREFIX}heif${CCGEN_SUFFIX}) - -target_compile_definitions(libccgen PRIVATE HAS_WEBP) -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/libwebp/src) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/libwebp/build) -target_link_libraries( - libccgen ${CCGEN_TD}/libwebp/build/${CCGEN_PREFIX}webpmux${CCGEN_SUFFIX} - ${CCGEN_TD}/libwebp/build/${CCGEN_PREFIX}webpdemux${CCGEN_SUFFIX} - ${CCGEN_TD}/libwebp/build/${CCGEN_PREFIX}webp${CCGEN_SUFFIX}) -# libccgen also uses WebP encoding and decoding through libwebp2. -target_compile_definitions(libccgen PRIVATE WP2_HAVE_WEBP) - -target_compile_definitions(libccgen PRIVATE HAS_OPENJPEG) -# For openjpeg.h. -target_include_directories(libccgen - PRIVATE ${CCGEN_TD}/openjpeg/src/lib/openjp2) -# For opj_config.h included by openjpeg.h. -target_include_directories(libccgen - PRIVATE ${CCGEN_TD}/openjpeg/build/src/lib/openjp2) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/openjpeg/build/bin) -target_link_libraries( - libccgen ${CCGEN_TD}/openjpeg/build/bin/${CCGEN_PREFIX}openjp2${CCGEN_SUFFIX}) - -target_compile_definitions(libccgen PRIVATE HAS_FFV1) -# For avcodec.h. -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/FFmpeg/build/include) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/FFmpeg/build/lib) -target_link_libraries( - libccgen ${CCGEN_TD}/FFmpeg/build/lib/${CCGEN_PREFIX}avcodec${CCGEN_SUFFIX} - ${CCGEN_TD}/FFmpeg/build/lib/${CCGEN_PREFIX}avutil${CCGEN_SUFFIX}) - -target_compile_definitions(libccgen PRIVATE HAS_JPEGXL) -target_include_directories(libccgen - PRIVATE ${CCGEN_TD}/libjxl/build/lib/include) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/libjxl/build/lib) -target_link_libraries( - libccgen ${CCGEN_TD}/libjxl/build/lib/${CCGEN_PREFIX}jxl${CCGEN_SUFFIX}) -# jpegli is part of libjxl. For lib/jpegli/types.h included by common.h included -# by codec_jpegli.cc: -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/libjxl) -# libjxl does not generate any shared libjpegli binary. Using the libjpeg -# drop-in replacement would result in symbol collisions with other similar -# implementations. Use the static binary and its dependency instead: -target_link_libraries( - libccgen - ${CCGEN_TD}/libjxl/build/lib/${CMAKE_STATIC_LIBRARY_PREFIX}jpegli-static${CMAKE_STATIC_LIBRARY_SUFFIX} - ${CCGEN_TD}/libjxl/build/third_party/highway/${CMAKE_STATIC_LIBRARY_PREFIX}hwy${CMAKE_STATIC_LIBRARY_SUFFIX} -) - -target_compile_definitions(libccgen PRIVATE HAS_JPEGMOZ) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/mozjpeg/build) -target_link_libraries( - libccgen ${CCGEN_TD}/mozjpeg/build/${CCGEN_PREFIX}jpeg${CCGEN_SUFFIX}) - -target_compile_definitions(libccgen PRIVATE HAS_JPEGSIMPLE) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/sjpeg/build) -target_link_libraries( - libccgen ${CCGEN_TD}/sjpeg/build/${CCGEN_PREFIX}sjpeg${CCGEN_SUFFIX}) - -target_compile_definitions(libccgen PRIVATE HAS_JPEGTURBO) -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/libjpeg_turbo - ${CCGEN_TD}/libjpeg_turbo/build) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/libjpeg_turbo/build) -target_link_libraries( - libccgen - ${CCGEN_TD}/libjpeg_turbo/build/${CCGEN_PREFIX}turbojpeg${CCGEN_SUFFIX}) - -target_compile_definitions(libccgen PRIVATE HAS_BASIS) -target_include_directories(libccgen PRIVATE ${CCGEN_TD}/basis_universal) -target_link_directories(libccgen PRIVATE ${CCGEN_TD}/basis_universal/build) -# The provided CMakeLists.txt only generates a static library. -target_link_libraries( - libccgen - ${CCGEN_TD}/basis_universal/build/${CCGEN_PREFIX}basisu_encoder${CMAKE_STATIC_LIBRARY_SUFFIX} -) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules") + +if(CCGEN_WASM) + set(CCGEN_ENABLE_PNG ON) # Could be an option for when CCGEN_WASM=OFF. + include(LocalLibzlib) + include(LocalLibpng) +else() + set(CCGEN_ENABLE_PNG OFF) +endif() + +if(CCGEN_ENABLE_JPEG) + target_compile_definitions(libccgen PRIVATE HAS_JPEGMOZ) + # mozjpeg (fork of turbojpeg) forbids add_subdirectory() and thus + # FetchContent. It also forbids ExternalProject_Add() initiated from a + # subdirectory. See + # https://github.com/mozilla/mozjpeg/blob/08265790774cd0714832c9e675522acbe5581437/CMakeLists.txt#L36-L62 + + set(mozjpeg_GIT_REPOSITORY "https://github.com/mozilla/mozjpeg.git") + # Keep JpegmozVersion() in src/codec_jpegmoz.cc in sync with GIT_TAG. + set(mozjpeg_GIT_TAG "08265790774cd0714832c9e675522acbe5581437") # 5.0.X + + include(ExternalProject) + if(CCGEN_WASM) + ExternalProject_Add( + mozjpeg + GIT_REPOSITORY ${mozjpeg_GIT_REPOSITORY} + GIT_TAG ${mozjpeg_GIT_TAG} + PREFIX ${CMAKE_BINARY_DIR}/_deps + CONFIGURE_COMMAND + emcmake ${CMAKE_COMMAND} -G ${CMAKE_GENERATOR} -S -B + -DCMAKE_BUILD_TYPE=$ + -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS} -DPNG_SUPPORTED=FALSE + -DWITH_TURBOJPEG=FALSE -DWITH_SIMD=FALSE + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + BUILD_COMMAND emmake ${CMAKE_COMMAND} --build --config + $ --parallel + INSTALL_COMMAND "" + BUILD_BYPRODUCTS /${CCGEN_PREFIX}jpeg${CCGEN_SUFFIX}) + add_library(jpeg STATIC IMPORTED GLOBAL) + add_dependencies(jpeg mozjpeg) + set_target_properties( + jpeg + PROPERTIES + IMPORTED_LOCATION + ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg-build/${CCGEN_PREFIX}jpeg${CCGEN_SUFFIX} + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg + ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg-build + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES + ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg + ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg-build) + else() + ExternalProject_Add( + mozjpeg + GIT_REPOSITORY ${mozjpeg_GIT_REPOSITORY} + GIT_TAG ${mozjpeg_GIT_TAG} + PREFIX ${CMAKE_BINARY_DIR}/_deps + CMAKE_ARGS -DPNG_SUPPORTED=FALSE -DWITH_TURBOJPEG=FALSE + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS} + INSTALL_COMMAND "" + BUILD_BYPRODUCTS /${CCGEN_PREFIX}jpeg${CCGEN_SUFFIX}) + endif() + target_include_directories( + libccgen PRIVATE ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg + ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg-build) + target_link_libraries( + libccgen + ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg-build/${CCGEN_PREFIX}jpeg${CCGEN_SUFFIX} + ) + + # Skip sjpeg's find_package(JPEG). + include(FetchContent) + FetchContent_Declare(JPEG DOWNLOAD_COMMAND "" OVERRIDE_FIND_PACKAGE SYSTEM) + set(JPEG_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg + ${CMAKE_BINARY_DIR}/_deps/src/mozjpeg-build) + set(JPEG_LIBRARIES jpeg) + + target_compile_definitions(libccgen PRIVATE HAS_JPEGSIMPLE) + include(LocalSjpeg) + target_link_libraries(libccgen sjpeg) + + target_compile_definitions(libccgen PRIVATE HAS_JPEGTURBO) + # libjpeg-turbo forbids add_subdirectory() and thus FetchContent. It also + # forbids ExternalProject_Add() initiated from a subdirectory. See + # https://github.com/libjpeg-turbo/libjpeg-turbo/blob/3.1.3/CMakeLists.txt#L45-L71 + + set(libjpeg_turbo_GIT_REPOSITORY + "https://github.com/libjpeg-turbo/libjpeg-turbo.git") + set(libjpeg_turbo_GIT_TAG "3.1.3") + + include(ExternalProject) + if(CCGEN_WASM) + ExternalProject_Add( + libjpeg_turbo + GIT_REPOSITORY ${libjpeg_turbo_GIT_REPOSITORY} + GIT_TAG ${libjpeg_turbo_GIT_TAG} + PREFIX ${CMAKE_BINARY_DIR}/_deps + CONFIGURE_COMMAND + emcmake ${CMAKE_COMMAND} -G ${CMAKE_GENERATOR} -S -B + -DCMAKE_BUILD_TYPE=$ + -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS} + -DENABLE_SHARED=${BUILD_SHARED_LIBS} + -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DWITH_TURBOJPEG=TRUE + -DWITH_SIMD=FALSE + BUILD_COMMAND emmake ${CMAKE_COMMAND} --build --config + $ --parallel + INSTALL_COMMAND "" + BUILD_BYPRODUCTS /${CCGEN_PREFIX}turbojpeg${CCGEN_SUFFIX}) + else() + ExternalProject_Add( + libjpeg_turbo + GIT_REPOSITORY ${libjpeg_turbo_GIT_REPOSITORY} + GIT_TAG ${libjpeg_turbo_GIT_TAG} + PREFIX ${CMAKE_BINARY_DIR}/_deps + CMAKE_ARGS -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS} + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + INSTALL_COMMAND "" + BUILD_BYPRODUCTS /${CCGEN_PREFIX}turbojpeg${CCGEN_SUFFIX}) + endif() + target_include_directories( + libccgen PRIVATE ${CMAKE_BINARY_DIR}/_deps/src/libjpeg_turbo + ${CMAKE_BINARY_DIR}/_deps/src/libjpeg_turbo-build) + target_link_libraries( + libccgen + ${CMAKE_BINARY_DIR}/_deps/src/libjpeg_turbo-build/${CCGEN_PREFIX}turbojpeg${CCGEN_SUFFIX} + ) +endif() + +if(CCGEN_ENABLE_AVIF) + include(LocalLibavif) + target_compile_definitions(libccgen PRIVATE HAS_AVIF) + target_link_libraries(libccgen avif) +endif() +if(CCGEN_ENABLE_HEIF) + if(NOT CCGEN_ENABLE_AVIF) + message(FATAL_ERROR "CCGEN_ENABLE_HEIF=ON requires CCGEN_ENABLE_AVIF=ON.") + endif() + include(LocalLibheif) + target_compile_definitions(libccgen PRIVATE HAS_HEIF) + target_link_libraries(libccgen heif) +endif() + +if(CCGEN_ENABLE_WEBP) + include(LocalLibwebp) + target_compile_definitions(libccgen PRIVATE HAS_WEBP) + target_link_libraries(libccgen webpmux webpdemux webp) +endif() + +if(CCGEN_ENABLE_WEBP2) + include(LocalLibwebp2) + target_compile_definitions(libccgen PRIVATE HAS_WEBP2) + target_link_libraries(libccgen webp2 imageio) + if(CCGEN_ENABLE_WEBP) + # libccgen also uses WebP encoding and decoding through libwebp2. + target_compile_definitions(libccgen PRIVATE WP2_HAVE_WEBP) + endif() + + # Hardcode this include path so that libwebp2/src/utils/utils.h is included + # instead of libwebp/src/utils/utils.h. + target_include_directories(libccgen PRIVATE ${libwebp2_SOURCE_DIR}) +else() + message(FATAL_ERROR "codec-compare-gen requires CCGEN_ENABLE_WEBP2=ON.") +endif() + +if(CCGEN_ENABLE_JPEG2000) + target_compile_definitions(libccgen PRIVATE HAS_OPENJPEG) + # FetchContent_MakeAvailable() results in weird build mixups such as: + # + # No rule to make target '_deps/libaom-build/libaom.a', needed by + # '_deps/libopenjpeg-build/bin/heif-test'. + # + # But there is no heif-test in libopenjpeg. Rely on ExternalProject_Add() + # instead. + include(ExternalProject) + ExternalProject_Add( + libopenjpeg + GIT_REPOSITORY https://github.com/uclouvain/openjpeg.git + GIT_TAG v2.5.4 + PREFIX ${CMAKE_BINARY_DIR}/_deps + INSTALL_COMMAND "" + BUILD_BYPRODUCTS /bin/${CCGEN_PREFIX}openjp2${CCGEN_SUFFIX}) + # For openjpeg.h. + target_include_directories( + libccgen PRIVATE ${CMAKE_BINARY_DIR}/_deps/src/libopenjpeg/src/lib/openjp2) + # For opj_config.h included by openjpeg.h. + target_include_directories( + libccgen + PRIVATE ${CMAKE_BINARY_DIR}/_deps/src/libopenjpeg-build/src/lib/openjp2) + target_link_libraries( + libccgen + ${CMAKE_BINARY_DIR}/_deps/src/libopenjpeg-build/bin/${CCGEN_PREFIX}openjp2${CCGEN_SUFFIX} + ) +endif() + +if(CCGEN_ENABLE_FFV1) + target_compile_definitions(libccgen PRIVATE HAS_FFV1) + include(ExternalProject) + ExternalProject_Add( + libavcodec + GIT_REPOSITORY https://github.com/FFmpeg/FFmpeg.git + GIT_TAG n8.1 + PREFIX ${CMAKE_BINARY_DIR}/_deps + CONFIGURE_COMMAND /configure --prefix= + --enable-shared + BUILD_COMMAND make libavcodec -j + INSTALL_COMMAND make install libavcodec -j + BUILD_BYPRODUCTS /lib/${CCGEN_PREFIX}avcodec${CCGEN_SUFFIX} + /lib/${CCGEN_PREFIX}avutil${CCGEN_SUFFIX}) + target_include_directories( + libccgen PRIVATE ${CMAKE_BINARY_DIR}/_deps/src/libavcodec-build/include) + target_link_libraries( + libccgen + ${CMAKE_BINARY_DIR}/_deps/src/libavcodec-build/lib/${CCGEN_PREFIX}avcodec${CCGEN_SUFFIX} + ${CMAKE_BINARY_DIR}/_deps/src/libavcodec-build/lib/${CCGEN_PREFIX}avutil${CCGEN_SUFFIX} + ) + if(NOT BUILD_SHARED_LIBS) + target_link_libraries(libccgen lzma swresample) + endif() + # Avoid race conditions leading to "No rule to make libavcodec" errors. + add_dependencies(libccgen libavcodec) +endif() + +if(CCGEN_ENABLE_JPEGXL) + include(LocalLibjxl) + target_compile_definitions(libccgen PRIVATE HAS_JPEGXL HAS_JPEGLI) + target_link_libraries(libccgen jxl jpegli-static) +elseif(NOT CCGEN_WASM) + message( + WARNING + "codec-compare-gen requires CCGEN_ENABLE_JPEGXL=ON when comparing lossy codecs." + ) +endif() + +# Metric binaries called by ccgen because the API is missing or weird. +if(CCGEN_ENABLE_DSSIM) + if(CCGEN_WASM) + message( + FATAL_ERROR "CCGEN_ENABLE_DSSIM=ON is not supported with CCGEN_WASM=ON.") + endif() + include(LocalDssim) + add_dependencies(libccgen dssim_binary) +elseif(NOT CCGEN_WASM) + message( + WARNING + "codec-compare-gen requires CCGEN_ENABLE_DSSIM=ON when comparing lossy codecs." + ) +endif() + +if(CCGEN_ENABLE_JPEGXL) + # These are built alongside libjxl. Reference them here anyway. + add_dependencies(libccgen ssimulacra_main) + add_dependencies(libccgen ssimulacra2) + add_dependencies(libccgen butteraugli_main) # Also used for P3-norm. +endif() + +if(CCGEN_ENABLE_BASIS) + target_compile_definitions(libccgen PRIVATE HAS_BASIS) + include(LocalBasisuniversal) + target_link_libraries(libccgen basisu_encoder) +endif() + +# WASM + +if(CCGEN_WASM) + if(NOT EMSCRIPTEN_VERSION) + message(WARNING "EMSCRIPTEN_VERSION not detected") + endif() + + add_executable(codec_wasm wasm/codec_wasm.cc) + target_include_directories(codec_wasm PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(codec_wasm libccgen) + set_target_properties(codec_wasm PROPERTIES OUTPUT_NAME "codec_wasm_bin") + set_target_properties(codec_wasm PROPERTIES SUFFIX ".js") + target_link_options( + codec_wasm PRIVATE "-sMODULARIZE=1" "-sEXPORT_NAME=loadCodecWasm" + "-sENVIRONMENT=node,worker" "--bind") + # Making the following work would be nicer for TypeScript integration: + # "--emit-tsd=${CMAKE_CURRENT_BINARY_DIR}/codec_wasm_bin.d.ts" +endif() # Tools -add_executable(ccgen tools/ccgen_impl.cc tools/ccgen.cc) -target_include_directories(ccgen PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(ccgen libccgen) +if(NOT CCGEN_WASM) + add_executable(ccgen tools/ccgen_impl.cc tools/ccgen.cc) + target_include_directories(ccgen PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(ccgen libccgen) -add_executable(are_images_equivalent tools/are_images_equivalent.cc) -target_include_directories(are_images_equivalent - PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(are_images_equivalent libccgen) -target_compile_definitions(are_images_equivalent PRIVATE HAS_WEBP2) + add_executable(are_images_equivalent tools/are_images_equivalent.cc) + target_include_directories(are_images_equivalent + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(are_images_equivalent libccgen) + target_compile_definitions(are_images_equivalent PRIVATE HAS_WEBP2) -add_executable(strip_metadata tools/strip_metadata.cc) -target_include_directories(strip_metadata PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(strip_metadata libccgen) -target_compile_definitions(strip_metadata PRIVATE HAS_WEBP2) + add_executable(strip_metadata tools/strip_metadata.cc) + target_include_directories(strip_metadata PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(strip_metadata libccgen) + target_compile_definitions(strip_metadata PRIVATE HAS_WEBP2) +endif() # Tests -option(BUILD_TESTING "Build the tests (requires GoogleTest)" OFF) -if(BUILD_TESTING) - find_package(GTest REQUIRED) +if(CCGEN_BUILD_TESTING) enable_testing() + if(CCGEN_WASM) + # Create a node.js package so that run npm i and npm run test can be used. + configure_file(wasm/package.json package.json COPYONLY) + configure_file(wasm/tsconfig.json tsconfig.json COPYONLY) + configure_file(wasm/tests/web-test-runner.config.mjs + web-test-runner.config.mjs COPYONLY) + add_custom_command( + TARGET codec_wasm + POST_BUILD + COMMAND npm i + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + + configure_file(wasm/tests/test_wasm_test.ts test_wasm_test.ts COPYONLY) + configure_file(tests/data/gradient32x32.png gradient32x32.png COPYONLY) + add_test( + NAME test_wasm_test + COMMAND npm run test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + else() + find_package(GTest REQUIRED) + + macro(add_ccgen_gtest TEST_NAME) + add_executable(${TEST_NAME} tools/ccgen_impl.cc tests/${TEST_NAME}.cc) + target_include_directories(${TEST_NAME} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(${TEST_NAME} PRIVATE libccgen GTest::gtest) + target_compile_definitions(${TEST_NAME} PRIVATE HAS_WEBP2) + if(${ARGC} EQUAL 3) + add_test(NAME ${TEST_NAME} + COMMAND ${TEST_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/${ARGV1}/ + ${ARGV2}/) + elseif(${ARGC} EQUAL 2) + add_test(NAME ${TEST_NAME} + COMMAND ${TEST_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/${ARGV1}/) + else() + target_link_libraries(${TEST_NAME} PRIVATE GTest::gtest_main) + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) + endif() + endmacro() - macro(add_ccgen_gtest TEST_NAME) - add_executable(${TEST_NAME} tools/ccgen_impl.cc tests/${TEST_NAME}.cc) - target_include_directories(${TEST_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(${TEST_NAME} PRIVATE libccgen GTest::gtest) - target_compile_definitions(${TEST_NAME} PRIVATE HAS_WEBP2) - if(${ARGC} EQUAL 2) - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME} - ${CMAKE_CURRENT_SOURCE_DIR}/${ARGV1}/) - else() - target_link_libraries(${TEST_NAME} PRIVATE GTest::gtest_main) - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) - endif() - endmacro() - - add_ccgen_gtest(test_ccgen tests/data) - add_ccgen_gtest(test_codec tests/data) - add_ccgen_gtest(test_codec_avif) - add_ccgen_gtest(test_codec_sjpeg tests/data) - add_ccgen_gtest(test_distortion tests/data) - add_ccgen_gtest(test_framework tests/data) - add_ccgen_gtest(test_serialization) - add_ccgen_gtest(test_task) - add_ccgen_gtest(test_worker) + add_ccgen_gtest(test_ccgen tests/data) + add_ccgen_gtest(test_codec tests/data) + add_ccgen_gtest(test_codec_avif) + add_ccgen_gtest(test_codec_sjpeg tests/data) + add_ccgen_gtest(test_distortion tests/data ${CMAKE_CURRENT_BINARY_DIR}) + add_ccgen_gtest(test_framework tests/data) + add_ccgen_gtest(test_serialization) + add_ccgen_gtest(test_task) + add_ccgen_gtest(test_worker) + endif() endif() diff --git a/README.md b/README.md index 731e3ef..29a2a7d 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,24 @@ Build `tools/ccgen.cc` and look at the description given by the `--help` flag. The `libccgen` API entrypoint lies in `src/framework.h`. -## CMake build +## Build The following instructions are used to build the library and the `ccgen` command line tool. +### Requirements + +```sh +# For libjxl +sudo apt install libhwy-dev +``` + +### Instructions + Clone the codec-compare-gen repository. Then run from its root folder: ```sh -./deps.sh -cmake -S . -B build -DCMAKE_CXX_COMPILER=clang++ +cmake -S . -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ cmake --build build --parallel ``` @@ -59,12 +67,48 @@ build/ccgen \ ## Tests The following instructions are used to make sure the unit tests pass. -`libgtest-dev` must be installed on the system. Run `deps.sh` if not done yet. +`libgtest-dev` must be installed on the system. ```sh -cmake -S . -B build -DBUILD_TESTING=ON -DCMAKE_CXX_COMPILER=clang++ -cmake --build build --parallel -ctest --test-dir build --output-on-failure -j7 +cmake -S . -B build \ + -DCCGEN_BUILD_TESTING=ON \ + -DCCGEN_ENABLE_AVIF=ON \ + -DCCGEN_ENABLE_HEIF=ON \ + -DCCGEN_ENABLE_JPEG=ON \ + -DCCGEN_ENABLE_JPEG2000=ON \ + -DCCGEN_ENABLE_JPEGXL=ON \ + -DCCGEN_ENABLE_WEBP=ON \ + -DCCGEN_ENABLE_WEBP2=ON \ + -DCCGEN_ENABLE_FFV1=ON \ + -DCCGEN_ENABLE_BASIS=ON \ + -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 +cmake --build build -j$(nproc) +ctest --test-dir build --output-on-failure -j$(nproc) +``` + +## WASM/emscripten build + +```sh +emcmake cmake -S . -B build_wasm \ + -DCCGEN_ENABLE_JPEGXL=OFF \ + -DCCGEN_ENABLE_WEBP2=ON \ + -DCCGEN_ENABLE_DSSIM=OFF \ + -DCCGEN_WASM=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF +emmake cmake --build build_wasm -j$(nproc) +``` + +### Typescript tests + +```sh +emcmake cmake -S . -B build_wasm \ + -DCCGEN_BUILD_TESTING=ON -DCCGEN_ENABLE_JPEG=ON -DCCGEN_ENABLE_JPEGXL=OFF \ + -DCCGEN_ENABLE_WEBP=ON -DCCGEN_ENABLE_WEBP2=ON -DCCGEN_ENABLE_DSSIM=OFF \ + -DCCGEN_WASM=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF +emmake cmake --build build_wasm -j$(nproc) +ctest --test-dir build --output-on-failure -j$(nproc) ``` ## C++ style @@ -72,7 +116,8 @@ ctest --test-dir build --output-on-failure -j7 Use the following to format the code: ```sh -clang-format -style=file -i src/*.cc src/*.h tests/*.cc tools/*.cc tools/*.h +clang-format -style=file -i src/* tests/*.cc tools/ wasm/*.cc +cmake-format -i CMakeLists.txt cmake/Modules/* ``` ## License diff --git a/cmake/Modules/CcgenFetchContent.cmake b/cmake/Modules/CcgenFetchContent.cmake new file mode 100644 index 0000000..b720200 --- /dev/null +++ b/cmake/Modules/CcgenFetchContent.cmake @@ -0,0 +1,22 @@ +include(FetchContent) + +function(ccgen_fetchcontent_makeavailable name) + FetchContent_GetProperties(${name}) + if(NOT ${name}_POPULATED) + set(BUILD_SHARED_LIBS OFF) + set(BUILD_TESTING OFF) + + FetchContent_MakeAvailable(${name}) + + FetchContent_GetProperties(${name}) + set(${name}_SOURCE_DIR + ${${name}_SOURCE_DIR} + PARENT_SCOPE) + set(${name}_BINARY_DIR + ${${name}_BINARY_DIR} + PARENT_SCOPE) + set(${name}_POPULATED + ${${name}_POPULATED} + PARENT_SCOPE) + endif() +endfunction() diff --git a/cmake/Modules/LocalBasisuniversal.cmake b/cmake/Modules/LocalBasisuniversal.cmake new file mode 100644 index 0000000..ed345c1 --- /dev/null +++ b/cmake/Modules/LocalBasisuniversal.cmake @@ -0,0 +1,19 @@ +include(FetchContent) +include(CcgenFetchContent) + +FetchContent_Declare( + libbasisu + GIT_REPOSITORY "https://github.com/BinomialLLC/basis_universal.git" + GIT_TAG v2_1_0 + GIT_PROGRESS ON + GIT_SHALLOW ON + UPDATE_COMMAND "") + +set(BASISU_SSE + ON + CACHE INTERNAL "") + +ccgen_fetchcontent_makeavailable(libbasisu) + +target_include_directories(basisu_encoder + INTERFACE $) diff --git a/cmake/Modules/LocalDssim.cmake b/cmake/Modules/LocalDssim.cmake new file mode 100644 index 0000000..9598fd8 --- /dev/null +++ b/cmake/Modules/LocalDssim.cmake @@ -0,0 +1,11 @@ +include(FetchContent) + +FetchContent_Declare( + dssim URL https://github.com/kornelski/dssim/archive/refs/tags/3.4.0.tar.gz + DOWNLOAD_EXTRACT_TIMESTAMP false) +FetchContent_MakeAvailable(dssim) + +add_custom_target( + dssim_binary + COMMAND cargo build --release + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_deps/dssim-src) diff --git a/cmake/Modules/LocalLibavif.cmake b/cmake/Modules/LocalLibavif.cmake new file mode 100644 index 0000000..63a1bde --- /dev/null +++ b/cmake/Modules/LocalLibavif.cmake @@ -0,0 +1,48 @@ +include(FetchContent) +include(CcgenFetchContent) + +FetchContent_Declare( + libavif + GIT_REPOSITORY "https://github.com/AOMediaCodec/libavif.git" + # GIT_TAG v1.4.1 does not contain the latest AVM tag. + GIT_TAG 257b1e45f979491d905786d80691ba33a7597290 + GIT_PROGRESS ON + GIT_SHALLOW OFF + UPDATE_COMMAND "") + +set(AVIF_BUILD_APPS + OFF + CACHE INTERNAL "") +set(AVIF_BUILD_EXAMPLES + OFF + CACHE INTERNAL "") +set(AVIF_BUILD_TESTS + OFF + CACHE INTERNAL "") +set(AVIF_CODEC_AOM + LOCAL + CACHE INTERNAL "") +set(AVIF_CODEC_DAV1D + LOCAL + CACHE INTERNAL "") +set(AVIF_CODEC_AVM + LOCAL + CACHE INTERNAL "") +set(AVIF_LIBYUV + LOCAL + CACHE INTERNAL "") +set(AVIF_LIBSHARPYUV + LOCAL + CACHE INTERNAL "") +set(AVIF_ENABLE_EXPERIMENTAL_MINI + ON + CACHE INTERNAL "") + +include_directories("${CMAKE_BINARY_DIR}/flatbuffers/include") + +ccgen_fetchcontent_makeavailable(libavif) + +if(CCGEN_ENABLE_WEBP) + # To avoid building libsharpyuv twice. + add_dependencies(avif webp) +endif() diff --git a/cmake/Modules/LocalLibheif.cmake b/cmake/Modules/LocalLibheif.cmake new file mode 100644 index 0000000..70c9cf4 --- /dev/null +++ b/cmake/Modules/LocalLibheif.cmake @@ -0,0 +1,55 @@ +include(FetchContent) +include(CcgenFetchContent) + +FetchContent_Declare( + libheif + GIT_REPOSITORY "https://github.com/strukturag/libheif.git" + GIT_TAG v1.21.2 + GIT_PROGRESS ON + GIT_SHALLOW ON + UPDATE_COMMAND "") + +set(ENABLE_PLUGIN_LOADING + OFF + CACHE INTERNAL "") +set(WITH_AOM_DECODER + OFF + CACHE INTERNAL "") +set(WITH_AOM_ENCODER + ON + CACHE INTERNAL "") +set(WITH_DAV1D + ON + CACHE INTERNAL "") +set(WITH_LIBSHARPYUV + OFF + CACHE INTERNAL "") +set(ENABLE_MULTITHREADING_SUPPORT + OFF + CACHE INTERNAL "") +set(ENABLE_PARALLEL_TILE_DECODING + OFF + CACHE INTERNAL "") + +# Reuse the libaom and dav1d dependencies from libavif. +set(AOM_INCLUDE_DIR + "${CMAKE_BINARY_DIR}/_deps/libaom-src/" + CACHE INTERNAL "") +set(AOM_LIBRARY + "${CMAKE_BINARY_DIR}/_deps/libaom-build/libaom.a" + CACHE INTERNAL "") +set(DAV1D_INCLUDE_DIR + "${CMAKE_BINARY_DIR}/_deps/dav1d-install/include/" + CACHE INTERNAL "") +set(DAV1D_LIBRARY + "${CMAKE_BINARY_DIR}/_deps/dav1d-install/lib/libdav1d.a" + CACHE INTERNAL "") + +ccgen_fetchcontent_makeavailable(libheif) + +target_include_directories( + heif INTERFACE $/libheif/api) +target_include_directories(heif + INTERFACE $) + +add_dependencies(heif aom dav1d) diff --git a/cmake/Modules/LocalLibjxl.cmake b/cmake/Modules/LocalLibjxl.cmake new file mode 100644 index 0000000..95be5b5 --- /dev/null +++ b/cmake/Modules/LocalLibjxl.cmake @@ -0,0 +1,68 @@ +include(FetchContent) +include(CcgenFetchContent) + +FetchContent_Declare( + libjxl + GIT_REPOSITORY "https://github.com/libjxl/libjxl.git" + GIT_TAG v0.11.2 + GIT_PROGRESS ON + GIT_SHALLOW ON + UPDATE_COMMAND "") + +set(JPEGXL_ENABLE_BENCHMARK + OFF + CACHE INTERNAL "") +set(JPEGXL_ENABLE_DOXYGEN + OFF + CACHE INTERNAL "") +set(JPEGXL_ENABLE_EXAMPLES + OFF + CACHE INTERNAL "") +set(JPEGXL_ENABLE_JNI + OFF + CACHE INTERNAL "") +set(JPEGXL_ENABLE_MANPAGES + OFF + CACHE INTERNAL "") +set(JPEGXL_ENABLE_OPENEXR + OFF + CACHE INTERNAL "") +set(JPEGXL_ENABLE_SJPEG + OFF + CACHE INTERNAL "") +set(JPEGXL_ENABLE_TRANSCODE_JPEG + OFF + CACHE INTERNAL "") +set(JPEGXL_ENABLE_WASM_THREADS + OFF + CACHE INTERNAL "") +set(JPEGXL_FORCE_SYSTEM_BROTLI + OFF + CACHE INTERNAL "") +set(JPEGXL_FORCE_SYSTEM_HWY + OFF + CACHE INTERNAL "") + +# jpegli was still part of libjxl in v0.11.2 but will be removed. See +# https://github.com/libjxl/libjxl/pull/4657. +set(JPEGXL_ENABLE_JPEGLI + ON + CACHE INTERNAL "") +set(JPEGXL_ENABLE_JPEGLI_LIBJPEG + OFF + CACHE INTERNAL "") + +# JPEGXL_ENABLE_DEVTOOLS=ON for Butteraugli and SSIMULACRA2 metric binaries. See +# https://github.com/cloudinary/ssimulacra2/blob/d2be72505ddc5c92aeb30f4a7f3ab53db45b314b/build_ssimulacra_from_libjxl_repo +set(JPEGXL_ENABLE_DEVTOOLS + ON + CACHE INTERNAL "") +# JPEGXL_ENABLE_DEVTOOLS=ON somehow requires JPEGXL_ENABLE_TOOLS=ON. +set(JPEGXL_ENABLE_TOOLS + ON + CACHE INTERNAL "") + +ccgen_fetchcontent_makeavailable(libjxl) + +target_include_directories(jxl + INTERFACE $) diff --git a/cmake/Modules/LocalLibpng.cmake b/cmake/Modules/LocalLibpng.cmake new file mode 100644 index 0000000..5dda2f2 --- /dev/null +++ b/cmake/Modules/LocalLibpng.cmake @@ -0,0 +1,30 @@ +include(FetchContent) +include(CcgenFetchContent) + +FetchContent_Declare( + PNG + GIT_REPOSITORY "https://github.com/pnggroup/libpng" + GIT_TAG v1.6.58 + GIT_PROGRESS ON + UPDATE_COMMAND "" OVERRIDE_FIND_PACKAGE SYSTEM) + +set(PNG_SHARED + OFF + CACHE INTERNAL "") +set(ZLIB_USE_STATIC_LIBS + ON + CACHE INTERNAL "") + +ccgen_fetchcontent_makeavailable(PNG) + +target_include_directories(png_static + INTERFACE "$") +target_link_libraries(png_static PRIVATE zlibstatic) +target_link_libraries(png_static PRIVATE ZLIB::ZLIB) + +add_library(PNG::PNG ALIAS png_static) + +add_dependencies(png_static zlibstatic) + +set(PNG_INCLUDE_DIRS ${PNG_SOURCE_DIR}) +set(PNG_LIBRARIES png_static) diff --git a/cmake/Modules/LocalLibwebp.cmake b/cmake/Modules/LocalLibwebp.cmake new file mode 100644 index 0000000..09bc403 --- /dev/null +++ b/cmake/Modules/LocalLibwebp.cmake @@ -0,0 +1,66 @@ +include(FetchContent) +include(CcgenFetchContent) + +# Use the same FetchContent_Declare() args as libavif's libsharpyuv dependency. +FetchContent_Declare( + libwebp + GIT_REPOSITORY "https://chromium.googlesource.com/webm/libwebp" + GIT_TAG v1.6.0 + GIT_PROGRESS ON + GIT_SHALLOW ON + UPDATE_COMMAND "") + +set(WEBP_BUILD_ANIM_UTILS + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_CWEBP + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_DWEBP + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_EXTRAS + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_FUZZTEST + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_GIF2WEBP + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_IMG2WEBP + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_VWEBP + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_WEBPINFO + OFF + CACHE INTERNAL "") +set(WEBP_BUILD_WEBPMUX + OFF + CACHE INTERNAL "") + +set(WEBP_BUILD_LIBWEBPMUX + ON + CACHE INTERNAL "") + +ccgen_fetchcontent_makeavailable(libwebp) + +if(CCGEN_WASM) + add_library(webpmux ALIAS libwebpmux) +endif() + +target_include_directories( + webp INTERFACE $ + $) + +set(WEBP_INCLUDE_DIRS ${libwebp_SOURCE_DIR}/src) +set(WEBP_LIBRARIES webpmux webpdemux webp) + +if(CCGEN_ENABLE_JPEG) + add_dependencies(webp mozjpeg) +endif() + +# Skip libwebp2's find_package(WebP). +FetchContent_Declare(WebP DOWNLOAD_COMMAND "" OVERRIDE_FIND_PACKAGE SYSTEM) diff --git a/cmake/Modules/LocalLibwebp2.cmake b/cmake/Modules/LocalLibwebp2.cmake new file mode 100644 index 0000000..a684fee --- /dev/null +++ b/cmake/Modules/LocalLibwebp2.cmake @@ -0,0 +1,36 @@ +include(FetchContent) +include(CcgenFetchContent) + +FetchContent_Declare( + libwebp2 + GIT_REPOSITORY "https://chromium.googlesource.com/codecs/libwebp2" + GIT_TAG 142784b3090acdf6d156dd81416f1a6b18fdbdf1 + GIT_PROGRESS ON + UPDATE_COMMAND "") + +set(WP2_BUILD_EXAMPLES + OFF + CACHE INTERNAL "") +set(WP2_BUILD_EXTRAS + OFF + CACHE INTERNAL "") +set(WP2_BUILD_TESTS + OFF + CACHE INTERNAL "") + +ccgen_fetchcontent_makeavailable(libwebp2) + +target_include_directories(webp2 + INTERFACE $) + +if(CCGEN_ENABLE_PNG) + add_dependencies(webp2 png_static) +endif() + +if(CCGEN_ENABLE_JPEG) + add_dependencies(webp2 mozjpeg) +endif() + +if(CCGEN_ENABLE_WEBP) + add_dependencies(webp2 webp) +endif() diff --git a/cmake/Modules/LocalLibzlib.cmake b/cmake/Modules/LocalLibzlib.cmake new file mode 100644 index 0000000..b90206a --- /dev/null +++ b/cmake/Modules/LocalLibzlib.cmake @@ -0,0 +1,31 @@ +include(FetchContent) +include(CcgenFetchContent) + +FetchContent_Declare( + ZLIB + GIT_REPOSITORY "https://github.com/madler/zlib" + GIT_TAG v1.3.2 + GIT_PROGRESS ON + UPDATE_COMMAND "" OVERRIDE_FIND_PACKAGE SYSTEM) + +set(ZLIB_BUILD_SHARED + OFF + CACHE INTERNAL "") +set(ZLIB_BUILD_STATIC + ON + CACHE INTERNAL "") +set(ZLIB_BUILD_TESTING + OFF + CACHE INTERNAL "") +ccgen_fetchcontent_makeavailable(ZLIB) + +target_include_directories(zlibstatic + INTERFACE "$") + +add_library(ZLIB::ZLIB ALIAS zlibstatic) + +set(ZLIB_INCLUDE_DIRS ${zlib_SOURCE_DIR}) +set(ZLIB_LIBRARIES zlibstatic) +find_package(ZLIB REQUIRED) +message(STATUS "ZLIB_INCLUDE_DIRS: ${ZLIB_INCLUDE_DIRS}") +message(STATUS "ZLIB_LIBRARIES: ${ZLIB_LIBRARIES}") diff --git a/cmake/Modules/LocalSjpeg.cmake b/cmake/Modules/LocalSjpeg.cmake new file mode 100644 index 0000000..de361f5 --- /dev/null +++ b/cmake/Modules/LocalSjpeg.cmake @@ -0,0 +1,27 @@ +include(FetchContent) +include(CcgenFetchContent) + +FetchContent_Declare( + libsjpeg + GIT_REPOSITORY "https://github.com/webmproject/sjpeg.git" + GIT_TAG 46da5aec5fce05faabf1facf0066e36e6b1c4dff + GIT_PROGRESS ON + GIT_SHALLOW ON + UPDATE_COMMAND "") + +set(SJPEG_BUILD_EXAMPLES + OFF + CACHE INTERNAL "") + +ccgen_fetchcontent_makeavailable(libsjpeg) + +target_include_directories(sjpeg + INTERFACE $) + +if(CCGEN_ENABLE_PNG) + add_dependencies(sjpeg png_static) +endif() + +if(CCGEN_ENABLE_JPEG) + add_dependencies(sjpeg mozjpeg) +endif() diff --git a/deps.sh b/deps.sh deleted file mode 100755 index b63cfe5..0000000 --- a/deps.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/bin/bash -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Required dependencies: -# sudo apt install cmake meson ninja-build clang yasm libpng-dev libjpeg-dev cargo rustc -# Required dependencies for BUILD_TESTING=ON: -# sudo apt install libgtest-dev - -set -e - -NPROC=$(nproc) - -mkdir third_party -pushd third_party - - git clone -b v1.4.1 --depth 1 https://github.com/AOMediaCodec/libavif.git - pushd libavif - git checkout 6543b22b5bc706c53f038a16fe515f921556d9b3 # v1.4.1 - cmake -S . -B build \ - -DAVIF_BUILD_APPS=ON \ - -DAVIF_BUILD_EXAMPLES=OFF \ - -DAVIF_BUILD_TESTS=OFF \ - -DAVIF_CODEC_AOM=LOCAL \ - -DAVIF_CODEC_DAV1D=LOCAL \ - -DAVIF_CODEC_AVM=LOCAL \ - -DAVIF_LIBYUV=LOCAL \ - -DAVIF_LIBSHARPYUV=LOCAL \ - -DAVIF_ENABLE_EXPERIMENTAL_MINI=ON \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ - -DBUILD_SHARED_LIBS=ON \ - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 - cmake --build build -j${NPROC} - popd - - git clone -b v1.21.2 --depth 1 https://github.com/strukturag/libheif.git - pushd libheif - git checkout 62f1b8c76ed4d8305071fdacbe74ef9717bacac5 # v1.21.2 - # Reuse the libaom and dav1d dependencies from libavif. - # pushd third-party - # chmod +x *.cmd - # ./aom.cmd - # ./dav1d.cmd - # popd - cmake -S . -B build \ - -DBUILD_TESTING=OFF \ - -DENABLE_PLUGIN_LOADING=OFF \ - -DWITH_AOM_DECODER=OFF \ - -DWITH_AOM_ENCODER=ON \ - -DAOM_INCLUDE_DIR=../libavif/build/_deps/libaom-src/ \ - -DAOM_LIBRARY=../libavif/build/_deps/libaom-build/libaom.a \ - -DWITH_DAV1D=ON \ - -DDAV1D_INCLUDE_DIR=../libavif/build/_deps/dav1d-src/include/ \ - -DDAV1D_LIBRARY=../libavif/build/_deps/dav1d-build/src/libdav1d.a \ - -DWITH_LIBSHARPYUV=OFF \ - -DENABLE_MULTITHREADING_SUPPORT=OFF \ - -DENABLE_PARALLEL_TILE_DECODING=OFF \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ - -DBUILD_SHARED_LIBS=ON - cmake --build build -j${NPROC} - popd - - git clone -b v1.6.0 --depth 1 https://chromium.googlesource.com/webm/libwebp - pushd libwebp - git checkout b7e29b9d75bd31422b00c2a446d49d7af06c328d # v1.6.0 - cmake -S . -B build \ - -DWEBP_BUILD_CWEBP=ON \ - -DWEBP_BUILD_DWEBP=ON \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ - -DBUILD_SHARED_LIBS=ON - cmake --build build -j${NPROC} - - # This file creates errors when referenced by CMAKE_PREFIX_PATH below. - mv build/WebPConfig.cmake build/WebPConfig.cmake.bck - popd - - git clone https://chromium.googlesource.com/codecs/libwebp2 - pushd libwebp2 - git checkout 8720150cdc4c5c51a11a809a93110f38035b6048 - cmake -S . -B build \ - -DCMAKE_PREFIX_PATH="../libwebp/src/;../libwebp/build/" \ - -DWP2_BUILD_TESTS=OFF \ - -DWP2_BUILD_EXTRAS=OFF \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ - -DBUILD_SHARED_LIBS=ON - cmake --build build -j${NPROC} - popd - - git clone -b v0.11.2 --depth 1 https://github.com/libjxl/libjxl.git - pushd libjxl - git checkout 332feb17d17311c748445f7ee75c4fb55cc38530 # v0.11.2 - ./deps.sh - # DEVTOOLS=ON for Butteraugli and SSIMULACRA2 metric binaries. See - # https://github.com/cloudinary/ssimulacra2/blob/d2be72505ddc5c92aeb30f4a7f3ab53db45b314b/build_ssimulacra_from_libjxl_repo - cmake -S . -B build \ - -DBUILD_TESTING=OFF \ - -DJPEGXL_ENABLE_BENCHMARK=OFF \ - -DJPEGXL_ENABLE_EXAMPLES=OFF \ - -DJPEGXL_ENABLE_JPEGLI=OFF \ - -DJPEGXL_ENABLE_OPENEXR=OFF \ - -DJPEGXL_ENABLE_DEVTOOLS=ON \ - -DJPEGXL_ENABLE_JPEGLI=ON -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=OFF \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ - -DBUILD_SHARED_LIBS=ON - cmake --build build -j${NPROC} - popd - - git clone -b v2.5.4 --depth 1 https://github.com/uclouvain/openjpeg.git - pushd openjpeg - git checkout 6c4a29b00211eb0430fa0e5e890f1ce5c80f409f # v2.5.4 - cmake -S . -B build \ - -DBUILD_TESTING=OFF \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_C_COMPILER=clang \ - -DBUILD_SHARED_LIBS=ON -DBUILD_STATIC_LIBS=OFF - cmake --build build -j${NPROC} - popd - - # FFV1 is part of FFmpeg. - git clone -b n8.1 --depth 1 https://github.com/FFmpeg/FFmpeg.git - pushd FFmpeg - git checkout 9047fa1b084f76b1b4d065af2d743df1b40dfb56 # n8.1 - ./configure --prefix=build --enable-shared --disable-static - make libavcodec -j${NPROC} - make install libavcodec -j${NPROC} - popd - - git clone -b v2_1_0 --depth 1 https://github.com/BinomialLLC/basis_universal.git - pushd basis_universal - git checkout 45d5f41015eecd9570d5a3f89ab9cc0037a25063 # v2.10 - cmake -S . -B build \ - -DCMAKE_BUILD_TYPE=Release -DBASISU_SSE=TRUE \ - -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ - -DBUILD_SHARED_LIBS=ON -DBASISU_STATIC=OFF - cmake --build build -j${NPROC} - popd - - git clone https://github.com/kornelski/dssim.git - pushd dssim - git checkout c86745c423478993a12edf59ec76047ff52b3da4 # 3.4.0 - cargo build --release - popd - - git clone -b 3.1.3 --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo.git libjpeg_turbo - pushd libjpeg_turbo - git checkout af9c1c268520a29adf98cad5138dafe612b3d318 # 3.1.3 - cmake -S . -B build - cmake --build build -j${NPROC} - popd - - git clone https://github.com/webmproject/sjpeg.git - pushd sjpeg - git checkout 46da5aec5fce05faabf1facf0066e36e6b1c4dff - cmake -S . -B build \ - -DSJPEG_BUILD_EXAMPLES=OFF \ - -DBUILD_SHARED_LIBS=ON - cmake --build build -j${NPROC} - popd - - git clone https://github.com/mozilla/mozjpeg.git - pushd mozjpeg - git checkout 08265790774cd0714832c9e675522acbe5581437 # 5.0.X - cmake -S . -B build -DWITH_TURBOJPEG=OFF \ - -DBUILD_SHARED_LIBS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON - cmake --build build -j${NPROC} - popd - -popd diff --git a/src/codec.cc b/src/codec.cc index baac08c..7e2c984 100644 --- a/src/codec.cc +++ b/src/codec.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -46,7 +47,8 @@ #include "src/timer.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "imageio/anim_image_dec.h" +#include "src/wp2/base.h" #endif // HAS_WEBP2 namespace codec_compare_gen { @@ -220,10 +222,16 @@ std::vector CodecLossyQualities(Codec codec) { return JpegXLLossyQualities(); case Codec::kAvif: case Codec::kAvifSsim: - case Codec::kAvifIq: case Codec::kAvifExp: case Codec::kAvifAvm: return AvifLossyQualities(); + case Codec::kAvifIq: { + std::vector qualities = AvifLossyQualities(); + // Remove the lossless quality because it is rejected with tune=iq. + assert(qualities.back() == 100); + qualities.pop_back(); + return qualities; + } case Codec::kAvifLibheif: return AvifLibheifLossyQualities(); case Codec::kCombination: @@ -434,6 +442,54 @@ StatusOr> DecodeAvifAvm(const TaskInput& input, return DecodeAvif(input, encoded_image, /*avm=*/true, quiet); } +typedef StatusOr (*EncodeFunc)(const TaskInput&, const Image&, + bool quiet); +typedef StatusOr> (*DecodeFunc)(const TaskInput&, + const WP2::Data&, + bool quiet); + +EncodeFunc GetEncodeFunc(Codec codec) { + return codec == Codec::kWebp ? &EncodeWebp + : codec == Codec::kWebp2 ? &EncodeWebp2 + : codec == Codec::kJpegXl ? &EncodeJxl + : codec == Codec::kAvif ? &EncodeAvifRegular + : codec == Codec::kAvifSsim ? &EncodeAvifSsim + : codec == Codec::kAvifIq ? &EncodeAvifIq + : codec == Codec::kAvifExp ? &EncodeAvifExp + : codec == Codec::kAvifAvm ? &EncodeAvifAvm + : codec == Codec::kAvifLibheif ? &EncodeAvifLibheif + : codec == Codec::kCombination ? &EncodeCodecCombination + : codec == Codec::kJpegturbo ? &EncodeJpegturbo + : codec == Codec::kJpegli ? &EncodeJpegli + : codec == Codec::kJpegsimple ? &EncodeJpegsimple + : codec == Codec::kJpegmoz ? &EncodeJpegmoz + : codec == Codec::kJp2 ? &EncodeOpenjpeg + : codec == Codec::kFfv1 ? &EncodeFfv1 + : codec == Codec::kBasis ? &EncodeBasis + : nullptr; +} + +DecodeFunc GetDecodeFunc(Codec codec) { + return codec == Codec::kWebp ? &DecodeWebp + : codec == Codec::kWebp2 ? &DecodeWebp2 + : codec == Codec::kJpegXl ? &DecodeJxl + : codec == Codec::kAvif ? &DecodeAvifRegularOrExp + : codec == Codec::kAvifSsim ? &DecodeAvifRegularOrExp + : codec == Codec::kAvifIq ? &DecodeAvifRegularOrExp + : codec == Codec::kAvifExp ? &DecodeAvifRegularOrExp + : codec == Codec::kAvifAvm ? &DecodeAvifAvm + : codec == Codec::kAvifLibheif ? &DecodeAvifLibheif + : codec == Codec::kCombination ? &DecodeCodecCombination + : codec == Codec::kJpegturbo ? &DecodeJpegturbo + : codec == Codec::kJpegli ? &DecodeJpegli + : codec == Codec::kJpegsimple ? &DecodeJpegsimple + : codec == Codec::kJpegmoz ? &DecodeJpegmoz + : codec == Codec::kJp2 ? &DecodeOpenjpeg + : codec == Codec::kFfv1 ? &DecodeFfv1 + : codec == Codec::kBasis ? &DecodeBasis + : nullptr; +} + } // namespace StatusOr EncodeDecode(const TaskInput& input, @@ -475,47 +531,6 @@ StatusOr EncodeDecode(const TaskInput& input, WP2Formatbpc(original_image.front().pixels.format())), quiet); - auto encode_func = - input.codec_settings.codec == Codec::kWebp ? &EncodeWebp - : input.codec_settings.codec == Codec::kWebp2 ? &EncodeWebp2 - : input.codec_settings.codec == Codec::kJpegXl ? &EncodeJxl - : input.codec_settings.codec == Codec::kAvif ? &EncodeAvifRegular - : input.codec_settings.codec == Codec::kAvifSsim ? &EncodeAvifSsim - : input.codec_settings.codec == Codec::kAvifIq ? &EncodeAvifIq - : input.codec_settings.codec == Codec::kAvifExp ? &EncodeAvifExp - : input.codec_settings.codec == Codec::kAvifAvm ? &EncodeAvifAvm - : input.codec_settings.codec == Codec::kAvifLibheif ? &EncodeAvifLibheif - : input.codec_settings.codec == Codec::kCombination - ? &EncodeCodecCombination - : input.codec_settings.codec == Codec::kJpegturbo ? &EncodeJpegturbo - : input.codec_settings.codec == Codec::kJpegli ? &EncodeJpegli - : input.codec_settings.codec == Codec::kJpegsimple ? &EncodeJpegsimple - : input.codec_settings.codec == Codec::kJpegmoz ? &EncodeJpegmoz - : input.codec_settings.codec == Codec::kJp2 ? &EncodeOpenjpeg - : input.codec_settings.codec == Codec::kFfv1 ? &EncodeFfv1 - : input.codec_settings.codec == Codec::kBasis ? &EncodeBasis - : nullptr; - auto decode_func = - input.codec_settings.codec == Codec::kWebp ? &DecodeWebp - : input.codec_settings.codec == Codec::kWebp2 ? &DecodeWebp2 - : input.codec_settings.codec == Codec::kJpegXl ? &DecodeJxl - : input.codec_settings.codec == Codec::kAvif ? &DecodeAvifRegularOrExp - : input.codec_settings.codec == Codec::kAvifSsim ? &DecodeAvifRegularOrExp - : input.codec_settings.codec == Codec::kAvifIq ? &DecodeAvifRegularOrExp - : input.codec_settings.codec == Codec::kAvifExp ? &DecodeAvifRegularOrExp - : input.codec_settings.codec == Codec::kAvifAvm ? &DecodeAvifAvm - : input.codec_settings.codec == Codec::kAvifLibheif ? &DecodeAvifLibheif - : input.codec_settings.codec == Codec::kCombination - ? &DecodeCodecCombination - : input.codec_settings.codec == Codec::kJpegturbo ? &DecodeJpegturbo - : input.codec_settings.codec == Codec::kJpegli ? &DecodeJpegli - : input.codec_settings.codec == Codec::kJpegsimple ? &DecodeJpegsimple - : input.codec_settings.codec == Codec::kJpegmoz ? &DecodeJpegmoz - : input.codec_settings.codec == Codec::kJp2 ? &DecodeOpenjpeg - : input.codec_settings.codec == Codec::kFfv1 ? &DecodeFfv1 - : input.codec_settings.codec == Codec::kBasis ? &DecodeBasis - : nullptr; - const Timer encoding_duration; WP2::Data encoded_image; if (encode_mode == EncodeMode::kLoadFromDisk) { @@ -528,6 +543,7 @@ StatusOr EncodeDecode(const TaskInput& input, file.read(reinterpret_cast(encoded_image.bytes), static_cast(length)); } else { + const EncodeFunc encode_func = GetEncodeFunc(input.codec_settings.codec); CHECK_OR_RETURN(encode_func != nullptr, quiet); ASSIGN_OR_RETURN(encoded_image, encode_func(input, original_image, quiet)); } @@ -540,6 +556,7 @@ StatusOr EncodeDecode(const TaskInput& input, const Timer decoding_duration; Image decoded_image; + const DecodeFunc decode_func = GetDecodeFunc(input.codec_settings.codec); CHECK_OR_RETURN(decode_func != nullptr, quiet); { ASSIGN_OR_RETURN(auto image_and_color_conversion_duration, @@ -597,10 +614,73 @@ StatusOr EncodeDecode(const TaskInput& input, return task; } +StatusOr> Encode(const uint8_t* argb, uint32_t width, + uint32_t height, Codec codec, + Subsampling chroma_subsampling, + int effort, int quality, bool quiet) { + TaskInput input; + input.codec_settings = {codec, chroma_subsampling, effort, quality}; + + WP2::ArgbBuffer buffer(WP2_ARGB_32); + OK_WP2_OR_RETURN(buffer.Import(WP2_ARGB_32, width, height, argb, width * 4), + quiet); + + Image original_image; + original_image.emplace_back(std::move(buffer), /*duration_ms=*/0); + + const bool has_transparency = original_image.front().pixels.HasTransparency(); + const WP2SampleFormat needed_format = + CodecToNeededFormat(codec, has_transparency); + if (original_image.front().pixels.format() != needed_format) { + ASSIGN_OR_RETURN(original_image, + CloneAs(original_image, needed_format, quiet)); + } + + const EncodeFunc encode_func = GetEncodeFunc(input.codec_settings.codec); + CHECK_OR_RETURN(encode_func != nullptr, quiet); + ASSIGN_OR_RETURN(WP2::Data encoded_image, + encode_func(input, original_image, quiet)); + + return std::vector(encoded_image.bytes, + encoded_image.bytes + encoded_image.size); +} + +StatusOr> DecodeToArgb(const uint8_t* encoded_image, + size_t encoded_size, + uint32_t* width, uint32_t* height, + bool quiet) { + WP2::ArgbBuffer buffer(WP2_ARGB_32); + WP2::ImageReader reader(encoded_image, encoded_size, &buffer); + bool is_last; + uint32_t duration_ms; + OK_WP2_OR_RETURN(reader.ReadFrame(&is_last, &duration_ms), quiet); + + *width = buffer.width(); + *height = buffer.height(); + std::vector output(buffer.width() * buffer.height() * 4); + for (uint32_t y = 0; y < buffer.height(); ++y) { + std::memcpy(&output[y * buffer.width() * 4], buffer.GetRow8(y), + buffer.width() * 4); + } + return output; +} + #else -StatusOr EncodeDecode(const TaskInput&, bool quiet) { - CHECK_OR_RETURN(false, quiet) << "Reading images requires HAS_WEBP2"; +StatusOr EncodeDecode(const TaskInput&, const std::string&, size_t, + EncodeMode, bool quiet) { + CHECK_OR_RETURN(false, quiet) + << "Encoding/decoding images requires HAS_WEBP2"; +} + +StatusOr> Encode(const uint8_t*, uint32_t, uint32_t, Codec, + Subsampling, int, int, bool quiet) { + CHECK_OR_RETURN(false, quiet) << "Encoding images requires HAS_WEBP2"; +} + +StatusOr> DecodeToArgb(const uint8_t*, size_t, uint32_t*, + uint32_t*, bool quiet) { + CHECK_OR_RETURN(false, quiet) << "Decoding images requires HAS_WEBP2"; } #endif // HAS_WEBP2 diff --git a/src/codec.h b/src/codec.h index 6283a99..29d3f58 100644 --- a/src/codec.h +++ b/src/codec.h @@ -16,6 +16,7 @@ #define SRC_CODEC_H_ #include +#include #include #include @@ -40,6 +41,15 @@ StatusOr EncodeDecode(const TaskInput& input, size_t thread_id, EncodeMode encode_mode, bool quiet); +StatusOr> Encode(const uint8_t* argb, uint32_t width, + uint32_t height, Codec codec, + Subsampling chroma_subsampling, + int effort, int quality, bool quiet); +StatusOr> DecodeToArgb(const uint8_t* encoded_image, + size_t encoded_size, + uint32_t* width, uint32_t* height, + bool quiet); + } // namespace codec_compare_gen #endif // SRC_CODEC_H_ diff --git a/src/codec_avif.cc b/src/codec_avif.cc index 5a3762b..3836919 100644 --- a/src/codec_avif.cc +++ b/src/codec_avif.cc @@ -14,6 +14,7 @@ #include "src/codec_avif.h" +#include #include #include #include @@ -27,7 +28,7 @@ #include "src/timer.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_AVIF) @@ -54,6 +55,7 @@ std::vector AvifLossyQualities() { // quantizer = ((100 - quality) * 63 + 50) / 100; qualities[i] = ((63 - i) * 100 + 63 / 2) / 63; } + std::reverse(qualities.begin(), qualities.end()); return qualities; // [0:63] (63 is lossless but in YUV so RGB is lossy). } @@ -246,8 +248,8 @@ StatusOr> DecodeAvif(const TaskInput& input, } #else -StatusOr EncodeAvif(const TaskInput&, const Image&, bool, bool, bool, - bool quiet) { +StatusOr EncodeAvif(const TaskInput&, const Image&, bool, bool, + const char*, bool, bool quiet) { CHECK_OR_RETURN(false, quiet) << "Encoding images requires HAS_AVIF"; } StatusOr> DecodeAvif(const TaskInput&, diff --git a/src/codec_avif.h b/src/codec_avif.h index 4de3815..45dd075 100644 --- a/src/codec_avif.h +++ b/src/codec_avif.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_avif_libheif.cc b/src/codec_avif_libheif.cc index 72cb8ce..db46688 100644 --- a/src/codec_avif_libheif.cc +++ b/src/codec_avif_libheif.cc @@ -33,7 +33,7 @@ #include "src/timer.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_HEIF) diff --git a/src/codec_avif_libheif.h b/src/codec_avif_libheif.h index 86a0691..cfa1d9c 100644 --- a/src/codec_avif_libheif.h +++ b/src/codec_avif_libheif.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_basis.cc b/src/codec_basis.cc index 6cc3fdb..09de2b3 100644 --- a/src/codec_basis.cc +++ b/src/codec_basis.cc @@ -26,14 +26,14 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_BASIS) -#include "third_party/basis_universal/encoder/basisu_comp.h" -#include "third_party/basis_universal/encoder/basisu_enc.h" -#include "third_party/basis_universal/transcoder/basisu.h" -#include "third_party/basis_universal/transcoder/basisu_transcoder.h" +#include "encoder/basisu_comp.h" +#include "encoder/basisu_enc.h" +#include "transcoder/basisu.h" +#include "transcoder/basisu_transcoder.h" #endif namespace codec_compare_gen { @@ -62,9 +62,15 @@ BasisContext::~BasisContext() { } std::vector BasisLossyQualities() { - std::vector qualities(basisu::BASISU_QUALITY_MAX - - basisu::BASISU_QUALITY_MIN + 1); - std::iota(qualities.begin(), qualities.end(), basisu::BASISU_QUALITY_MIN); +#if defined(HAS_BASIS) + const uint32_t kBasisQualityMin = basisu::BASISU_QUALITY_MIN; + const uint32_t kBasisQualityMax = basisu::BASISU_QUALITY_MAX; +#else + const uint32_t kBasisQualityMin = 1; + const uint32_t kBasisQualityMax = 255; +#endif + std::vector qualities(kBasisQualityMax - kBasisQualityMin + 1); + std::iota(qualities.begin(), qualities.end(), kBasisQualityMin); return qualities; } @@ -100,6 +106,7 @@ StatusOr EncodeBasis(const TaskInput& input, params.m_quality_level = input.codec_settings.quality; params.m_mip_gen = false; params.m_multithreading = false; + params.m_status_output = false; // There must be at least one thread on top of the calling thread apparently. basisu::job_pool job_pool(/*num_threads=*/1); diff --git a/src/codec_basis.h b/src/codec_basis.h index 4be1c78..fc42f9c 100644 --- a/src/codec_basis.h +++ b/src/codec_basis.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_combination.cc b/src/codec_combination.cc index d1f3ac5..d412957 100644 --- a/src/codec_combination.cc +++ b/src/codec_combination.cc @@ -30,7 +30,7 @@ #include "src/timer.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_combination.h b/src/codec_combination.h index 013acfc..cf2aa2c 100644 --- a/src/codec_combination.h +++ b/src/codec_combination.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_ffv1.cc b/src/codec_ffv1.cc index 71766b3..818fea6 100644 --- a/src/codec_ffv1.cc +++ b/src/codec_ffv1.cc @@ -27,12 +27,12 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_FFV1) extern "C" { -#include "third_party/FFmpeg/libavcodec/avcodec.h" +#include "libavcodec/avcodec.h" } #endif @@ -127,6 +127,7 @@ StatusOr EncodeFfv1(const TaskInput& input, ffv1.context->time_base = {1, 25}; ffv1.context->framerate = {25, 1}; ffv1.context->thread_count = 1; + ffv1.context->slices = 1; // TODO(yguyon): Support 16-bit. CHECK_OR_RETURN(WP2Formatbpc(pixels.format()) == 8, quiet); ffv1.context->pix_fmt = diff --git a/src/codec_ffv1.h b/src/codec_ffv1.h index 58ddbcf..5a5e37b 100644 --- a/src/codec_ffv1.h +++ b/src/codec_ffv1.h @@ -23,7 +23,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_jpegli.cc b/src/codec_jpegli.cc index dd71ba6..84355ba 100644 --- a/src/codec_jpegli.cc +++ b/src/codec_jpegli.cc @@ -31,16 +31,16 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_JPEGTURBO) -#include "third_party/libjpeg_turbo/src/jpeglib.h" +#include "src/jpeglib.h" #endif -#if defined(HAS_JPEGXL) -#include "third_party/libjxl/lib/jpegli/common.h" -#include "third_party/libjxl/lib/jpegli/encode.h" +#if defined(HAS_JPEGLI) +#include "lib/jpegli/common.h" +#include "lib/jpegli/encode.h" #endif namespace codec_compare_gen { @@ -57,7 +57,7 @@ std::vector JpegliLossyQualities() { #if defined(HAS_WEBP2) -#if defined(HAS_JPEGXL) && defined(HAS_JPEGTURBO) +#if defined(HAS_JPEGLI) && defined(HAS_JPEGTURBO) namespace { @@ -165,14 +165,14 @@ StatusOr> DecodeJpegli(const TaskInput& input, #else StatusOr EncodeJpegli(const TaskInput&, const Image&, bool quiet) { CHECK_OR_RETURN(false, quiet) - << "Encoding images requires HAS_JPEGXL and HAS_JPEGTURBO"; + << "Encoding images requires HAS_JPEGLI and HAS_JPEGTURBO"; } StatusOr> DecodeJpegli(const TaskInput&, const WP2::Data&, bool quiet) { CHECK_OR_RETURN(false, quiet) - << "Decoding images requires HAS_JPEGXL and HAS_JPEGTURBO"; + << "Decoding images requires HAS_JPEGLI and HAS_JPEGTURBO"; } -#endif // HAS_JPEGXL && HAS_JPEGTURBO +#endif // HAS_JPEGLI && HAS_JPEGTURBO #endif // HAS_WEBP2 diff --git a/src/codec_jpegli.h b/src/codec_jpegli.h index ae51887..6a9e035 100644 --- a/src/codec_jpegli.h +++ b/src/codec_jpegli.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_jpegmoz.cc b/src/codec_jpegmoz.cc index d133f38..9fd3b49 100644 --- a/src/codec_jpegmoz.cc +++ b/src/codec_jpegmoz.cc @@ -32,11 +32,11 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_JPEGMOZ) -#include "third_party/mozjpeg/jpeglib.h" +#include "jpeglib.h" #endif namespace codec_compare_gen { @@ -45,8 +45,8 @@ std::string JpegmozVersion() { #if defined(HAS_JPEGMOZ) // JPEG_LIB_VERSION in jconfig.h seems to be the version of the turbojpeg // library MozJPEG is based on. I could not find an API for MozJPEG version. - // Hardcode the version tied to the GitHub commit used in deps.sh. - return "4.1.5"; // 6c9f0897afa1c2738d7222a0a9ab49e8b536a267 + // Hardcode the version tied to the GitHub commit used in CMakeLists.txt. + return "5.0.X"; // 08265790774cd0714832c9e675522acbe5581437 #else return "n/a"; #endif diff --git a/src/codec_jpegmoz.h b/src/codec_jpegmoz.h index 032da25..5922e2a 100644 --- a/src/codec_jpegmoz.h +++ b/src/codec_jpegmoz.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_jpegsimple.cc b/src/codec_jpegsimple.cc index 02afb36..fe231ad 100644 --- a/src/codec_jpegsimple.cc +++ b/src/codec_jpegsimple.cc @@ -28,11 +28,11 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_JPEGSIMPLE) -#include "third_party/sjpeg/src/sjpeg.h" +#include "src/sjpeg.h" #endif namespace codec_compare_gen { diff --git a/src/codec_jpegsimple.h b/src/codec_jpegsimple.h index fd4fd49..4759578 100644 --- a/src/codec_jpegsimple.h +++ b/src/codec_jpegsimple.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_jpegturbo.cc b/src/codec_jpegturbo.cc index 96528ce..f2de10b 100644 --- a/src/codec_jpegturbo.cc +++ b/src/codec_jpegturbo.cc @@ -26,7 +26,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_JPEGTURBO) diff --git a/src/codec_jpegturbo.h b/src/codec_jpegturbo.h index a989be1..07359e9 100644 --- a/src/codec_jpegturbo.h +++ b/src/codec_jpegturbo.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_jpegxl.cc b/src/codec_jpegxl.cc index a9c175a..91acbed 100644 --- a/src/codec_jpegxl.cc +++ b/src/codec_jpegxl.cc @@ -28,17 +28,17 @@ #include "src/timer.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_JPEGXL) -#include "third_party/libjxl/lib/include/jxl/codestream_header.h" -#include "third_party/libjxl/lib/include/jxl/color_encoding.h" -#include "third_party/libjxl/lib/include/jxl/decode.h" -#include "third_party/libjxl/lib/include/jxl/decode_cxx.h" -#include "third_party/libjxl/lib/include/jxl/encode.h" -#include "third_party/libjxl/lib/include/jxl/encode_cxx.h" -#include "third_party/libjxl/lib/include/jxl/types.h" +#include "lib/include/jxl/codestream_header.h" +#include "lib/include/jxl/color_encoding.h" +#include "lib/include/jxl/decode.h" +#include "lib/include/jxl/decode_cxx.h" +#include "lib/include/jxl/encode.h" +#include "lib/include/jxl/encode_cxx.h" +#include "lib/include/jxl/types.h" #endif namespace codec_compare_gen { diff --git a/src/codec_jpegxl.h b/src/codec_jpegxl.h index 5c46a2f..6c48486 100644 --- a/src/codec_jpegxl.h +++ b/src/codec_jpegxl.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_openjpeg.cc b/src/codec_openjpeg.cc index 212ef6d..2ed1b64 100644 --- a/src/codec_openjpeg.cc +++ b/src/codec_openjpeg.cc @@ -36,7 +36,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_OPENJPEG) diff --git a/src/codec_openjpeg.h b/src/codec_openjpeg.h index bcef7d0..9796e5a 100644 --- a/src/codec_openjpeg.h +++ b/src/codec_openjpeg.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_webp.cc b/src/codec_webp.cc index 0377caf..b2101d6 100644 --- a/src/codec_webp.cc +++ b/src/codec_webp.cc @@ -27,17 +27,17 @@ #include "src/frame.h" #include "src/task.h" #include "src/timer.h" -#include "third_party/libwebp/src/webp/mux_types.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif #if defined(HAS_WEBP) -#include "third_party/libwebp/src/webp/decode.h" -#include "third_party/libwebp/src/webp/demux.h" -#include "third_party/libwebp/src/webp/encode.h" -#include "third_party/libwebp/src/webp/mux.h" +#include "src/webp/decode.h" +#include "src/webp/demux.h" +#include "src/webp/encode.h" +#include "src/webp/mux.h" +#include "src/webp/mux_types.h" #endif namespace codec_compare_gen { diff --git a/src/codec_webp.h b/src/codec_webp.h index dc89359..6a11cb5 100644 --- a/src/codec_webp.h +++ b/src/codec_webp.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/codec_webp2.cc b/src/codec_webp2.cc index 76714bf..2fb786c 100644 --- a/src/codec_webp2.cc +++ b/src/codec_webp2.cc @@ -28,10 +28,10 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" -#include "third_party/libwebp2/src/wp2/decode.h" -#include "third_party/libwebp2/src/wp2/encode.h" -#include "third_party/libwebp2/src/wp2/format_constants.h" +#include "src/wp2/base.h" +#include "src/wp2/decode.h" +#include "src/wp2/encode.h" +#include "src/wp2/format_constants.h" #endif // HAS_WEBP2 namespace codec_compare_gen { diff --git a/src/codec_webp2.h b/src/codec_webp2.h index e909bae..3b25690 100644 --- a/src/codec_webp2.h +++ b/src/codec_webp2.h @@ -24,7 +24,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/distortion.cc b/src/distortion.cc index 9a5e176..26e3a89 100644 --- a/src/distortion.cc +++ b/src/distortion.cc @@ -37,8 +37,8 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/imageio/image_enc.h" -#include "third_party/libwebp2/src/wp2/base.h" +#include "imageio/image_enc.h" +#include "src/wp2/base.h" #endif // HAS_WEBP2 namespace codec_compare_gen { @@ -154,9 +154,9 @@ Status ScaleTill8x8(const WP2::ArgbBuffer& from, WP2::ArgbBuffer& to, OK_WP2_OR_RETURN( to.Resize(std::max(from.width(), 8u), std::max(from.height(), 8u)), quiet); - for (uint8_t to_y = 0; to_y < to.height(); ++to_y) { + for (uint32_t to_y = 0; to_y < to.height(); ++to_y) { const uint32_t from_y = to_y * (from.height() - 1) / (to.height() - 1); - for (uint8_t to_x = 0; to_x < to.width(); ++to_x) { + for (uint32_t to_x = 0; to_x < to.width(); ++to_x) { const uint32_t from_x = to_x * (from.width() - 1) / (to.width() - 1); if (WP2Formatbpc(to.format()) == 8) { to.GetRow8(to_y)[to_x] = from.GetRow8(from_y)[from_x]; @@ -192,7 +192,8 @@ StatusOr GetBinaryDistortion( WP2::ArgbBuffer final_reference(reference.format()); WP2::ArgbBuffer final_image(image.format()); - if (metric_supports_tiny_dimensions) { + if (metric_supports_tiny_dimensions || + (reference.width() >= 8 && reference.height() >= 8)) { OK_WP2_OR_RETURN(final_reference.SetView(reference), quiet); OK_WP2_OR_RETURN(final_image.SetView(image), quiet); } else { @@ -250,8 +251,8 @@ StatusOr GetLibjxlDistortion( metric_supports_tiny_dimensions = false; } const std::string metric_binary_path = - std::filesystem::path(metric_binary_folder_path) / "libjxl" / "build" / - "tools" / metric_binary_name; + std::filesystem::path(metric_binary_folder_path) / "_deps" / + "libjxl-build" / "tools" / metric_binary_name; ASSIGN_OR_RETURN( std::string standard_output, GetBinaryDistortion(reference_path, reference, image_path, image, task, @@ -284,8 +285,8 @@ StatusOr GetDssimDistortion(const std::string& reference_path, if (metric_binary_folder_path.empty()) return -1; const std::string metric_binary_path = - std::filesystem::path(metric_binary_folder_path) / "dssim" / "target" / - "release" / "dssim"; + std::filesystem::path(metric_binary_folder_path) / "_deps" / "dssim-src" / + "target" / "release" / "dssim"; const bool metric_supports_tiny_dimensions = true; ASSIGN_OR_RETURN( const std::string standard_output, diff --git a/src/distortion.h b/src/distortion.h index 4a4529b..650dbf5 100644 --- a/src/distortion.h +++ b/src/distortion.h @@ -23,7 +23,7 @@ #include "src/task.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/frame.cc b/src/frame.cc index 05ba27e..156a32a 100644 --- a/src/frame.cc +++ b/src/frame.cc @@ -21,13 +21,13 @@ #include #include +#include "imageio/anim_image_dec.h" +#include "imageio/image_enc.h" #include "src/base.h" #include "src/codec_webp.h" #include "src/distortion.h" #include "src/task.h" -#include "third_party/libwebp2/imageio/anim_image_dec.h" -#include "third_party/libwebp2/imageio/image_enc.h" -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif // HAS_WEBP2 namespace codec_compare_gen { diff --git a/src/frame.h b/src/frame.h index 72348f7..c75c5c3 100644 --- a/src/frame.h +++ b/src/frame.h @@ -22,7 +22,7 @@ #include "src/base.h" #if defined(HAS_WEBP2) -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" #endif namespace codec_compare_gen { diff --git a/src/result_json.cc b/src/result_json.cc index bd9af69..94e76e3 100644 --- a/src/result_json.cc +++ b/src/result_json.cc @@ -134,12 +134,34 @@ Status TasksToJson(const std::string& batch_pretty_name, CodecSettings settings, const std::string encoded_parent = AppendDirectorySeparator(RemovePrefix( /*prefix=*/encoded_common_parent.parent_path(), encoded_common_parent)); + const std::string build_option = + settings.codec == Codec::kWebp ? " -DCCGEN_ENABLE_WEBP=ON" + : settings.codec == Codec::kWebp2 ? " -DCCGEN_ENABLE_WEBP2=ON" + : settings.codec == Codec::kJpegXl || settings.codec == Codec::kJpegli + ? " -DCCGEN_ENABLE_JPEGXL=ON" + : settings.codec == Codec::kAvif || settings.codec == Codec::kAvifSsim || + settings.codec == Codec::kAvifIq || + settings.codec == Codec::kAvifExp || + settings.codec == Codec::kAvifAvm || + settings.codec == Codec::kAvifLibheif + ? " -DCCGEN_ENABLE_AVIF=ON" + : settings.codec == Codec::kCombination ? "" + : settings.codec == Codec::kJpegturbo || + settings.codec == Codec::kJpegsimple || + settings.codec == Codec::kJpegmoz + ? " -DCCGEN_ENABLE_JPEG=ON" + : settings.codec == Codec::kJp2 ? " -DCCGEN_ENABLE_JPEG2000=ON" + : settings.codec == Codec::kFfv1 ? " -DCCGEN_ENABLE_FFV1=ON" + : settings.codec == Codec::kBasis ? " -DCCGEN_ENABLE_BASIS=ON" + : ""; + const std::string build_cmd = - "git clone -b v0.6.7 --depth 1" - " https://github.com/webmproject/codec-compare-gen.git" - " && cd codec-compare-gen && ./deps.sh" - " && cmake -S . -B build -DCMAKE_CXX_COMPILER=clang++" - " && cmake --build build --parallel && cd .."; + "git clone -b v0.7.0 --depth 1" + " https://github.com/webmproject/codec-compare-gen.git ccgen" + " && cmake -S ccgen -B ccgen/build" + + build_option + + " -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++" + " && cmake --build ccgen/build --parallel"; const std::string effort_str = (settings.codec == Codec::kWebp || settings.codec == Codec::kWebp2 || settings.codec == Codec::kJpegXl || settings.codec == Codec::kAvif || @@ -150,13 +172,13 @@ Status TasksToJson(const std::string& batch_pretty_name, CodecSettings settings, ? " " + std::to_string(settings.effort) : ""; // kJpegturbo, kJpegli, and kJpegmoz have no effort setting. std::string encoding_cmd = - "codec-compare-gen/build/ccgen --codec " + CodecName(settings.codec) + - " " + SubsamplingToString(settings.chroma_subsampling) + effort_str; + "ccgen/build/ccgen --codec " + CodecName(settings.codec) + " " + + SubsamplingToString(settings.chroma_subsampling) + effort_str; if (settings.quality == kQualityLossless) { encoding_cmd += " --lossless"; } else { encoding_cmd += " --lossy --quality ${quality}"; - encoding_cmd += " --metric_binary_folder codec-compare-gen/third_party/"; + encoding_cmd += " --metric_binary_folder ccgen/build/"; } encoding_cmd += " -- ${original_name}"; diff --git a/tests/test_codec_avif.cc b/tests/test_codec_avif.cc index b975f10..a762cd4 100644 --- a/tests/test_codec_avif.cc +++ b/tests/test_codec_avif.cc @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include #include @@ -38,6 +39,7 @@ TEST(AvifTest, Qualities) { std::vector expected_quantizers(64); std::iota(expected_quantizers.begin(), expected_quantizers.end(), 0); + std::reverse(expected_quantizers.begin(), expected_quantizers.end()); // Make sure the AVIF quality list maps to the exact range [0:63] without gaps // or duplicates. diff --git a/tests/test_distortion.cc b/tests/test_distortion.cc index 3ee909e..f1d7bd0 100644 --- a/tests/test_distortion.cc +++ b/tests/test_distortion.cc @@ -20,7 +20,7 @@ #include "src/base.h" #include "src/distortion.h" #include "src/frame.h" -#include "third_party/libwebp2/src/wp2/base.h" +#include "src/wp2/base.h" namespace codec_compare_gen { namespace { @@ -28,6 +28,9 @@ namespace { // Used to pass the data folder path to the GoogleTest suites. const char* data_path = nullptr; +// Used to pass the metric binary folder path to the GoogleTest suites. +const char* binary_path = nullptr; + constexpr bool kQuiet = false; constexpr size_t kThreadId = 0; @@ -122,6 +125,71 @@ TEST(DistortionTest, DifferentFrameCount) { EXPECT_GT(distortion.value, 20.0f); } +TEST(DistortionTest, Dssim) { + const std::string gif_path = std::string(data_path) + "anim80x80.gif"; + const StatusOr gif = + ReadStillImageOrAnimation(gif_path.c_str(), WP2_ARGB_32, kQuiet); + ASSERT_EQ(gif.status, Status::kOk); + + const StatusOr distortion = GetAverageDistortion( + gif_path, gif.value, gif_path, gif.value, {}, binary_path, + DistortionMetric::kDssim, kThreadId, kQuiet); + ASSERT_EQ(distortion.status, Status::kOk); + EXPECT_EQ(distortion.value, 0); // Equality. +} + +TEST(DistortionTest, Ssimulacra) { + const std::string gif_path = std::string(data_path) + "anim80x80.gif"; + const StatusOr gif = + ReadStillImageOrAnimation(gif_path.c_str(), WP2_ARGB_32, kQuiet); + ASSERT_EQ(gif.status, Status::kOk); + + const StatusOr distortion = GetAverageDistortion( + gif_path, gif.value, gif_path, gif.value, {}, binary_path, + DistortionMetric::kLibjxlSsimulacra, kThreadId, kQuiet); + ASSERT_EQ(distortion.status, Status::kOk); + EXPECT_EQ(distortion.value, 0); // Equality. +} + +TEST(DistortionTest, Ssimulacra2) { + const std::string gif_path = std::string(data_path) + "anim80x80.gif"; + const StatusOr gif = + ReadStillImageOrAnimation(gif_path.c_str(), WP2_ARGB_32, kQuiet); + ASSERT_EQ(gif.status, Status::kOk); + + const StatusOr distortion = GetAverageDistortion( + gif_path, gif.value, gif_path, gif.value, {}, binary_path, + DistortionMetric::kLibjxlSsimulacra2, kThreadId, kQuiet); + ASSERT_EQ(distortion.status, Status::kOk); + EXPECT_EQ(distortion.value, 100); // Equality. +} + +TEST(DistortionTest, Butteraugli) { + const std::string gif_path = std::string(data_path) + "anim80x80.gif"; + const StatusOr gif = + ReadStillImageOrAnimation(gif_path.c_str(), WP2_ARGB_32, kQuiet); + ASSERT_EQ(gif.status, Status::kOk); + + const StatusOr distortion = GetAverageDistortion( + gif_path, gif.value, gif_path, gif.value, {}, binary_path, + DistortionMetric::kLibjxlButteraugli, kThreadId, kQuiet); + ASSERT_EQ(distortion.status, Status::kOk); + EXPECT_EQ(distortion.value, 0); // Equality. +} + +TEST(DistortionTest, P3norm) { + const std::string gif_path = std::string(data_path) + "anim80x80.gif"; + const StatusOr gif = + ReadStillImageOrAnimation(gif_path.c_str(), WP2_ARGB_32, kQuiet); + ASSERT_EQ(gif.status, Status::kOk); + + const StatusOr distortion = GetAverageDistortion( + gif_path, gif.value, gif_path, gif.value, {}, binary_path, + DistortionMetric::kLibjxlP3norm, kThreadId, kQuiet); + ASSERT_EQ(distortion.status, Status::kOk); + EXPECT_EQ(distortion.value, 0); // Equality. +} + //------------------------------------------------------------------------------ } // namespace @@ -129,12 +197,13 @@ TEST(DistortionTest, DifferentFrameCount) { int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); - if (argc != 2) { - std::cerr << "There must be exactly one argument containing the path to " - "the test data folder" + if (argc != 3) { + std::cerr << "There must be exactly two arguments containing the paths to " + "the test data folder and to the binary folder." << std::endl; return 1; } codec_compare_gen::data_path = argv[1]; + codec_compare_gen::binary_path = argv[2]; return RUN_ALL_TESTS(); } diff --git a/tests/test_encode.cc b/tests/test_encode.cc new file mode 100644 index 0000000..1af4666 --- /dev/null +++ b/tests/test_encode.cc @@ -0,0 +1,80 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "src/base.h" +#include "src/codec.h" + +namespace codec_compare_gen { +namespace { + +// Used to pass the data folder path to the GoogleTest suites. +const char* data_path = nullptr; + +Status EncodeTest(const std::string& image_path, bool quiet = false) { + std::ifstream file(image_path, std::ios::in | std::ios::binary); + CHECK_OR_RETURN(file.good(), quiet); + std::vector bytes((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + uint32_t width, height; + ASSIGN_OR_RETURN( + std::vector argb, + DecodeToArgb(bytes.data(), bytes.size(), &width, &height, quiet)); + ASSIGN_OR_RETURN(std::vector encoded_image, + Encode(argb.data(), width, height, Codec::kWebp, + Subsampling::kDefault, 0, kQualityLossless, quiet)); + uint32_t decoded_width, decoded_height; + ASSIGN_OR_RETURN(std::vector decoded_argb, + DecodeToArgb(encoded_image.data(), encoded_image.size(), + &decoded_width, &decoded_height, quiet)); + CHECK_OR_RETURN(width == decoded_width && height == decoded_height, quiet); + CHECK_OR_RETURN(argb.size() == decoded_argb.size(), quiet); + CHECK_OR_RETURN(argb == decoded_argb, quiet); + return Status::kOk; +} + +//------------------------------------------------------------------------------ + +TEST(EncodeTest, WrongPath) { + EXPECT_EQ(EncodeTest("wrong path", /*quiet=*/true), Status::kUnknownError); +} + +TEST(EncodeTest, WebPMinEffort) { + EXPECT_EQ(EncodeTest(std::string(data_path) + "gradient32x32.png"), + Status::kOk); +} + +//------------------------------------------------------------------------------ + +} // namespace +} // namespace codec_compare_gen + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + if (argc != 2) { + std::cerr << "There must be exactly one argument containing the path to " + "the test data folder" + << std::endl; + return 1; + } + codec_compare_gen::data_path = argv[1]; + return RUN_ALL_TESTS(); +} diff --git a/tools/ccgen_impl.cc b/tools/ccgen_impl.cc index fa9daaf..a9554ef 100644 --- a/tools/ccgen_impl.cc +++ b/tools/ccgen_impl.cc @@ -112,8 +112,7 @@ int Main(int argc, const char* const argv[]) { << (kDefSet.abort_above_fail_ratio * 100) << "%" << std::endl << " [--skip_all_remaining]" << std::endl << " [--quiet]" << std::endl - << " [--metric_binary_folder {path to third_party created by " - "deps.sh}]" + << " [--metric_binary_folder {path to CMake build folder}]" << std::endl << " [--encoded_folder {path}]" << std::endl << " --progress_file {path}" << std::endl @@ -252,6 +251,14 @@ int Main(int argc, const char* const argv[]) { << std::endl; return 1; } + if (lossy && settings.metric_binary_folder_path.empty() && argc >= 1) { + const std::filesystem::path metric_binary_folder_path = + std::filesystem::path(argv[0]).parent_path(); + if (std::filesystem::exists(metric_binary_folder_path / "_deps" / + "dssim-src" / "target" / "release" / "dssim")) { + settings.metric_binary_folder_path = metric_binary_folder_path; + } + } if (lossy && settings.metric_binary_folder_path.empty()) { std::cerr << "Missing --metric_binary_folder for lossy evaluations" << std::endl; diff --git a/wasm/codec_wasm.cc b/wasm/codec_wasm.cc new file mode 100644 index 0000000..1e2e7ef --- /dev/null +++ b/wasm/codec_wasm.cc @@ -0,0 +1,86 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include +#include +#include + +#include "src/base.h" +#include "src/codec.h" + +namespace codec_compare_gen { +struct DecodedImage { + std::vector argb; + uint32_t width; + uint32_t height; +}; + +DecodedImage WasmDecodeToArgb(const std::vector& encoded_image) { + uint32_t width, height; + auto status_or = + DecodeToArgb(encoded_image.data(), encoded_image.size(), &width, &height, + /*quiet=*/false); + if (status_or.status != Status::kOk) { + emscripten::val::global("Error")(std::string("Failed to decode image")) + .throw_(); + } + return {std::move(status_or.value), width, height}; +} + +std::vector WasmEncode(const std::vector& argb, + uint32_t width, uint32_t height, Codec codec, + Subsampling subsampling, int effort, + int quality) { + auto status_or = + Encode(argb.data(), width, height, codec, subsampling, effort, quality, + /*quiet=*/false); + if (status_or.status != Status::kOk) { + emscripten::val::global("Error")(std::string("Failed to encode image")) + .throw_(); + } + return std::move(status_or.value); +} + +} // namespace codec_compare_gen + +EMSCRIPTEN_BINDINGS(codec_compare_gen) { + using namespace codec_compare_gen; + + emscripten::enum_("Codec") + .value("Webp", Codec::kWebp) + .value("Webp2", Codec::kWebp2) + .value("JpegXl", Codec::kJpegXl) + .value("Avif", Codec::kAvif) + .value("Jpegturbo", Codec::kJpegturbo) + .value("Jpegli", Codec::kJpegli) + .value("Jpegsimple", Codec::kJpegsimple) + .value("Basis", Codec::kBasis); + + emscripten::enum_("Subsampling") + .value("Default", Subsampling::kDefault) + .value("Yuv444", Subsampling::k444) + .value("Yuv420", Subsampling::k420); + + emscripten::register_vector("ByteVector"); + emscripten::value_object("DecodedImage") + .field("argb", &DecodedImage::argb) + .field("width", &DecodedImage::width) + .field("height", &DecodedImage::height); + + emscripten::function("decodeToArgb", &WasmDecodeToArgb); + emscripten::function("encode", &WasmEncode); +} diff --git a/wasm/package.json b/wasm/package.json new file mode 100644 index 0000000..c9a26ca --- /dev/null +++ b/wasm/package.json @@ -0,0 +1,20 @@ +{ + "name": "codec_compare_gen_wasm", + "version": "1.0.0", + "description": "Codec-Compare-Gen WASM package", + "main": "codec_wasm_bin.js", + "license": "Apache-2.0", + "private": true, + "scripts": { + "test": "web-test-runner --files \"*_test.ts\" --node-resolve" + }, + "devDependencies": { + "@types/jasmine": "^5.1.5", + "@types/node": "^22.10.2", + "@web/dev-server-esbuild": "^1.0.4", + "@web/test-runner": "^0.19.0", + "jasmine-core": "^5.5.0", + "typescript": "^5.7.2", + "web-test-runner-jasmine": "^0.0.6" + } +} diff --git a/wasm/tests/test_wasm_test.ts b/wasm/tests/test_wasm_test.ts new file mode 100644 index 0000000..f4231af --- /dev/null +++ b/wasm/tests/test_wasm_test.ts @@ -0,0 +1,148 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {trustedResourceUrl} from 'safevalues'; +import {setScriptSrc} from 'safevalues/dom'; + +async function fetchBytes(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.statusText} at ${url}`); + } + return new Uint8Array(await response.arrayBuffer()); +} + +function toByteVector(module: any, bytes: Uint8Array): any { + const v = new module.ByteVector(); + // Using a loop for simplicity, can be optimized later + for (let i = 0; i < bytes.length; i++) { + v.push_back(bytes[i]); + } + return v; +} + +function fromByteVector(v: any): Uint8Array { + const size = v.size(); + const bytes = new Uint8Array(size); + for (let i = 0; i < size; i++) { + bytes[i] = v.get(i); + } + return bytes; +} + +describe('Codec WASM', () => { + let module: any; + let pngBytes: Uint8Array; + + beforeAll(async () => { + const factory = (window as any).loadCodecWasm; + console.log('factory (window.loadCodecWasm):', factory); + + if (typeof factory !== 'function') { + throw new Error(`loadCodecWasm is not a function. window.loadCodecWasm: ${ + typeof factory}. It should have been loaded by Karma via deps.`); + } + + module = await factory({ + locateFile: (path: string) => { + if (path.endsWith('.wasm')) { + const wasmPath = + 'codec_wasm_bin.wasm'; + console.log('locateFile:', wasmPath); + return wasmPath; + } + return path; + } + }); + + const pngPath = + 'gradient32x32.png'; + let fetchedBytes: Uint8Array|undefined; + try { + console.log('Trying to fetch PNG from:', pngPath); + fetchedBytes = await fetchBytes(pngPath); + console.log('Successfully fetched from:', pngPath); + } catch (e) { + console.log('Failed to fetch from:', pngPath); + } + if (!fetchedBytes) throw new Error('Could not find gradient32x32.png'); + pngBytes = fetchedBytes; + }); + + it('should encode and decode gradient32x32.png with WebP', async () => { + // Decode PNG to ARGB + const pngVector = toByteVector(module, pngBytes); + const decoded = module.decodeToArgb(pngVector); + expect(decoded.width).toBe(32); + expect(decoded.height).toBe(32); + expect(decoded.argb.size()).toBe(32 * 32 * 4); + + // Encode ARGB to WebP + const EFFORT = 0; + const LOSSLESS_QUALITY = -1; + const encodedVector = module.encode( + decoded.argb, decoded.width, decoded.height, module.Codec.Webp, + module.Subsampling.Default, EFFORT, LOSSLESS_QUALITY); + expect(encodedVector.size()).toBeGreaterThan(0); + + // Decode WebP back to ARGB + const decodedAgain = module.decodeToArgb(encodedVector); + expect(decodedAgain).toEqual(decoded); + + // Clean up + pngVector.delete(); + decoded.argb.delete(); + encodedVector.delete(); + decodedAgain.argb.delete(); + }); + + it('should encode and decode with Jpegturbo', async () => { + const pngVector = toByteVector(module, pngBytes); + const decoded = module.decodeToArgb(pngVector); + + const encodedVector = module.encode( + decoded.argb, decoded.width, decoded.height, module.Codec.Jpegturbo, + module.Subsampling.Default, 0, 90); + expect(encodedVector.size()).toBeGreaterThan(0); + + const decodedAgain = module.decodeToArgb(encodedVector); + expect(decodedAgain.width).toBe(32); + expect(decodedAgain.height).toBe(32); + + let maxDiff = 0; + for (let i = 0; i < decoded.argb.size(); i++) { + const expected = decoded.argb.get(i); + const actual = decodedAgain.argb.get(i); + const diff = Math.abs(expected - actual); + if (diff > maxDiff) maxDiff = diff; + } + console.log('Jpegturbo Maximum pixel difference:', maxDiff); + // JPEG-turbo is lossy, so we expect some difference. 25 is safe for 90 + // quality. + expect(maxDiff).toBeLessThanOrEqual(25); + + pngVector.delete(); + decoded.argb.delete(); + encodedVector.delete(); + decodedAgain.argb.delete(); + }); + + it('should throw an error on invalid bytes', () => { + const invalidBytes = toByteVector(module, new Uint8Array([1, 2, 3])); + expect(() => { + module.decodeToArgb(invalidBytes); + }).toThrowError(/Failed to decode image/); + invalidBytes.delete(); + }); +}); diff --git a/wasm/tests/web-test-runner.config.mjs b/wasm/tests/web-test-runner.config.mjs new file mode 100644 index 0000000..616b1bc --- /dev/null +++ b/wasm/tests/web-test-runner.config.mjs @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {esbuildPlugin} from '@web/dev-server-esbuild'; +import {jasmineTestRunnerConfig} from 'web-test-runner-jasmine'; + +export default { + // The following would keep web-test-runner-jasmine's testRunnerHtml as is: + // ...jasmineTestRunnerConfig(), + // Instead, reuse most of that testRunnerHtml and inject codec_wasm_bin.js. + reporters: jasmineTestRunnerConfig().reporters, + testRunnerHtml: + (testRunnerImport, config) => { + const html = + jasmineTestRunnerConfig().testRunnerHtml(testRunnerImport, config); + const parts = html.split(''); + return parts[0].concat( + '', + '', + parts[1]); + }, + + nodeResolve: true, + files: ['*_test.ts'], + plugins: [esbuildPlugin({ts: true, tsconfig: './tsconfig.json'})], +}; diff --git a/wasm/tsconfig.json b/wasm/tsconfig.json new file mode 100644 index 0000000..66fbc34 --- /dev/null +++ b/wasm/tsconfig.json @@ -0,0 +1,5 @@ +{ + "include": ["."], + "exclude": ["dist", "node_modules"], + "compilerOptions": { "outDir": "./dist" } +}