diff options
author | Job Bautista <jobbautista9@protonmail.com> | 2022-06-19 15:34:18 +0800 |
---|---|---|
committer | Job Bautista <jobbautista9@protonmail.com> | 2022-06-29 13:53:09 +0800 |
commit | 73e06b6df9e396ef080fefcaa3196265bff723c4 (patch) | |
tree | 47caa76143ab4bdf70c2a0eb5f4657d2fe0ee22b /media/libjxl/src/tools | |
parent | 8c1064bc1cb494e445a6447587afb8942a6b783b (diff) | |
download | uxp-73e06b6df9e396ef080fefcaa3196265bff723c4.tar.gz |
Issue #1769 - Part 1: Add vendored libjxl and highway sources.
Used old-configure to add the build option for enabling JPEG-XL support.
Highway version: 0.17.0
libjxl version: tree of commit 318c592d98b97d103941b90d47107f06a10c71da
Diffstat (limited to 'media/libjxl/src/tools')
181 files changed, 23892 insertions, 0 deletions
diff --git a/media/libjxl/src/tools/CMakeLists.txt b/media/libjxl/src/tools/CMakeLists.txt new file mode 100644 index 0000000000..9ee2e53fcb --- /dev/null +++ b/media/libjxl/src/tools/CMakeLists.txt @@ -0,0 +1,483 @@ +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# ICC detection library used by the comparison and viewer tools. +if(JPEGXL_ENABLE_VIEWERS) +if(WIN32) + find_package(Qt5 QUIET COMPONENTS Widgets) + if (NOT Qt5_FOUND) + message(WARNING "Qt5 was not found.") + else() + add_library(icc_detect STATIC EXCLUDE_FROM_ALL + icc_detect/icc_detect_win32.cc + icc_detect/icc_detect.h + ) + target_include_directories(icc_detect PRIVATE "${PROJECT_SOURCE_DIR}") + target_link_libraries(icc_detect PUBLIC Qt5::Widgets) + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/libqt5widgets5/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.libqt5widgets5 COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR + endif() +elseif(APPLE) + find_package(Qt5 QUIET COMPONENTS Widgets) + if (Qt5_FOUND) + add_library(icc_detect STATIC EXCLUDE_FROM_ALL + icc_detect/icc_detect_empty.cc + icc_detect/icc_detect.h + ) + target_include_directories(icc_detect PRIVATE "${PROJECT_SOURCE_DIR}") + target_link_libraries(icc_detect PUBLIC Qt5::Widgets) + else() + message(WARNING "APPLE: Qt5 was not found.") + endif() +else() + find_package(Qt5 QUIET COMPONENTS Widgets X11Extras) + find_package(ECM QUIET NO_MODULE) + if (NOT Qt5_FOUND OR NOT ECM_FOUND) + if (NOT Qt5_FOUND) + message(WARNING "Qt5 was not found.") + else() + message(WARNING "extra-cmake-modules were not found.") + endif() + else() + set(CMAKE_MODULE_PATH ${ECM_FIND_MODULE_DIR}) + find_package(XCB COMPONENTS XCB) + if (XCB_FOUND) + add_library(icc_detect STATIC EXCLUDE_FROM_ALL + icc_detect/icc_detect_x11.cc + icc_detect/icc_detect.h + ) + target_link_libraries(icc_detect PUBLIC jxl-static Qt5::Widgets Qt5::X11Extras XCB::XCB) + endif () + endif() +endif() +endif() # JPEGXL_ENABLE_VIEWERS + +# Tools are added conditionally below. +set(TOOL_BINARIES) + +add_library(jxl_tool STATIC EXCLUDE_FROM_ALL + cmdline.cc + codec_config.cc + tool_version.cc +) +target_compile_options(jxl_tool PUBLIC "${JPEGXL_INTERNAL_FLAGS}") +target_link_libraries(jxl_tool jxl-static) + +target_include_directories(jxl_tool + PUBLIC "${PROJECT_SOURCE_DIR}") + +# The JPEGXL_VERSION is set from the builders. +if(NOT DEFINED JPEGXL_VERSION OR JPEGXL_VERSION STREQUAL "") + find_package(Git QUIET) + execute_process( + COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD + OUTPUT_VARIABLE GIT_REV + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + ERROR_QUIET) + string(STRIP "${GIT_REV}" GIT_REV) + if(GIT_REV STREQUAL "") + set(JPEGXL_VERSION "(unknown)") + endif() +endif() + +if(NOT DEFINED JPEGXL_VERSION OR JPEGXL_VERSION STREQUAL "") + # We are building from a git environment and the user didn't set + # JPEGXL_VERSION. Make a target that computes the GIT_REV at build-time always + # but only updates the file if it changed. This allows rebuilds without + # modifying cmake files to update the JPEGXL_VERSION. + message(STATUS "Building with JPEGXL_VERSION=${GIT_REV} (auto-updated)") + add_custom_target( + tool_version_git + ${CMAKE_COMMAND} + -D JPEGXL_ROOT_DIR=${CMAKE_SOURCE_DIR} + -D DST=${CMAKE_CURRENT_BINARY_DIR}/tool_version_git.h + -P ${CMAKE_CURRENT_SOURCE_DIR}/git_version.cmake + BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/tool_version_git.h" + ) + add_dependencies(jxl_tool tool_version_git) + + set_source_files_properties(tool_version.cc PROPERTIES + COMPILE_DEFINITIONS JPEGXL_VERSION_FROM_GIT=1) + target_include_directories(jxl_tool PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + # Note: Ninja looks for dependencies on the jxl_tool target before running + # the tool_version_git targets, so when updating the tool_version_git.h the + # jxl_tool target is not rebuilt. This forces to generate it at configure time + # if needed. + execute_process( + COMMAND ${CMAKE_COMMAND} + -D JPEGXL_ROOT_DIR=${CMAKE_SOURCE_DIR} + -D DST=${CMAKE_CURRENT_BINARY_DIR}/tool_version_git.h + -P ${CMAKE_CURRENT_SOURCE_DIR}/git_version.cmake) +else() + message(STATUS "Building with JPEGXL_VERSION=${JPEGXL_VERSION}") + set_source_files_properties(tool_version.cc PROPERTIES + COMPILE_DEFINITIONS JPEGXL_VERSION=\"${JPEGXL_VERSION}\") +endif() + +if(JPEGXL_ENABLE_TOOLS) + list(APPEND TOOL_BINARIES + cjxl + cjxl_ng + djxl + djxl_ng + cjpeg_hdr + jxlinfo + ) + + # Main compressor. + add_executable(cjxl + cjxl.cc + speed_stats.cc + cjxl_main.cc + ) + target_link_libraries(cjxl + box + jxl-static + jxl_extras-static + jxl_threads-static + ) + + + add_executable(cjxl_ng + cjxl_ng_main.cc + ) + target_link_libraries(cjxl_ng + jxl + jxl_extras_dec-static + jxl_threads + hwy + jxl_gflags + ) + target_include_directories(cjxl_ng PRIVATE "${PROJECT_SOURCE_DIR}") + if(JPEGXL_EMSCRIPTEN) + set_target_properties(cjxl_ng PROPERTIES LINK_FLAGS "-s USE_LIBPNG=1") + endif() + + add_executable(djxl_ng + djxl_ng_main.cc + ) + target_link_libraries(djxl_ng + jxl + jxl_gflags + ) + + add_executable(cjpeg_hdr + cjpeg_hdr.cc + ) + target_link_libraries(cjpeg_hdr + box + jxl-static + jxl_extras-static + jxl_threads-static + ) + + # Main decompressor. + add_library(djxltool STATIC + djxl.cc + speed_stats.cc + ) + target_link_libraries(djxltool + box + jxl-static + jxl_extras-static + jxl_threads-static + ) + + add_executable(djxl + djxl_main.cc + ) + target_link_libraries(djxl djxltool) + + add_executable(jxlinfo + jxlinfo.c + ) + target_link_libraries(jxlinfo jxl) + + if(NOT ${SANITIZER} STREQUAL "none") + # Linking a C test binary with the C++ JPEG XL implementation when using + # address sanitizer is not well supported by clang 9, so force using clang++ + # for linking this test if a sanitizer is used. + set_target_properties(jxlinfo PROPERTIES LINKER_LANGUAGE CXX) + endif() # SANITIZER != "none" + +endif() # JPEGXL_ENABLE_TOOLS + +# Other developer tools. +if(${JPEGXL_ENABLE_DEVTOOLS}) + list(APPEND TOOL_BINARIES + fuzzer_corpus + butteraugli_main + decode_and_encode + display_to_hlg + pq_to_hlg + render_hlg + tone_map + texture_to_cube + generate_lut_template + ssimulacra_main + xyb_range + jxl_from_tree + ) + + add_executable(fuzzer_corpus fuzzer_corpus.cc) + + add_executable(ssimulacra_main ssimulacra_main.cc ssimulacra.cc) + add_executable(butteraugli_main butteraugli_main.cc) + add_executable(decode_and_encode decode_and_encode.cc) + add_executable(display_to_hlg hdr/display_to_hlg.cc) + add_executable(pq_to_hlg hdr/pq_to_hlg.cc) + add_executable(render_hlg hdr/render_hlg.cc) + add_executable(tone_map hdr/tone_map.cc) + add_executable(texture_to_cube hdr/texture_to_cube.cc) + add_executable(generate_lut_template hdr/generate_lut_template.cc) + add_executable(xyb_range xyb_range.cc) + add_executable(jxl_from_tree jxl_from_tree.cc) +endif() # JPEGXL_ENABLE_DEVTOOLS + +# Benchmark tools. +if(${JPEGXL_ENABLE_BENCHMARK}) + list(APPEND TOOL_BINARIES + benchmark_xl + ) + + add_executable(benchmark_xl + benchmark/benchmark_xl.cc + benchmark/benchmark_args.cc + benchmark/benchmark_codec.cc + benchmark/benchmark_file_io.cc + benchmark/benchmark_stats.cc + benchmark/benchmark_utils.cc + benchmark/benchmark_utils.h + benchmark/benchmark_codec_custom.cc + benchmark/benchmark_codec_custom.h + benchmark/benchmark_codec_jxl.cc + benchmark/benchmark_codec_jxl.h + speed_stats.cc + speed_stats.h + ../third_party/dirent.cc + ) + target_link_libraries(benchmark_xl Threads::Threads) + if(MINGW) + # MINGW doesn't support glob.h. + target_compile_definitions(benchmark_xl PRIVATE "-DHAS_GLOB=0") + endif() # MINGW + + find_package(JPEG) + if(JPEG_FOUND) + target_sources(benchmark_xl PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_jpeg.cc" + "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_jpeg.h" + ) + endif () + + if(NOT JPEGXL_BUNDLE_LIBPNG) + find_package(PNG) + endif() + if(PNG_FOUND) + target_sources(benchmark_xl PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_png.cc" + "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_png.h" + ) + endif() + + find_package(PkgConfig) + pkg_check_modules(WebP IMPORTED_TARGET libwebp) + if(WebP_FOUND) + target_sources(benchmark_xl PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_webp.cc" + "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_webp.h" + ) + target_compile_definitions(benchmark_xl PRIVATE -DBENCHMARK_WEBP) + + # Use the static version of webp if available. + find_library(WebP_STATIC_LINK_LIBRARY NAMES libwebp.a + PATHS "${WebP_LIBDIR}") + if("${WebP_STATIC_LINK_LIBRARY}" STREQUAL "WebP_STATIC_LINK_LIBRARY-NOTFOUND") + message(WARNING "Using dynamic libwebp") + target_link_libraries(benchmark_xl PkgConfig::WebP) + else() + target_link_libraries(benchmark_xl "${WebP_STATIC_LINK_LIBRARY}") + target_include_directories(benchmark_xl + PRIVATE ${WebP_STATIC_INCLUDE_DIRS}) + target_compile_options(benchmark_xl PRIVATE ${WebP_STATIC_CFLAGS_OTHER}) + endif() + endif() + + pkg_check_modules(AVIF IMPORTED_TARGET libavif) + if(AVIF_FOUND) + target_sources(benchmark_xl PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_avif.cc" + "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_avif.h" + ) + target_compile_definitions(benchmark_xl PRIVATE -DBENCHMARK_AVIF) + target_link_libraries(benchmark_xl PkgConfig::AVIF) + endif() +endif() # JPEGXL_ENABLE_BENCHMARK + +# All tool binaries depend on "jxl" library and the tool helpers. +foreach(BINARY IN LISTS TOOL_BINARIES) + target_include_directories("${BINARY}" PRIVATE "${PROJECT_SOURCE_DIR}") + target_link_libraries("${BINARY}" box jxl-static jxl_extras-static jxl_threads-static jxl_tool) + if(JPEGXL_EMSCRIPTEN) + set_target_properties(${BINARY} PROPERTIES LINK_FLAGS "-s USE_LIBPNG=1") + endif() + +endforeach() +install(TARGETS ${TOOL_BINARIES} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +message(STATUS "Building tools: ${TOOL_BINARIES}") + +set(FUZZER_BINARIES + color_encoding_fuzzer + decode_basic_info_fuzzer + djxl_fuzzer + icc_codec_fuzzer + fields_fuzzer + rans_fuzzer + set_from_bytes_fuzzer + transforms_fuzzer +) + +# Fuzzers. +foreach(FUZZER IN LISTS FUZZER_BINARIES) + if(${JPEGXL_ENABLE_FUZZERS}) + set(BINARY "${FUZZER}") + add_executable("${BINARY}" "${BINARY}.cc") + target_link_libraries("${BINARY}" ${JPEGXL_FUZZER_LINK_FLAGS}) + else() + # When not enabled we want a lightweight alternative for regular fuzzers + # that just run the target. + set(BINARY "${FUZZER}_runner") + add_executable("${BINARY}" EXCLUDE_FROM_ALL + "fuzzer_stub.cc" "${FUZZER}.cc") + endif() # JPEGXL_ENABLE_FUZZERS + target_include_directories("${BINARY}" PRIVATE "${CMAKE_SOURCE_DIR}") + if(${FUZZER} STREQUAL djxl_fuzzer) + target_link_libraries("${BINARY}" + jxl_dec-static + jxl_threads-static + ) + else() + target_link_libraries("${BINARY}" + jxl-static + jxl_extras-static + jxl_threads-static + jxl_tool + ) + endif() +endforeach() + +# EMSCRIPTEN doesn't support dynamic libraries so testing for linkage there +# doesn't make much sense. +if(BUILD_TESTING AND TARGET jxl AND NOT JPEGXL_EMSCRIPTEN) +# Library API test. This test is only to check that we can link against the +# shared library from C99 file and don't need to use internal symbols. +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tests) +add_executable(libjxl_test libjxl_test.c) +set_property(TARGET libjxl_test PROPERTY C_STANDARD 99) +if(NOT ${SANITIZER} STREQUAL "none") + # Linking a C test binary with the C++ JPEG XL implementation when using + # address sanitizer is not well supported by clang 9, so force using clang++ + # for linking this test if a sanitizer is used. + set_target_properties(libjxl_test PROPERTIES LINKER_LANGUAGE CXX) +endif() # SANITIZER != "none" +set_target_properties(libjxl_test PROPERTIES PREFIX "tests/") +target_link_libraries(libjxl_test jxl) +if (NOT MSVC) +target_compile_options(libjxl_test PRIVATE -Wall -Wextra -Werror) +if(NOT WIN32) + target_compile_options(libjxl_test PRIVATE -pedantic) +endif() # NOT WIN32 +endif() # NOT MSVC + +add_test(NAME LibraryCLinkageTest COMMAND libjxl_test) + +endif() # BUILD_TESTING AND TARGET jxl AND NOT JPEGXL_EMSCRIPTEN + +# Tools defined in subdirectories. +if(${JPEGXL_ENABLE_VIEWERS}) +add_subdirectory(viewer) +add_subdirectory(comparison_viewer) +add_subdirectory(flicker_test) +endif() + +add_subdirectory(box) +add_subdirectory(conformance) + + +if ("${JPEGXL_EMSCRIPTEN}") +# WASM API facade. +add_executable(jxl_emcc jxl_emcc.cc) +target_link_libraries(jxl_emcc jxl-static jxl_extras-static) +set_target_properties(jxl_emcc PROPERTIES LINK_FLAGS "\ + -O3\ + --closure 1 \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s USE_LIBPNG=1 \ + -s DISABLE_EXCEPTION_CATCHING=1 \ + -s MODULARIZE=1 \ + -s FILESYSTEM=0 \ + -s EXPORT_NAME=\"JxlCodecModule\"\ + -s \"EXPORTED_FUNCTIONS=[\ + _jxlCompress,\ + _jxlDecompress,\ + _free,\ + _malloc\ + ]\"\ +") +endif () # JPEGXL_EMSCRIPTEN + +if(JPEGXL_ENABLE_JNI) +find_package(JNI QUIET) +find_package(Java QUIET) + +if ("${JNI_FOUND}" AND "${Java_FOUND}") + include(UseJava) + + # decoder_jni_onload.cc might be necessary for Android; not used yet. + add_library(jxl_jni SHARED jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc) + target_include_directories(jxl_jni PRIVATE "${JNI_INCLUDE_DIRS}" "${PROJECT_SOURCE_DIR}") + target_link_libraries(jxl_jni PUBLIC jxl_dec-static jxl_threads-static) + if(NOT DEFINED JPEGXL_INSTALL_JNIDIR) + set(JPEGXL_INSTALL_JNIDIR ${CMAKE_INSTALL_LIBDIR}) + endif() + install(TARGETS jxl_jni DESTINATION ${JPEGXL_INSTALL_JNIDIR}) + + add_jar(jxl_jni_wrapper SOURCES + jni/org/jpeg/jpegxl/wrapper/Decoder.java + jni/org/jpeg/jpegxl/wrapper/DecoderJni.java + jni/org/jpeg/jpegxl/wrapper/ImageData.java + jni/org/jpeg/jpegxl/wrapper/PixelFormat.java + jni/org/jpeg/jpegxl/wrapper/Status.java + jni/org/jpeg/jpegxl/wrapper/StreamInfo.java + OUTPUT_NAME org.jpeg.jpegxl + ) + get_target_property(JXL_JNI_WRAPPER_JAR jxl_jni_wrapper JAR_FILE) + if(NOT DEFINED JPEGXL_INSTALL_JARDIR) + set(JPEGXL_INSTALL_JARDIR ${CMAKE_INSTALL_LIBDIR}) + endif() + install_jar(jxl_jni_wrapper DESTINATION ${JPEGXL_INSTALL_JARDIR}) + + add_jar(jxl_jni_wrapper_test + SOURCES jni/org/jpeg/jpegxl/wrapper/DecoderTest.java + INCLUDE_JARS jxl_jni_wrapper + ) + get_target_property(JXL_JNI_WRAPPER_TEST_JAR jxl_jni_wrapper_test JAR_FILE) + + if(NOT "${SANITIZER}" MATCHES ".san") + # NB: Vanilla OpenJDK 8 / 11 are known to work well (i.e. either + # "which java" or JAVA_HOME environment variable point to the path like + # "/usr/lib/jvm/java-xx-openjdk-yyy" on Debian Linux). + add_test( + NAME test_jxl_jni_wrapper + COMMAND ${Java_JAVA_EXECUTABLE} + -cp "${JXL_JNI_WRAPPER_JAR}:${JXL_JNI_WRAPPER_TEST_JAR}" + -Dorg.jpeg.jpegxl.wrapper.lib=$<TARGET_FILE:jxl_jni> + org.jpeg.jpegxl.wrapper.DecoderTest + ) + endif() # JPEGXL_ENABLE_FUZZERS +endif() # JNI_FOUND & Java_FOUND +endif() # JPEGXL_ENABLE_JNI diff --git a/media/libjxl/src/tools/README.cjpeg_hdr.md b/media/libjxl/src/tools/README.cjpeg_hdr.md new file mode 100644 index 0000000000..bd7c793bdb --- /dev/null +++ b/media/libjxl/src/tools/README.cjpeg_hdr.md @@ -0,0 +1,73 @@ +# High bit depth JPEG encoder +`cjpeg_hdr` is an (experimental) JPEG encoder that can preserve a higher bit +depth than a traditional JPEG encoder. In particular, it may be used to produce +HDR JPEGs that do not show obvious signs of banding. + +Note that at this point in time `cjpeg_hdr` does not attempt to actually +*compress* the image - it behaves in the same way as a "quality 100" JPEG +encoder would normally do, i.e. no quantization, to achieve the maximum +possible visual quality. Moreover, no Huffman optimization is performed. + +## Generating HBD JPEGs +Note: this and the following sections assume that `libjxl` has been built in +the `build/` directory, either by using CMake or by running `./ci.sh opt`. + +It should be sufficient to run `build/tools/cjpeg_hdr input_image output.jpg`. +Various input formats are supported, including NetBPM and (8- or 16-bit) PNG. + +If the PNG image includes a colour profile, it will be copied in the resulting +JPEG image. If this colour profile approximates the PQ or HLG transfer curves, +some applications will consider the resulting image to be HDR. + +To attach a PQ profile to an image without a colour profile (or with a +different colour profile), the following command can be used: + +``` + build/tools/decode_and_encode input RGB_D65_202_Rel_PeQ output_with_pq.png 16 +``` + +Similarly, to attach an HLG profile, the following command can be used + +``` + build/tools/decode_and_encode input RGB_D65_202_Rel_HLG output_with_pq.png 16 +``` + +## Decoding HBD JPEGs +HBD JPEGs are fully retrocompatible with libjpeg, and any JPEG viewer ought to +be able to visualize them. Nonetheless, to achieve the best visual quality, a +high bit depth decoder should be used. + +Such a decoder does not exist today. As a workaround, it is possible to do a +lossless conversion to JPEG XL and then view the resulting image: + +``` + build/tools/cjxl --jpeg_transcode_disable_cfl hbd.jpeg hbd.jxl +``` + +The resulting JPEG XL file can be visualized, for example, in a browser, +assuming that the corresponding flag is enabled in the settings. + +In particular, if the HBD JPEG has a PQ or HLG profile attached and the current +display is an HDR display, Chrome ought to visualize the image as HDR content. + +It is also possible to convert the JPEG XL file back to a 16-bit PNG: + +``` + build/tools/djxl hbd.jxl --bits_per_sample=16 output.png +``` + +Note however that as of today (2 Nov 2021) Chrome does not interpret such a PNG +as an HDR image, even if a PQ or HLG profile is attached. Thus, to display the +HDR image correctly it is recommended to either display the JPEG XL image +directly or to convert the PNG to a format that Chrome interprets as HDR, such +as AVIF. This can be done with the following command for a PQ image: + +``` + avifenc -l -y 444 --depth 10 --cicp 9/16/9 image.png output.avif +``` + +and the following one for an HLG image: + +``` + avifenc -l -y 444 --depth 10 --cicp 9/18/9 image.png output.avif +``` diff --git a/media/libjxl/src/tools/args.h b/media/libjxl/src/tools/args.h new file mode 100644 index 0000000000..cc26a35dc7 --- /dev/null +++ b/media/libjxl/src/tools/args.h @@ -0,0 +1,163 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_ARGS_H_ +#define TOOLS_ARGS_H_ + +// Helpers for parsing command line arguments. No include guard needed. + +#include <inttypes.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include <string> +#include <vector> + +#include "lib/extras/dec/color_hints.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" // DecoderHints +#include "lib/jxl/gaborish.h" +#include "lib/jxl/modular/options.h" + +namespace jpegxl { +namespace tools { + +static inline bool ParseOverride(const char* arg, jxl::Override* out) { + const std::string s_arg(arg); + if (s_arg == "1") { + *out = jxl::Override::kOn; + return true; + } + if (s_arg == "0") { + *out = jxl::Override::kOff; + return true; + } + fprintf(stderr, "Invalid flag, %s must be 0 or 1\n", arg); + return JXL_FAILURE("Args"); +} + +static inline bool ParseUnsigned(const char* arg, size_t* out) { + char* end; + *out = static_cast<size_t>(strtoull(arg, &end, 0)); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as unsigned integer: %s.\n", arg); + return JXL_FAILURE("Args"); + } + return true; +} + +static inline bool ParseUint32(const char* arg, uint32_t* out) { + size_t value = 0; + bool ret = ParseUnsigned(arg, &value); + if (ret) *out = value; + return ret; +} + +static inline bool ParseSigned(const char* arg, int* out) { + char* end; + *out = static_cast<int>(strtol(arg, &end, 0)); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as signed integer: %s.\n", arg); + return JXL_FAILURE("Args"); + } + return true; +} + +static inline bool ParseFloat(const char* arg, float* out) { + char* end; + *out = static_cast<float>(strtod(arg, &end)); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as float: %s.\n", arg); + return JXL_FAILURE("Args"); + } + return true; +} + +static inline bool ParseFloatPair(const char* arg, + std::pair<float, float>* out) { + int parsed = sscanf(arg, "%f,%f", &out->first, &out->second); + if (parsed == 1) { + out->second = out->first; + } else if (parsed != 2) { + fprintf(stderr, + "Unable to interpret as float pair separated by a comma: %s.\n", + arg); + return JXL_FAILURE("Args"); + } + return true; +} + +static inline bool ParseDouble(const char* arg, double* out) { + char* end; + *out = static_cast<double>(strtod(arg, &end)); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as double: %s.\n", arg); + return JXL_FAILURE("Args"); + } + return true; +} + +static inline bool ParseAndAppendKeyValue(const char* arg, + jxl::extras::ColorHints* out) { + const char* eq = strchr(arg, '='); + if (!eq) { + fprintf(stderr, "Expected argument as 'key=value' but received '%s'\n", + arg); + return false; + } + std::string key(arg, eq); + out->Add(key, std::string(eq + 1)); + return true; +} + +static inline bool ParsePredictor(const char* arg, jxl::Predictor* out) { + char* end; + uint64_t p = static_cast<uint64_t>(strtoull(arg, &end, 0)); + if (end[0] != '\0') { + fprintf(stderr, "Invalid predictor: %s.\n", arg); + return JXL_FAILURE("Args"); + } + if (p >= jxl::kNumModularEncoderPredictors) { + fprintf(stderr, + "Invalid predictor value %" PRIu64 ", must be less than %" PRIu64 + ".\n", + p, static_cast<uint64_t>(jxl::kNumModularEncoderPredictors)); + return JXL_FAILURE("Args"); + } + *out = static_cast<jxl::Predictor>(p); + return true; +} + +static inline bool ParseString(const char* arg, std::string* out) { + out->assign(arg); + return true; +} + +static inline bool ParseCString(const char* arg, const char** out) { + *out = arg; + return true; +} + +static inline bool SetBooleanTrue(bool* out) { + *out = true; + return true; +} + +static inline bool SetBooleanFalse(bool* out) { + *out = false; + return true; +} + +static inline bool IncrementUnsigned(size_t* out) { + (*out)++; + return true; +} + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_ARGS_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_args.cc b/media/libjxl/src/tools/benchmark/benchmark_args.cc new file mode 100644 index 0000000000..2bd3eb8932 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_args.cc @@ -0,0 +1,281 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/benchmark/benchmark_args.h" + +#include <stddef.h> +#include <stdlib.h> + +#include <algorithm> +#include <string> +#include <vector> + +#include "lib/extras/codec.h" +#include "lib/extras/dec/color_description.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "tools/benchmark/benchmark_codec_jpeg.h" // for AddCommand.. +#include "tools/benchmark/benchmark_codec_jxl.h" +#if JPEGXL_ENABLE_APNG +#include "tools/benchmark/benchmark_codec_png.h" +#endif + +#ifdef BENCHMARK_WEBP +#include "tools/benchmark/benchmark_codec_webp.h" +#endif // BENCHMARK_WEBP + +#ifdef BENCHMARK_AVIF +#include "tools/benchmark/benchmark_codec_avif.h" +#endif // BENCHMARK_AVIF + +namespace jxl { + +std::vector<std::string> SplitString(const std::string& s, char c) { + std::vector<std::string> result; + size_t pos = 0; + for (size_t i = 0; i <= s.size(); i++) { + if (i == s.size() || s[i] == c) { + result.push_back(s.substr(pos, i - pos)); + pos = i + 1; + } + } + return result; +} + +int ParseIntParam(const std::string& param, int lower_bound, int upper_bound) { + int val = strtol(param.substr(1).c_str(), nullptr, 10); + JXL_CHECK(val >= lower_bound && val <= upper_bound); + return val; +} + +BenchmarkArgs* Args() { + static BenchmarkArgs args; + return &args; +} + +Status BenchmarkArgs::AddCommandLineOptions() { + AddString(&input, "input", "File or file pattern matching input files."); + AddString(&codec, "codec", + "Comma separated list of image codec descriptions to benchmark.", + "jxl"); + AddFlag(&print_details, "print_details", + "Prints size and distortion for each image. Not safe for " + "concurrent benchmark runs.", + false); + AddFlag(&print_details_csv, "print_details_csv", + "When print_details is used, print as CSV.", false); + AddString(&extra_metrics, "extra_metrics", + "Extra metrics to be computed. Only displayed with --print_details " + "or --print_details_csv. Comma-separated list of NAME:COMMAND " + "pairs; COMMAND is invoked with the original image as the first " + "argument, the decompressed image as a second argument, and the " + "name of the file where to write the metric value (as a single " + "floating point number) as the third argument.", + ""); + AddFlag( + &print_more_stats, "print_more_stats", + "Prints codec-specific stats. Not safe for concurrent benchmark runs.", + false); + AddFlag(&print_distance_percentiles, "print_distance_percentiles", + "Prints distance percentiles for the corpus. Not safe for " + "concurrent benchmark runs.", + false); + AddFlag(&silent_errors, "silent_errors", + "If true, doesn't print error messages on compression or" + " decompression errors. Errors counts are still visible in the" + " 'Errors' column of the result table. Please note that depending" + " depending on the JXL build settings, error messages and asserts" + " from within the codec may be printed irrespective of this flag" + " anyway, use release build to ensure no messages.", + false); + AddFlag(&save_compressed, "save_compressed", + "Saves the compressed files for each input image and each codec.", + false); + AddFlag(&save_decompressed, "save_decompressed", + "Saves the decompressed files as PNG for each input image " + "and each codec.", + false); + AddString(&output_extension, "output_extension", + "Extension (starting with dot) to use for saving output images.", + ".png"); + AddString(&output_description, "output_description", + "Color encoding (see ParseDescription; e.g. RGB_D65_SRG_Rel_709) " + "for saving output images, " + " defaults to sRGB."); + + AddFloat(&intensity_target, "intensity_target", + "Intended viewing intensity target in nits. Defaults to 255 for " + "SDR images, 4000 for HDR images (when the input image uses PQ or " + "HLG transfer function)", + 0); + + AddString(&color_hints_string, "dec-hints", + "Color encoding hints for the input images to encoder. Comma " + "separated key=value pairs. The key color_space indicates " + "ColorEncoding (see ParseDescription; e.g. RGB_D65_SRG_Rel_709) " + "for input images without color encoding (such as PNM)"); + + AddUnsigned( + &override_bitdepth, "override_bitdepth", + "If nonzero, store the given bit depth in the JPEG XL file metadata" + " (1-32), instead of using the bit depth from the original input" + " image.", + 0); + + AddDouble(&mul_output, "mul_output", + "If nonzero, multiplies linear sRGB by this and clamps to 255", + 0.0); + AddDouble(&heatmap_good, "heatmap_good", + "If greater than zero, use this as the good " + "threshold for creating heatmap images.", + 0.0); + AddDouble(&heatmap_bad, "heatmap_bad", + "If greater than zero, use this as the bad " + "threshold for creating heatmap images.", + 0.0); + + AddFlag(&write_html_report, "write_html_report", + "Creates an html report with original and compressed images.", false); + AddFlag(&html_report_self_contained, "html_report_self_contained", + "Base64-encode the images in the HTML report rather than use " + "external file names. May cause very large HTML data size.", + false); + + AddFlag( + &markdown, "markdown", + "Adds formatting around ASCII table to render correctly in Markdown based" + " interfaces", + true); + + AddFlag(&more_columns, "more_columns", "Print extra columns in the table", + false); + + AddString(&originals_url, "originals_url", + "Url prefix to serve original images from in the html report."); + AddString(&output_dir, "output_dir", + "If not empty, save compressed and decompressed " + "images here."); + + AddSigned(&num_threads, "num_threads", + "The number of threads for concurrent benchmarking. Defaults to " + "1 thread per CPU core (if negative).", + -1); + AddSigned(&inner_threads, "inner_threads", + "The number of extra threads per task. " + "Defaults to occupy cores (if negative).", + -1); + AddUnsigned(&encode_reps, "encode_reps", + "How many times to encode (>1 for more precise measurements). " + "Defaults to 1.", + 1); + AddUnsigned(&decode_reps, "decode_reps", + "How many times to decode (>1 for more precise measurements). " + "Defaults to 1.", + 1); + + AddString(&sample_tmp_dir, "sample_tmp_dir", + "Directory to put samples from input images."); + + AddSigned(&num_samples, "num_samples", "How many sample areas to take.", 0); + AddSigned(&sample_dimensions, "sample_dimensions", + "How big areas to sample from the input.", 64); + + AddDouble(&error_pnorm, "error_pnorm", + "smallest p norm for pooling butteraugli values", 3.0); + + AddFloat(&ba_params.hf_asymmetry, "hf_asymmetry", + "Multiplier for weighting HF artefacts more than features " + "being smoothed out. 1.0 means no HF asymmetry. 0.3 is " + "a good value to start exploring for asymmetry.", + 0.8f); + AddFlag(&profiler, "profiler", "If true, print profiler results.", false); + + AddFlag(&show_progress, "show_progress", + "Show activity dots per completed file during benchmark.", false); + + AddFlag(&skip_butteraugli, "skip_butteraugli", + "If true, doesn't compute distance metrics, only compression and" + " decompression speed and size. Distance numbers shown in the" + " table are invalid.", + false); + + AddFlag( + &decode_only, "decode_only", + "If true, only decodes, and the input files must be compressed with a " + "compatible format for the given codec(s). Only measures decompression " + "speed and sizes, and can only use a single set of compatible decoders. " + "Distance numbers and compression speeds shown in the table are invalid.", + false); + + if (!AddCommandLineOptionsJxlCodec(this)) return false; +#ifdef BENCHMARK_JPEG + if (!AddCommandLineOptionsJPEGCodec(this)) return false; +#endif // BENCHMARK_JPEG +#if JPEGXL_ENABLE_APNG + if (!AddCommandLineOptionsPNGCodec(this)) return false; +#endif +#ifdef BENCHMARK_WEBP + if (!AddCommandLineOptionsWebPCodec(this)) return false; +#endif // BENCHMARK_WEBP +#ifdef BENCHMARK_AVIF + if (!AddCommandLineOptionsAvifCodec(this)) return false; +#endif // BENCHMARK_AVIF + + return true; +} + +Status BenchmarkArgs::ValidateArgs() { + size_t bits_per_sample = 0; // unused + if (input.empty()) { + fprintf(stderr, "Missing --input filename(s).\n"); + return false; + } + if (extras::CodecFromExtension(output_extension, &bits_per_sample) == + extras::Codec::kUnknown) { + JXL_WARNING("Unrecognized output_extension %s, try .png", + output_extension.c_str()); + return false; // already warned + } + + // If empty, don't do anything; callers must only use output_encoding if + // output_description is not empty. + if (!output_description.empty()) { + // Validate, but also create the profile (only needs to happen once). + JxlColorEncoding output_encoding_external; + if (!ParseDescription(output_description, &output_encoding_external)) { + JXL_WARNING("Unrecognized output_description %s, try RGB_D65_SRG_Rel_Lin", + output_description.c_str()); + return false; // already warned + } + JXL_RETURN_IF_ERROR(jxl::ConvertExternalToInternalColorEncoding( + output_encoding_external, &output_encoding)); + JXL_RETURN_IF_ERROR(output_encoding.CreateICC()); + } + + JXL_RETURN_IF_ERROR(ValidateArgsJxlCodec(this)); + + if (print_details_csv) print_details = true; + + if (override_bitdepth > 32) { + return JXL_FAILURE("override_bitdepth must be <= 32"); + } + + if (!color_hints_string.empty()) { + std::vector<std::string> hints = SplitString(color_hints_string, ','); + for (const auto& hint : hints) { + std::vector<std::string> kv = SplitString(hint, '='); + if (kv.size() != 2) { + return JXL_FAILURE( + "dec-hints key value pairs must have the form 'key=value'"); + } + color_hints.Add(kv[0], kv[1]); + } + } + + return true; +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/benchmark/benchmark_args.h b/media/libjxl/src/tools/benchmark/benchmark_args.h new file mode 100644 index 0000000000..bebc0ac49d --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_args.h @@ -0,0 +1,174 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_ARGS_H_ +#define TOOLS_BENCHMARK_BENCHMARK_ARGS_H_ + +// Command line parsing and arguments for benchmark_xl + +#include <stddef.h> + +#include <algorithm> +#include <deque> +#include <string> +#include <vector> + +#include "lib/extras/dec/color_hints.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/color_encoding_internal.h" +#include "tools/args.h" +#include "tools/cmdline.h" + +namespace jxl { + +std::vector<std::string> SplitString(const std::string& s, char c); + +int ParseIntParam(const std::string& param, int lower_bound, int upper_bound); + +struct BenchmarkArgs { + using OptionId = jpegxl::tools::CommandLineParser::OptionId; + + void AddFlag(bool* field, const char* longName, const char* help, + bool defaultValue) { + const char* noName = RememberString_(std::string("no") + longName); + cmdline.AddOptionFlag('\0', longName, nullptr, field, + &jpegxl::tools::SetBooleanTrue); + cmdline.AddOptionFlag('\0', noName, help, field, + &jpegxl::tools::SetBooleanFalse); + *field = defaultValue; + } + + OptionId AddOverride(Override* field, const char* longName, + const char* help) { + OptionId result = cmdline.AddOptionValue('\0', longName, "0|1", help, field, + &jpegxl::tools::ParseOverride); + *field = Override::kDefault; + return result; + } + + OptionId AddString(std::string* field, const char* longName, const char* help, + const std::string& defaultValue = "") { + OptionId result = cmdline.AddOptionValue( + '\0', longName, "<string>", help, field, &jpegxl::tools::ParseString); + *field = defaultValue; + return result; + } + + OptionId AddFloat(float* field, const char* longName, const char* help, + float defaultValue) { + OptionId result = cmdline.AddOptionValue('\0', longName, "<scalar>", help, + field, &jpegxl::tools::ParseFloat); + *field = defaultValue; + return result; + } + + OptionId AddDouble(double* field, const char* longName, const char* help, + double defaultValue) { + OptionId result = cmdline.AddOptionValue( + '\0', longName, "<scalar>", help, field, &jpegxl::tools::ParseDouble); + *field = defaultValue; + return result; + } + + OptionId AddSigned(int* field, const char* longName, const char* help, + int defaultValue) { + OptionId result = cmdline.AddOptionValue( + '\0', longName, "<integer>", help, field, &jpegxl::tools::ParseSigned); + *field = defaultValue; + return result; + } + + OptionId AddUnsigned(size_t* field, const char* longName, const char* help, + size_t defaultValue) { + OptionId result = + cmdline.AddOptionValue('\0', longName, "<unsigned>", help, field, + &jpegxl::tools::ParseUnsigned); + *field = defaultValue; + return result; + } + + Status AddCommandLineOptions(); + + Status ValidateArgs(); + + bool Parse(int argc, const char** argv) { return cmdline.Parse(argc, argv); } + + void PrintHelp() const { cmdline.PrintHelp(); } + + std::string input; + std::string codec; + bool print_details; + bool print_details_csv; + bool print_more_stats; + bool print_distance_percentiles; + bool silent_errors; + bool save_compressed; + bool save_decompressed; + std::string output_extension; // see CodecFromExtension + std::string output_description; // see ParseDescription + ColorEncoding output_encoding; // determined by output_description + + bool decode_only; + bool skip_butteraugli; + + float intensity_target; + + std::string color_hints_string; + jxl::extras::ColorHints color_hints; + + size_t override_bitdepth; + + double mul_output; + double heatmap_good; + double heatmap_bad; + + bool write_html_report; + bool html_report_self_contained; + bool markdown; + bool more_columns; + + std::string originals_url; + std::string output_dir; + + int num_threads; + int inner_threads; + size_t decode_reps; + size_t encode_reps; + + std::string sample_tmp_dir; + + int num_samples; + int sample_dimensions; + ButteraugliParams ba_params; + + bool profiler; + double error_pnorm; + bool show_progress; + + std::string extra_metrics; + + jpegxl::tools::CommandLineParser cmdline; + + private: + const char* RememberString_(const std::string& text) { + const char* data = text.c_str(); + std::vector<char> copy(data, data + text.size() + 1); + string_pool_.push_back(copy); + return string_pool_.back().data(); + } + + // A memory pool with stable addresses for strings to provide stable + // const char pointers to cmdline.h for dynamic help/name strings. + std::deque<std::vector<char>> string_pool_; +}; + +// Returns singleton +BenchmarkArgs* Args(); + +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_ARGS_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec.cc b/media/libjxl/src/tools/benchmark/benchmark_codec.cc new file mode 100644 index 0000000000..2deb7409dd --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec.cc @@ -0,0 +1,192 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/benchmark/benchmark_codec.h" + +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include <string> +#include <utility> +#include <vector> + +#include "lib/extras/time.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_codec_custom.h" +#ifdef JPEGXL_ENABLE_JPEG +#include "tools/benchmark/benchmark_codec_jpeg.h" +#endif // JPEG_ENABLE_JPEG +#include "tools/benchmark/benchmark_codec_jxl.h" +#include "tools/benchmark/benchmark_codec_png.h" +#include "tools/benchmark/benchmark_stats.h" + +#ifdef BENCHMARK_WEBP +#include "tools/benchmark/benchmark_codec_webp.h" +#endif // BENCHMARK_WEBP + +#ifdef BENCHMARK_AVIF +#include "tools/benchmark/benchmark_codec_avif.h" +#endif // BENCHMARK_AVIF + +namespace jxl { + +void ImageCodec::ParseParameters(const std::string& parameters) { + params_ = parameters; + std::vector<std::string> parts = SplitString(parameters, ':'); + for (size_t i = 0; i < parts.size(); ++i) { + if (!ParseParam(parts[i])) { + JXL_ABORT("Invalid parameter %s", parts[i].c_str()); + } + } +} + +Status ImageCodec::ParseParam(const std::string& param) { + if (param[0] == 'q') { // libjpeg-style quality, [0,100] + const std::string quality_param = param.substr(1); + char* end; + const float q_target = strtof(quality_param.c_str(), &end); + if (end == quality_param.c_str() || + end != quality_param.c_str() + quality_param.size()) { + return false; + } + q_target_ = q_target; + return true; + } + if (param[0] == 'd') { // butteraugli distance + const std::string distance_param = param.substr(1); + char* end; + const float butteraugli_target = strtof(distance_param.c_str(), &end); + if (end == distance_param.c_str() || + end != distance_param.c_str() + distance_param.size()) { + return false; + } + butteraugli_target_ = butteraugli_target; + + // full hf asymmetry at high distance + static const double kHighDistance = 2.5; + + // no hf asymmetry at low distance + static const double kLowDistance = 0.6; + + if (butteraugli_target_ >= kHighDistance) { + ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry; + } else if (butteraugli_target_ >= kLowDistance) { + float w = + (butteraugli_target_ - kLowDistance) / (kHighDistance - kLowDistance); + ba_params_.hf_asymmetry = + args_.ba_params.hf_asymmetry * w + 1.0f * (1.0f - w); + } else { + ba_params_.hf_asymmetry = 1.0f; + } + return true; + } else if (param[0] == 'r') { + butteraugli_target_ = -1.0; + ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry; + bitrate_target_ = strtof(param.substr(1).c_str(), nullptr); + return true; + } + return false; +} + +// Low-overhead "codec" for measuring benchmark overhead. +class NoneCodec : public ImageCodec { + public: + explicit NoneCodec(const BenchmarkArgs& args) : ImageCodec(args) {} + Status ParseParam(const std::string& param) override { return true; } + + Status Compress(const std::string& filename, const CodecInOut* io, + ThreadPoolInternal* pool, PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) override { + PROFILER_ZONE("NoneCompress"); + const double start = Now(); + // Encode image size so we "decompress" something of the same size, as + // required by butteraugli. + const uint32_t xsize = io->xsize(); + const uint32_t ysize = io->ysize(); + compressed->resize(8); + memcpy(compressed->data(), &xsize, 4); + memcpy(compressed->data() + 4, &ysize, 4); + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + return true; + } + + Status Decompress(const std::string& filename, + const Span<const uint8_t> compressed, + ThreadPoolInternal* pool, CodecInOut* io, + jpegxl::tools::SpeedStats* speed_stats) override { + PROFILER_ZONE("NoneDecompress"); + const double start = Now(); + JXL_ASSERT(compressed.size() == 8); + uint32_t xsize, ysize; + memcpy(&xsize, compressed.data(), 4); + memcpy(&ysize, compressed.data() + 4, 4); + Image3F image(xsize, ysize); + ZeroFillImage(&image); + io->metadata.m.SetFloat32Samples(); + io->metadata.m.color_encoding = ColorEncoding::SRGB(); + io->SetFromImage(std::move(image), io->metadata.m.color_encoding); + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + return true; + } + + void GetMoreStats(BenchmarkStats* stats) override {} +}; + +ImageCodecPtr CreateImageCodec(const std::string& description) { + std::string name = description; + std::string parameters = ""; + size_t colon = description.find(':'); + if (colon < description.size()) { + name = description.substr(0, colon); + parameters = description.substr(colon + 1); + } + ImageCodecPtr result; + if (name == "jxl") { + result.reset(CreateNewJxlCodec(*Args())); +#if !defined(__wasm__) + } else if (name == "custom") { + result.reset(CreateNewCustomCodec(*Args())); +#endif +#ifdef JPEGXL_ENABLE_JPEG + } else if (name == "jpeg") { + result.reset(CreateNewJPEGCodec(*Args())); +#endif // BENCHMARK_JPEG +#if JPEGXL_ENABLE_APNG + } else if (name == "png") { + result.reset(CreateNewPNGCodec(*Args())); +#endif + } else if (name == "none") { + result.reset(new NoneCodec(*Args())); +#ifdef BENCHMARK_WEBP + } else if (name == "webp") { + result.reset(CreateNewWebPCodec(*Args())); +#endif // BENCHMARK_WEBP +#ifdef BENCHMARK_AVIF + } else if (name == "avif") { + result.reset(CreateNewAvifCodec(*Args())); +#endif // BENCHMARK_AVIF + } else { + JXL_ABORT("Unknown image codec: %s", name.c_str()); + } + result->set_description(description); + if (!parameters.empty()) result->ParseParameters(parameters); + return result; +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec.h b/media/libjxl/src/tools/benchmark/benchmark_codec.h new file mode 100644 index 0000000000..abc5c1a618 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec.h @@ -0,0 +1,102 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_H_ +#define TOOLS_BENCHMARK_BENCHMARK_CODEC_H_ + +#include <stdint.h> + +#include <deque> +#include <string> +#include <vector> + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/image.h" +#include "tools/args.h" +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_stats.h" +#include "tools/cmdline.h" +#include "tools/speed_stats.h" + +namespace jxl { + +// Thread-compatible. +class ImageCodec { + public: + explicit ImageCodec(const BenchmarkArgs& args) + : args_(args), + butteraugli_target_(1.0f), + q_target_(100.0f), + bitrate_target_(0.0f) {} + + virtual ~ImageCodec() = default; + + void set_description(const std::string& desc) { description_ = desc; } + const std::string& description() const { return description_; } + + const ButteraugliParams& BaParams() const { return ba_params_; } + + virtual void ParseParameters(const std::string& parameters); + + virtual Status ParseParam(const std::string& param); + + // Returns true iff the codec instance (including parameters) can tolerate + // ImageBundle c_current() != metadata()->color_encoding, and the possibility + // of negative (out of gamut) pixel values. + virtual bool IsColorAware() const { return false; } + + // Returns true iff the codec instance (including parameters) will operate + // only with quantized DCT (JPEG) coefficients in input. + virtual bool IsJpegTranscoder() const { return false; } + + virtual Status Compress(const std::string& filename, const CodecInOut* io, + ThreadPoolInternal* pool, PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) = 0; + + virtual Status Decompress(const std::string& filename, + const Span<const uint8_t> compressed, + ThreadPoolInternal* pool, CodecInOut* io, + jpegxl::tools::SpeedStats* speed_stats) = 0; + + virtual void GetMoreStats(BenchmarkStats* stats) {} + + virtual Status CanRecompressJpeg() const { return false; } + virtual Status RecompressJpeg(const std::string& filename, + const std::string& data, + PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) { + return false; + } + + virtual std::string GetErrorMessage() const { return error_message_; } + + protected: + const BenchmarkArgs& args_; + std::string params_; + std::string description_; + float butteraugli_target_; + float q_target_; + float bitrate_target_; + ButteraugliParams ba_params_; + std::string error_message_; +}; + +using ImageCodecPtr = std::unique_ptr<ImageCodec>; + +// Creates an image codec by name, e.g. "jxl" to get a new instance of the +// jxl codec. Optionally, behind a colon, parameters can be specified, +// then ParseParameters of the codec gets called with the part behind the colon. +ImageCodecPtr CreateImageCodec(const std::string& description); + +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc new file mode 100644 index 0000000000..68f47e69a4 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc @@ -0,0 +1,358 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +#include "tools/benchmark/benchmark_codec_avif.h" + +#include <avif/avif.h> + +#include "lib/extras/time.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/enc_external_image.h" +#include "tools/cmdline.h" + +#define JXL_RETURN_IF_AVIF_ERROR(result) \ + do { \ + avifResult jxl_return_if_avif_error_result = (result); \ + if (jxl_return_if_avif_error_result != AVIF_RESULT_OK) { \ + return JXL_FAILURE("libavif error: %s", \ + avifResultToString(jxl_return_if_avif_error_result)); \ + } \ + } while (false) + +namespace jxl { + +namespace { + +struct AvifArgs { + avifPixelFormat chroma_subsampling = AVIF_PIXEL_FORMAT_YUV444; +}; + +AvifArgs* const avifargs = new AvifArgs; + +bool ParseChromaSubsampling(const char* arg, avifPixelFormat* subsampling) { + if (strcmp(arg, "444") == 0) { + *subsampling = AVIF_PIXEL_FORMAT_YUV444; + return true; + } + if (strcmp(arg, "422") == 0) { + *subsampling = AVIF_PIXEL_FORMAT_YUV422; + return true; + } + if (strcmp(arg, "420") == 0) { + *subsampling = AVIF_PIXEL_FORMAT_YUV420; + return true; + } + if (strcmp(arg, "400") == 0) { + *subsampling = AVIF_PIXEL_FORMAT_YUV400; + return true; + } + return false; +} + +void SetUpAvifColor(const ColorEncoding& color, avifImage* const image) { + bool need_icc = color.white_point != WhitePoint::kD65; + + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT709; + if (!color.HasPrimaries()) { + need_icc = true; + } else { + switch (color.primaries) { + case Primaries::kSRGB: + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + break; + case Primaries::k2100: + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT2020; + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT2020_NCL; + break; + default: + need_icc = true; + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNKNOWN; + break; + } + } + + switch (color.tf.GetTransferFunction()) { + case TransferFunction::kSRGB: + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + break; + case TransferFunction::kLinear: + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_LINEAR; + break; + case TransferFunction::kPQ: + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084; + break; + case TransferFunction::kHLG: + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_HLG; + break; + default: + need_icc = true; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNKNOWN; + break; + } + + if (need_icc) { + avifImageSetProfileICC(image, color.ICC().data(), color.ICC().size()); + } +} + +Status ReadAvifColor(const avifImage* const image, ColorEncoding* const color) { + if (image->icc.size != 0) { + PaddedBytes icc; + icc.assign(image->icc.data, image->icc.data + image->icc.size); + return color->SetICC(std::move(icc)); + } + + color->white_point = WhitePoint::kD65; + switch (image->colorPrimaries) { + case AVIF_COLOR_PRIMARIES_BT709: + color->primaries = Primaries::kSRGB; + break; + case AVIF_COLOR_PRIMARIES_BT2020: + color->primaries = Primaries::k2100; + break; + default: + return JXL_FAILURE("unsupported avif primaries"); + } + switch (image->transferCharacteristics) { + case AVIF_TRANSFER_CHARACTERISTICS_BT470M: + JXL_RETURN_IF_ERROR(color->tf.SetGamma(2.2)); + break; + case AVIF_TRANSFER_CHARACTERISTICS_BT470BG: + JXL_RETURN_IF_ERROR(color->tf.SetGamma(2.8)); + break; + case AVIF_TRANSFER_CHARACTERISTICS_LINEAR: + color->tf.SetTransferFunction(TransferFunction::kLinear); + break; + case AVIF_TRANSFER_CHARACTERISTICS_SRGB: + color->tf.SetTransferFunction(TransferFunction::kSRGB); + break; + case AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084: + color->tf.SetTransferFunction(TransferFunction::kPQ); + break; + case AVIF_TRANSFER_CHARACTERISTICS_HLG: + color->tf.SetTransferFunction(TransferFunction::kHLG); + break; + default: + return JXL_FAILURE("unsupported avif TRC"); + } + return color->CreateICC(); +} + +} // namespace + +Status AddCommandLineOptionsAvifCodec(BenchmarkArgs* args) { + args->cmdline.AddOptionValue( + '\0', "avif_chroma_subsampling", "444/422/420/400", + "default AVIF chroma subsampling (default: 444).", + &avifargs->chroma_subsampling, &ParseChromaSubsampling); + return true; +} + +class AvifCodec : public ImageCodec { + public: + explicit AvifCodec(const BenchmarkArgs& args) : ImageCodec(args) { + chroma_subsampling_ = avifargs->chroma_subsampling; + } + + Status ParseParam(const std::string& param) override { + if (param.compare(0, 3, "yuv") == 0) { + if (param.size() != 6) return false; + return ParseChromaSubsampling(param.c_str() + 3, &chroma_subsampling_); + } + if (param.compare(0, 10, "log2_cols=") == 0) { + log2_cols = strtol(param.c_str() + 10, nullptr, 10); + return true; + } + if (param.compare(0, 10, "log2_rows=") == 0) { + log2_rows = strtol(param.c_str() + 10, nullptr, 10); + return true; + } + if (param[0] == 's') { + speed_ = strtol(param.c_str() + 1, nullptr, 10); + return true; + } + if (param == "aomenc") { + encoder_ = AVIF_CODEC_CHOICE_AOM; + return true; + } + if (param == "aomdec") { + decoder_ = AVIF_CODEC_CHOICE_AOM; + return true; + } + if (param == "aom") { + encoder_ = AVIF_CODEC_CHOICE_AOM; + decoder_ = AVIF_CODEC_CHOICE_AOM; + return true; + } + if (param == "rav1e") { + encoder_ = AVIF_CODEC_CHOICE_RAV1E; + return true; + } + if (param == "dav1d") { + decoder_ = AVIF_CODEC_CHOICE_DAV1D; + return true; + } + if (param.compare(0, 2, "a=") == 0) { + std::string subparam = param.substr(2); + size_t pos = subparam.find('='); + if (pos == std::string::npos) { + codec_specific_options_.emplace_back(subparam, ""); + } else { + std::string key = subparam.substr(0, pos); + std::string value = subparam.substr(pos + 1); + codec_specific_options_.emplace_back(key, value); + } + return true; + } + return ImageCodec::ParseParam(param); + } + + Status Compress(const std::string& filename, const CodecInOut* io, + ThreadPoolInternal* pool, PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) override { + double elapsed_convert_image = 0; + const double start = Now(); + { + const auto depth = + std::min<int>(16, io->metadata.m.bit_depth.bits_per_sample); + std::unique_ptr<avifEncoder, void (*)(avifEncoder*)> encoder( + avifEncoderCreate(), &avifEncoderDestroy); + encoder->codecChoice = encoder_; + // TODO(sboukortt): configure this separately. + encoder->minQuantizer = 0; + encoder->maxQuantizer = 63; + encoder->tileColsLog2 = log2_cols; + encoder->tileRowsLog2 = log2_rows; + encoder->speed = speed_; + encoder->maxThreads = pool->NumThreads(); + for (const auto& opts : codec_specific_options_) { + avifEncoderSetCodecSpecificOption(encoder.get(), opts.first.c_str(), + opts.second.c_str()); + } + avifAddImageFlags add_image_flags = AVIF_ADD_IMAGE_FLAG_SINGLE; + if (io->metadata.m.have_animation) { + encoder->timescale = std::lround( + static_cast<float>(io->metadata.m.animation.tps_numerator) / + io->metadata.m.animation.tps_denominator); + add_image_flags = AVIF_ADD_IMAGE_FLAG_NONE; + } + for (const ImageBundle& ib : io->frames) { + std::unique_ptr<avifImage, void (*)(avifImage*)> image( + avifImageCreate(ib.xsize(), ib.ysize(), depth, chroma_subsampling_), + &avifImageDestroy); + image->width = ib.xsize(); + image->height = ib.ysize(); + image->depth = depth; + SetUpAvifColor(ib.c_current(), image.get()); + std::unique_ptr<avifRWData, void (*)(avifRWData*)> icc_freer( + &image->icc, &avifRWDataFree); + avifRGBImage rgb_image; + avifRGBImageSetDefaults(&rgb_image, image.get()); + rgb_image.format = + ib.HasAlpha() ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + avifRGBImageAllocatePixels(&rgb_image); + std::unique_ptr<avifRGBImage, void (*)(avifRGBImage*)> pixels_freer( + &rgb_image, &avifRGBImageFreePixels); + const double start_convert_image = Now(); + JXL_RETURN_IF_ERROR(ConvertToExternal( + ib, depth, /*float_out=*/false, + /*num_channels=*/ib.HasAlpha() ? 4 : 3, JXL_NATIVE_ENDIAN, + /*stride=*/rgb_image.rowBytes, pool, rgb_image.pixels, + rgb_image.rowBytes * rgb_image.height, + /*out_callback=*/{}, jxl::Orientation::kIdentity)); + const double end_convert_image = Now(); + elapsed_convert_image += end_convert_image - start_convert_image; + JXL_RETURN_IF_AVIF_ERROR(avifImageRGBToYUV(image.get(), &rgb_image)); + JXL_RETURN_IF_AVIF_ERROR(avifEncoderAddImage( + encoder.get(), image.get(), ib.duration, add_image_flags)); + } + avifRWData buffer = AVIF_DATA_EMPTY; + JXL_RETURN_IF_AVIF_ERROR(avifEncoderFinish(encoder.get(), &buffer)); + compressed->assign(buffer.data, buffer.data + buffer.size); + avifRWDataFree(&buffer); + } + const double end = Now(); + speed_stats->NotifyElapsed(end - start - elapsed_convert_image); + return true; + } + + Status Decompress(const std::string& filename, + const Span<const uint8_t> compressed, + ThreadPoolInternal* pool, CodecInOut* io, + jpegxl::tools::SpeedStats* speed_stats) override { + io->frames.clear(); + io->dec_pixels = 0; + double elapsed_convert_image = 0; + const double start = Now(); + { + std::unique_ptr<avifDecoder, void (*)(avifDecoder*)> decoder( + avifDecoderCreate(), &avifDecoderDestroy); + decoder->codecChoice = decoder_; + decoder->maxThreads = pool->NumThreads(); + JXL_RETURN_IF_AVIF_ERROR(avifDecoderSetIOMemory( + decoder.get(), compressed.data(), compressed.size())); + JXL_RETURN_IF_AVIF_ERROR(avifDecoderParse(decoder.get())); + const bool has_alpha = decoder->alphaPresent; + io->metadata.m.have_animation = decoder->imageCount > 1; + io->metadata.m.animation.tps_numerator = decoder->timescale; + io->metadata.m.animation.tps_denominator = 1; + io->metadata.m.SetUintSamples(decoder->image->depth); + io->SetSize(decoder->image->width, decoder->image->height); + avifResult next_image; + while ((next_image = avifDecoderNextImage(decoder.get())) == + AVIF_RESULT_OK) { + ColorEncoding color; + JXL_RETURN_IF_ERROR(ReadAvifColor(decoder->image, &color)); + avifRGBImage rgb_image; + avifRGBImageSetDefaults(&rgb_image, decoder->image); + rgb_image.format = + has_alpha ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + avifRGBImageAllocatePixels(&rgb_image); + std::unique_ptr<avifRGBImage, void (*)(avifRGBImage*)> pixels_freer( + &rgb_image, &avifRGBImageFreePixels); + JXL_RETURN_IF_AVIF_ERROR(avifImageYUVToRGB(decoder->image, &rgb_image)); + const double start_convert_image = Now(); + { + ImageBundle ib(&io->metadata.m); + JXL_RETURN_IF_ERROR(ConvertFromExternal( + Span<const uint8_t>(rgb_image.pixels, + rgb_image.height * rgb_image.rowBytes), + rgb_image.width, rgb_image.height, color, (has_alpha ? 4 : 3), + /*alpha_is_premultiplied=*/false, rgb_image.depth, + JXL_NATIVE_ENDIAN, /*flipped_y=*/false, pool, &ib, + /*float_in=*/false, /*align=*/0)); + io->frames.push_back(std::move(ib)); + io->dec_pixels += rgb_image.width * rgb_image.height; + } + const double end_convert_image = Now(); + elapsed_convert_image += end_convert_image - start_convert_image; + } + if (next_image != AVIF_RESULT_NO_IMAGES_REMAINING) { + JXL_RETURN_IF_AVIF_ERROR(next_image); + } + } + const double end = Now(); + speed_stats->NotifyElapsed(end - start - elapsed_convert_image); + return true; + } + + protected: + avifPixelFormat chroma_subsampling_; + avifCodecChoice encoder_ = AVIF_CODEC_CHOICE_AUTO; + avifCodecChoice decoder_ = AVIF_CODEC_CHOICE_AUTO; + int speed_ = AVIF_SPEED_DEFAULT; + int log2_cols = 0; + int log2_rows = 0; + std::vector<std::pair<std::string, std::string>> codec_specific_options_; +}; + +ImageCodec* CreateNewAvifCodec(const BenchmarkArgs& args) { + return new AvifCodec(args); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_avif.h b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.h new file mode 100644 index 0000000000..b3dc38e97e --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.h @@ -0,0 +1,20 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_AVIF_H_ +#define TOOLS_BENCHMARK_BENCHMARK_CODEC_AVIF_H_ + +#include "lib/jxl/base/status.h" +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_codec.h" + +namespace jxl { +ImageCodec* CreateNewAvifCodec(const BenchmarkArgs& args); + +// Registers the avif-specific command line options. +Status AddCommandLineOptionsAvifCodec(BenchmarkArgs* args); +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_AVIF_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc new file mode 100644 index 0000000000..a93fd50193 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc @@ -0,0 +1,163 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/benchmark/benchmark_codec_custom.h" + +// Not supported on Windows due to Linux-specific functions. +#ifndef _WIN32 + +#include <libgen.h> + +#include <fstream> + +#include "lib/extras/codec.h" +#include "lib/extras/enc/apng.h" +#include "lib/extras/time.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/image_bundle.h" +#include "tools/benchmark/benchmark_utils.h" + +namespace jxl { +namespace { + +std::string GetBaseName(std::string filename) { + std::string result = std::move(filename); + result = basename(&result[0]); + const size_t dot = result.rfind('.'); + if (dot != std::string::npos) { + result.resize(dot); + } + return result; +} + +// This uses `output_filename` to determine the name of the corresponding +// `.time` file. +template <typename F> +Status ReportCodecRunningTime(F&& function, std::string output_filename, + jpegxl::tools::SpeedStats* const speed_stats) { + const double start = Now(); + JXL_RETURN_IF_ERROR(function()); + const double end = Now(); + const std::string time_filename = + GetBaseName(std::move(output_filename)) + ".time"; + std::ifstream time_stream(time_filename); + double time; + if (time_stream >> time) { + // Report the time measured by the external codec itself. + speed_stats->NotifyElapsed(time); + } else { + // Fall back to the less accurate time that we measured. + speed_stats->NotifyElapsed(end - start); + } + if (time_stream.is_open()) { + remove(time_filename.c_str()); + } + return true; +} + +class CustomCodec : public ImageCodec { + public: + explicit CustomCodec(const BenchmarkArgs& args) : ImageCodec(args) {} + + Status ParseParam(const std::string& param) override { + switch (param_index_) { + case 0: + extension_ = param; + break; + + case 1: + compress_command_ = param; + break; + + case 2: + decompress_command_ = param; + break; + + default: + compress_args_.push_back(param); + break; + } + ++param_index_; + return true; + } + + Status Compress(const std::string& filename, const CodecInOut* io, + ThreadPoolInternal* pool, PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) override { + JXL_RETURN_IF_ERROR(param_index_ > 2); + + const std::string basename = GetBaseName(filename); + TemporaryFile png_file(basename, "png"), encoded_file(basename, extension_); + std::string png_filename, encoded_filename; + JXL_RETURN_IF_ERROR(png_file.GetFileName(&png_filename)); + JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename)); + saved_intensity_target_ = io->metadata.m.IntensityTarget(); + + const size_t bits = io->metadata.m.bit_depth.bits_per_sample; + PaddedBytes png; + JXL_RETURN_IF_ERROR( + extras::EncodeImageAPNG(io, io->Main().c_current(), bits, pool, &png)); + JXL_RETURN_IF_ERROR(WriteFile(png, png_filename)); + std::vector<std::string> arguments = compress_args_; + arguments.push_back(png_filename); + arguments.push_back(encoded_filename); + JXL_RETURN_IF_ERROR(ReportCodecRunningTime( + [&, this] { return RunCommand(compress_command_, arguments); }, + encoded_filename, speed_stats)); + return ReadFile(encoded_filename, compressed); + } + + Status Decompress(const std::string& filename, + const Span<const uint8_t> compressed, + ThreadPoolInternal* pool, CodecInOut* io, + jpegxl::tools::SpeedStats* speed_stats) override { + const std::string basename = GetBaseName(filename); + TemporaryFile encoded_file(basename, extension_), png_file(basename, "png"); + std::string encoded_filename, png_filename; + JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename)); + JXL_RETURN_IF_ERROR(png_file.GetFileName(&png_filename)); + + JXL_RETURN_IF_ERROR(WriteFile(compressed, encoded_filename)); + JXL_RETURN_IF_ERROR(ReportCodecRunningTime( + [&, this] { + return RunCommand( + decompress_command_, + std::vector<std::string>{encoded_filename, png_filename}); + }, + png_filename, speed_stats)); + JXL_RETURN_IF_ERROR( + SetFromFile(png_filename, extras::ColorHints(), io, pool)); + io->metadata.m.SetIntensityTarget(saved_intensity_target_); + return true; + } + + private: + std::string extension_; + std::string compress_command_; + std::string decompress_command_; + std::vector<std::string> compress_args_; + int param_index_ = 0; + int saved_intensity_target_ = 255; +}; + +} // namespace + +ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args) { + return new CustomCodec(args); +} + +} // namespace jxl + +#else + +namespace jxl { + +ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args) { return nullptr; } + +} // namespace jxl + +#endif // _MSC_VER diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_custom.h b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.h new file mode 100644 index 0000000000..b2711cd5cc --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.h @@ -0,0 +1,46 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_CUSTOM_H_ +#define TOOLS_BENCHMARK_BENCHMARK_CODEC_CUSTOM_H_ + +// This is a benchmark codec that can be used with any command-line +// encoder/decoder that satisfies the following conditions: +// +// - the encoder can read from a PNG file `$input.png` and write the encoded +// image to `$encoded.$ext` if it is called as: +// +// $encoder [OPTIONS] $input.png $encoded.$ext +// +// - the decoder can read from an encoded file `$encoded.$ext` and write to a +// PNG file `$decoded.png` if it is called as: +// +// $decoder $encoded.$ext $decoded.png +// +// On the benchmark command line, the codec must be specified as: +// +// custom:$ext:$encoder:$decoder:$options +// +// Where the options are also separated by colons. +// +// An example with JPEG XL itself would be: +// +// custom:jxl:cjxl:djxl:--distance:3 +// +// Optionally, to have encoding and decoding speed reported, the codec may write +// the number of seconds (as a floating point number) elapsed during actual +// encoding/decoding to $encoded.time and $decoded.time, respectively (replacing +// the .$ext and .png extensions). + +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_codec.h" + +namespace jxl { + +ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args); + +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_CUSTOM_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc new file mode 100644 index 0000000000..78b304e832 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc @@ -0,0 +1,125 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +#include "tools/benchmark/benchmark_codec_jpeg.h" + +#include <stddef.h> +#include <stdio.h> +// After stddef/stdio +#include <stdint.h> +#include <string.h> + +#include <numeric> // partial_sum +#include <string> + +#include "lib/extras/dec/jpg.h" +#include "lib/extras/enc/jpg.h" +#include "lib/extras/packed_image.h" +#include "lib/extras/packed_image_convert.h" +#include "lib/extras/time.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "tools/cmdline.h" + +using jxl::extras::JpegEncoder; + +namespace jxl { + +namespace { + +struct JPEGArgs { + JpegEncoder encoder = JpegEncoder::kLibJpeg; + YCbCrChromaSubsampling chroma_subsampling; +}; + +JPEGArgs* const jpegargs = new JPEGArgs; + +bool ParseChromaSubsampling(const char* param, + YCbCrChromaSubsampling* subsampling) { + std::vector<std::pair< + std::string, std::pair<std::array<uint8_t, 3>, std::array<uint8_t, 3>>>> + options = {{"444", {{{1, 1, 1}}, {{1, 1, 1}}}}, + {"420", {{{2, 1, 1}}, {{2, 1, 1}}}}, + {"422", {{{2, 1, 1}}, {{1, 1, 1}}}}, + {"440", {{{1, 1, 1}}, {{2, 1, 1}}}}}; + for (const auto& option : options) { + if (param == option.first) { + JXL_CHECK(subsampling->Set(option.second.first.data(), + option.second.second.data())); + return true; + } + } + return false; +} + +} // namespace + +Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args) { + args->cmdline.AddOptionValue( + '\0', "chroma_subsampling", "444/422/420/411", + "default JPEG chroma subsampling (default: 444).", + &jpegargs->chroma_subsampling, &ParseChromaSubsampling); + return true; +} + +class JPEGCodec : public ImageCodec { + public: + explicit JPEGCodec(const BenchmarkArgs& args) : ImageCodec(args) { + encoder_ = jpegargs->encoder; + chroma_subsampling_ = jpegargs->chroma_subsampling; + } + + Status ParseParam(const std::string& param) override { + if (ImageCodec::ParseParam(param)) { + return true; + } + if (param == "sjpeg") { + encoder_ = JpegEncoder::kSJpeg; + return true; + } + if (param.compare(0, 3, "yuv") == 0) { + if (param.size() != 6) return false; + return ParseChromaSubsampling(param.c_str() + 3, &chroma_subsampling_); + } + return false; + } + + Status Compress(const std::string& filename, const CodecInOut* io, + ThreadPoolInternal* pool, PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) override { + const double start = Now(); + JXL_RETURN_IF_ERROR(EncodeImageJPG(io, encoder_, + static_cast<int>(std::round(q_target_)), + chroma_subsampling_, pool, compressed)); + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + return true; + } + + Status Decompress(const std::string& filename, + const Span<const uint8_t> compressed, + ThreadPoolInternal* pool, CodecInOut* io, + jpegxl::tools::SpeedStats* speed_stats) override { + extras::PackedPixelFile ppf; + const double start = Now(); + JXL_RETURN_IF_ERROR(DecodeImageJPG(compressed, extras::ColorHints(), + SizeConstraints(), &ppf)); + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); + return true; + } + + protected: + JpegEncoder encoder_; + YCbCrChromaSubsampling chroma_subsampling_; +}; + +ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args) { + return new JPEGCodec(args); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.h b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.h new file mode 100644 index 0000000000..cd4b009a78 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.h @@ -0,0 +1,20 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_JPEG_H_ +#define TOOLS_BENCHMARK_BENCHMARK_CODEC_JPEG_H_ + +#include "lib/jxl/base/status.h" +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_codec.h" + +namespace jxl { +ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args); + +// Registers the jpeg-specific command line options. +Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args); +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_JPEG_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc new file mode 100644 index 0000000000..6dd1ad016e --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc @@ -0,0 +1,405 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +#include "tools/benchmark/benchmark_codec_jxl.h" + +#include <cstdint> +#include <cstdlib> +#include <functional> +#include <sstream> +#include <string> +#include <utility> +#include <vector> + +#include "jxl/decode_cxx.h" +#include "jxl/thread_parallel_runner_cxx.h" +#include "lib/extras/codec.h" +#include "lib/extras/time.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_metadata.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "tools/benchmark/benchmark_file_io.h" +#include "tools/benchmark/benchmark_stats.h" +#include "tools/cmdline.h" + +namespace jxl { + +// Output function for EncodeBrunsli. +size_t OutputToBytes(void* data, const uint8_t* buf, size_t count) { + PaddedBytes* output = reinterpret_cast<PaddedBytes*>(data); + output->append(buf, buf + count); + return count; +} + +struct JxlArgs { + double xmul; + double quant_bias; + + bool use_ac_strategy; + bool qprogressive; // progressive with shift-quantization. + bool progressive; + int progressive_dc; + + Override noise; + Override dots; + Override patches; + + std::string debug_image_dir; +}; + +static JxlArgs* const jxlargs = new JxlArgs; + +Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args) { + args->AddDouble(&jxlargs->xmul, "xmul", + "Multiplier for the difference in X channel in Butteraugli.", + 1.0); + args->AddDouble(&jxlargs->quant_bias, "quant_bias", + "Bias border pixels during quantization by this ratio.", 0.0); + args->AddFlag(&jxlargs->use_ac_strategy, "use_ac_strategy", + "If true, AC strategy will be used.", false); + args->AddFlag(&jxlargs->qprogressive, "qprogressive", + "Enable quantized progressive mode for AC.", false); + args->AddFlag(&jxlargs->progressive, "progressive", + "Enable progressive mode for AC.", false); + args->AddSigned(&jxlargs->progressive_dc, "progressive_dc", + "Enable progressive mode for DC.", -1); + + args->AddOverride(&jxlargs->noise, "noise", + "Enable(1)/disable(0) noise generation."); + args->AddOverride(&jxlargs->dots, "dots", + "Enable(1)/disable(0) dots generation."); + args->AddOverride(&jxlargs->patches, "patches", + "Enable(1)/disable(0) patch dictionary."); + + args->AddString( + &jxlargs->debug_image_dir, "debug_image_dir", + "If not empty, saves debug images for each " + "input image and each codec that provides it to this directory."); + + return true; +} + +Status ValidateArgsJxlCodec(BenchmarkArgs* args) { return true; } + +class JxlCodec : public ImageCodec { + public: + explicit JxlCodec(const BenchmarkArgs& args) : ImageCodec(args) {} + + Status ParseParam(const std::string& param) override { + const std::string kMaxPassesPrefix = "max_passes="; + const std::string kDownsamplingPrefix = "downsampling="; + const std::string kResamplingPrefix = "resampling="; + const std::string kEcResamplingPrefix = "ec_resampling="; + + if (param.substr(0, kResamplingPrefix.size()) == kResamplingPrefix) { + std::istringstream parser(param.substr(kResamplingPrefix.size())); + parser >> cparams_.resampling; + } else if (param.substr(0, kEcResamplingPrefix.size()) == + kEcResamplingPrefix) { + std::istringstream parser(param.substr(kEcResamplingPrefix.size())); + parser >> cparams_.ec_resampling; + } else if (ImageCodec::ParseParam(param)) { + // Nothing to do. + } else if (param == "uint8") { + uint8_ = true; + } else if (param[0] == 'u') { + char* end; + cparams_.uniform_quant = strtof(param.c_str() + 1, &end); + if (end == param.c_str() + 1 || *end != '\0') { + return JXL_FAILURE("failed to parse uniform quant parameter %s", + param.c_str()); + } + ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry; + } else if (param.substr(0, kMaxPassesPrefix.size()) == kMaxPassesPrefix) { + std::istringstream parser(param.substr(kMaxPassesPrefix.size())); + parser >> dparams_.max_passes; + } else if (param.substr(0, kDownsamplingPrefix.size()) == + kDownsamplingPrefix) { + std::istringstream parser(param.substr(kDownsamplingPrefix.size())); + parser >> dparams_.max_downsampling; + } else if (ParseSpeedTier(param, &cparams_.speed_tier)) { + // Nothing to do. + } else if (param[0] == 'X') { + cparams_.channel_colors_pre_transform_percent = + strtol(param.substr(1).c_str(), nullptr, 10); + } else if (param[0] == 'Y') { + cparams_.channel_colors_percent = + strtol(param.substr(1).c_str(), nullptr, 10); + } else if (param[0] == 'p') { + cparams_.palette_colors = strtol(param.substr(1).c_str(), nullptr, 10); + } else if (param == "lp") { + cparams_.lossy_palette = true; + } else if (param[0] == 'C') { + cparams_.colorspace = strtol(param.substr(1).c_str(), nullptr, 10); + } else if (param[0] == 'c') { + cparams_.color_transform = + (jxl::ColorTransform)strtol(param.substr(1).c_str(), nullptr, 10); + has_ctransform_ = true; + } else if (param[0] == 'I') { + cparams_.options.nb_repeats = strtof(param.substr(1).c_str(), nullptr); + } else if (param[0] == 'E') { + cparams_.options.max_properties = + strtof(param.substr(1).c_str(), nullptr); + } else if (param[0] == 'P') { + cparams_.options.predictor = + static_cast<Predictor>(strtof(param.substr(1).c_str(), nullptr)); + } else if (param == "slow") { + cparams_.options.nb_repeats = 2; + } else if (param == "R") { + cparams_.responsive = 1; + } else if (param[0] == 'R') { + cparams_.responsive = strtol(param.substr(1).c_str(), nullptr, 10); + } else if (param == "m") { + cparams_.modular_mode = true; + cparams_.color_transform = jxl::ColorTransform::kNone; + } else if (param.substr(0, 3) == "gab") { + long gab = strtol(param.substr(3).c_str(), nullptr, 10); + if (gab != 0 && gab != 1) { + return JXL_FAILURE("Invalid gab value"); + } + cparams_.gaborish = static_cast<Override>(gab); + } else if (param[0] == 'g') { + long gsize = strtol(param.substr(1).c_str(), nullptr, 10); + if (gsize < 0 || gsize > 3) { + return JXL_FAILURE("Invalid group size shift value"); + } + cparams_.modular_group_size_shift = gsize; + } else if (param == "new_heuristics") { + cparams_.use_new_heuristics = true; + } else if (param == "plt") { + cparams_.options.max_properties = 0; + cparams_.options.nb_repeats = 0; + cparams_.options.predictor = Predictor::Zero; + cparams_.responsive = 0; + cparams_.colorspace = 0; + cparams_.channel_colors_pre_transform_percent = 0; + cparams_.channel_colors_percent = 0; + } else if (param.substr(0, 3) == "epf") { + cparams_.epf = strtol(param.substr(3).c_str(), nullptr, 10); + if (cparams_.epf > 3) { + return JXL_FAILURE("Invalid epf value"); + } + } else if (param.substr(0, 16) == "faster_decoding=") { + cparams_.decoding_speed_tier = + strtol(param.substr(16).c_str(), nullptr, 10); + } else { + return JXL_FAILURE("Unrecognized param"); + } + return true; + } + + bool IsColorAware() const override { + // Can't deal with negative values from color space conversion. + if (cparams_.modular_mode) return false; + // Otherwise, input may be in any color space. + return true; + } + + bool IsJpegTranscoder() const override { + // TODO(veluca): figure out when to turn this on. + return false; + } + + Status Compress(const std::string& filename, const CodecInOut* io, + ThreadPoolInternal* pool, PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) override { + if (!jxlargs->debug_image_dir.empty()) { + cinfo_.dump_image = [](const CodecInOut& io, const std::string& path) { + return EncodeToFile(io, path); + }; + cinfo_.debug_prefix = + JoinPath(jxlargs->debug_image_dir, FileBaseName(filename)) + + ".jxl:" + params_ + ".dbg/"; + JXL_RETURN_IF_ERROR(MakeDir(cinfo_.debug_prefix)); + } + cparams_.butteraugli_distance = butteraugli_target_; + cparams_.target_bitrate = bitrate_target_; + + cparams_.dots = jxlargs->dots; + cparams_.patches = jxlargs->patches; + + cparams_.progressive_mode = jxlargs->progressive; + cparams_.qprogressive_mode = jxlargs->qprogressive; + cparams_.progressive_dc = jxlargs->progressive_dc; + + cparams_.noise = jxlargs->noise; + + cparams_.quant_border_bias = static_cast<float>(jxlargs->quant_bias); + cparams_.ba_params.hf_asymmetry = ba_params_.hf_asymmetry; + cparams_.ba_params.xmul = static_cast<float>(jxlargs->xmul); + + if (cparams_.butteraugli_distance > 0.f && + cparams_.color_transform == ColorTransform::kNone && + cparams_.modular_mode && !has_ctransform_) { + cparams_.color_transform = ColorTransform::kXYB; + } + + const double start = Now(); + PassesEncoderState passes_encoder_state; + if (cparams_.use_new_heuristics) { + passes_encoder_state.heuristics = + jxl::make_unique<jxl::FastEncoderHeuristics>(); + } + JXL_RETURN_IF_ERROR(EncodeFile(cparams_, io, &passes_encoder_state, + compressed, GetJxlCms(), &cinfo_, pool)); + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + return true; + } + + Status Decompress(const std::string& filename, + const Span<const uint8_t> compressed, + ThreadPoolInternal* pool, CodecInOut* io, + jpegxl::tools::SpeedStats* speed_stats) override { + io->frames.clear(); + if (dparams_.max_passes != DecompressParams().max_passes || + dparams_.max_downsampling != DecompressParams().max_downsampling) { + // Must use the C++ API to honor non-default dparams. + if (uint8_) { + return JXL_FAILURE( + "trying to use decompress params that are not all available in " + "either decoding API"); + } + const double start = Now(); + JXL_RETURN_IF_ERROR(DecodeFile(dparams_, compressed, io, pool)); + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + return true; + } + + double elapsed_convert_image = 0; + const double start = Now(); + { + std::vector<uint8_t> pixel_data; + PaddedBytes icc_profile; + auto runner = JxlThreadParallelRunnerMake(nullptr, pool->NumThreads()); + auto dec = JxlDecoderMake(nullptr); + // By default, the decoder will undo exif orientation, giving an image + // with identity exif rotation as result. However, the benchmark does + // not undo exif orientation of the originals, and compares against the + // originals, so we must set the option to keep the original orientation + // instead. + JxlDecoderSetKeepOrientation(dec.get(), JXL_TRUE); + JXL_RETURN_IF_ERROR( + JXL_DEC_SUCCESS == + JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | + JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE)); + JxlBasicInfo info{}; + JxlPixelFormat format = {/*num_channels=*/3, + /*data_type=*/JXL_TYPE_FLOAT, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}; + if (uint8_) { + format.data_type = JXL_TYPE_UINT8; + } + JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); + JxlDecoderStatus status; + while ((status = JxlDecoderProcessInput(dec.get())) != JXL_DEC_SUCCESS) { + switch (status) { + case JXL_DEC_ERROR: + return JXL_FAILURE("decoder error"); + case JXL_DEC_NEED_MORE_INPUT: + return JXL_FAILURE("decoder requests more input"); + case JXL_DEC_BASIC_INFO: + JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS == + JxlDecoderGetBasicInfo(dec.get(), &info)); + format.num_channels = info.num_color_channels; + if (info.alpha_bits != 0) { + ++format.num_channels; + io->metadata.m.extra_channel_info.resize(1); + io->metadata.m.extra_channel_info[0].type = + jxl::ExtraChannel::kAlpha; + } + break; + case JXL_DEC_COLOR_ENCODING: { + size_t icc_size; + JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS == + JxlDecoderGetICCProfileSize( + dec.get(), &format, + JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)); + icc_profile.resize(icc_size); + JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS == + JxlDecoderGetColorAsICCProfile( + dec.get(), &format, + JXL_COLOR_PROFILE_TARGET_DATA, + icc_profile.data(), icc_profile.size())); + break; + } + case JXL_DEC_NEED_IMAGE_OUT_BUFFER: { + size_t buffer_size; + JXL_RETURN_IF_ERROR( + JXL_DEC_SUCCESS == + JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)); + JXL_RETURN_IF_ERROR(buffer_size == + info.xsize * info.ysize * format.num_channels * + (uint8_ ? sizeof(uint8_t) : sizeof(float))); + pixel_data.resize(buffer_size); + JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS == + JxlDecoderSetImageOutBuffer(dec.get(), &format, + pixel_data.data(), + buffer_size)); + break; + } + case JXL_DEC_FULL_IMAGE: { + const double start_convert_image = Now(); + { + ColorEncoding color_encoding; + JXL_RETURN_IF_ERROR( + color_encoding.SetICC(PaddedBytes(icc_profile))); + ImageBundle frame(&io->metadata.m); + JXL_RETURN_IF_ERROR(BufferToImageBundle( + format, info.xsize, info.ysize, pixel_data.data(), + pixel_data.size(), pool, color_encoding, &frame)); + io->frames.push_back(std::move(frame)); + io->dec_pixels += info.xsize * info.ysize; + } + const double end_convert_image = Now(); + elapsed_convert_image += end_convert_image - start_convert_image; + break; + } + default: + return JXL_FAILURE("unrecognized status %d", + static_cast<int>(status)); + } + } + } + const double end = Now(); + speed_stats->NotifyElapsed(end - start - elapsed_convert_image); + return true; + } + + void GetMoreStats(BenchmarkStats* stats) override { + JxlStats jxl_stats; + jxl_stats.num_inputs = 1; + jxl_stats.aux_out = cinfo_; + stats->jxl_stats.Assimilate(jxl_stats); + } + + protected: + AuxOut cinfo_; + CompressParams cparams_; + bool has_ctransform_ = false; + DecompressParams dparams_; + bool uint8_ = false; +}; + +ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args) { + return new JxlCodec(args); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.h b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.h new file mode 100644 index 0000000000..12e9fef79a --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.h @@ -0,0 +1,23 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_JXL_H_ +#define TOOLS_BENCHMARK_BENCHMARK_CODEC_JXL_H_ + +#include <string> + +#include "lib/jxl/base/status.h" +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_codec.h" + +namespace jxl { +ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args); + +// Registers the jxl-specific command line options. +Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args); +Status ValidateArgsJxlCodec(BenchmarkArgs* args); +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_JXL_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc new file mode 100644 index 0000000000..e479d7ad24 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc @@ -0,0 +1,75 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#if JPEGXL_ENABLE_APNG + +#include "tools/benchmark/benchmark_codec_png.h" + +#include <stddef.h> +#include <stdint.h> + +#include <string> + +#include "lib/extras/dec/apng.h" +#include "lib/extras/enc/apng.h" +#include "lib/extras/packed_image.h" +#include "lib/extras/packed_image_convert.h" +#include "lib/extras/time.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { + +struct PNGArgs { + // Empty, no PNG-specific args currently. +}; + +static PNGArgs* const pngargs = new PNGArgs; + +Status AddCommandLineOptionsPNGCodec(BenchmarkArgs* args) { return true; } + +// Lossless. +class PNGCodec : public ImageCodec { + public: + explicit PNGCodec(const BenchmarkArgs& args) : ImageCodec(args) {} + + Status ParseParam(const std::string& param) override { return true; } + + Status Compress(const std::string& filename, const CodecInOut* io, + ThreadPoolInternal* pool, PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) override { + const size_t bits = io->metadata.m.bit_depth.bits_per_sample; + const double start = Now(); + JXL_RETURN_IF_ERROR(extras::EncodeImageAPNG(io, io->Main().c_current(), + bits, pool, compressed)); + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + return true; + } + + Status Decompress(const std::string& /*filename*/, + const Span<const uint8_t> compressed, + ThreadPoolInternal* pool, CodecInOut* io, + jpegxl::tools::SpeedStats* speed_stats) override { + extras::PackedPixelFile ppf; + const double start = Now(); + JXL_RETURN_IF_ERROR(extras::DecodeImageAPNG( + compressed, extras::ColorHints(), SizeConstraints(), &ppf)); + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); + return true; + } +}; + +ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args) { + return new PNGCodec(args); +} + +} // namespace jxl + +#endif
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_png.h b/media/libjxl/src/tools/benchmark/benchmark_codec_png.h new file mode 100644 index 0000000000..23d982e17b --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_png.h @@ -0,0 +1,26 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_ +#define TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_ + +#if JPEGXL_ENABLE_APNG + +#include <string> + +#include "lib/jxl/base/status.h" +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_codec.h" + +namespace jxl { +ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args); + +// Registers the png-specific command line options. +Status AddCommandLineOptionsPNGCodec(BenchmarkArgs* args); +} // namespace jxl + +#endif + +#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc new file mode 100644 index 0000000000..376018c364 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc @@ -0,0 +1,280 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +#include "tools/benchmark/benchmark_codec_webp.h" + +#include <stdint.h> +#include <string.h> +#include <webp/decode.h> +#include <webp/encode.h> + +#include <string> +#include <vector> + +#include "lib/extras/time.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/enc_image_bundle.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/sanitizers.h" + +namespace jxl { + +// Sets image data from 8-bit sRGB pixel array in bytes. +// Amount of input bytes per pixel must be: +// (is_gray ? 1 : 3) + (has_alpha ? 1 : 0) +Status FromSRGB(const size_t xsize, const size_t ysize, const bool is_gray, + const bool has_alpha, const bool alpha_is_premultiplied, + const bool is_16bit, const JxlEndianness endianness, + const uint8_t* pixels, const uint8_t* end, ThreadPool* pool, + ImageBundle* ib) { + const ColorEncoding& c = ColorEncoding::SRGB(is_gray); + const size_t bits_per_sample = (is_16bit ? 2 : 1) * kBitsPerByte; + const Span<const uint8_t> span(pixels, end - pixels); + return ConvertFromExternal(span, xsize, ysize, c, + (is_gray ? 1 : 3) + (has_alpha ? 1 : 0), + alpha_is_premultiplied, bits_per_sample, + endianness, /*flipped_y=*/false, pool, ib, + /*float_in=*/false, /*align=*/0); +} + +struct WebPArgs { + // Empty, no WebP-specific args currently. +}; + +static WebPArgs* const webpargs = new WebPArgs; + +Status AddCommandLineOptionsWebPCodec(BenchmarkArgs* args) { return true; } + +class WebPCodec : public ImageCodec { + public: + explicit WebPCodec(const BenchmarkArgs& args) : ImageCodec(args) {} + + Status ParseParam(const std::string& param) override { + // Ensure that the 'q' parameter is not used up by ImageCodec. + if (param[0] == 'q') { + if (near_lossless_) { + near_lossless_quality_ = ParseIntParam(param, 0, 99); + } else { + quality_ = ParseIntParam(param, 1, 100); + } + return true; + } else if (ImageCodec::ParseParam(param)) { + return true; + } else if (param == "ll") { + lossless_ = true; + JXL_CHECK(!near_lossless_); + return true; + } else if (param == "nl") { + near_lossless_ = true; + JXL_CHECK(!lossless_); + return true; + } else if (param[0] == 'm') { + method_ = ParseIntParam(param, 1, 6); + return true; + } + return false; + } + + Status Compress(const std::string& filename, const CodecInOut* io, + ThreadPoolInternal* pool, PaddedBytes* compressed, + jpegxl::tools::SpeedStats* speed_stats) override { + const double start = Now(); + const ImageBundle& ib = io->Main(); + + if (ib.HasAlpha() && ib.metadata()->GetAlphaBits() > 8) { + return JXL_FAILURE("WebP alpha must be 8-bit"); + } + + size_t num_chans = (ib.HasAlpha() ? 4 : 3); + ImageMetadata metadata = io->metadata.m; + ImageBundle store(&metadata); + const ImageBundle* transformed; + const ColorEncoding& c_desired = ColorEncoding::SRGB(false); + JXL_RETURN_IF_ERROR(TransformIfNeeded(ib, c_desired, GetJxlCms(), pool, + &store, &transformed)); + size_t xsize = ib.oriented_xsize(); + size_t ysize = ib.oriented_ysize(); + size_t stride = xsize * num_chans; + PaddedBytes srgb(stride * ysize); + JXL_RETURN_IF_ERROR(ConvertToExternal( + *transformed, 8, /*float_out=*/false, num_chans, JXL_BIG_ENDIAN, stride, + pool, srgb.data(), srgb.size(), + /*out_callback=*/{}, metadata.GetOrientation())); + + if (lossless_ || near_lossless_) { + // The lossless codec does not support 16-bit channels. + // Color models are currently not supported here and the sRGB 8-bit + // conversion causes loss due to clipping. + if (!ib.IsSRGB() || ib.metadata()->bit_depth.bits_per_sample > 8 || + ib.metadata()->bit_depth.exponent_bits_per_sample > 0) { + return JXL_FAILURE("%s: webp:ll/nl requires 8-bit sRGB", + filename.c_str()); + } + JXL_RETURN_IF_ERROR( + CompressInternal(srgb, xsize, ysize, num_chans, 100, compressed)); + } else if (bitrate_target_ > 0.0) { + int quality_bad = 100; + int quality_good = 92; + size_t target_size = xsize * ysize * bitrate_target_ / 8.0; + while (quality_good > 0 && + CompressInternal(srgb, xsize, ysize, num_chans, quality_good, + compressed) && + compressed->size() > target_size) { + quality_bad = quality_good; + quality_good -= 8; + } + if (quality_good <= 0) quality_good = 1; + while (quality_good + 1 < quality_bad) { + int quality = (quality_bad + quality_good) / 2; + if (!CompressInternal(srgb, xsize, ysize, num_chans, quality, + compressed)) { + break; + } + if (compressed->size() <= target_size) { + quality_good = quality; + } else { + quality_bad = quality; + } + } + JXL_RETURN_IF_ERROR(CompressInternal(srgb, xsize, ysize, num_chans, + quality_good, compressed)); + } else if (quality_ > 0) { + JXL_RETURN_IF_ERROR(CompressInternal(srgb, xsize, ysize, num_chans, + quality_, compressed)); + } else { + return false; + } + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + return true; + } + + Status Decompress(const std::string& filename, + const Span<const uint8_t> compressed, + ThreadPoolInternal* pool, CodecInOut* io, + jpegxl::tools::SpeedStats* speed_stats) override { + WebPDecoderConfig config; +#ifdef MEMORY_SANITIZER + // config is initialized by libwebp, which we are not instrumenting with + // msan, therefore we need to initialize it here. + memset(&config, 0, sizeof(config)); +#endif + JXL_RETURN_IF_ERROR(WebPInitDecoderConfig(&config) == 1); + config.options.use_threads = 0; + config.options.dithering_strength = 0; + config.options.bypass_filtering = 0; + config.options.no_fancy_upsampling = 0; + WebPDecBuffer* const buf = &config.output; + buf->colorspace = MODE_RGBA; + const uint8_t* webp_data = compressed.data(); + const int webp_size = compressed.size(); + const double start = Now(); + if (WebPDecode(webp_data, webp_size, &config) != VP8_STATUS_OK) { + return JXL_FAILURE("WebPDecode failed"); + } + const double end = Now(); + speed_stats->NotifyElapsed(end - start); + JXL_CHECK(buf->u.RGBA.stride == buf->width * 4); + + const bool is_gray = false; + const bool has_alpha = true; + const uint8_t* data_begin = &buf->u.RGBA.rgba[0]; + const uint8_t* data_end = data_begin + buf->width * buf->height * 4; + // The image data is initialized by libwebp, which we are not instrumenting + // with msan. + msan::UnpoisonMemory(data_begin, data_end - data_begin); + if (io->metadata.m.color_encoding.IsGray() != is_gray) { + // TODO(lode): either ensure is_gray matches what the color profile says, + // or set a correct color profile, e.g. + // io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray); + // Return a standard failure because SetFromSRGB triggers a fatal assert + // for this instead. + return JXL_FAILURE("Color profile is-gray mismatch"); + } + io->metadata.m.SetAlphaBits(8); + const Status ok = + FromSRGB(buf->width, buf->height, is_gray, has_alpha, + /*alpha_is_premultiplied=*/false, /*is_16bit=*/false, + JXL_LITTLE_ENDIAN, data_begin, data_end, pool, &io->Main()); + WebPFreeDecBuffer(buf); + JXL_RETURN_IF_ERROR(ok); + io->dec_pixels = buf->width * buf->height; + return true; + } + + private: + static int WebPStringWrite(const uint8_t* data, size_t data_size, + const WebPPicture* const picture) { + if (data_size) { + PaddedBytes* const out = static_cast<PaddedBytes*>(picture->custom_ptr); + const size_t pos = out->size(); + out->resize(pos + data_size); + memcpy(out->data() + pos, data, data_size); + } + return 1; + } + Status CompressInternal(const PaddedBytes& srgb, size_t xsize, size_t ysize, + size_t num_chans, int quality, + PaddedBytes* compressed) { + *compressed = PaddedBytes(); + WebPConfig config; + WebPConfigInit(&config); + JXL_ASSERT(!lossless_ || !near_lossless_); // can't have both + config.lossless = lossless_; + config.quality = quality; + config.method = method_; +#if WEBP_ENCODER_ABI_VERSION >= 0x020a + config.near_lossless = near_lossless_ ? near_lossless_quality_ : 100; +#else + if (near_lossless_) { + JXL_WARNING("Near lossless not supported by this WebP version"); + } +#endif + JXL_CHECK(WebPValidateConfig(&config)); + + WebPPicture pic; + WebPPictureInit(&pic); + pic.width = static_cast<int>(xsize); + pic.height = static_cast<int>(ysize); + pic.writer = &WebPStringWrite; + if (lossless_ || near_lossless_) pic.use_argb = 1; + pic.custom_ptr = compressed; + + if (num_chans == 3) { + WebPPictureImportRGB(&pic, srgb.data(), 3 * xsize); + } else { + WebPPictureImportRGBA(&pic, srgb.data(), 4 * xsize); + } + + // WebP encoding may fail, for example, if the image is more than 16384 + // pixels high or wide. + bool ok = WebPEncode(&config, &pic); + WebPPictureFree(&pic); + // Compressed image data is initialized by libwebp, which we are not + // instrumenting with msan. + msan::UnpoisonMemory(compressed->data(), compressed->size()); + return ok; + } + + int quality_ = 90; + bool lossless_ = false; + bool near_lossless_ = false; + bool near_lossless_quality_ = 40; // only used if near_lossless_ + int method_ = 6; // smallest, some speed cost +}; + +ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args) { + return new WebPCodec(args); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_webp.h b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.h new file mode 100644 index 0000000000..cd4c60fb5e --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.h @@ -0,0 +1,23 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_WEBP_H_ +#define TOOLS_BENCHMARK_BENCHMARK_CODEC_WEBP_H_ + +// To support webp, install libwebp-dev and rerun cmake. + +#include <string> + +#include "lib/jxl/base/status.h" +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_codec.h" + +namespace jxl { +ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args); + +// Registers the webp-specific command line options. +Status AddCommandLineOptionsWebPCodec(BenchmarkArgs* args); +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_WEBP_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_file_io.cc b/media/libjxl/src/tools/benchmark/benchmark_file_io.cc new file mode 100644 index 0000000000..c5db02b8f1 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_file_io.cc @@ -0,0 +1,232 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +#include "tools/benchmark/benchmark_file_io.h" + +#include <errno.h> +#include <sys/stat.h> + +#include <cstdio> + +#if defined(_WIN32) || defined(_WIN64) +#include "third_party/dirent.h" +#else +#include <dirent.h> +#include <unistd.h> +#endif + +#ifndef HAS_GLOB +#define HAS_GLOB 0 +#if defined __has_include +// <glob.h> is included in previous APIs but glob() function is not defined +// until API 28. +#if __has_include(<glob.h>) && \ + (!defined(__ANDROID_API__) || __ANDROID_API__ >= 28) +#undef HAS_GLOB +#define HAS_GLOB 1 +#endif // __has_include(<glob.h>) +#endif // __has_include +#endif // HAS_GLOB + +#if HAS_GLOB +#include <glob.h> +#endif // HAS_GLOB + +// There is no "user" in embedded filesystems. +#ifndef GLOB_TILDE +#define GLOB_TILDE 0 +#endif + +namespace jxl { + +const char kPathSeparator = '/'; + +// RAII, ensures dir is closed even when returning early. +class DirWrapper { + public: + DirWrapper(const DirWrapper& other) = delete; + DirWrapper& operator=(const DirWrapper& other) = delete; + + explicit DirWrapper(const std::string& pathname) + : dir_(opendir(pathname.c_str())) {} + + ~DirWrapper() { + if (dir_ != nullptr) { + const int err = closedir(dir_); + JXL_CHECK(err == 0); + } + } + + // NOLINTNEXTLINE(google-explicit-constructor) + operator DIR*() const { return dir_; } + + private: + DIR* const dir_; +}; + +// Checks if the file exists, either as file or as directory +bool PathExists(const std::string& fname) { + struct stat s; + if (stat(fname.c_str(), &s) != 0) return false; + return true; +} + +// Checks if the file exists and is a regular file. +bool IsRegularFile(const std::string& fname) { + struct stat s; + if (stat(fname.c_str(), &s) != 0) return false; + return S_ISREG(s.st_mode); +} + +// Checks if the file exists and is a directory. +bool IsDirectory(const std::string& fname) { + struct stat s; + if (stat(fname.c_str(), &s) != 0) return false; + return S_ISDIR(s.st_mode); +} + +// Recursively makes dir, or successfully does nothing if it already exists. +Status MakeDir(const std::string& dirname) { + size_t pos = 0; + for (pos = dirname.size(); pos > 0; pos--) { + if (pos == dirname.size() || dirname[pos] == kPathSeparator) { + // Found existing dir or regular file, break and then start creating + // from here (in the latter case we'll get error below). + if (PathExists(dirname.substr(0, pos + 1))) { + pos += 1; // Skip past this existing path + break; + } + } + } + for (; pos <= dirname.size(); pos++) { + if (pos == dirname.size() || dirname[pos] == kPathSeparator) { + std::string subdir = dirname.substr(0, pos + 1); + if (mkdir(subdir.c_str(), 0777) && errno != EEXIST) { + return JXL_FAILURE("Failed to create directory"); + } + } + } + if (!IsDirectory(dirname)) return JXL_FAILURE("Failed to create directory"); + return true; // success +} + +Status DeleteFile(const std::string& fname) { + if (!IsRegularFile(fname)) { + return JXL_FAILURE("Trying to delete non-regular file"); + } + if (std::remove(fname.c_str())) return JXL_FAILURE("Failed to delete file"); + return true; +} + +std::string FileBaseName(const std::string& fname) { + size_t pos = fname.rfind('/'); + if (pos == std::string::npos) return fname; + return fname.substr(pos + 1); +} + +std::string FileDirName(const std::string& fname) { + size_t pos = fname.rfind('/'); + if (pos == std::string::npos) return ""; + return fname.substr(0, pos); +} + +std::string FileExtension(const std::string& fname) { + size_t pos = fname.rfind('.'); + if (pos == std::string::npos) return ""; + return fname.substr(pos); +} + +std::string JoinPath(const std::string& first, const std::string& second) { + JXL_CHECK(second.empty() || second[0] != kPathSeparator); + return (!first.empty() && first.back() == kPathSeparator) + ? (first + second) + : (first + kPathSeparator + second); +} + +// Can match a single file, or multiple files in a directory (non-recursive). +// With POSIX, supports glob(), otherwise supports a subset. +Status MatchFiles(const std::string& pattern, std::vector<std::string>* list) { +#if HAS_GLOB + glob_t g; + memset(&g, 0, sizeof(g)); + int error = glob(pattern.c_str(), GLOB_TILDE, NULL, &g); + if (!error) { + for (size_t i = 0; i < g.gl_pathc; ++i) { + list->push_back(g.gl_pathv[i]); + } + } + globfree(&g); + if (error) return JXL_FAILURE("glob failed for %s", pattern.c_str()); + return true; +#else + std::string dirname = FileDirName(pattern); + std::string basename = FileBaseName(pattern); + size_t pos0 = basename.find('*'); + size_t pos1 = pos0 == std::string::npos ? pos0 : basename.find('*', pos0 + 1); + std::string prefix, middle, suffix; + if (pos0 != std::string::npos) { + prefix = basename.substr(0, pos0); + if (pos1 != std::string::npos) { + middle = basename.substr(pos0 + 1, pos1 - pos0 - 1); + suffix = basename.substr(pos1 + 1); + } else { + suffix = basename.substr(pos0 + 1); + } + } + + if (prefix.find_first_of("*?[") != std::string::npos || + middle.find_first_of("*?[") != std::string::npos || + suffix.find_first_of("*?[") != std::string::npos || + dirname.find_first_of("*?[") != std::string::npos) { + return JXL_FAILURE( + "Only glob patterns with max two '*' in the basename" + " are supported, e.g. directory/path/*.png or" + " /directory/path/*heatmap*"); + } + + if (pos0 != std::string::npos) { + DirWrapper dir(dirname); + if (!dir) return JXL_FAILURE("directory %s doesn't exist", dirname.c_str()); + for (;;) { + dirent* ent = readdir(dir); + if (!ent) break; + std::string name = ent->d_name; + // If there was a suffix, only add if it matches (e.g. ".png") + bool matches = + name.size() >= (prefix.size() + middle.size() + suffix.size()); + if (matches) { + if (!prefix.empty() && name.substr(0, prefix.size()) != prefix) { + matches = false; + } + if (!middle.empty()) { + size_t pos = name.find(middle, prefix.size()); + if (pos == std::string::npos || + pos + middle.size() > name.size() - suffix.size()) { + matches = false; + } + } + if (!suffix.empty() && + name.substr(name.size() - suffix.size()) != suffix) { + matches = false; + } + } + if (matches) { + std::string path = JoinPath(dirname, name); + + if (IsRegularFile(path)) { + list->push_back(path); + } + } + } + return true; + } + // No *, so a single regular file is intended + if (IsRegularFile(pattern)) { + list->push_back(pattern); + } + return true; +#endif // HAS_GLOB +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/benchmark/benchmark_file_io.h b/media/libjxl/src/tools/benchmark/benchmark_file_io.h new file mode 100644 index 0000000000..ecb83590d6 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_file_io.h @@ -0,0 +1,53 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// File utilities for benchmarking and testing, but which are not needed for +// main jxl itself. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_FILE_IO_H_ +#define TOOLS_BENCHMARK_BENCHMARK_FILE_IO_H_ + +#include <string> +#include <vector> + +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +// Checks if the file exists, either as file or as directory +bool PathExists(const std::string& fname); + +// Checks if the file exists and is a regular file. +bool IsRegularFile(const std::string& fname); + +// Checks if the file exists and is a directory. +bool IsDirectory(const std::string& fname); + +// Recursively makes dir, or successfully does nothing if it already exists. +Status MakeDir(const std::string& dirname); + +// Deletes a single regular file. +Status DeleteFile(const std::string& fname); + +// Returns value similar to unix basename, except it returns empty string if +// fname ends in '/'. +std::string FileBaseName(const std::string& fname); +// Returns value similar to unix dirname, except returns up to before the last +// slash if fname ends in '/'. +std::string FileDirName(const std::string& fname); + +// Returns the part of the filename starting from the last dot, or empty +// string if there is no dot. +std::string FileExtension(const std::string& fname); + +// Matches one or more files given glob pattern. +Status MatchFiles(const std::string& pattern, std::vector<std::string>* list); + +std::string JoinPath(const std::string& first, const std::string& second); + +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_FILE_IO_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_stats.cc b/media/libjxl/src/tools/benchmark/benchmark_stats.cc new file mode 100644 index 0000000000..ef5932aa9d --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_stats.cc @@ -0,0 +1,369 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/benchmark/benchmark_stats.h" + +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> + +#include <algorithm> +#include <cmath> + +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/status.h" +#include "tools/benchmark/benchmark_args.h" + +namespace jxl { +namespace { + +// Computes longest codec name from Args()->codec, for table alignment. +uint32_t ComputeLargestCodecName() { + std::vector<std::string> methods = SplitString(Args()->codec, ','); + size_t max = strlen("Aggregate:"); // Include final row's name + for (const auto& method : methods) { + max = std::max(max, method.size()); + } + return max; +} + +// The benchmark result is a table of heterogeneous data, the column type +// specifies its data type. The type affects how it is printed as well as how +// aggregate values are computed. +enum ColumnType { + // Formatted string + TYPE_STRING, + // Positive size, prints 0 as "---" + TYPE_SIZE, + // Floating point value (double precision) which is interpreted as + // "not applicable" if <= 0, must be strictly positive to be valid but can be + // set to 0 or negative to be printed as "---", for example for a speed that + // is not measured. + TYPE_POSITIVE_FLOAT, + // Counts of some event + TYPE_COUNT, +}; + +struct ColumnDescriptor { + // Column name + std::string label; + // Total width to render the values of this column. If t his is a floating + // point value, make sure this is large enough to contain a space and the + // point, plus precision digits after the point, plus the max amount of + // integer digits you expect in front of the point. + uint32_t width; + // Amount of digits after the point, or 0 if not a floating point value. + uint32_t precision; + ColumnType type; + bool more; // Whether to print only if more_columns is enabled +}; + +static const ColumnDescriptor ExtraMetricDescriptor() { + ColumnDescriptor d{{"DO NOT USE"}, 12, 4, TYPE_POSITIVE_FLOAT, false}; + return d; +} + +// To add or change a column to the benchmark ASCII table output, add/change +// an entry here with table header line 1, table header line 2, width of the +// column, precision after the point in case of floating point, and the +// data type. Then add/change the corresponding formula or formatting in +// the function ComputeColumns. +std::vector<ColumnDescriptor> GetColumnDescriptors(size_t num_extra_metrics) { + // clang-format off + std::vector<ColumnDescriptor> result = { + {{"Encoding"}, ComputeLargestCodecName() + 1, 0, TYPE_STRING, false}, + {{"kPixels"}, 10, 0, TYPE_SIZE, false}, + {{"Bytes"}, 9, 0, TYPE_SIZE, false}, + {{"BPP"}, 13, 7, TYPE_POSITIVE_FLOAT, false}, + {{"E MP/s"}, 8, 3, TYPE_POSITIVE_FLOAT, false}, + {{"D MP/s"}, 8, 3, TYPE_POSITIVE_FLOAT, false}, + {{"Max norm"}, 13, 8, TYPE_POSITIVE_FLOAT, false}, + {{"pnorm"}, 13, 8, TYPE_POSITIVE_FLOAT, false}, + {{"PSNR"}, 7, 2, TYPE_POSITIVE_FLOAT, true}, + {{"QABPP"}, 8, 3, TYPE_POSITIVE_FLOAT, true}, + {{"SmallB"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"DCT4x8"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"AFV"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"DCT8x8"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"8x16"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"8x32"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"16"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"16x32"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"32"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"32x64"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"64"}, 8, 4, TYPE_POSITIVE_FLOAT, true}, + {{"BPP*pnorm"}, 16, 12, TYPE_POSITIVE_FLOAT, false}, + {{"Bugs"}, 7, 5, TYPE_COUNT, false}, + }; + // clang-format on + + for (size_t i = 0; i < num_extra_metrics; i++) { + result.push_back(ExtraMetricDescriptor()); + } + + return result; +} + +// Computes throughput [megapixels/s] as reported in the report table +static double ComputeSpeed(size_t pixels, double time_s) { + if (time_s == 0.0) return 0; + return pixels * 1E-6 / time_s; +} + +static std::string FormatFloat(const ColumnDescriptor& label, double value) { + std::string result = + StringPrintf("%*.*f", label.width - 1, label.precision, value); + + // Reduce precision if the value is too wide for the column. However, keep + // at least one digit to the right of the point, and especially the integer + // digits. + if (result.size() >= label.width) { + size_t point = result.rfind('.'); + if (point != std::string::npos) { + int end = std::max<int>(point + 2, label.width - 1); + result = result.substr(0, end); + } + } + return result; +} + +} // namespace + +std::string StringPrintf(const char* format, ...) { + char buf[2000]; + va_list args; + va_start(args, format); + vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + return std::string(buf); +} + +void BenchmarkStats::Assimilate(const BenchmarkStats& victim) { + total_input_files += victim.total_input_files; + total_input_pixels += victim.total_input_pixels; + total_compressed_size += victim.total_compressed_size; + total_adj_compressed_size += victim.total_adj_compressed_size; + total_time_encode += victim.total_time_encode; + total_time_decode += victim.total_time_decode; + max_distance = std::max(max_distance, victim.max_distance); + distance_p_norm += victim.distance_p_norm; + distance_2 += victim.distance_2; + distances.insert(distances.end(), victim.distances.begin(), + victim.distances.end()); + total_errors += victim.total_errors; + jxl_stats.Assimilate(victim.jxl_stats); + if (extra_metrics.size() < victim.extra_metrics.size()) { + extra_metrics.resize(victim.extra_metrics.size()); + } + for (size_t i = 0; i < victim.extra_metrics.size(); i++) { + extra_metrics[i] += victim.extra_metrics[i]; + } +} + +void BenchmarkStats::PrintMoreStats() const { + if (Args()->print_more_stats) { + jxl_stats.Print(); + } + if (Args()->print_distance_percentiles) { + std::vector<float> sorted = distances; + std::sort(sorted.begin(), sorted.end()); + int p50idx = 0.5 * distances.size(); + int p90idx = 0.9 * distances.size(); + printf("50th/90th percentile distance: %.8f %.8f\n", sorted[p50idx], + sorted[p90idx]); + } +} + +std::vector<ColumnValue> BenchmarkStats::ComputeColumns( + const std::string& codec_desc, size_t corpus_size) const { + JXL_CHECK(total_input_files == corpus_size); + const double comp_bpp = total_compressed_size * 8.0 / total_input_pixels; + const double adj_comp_bpp = + total_adj_compressed_size * 8.0 / total_input_pixels; + // Note: this is not affected by alpha nor bit depth. + const double compression_speed = + ComputeSpeed(total_input_pixels, total_time_encode); + const double decompression_speed = + ComputeSpeed(total_input_pixels, total_time_decode); + // Already weighted, no need to divide by #channels. + const double rmse = std::sqrt(distance_2 / total_input_pixels); + const double psnr = total_compressed_size == 0 ? 0.0 + : (distance_2 == 0) ? 99.99 + : (20 * std::log10(1 / rmse)); + const double p_norm = distance_p_norm / total_input_pixels; + const double bpp_p_norm = p_norm * comp_bpp; + + std::vector<ColumnValue> values( + GetColumnDescriptors(extra_metrics.size()).size()); + + values[0].s = codec_desc; + values[1].i = total_input_pixels / 1000; + values[2].i = total_compressed_size; + values[3].f = comp_bpp; + values[4].f = compression_speed; + values[5].f = decompression_speed; + values[6].f = static_cast<double>(max_distance); + values[7].f = p_norm; + values[8].f = psnr; + values[9].f = adj_comp_bpp; + // The DCT2, DCT4, AFV and DCT4X8 are applied to an 8x8 block by having 4x4 + // DCT2X2s, 2x2 DCT4x4s/AFVs, or 2x1 DCT4X8s, filling the whole 8x8 blocks. + // Thus we need to multiply the block count by 8.0 * 8.0 pixels for these + // transforms. + values[10].f = 100.f * jxl_stats.aux_out.num_small_blocks * 8.0 * 8.0 / + total_input_pixels; + values[11].f = 100.f * jxl_stats.aux_out.num_dct4x8_blocks * 8.0 * 8.0 / + total_input_pixels; + values[12].f = + 100.f * jxl_stats.aux_out.num_afv_blocks * 8.0 * 8.0 / total_input_pixels; + values[13].f = 100.f * jxl_stats.aux_out.num_dct8_blocks * 8.0 * 8.0 / + total_input_pixels; + values[14].f = 100.f * jxl_stats.aux_out.num_dct8x16_blocks * 8.0 * 16.0 / + total_input_pixels; + values[15].f = 100.f * jxl_stats.aux_out.num_dct8x32_blocks * 8.0 * 32.0 / + total_input_pixels; + values[16].f = 100.f * jxl_stats.aux_out.num_dct16_blocks * 16.0 * 16.0 / + total_input_pixels; + values[17].f = 100.f * jxl_stats.aux_out.num_dct16x32_blocks * 16.0 * 32.0 / + total_input_pixels; + values[18].f = 100.f * jxl_stats.aux_out.num_dct32_blocks * 32.0 * 32.0 / + total_input_pixels; + values[19].f = 100.f * jxl_stats.aux_out.num_dct32x64_blocks * 32.0 * 64.0 / + total_input_pixels; + values[20].f = 100.f * jxl_stats.aux_out.num_dct64_blocks * 64.0 * 64.0 / + total_input_pixels; + values[21].f = bpp_p_norm; + values[22].i = total_errors; + for (size_t i = 0; i < extra_metrics.size(); i++) { + values[23 + i].f = extra_metrics[i] / total_input_files; + } + return values; +} + +static std::string PrintFormattedEntries( + size_t num_extra_metrics, const std::vector<ColumnValue>& values) { + const auto& descriptors = GetColumnDescriptors(num_extra_metrics); + + std::string out; + for (size_t i = 0; i < descriptors.size(); i++) { + if (!Args()->more_columns && descriptors[i].more) continue; + std::string value; + if (descriptors[i].type == TYPE_STRING) { + value = values[i].s; + } else if (descriptors[i].type == TYPE_SIZE) { + value = values[i].i ? StringPrintf("%" PRIdS, values[i].i) : "---"; + } else if (descriptors[i].type == TYPE_POSITIVE_FLOAT) { + value = FormatFloat(descriptors[i], values[i].f); + value = FormatFloat(descriptors[i], values[i].f); + } else if (descriptors[i].type == TYPE_COUNT) { + value = StringPrintf("%" PRIdS, values[i].i); + } + + int numspaces = descriptors[i].width - value.size(); + if (numspaces < 1) { + numspaces = 1; + } + // All except the first one are right-aligned, the first one is the name, + // others are numbers with digits matching from the right. + if (i == 0) out += value.c_str(); + out += std::string(numspaces, ' '); + if (i != 0) out += value.c_str(); + } + return out + "\n"; +} + +std::string BenchmarkStats::PrintLine(const std::string& codec_desc, + size_t corpus_size) const { + std::vector<ColumnValue> values = ComputeColumns(codec_desc, corpus_size); + return PrintFormattedEntries(extra_metrics.size(), values); +} + +std::string PrintHeader(const std::vector<std::string>& extra_metrics_names) { + std::string out; + // Extra metrics are handled separately. + const auto& descriptors = GetColumnDescriptors(0); + for (size_t i = 0; i < descriptors.size(); i++) { + if (!Args()->more_columns && descriptors[i].more) continue; + const std::string& label = descriptors[i].label; + int numspaces = descriptors[i].width - label.size(); + // All except the first one are right-aligned. + if (i == 0) out += label.c_str(); + out += std::string(numspaces, ' '); + if (i != 0) out += label.c_str(); + } + for (const std::string& em : extra_metrics_names) { + int numspaces = ExtraMetricDescriptor().width - em.size(); + JXL_CHECK(numspaces >= 1); + out += std::string(numspaces, ' '); + out += em; + } + out += '\n'; + for (const auto& descriptor : descriptors) { + if (!Args()->more_columns && descriptor.more) continue; + out += std::string(descriptor.width, '-'); + } + out += std::string(ExtraMetricDescriptor().width * extra_metrics_names.size(), + '-'); + return out + "\n"; +} + +std::string PrintAggregate( + size_t num_extra_metrics, + const std::vector<std::vector<ColumnValue>>& aggregate) { + const auto& descriptors = GetColumnDescriptors(num_extra_metrics); + + for (size_t i = 0; i < aggregate.size(); i++) { + // Check when statistics has wrong amount of column entries + JXL_CHECK(aggregate[i].size() == descriptors.size()); + } + + std::vector<ColumnValue> result(descriptors.size()); + + // Statistics for the aggregate row are combined together with different + // formulas than Assimilate uses for combining the statistics of files. + for (size_t i = 0; i < descriptors.size(); i++) { + if (descriptors[i].type == TYPE_STRING) { + // "---" for the Iters column since this does not have meaning for + // the aggregate stats. + result[i].s = i == 0 ? "Aggregate:" : "---"; + continue; + } + if (descriptors[i].type == TYPE_COUNT) { + size_t sum = 0; + for (size_t j = 0; j < aggregate.size(); j++) { + sum += aggregate[j][i].i; + } + result[i].i = sum; + continue; + } + + ColumnType type = descriptors[i].type; + + double logsum = 0; + size_t numvalid = 0; + for (size_t j = 0; j < aggregate.size(); j++) { + double value = + (type == TYPE_SIZE) ? aggregate[j][i].i : aggregate[j][i].f; + if (value > 0) { + numvalid++; + logsum += std::log2(value); + } + } + double geomean = numvalid ? std::exp2(logsum / numvalid) : 0.0; + + if (type == TYPE_SIZE || type == TYPE_COUNT) { + result[i].i = static_cast<size_t>(geomean + 0.5); + } else if (type == TYPE_POSITIVE_FLOAT) { + result[i].f = geomean; + } else { + JXL_ABORT("unknown entry type"); + } + } + + return PrintFormattedEntries(num_extra_metrics, result); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/benchmark/benchmark_stats.h b/media/libjxl/src/tools/benchmark/benchmark_stats.h new file mode 100644 index 0000000000..a23c4a1aeb --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_stats.h @@ -0,0 +1,81 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_STATS_H_ +#define TOOLS_BENCHMARK_BENCHMARK_STATS_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <string> +#include <vector> + +#include "lib/jxl/aux_out.h" + +namespace jxl { + +std::string StringPrintf(const char* format, ...); + +struct JxlStats { + JxlStats() { + num_inputs = 0; + aux_out = AuxOut(); + } + void Assimilate(const JxlStats& victim) { + num_inputs += victim.num_inputs; + aux_out.Assimilate(victim.aux_out); + } + void Print() const { aux_out.Print(num_inputs); } + + size_t num_inputs; + AuxOut aux_out; +}; + +// The value of an entry in the table. Depending on the ColumnType, the string, +// size_t or double should be used. +struct ColumnValue { + std::string s; // for TYPE_STRING + size_t i; // for TYPE_SIZE and TYPE_COUNT + double f; // for TYPE_POSITIVE_FLOAT +}; + +struct BenchmarkStats { + void Assimilate(const BenchmarkStats& victim); + + std::vector<ColumnValue> ComputeColumns(const std::string& codec_desc, + size_t corpus_size) const; + + std::string PrintLine(const std::string& codec_desc, + size_t corpus_size) const; + + void PrintMoreStats() const; + + size_t total_input_files = 0; + size_t total_input_pixels = 0; + size_t total_compressed_size = 0; + size_t total_adj_compressed_size = 0; + double total_time_encode = 0.0; + double total_time_decode = 0.0; + float max_distance = -1.0; // Max butteraugli score + // sum of 8th powers of butteraugli distmap pixels. + double distance_p_norm = 0.0; + // sum of 2nd powers of differences between R, G, B. + double distance_2 = 0.0; + std::vector<float> distances; + size_t total_errors = 0; + JxlStats jxl_stats; + std::vector<float> extra_metrics; +}; + +std::string PrintHeader(const std::vector<std::string>& extra_metrics_names); + +// Given the rows of all printed statistics, print an aggregate row. +std::string PrintAggregate( + size_t num_extra_metrics, + const std::vector<std::vector<ColumnValue>>& aggregate); + +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_STATS_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_utils.cc b/media/libjxl/src/tools/benchmark/benchmark_utils.cc new file mode 100644 index 0000000000..4b531317e6 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_utils.cc @@ -0,0 +1,90 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#define _DEFAULT_SOURCE // for mkstemps(). + +#include "tools/benchmark/benchmark_utils.h" + +// Not supported on Windows due to Linux-specific functions. +// Not supported in Android NDK before API 28. +#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && \ + (!defined(__ANDROID_API__) || __ANDROID_API__ >= 28) + +#include <libgen.h> +#include <spawn.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <fstream> + +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/image_bundle.h" + +extern char** environ; + +namespace jxl { +TemporaryFile::TemporaryFile(std::string basename, std::string extension) { + const auto extension_size = 1 + extension.size(); + temp_filename_ = std::move(basename) + "_XXXXXX." + std::move(extension); + const int fd = mkstemps(&temp_filename_[0], extension_size); + if (fd == -1) { + ok_ = false; + return; + } + close(fd); +} +TemporaryFile::~TemporaryFile() { + if (ok_) { + unlink(temp_filename_.c_str()); + } +} + +Status TemporaryFile::GetFileName(std::string* const output) const { + JXL_RETURN_IF_ERROR(ok_); + *output = temp_filename_; + return true; +} + +Status RunCommand(const std::string& command, + const std::vector<std::string>& arguments) { + std::vector<char*> args; + args.reserve(arguments.size() + 2); + args.push_back(const_cast<char*>(command.c_str())); + for (const std::string& argument : arguments) { + args.push_back(const_cast<char*>(argument.c_str())); + } + args.push_back(nullptr); + pid_t pid; + JXL_RETURN_IF_ERROR(posix_spawnp(&pid, command.c_str(), nullptr, nullptr, + args.data(), environ) == 0); + int wstatus; + waitpid(pid, &wstatus, 0); + return WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == EXIT_SUCCESS; +} + +} // namespace jxl + +#else + +namespace jxl { + +TemporaryFile::TemporaryFile(std::string basename, std::string extension) {} +TemporaryFile::~TemporaryFile() {} +Status TemporaryFile::GetFileName(std::string* const output) const { + (void)ok_; + return JXL_FAILURE("Not supported on this build"); +} + +Status RunCommand(const std::string& command, + const std::vector<std::string>& arguments) { + return JXL_FAILURE("Not supported on this build"); +} + +} // namespace jxl + +#endif // _MSC_VER diff --git a/media/libjxl/src/tools/benchmark/benchmark_utils.h b/media/libjxl/src/tools/benchmark/benchmark_utils.h new file mode 100644 index 0000000000..027fa0868f --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_utils.h @@ -0,0 +1,35 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_BENCHMARK_BENCHMARK_UTILS_H_ +#define TOOLS_BENCHMARK_BENCHMARK_UTILS_H_ + +#include <string> +#include <vector> + +#include "lib/jxl/base/status.h" + +namespace jxl { + +class TemporaryFile final { + public: + explicit TemporaryFile(std::string basename, std::string extension); + TemporaryFile(const TemporaryFile&) = delete; + TemporaryFile& operator=(const TemporaryFile&) = delete; + ~TemporaryFile(); + Status GetFileName(std::string* output) const; + + private: + bool ok_ = true; + + std::string temp_filename_; +}; + +Status RunCommand(const std::string& command, + const std::vector<std::string>& arguments); + +} // namespace jxl + +#endif // TOOLS_BENCHMARK_BENCHMARK_UTILS_H_ diff --git a/media/libjxl/src/tools/benchmark/benchmark_xl.cc b/media/libjxl/src/tools/benchmark/benchmark_xl.cc new file mode 100644 index 0000000000..e91fbb8c5a --- /dev/null +++ b/media/libjxl/src/tools/benchmark/benchmark_xl.cc @@ -0,0 +1,1084 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <math.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <algorithm> +#include <memory> +#include <mutex> +#include <numeric> +#include <string> +#include <utility> +#include <vector> + +#include "jxl/decode.h" +#include "lib/extras/codec.h" +#include "lib/extras/dec/color_hints.h" +#include "lib/extras/time.h" +#include "lib/jxl/alpha.h" +#include "lib/jxl/base/cache_aligned.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/random.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_butteraugli_pnorm.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "tools/benchmark/benchmark_args.h" +#include "tools/benchmark/benchmark_codec.h" +#include "tools/benchmark/benchmark_file_io.h" +#include "tools/benchmark/benchmark_stats.h" +#include "tools/benchmark/benchmark_utils.h" +#include "tools/codec_config.h" +#include "tools/speed_stats.h" + +namespace jxl { +namespace { + +Status WriteImage(Image3F&& image, ThreadPool* pool, + const std::string& filename) { + CodecInOut io; + io.metadata.m.SetUintSamples(8); + io.metadata.m.color_encoding = ColorEncoding::SRGB(); + io.SetFromImage(std::move(image), io.metadata.m.color_encoding); + return EncodeToFile(io, filename, pool); +} + +Status ReadPNG(const std::string& filename, Image3F* image) { + CodecInOut io; + JXL_CHECK(SetFromFile(filename, extras::ColorHints(), &io)); + *image = CopyImage(*io.Main().color()); + return true; +} + +void DoCompress(const std::string& filename, const CodecInOut& io, + const std::vector<std::string>& extra_metrics_commands, + ImageCodec* codec, ThreadPoolInternal* inner_pool, + PaddedBytes* compressed, BenchmarkStats* s) { + PROFILER_FUNC; + ++s->total_input_files; + + if (io.frames.size() != 1) { + // Multiple frames not supported (io.xsize() will checkfail) + s->total_errors++; + if (!Args()->silent_errors) { + JXL_WARNING("multiframe input image not supported %s", filename.c_str()); + } + return; + } + const size_t xsize = io.xsize(); + const size_t ysize = io.ysize(); + const size_t input_pixels = xsize * ysize; + + jpegxl::tools::SpeedStats speed_stats; + jpegxl::tools::SpeedStats::Summary summary; + + bool valid = true; // false if roundtrip, encoding or decoding errors occur. + + if (!Args()->decode_only && (io.xsize() == 0 || io.ysize() == 0)) { + // This means the benchmark couldn't load the image, e.g. due to invalid + // ICC profile. Warning message about that was already printed. Continue + // this function to indicate it as error in the stats. + valid = false; + } + + std::string ext = FileExtension(filename); + if (valid && !Args()->decode_only) { + for (size_t i = 0; i < Args()->encode_reps; ++i) { + if (codec->CanRecompressJpeg() && (ext == ".jpg" || ext == ".jpeg")) { + std::string data_in; + JXL_CHECK(ReadFile(filename, &data_in)); + JXL_CHECK( + codec->RecompressJpeg(filename, data_in, compressed, &speed_stats)); + } else { + Status status = codec->Compress(filename, &io, inner_pool, compressed, + &speed_stats); + if (!status) { + valid = false; + if (!Args()->silent_errors) { + std::string message = codec->GetErrorMessage(); + if (!message.empty()) { + fprintf(stderr, "Error in %s codec: %s\n", + codec->description().c_str(), message.c_str()); + } else { + fprintf(stderr, "Error in %s codec\n", + codec->description().c_str()); + } + } + } + } + } + JXL_CHECK(speed_stats.GetSummary(&summary)); + s->total_time_encode += summary.central_tendency; + } + + if (valid && Args()->decode_only) { + std::string data_in; + JXL_CHECK(ReadFile(filename, &data_in)); + compressed->append((uint8_t*)data_in.data(), + (uint8_t*)data_in.data() + data_in.size()); + } + + // Decompress + CodecInOut io2; + io2.metadata.m = io.metadata.m; + if (valid) { + speed_stats = jpegxl::tools::SpeedStats(); + for (size_t i = 0; i < Args()->decode_reps; ++i) { + if (!codec->Decompress(filename, Span<const uint8_t>(*compressed), + inner_pool, &io2, &speed_stats)) { + if (!Args()->silent_errors) { + fprintf(stderr, + "%s failed to decompress encoded image. Original source:" + " %s\n", + codec->description().c_str(), filename.c_str()); + } + valid = false; + } + + // io2.dec_pixels increases each time, but the total should be independent + // of decode_reps, so only take the value from the first iteration. + if (i == 0) s->total_input_pixels += io2.dec_pixels; + } + JXL_CHECK(speed_stats.GetSummary(&summary)); + s->total_time_decode += summary.central_tendency; + } + + std::string name = FileBaseName(filename); + std::string codec_name = codec->description(); + + if (!valid) { + s->total_errors++; + } + + if (io.frames.size() != io2.frames.size()) { + if (!Args()->silent_errors) { + // Animated gifs not supported yet? + fprintf(stderr, + "Frame sizes not equal, is this an animated gif? %s %s %" PRIuS + " %" PRIuS "\n", + codec_name.c_str(), name.c_str(), io.frames.size(), + io2.frames.size()); + } + valid = false; + } + + bool lossless = codec->IsJpegTranscoder(); + bool skip_butteraugli = + Args()->skip_butteraugli || Args()->decode_only || lossless; + ImageF distmap; + float max_distance = 1.0f; + + if (valid && !skip_butteraugli) { + JXL_ASSERT(io.frames.size() == io2.frames.size()); + for (size_t i = 0; i < io.frames.size(); i++) { + const ImageBundle& ib1 = io.frames[i]; + ImageBundle& ib2 = io2.frames[i]; + + // Verify output + PROFILER_ZONE("Benchmark stats"); + float distance; + if (SameSize(ib1, ib2)) { + ButteraugliParams params = codec->BaParams(); + if (ib1.metadata()->IntensityTarget() != + ib2.metadata()->IntensityTarget()) { + fprintf(stderr, + "WARNING: input and output images have different intensity " + "targets"); + } + params.intensity_target = ib1.metadata()->IntensityTarget(); + // Hack the default intensity target value to be 80.0, the intensity + // target of sRGB images and a more reasonable viewing default than + // JPEG XL file format's default. + if (fabs(params.intensity_target - 255.0f) < 1e-3) { + params.intensity_target = 80.0; + } + distance = ButteraugliDistance(ib1, ib2, params, GetJxlCms(), &distmap, + inner_pool); + // Ensure pixels in range 0-1 + s->distance_2 += ComputeDistance2(ib1, ib2, GetJxlCms()); + } else { + // TODO(veluca): re-upsample and compute proper distance. + distance = 1e+4f; + distmap = ImageF(1, 1); + distmap.Row(0)[0] = distance; + s->distance_2 += distance; + } + // Update stats + s->distance_p_norm += + ComputeDistanceP(distmap, Args()->ba_params, Args()->error_pnorm) * + input_pixels; + s->max_distance = std::max(s->max_distance, distance); + s->distances.push_back(distance); + max_distance = std::max(max_distance, distance); + } + } + + s->total_compressed_size += compressed->size(); + s->total_adj_compressed_size += compressed->size() * max_distance; + codec->GetMoreStats(s); + + if (io2.frames.size() == 1 && + (Args()->save_compressed || Args()->save_decompressed)) { + JXL_ASSERT(io2.frames.size() == 1); + ImageBundle& ib2 = io2.Main(); + + // By default the benchmark will save the image after roundtrip with the + // same color encoding as the image before roundtrip. Not all codecs + // necessarily preserve the amount of channels (1 for gray, 3 for RGB) + // though, since not all image formats necessarily allow a way to remember + // what amount of channels you happened to give the benchmark codec + // input (say, an RGB-only format) and that is fine since in the end what + // matters is that the pixels look the same on a 3-channel RGB monitor + // while using grayscale encoding is an internal compression optimization. + // If that is the case, output with the current color model instead, + // because CodecInOut does not automatically convert between 1 or 3 + // channels, and giving a ColorEncoding with a different amount of + // channels is not allowed. + const ColorEncoding* c_desired = + (ib2.metadata()->color_encoding.Channels() == + ib2.c_current().Channels()) + ? &ib2.metadata()->color_encoding + : &ib2.c_current(); + // Allow overriding via --output_encoding. + if (!Args()->output_description.empty()) { + c_desired = &Args()->output_encoding; + } + + std::string dir = FileDirName(filename); + std::string outdir = + Args()->output_dir.empty() ? dir + "/out" : Args()->output_dir; + // Make compatible for filename + std::replace(codec_name.begin(), codec_name.end(), ':', '_'); + std::string compressed_fn = outdir + "/" + name + "." + codec_name; + std::string decompressed_fn = compressed_fn + Args()->output_extension; +#if JPEGXL_ENABLE_APNG + std::string heatmap_fn = compressed_fn + ".heatmap.png"; +#else + std::string heatmap_fn = compressed_fn + ".heatmap.ppm"; +#endif + JXL_CHECK(MakeDir(outdir)); + if (Args()->save_compressed) { + std::string compressed_str( + reinterpret_cast<const char*>(compressed->data()), + compressed->size()); + JXL_CHECK(WriteFile(compressed_str, compressed_fn)); + } + if (Args()->save_decompressed && valid) { + // For verifying HDR: scale output. + if (Args()->mul_output != 0.0) { + fprintf(stderr, "WARNING: scaling outputs by %f\n", Args()->mul_output); + JXL_CHECK(ib2.TransformTo(ColorEncoding::LinearSRGB(ib2.IsGray()), + GetJxlCms(), inner_pool)); + ScaleImage(static_cast<float>(Args()->mul_output), ib2.color()); + } + + JXL_CHECK(EncodeToFile(io2, *c_desired, + ib2.metadata()->bit_depth.bits_per_sample, + decompressed_fn)); + if (!skip_butteraugli) { + float good = Args()->heatmap_good > 0.0f ? Args()->heatmap_good + : ButteraugliFuzzyInverse(1.5); + float bad = Args()->heatmap_bad > 0.0f ? Args()->heatmap_bad + : ButteraugliFuzzyInverse(0.5); + JXL_CHECK(WriteImage(CreateHeatMapImage(distmap, good, bad), inner_pool, + heatmap_fn)); + } + } + } + if (!extra_metrics_commands.empty()) { + CodecInOut in_copy; + in_copy.SetFromImage(std::move(*io.Main().Copy().color()), + io.Main().c_current()); + TemporaryFile tmp_in("original", "pfm"); + TemporaryFile tmp_out("decoded", "pfm"); + TemporaryFile tmp_res("result", "txt"); + std::string tmp_in_fn, tmp_out_fn, tmp_res_fn; + JXL_CHECK(tmp_in.GetFileName(&tmp_in_fn)); + JXL_CHECK(tmp_out.GetFileName(&tmp_out_fn)); + JXL_CHECK(tmp_res.GetFileName(&tmp_res_fn)); + + // Convert everything to non-linear SRGB - this is what most metrics expect. + const ColorEncoding& c_desired = ColorEncoding::SRGB(io.Main().IsGray()); + JXL_CHECK(EncodeToFile(io, c_desired, + io.metadata.m.bit_depth.bits_per_sample, tmp_in_fn)); + JXL_CHECK(EncodeToFile( + io2, c_desired, io.metadata.m.bit_depth.bits_per_sample, tmp_out_fn)); + if (io.metadata.m.IntensityTarget() != io2.metadata.m.IntensityTarget()) { + fprintf(stderr, + "WARNING: original and decoded have different intensity targets " + "(%f vs. %f).\n", + io.metadata.m.IntensityTarget(), + io2.metadata.m.IntensityTarget()); + } + std::string intensity_target; + { + std::ostringstream intensity_target_oss; + intensity_target_oss << io.metadata.m.IntensityTarget(); + intensity_target = intensity_target_oss.str(); + } + for (size_t i = 0; i < extra_metrics_commands.size(); i++) { + float res = nanf(""); + bool error = false; + if (RunCommand(extra_metrics_commands[i], + {tmp_in_fn, tmp_out_fn, tmp_res_fn, intensity_target})) { + FILE* f = fopen(tmp_res_fn.c_str(), "r"); + if (fscanf(f, "%f", &res) != 1) { + error = true; + } + fclose(f); + } else { + error = true; + } + if (error) { + fprintf(stderr, + "WARNING: Computation of metric with command %s failed\n", + extra_metrics_commands[i].c_str()); + } + s->extra_metrics.push_back(res); + } + } + + if (Args()->show_progress) { + fprintf(stderr, "."); + fflush(stderr); + } +} + +// Makes a base64 data URI for embedded image in HTML +std::string Base64Image(const std::string& filename) { + PaddedBytes bytes; + if (!ReadFile(filename, &bytes)) { + return ""; + } + static const char* symbols = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string result; + for (size_t i = 0; i < bytes.size(); i += 3) { + uint8_t o0 = bytes[i + 0]; + uint8_t o1 = (i + 1 < bytes.size()) ? bytes[i + 1] : 0; + uint8_t o2 = (i + 2 < bytes.size()) ? bytes[i + 2] : 0; + uint32_t value = (o0 << 16) | (o1 << 8) | o2; + for (size_t j = 0; j < 4; j++) { + result += (i + j <= bytes.size()) ? symbols[(value >> (6 * (3 - j))) & 63] + : '='; + } + } + // NOTE: Chrome supports max 2MB of data this way for URLs, but appears to + // support larger images anyway as long as it's embedded in the HTML file + // itself. If more data is needed, use createObjectURL. + return "data:image;base64," + result; +} + +struct Task { + ImageCodecPtr codec; + size_t idx_image; + size_t idx_method; + const CodecInOut* image; + BenchmarkStats stats; +}; + +void WriteHtmlReport(const std::string& codec_desc, + const std::vector<std::string>& fnames, + const std::vector<const Task*>& tasks, + const std::vector<const CodecInOut*>& images, + bool self_contained) { + std::string toggle_js = + "<script type=\"text/javascript\">\n" + " var codecname = '" + + codec_desc + "';\n"; + toggle_js += R"( + var maintitle = codecname + ' - click images to toggle, press space to' + + ' toggle all, h to toggle all heatmaps. Zoom in with CTRL+wheel or' + + ' CTRL+plus.'; + document.title = maintitle; + var counter = []; + function setState(i, s) { + var preview = document.getElementById("preview" + i); + var orig = document.getElementById("orig" + i); + var hm = document.getElementById("hm" + i); + if (s == 0) { + preview.style.display = 'none'; + orig.style.display = 'block'; + hm.style.display = 'none'; + } else if (s == 1) { + preview.style.display = 'block'; + orig.style.display = 'none'; + hm.style.display = 'none'; + } else if (s == 2) { + preview.style.display = 'none'; + orig.style.display = 'none'; + hm.style.display = 'block'; + } + } + function toggle3(i) { + for (index = counter.length; index <= i; index++) { + counter.push(1); + } + setState(i, counter[i]); + counter[i] = (counter[i] + 1) % 3; + document.title = maintitle; + } + var toggleall_state = 1; + document.body.onkeydown = function(e) { + // space (32) to toggle orig/compr, 'h' (72) to toggle heatmap/compr + if (e.keyCode == 32 || e.keyCode == 72) { + var divs = document.getElementsByTagName('div'); + var key_state = (e.keyCode == 32) ? 0 : 2; + toggleall_state = (toggleall_state == key_state) ? 1 : key_state; + document.title = codecname + ' - ' + (toggleall_state == 0 ? + 'originals' : (toggleall_state == 1 ? 'compressed' : 'heatmaps')); + for (var i = 0; i < divs.length; i++) { + setState(i, toggleall_state); + } + return false; + } + }; +</script> +)"; + std::string out_html; + std::string outdir; + out_html += "<body bgcolor=\"#000\">\n"; + out_html += "<style>img { image-rendering: pixelated; }</style>\n"; + std::string codec_name = codec_desc; + // Make compatible for filename + std::replace(codec_name.begin(), codec_name.end(), ':', '_'); + for (size_t i = 0; i < fnames.size(); ++i) { + std::string name = FileBaseName(fnames[i]); + std::string dir = FileDirName(fnames[i]); + outdir = Args()->output_dir.empty() ? dir + "/out" : Args()->output_dir; + std::string name_out = name + "." + codec_name + Args()->output_extension; + std::string heatmap_out = name + "." + codec_name + ".heatmap.png"; + + std::string fname_orig = fnames[i]; + std::string fname_out = outdir + "/" + name_out; + std::string fname_heatmap = outdir + "/" + heatmap_out; + std::string url_orig = Args()->originals_url.empty() + ? ("file://" + fnames[i]) + : (Args()->originals_url + "/" + name); + std::string url_out = name_out; + std::string url_heatmap = heatmap_out; + if (self_contained) { + url_orig = Base64Image(fname_orig); + url_out = Base64Image(fname_out); + url_heatmap = Base64Image(fname_heatmap); + } + std::string number = StringPrintf("%" PRIuS, i); + const CodecInOut& image = *images[i]; + size_t xsize = image.frames.size() == 1 ? image.xsize() : 0; + size_t ysize = image.frames.size() == 1 ? image.ysize() : 0; + std::string html_width = StringPrintf("%" PRIuS "px", xsize); + std::string html_height = StringPrintf("%" PRIuS "px", ysize); + double bpp = tasks[i]->stats.total_compressed_size * 8.0 / + tasks[i]->stats.total_input_pixels; + double pnorm = + tasks[i]->stats.distance_p_norm / tasks[i]->stats.total_input_pixels; + double max_dist = tasks[i]->stats.max_distance; + std::string compressed_title = StringPrintf( + "compressed. bpp: %f, pnorm: %f, max dist: %f", bpp, pnorm, max_dist); + out_html += "<div onclick=\"toggle3(" + number + + ");\" style=\"display:inline-block;width:" + html_width + + ";height:" + html_height + + ";\">\n" + " <img title=\"" + + compressed_title + "\" id=\"preview" + number + "\" src="; + out_html += "\"" + url_out + "\""; + out_html += + " style=\"display:block;\"/>\n" + " <img title=\"original\" id=\"orig" + + number + "\" src="; + out_html += "\"" + url_orig + "\""; + out_html += + " style=\"display:none;\"/>\n" + " <img title=\"heatmap\" id=\"hm" + + number + "\" src="; + out_html += "\"" + url_heatmap + "\""; + out_html += " style=\"display:none;\"/>\n</div>\n"; + } + out_html += "</body>\n"; + out_html += toggle_js; + JXL_CHECK(WriteFile(out_html, outdir + "/index." + codec_name + ".html")); +} + +// Prints the detailed and aggregate statistics, in the correct order but as +// soon as possible when multithreaded tasks are done. +struct StatPrinter { + StatPrinter(const std::vector<std::string>& methods, + const std::vector<std::string>& extra_metrics_names, + const std::vector<std::string>& fnames, + const std::vector<Task>& tasks) + : methods_(&methods), + extra_metrics_names_(&extra_metrics_names), + fnames_(&fnames), + tasks_(&tasks), + tasks_done_(0), + stats_printed_(0), + details_printed_(0) { + stats_done_.resize(methods.size(), 0); + details_done_.resize(tasks.size(), 0); + max_fname_width_ = 0; + for (const auto& fname : fnames) { + max_fname_width_ = std::max(max_fname_width_, FileBaseName(fname).size()); + } + max_method_width_ = 0; + for (const auto& method : methods) { + max_method_width_ = + std::max(max_method_width_, FileBaseName(method).size()); + } + } + + void TaskDone(size_t task_index, const Task& t) { + PROFILER_FUNC; + std::lock_guard<std::mutex> guard(mutex); + tasks_done_++; + if (Args()->print_details || Args()->show_progress) { + if (Args()->print_details) { + // Render individual results as soon as they are ready and all previous + // ones in task order are ready. + details_done_[task_index] = 1; + if (task_index == details_printed_) { + while (details_printed_ < tasks_->size() && + details_done_[details_printed_]) { + PrintDetails((*tasks_)[details_printed_]); + details_printed_++; + } + } + } + // When using "show_progress" or "print_details", the table must be + // rendered at the very end, else the details or progress would be + // rendered in-between the table rows. + if (tasks_done_ == tasks_->size()) { + PrintStatsHeader(); + for (size_t i = 0; i < methods_->size(); i++) { + PrintStats((*methods_)[i], i); + } + PrintStatsFooter(); + } + } else { + if (tasks_done_ == 1) { + PrintStatsHeader(); + } + // Render lines of the table as soon as it is ready and all previous + // lines have been printed. + stats_done_[t.idx_method]++; + if (stats_done_[t.idx_method] == fnames_->size() && + t.idx_method == stats_printed_) { + while (stats_printed_ < stats_done_.size() && + stats_done_[stats_printed_] == fnames_->size()) { + PrintStats((*methods_)[stats_printed_], stats_printed_); + stats_printed_++; + } + } + if (tasks_done_ == tasks_->size()) { + PrintStatsFooter(); + } + } + } + + void PrintDetails(const Task& t) { + double comp_bpp = + t.stats.total_compressed_size * 8.0 / t.stats.total_input_pixels; + double p_norm = t.stats.distance_p_norm / t.stats.total_input_pixels; + double bpp_p_norm = p_norm * comp_bpp; + + const double adj_comp_bpp = + t.stats.total_adj_compressed_size * 8.0 / t.stats.total_input_pixels; + + const double rmse = + std::sqrt(t.stats.distance_2 / t.stats.total_input_pixels); + const double psnr = t.stats.total_compressed_size == 0 ? 0.0 + : (t.stats.distance_2 == 0) + ? 99.99 + : (20 * std::log10(1 / rmse)); + size_t pixels = t.stats.total_input_pixels; + + const double enc_mps = + t.stats.total_input_pixels / (1000000.0 * t.stats.total_time_encode); + const double dec_mps = + t.stats.total_input_pixels / (1000000.0 * t.stats.total_time_decode); + if (Args()->print_details_csv) { + printf("%s,%s,%" PRIdS ",%" PRIdS ",%" PRIdS + ",%.8f,%.8f,%.8f,%.8f,%.8f,%.8f,%.8f,%.8f", + (*methods_)[t.idx_method].c_str(), + FileBaseName((*fnames_)[t.idx_image]).c_str(), + t.stats.total_errors, t.stats.total_compressed_size, pixels, + enc_mps, dec_mps, comp_bpp, t.stats.max_distance, psnr, p_norm, + bpp_p_norm, adj_comp_bpp); + for (float m : t.stats.extra_metrics) { + printf(",%.8f", m); + } + printf("\n"); + } else { + printf("%s", (*methods_)[t.idx_method].c_str()); + for (size_t i = (*methods_)[t.idx_method].size(); i <= max_method_width_; + i++) { + printf(" "); + } + printf("%s", FileBaseName((*fnames_)[t.idx_image]).c_str()); + for (size_t i = FileBaseName((*fnames_)[t.idx_image]).size(); + i <= max_fname_width_; i++) { + printf(" "); + } + printf( + "error:%" PRIdS " size:%8" PRIdS " pixels:%9" PRIdS + " enc_speed:%8.8f dec_speed:%8.8f bpp:%10.8f dist:%10.8f" + " psnr:%10.8f p:%10.8f bppp:%10.8f qabpp:%10.8f ", + t.stats.total_errors, t.stats.total_compressed_size, pixels, enc_mps, + dec_mps, comp_bpp, t.stats.max_distance, psnr, p_norm, bpp_p_norm, + adj_comp_bpp); + for (size_t i = 0; i < t.stats.extra_metrics.size(); i++) { + printf(" %s:%.8f", (*extra_metrics_names_)[i].c_str(), + t.stats.extra_metrics[i]); + } + printf("\n"); + } + fflush(stdout); + } + + void PrintStats(const std::string& method, size_t idx_method) { + PROFILER_FUNC; + // Assimilate all tasks with the same idx_method. + BenchmarkStats method_stats; + std::vector<const CodecInOut*> images; + std::vector<const Task*> tasks; + for (const Task& t : *tasks_) { + if (t.idx_method == idx_method) { + method_stats.Assimilate(t.stats); + images.push_back(t.image); + tasks.push_back(&t); + } + } + + std::string out; + + method_stats.PrintMoreStats(); // not concurrent + out += method_stats.PrintLine(method, fnames_->size()); + + if (Args()->write_html_report) { + WriteHtmlReport(method, *fnames_, tasks, images, + Args()->html_report_self_contained); + } + + stats_aggregate_.push_back( + method_stats.ComputeColumns(method, fnames_->size())); + + printf("%s", out.c_str()); + fflush(stdout); + } + + void PrintStatsHeader() { + if (Args()->markdown) { + if (Args()->show_progress) { + fprintf(stderr, "\n"); + fflush(stderr); + } + printf("```\n"); + } + if (fnames_->size() == 1) printf("%s\n", (*fnames_)[0].c_str()); + printf("%s", PrintHeader(*extra_metrics_names_).c_str()); + fflush(stdout); + } + + void PrintStatsFooter() { + printf( + "%s", + PrintAggregate(extra_metrics_names_->size(), stats_aggregate_).c_str()); + if (Args()->markdown) printf("```\n"); + printf("\n"); + fflush(stdout); + } + + const std::vector<std::string>* methods_; + const std::vector<std::string>* extra_metrics_names_; + const std::vector<std::string>* fnames_; + const std::vector<Task>* tasks_; + + size_t tasks_done_; + + size_t stats_printed_; + std::vector<size_t> stats_done_; + + size_t details_printed_; + std::vector<size_t> details_done_; + + size_t max_fname_width_; + size_t max_method_width_; + + std::vector<std::vector<ColumnValue>> stats_aggregate_; + + std::mutex mutex; +}; + +class Benchmark { + using StringVec = std::vector<std::string>; + + public: + // Return the exit code of the program. + static int Run() { + int ret = EXIT_SUCCESS; + { + PROFILER_FUNC; + + const StringVec methods = GetMethods(); + const StringVec extra_metrics_names = GetExtraMetricsNames(); + const StringVec extra_metrics_commands = GetExtraMetricsCommands(); + const StringVec fnames = GetFilenames(); + bool all_color_aware; + bool jpeg_transcoding_requested; + // (non-const because Task.stats are updated) + std::vector<Task> tasks = CreateTasks(methods, fnames, &all_color_aware, + &jpeg_transcoding_requested); + + std::unique_ptr<ThreadPoolInternal> pool; + std::vector<std::unique_ptr<ThreadPoolInternal>> inner_pools; + InitThreads(static_cast<int>(tasks.size()), &pool, &inner_pools); + + const std::vector<CodecInOut> loaded_images = LoadImages( + fnames, all_color_aware, jpeg_transcoding_requested, pool.get()); + + if (RunTasks(methods, extra_metrics_names, extra_metrics_commands, fnames, + loaded_images, pool.get(), inner_pools, &tasks) != 0) { + ret = EXIT_FAILURE; + if (!Args()->silent_errors) { + fprintf(stderr, "There were error(s) in the benchmark.\n"); + } + } + } + + // Must have exited profiler zone above before calling. + if (Args()->profiler) { + PROFILER_PRINT_RESULTS(); + } + CacheAligned::PrintStats(); + return ret; + } + + private: + static int NumOuterThreads(const int num_hw_threads, const int num_tasks) { + int num_threads = Args()->num_threads; + // Default to #cores + if (num_threads < 0) num_threads = num_hw_threads; + + // As a safety precaution, limit the number of threads to 4x the number of + // available CPUs. + num_threads = + std::min<int>(num_threads, 4 * std::thread::hardware_concurrency()); + + // Don't create more threads than there are tasks (pointless/wasteful). + num_threads = std::min(num_threads, num_tasks); + + // Just one thread is counterproductive. + if (num_threads == 1) num_threads = 0; + + return num_threads; + } + + static int NumInnerThreads(const int num_hw_threads, const int num_threads) { + int num_inner = Args()->inner_threads; + + // Default: distribute remaining cores among tasks. + if (num_inner < 0) { + const int cores_for_outer = num_hw_threads - num_threads; + num_inner = + num_threads == 0 ? num_hw_threads : cores_for_outer / num_threads; + } + + // Just one thread is counterproductive. + if (num_inner == 1) num_inner = 0; + + return num_inner; + } + + static void InitThreads( + const int num_tasks, std::unique_ptr<ThreadPoolInternal>* pool, + std::vector<std::unique_ptr<ThreadPoolInternal>>* inner_pools) { + const int num_hw_threads = std::thread::hardware_concurrency(); + const int num_threads = NumOuterThreads(num_hw_threads, num_tasks); + const int num_inner = NumInnerThreads(num_hw_threads, num_threads); + + fprintf(stderr, + "%d total threads, %d tasks, %d threads, %d inner threads\n", + num_hw_threads, num_tasks, num_threads, num_inner); + + pool->reset(new ThreadPoolInternal(num_threads)); + // Main thread OR worker threads in pool each get a possibly empty nested + // pool (helps use all available cores when #tasks < #threads) + for (size_t i = 0; i < (*pool)->NumThreads(); ++i) { + inner_pools->emplace_back(new ThreadPoolInternal(num_inner)); + } + } + + static StringVec GetMethods() { + StringVec methods = SplitString(Args()->codec, ','); + for (auto it = methods.begin(); it != methods.end();) { + if (it->empty()) { + it = methods.erase(it); + } else { + ++it; + } + } + return methods; + } + + static StringVec GetExtraMetricsNames() { + StringVec metrics = SplitString(Args()->extra_metrics, ','); + for (auto it = metrics.begin(); it != metrics.end();) { + if (it->empty()) { + it = metrics.erase(it); + } else { + *it = SplitString(*it, ':')[0]; + ++it; + } + } + return metrics; + } + + static StringVec GetExtraMetricsCommands() { + StringVec metrics = SplitString(Args()->extra_metrics, ','); + for (auto it = metrics.begin(); it != metrics.end();) { + if (it->empty()) { + it = metrics.erase(it); + } else { + auto s = SplitString(*it, ':'); + JXL_CHECK(s.size() == 2); + *it = s[1]; + ++it; + } + } + return metrics; + } + + static StringVec SampleFromInput(const StringVec& fnames, + const std::string& sample_tmp_dir, + int num_samples, size_t size) { + JXL_CHECK(!sample_tmp_dir.empty()); + fprintf(stderr, "Creating samples of %" PRIuS "x%" PRIuS " tiles...\n", + size, size); + StringVec fnames_out; + std::vector<Image3F> images; + std::vector<size_t> offsets; + size_t total_num_tiles = 0; + for (const auto& fname : fnames) { + Image3F img; + JXL_CHECK(ReadPNG(fname, &img)); + JXL_CHECK(img.xsize() >= size); + JXL_CHECK(img.ysize() >= size); + total_num_tiles += (img.xsize() - size + 1) * (img.ysize() - size + 1); + offsets.push_back(total_num_tiles); + images.emplace_back(std::move(img)); + } + JXL_CHECK(MakeDir(sample_tmp_dir)); + Rng rng(0); + for (int i = 0; i < num_samples; ++i) { + int val = rng.UniformI(0, offsets.back()); + size_t idx = (std::lower_bound(offsets.begin(), offsets.end(), val) - + offsets.begin()); + JXL_CHECK(idx < images.size()); + const Image3F& img = images[idx]; + int x0 = rng.UniformI(0, img.xsize() - size); + int y0 = rng.UniformI(0, img.ysize() - size); + Image3F sample(size, size); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < size; ++y) { + const float* JXL_RESTRICT row_in = img.PlaneRow(c, y0 + y); + float* JXL_RESTRICT row_out = sample.PlaneRow(c, y); + memcpy(row_out, &row_in[x0], size * sizeof(row_out[0])); + } + } + std::string fn_output = + StringPrintf("%s/%s.crop_%dx%d+%d+%d.png", sample_tmp_dir.c_str(), + FileBaseName(fnames[idx]).c_str(), size, size, x0, y0); + ThreadPool* null_pool = nullptr; + JXL_CHECK(WriteImage(std::move(sample), null_pool, fn_output)); + fnames_out.push_back(fn_output); + } + fprintf(stderr, "Created %d sample tiles\n", num_samples); + return fnames_out; + } + + static StringVec GetFilenames() { + StringVec fnames; + JXL_CHECK(MatchFiles(Args()->input, &fnames)); + if (fnames.empty()) { + JXL_ABORT("No input file matches pattern: '%s'", Args()->input.c_str()); + } + if (Args()->print_details) { + std::sort(fnames.begin(), fnames.end()); + } + + if (Args()->num_samples > 0) { + fnames = SampleFromInput(fnames, Args()->sample_tmp_dir, + Args()->num_samples, Args()->sample_dimensions); + } + return fnames; + } + + // (Load only once, not for every codec) + static std::vector<CodecInOut> LoadImages( + const StringVec& fnames, const bool all_color_aware, + const bool jpeg_transcoding_requested, ThreadPool* pool) { + PROFILER_FUNC; + std::vector<CodecInOut> loaded_images; + loaded_images.resize(fnames.size()); + JXL_CHECK(RunOnPool( + pool, 0, static_cast<uint32_t>(fnames.size()), ThreadPool::NoInit, + [&](const uint32_t task, size_t /*thread*/) { + const size_t i = static_cast<size_t>(task); + Status ok = true; + + if (!Args()->decode_only) { + PaddedBytes encoded; + ok = ReadFile(fnames[i], &encoded) && + (jpeg_transcoding_requested + ? jpeg::DecodeImageJPG(Span<const uint8_t>(encoded), + &loaded_images[i]) + : SetFromBytes(Span<const uint8_t>(encoded), + Args()->color_hints, &loaded_images[i])); + if (ok && Args()->intensity_target != 0) { + loaded_images[i].metadata.m.SetIntensityTarget( + Args()->intensity_target); + } + } + if (!ok) { + if (!Args()->silent_errors) { + fprintf(stderr, "Failed to load image %s\n", fnames[i].c_str()); + } + return; + } + + if (!Args()->decode_only && all_color_aware) { + const bool is_gray = loaded_images[i].Main().IsGray(); + const ColorEncoding& c_desired = ColorEncoding::LinearSRGB(is_gray); + if (!loaded_images[i].TransformTo(c_desired, GetJxlCms(), + /*pool=*/nullptr)) { + JXL_ABORT("Failed to transform to lin. sRGB %s", + fnames[i].c_str()); + } + } + + if (!Args()->decode_only && Args()->override_bitdepth != 0) { + if (Args()->override_bitdepth == 32) { + loaded_images[i].metadata.m.SetFloat32Samples(); + } else { + loaded_images[i].metadata.m.SetUintSamples( + Args()->override_bitdepth); + } + } + }, + "Load images")); + return loaded_images; + } + + static std::vector<Task> CreateTasks(const StringVec& methods, + const StringVec& fnames, + bool* all_color_aware, + bool* jpeg_transcoding_requested) { + std::vector<Task> tasks; + tasks.reserve(methods.size() * fnames.size()); + *all_color_aware = true; + *jpeg_transcoding_requested = false; + for (size_t idx_image = 0; idx_image < fnames.size(); ++idx_image) { + for (size_t idx_method = 0; idx_method < methods.size(); ++idx_method) { + tasks.emplace_back(); + Task& t = tasks.back(); + t.codec = CreateImageCodec(methods[idx_method]); + *all_color_aware &= t.codec->IsColorAware(); + *jpeg_transcoding_requested |= t.codec->IsJpegTranscoder(); + t.idx_image = idx_image; + t.idx_method = idx_method; + // t.stats is default-initialized. + } + } + JXL_ASSERT(tasks.size() == tasks.capacity()); + return tasks; + } + + // Return the total number of errors. + static size_t RunTasks( + const StringVec& methods, const StringVec& extra_metrics_names, + const StringVec& extra_metrics_commands, const StringVec& fnames, + const std::vector<CodecInOut>& loaded_images, ThreadPoolInternal* pool, + const std::vector<std::unique_ptr<ThreadPoolInternal>>& inner_pools, + std::vector<Task>* tasks) { + PROFILER_FUNC; + StatPrinter printer(methods, extra_metrics_names, fnames, *tasks); + if (Args()->print_details_csv) { + // Print CSV header + printf( + "method,image,error,size,pixels,enc_speed,dec_speed," + "bpp,dist,psnr,p,bppp,qabpp"); + for (const std::string& s : extra_metrics_names) { + printf(",%s", s.c_str()); + } + printf("\n"); + } + + std::vector<uint64_t> errors_thread; + JXL_CHECK(RunOnPool( + pool, 0, tasks->size(), + [&](const size_t num_threads) { + // Reduce false sharing by only writing every 8th slot (64 bytes). + errors_thread.resize(8 * num_threads); + return true; + }, + [&](const uint32_t i, const size_t thread) { + Task& t = (*tasks)[i]; + const CodecInOut& image = loaded_images[t.idx_image]; + t.image = ℑ + PaddedBytes compressed; + DoCompress(fnames[t.idx_image], image, extra_metrics_commands, + t.codec.get(), inner_pools[thread].get(), &compressed, + &t.stats); + printer.TaskDone(i, t); + errors_thread[8 * thread] += t.stats.total_errors; + }, + "Benchmark tasks")); + if (Args()->show_progress) fprintf(stderr, "\n"); + return std::accumulate(errors_thread.begin(), errors_thread.end(), 0); + } +}; + +int BenchmarkMain(int argc, const char** argv) { + fprintf(stderr, "benchmark_xl %s\n", + jpegxl::tools::CodecConfigString(JxlDecoderVersion()).c_str()); + + JXL_CHECK(Args()->AddCommandLineOptions()); + + if (!Args()->Parse(argc, argv)) { + fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); + return 1; + } + + if (Args()->cmdline.HelpFlagPassed()) { + Args()->PrintHelp(); + return 0; + } + if (!Args()->ValidateArgs()) { + fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); + return 1; + } + return Benchmark::Run(); +} + +} // namespace +} // namespace jxl + +int main(int argc, const char** argv) { return jxl::BenchmarkMain(argc, argv); } diff --git a/media/libjxl/src/tools/benchmark/hm/README.md b/media/libjxl/src/tools/benchmark/hm/README.md new file mode 100644 index 0000000000..e54904eff9 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/hm/README.md @@ -0,0 +1,12 @@ +This directory contains encoding and decoding scripts for HEVC, for use with +the benchmark custom codec. They use the HEVC reference encoder at https://hevc.hhi.fraunhofer.de/svn/svn_HEVCSoftware/ +and require the `TAppEncoderHighBitDepthStatic` and +`TAppDecoderHighBitDepthStatic` binaries to be placed in this directory. + +Example usage, for encoding at QP = 30: + +``` +tools/benchmark_xl --input=image.png --codec='custom:bin:.../tools/benchmark/hm/encode.sh:.../tools/benchmark/hm/decode.sh:-q:30' +``` + +The paths to the encode and decode scripts should be adjusted as necessary. diff --git a/media/libjxl/src/tools/benchmark/hm/decode.sh b/media/libjxl/src/tools/benchmark/hm/decode.sh new file mode 100755 index 0000000000..624c8ba729 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/hm/decode.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +set -euo pipefail + +decoder="$(dirname "$0")"/TAppDecoderHighBitDepthStatic + +usage() { + echo "$0 [-v] <input.bin> <output.png>" >&2 + exit 1 +} + +verbose=0 + +while getopts ':hv' arg; do + case "$arg" in + h) + usage + ;; + + v) + verbose=1 + ;; + + \?) + echo "Unrecognized option -$OPTARG" >&2 + exit 1 + ;; + esac +done +shift $((OPTIND-1)) + +if [ $# -lt 2 ]; then + usage +fi + +run() { + if [ "$verbose" -eq 1 ]; then + "$@" + else + "$@" > /dev/null 2>&1 + fi +} + +input="$1" +output="$2" + +bin="$(mktemp)" +yuv="$(mktemp)" +width_file="$(mktemp)" +height_file="$(mktemp)" +icc_file="$(mktemp --suffix=.icc)" + +cleanup() { + rm -- "$bin" "$yuv" "$width_file" "$height_file" "$icc_file" +} +trap cleanup EXIT + +unpack_program="$(cat <<'END' + use File::Copy; + my ($input, $bin, $width_file, $height_file, $icc_file) = @ARGV; + open my $input_fh, '<:raw', $input; + sysread($input_fh, my $size, 8) == 8 or die; + my ($width, $height) = unpack 'NN', $size; + open my $width_fh, '>', $width_file; + print {$width_fh} "$width\n"; + open my $height_fh, '>', $height_file; + print {$height_fh} "$height\n"; + sysread($input_fh, my $icc_size, 4) == 4 or die; + $icc_size = unpack 'N', $icc_size; + sysread($input_fh, my $icc_data, $icc_size) == $icc_size or die; + open my $icc_fh, '>', $icc_file; + print {$icc_fh} $icc_data; + copy $input_fh, $bin; +END +)" +run perl -Mstrict -Mwarnings -Mautodie -e "$unpack_program" -- "$input" "$bin" "$width_file" "$height_file" "$icc_file" + +width="$(cat "$width_file")" +height="$(cat "$height_file")" + +start="$EPOCHREALTIME" +run "$decoder" --OutputBitDepth=10 -b "$bin" -o "$yuv" +end="$EPOCHREALTIME" + +elapsed="$(echo "$end - $start" | bc)" +run echo "Completed in $elapsed seconds" + +echo "$elapsed" > "${output%.png}".time + +run ffmpeg -hide_banner -f rawvideo -vcodec rawvideo -s "${width}x$height" -r 25 -pix_fmt yuv444p10le -i "$yuv" -pix_fmt rgb24 -vf scale=in_color_matrix=bt709 -y "$output" +if [ -s "$icc_file" ]; then + mogrify -profile "$icc_file" "$output" +fi diff --git a/media/libjxl/src/tools/benchmark/hm/encode.sh b/media/libjxl/src/tools/benchmark/hm/encode.sh new file mode 100755 index 0000000000..319ba6953c --- /dev/null +++ b/media/libjxl/src/tools/benchmark/hm/encode.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +set -euo pipefail + +encoder="$(dirname "$0")"/TAppEncoderHighBitDepthStatic +cfg_dir="$(dirname "$0")"/../../../third_party/HEVCSoftware/cfg + +usage() { + echo "$0 [-v] [-q <N>] <input.png> <output.bin>" >&2 + exit 1 +} + +q=27 +verbose=0 + +while getopts ':hq:v' arg; do + case "$arg" in + h) + usage + ;; + + q) + q="$OPTARG" + ;; + + v) + verbose=1 + ;; + + \?) + echo "Unrecognized option -$OPTARG" >&2 + exit 1 + ;; + esac +done +shift $((OPTIND-1)) + +if [ $# -lt 2 ]; then + usage +fi + +run() { + if [ "$verbose" -eq 1 ]; then + "$@" + else + "$@" > /dev/null 2>&1 + fi +} + +input="$1" +output="$2" + +yuv="$(mktemp)" +bin="$(mktemp)" + +to_clean=("$yuv" "$bin") +cleanup() { + rm -- "${to_clean[@]}" +} +trap cleanup EXIT + +run ffmpeg -hide_banner -i "$input" -pix_fmt yuv444p10le -vf scale=out_color_matrix=bt709 -color_primaries bt709 -color_trc bt709 -colorspace bt709 -f rawvideo -y "$yuv" + +width="$(identify -format '%w' "$input")" +height="$(identify -format '%h' "$input")" + +start="$EPOCHREALTIME" +run "$encoder" -c "$cfg_dir"/encoder_intra_main_scc_10.cfg -f 1 -fr 1 -wdt "$width" -hgt "$height" --InputChromaFormat=444 --InputBitDepth=10 --ConformanceWindowMode=1 -i "$yuv" -b "$bin" -q "$q" +end="$EPOCHREALTIME" + +elapsed="$(echo "$end - $start" | bc)" +run echo "Completed in $elapsed seconds" + +echo "$elapsed" > "${output%.bin}".time + +icc="${output%.*}.icc" +if run convert "$input" "$icc"; then + to_clean+=("$icc") +fi + +pack_program="$(cat <<'END' + use File::Copy; + use IO::Handle; + my ($width, $height, $bin, $icc, $output) = @ARGV; + open my $output_fh, '>:raw', $output; + syswrite $output_fh, pack 'NN', $width, $height; + syswrite $output_fh, pack 'N', -s $icc; + copy $icc, $output_fh; + copy $bin, $output_fh; +END +)" +run perl -Mstrict -Mwarnings -Mautodie -e "$pack_program" -- "$width" "$height" "$bin" "$icc" "$output" diff --git a/media/libjxl/src/tools/benchmark/metrics/compute-hdrvdp.m b/media/libjxl/src/tools/benchmark/metrics/compute-hdrvdp.m new file mode 100644 index 0000000000..60e40bf32f --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/compute-hdrvdp.m @@ -0,0 +1,17 @@ +% Copyright (c) the JPEG XL Project Authors. All rights reserved. +% +% Use of this source code is governed by a BSD-style +% license that can be found in the LICENSE file. + +pkg load image; + +args = argv(); + +original_filename = args{1}; +decoded_filename = args{2}; + +original = pfs_read_luminance(original_filename); +decoded = pfs_read_luminance(decoded_filename); + +res = hdrvdp(decoded, original, 'luminance', 30, {}); +printf("%f\n", res.Q); diff --git a/media/libjxl/src/tools/benchmark/metrics/compute-pumetrics.m b/media/libjxl/src/tools/benchmark/metrics/compute-pumetrics.m new file mode 100644 index 0000000000..df0fe4bd0e --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/compute-pumetrics.m @@ -0,0 +1,26 @@ +% Copyright (c) the JPEG XL Project Authors. All rights reserved. +% +% Use of this source code is governed by a BSD-style +% license that can be found in the LICENSE file. + +pkg load image; + +args = argv(); + +metric = args{1}; +original_filename = args{2}; +decoded_filename = args{3}; + +original = pfs_read_luminance(original_filename); +decoded = pfs_read_luminance(decoded_filename); + +switch (metric) + case "psnr" + res = qm_pu2_psnr(original, decoded); + case "ssim" + res = qm_pu2_ssim(original, decoded); + otherwise + error(sprintf("unrecognized metric %s", metric)); +end + +printf("%f\n", res); diff --git a/media/libjxl/src/tools/benchmark/metrics/compute_octave_metric.sh b/media/libjxl/src/tools/benchmark/metrics/compute_octave_metric.sh new file mode 100755 index 0000000000..a31c266592 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/compute_octave_metric.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# Usage: ./compute-octave-metric.sh <original> <decoded> <output> <intensity_target> [octave args...] +# Where octave args do not need to contain -qf or the path to the original and decoded images. + +set -euo pipefail + +original="$1" +decoded="$2" +output="$3" +intensity_target="$4" +shift 4 + +tmpdir="$(mktemp --directory)" + +linearized_original="$(mktemp --tmpdir="$tmpdir" --suffix='.pfm')" +linearized_decoded="$(mktemp --tmpdir="$tmpdir" --suffix='.pfm')" + +cleanup() { + rm -- "$linearized_original" "$linearized_decoded" + rmdir --ignore-fail-on-non-empty -- "$tmpdir" +} +trap cleanup EXIT + +linearize() { + local input="$1" + local output="$2" + convert "$input" -set colorspace sRGB -colorspace RGB -evaluate multiply "$intensity_target" "$output" +} + +linearize "$original" "$linearized_original" +linearize "$decoded" "$linearized_decoded" + +octave -qf "$@" \ + "$linearized_original" "$linearized_decoded" \ + 2> /dev/null \ + > "$output" diff --git a/media/libjxl/src/tools/benchmark/metrics/dists-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/dists-rgb.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/dists-rgb.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/fsim-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/fsim-rgb.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/fsim-rgb.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/fsim-y.sh b/media/libjxl/src/tools/benchmark/metrics/fsim-y.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/fsim-y.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/gmsd-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/gmsd-rgb.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/gmsd-rgb.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/hdr_plots.sh b/media/libjxl/src/tools/benchmark/metrics/hdr_plots.sh new file mode 100755 index 0000000000..4ce5d9fc4b --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/hdr_plots.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"$(dirname "$0")/run_all_hdr_metrics.sh" "$@" | sed -n '/```/q;p' > hdr_results.csv +mkdir -p hdr_plots/ +rm -rf hdr_plots/* +python3 "$(dirname "$0")/plots.py" hdr_results.csv hdr_plots diff --git a/media/libjxl/src/tools/benchmark/metrics/hdrvdp-fixes.patch b/media/libjxl/src/tools/benchmark/metrics/hdrvdp-fixes.patch new file mode 100644 index 0000000000..23f3f17b6d --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/hdrvdp-fixes.patch @@ -0,0 +1,110 @@ +From 44a21be2c4de409f80d90cbcc2c20cb3f42e859e Mon Sep 17 00:00:00 2001 +From: Sami Boukortt <sboukortt@google.com> +Date: Fri, 16 Oct 2020 20:01:02 +0200 +Subject: [PATCH] Fixes for Octave +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Copyright (c) the JPEG XL Project Authors. All rights reserved. + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file. + +---- + +ifft2: https://savannah.gnu.org/bugs/?43742 + +Removing #include <matrix.h>: https://octave.org/doc/v5.2.0/Getting-Started-with-Mex_002dFiles.html +“One important difference between Octave and MATLAB is that the header +"matrix.h" is implicitly included through the inclusion of "mex.h".” + +Length checks: it appears that functions(…).file for MEX files in Octave +is empty. +--- + fast_conv_fft.m | 2 +- + matlabPyrTools_1.4_fixed/MEX/corrDn.c | 1 - + matlabPyrTools_1.4_fixed/MEX/pointOp.c | 1 - + matlabPyrTools_1.4_fixed/MEX/upConv.c | 1 - + matlabPyrTools_1.4_fixed/reconSpyr.m | 2 +- + matlabPyrTools_1.4_fixed/reconSpyrLevs.m | 2 +- + 6 files changed, 3 insertions(+), 6 deletions(-) + +diff --git a/fast_conv_fft.m b/fast_conv_fft.m +index 65ceef8..b89e54b 100644 +--- a/fast_conv_fft.m ++++ b/fast_conv_fft.m +@@ -16,7 +16,7 @@ pad_size = (size(fH)-size(X)); + + fX = fft2( padarray( X, pad_size, pad_value, 'post' ) ); + +-Yl = real(ifft2( fX.*fH, size(fX,1), size(fX,2), 'symmetric' )); ++Yl = real(ifft2( fX.*fH, size(fX,1), size(fX,2))); + + Y = Yl(1:size(X,1),1:size(X,2)); + +diff --git a/matlabPyrTools_1.4_fixed/MEX/corrDn.c b/matlabPyrTools_1.4_fixed/MEX/corrDn.c +index d02e272..17e739e 100755 +--- a/matlabPyrTools_1.4_fixed/MEX/corrDn.c ++++ b/matlabPyrTools_1.4_fixed/MEX/corrDn.c +@@ -6,7 +6,6 @@ RES = corrDn(IM, FILT, EDGES, STEP, START, STOP); + */ + + #define V4_COMPAT +-#include <matrix.h> /* Matlab matrices */ + #include <mex.h> + + #include "convolve.h" +diff --git a/matlabPyrTools_1.4_fixed/MEX/pointOp.c b/matlabPyrTools_1.4_fixed/MEX/pointOp.c +index 3623a02..e553adf 100755 +--- a/matlabPyrTools_1.4_fixed/MEX/pointOp.c ++++ b/matlabPyrTools_1.4_fixed/MEX/pointOp.c +@@ -5,7 +5,6 @@ RES = pointOp(IM, LUT, ORIGIN, INCREMENT, WARNINGS) + */ + + #define V4_COMPAT +-#include <matrix.h> /* Matlab matrices */ + #include <mex.h> + + #include <stddef.h> /* NULL */ +diff --git a/matlabPyrTools_1.4_fixed/MEX/upConv.c b/matlabPyrTools_1.4_fixed/MEX/upConv.c +index 98a2bec..08fdf75 100755 +--- a/matlabPyrTools_1.4_fixed/MEX/upConv.c ++++ b/matlabPyrTools_1.4_fixed/MEX/upConv.c +@@ -6,7 +6,6 @@ RES = upConv(IM, FILT, EDGES, STEP, START, STOP, RES); + */ + + #define V4_COMPAT +-#include <matrix.h> /* Matlab matrices */ + #include <mex.h> + + #include "convolve.h" +diff --git a/matlabPyrTools_1.4_fixed/reconSpyr.m b/matlabPyrTools_1.4_fixed/reconSpyr.m +index 05eeafb..1440d8a 100644 +--- a/matlabPyrTools_1.4_fixed/reconSpyr.m ++++ b/matlabPyrTools_1.4_fixed/reconSpyr.m +@@ -31,7 +31,7 @@ function res = reconSpyr(pyr, pind, filtfile, edges, levs, bands) + % Deterimine whether a MEX version of upConv is available + is_mex = true; + finfo = functions( @upConv ); +-if( strcmp( finfo.file((end-2):end), '.m') ) ++if( length(finfo.file) > 2 && strcmp( finfo.file((end-2):end), '.m') ) + is_mex = false; + end + +diff --git a/matlabPyrTools_1.4_fixed/reconSpyrLevs.m b/matlabPyrTools_1.4_fixed/reconSpyrLevs.m +index ac5e2b1..d3b91d5 100644 +--- a/matlabPyrTools_1.4_fixed/reconSpyrLevs.m ++++ b/matlabPyrTools_1.4_fixed/reconSpyrLevs.m +@@ -11,7 +11,7 @@ function res = reconSpyrLevs(pyr,pind,lofilt,bfilts,edges,levs,bands) + % Deterimine whether MEX version of upConv is available + is_mex = true; + finfo = functions( @upConv ); +-if( strcmp( finfo.file((end-2):end), '.m') ) ++if( length(finfo.file) > 2 && strcmp( finfo.file((end-2):end), '.m') ) + is_mex = false; + end + +-- +2.28.0 + diff --git a/media/libjxl/src/tools/benchmark/metrics/hdrvdp.sh b/media/libjxl/src/tools/benchmark/metrics/hdrvdp.sh new file mode 100755 index 0000000000..659ab85308 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/hdrvdp.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"$(dirname "$0")"/compute_octave_metric.sh "$@" \ + --path "$(dirname "$0")"/../../../third_party/hdrvdp-2.2.2/ \ + "$(dirname "$0")"/compute-hdrvdp.m diff --git a/media/libjxl/src/tools/benchmark/metrics/iqa.py b/media/libjxl/src/tools/benchmark/metrics/iqa.py new file mode 100644 index 0000000000..1be9699926 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/iqa.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +import os +import sys +import pathlib +import torch +from torchvision import transforms +import numpy as np + +path = pathlib.Path(__file__).parent.absolute( +) / '..' / '..' / '..' / 'third_party' / 'IQA-optimization' +sys.path.append(str(path)) + +from IQA_pytorch import SSIM, MS_SSIM, CW_SSIM, GMSD, LPIPSvgg, DISTS, NLPD, FSIM, VSI, VIFs, VIF, MAD + + +# only really works with the output from JXL, but we don't need more than that. +def read_pfm(fname): + with open(fname, 'rb') as f: + header_width_height = [] + while len(header_width_height) < 3: + header_width_height += f.readline().rstrip().split() + header, width, height = header_width_height + assert header == b'PF' or header == b'Pf' + width, height = int(width), int(height) + scale = float(f.readline().rstrip()) + fmt = '<f' if scale < 0 else '>f' + data = np.fromfile(f, fmt) + if header == b'PF': + out = np.reshape(data, (height, width, 3))[::-1, :, :] + else: + out = np.reshape(data, (height, width))[::-1, :] + return out.astype(np.float) + + +D_dict = { + 'cwssim': CW_SSIM, + 'dists': DISTS, + 'fsim': FSIM, + 'gmsd': GMSD, + 'lpips': LPIPSvgg, + 'mad': MAD, + 'msssim': MS_SSIM, + 'nlpd': NLPD, + 'ssim': SSIM, + 'vif': VIF, + 'vsi': VSI, +} + +algo = os.path.basename(sys.argv[1]).split('.')[0] +algo, color = algo.split('-') + +channels = 3 + +if color == 'y': + channels = 1 + + +def Load(path): + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + transform = transforms.Compose([ + transforms.ToTensor(), + ]) + img = read_pfm(path) + if len(img.shape) == 3 and channels == 1: # rgb -> Y + assert img.shape[2] == 3 + tmp = np.zeros((img.shape[0], img.shape[1], 1), dtype=float) + tmp[:, :, 0] = (0.2126 * img[:, :, 0] + 0.7152 * img[:, :, 1] + + 0.0722 * img[:, :, 2]) + img = tmp + if len(img.shape) == 2 and channels == 3: # Y -> rgb + gray = img + img = np.zeros((img.shape[0], img.shape[1], 3), dtype=float) + img[:, :, 0] = img[:, :, 1] = img[:, :, 2] = gray + if len(img.shape) == 3: + img = np.transpose(img, axes=(2, 0, 1)).copy() + return torch.FloatTensor(img).unsqueeze(0).to(device) + + +ref_img = Load(sys.argv[2]) +enc_img = Load(sys.argv[3]) +D = D_dict[algo](channels=channels) +score = D(ref_img, enc_img, as_loss=False) + +with open(sys.argv[4], 'w') as f: + print(score.item(), file=f) diff --git a/media/libjxl/src/tools/benchmark/metrics/iqa_wrapper.sh b/media/libjxl/src/tools/benchmark/metrics/iqa_wrapper.sh new file mode 100755 index 0000000000..1d179fdedc --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/iqa_wrapper.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +python3 "$(dirname "$0")/iqa.py" "$0" "$@" diff --git a/media/libjxl/src/tools/benchmark/metrics/lpips-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/lpips-rgb.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/lpips-rgb.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/mrse.sh b/media/libjxl/src/tools/benchmark/metrics/mrse.sh new file mode 100755 index 0000000000..54d18d6fe0 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/mrse.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +set -euo pipefail + +original="$1" +decoded="$2" +output="$3" +intensity_target="$4" + +tmpdir="$(mktemp --directory)" + +linearized_original="$(mktemp --tmpdir="$tmpdir" --suffix='.pfm')" +linearized_decoded="$(mktemp --tmpdir="$tmpdir" --suffix='.pfm')" + +cleanup() { + rm -- "$linearized_original" "$linearized_decoded" + rmdir --ignore-fail-on-non-empty -- "$tmpdir" +} +trap cleanup EXIT + +linearize() { + local input="$1" + local output="$2" + convert "$input" -set colorspace sRGB -colorspace RGB -evaluate multiply "$intensity_target" "$output" +} + +linearize "$original" "$linearized_original" +linearize "$decoded" "$linearized_decoded" + +"$(dirname "$0")"/../../../third_party/difftest_ng/difftest_ng --mrse "$linearized_original" "$linearized_decoded" \ + | sed -e 's/^MRSE:\s*//' \ + > "$output" diff --git a/media/libjxl/src/tools/benchmark/metrics/msssim-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/msssim-rgb.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/msssim-rgb.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/msssim-y.sh b/media/libjxl/src/tools/benchmark/metrics/msssim-y.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/msssim-y.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/nlpd-y.sh b/media/libjxl/src/tools/benchmark/metrics/nlpd-y.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/nlpd-y.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/plots.py b/media/libjxl/src/tools/benchmark/metrics/plots.py new file mode 100755 index 0000000000..04b2bb24e5 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/plots.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +import csv +import sys +import math +import plotly.graph_objects as go + +_, results, output_dir, *rest = sys.argv +OUTPUT = rest[0] if rest else 'svg' +# valid values: html, svg, png, webp, jpeg, pdf + +with open(results, 'r') as f: + reader = csv.DictReader(f) + all_results = list(reader) + +nonmetric_columns = set([ + "method", "image", "error", "size", "pixels", "enc_speed", "dec_speed", + "bpp", "bppp", "qabpp" +]) + +metrics = set(all_results[0].keys()) - nonmetric_columns + + +def codec(method): + sm = method.split(':') + ssm = set(sm) + speeds = set([ + 'kitten', 'falcon', 'wombat', 'cheetah', 'tortoise', 'squirrel', + 'hare', 'fast' + ]) + s = speeds.intersection(ssm) + if sm[0] == 'custom': + return sm[1] + if sm[0] == 'jxl' and s: + return 'jxl-' + list(s)[0] + return sm[0] + + +data = {(m, img): {c: [] + for c in {codec(x['method']) + for x in all_results}} + for m in metrics for img in {x['image'] + for x in all_results}} + +for r in all_results: + c = codec(r['method']) + img = r['image'] + bpp = r['bpp'] + for m in metrics: + data[(m, img)][c].append((float(bpp), float(r[m]))) + + +def pos(codec): + if 'jxl-dis' in codec: + return 6, codec + elif 'jxl' in codec: + return 7, codec + elif 'avif' in codec: + return 5, codec + elif 'kdu' in codec: + return 4, codec + elif 'heif' in codec: + return 3, codec + elif 'fuif' in codec or 'pik' in codec: + return 2, codec + elif 'jpg' in codec or 'jpeg' in codec or 'web' in codec: + return 1, codec + else: + return 0, codec + + +def style(codec): + configs = { + 'jxl-cheetah': { + 'color': '#e41a1c', + 'dash': '1px, 1px', + 'width': 2 + }, + 'jxl-wombat': { + 'color': '#e41a1c', + 'dash': '2px, 2px', + 'width': 2 + }, + 'jxl-squirrel': { + 'color': '#e41a1c', + 'dash': '5px, 5px', + 'width': 2 + }, + 'jxl-kitten': { + 'color': '#e41a1c', + 'width': 2 + }, + 'jxl-dis-cheetah': { + 'color': '#377eb8', + 'dash': '1px, 1px', + 'width': 2 + }, + 'jxl-dis-wombat': { + 'color': '#377eb8', + 'dash': '2px, 2px', + 'width': 2 + }, + 'jxl-dis-squirrel': { + 'color': '#377eb8', + 'dash': '5px, 5px', + 'width': 2 + }, + 'jxl-dis-kitten': { + 'color': '#377eb8', + 'width': 2 + }, + 'rav1e.avif': { + 'color': '#4daf4a', + 'dash': '3px, 3px', + 'width': 2 + }, + '420.rav1e.avif': { + 'color': '#4daf4a', + 'dash': '1px, 1px', + 'width': 2 + }, + '444.rav1e.avif': { + 'color': '#4daf4a', + 'dash': '3px, 3px', + 'width': 2 + }, + 'psnr.420.aom.avif': { + 'color': '#4daf4a', + 'dash': '5px, 5px', + 'width': 2 + }, + 'psnr.444.aom.avif': { + 'color': '#4daf4a', + 'dash': '7px, 7px', + 'width': 2 + }, + 'ssim.420.aom.avif': { + 'color': '#4daf4a', + 'dash': '9px, 9px', + 'width': 2 + }, + 'ssim.444.aom.avif': { + 'color': '#4daf4a', + 'width': 2 + }, + 'heif': { + 'color': '#984ea3', + 'width': 2 + }, + 'fuif': { + 'color': '#ff7f00', + 'dash': '2px, 2px', + 'width': 2 + }, + 'pik-cfp': { + 'color': '#ff7f00', + 'width': 2 + }, + 'pik-cfp-fast': { + 'color': '#ff7f00', + 'dash': '4px, 4px', + 'width': 2 + }, + 'webp': { + 'color': '#000000', + 'width': 2 + }, + 'jpeg': { + 'color': '#a65628', + 'width': 2 + }, + 'xt.jpg': { + 'color': '#a65628', + 'width': 2 + }, + 'perc1.kdu.j2k': { + 'color': '#f781bf', + 'dash': '1px, 1px', + 'width': 2 + }, + 'perc2.kdu.j2k': { + 'color': '#f781bf', + 'dash': '3px, 3px', + 'width': 2 + }, + 'perc3.kdu.j2k': { + 'color': '#f781bf', + 'dash': '5px, 5px', + 'width': 2 + }, + 'perc4.kdu.j2k': { + 'color': '#f781bf', + 'dash': '7px, 7px', + 'width': 2 + }, + 'default.kdu.j2k': { + 'color': '#f781bf', + 'width': 2 + }, + } + return configs.get(codec, dict()) + + +visible_by_default = set([ + 'jxl-kitten', 'ssim.444.aom.avif', 'heif', 'webp', 'jpeg', 'xt.jpg', + 'default.kdu.j2k' +]) + +column_remap = { + 'p': '6-Butteraugli', + 'dist': 'Max-Butteraugli', + 'psnr': "PSNR-YUV 6/8 Y", + 'MS-SSIM-Y': '-log10(1 - MS-SSIM-Y)', + 'puSSIM': '-log10(1 - puSSIM)', + 'FSIM-Y': '-log10(1 - FSIM-Y)', + 'FSIM-RGB': '-log10(1 - FSIM-RGB)', + 'VMAF': '-log10(1 - VMAF / 100)', +} + + +def remap(metric): + funs = { + 'MS-SSIM-Y': lambda x: -math.log10(1 - x), + 'puSSIM': lambda x: -math.log10(1 - x), + 'FSIM-Y': lambda x: -math.log10(1 - x), + 'FSIM-RGB': lambda x: -math.log10(1 - x), + 'VMAF': lambda x: -math.log10(1 + 1e-8 - x / 100), + } + return funs.get(metric, lambda x: x) + + +for (m, img) in data: + fname = "%s/%s_%s" % (output_dir, m, img) + fig = go.Figure() + for method in sorted(data[(m, img)].keys(), key=pos): + vals = data[(m, img)][method] + zvals = list(zip(*sorted(vals))) + if not zvals: + continue + fig.add_trace( + go.Scatter(x=zvals[0], + y=[remap(m)(x) for x in zvals[1]], + mode='lines', + name=method, + line=style(method), + visible=True + if method in visible_by_default else 'legendonly')) + fig.update_layout(title=img, + xaxis_title='bpp', + yaxis_title=column_remap.get(m, m)) + fig.update_xaxes(type='log') + if OUTPUT == 'html': + fig.write_html(fname + '.html', include_plotlyjs='directory') + else: + fig.write_image(fname + '.' + OUTPUT, scale=4) diff --git a/media/libjxl/src/tools/benchmark/metrics/prepare_metrics.sh b/media/libjxl/src/tools/benchmark/metrics/prepare_metrics.sh new file mode 100755 index 0000000000..7ecfaaf194 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/prepare_metrics.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +set -eu + +MYDIR=$(dirname $(realpath "$0")) + + +main() { + cd "${MYDIR}/../../../third_party" + local zipurl + local repourl + for repourl in \ + 'https://github.com/veluca93/IQA-optimization.git' \ + 'https://github.com/Netflix/vmaf.git' \ + 'https://github.com/thorfdbg/difftest_ng.git' + do + local reponame=$(basename "${repourl%.git}") + local dirname=$(basename "${reponame}") + if [[ ! -e "${dirname}" ]]; then + git clone "${repourl}" + fi + done + for zipurl in \ + 'https://sourceforge.net/projects/hdrvdp/files/hdrvdp/2.2.2/hdrvdp-2.2.2.zip' \ + 'https://sourceforge.net/projects/hdrvdp/files/simple_metrics/1.0/hdr_metrics.zip' + do + local zipfile="$(basename "${zipurl}")" + local dirname="$(basename "${zipfile}" '.zip')" + rm -fr "${dirname}" + if [[ ! -e "${zipfile}" ]]; then + wget -O "${zipfile}.tmp" "${zipurl}" + mv "${zipfile}.tmp" "${zipfile}" + fi + unzip "${zipfile}" "${dirname}"/'*' + done + + pushd hdrvdp-2.2.2 + patch -p1 < ../../tools/benchmark/metrics/hdrvdp-fixes.patch + pushd matlabPyrTools_1.4_fixed + mkoctfile --mex MEX/corrDn.c MEX/convolve.c MEX/wrap.c MEX/edges.c + mkoctfile --mex MEX/pointOp.c + mkoctfile --mex MEX/upConv.c + popd + popd + + + pushd difftest_ng + ./configure + make + popd + + + pushd vmaf/libvmaf + rm -rf build + meson build --buildtype release + ninja -vC build + popd +} +main "$@" + diff --git a/media/libjxl/src/tools/benchmark/metrics/pupsnr.sh b/media/libjxl/src/tools/benchmark/metrics/pupsnr.sh new file mode 100755 index 0000000000..869fc36173 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/pupsnr.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +./compute_octave_metric.sh "$@" \ + --path "$(dirname "$0")"/../../../third_party/hdr_metrics/ \ + "$(dirname "$0")"/compute-pumetrics.m 'psnr' diff --git a/media/libjxl/src/tools/benchmark/metrics/pussim.sh b/media/libjxl/src/tools/benchmark/metrics/pussim.sh new file mode 100755 index 0000000000..957cfa1dc1 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/pussim.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +./compute_octave_metric.sh "$@" \ + --path "$(dirname "$0")"/../../../third_party/hdr_metrics/ \ + "$(dirname "$0")"/compute-pumetrics.m 'ssim' diff --git a/media/libjxl/src/tools/benchmark/metrics/run_all_hdr_metrics.sh b/media/libjxl/src/tools/benchmark/metrics/run_all_hdr_metrics.sh new file mode 100755 index 0000000000..5fb769d667 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/run_all_hdr_metrics.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +set -eu +dir="$(dirname "$0")" + +main() { + local metrics=( + HDR-VDP:"${dir}"/hdrvdp.sh + MRSE:"${dir}"/mrse.sh + puPSNR:"${dir}"/pupsnr.sh + puSSIM:"${dir}"/pussim.sh + ) + + local metrics_args=$(printf '%s' "${metrics[@]/#/,}") + metrics_args=${metrics_args:1} + + + "${dir}/../../../build/tools/benchmark_xl" \ + --print_details_csv \ + --num_threads=32 \ + --error_pnorm=6 \ + --extra_metrics ${metrics_args} \ + "$@" +} + +main "$@" diff --git a/media/libjxl/src/tools/benchmark/metrics/run_all_sdr_metrics.sh b/media/libjxl/src/tools/benchmark/metrics/run_all_sdr_metrics.sh new file mode 100755 index 0000000000..def887b09e --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/run_all_sdr_metrics.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +set -eu +dir="$(dirname "$0")" + +main() { + local metrics=( + FSIM-Y:"${dir}"/fsim-y.sh + FSIM-RGB:"${dir}"/fsim-rgb.sh + LPIPS:"${dir}"/lpips-rgb.sh + MS-SSIM-Y:"${dir}"/msssim-y.sh + NLPD:"${dir}"/nlpd-y.sh + SSIMULACRA:"${dir}"/ssimulacra.sh + VIF:"${dir}"/vif-rgb.sh + VMAF:"${dir}"/vmaf.sh + ) + # other metrics, not in core experiments: +# VSI:"${dir}"/vsi-rgb.sh +# SSIM-RGB:"${dir}"/ssim-rgb.sh +# SSIM-Y:"${dir}"/ssim-y.sh +# GMSD:"${dir}"/gmsd.sh +# DISTS:"${dir}"/dists-rgb.sh +# MS-SSIM-RGB:"${dir}"/msssim-rgb.sh + + local metrics_args=$(printf '%s' "${metrics[@]/#/,}") + metrics_args=${metrics_args:1} + + + "${dir}/../../../build/tools/benchmark_xl" \ + --print_details_csv \ + --num_threads=1 \ + --error_pnorm=6 \ + --extra_metrics ${metrics_args} \ + "$@" +} + +main "$@" diff --git a/media/libjxl/src/tools/benchmark/metrics/sdr_plots.sh b/media/libjxl/src/tools/benchmark/metrics/sdr_plots.sh new file mode 100755 index 0000000000..d97648e8f8 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/sdr_plots.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"$(dirname "$0")/run_all_sdr_metrics.sh" "$@" | sed -n '/```/q;p' > sdr_results.csv +mkdir -p sdr_plots/ +rm -rf sdr_plots/* +python3 "$(dirname "$0")/plots.py" sdr_results.csv sdr_plots diff --git a/media/libjxl/src/tools/benchmark/metrics/ssim-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/ssim-rgb.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/ssim-rgb.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/ssim-y.sh b/media/libjxl/src/tools/benchmark/metrics/ssim-y.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/ssim-y.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/ssimulacra.sh b/media/libjxl/src/tools/benchmark/metrics/ssimulacra.sh new file mode 100755 index 0000000000..65617d1c08 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/ssimulacra.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"$(dirname "$0")"/../../../build/tools/ssimulacra_main "$1" "$2" > "$3" 2>/dev/null diff --git a/media/libjxl/src/tools/benchmark/metrics/vif-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/vif-rgb.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/vif-rgb.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/benchmark/metrics/vmaf.sh b/media/libjxl/src/tools/benchmark/metrics/vmaf.sh new file mode 100755 index 0000000000..ab406d011c --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/vmaf.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +set -euo pipefail + +original="$1" +decoded="$2" +output="$3" + +tmpdir="$(mktemp --directory)" + +exr_original="$(mktemp --tmpdir="$tmpdir" --suffix='.exr')" +exr_decoded="$(mktemp --tmpdir="$tmpdir" --suffix='.exr')" + +yuv_original="$(mktemp --tmpdir="$tmpdir" --suffix='.yuv')" +yuv_decoded="$(mktemp --tmpdir="$tmpdir" --suffix='.yuv')" + +vmaf_csv="$(mktemp --tmpdir="$tmpdir" --suffix='.csv')" + +cleanup() { + rm -- "$exr_original" "$exr_decoded" "$yuv_original" "$yuv_decoded" "$vmaf_csv" + rmdir --ignore-fail-on-non-empty -- "$tmpdir" +} +trap cleanup EXIT + +convert "$original" "$exr_original" +convert "$decoded" "$exr_decoded" + +srgb=(-colorspace bt709 -color_primaries bt709 -color_trc iec61966-2-1) +ffmpeg "${srgb[@]}" -i "$exr_original" -pix_fmt yuv444p10le "${srgb[@]}" -y "$yuv_original" &>/dev/null +ffmpeg "${srgb[@]}" -i "$exr_decoded" -pix_fmt yuv444p10le "${srgb[@]}" -y "$yuv_decoded" &>/dev/null + +"$(dirname "$0")"/../../../third_party/vmaf/libvmaf/build/tools/vmafossexec \ + yuv444p10le \ + "$(identify -format '%w' "$original")" "$(identify -format '%h' "$original")" \ + "$yuv_original" "$yuv_decoded" \ + "$(dirname "$0")/../../../third_party/vmaf/model/vmaf_v0.6.1.pkl" \ + --log-fmt csv --log "$vmaf_csv" &>/dev/null + +read_csv="$(cat <<'END' +import csv +import sys +reader = csv.DictReader(sys.stdin) +for row in reader: + print(row['vmaf']) +END +)" + +python -c "$read_csv" < "$vmaf_csv" > "$output" diff --git a/media/libjxl/src/tools/benchmark/metrics/vsi-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/vsi-rgb.sh new file mode 120000 index 0000000000..9e57c8f660 --- /dev/null +++ b/media/libjxl/src/tools/benchmark/metrics/vsi-rgb.sh @@ -0,0 +1 @@ +iqa_wrapper.sh
\ No newline at end of file diff --git a/media/libjxl/src/tools/bisector b/media/libjxl/src/tools/bisector new file mode 100755 index 0000000000..2552045dfd --- /dev/null +++ b/media/libjxl/src/tools/bisector @@ -0,0 +1,281 @@ +#!/usr/bin/env python +r"""General-purpose bisector + +Prints a space-separated list of values to stdout: +1_if_success_0_otherwise left_x left_f(x) right_x right_f(x) + +Usage examples: + +# Finding the square root of 200 via bisection: +bisector --var=BB --range=0.0,100.0 --target=200 --maxiter=100 \ + --atol_val=1e-12 --rtol_val=0 --cmd='echo "$BB * $BB" | bc' +# => 1 14.142135623730923 199.99999999999923 14.142135623731633 200.0000000000193 + +# Finding an integer approximation to sqrt(200) via bisection: +bisector --var=BB --range=0,100 --target=200 --maxiter=100 \ + --atol_arg=1 --cmd='echo "$BB * $BB" | bc' +# => 1 14 196.0 15 225.0 + +# Finding a change-id that broke something via bisection: +bisector --var=BB --range=0,1000000 --target=0.5 --maxiter=100 \ + --atol_arg=1 \ + --cmd='test $BB -gt 123456 && echo 1 || echo 0' --verbosity=3 +# => 1 123456 0.0 123457 1.0 + +# Finding settings that compress /usr/share/dict/words to a given target size: +bisector --var=BB --range=1,9 --target=250000 --atol_arg=1 \ + --cmd='gzip -$BB </usr/share/dict/words >/tmp/w_$BB.gz; wc -c /tmp/w_$BB.gz' \ + --final='mv /tmp/w_$BB.gz /tmp/words.gz; rm /tmp/w_*.gz' \ + --verbosity=1 +# => 1 3 263170.0 4 240043.0 + +# JXL-encoding with bisection-for-size (tolerance 0.5%): +bisector --var=BB --range=0.1,3.0 --target=3500 --rtol_val=0.005 \ + --cmd='(build/tools/cjxl --distance=$BB /tmp/baseball.png /tmp/baseball_$BB.jxl && wc -c /tmp/baseball_$BB.jxl)' \ + --final='mv /tmp/baseball_$BB.jxl /tmp/baseball.jxl; rm -f /tmp/baseball_*.jxl' \ + --verbosity=1 +# => 1 1.1875 3573.0 1.278125 3481.0 + +# JXL-encoding with bisection-for-bits-per-pixel (tolerance 0.5%), using helper: +bisector --var=BB --range=0.1,3.0 --target=1.2 --rtol_val=0.005 \ + --cmd='(build/tools/cjxl --distance=$BB /tmp/baseball.png /tmp/baseball_$BB.jxl && get_bpp /tmp/baseball_$BB.jxl)' \ + --final='mv /tmp/baseball_$BB.jxl /tmp/baseball.jxl; rm -f /tmp/baseball_*.jxl' \ + --verbosity=1 +# => ... +""" + +import argparse +import os +import re +import subprocess +import sys + + +def _expandvars(vardef, env, + max_recursion=100, + max_length=10**6, + verbosity=0): + """os.path.expandvars() variant using parameter env rather than os.environ.""" + current_expanded = vardef + for num_recursions in range(max_recursion): + if verbosity >= 3: + print(f'_expandvars(): num_recursions={num_recursions}, ' + f'len={len(current_expanded)}' + + (', current: ' + current_expanded if verbosity >= 4 else '')) + if len > max_length: + break + current_expanded, num_replacements = re.subn( + r'$\{(\w+)\}|$(\w+)', + lambda m: env.get(m[1] if m[1] is not None else m[2], ''), + current_expanded) + if num_replacements == 0: + break + return current_expanded + + +def _strtod(string): + """Extracts leftmost float from string (like strtod(3)).""" + match = re.match(r'[+-]?\d*[.]?\d*(?:[eE][+-]?\d+)?', string) + return float(match[0]) if match[0] else None + + +def run_shell_command(shell_command, + bisect_var, bisect_val, + extra_env_defs, + verbosity=0): + """Runs a shell command with env modifications, fetching return value.""" + shell_env = dict(os.environ) + shell_env[bisect_var] = str(bisect_val) + for env_def in extra_env_defs: + varname, vardef = env_def.split('=', 1) + shell_env[varname] = _expandvars(vardev, shell_env, + verbosity=verbosity) + shell_ret = subprocess.run(shell_command, + # We explicitly want subshell semantics! + shell=True, + capture_output=True, + env=shell_env) + stdout = shell_ret.stdout.decode('utf-8') + score = _strtod(stdout) + if verbosity >= 2: + print(f'{bisect_var}={bisect_val} {shell_command} => ' + f'{shell_ret.returncode} # {stdout.strip()}') + return (shell_ret.returncode == 0, # Command was successful? + score) + + +def _bisect(*, + shell_command, + final_shell_command, + target, + int_args, + bisect_var, bisect_left, bisect_right, + rtol_val, atol_val, rtol_arg, atol_arg, + maxiter, + extra_env_defs, + verbosity=0 + ): + """Performs bisection.""" + def _get_val(x): + success, val = run_shell_command(shell_command, + bisect_var, x, + extra_env_defs, + verbosity=verbosity) + if not success: + raise RuntimeError(f'Bisection failed for: {bisect_var}={x}: ' + f'success={success}, val={val}, ' + f'cmd={shell_command}, var={bisect_var}') + return val + # + bisect_mid, value_mid = None, None + try: + value_left = _get_val(bisect_left) + value_right = _get_val(bisect_right) + if (value_left < target) != (target <= value_right): + raise RuntimeError( + f'Cannot bisect: target={target}, value_left={value_left}, ' + f'value_right={value_right}') + for num_iter in range(maxiter): + bisect_mid_f = 0.5 * (bisect_left + bisect_right) + bisect_mid = round(bisect_mid_f) if int_args else bisect_mid_f + value_mid = _get_val(bisect_mid) + if (value_left < target) == (value_mid < target): + # Relative to target, `value_mid` is on the same side + # as `value_left`. + bisect_left = bisect_mid + value_left = value_mid + else: + # Otherwise, this situation must hold for value_right + # ("tertium non datur"). + bisect_right = bisect_mid + value_right = value_mid + if verbosity >= 1: + print(f'bisect target={target}, ' + f'left: {value_left} at {bisect_left}, ' + f'right: {value_right} at {bisect_right}, ' + f'mid: {value_mid} at {bisect_mid}') + delta_val = target - value_mid + if abs(delta_val) <= atol_val + rtol_val * abs(target): + return 1, bisect_left, value_left, bisect_right, value_right + delta_arg = bisect_right - bisect_left + # Also check whether the argument is "within tolerance". + # Here, we have to be careful if bisect_left and bisect_right + # have different signs: Then, their absolute magnitude + # "sets the relevant scale". + if abs(delta_arg) <= atol_arg + ( + rtol_arg * 0.5 * (abs(bisect_left) + abs(bisect_right))): + return 1, bisect_left, value_left, bisect_right, value_right + return 0, bisect_left, value_left, bisect_right, value_right + finally: + # If cleanup is specified, always run it + if final_shell_command: + run_shell_command( + final_shell_command, + bisect_var, + bisect_mid if bisect_mid is not None else bisect_left, + extra_env_defs, verbosity=verbosity) + + +def main(args): + """Main entry point.""" + parser = argparse.ArgumentParser(description='mhtml_walk args') + parser.add_argument( + '--var', + help='The variable to use for bisection.', + default='BISECT') + parser.add_argument( + '--range', + help=('The argument range for bisecting, as {low},{high}. ' + 'If no argument has a decimal dot, assume integer parameters.'), + default='0.0,1.0') + parser.add_argument( + '--max', + help='The maximal value for bisecting.', + type=float, + default=0.0) + parser.add_argument( + '--target', + help='The target value to aim for.', + type=float, + default=1.0) + parser.add_argument( + '--maxiter', + help='The maximal number of iterations to perform.', + type=int, + default=40) + parser.add_argument( + '--rtol_val', + help='Relative tolerance to accept for deviations from target value.', + type=float, + default=0.0) + parser.add_argument( + '--atol_val', + help='Absolute tolerance to accept for deviations from target value.', + type=float, + default=0.0) + parser.add_argument( + '--rtol_arg', + help='Relative tolerance to accept for the argument.', + type=float, + default=0.0) + parser.add_argument( + '--atol_arg', + help=('Absolute tolerance to accept for the argument ' + '(e.g. for bisecting change-IDs).'), + type=float, + default=0.0) + parser.add_argument( + '--verbosity', + help='The verbosity level.', + type=int, + default=1) + parser.add_argument( + '--env', + help=('Comma-separated list of extra environment variables ' + 'to incrementally add before executing the shell-command.'), + default='') + parser.add_argument( + '--cmd', + help=('The shell command to execute. Must print a numerical result ' + 'to stdout.')) + parser.add_argument( + '--final', + help='The cleanup shell command to execute.') + # + parsed = parser.parse_args(args) + extra_env_defs = tuple(filter(None, parsed.env.split(','))) + try: + low_high = parsed.range.split(',') + if len(low_high) != 2: + raise ValueError('--range must be {low},{high}') + int_args = False + low_val, high_val = map(float, low_high) + low_val_int = round(low_val) + high_val_int = round(high_val) + if low_high == [str(low_val_int), str(high_val_int)]: + int_args = True + low_val = low_val_int + high_val = high_val_int + ret = _bisect( + shell_command=parsed.cmd, + final_shell_command=parsed.final, + target=parsed.target, + int_args=int_args, + bisect_var=parsed.var, + bisect_left=low_val, + bisect_right=high_val, + rtol_val=parsed.rtol_val, + atol_val=parsed.atol_val, + rtol_arg=parsed.rtol_arg, + atol_arg=parsed.atol_arg, + maxiter=parsed.maxiter, + extra_env_defs=extra_env_defs, + verbosity=parsed.verbosity, + ) + print(' '.join(map(str, ret))) + except Exception as exn: + sys.exit(f'Problem: {exn}') + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/media/libjxl/src/tools/box/CMakeLists.txt b/media/libjxl/src/tools/box/CMakeLists.txt new file mode 100644 index 0000000000..3072cfef0b --- /dev/null +++ b/media/libjxl/src/tools/box/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +add_library(box STATIC EXCLUDE_FROM_ALL + box.cc + box.h +) +# This library can be included into position independent binaries. +set_target_properties(box PROPERTIES POSITION_INDEPENDENT_CODE TRUE) +target_link_libraries(box + jxl-static + jxl_threads-static +) +target_include_directories(box + PRIVATE + "${PROJECT_SOURCE_DIR}" +) + +if(${JPEGXL_ENABLE_DEVTOOLS}) +add_executable(box_list + box_list_main.cc +) +target_link_libraries(box_list + box +) +endif() # JPEGXL_ENABLE_DEVTOOLS diff --git a/media/libjxl/src/tools/box/box.cc b/media/libjxl/src/tools/box/box.cc new file mode 100644 index 0000000000..a60af5b14b --- /dev/null +++ b/media/libjxl/src/tools/box/box.cc @@ -0,0 +1,334 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/box/box.h" + +#include "lib/jxl/base/byte_order.h" // for GetMaximumBrunsliEncodedSize +#include "lib/jxl/jpeg/dec_jpeg_data.h" +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jpegxl { +namespace tools { + +namespace { +// Checks if a + b > size, taking possible integer overflow into account. +bool OutOfBounds(size_t a, size_t b, size_t size) { + size_t pos = a + b; + if (pos > size) return true; + if (pos < a) return true; // overflow happened + return false; +} +} // namespace + +// Parses the header of a BMFF box. Returns the result in a Box struct. +// Sets the position to the end of the box header after parsing. The data size +// is output if known, or must be handled by the caller and runs until the end +// of the container file if not known. +jxl::Status ParseBoxHeader(const uint8_t** next_in, size_t* available_in, + Box* box) { + size_t pos = 0; + size_t size = *available_in; + const uint8_t* in = *next_in; + + if (OutOfBounds(pos, 8, size)) return JXL_FAILURE("out of bounds"); + + const size_t initial_pos = pos; + + // Total box_size including this header itself. + uint64_t box_size = LoadBE32(in + pos); + memcpy(box->type, in + pos + 4, 4); + + pos += 8; + + if (box_size == 1) { + // If the size is 1, it indicates extended size read from 64-bit integer. + if (OutOfBounds(pos, 8, size)) return JXL_FAILURE("out of bounds"); + box_size = LoadBE64(in + pos); + pos += 8; + } + + if (!memcmp("uuid", box->type, 4)) { + if (OutOfBounds(pos, 16, size)) return JXL_FAILURE("out of bounds"); + memcpy(box->extended_type, in + pos, 16); + pos += 16; + } + + // This is the end of the box header, the box data begins here. Handle + // the data size now. + const size_t data_pos = pos; + const size_t header_size = data_pos - initial_pos; + + if (box_size != 0) { + if (box_size < header_size) { + return JXL_FAILURE("invalid box size"); + } + box->data_size_given = true; + box->data_size = box_size - header_size; + } else { + // The size extends to the end of the file. We don't necessarily know the + // end of the file here, since the input size may be only part of the full + // container file. Indicate the size is not given, the caller must handle + // this. + box->data_size_given = false; + box->data_size = 0; + } + + // The remaining bytes are the data. If the box is a full box, the first + // bytes of the data have a certain structure but this is to be handled by + // the caller for the appropriate box type. + *next_in += pos; + *available_in -= pos; + + return true; +} + +jxl::Status AppendBoxHeader(const Box& box, jxl::PaddedBytes* out) { + bool use_extended = !memcmp("uuid", box.type, 4); + + uint64_t box_size = 0; + bool large_size = false; + if (box.data_size_given) { + box_size = box.data_size + 8 + (use_extended ? 16 : 0); + if (box_size >= 0x100000000ull) { + large_size = true; + } + } + + out->resize(out->size() + 4); + StoreBE32(large_size ? 1 : box_size, &out->back() - 4 + 1); + + out->resize(out->size() + 4); + memcpy(&out->back() - 4 + 1, box.type, 4); + + if (large_size) { + out->resize(out->size() + 8); + StoreBE64(box_size, &out->back() - 8 + 1); + } + + if (use_extended) { + out->resize(out->size() + 16); + memcpy(&out->back() - 16 + 1, box.extended_type, 16); + } + + return true; +} + +bool IsContainerHeader(const uint8_t* data, size_t size) { + const uint8_t box_header[] = {0, 0, 0, 0xc, 'J', 'X', + 'L', ' ', 0xd, 0xa, 0x87, 0xa}; + if (size < sizeof(box_header)) return false; + return memcmp(box_header, data, sizeof(box_header)) == 0; +} + +jxl::Status DecodeJpegXlContainerOneShot(const uint8_t* data, size_t size, + JpegXlContainer* container) { + const uint8_t* in = data; + size_t available_in = size; + + container->exif = nullptr; + container->exif_size = 0; + container->exfc = nullptr; + container->exfc_size = 0; + container->xml.clear(); + container->xmlc.clear(); + container->jumb = nullptr; + container->jumb_size = 0; + container->codestream.clear(); + container->jpeg_reconstruction = nullptr; + container->jpeg_reconstruction_size = 0; + + size_t box_index = 0; + + while (available_in != 0) { + Box box; + if (!ParseBoxHeader(&in, &available_in, &box)) { + return JXL_FAILURE("Invalid box header"); + } + + size_t data_size = box.data_size_given ? box.data_size : available_in; + + if (box.data_size > available_in) { + return JXL_FAILURE("Unexpected end of file"); + } + + if (box_index == 0) { + // TODO(lode): leave out magic signature box? + // Must be magic signature box. + if (memcmp("JXL ", box.type, 4) != 0) { + return JXL_FAILURE("Invalid magic signature"); + } + if (box.data_size != 4) return JXL_FAILURE("Invalid magic signature"); + if (in[0] != 0xd || in[1] != 0xa || in[2] != 0x87 || in[3] != 0xa) { + return JXL_FAILURE("Invalid magic signature"); + } + } else if (box_index == 1) { + // Must be ftyp box. + if (memcmp("ftyp", box.type, 4) != 0) { + return JXL_FAILURE("Invalid ftyp"); + } + if (box.data_size != 12) return JXL_FAILURE("Invalid ftyp"); + const char* expected = "jxl \0\0\0\0jxl "; + if (memcmp(expected, in, 12) != 0) return JXL_FAILURE("Invalid ftyp"); + } else if (!memcmp("jxli", box.type, 4)) { + // TODO(lode): parse JXL frame index box + if (!container->codestream.empty()) { + return JXL_FAILURE("frame index must come before codestream"); + } + } else if (!memcmp("jxlc", box.type, 4)) { + container->codestream.append(in, in + data_size); + } else if (!memcmp("jxlp", box.type, 4)) { + if (data_size < 4) return JXL_FAILURE("Invalid jxlp"); + // TODO(jon): don't just ignore the counter + container->codestream.append(in + 4, in + data_size); + } else if (!memcmp("Exif", box.type, 4)) { + if (data_size < 4) return JXL_FAILURE("Invalid Exif"); + uint32_t tiff_header_offset = LoadBE32(in); + if (tiff_header_offset > data_size - 4) + return JXL_FAILURE("Invalid Exif tiff header offset"); + container->exif = in + 4 + tiff_header_offset; + container->exif_size = data_size - 4 - tiff_header_offset; + } else if (!memcmp("Exfc", box.type, 4)) { + container->exfc = in; + container->exfc_size = data_size; + } else if (!memcmp("xml ", box.type, 4)) { + container->xml.emplace_back(in, data_size); + } else if (!memcmp("xmlc", box.type, 4)) { + container->xmlc.emplace_back(in, data_size); + } else if (!memcmp("jumb", box.type, 4)) { + container->jumb = in; + container->jumb_size = data_size; + } else if (!memcmp("jbrd", box.type, 4)) { + container->jpeg_reconstruction = in; + container->jpeg_reconstruction_size = data_size; + } else { + // Do nothing: box not recognized here but may be recognizable by + // other software. + } + + in += data_size; + available_in -= data_size; + box_index++; + } + + return true; +} + +static jxl::Status AppendBoxAndData(const char type[4], const uint8_t* data, + size_t data_size, jxl::PaddedBytes* out, + bool exif = false) { + Box box; + memcpy(box.type, type, 4); + box.data_size = data_size + (exif ? 4 : 0); + box.data_size_given = true; + JXL_RETURN_IF_ERROR(AppendBoxHeader(box, out)); + // for Exif: always use tiff header offset 0 + if (exif) + for (int i = 0; i < 4; i++) out->push_back(0); + out->append(data, data + data_size); + return true; +} + +jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container, + jxl::PaddedBytes* out) { + const unsigned char header[] = {0, 0, 0, 0xc, 'J', 'X', 'L', ' ', + 0xd, 0xa, 0x87, 0xa, 0, 0, 0, 0x14, + 'f', 't', 'y', 'p', 'j', 'x', 'l', ' ', + 0, 0, 0, 0, 'j', 'x', 'l', ' '}; + size_t header_size = sizeof(header); + out->append(header, header + header_size); + + if (container.exif) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("Exif", container.exif, + container.exif_size, out, true)); + } + + if (container.exfc) { + JXL_RETURN_IF_ERROR( + AppendBoxAndData("Exfc", container.exfc, container.exfc_size, out)); + } + + for (size_t i = 0; i < container.xml.size(); i++) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("xml ", container.xml[i].first, + container.xml[i].second, out)); + } + + for (size_t i = 0; i < container.xmlc.size(); i++) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("xmlc", container.xmlc[i].first, + container.xmlc[i].second, out)); + } + + if (container.jpeg_reconstruction) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("jbrd", container.jpeg_reconstruction, + container.jpeg_reconstruction_size, + out)); + } + + if (!container.codestream.empty()) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("jxlc", container.codestream.data(), + container.codestream.size(), out)); + } else { + return JXL_FAILURE("must have primary image frame"); + } + + if (container.jumb) { + JXL_RETURN_IF_ERROR( + AppendBoxAndData("jumb", container.jumb, container.jumb_size, out)); + } + + return true; +} + +// TODO(veluca): the format defined here encode some things multiple times. Fix +// that. +jxl::Status DecodeJpegXlToJpeg(jxl::DecompressParams params, + const JpegXlContainer& container, + jxl::CodecInOut* io, jxl::ThreadPool* pool) { + params.keep_dct = true; + if (container.jpeg_reconstruction == nullptr) { + return JXL_FAILURE( + "Cannot decode to JPEG without a JPEG reconstruction box"); + } + + io->Main().jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>(); + + JXL_RETURN_IF_ERROR(DecodeJPEGData( + jxl::Span<const uint8_t>(container.jpeg_reconstruction, + container.jpeg_reconstruction_size), + io->Main().jpeg_data.get())); + + auto& jpeg_data = io->Main().jpeg_data; + bool have_exif = false, have_xmp = false; + for (size_t i = 0; i < jpeg_data->app_data.size(); i++) { + if (jpeg_data->app_marker_type[i] == jxl::jpeg::AppMarkerType::kExif) { + if (have_exif) + return JXL_FAILURE("Unexpected: more than one Exif box required?"); + if (jpeg_data->app_data[i].size() != container.exif_size + 9) { + return JXL_FAILURE( + "Exif box size does not match JPEG reconstruction data"); + } + have_exif = true; + memcpy(&jpeg_data->app_data[i][3 + 6], container.exif, + container.exif_size); + } + if (jpeg_data->app_marker_type[i] == jxl::jpeg::AppMarkerType::kXMP) { + if (have_xmp) + return JXL_FAILURE("Unexpected: more than one XMP box required?"); + if (jpeg_data->app_data[i].size() != container.xml[0].second + 32) { + return JXL_FAILURE( + "XMP box size does not match JPEG reconstruction data"); + } + have_xmp = true; + memcpy(&jpeg_data->app_data[i][3 + 29], container.xml[0].first, + container.xml[0].second); + } + } + + JXL_RETURN_IF_ERROR(DecodeFile( + params, jxl::Span<const uint8_t>(container.codestream), io, pool)); + return true; +} + +} // namespace tools +} // namespace jpegxl diff --git a/media/libjxl/src/tools/box/box.h b/media/libjxl/src/tools/box/box.h new file mode 100644 index 0000000000..d6fd34fb67 --- /dev/null +++ b/media/libjxl/src/tools/box/box.h @@ -0,0 +1,120 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Tools for reading from / writing to ISOBMFF format for JPEG XL. + +#ifndef TOOLS_BOX_BOX_H_ +#define TOOLS_BOX_BOX_H_ + +#include <string> +#include <vector> + +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/enc_file.h" + +namespace jpegxl { +namespace tools { + +// A top-level box in the box format. +struct Box { + // The type of the box. + // If "uuid", use extended_type instead + char type[4]; + + // The extended_type is only used when type == "uuid". + // Extended types are not used in JXL. However, the box format itself + // supports this so they are handled correctly. + char extended_type[16]; + + // Size of the data, excluding box header. The box ends, and next box + // begins, at data + size. May not be used if data_size_given is false. + uint64_t data_size; + + // If the size is not given, the datasize extends to the end of the file. + // If this field is false, the size field may not be used. + bool data_size_given; +}; + +// Parses the header of a BMFF box. Returns the result in a Box struct. +// Updates next_in and available_in to point at the data in the box, directly +// after the header. +// Sets the data_size if known, or must be handled by the caller and runs until +// the end of the container file if not known. +// NOTE: available_in should be at least 8 up to 32 bytes to parse the +// header without error. +jxl::Status ParseBoxHeader(const uint8_t** next_in, size_t* available_in, + Box* box); + +// TODO(lode): streaming C API +jxl::Status AppendBoxHeader(const Box& box, jxl::PaddedBytes* out); + +// NOTE: after DecodeJpegXlContainerOneShot, the exif etc. pointers point to +// regions within the input data passed to that function. +struct JpegXlContainer { + // Exif metadata, or null if not present in the container. + // The exif data has the format of 'Exif block' as defined in + // ISO/IEC23008-12:2017 Clause A.2.1 + // Here we assume the tiff header offset is 0 and store only the + // actual Exif data (starting with the tiff header MM or II) + // TODO(lode): support the theoretical case of multiple exif boxes + const uint8_t* exif = nullptr; // Not owned + size_t exif_size = 0; + + // Brotli-compressed exif metadata, if present. The data points to the brotli + // compressed stream, it is not decompressed here. + const uint8_t* exfc = nullptr; // Not owned + size_t exfc_size = 0; + + // XML boxes for XMP. There may be multiple XML boxes. + // Each entry points to XML location and provides size. + // The memory is not owned. + // TODO(lode): for C API, cannot use std::vector. + std::vector<std::pair<const uint8_t*, size_t>> xml; + + // Brotli-compressed xml boxes. The bytes are given in brotli-compressed form + // and are not decompressed here. + std::vector<std::pair<const uint8_t*, size_t>> xmlc; + + // JUMBF superbox data, or null if not present in the container. + // The parsing of the nested boxes inside is not handled here. + const uint8_t* jumb = nullptr; // Not owned + size_t jumb_size = 0; + + // TODO(lode): add frame index data + + // JPEG reconstruction data, or null if not present in the container. + const uint8_t* jpeg_reconstruction = nullptr; + size_t jpeg_reconstruction_size = 0; + + // The main JPEG XL codestream, of which there must be 1 in the container. + jxl::PaddedBytes codestream; +}; + +// Returns whether `data` starts with a container header; definitely returns +// false if `size` is less than 12 bytes. +bool IsContainerHeader(const uint8_t* data, size_t size); + +// NOTE: the input data must remain valid as long as `container` is used, +// because its exif etc. pointers point to that data. +jxl::Status DecodeJpegXlContainerOneShot(const uint8_t* data, size_t size, + JpegXlContainer* container); + +// TODO(lode): streaming C API +jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container, + jxl::PaddedBytes* out); + +// TODO(veluca): this doesn't really belong here. +jxl::Status DecodeJpegXlToJpeg(jxl::DecompressParams params, + const JpegXlContainer& container, + jxl::CodecInOut* io, + jxl::ThreadPool* pool = nullptr); + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_BOX_BOX_H_ diff --git a/media/libjxl/src/tools/box/box_list_main.cc b/media/libjxl/src/tools/box/box_list_main.cc new file mode 100644 index 0000000000..40ca910e5e --- /dev/null +++ b/media/libjxl/src/tools/box/box_list_main.cc @@ -0,0 +1,90 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This binary tool lists the boxes of any box-based format (JPEG XL, +// JPEG 2000, MP4, ...). +// This exists as a test for manual verification, rather than an actual tool. + +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/status.h" +#include "tools/box/box.h" + +namespace jpegxl { +namespace tools { + +int RunMain(int argc, const char* argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: %s <filename>", argv[0]); + return 1; + } + + jxl::PaddedBytes compressed; + if (!jxl::ReadFile(argv[1], &compressed)) return 1; + fprintf(stderr, "Read %" PRIuS " compressed bytes\n", compressed.size()); + + const uint8_t* in = compressed.data(); + size_t available_in = compressed.size(); + + fprintf(stderr, "File size: %" PRIuS "\n", compressed.size()); + + while (available_in != 0) { + const uint8_t* start = in; + Box box; + if (!ParseBoxHeader(&in, &available_in, &box)) { + fprintf(stderr, "Failed at %" PRIuS "\n", + compressed.size() - available_in); + break; + } + + size_t data_size = box.data_size_given ? box.data_size : available_in; + size_t header_size = in - start; + size_t box_size = header_size + data_size; + + for (size_t i = 0; i < sizeof(box.type); i++) { + char c = box.type[i]; + if (c < 32 || c > 127) { + printf("Unprintable character in box type, likely not a box file.\n"); + return 0; + } + } + + printf("box: \"%.4s\" box_size:%" PRIuS " data_size:%" PRIuS, box.type, + box_size, data_size); + if (!memcmp("uuid", box.type, 4)) { + printf(" -- extended type:\"%.16s\"", box.extended_type); + } + if (!memcmp("ftyp", box.type, 4) && data_size > 4) { + std::string ftype(in, in + 4); + printf(" -- ftype:\"%s\"", ftype.c_str()); + } + printf("\n"); + + if (data_size > available_in) { + fprintf( + stderr, "Unexpected end of file %" PRIuS " %" PRIuS " %" PRIuS "\n", + static_cast<size_t>(box.data_size), available_in, compressed.size()); + break; + } + + in += data_size; + available_in -= data_size; + } + + return 0; +} + +} // namespace tools +} // namespace jpegxl + +int main(int argc, const char* argv[]) { + return jpegxl::tools::RunMain(argc, argv); +} diff --git a/media/libjxl/src/tools/box/box_test.cc b/media/libjxl/src/tools/box/box_test.cc new file mode 100644 index 0000000000..3146bcfa6f --- /dev/null +++ b/media/libjxl/src/tools/box/box_test.cc @@ -0,0 +1,76 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/box/box.h" + +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include "gtest/gtest.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" + +TEST(BoxTest, BoxTest) { + size_t test_size = 256; + jxl::PaddedBytes exif(test_size); + jxl::PaddedBytes xml0(test_size); + jxl::PaddedBytes xml1(test_size); + jxl::PaddedBytes jumb(test_size); + jxl::PaddedBytes codestream(test_size); + // Generate arbitrary data for the codestreams: the test is not testing + // the contents of them but whether they are preserved in the container. + uint8_t v = 0; + for (size_t i = 0; i < test_size; ++i) { + exif[i] = v++; + xml0[i] = v++; + xml1[i] = v++; + jumb[i] = v++; + codestream[i] = v++; + } + + jpegxl::tools::JpegXlContainer container; + container.exif = exif.data(); + container.exif_size = exif.size(); + container.xml.emplace_back(xml0.data(), xml0.size()); + container.xml.emplace_back(xml1.data(), xml1.size()); + container.xmlc.emplace_back(xml1.data(), xml1.size()); + container.jumb = jumb.data(); + container.jumb_size = jumb.size(); + container.codestream = std::move(codestream); + + jxl::PaddedBytes file; + EXPECT_EQ(true, + jpegxl::tools::EncodeJpegXlContainerOneShot(container, &file)); + + jpegxl::tools::JpegXlContainer container2; + EXPECT_EQ(true, jpegxl::tools::DecodeJpegXlContainerOneShot( + file.data(), file.size(), &container2)); + + EXPECT_EQ(exif.size(), container2.exif_size); + EXPECT_EQ(0, memcmp(exif.data(), container2.exif, container2.exif_size)); + EXPECT_EQ(2u, container2.xml.size()); + if (container2.xml.size() == 2) { + EXPECT_EQ(xml0.size(), container2.xml[0].second); + EXPECT_EQ(0, memcmp(xml0.data(), container2.xml[0].first, + container2.xml[0].second)); + EXPECT_EQ(xml1.size(), container2.xml[1].second); + EXPECT_EQ(0, memcmp(xml1.data(), container2.xml[1].first, + container2.xml[1].second)); + } + EXPECT_EQ(1u, container2.xmlc.size()); + if (container2.xmlc.size() == 1) { + EXPECT_EQ(xml1.size(), container2.xmlc[0].second); + EXPECT_EQ(0, memcmp(xml1.data(), container2.xmlc[0].first, + container2.xmlc[0].second)); + } + EXPECT_EQ(jumb.size(), container2.jumb_size); + EXPECT_EQ(0, memcmp(jumb.data(), container2.jumb, container2.jumb_size)); + EXPECT_EQ(container.codestream.size(), container2.codestream.size()); + EXPECT_EQ(0, memcmp(container.codestream.data(), container2.codestream.data(), + container2.codestream.size())); +} diff --git a/media/libjxl/src/tools/build_cleaner.py b/media/libjxl/src/tools/build_cleaner.py new file mode 100755 index 0000000000..0a0df75635 --- /dev/null +++ b/media/libjxl/src/tools/build_cleaner.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + + +"""build_cleaner.py: Update build files. + +This tool keeps certain parts of the build files up to date. +""" + +import argparse +import collections +import locale +import os +import re +import subprocess +import sys +import tempfile + + +def RepoFiles(src_dir): + """Return the list of files from the source git repository""" + git_bin = os.environ.get('GIT_BIN', 'git') + files = subprocess.check_output([git_bin, '-C', src_dir, 'ls-files']) + ret = files.decode(locale.getpreferredencoding()).splitlines() + ret.sort() + return ret + +def GetPrefixLibFiles(repo_files, prefix, suffixes=('.h', '.cc', '.ui')): + """Gets the library files that start with the prefix and end with source + code suffix.""" + prefix_files = [ + fn for fn in repo_files + if fn.startswith(prefix) and any(fn.endswith(suf) for suf in suffixes)] + return prefix_files + +# Type holding the different types of sources in libjxl: +# * decoder and common sources, +# * encoder-only sources, +# * tests-only sources, +# * google benchmark sources, +# * threads library sources, +# * extras library sources, +# * libjxl (encoder+decoder) public include/ headers and +# * threads public include/ headers. +JxlSources = collections.namedtuple( + 'JxlSources', ['dec', 'enc', 'test', 'gbench', 'threads', + 'extras', 'jxl_public_hdrs', 'threads_public_hdrs']) + +def SplitLibFiles(repo_files): + """Splits the library files into the different groups. + + """ + testonly = ( + 'testdata.h', 'test_utils.h', '_test.h', '_test.cc', + # _testonly.* files are library code used in tests only. + '_testonly.h', '_testonly.cc' + ) + main_srcs = GetPrefixLibFiles(repo_files, 'lib/jxl/') + extras_srcs = GetPrefixLibFiles(repo_files, 'lib/extras/') + test_srcs = [fn for fn in main_srcs + if any(patt in fn for patt in testonly)] + lib_srcs = [fn for fn in main_srcs + if not any(patt in fn for patt in testonly)] + + # Google benchmark sources. + gbench_srcs = sorted(fn for fn in lib_srcs + extras_srcs + if fn.endswith('_gbench.cc')) + lib_srcs = [fn for fn in lib_srcs if fn not in gbench_srcs] + # Exclude optional codecs from extras. + exclude_extras = [ + '/dec/gif', + '/dec/apng', '/enc/apng', + '/dec/exr', '/enc/exr', + '/dec/jpg', '/enc/jpg', + ] + extras_srcs = [fn for fn in extras_srcs if fn not in gbench_srcs and + not any(patt in fn for patt in testonly) and + not any(patt in fn for patt in exclude_extras)] + + + enc_srcs = [fn for fn in lib_srcs + if os.path.basename(fn).startswith('enc_') or + os.path.basename(fn).startswith('butteraugli')] + enc_srcs.extend([ + "lib/jxl/encode.cc", + "lib/jxl/encode_internal.h", + "lib/jxl/gaborish.cc", + "lib/jxl/gaborish.h", + "lib/jxl/huffman_tree.cc", + "lib/jxl/huffman_tree.h", + # Only the inlines in linalg.h header are used in the decoder. + # TODO(deymo): split out encoder only linalg.h functions. + "lib/jxl/linalg.cc", + "lib/jxl/optimize.cc", + "lib/jxl/optimize.h", + "lib/jxl/progressive_split.cc", + "lib/jxl/progressive_split.h", + # TODO(deymo): Add luminance.cc and luminance.h here too. Currently used + # by aux_out.h. + # dec_file is not intended to be part of the decoder library, so move it + # to the encoder source set + "lib/jxl/dec_file.cc", + "lib/jxl/dec_file.h", + ]) + # Temporarily remove enc_bit_writer from the encoder sources: a lot of + # decoder source code still needs to be split up into encoder and decoder. + # Including the enc_bit_writer in the decoder allows to build a working + # libjxl_dec library. + # TODO(lode): remove the dependencies of the decoder on enc_bit_writer and + # remove enc_bit_writer from the dec_srcs again. + enc_srcs.remove("lib/jxl/enc_bit_writer.cc") + enc_srcs.remove("lib/jxl/enc_bit_writer.h") + enc_srcs.sort() + + enc_srcs_set = set(enc_srcs) + lib_srcs = [fn for fn in lib_srcs if fn not in enc_srcs_set] + + # The remaining of the files are in the dec_library. + dec_srcs = lib_srcs + + thread_srcs = GetPrefixLibFiles(repo_files, 'lib/threads/') + thread_srcs = [fn for fn in thread_srcs + if not any(patt in fn for patt in testonly)] + public_hdrs = GetPrefixLibFiles(repo_files, 'lib/include/jxl/') + + threads_public_hdrs = [fn for fn in public_hdrs if '_parallel_runner' in fn] + jxl_public_hdrs = list(sorted(set(public_hdrs) - set(threads_public_hdrs))) + return JxlSources(dec_srcs, enc_srcs, test_srcs, gbench_srcs, thread_srcs, + extras_srcs, jxl_public_hdrs, threads_public_hdrs) + + +def CleanFile(args, filename, pattern_data_list): + """Replace a pattern match with new data in the passed file. + + Given a regular expression pattern with a single () match, it runs the regex + over the passed filename and replaces the match () with the new data. If + args.update is set, it will update the file with the new contents, otherwise + it will return True when no changes were needed. + + Multiple pairs of (regular expression, new data) can be passed to the + pattern_data_list parameter and will be applied in order. + + The regular expression must match at least once in the file. + """ + filepath = os.path.join(args.src_dir, filename) + with open(filepath, 'r') as f: + src_text = f.read() + + if not pattern_data_list: + return True + + new_text = src_text + + for pattern, data in pattern_data_list: + offset = 0 + chunks = [] + for match in re.finditer(pattern, new_text): + chunks.append(new_text[offset:match.start(1)]) + offset = match.end(1) + chunks.append(data) + if not chunks: + raise Exception('Pattern not found for %s: %r' % (filename, pattern)) + chunks.append(new_text[offset:]) + new_text = ''.join(chunks) + + if new_text == src_text: + return True + + if args.update: + print('Updating %s' % filename) + with open(filepath, 'w') as f: + f.write(new_text) + return True + else: + with tempfile.NamedTemporaryFile( + mode='w', prefix=os.path.basename(filename)) as new_file: + new_file.write(new_text) + new_file.flush() + subprocess.call( + ['diff', '-u', filepath, '--label', 'a/' + filename, new_file.name, + '--label', 'b/' + filename]) + return False + + +def BuildCleaner(args): + repo_files = RepoFiles(args.src_dir) + ok = True + + # jxl version + with open(os.path.join(args.src_dir, 'lib/CMakeLists.txt'), 'r') as f: + cmake_text = f.read() + + gni_patterns = [] + for varname in ('JPEGXL_MAJOR_VERSION', 'JPEGXL_MINOR_VERSION', + 'JPEGXL_PATCH_VERSION'): + # Defined in CMakeLists.txt as "set(varname 1234)" + match = re.search(r'set\(' + varname + r' ([0-9]+)\)', cmake_text) + version_value = match.group(1) + gni_patterns.append((r'"' + varname + r'=([0-9]+)"', version_value)) + + jxl_src = SplitLibFiles(repo_files) + + # libjxl + jxl_cmake_patterns = [] + jxl_cmake_patterns.append( + (r'set\(JPEGXL_INTERNAL_SOURCES_DEC\n([^\)]+)\)', + ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.dec))) + jxl_cmake_patterns.append( + (r'set\(JPEGXL_INTERNAL_SOURCES_ENC\n([^\)]+)\)', + ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.enc))) + ok = CleanFile( + args, 'lib/jxl.cmake', + jxl_cmake_patterns) and ok + + ok = CleanFile( + args, 'lib/jxl_benchmark.cmake', + [(r'set\(JPEGXL_INTERNAL_SOURCES_GBENCH\n([^\)]+)\)', + ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.gbench))]) and ok + + gni_patterns.append(( + r'libjxl_dec_sources = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.dec))) + gni_patterns.append(( + r'libjxl_enc_sources = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.enc))) + gni_patterns.append(( + r'libjxl_gbench_sources = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.gbench))) + + + tests = [fn[len('lib/'):] for fn in jxl_src.test if fn.endswith('_test.cc')] + testlib = [fn[len('lib/'):] for fn in jxl_src.test + if not fn.endswith('_test.cc')] + gni_patterns.append(( + r'libjxl_tests_sources = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn for fn in tests))) + gni_patterns.append(( + r'libjxl_testlib_sources = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn for fn in testlib))) + + # libjxl_threads + ok = CleanFile( + args, 'lib/jxl_threads.cmake', + [(r'set\(JPEGXL_THREADS_SOURCES\n([^\)]+)\)', + ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.threads))]) and ok + + gni_patterns.append(( + r'libjxl_threads_sources = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.threads))) + + # libjxl_extras + ok = CleanFile( + args, 'lib/jxl_extras.cmake', + [(r'set\(JPEGXL_EXTRAS_SOURCES\n([^\)]+)\)', + ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.extras))]) and ok + + gni_patterns.append(( + r'libjxl_extras_sources = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.extras))) + + # libjxl_profiler + profiler_srcs = [fn[len('lib/'):] for fn in repo_files + if fn.startswith('lib/profiler')] + ok = CleanFile( + args, 'lib/jxl_profiler.cmake', + [(r'set\(JPEGXL_PROFILER_SOURCES\n([^\)]+)\)', + ''.join(' %s\n' % fn for fn in profiler_srcs))]) and ok + + gni_patterns.append(( + r'libjxl_profiler_sources = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn for fn in profiler_srcs))) + + # Public headers. + gni_patterns.append(( + r'libjxl_public_headers = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn[len('lib/'):] + for fn in jxl_src.jxl_public_hdrs))) + gni_patterns.append(( + r'libjxl_threads_public_headers = \[\n([^\]]+)\]', + ''.join(' "%s",\n' % fn[len('lib/'):] + for fn in jxl_src.threads_public_hdrs))) + + + # Update the list of tests. CMake version include test files in other libs, + # not just in libjxl. + tests = [fn[len('lib/'):] for fn in repo_files + if fn.endswith('_test.cc') and fn.startswith('lib/')] + ok = CleanFile( + args, 'lib/jxl_tests.cmake', + [(r'set\(TEST_FILES\n([^\)]+) ### Files before this line', + ''.join(' %s\n' % fn for fn in tests))]) and ok + ok = CleanFile( + args, 'lib/jxl_tests.cmake', + [(r'set\(TESTLIB_FILES\n([^\)]+)\)', + ''.join(' %s\n' % fn for fn in testlib))]) and ok + + # Update lib.gni + ok = CleanFile(args, 'lib/lib.gni', gni_patterns) and ok + + return ok + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--src-dir', + default=os.path.realpath(os.path.join( + os.path.dirname(__file__), '..')), + help='path to the build directory') + parser.add_argument('--update', default=False, action='store_true', + help='update the build files instead of only checking') + args = parser.parse_args() + if not BuildCleaner(args): + print('Build files need update.') + sys.exit(2) + + +if __name__ == '__main__': + main() diff --git a/media/libjxl/src/tools/build_stats.py b/media/libjxl/src/tools/build_stats.py new file mode 100755 index 0000000000..b1dc1ea393 --- /dev/null +++ b/media/libjxl/src/tools/build_stats.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + + +"""build_stats.py: Gather statistics about sizes of dependencies. + +This tools computes a realistic estimate of the size contribution to a binary +from a statically linked library. Statically linked libraries compiled with +-ffunction-sections and linked -gc-sections mean that we could drop part of the +library at the final binary linking time. This tool takes that into account the +symbols that end up in the final binary and not just all the symbols of the +components. +""" + +import argparse +import collections +import itertools +import json +import os +import re +import struct +import subprocess +import sys +import tempfile + +# Ignore functions with stack size smaller than this value. +MIN_STACK_SIZE = 32 + + +Symbol = collections.namedtuple('Symbol', ['address', 'size', 'typ', 'name']) + +# Represents the stack size information of a function (defined by its address). +SymbolStack = collections.namedtuple('SymbolStack', + ['address', 'stack_size']) + +ObjectStats = collections.namedtuple('ObjectStats', + ['name', 'in_partition', 'size_map']) + +# An object target file in the build system. +Target = collections.namedtuple('Target', + ['name', 'deps', 'filename']) + +# Sections that end up in the binary file. +# t - text (code), d - global non-const data, n/r - read-only data, +# w - weak symbols (likely inline code not inlined), +# v - weak symbols (vtable / typeinfo) +# u - unique symbols +BIN_SIZE = 'tdnrwvu' + +# Sections that end up in static RAM. +RAM_SIZE = 'dbs' + +# u - symbols imported from some other library +# a - absolute address symbols +IGNORE_SYMBOLS = 'ua' + +SIMD_NAMESPACES = [ + 'N_SCALAR', 'N_WASM', 'N_NEON', 'N_PPC8', 'N_SSE4', 'N_AVX2', 'N_AVX3'] + + +def LoadSymbols(filename): + ret = [] + nmout = subprocess.check_output(['nm', '--format=posix', filename]) + for line in nmout.decode('utf-8').splitlines(): + if line.rstrip().endswith(':'): + # Ignore object names. + continue + # symbol_name, symbol_type, (optional) address, (optional) size + symlist = line.rstrip().split(' ') + assert 2 <= len(symlist) <= 4 + ret.append(Symbol( + int(symlist[2], 16) if len(symlist) > 2 else None, + int(symlist[3], 16) if len(symlist) > 3 else None, + symlist[1], + symlist[0])) + return ret + +def LoadTargetCommand(target, build_dir): + stdout = subprocess.check_output( + ['ninja', '-C', build_dir, '-t', 'commands', target]) + # The last command is always the command to build (link) the requested + # target. + command = stdout.splitlines()[-1] + return command.decode('utf-8') + + +def LoadTarget(target, build_dir): + """Loads a build system target and its dependencies into a Target object""" + if target.endswith('.o'): + # Speed up this case. + return Target(target, [], target) + + link_params = LoadTargetCommand(target, build_dir).split() + if 'cmake_symlink_library' in link_params: + # The target is a library symlinked, use the target of the symlink + # instead. + target = link_params[link_params.index('cmake_symlink_library') + 1] + link_params = LoadTargetCommand(target, build_dir).split() + + # The target name is not always the same as the filename of the output, for + # example, "djxl" target generates "tools/djxl" file. + if '-o' in link_params: + target_filename = link_params[link_params.index('-o') + 1] + elif target.endswith('.a'): + # Command is '/path/to/ar', 'qc', 'target.a', ... + target_filename = link_params[link_params.index('qc') + 1] + else: + raise Exception('Unknown "%s" output filename in command: %r' % + (target, link_params)) + + tgt_libs = [] + for entry in link_params: + if not entry or not (entry.endswith('.o') or entry.endswith('.a')): + continue + if entry == target_filename: + continue + fn = os.path.join(build_dir, entry) + if not os.path.exists(fn): + continue + if entry in tgt_libs: + continue + tgt_libs.append(entry) + + return Target(target, tgt_libs, target_filename) + + +def TargetTransitiveDeps(all_tgts, target): + """Returns the list of all transitive dependencies of target""" + ret = all_tgts[target].deps + # There can't be loop dependencies in the targets. + i = 0 + while i < len(ret): + ret.extend(all_tgts[ret[i]].deps) + i += 1 + return ret + + +def LoadStackSizes(filename, binutils=''): + """Loads the stack size used by functions from the ELF. + + This function loads the stack size the compiler stored in the .stack_sizes + section, which can be done by compiling with -fstack-size-section in clang. + """ + with tempfile.NamedTemporaryFile() as stack_sizes_sec: + subprocess.check_call( + [binutils + 'objcopy', '-O', 'binary', '--only-section=.stack_sizes', + '--set-section-flags', '.stack_sizes=alloc', filename, + stack_sizes_sec.name]) + stack_sizes = stack_sizes_sec.read() + # From the documentation: + # The section will contain an array of pairs of function symbol values + # (pointer size) and stack sizes (unsigned LEB128). The stack size values + # only include the space allocated in the function prologue. Functions with + # dynamic stack allocations are not included. + + # Get the pointer format based on the ELF file. + output = subprocess.check_output( + [binutils + 'objdump', '-a', filename]).decode('utf-8') + elf_format = re.search('file format (.*)$', output, re.MULTILINE).group(1) + if elf_format.startswith('elf64-little') or elf_format == 'elf64-x86-64': + pointer_fmt = '<Q' + elif elf_format.startswith('elf32-little') or elf_format == 'elf32-i386': + pointer_fmt = '<I' + else: + raise Exception('Unknown ELF format: %s' % elf_format) + pointer_size = struct.calcsize(pointer_fmt) + + ret = [] + i = 0 + while i < len(stack_sizes): + assert len(stack_sizes) >= i + pointer_size + addr, = struct.unpack_from(pointer_fmt, stack_sizes, i) + i += pointer_size + # Parse LEB128 + size = 0 + for j in range(10): + b = stack_sizes[i] + i += 1 + size += (b & 0x7f) << (7 * j) + if (b & 0x80) == 0: + break + if size >= MIN_STACK_SIZE: + ret.append(SymbolStack(addr, size)) + return ret + + +def TargetSize(symbols, symbol_filter=None): + ret = {} + for sym in symbols: + if not sym.size or (symbol_filter is not None and + sym.name not in symbol_filter): + continue + t = sym.typ.lower() + # We can remove symbols if they appear in multiple objects since they will + # be merged by the linker. + if symbol_filter is not None and (t == sym.typ or t in 'wv'): + symbol_filter.remove(sym.name) + ret.setdefault(t, 0) + ret[t] += sym.size + return ret + + +def PrintStats(stats): + """Print a table with the size stats for a target""" + table = [] + sum_bin_size = 0 + sum_ram_size = 0 + + for objstat in stats: + bin_size = 0 + ram_size = 0 + for typ, size in objstat.size_map.items(): + if typ in BIN_SIZE: + bin_size += size + if typ in RAM_SIZE: + ram_size += size + if typ not in BIN_SIZE + RAM_SIZE: + raise Exception('Unknown type "%s"' % typ) + if objstat.in_partition: + sum_bin_size += bin_size + sum_ram_size += ram_size + + table.append((objstat.name, bin_size, ram_size)) + mx_bin_size = max(row[1] for row in table) + mx_ram_size = max(row[2] for row in table) + + table.append(('-- unknown --', mx_bin_size - sum_bin_size, + mx_ram_size - sum_ram_size)) + + # Print the table + print('%-32s %17s %17s' % ('Object name', 'Binary size', 'Static RAM size')) + for name, bin_size, ram_size in table: + print('%-32s %8d (%5.1f%%) %8d (%5.1f%%)' % ( + name, bin_size, 100. * bin_size / mx_bin_size, + ram_size, (100. * ram_size / mx_ram_size) if mx_ram_size else 0)) + print() + + +def PrintStackStats(tgt_stack_sizes, top_entries=20): + if not tgt_stack_sizes: + return + print(' Stack Symbol name') + for i, (name, size) in zip(itertools.count(), tgt_stack_sizes.items()): + if top_entries > 0 and i >= top_entries: + break + print('%8d %s' % (size, name)) + print() + + +def PrintTopSymbols(tgt_top_symbols): + if not tgt_top_symbols: + return + print(' Size T Symbol name') + for size, typ, name in tgt_top_symbols: + print('%9d %s %s' % (size, typ, name)) + print() + + +def SizeStats(args): + """Main entry point of the program after parsing parameters. + + Computes the size statistics of the given targets and their components.""" + # The dictionary with the stats that we store on disk as a json. This includes + # one entry per passed args.target. + stats = {} + + # Cache of Target object of a target. + tgts = {} + + # Load all the targets. + pending = set(args.target) + while pending: + target = pending.pop() + tgt = LoadTarget(target, args.build_dir) + tgts[target] = tgt + if args.recursive: + for dep in tgt.deps: + if dep not in tgts: + pending.add(dep) + + # Cache of symbols of a target. + syms = {} + # Load the symbols from the all targets and its deps. + all_deps = set(tgts.keys()).union(*[set(tgt.deps) for tgt in tgts.values()]) + for entry in all_deps: + fn = os.path.join(args.build_dir, + tgts[entry].filename if entry in tgts else entry) + syms[entry] = LoadSymbols(fn) + + for target in args.target: + tgt_stats = [] + tgt = tgts[target] + + tgt_syms = syms[target] + used_syms = set() + for sym in tgt_syms: + if sym.typ.lower() in BIN_SIZE + RAM_SIZE: + used_syms.add(sym.name) + elif sym.typ.lower() in IGNORE_SYMBOLS: + continue + else: + print('Unknown: %s %s' % (sym.typ, sym.name)) + + target_path = os.path.join(args.build_dir, tgt.filename) + sym_stacks = [] + if not target_path.endswith('.a'): + sym_stacks = LoadStackSizes(target_path, args.binutils) + symbols_by_addr = {sym.address: sym for sym in tgt_syms + if sym.typ.lower() in 'tw'} + tgt_stack_sizes = collections.OrderedDict() + for sym_stack in sorted(sym_stacks, key=lambda s: -s.stack_size): + tgt_stack_sizes[ + symbols_by_addr[sym_stack.address].name] = sym_stack.stack_size + + tgt_top_symbols = [] + if args.top_symbols: + tgt_top_symbols = [(sym.size, sym.typ, sym.name) for sym in tgt_syms + if sym.name in used_syms and sym.size] + tgt_top_symbols.sort(key=lambda t: (-t[0], t[2])) + tgt_top_symbols = tgt_top_symbols[:args.top_symbols] + + tgt_size = TargetSize(tgt_syms) + tgt_stats.append(ObjectStats(target, False, tgt_size)) + + # Split out by SIMD. + for namespace in SIMD_NAMESPACES: + mangled = str(len(namespace)) + namespace + if not any(mangled in sym.name for sym in tgt_syms): + continue + ret = {} + for sym in tgt_syms: + if not sym.size or mangled not in sym.name: + continue + t = sym.typ.lower() + ret.setdefault(t, 0) + ret[t] += sym.size + # SIMD namespaces are not part of the partition, they are already included + # in the jpegxl-static normally. + if not ret: + continue + tgt_stats.append(ObjectStats('\\--> ' + namespace, False, ret)) + + for obj in tgt.deps: + dep_used_syms = used_syms.copy() + obj_size = TargetSize(syms[obj], used_syms) + if not obj_size: + continue + tgt_stats.append(ObjectStats(os.path.basename(obj), True, obj_size)) + if args.recursive: + # Not really recursive, but it shows all the remaining deps at a second + # level. + for obj_dep in sorted(TargetTransitiveDeps(tgts, obj), + key=os.path.basename): + obj_dep_size = TargetSize(syms[obj_dep], dep_used_syms) + if not obj_dep_size: + continue + tgt_stats.append(ObjectStats( + ' '+ os.path.basename(obj_dep), False, obj_dep_size)) + + PrintStats(tgt_stats) + PrintStackStats(tgt_stack_sizes) + PrintTopSymbols(tgt_top_symbols) + stats[target] = { + 'build': tgt_stats, + 'stack': tgt_stack_sizes, + 'top': tgt_top_symbols, + } + + if args.save: + with open(args.save, 'w') as f: + json.dump(stats, f) + + # Check the maximum stack size. + exit_code = 0 + if args.max_stack: + for name, size in tgt_stack_sizes.items(): + if size > args.max_stack: + print('Error: %s exceeds stack limit: %d vs %d' % ( + name, size, args.max_stack), + file=sys.stderr) + exit_code = 1 + + return exit_code + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('target', type=str, nargs='+', + help='target(s) to analyze') + parser.add_argument('--build-dir', default='build', + help='path to the build directory') + parser.add_argument('--save', default=None, + help='path to save the stats as JSON file') + parser.add_argument('-r', '--recursive', default=False, action='store_true', + help='Print recursive entries.') + parser.add_argument('--top-symbols', default=0, type=int, + help='Number of largest symbols to print') + parser.add_argument('--binutils', default='', + help='prefix path to binutils tools, such as ' + 'aarch64-linux-gnu-') + parser.add_argument('--max-stack', default=None, type=int, + help=('Maximum static stack size of a function. If a ' + 'static stack is larger it will exit with an error ' + 'code.')) + args = parser.parse_args() + sys.exit(SizeStats(args)) + + +if __name__ == '__main__': + main() diff --git a/media/libjxl/src/tools/butteraugli_main.cc b/media/libjxl/src/tools/butteraugli_main.cc new file mode 100644 index 0000000000..f212aadd7c --- /dev/null +++ b/media/libjxl/src/tools/butteraugli_main.cc @@ -0,0 +1,142 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdint.h> +#include <stdio.h> + +#include <string> +#include <vector> + +#include "lib/extras/codec.h" +#include "lib/extras/dec/color_hints.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_butteraugli_pnorm.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { +namespace { + +Status WriteImage(Image3F&& image, const std::string& filename) { + ThreadPoolInternal pool(4); + CodecInOut io; + io.metadata.m.SetUintSamples(8); + io.metadata.m.color_encoding = ColorEncoding::SRGB(); + io.SetFromImage(std::move(image), io.metadata.m.color_encoding); + return EncodeToFile(io, filename, &pool); +} + +Status RunButteraugli(const char* pathname1, const char* pathname2, + const std::string& distmap_filename, + const std::string& colorspace_hint, double p, + float intensity_target) { + extras::ColorHints color_hints; + if (!colorspace_hint.empty()) { + color_hints.Add("color_space", colorspace_hint); + } + + CodecInOut io1; + ThreadPoolInternal pool(4); + if (!SetFromFile(pathname1, color_hints, &io1, &pool)) { + fprintf(stderr, "Failed to read image from %s\n", pathname1); + return false; + } + + CodecInOut io2; + if (!SetFromFile(pathname2, color_hints, &io2, &pool)) { + fprintf(stderr, "Failed to read image from %s\n", pathname2); + return false; + } + + if (io1.xsize() != io2.xsize()) { + fprintf(stderr, "Width mismatch: %" PRIuS " %" PRIuS "\n", io1.xsize(), + io2.xsize()); + return false; + } + if (io1.ysize() != io2.ysize()) { + fprintf(stderr, "Height mismatch: %" PRIuS " %" PRIuS "\n", io1.ysize(), + io2.ysize()); + return false; + } + + ImageF distmap; + ButteraugliParams ba_params; + ba_params.hf_asymmetry = 0.8f; + ba_params.xmul = 1.0f; + ba_params.intensity_target = intensity_target; + const float distance = ButteraugliDistance(io1.Main(), io2.Main(), ba_params, + GetJxlCms(), &distmap, &pool); + printf("%.10f\n", distance); + + double pnorm = ComputeDistanceP(distmap, ba_params, p); + printf("%g-norm: %f\n", p, pnorm); + + if (!distmap_filename.empty()) { + float good = ButteraugliFuzzyInverse(1.5); + float bad = ButteraugliFuzzyInverse(0.5); + JXL_CHECK( + WriteImage(CreateHeatMapImage(distmap, good, bad), distmap_filename)); + } + return true; +} + +} // namespace +} // namespace jxl + +int main(int argc, char** argv) { + if (argc < 3) { + fprintf(stderr, + "Usage: %s <reference> <distorted> [--distmap <distmap>] " + "[--intensity_target <intensity_target>]\n" + "[--colorspace <colorspace_hint>]\n" + "NOTE: images get converted to linear sRGB for butteraugli. Images" + " without attached profiles (such as ppm or pfm) are interpreted" + " as nonlinear sRGB. The hint format is RGB_D65_SRG_Rel_Lin for" + " linear sRGB. Intensity target is viewing conditions screen nits" + ", defaults to 80.\n", + argv[0]); + return 1; + } + std::string distmap; + std::string colorspace; + double p = 3; + float intensity_target = 80.0; // sRGB intensity target. + for (int i = 3; i < argc; i++) { + if (std::string(argv[i]) == "--distmap" && i + 1 < argc) { + distmap = argv[++i]; + } else if (std::string(argv[i]) == "--colorspace" && i + 1 < argc) { + colorspace = argv[++i]; + } else if (std::string(argv[i]) == "--intensity_target" && i + 1 < argc) { + intensity_target = std::stof(std::string(argv[i + 1])); + } else if (std::string(argv[i]) == "--pnorm" && i + 1 < argc) { + char* end; + p = strtod(argv[++i], &end); + if (end == argv[i]) { + fprintf(stderr, "Failed to parse pnorm \"%s\".\n", argv[i]); + return 1; + } + } else { + fprintf(stderr, "Unrecognized flag \"%s\".\n", argv[i]); + return 1; + } + } + + return jxl::RunButteraugli(argv[1], argv[2], distmap, colorspace, p, + intensity_target) + ? 0 + : 1; +} diff --git a/media/libjxl/src/tools/check_author.py b/media/libjxl/src/tools/check_author.py new file mode 100755 index 0000000000..ae1c2798fb --- /dev/null +++ b/media/libjxl/src/tools/check_author.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + + +"""check_author.py: Check that a given author is listed in the AUTHORS file.""" + +import argparse +import fnmatch +import os +import re +import sys + + +def IsAuthorInFile(email, name, filename): + """Return whether we find the name/email in the authors filename""" + # Organization emails have emails listed as <*@domain.com>. This matches those + # patterns. + email_pattern_regex = re.compile(r'.*<([^>]+)>') + + with open(filename, 'r') as f: + for line in f: + line = line.strip() + if line.startswith('#') or not line: + continue + # Exact match for a line without an email is OK. + if line == name: + return True + # Exact email address match is OK, even if the name is different. + if fnmatch.fnmatch(line, '* <%s>' % email): + print( + "User %s <%s> matched with different name %s" % (name, email, line), + file=sys.stderr) + return True + # Organizations often have *@domain.com email patterns which don't match + # the name. + if '*' in line: + m = email_pattern_regex.match(line) + if m and fnmatch.fnmatch(email, m.group(1)): + print("User %s <%s> matched pattern %s" % (name, email, line), + file=sys.stderr) + return True + return False + +def IndividualsInAlphabeticOrder(filename): + """Checks if the names are in alphabetic order""" + with open(filename, 'r') as f: + lines = f.readlines() + individual_header = '# Individuals:\n' + if individual_header in lines: + individual_authors = lines[lines.index(individual_header) + 1:] + sorted_authors = sorted(individual_authors, key=str.casefold) + if sorted_authors == individual_authors: + print("Individual authors are sorted alphabetically.") + return True + else: + print("Individual authors are not sorted alphabetically." + " The expected order is:") + print(''.join(sorted_authors)) + return False + else: + print("Cannot find line '# Individuals:' in file.") + return False + + +def CheckAuthor(args): + authors_path = os.path.join(args.source_dir, 'AUTHORS') + author_in_file = IsAuthorInFile( + args.email, args.name, authors_path) + if not author_in_file: + print("User %s <%s> not found, please add yourself to the AUTHORS file" % ( + args.name, args.email), + file=sys.stderr) + + sorted_alphabetically = IndividualsInAlphabeticOrder(authors_path) + if not sorted_alphabetically: + print("Authors not in alphabetical order, please sort them.", file=sys.stderr) + if not author_in_file or not sorted_alphabetically: + if not args.dry_run: + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('email', type=str, + help='email of the commit author to check') + parser.add_argument('name', type=str, + help='name of the commit author to check') + parser.add_argument( + '--source-dir', + default=os.path.dirname(os.path.dirname(os.path.realpath(__file__))), + help='path to the source directory where the AUTHORS file is located') + parser.add_argument('--dry-run', default=False, action='store_true', + help='Don\'t return an exit code in case of failure') + args = parser.parse_args() + CheckAuthor(args) + + +if __name__ == '__main__': + main() diff --git a/media/libjxl/src/tools/cjpeg_hdr.cc b/media/libjxl/src/tools/cjpeg_hdr.cc new file mode 100644 index 0000000000..cfe272ee25 --- /dev/null +++ b/media/libjxl/src/tools/cjpeg_hdr.cc @@ -0,0 +1,306 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <stdlib.h> + +#include <tuple> + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "tools/cjpeg_hdr.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/extras/codec.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_adaptive_quantization.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/enc_transforms.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_metadata.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/jpeg/dec_jpeg_data_writer.h" +#include "lib/jxl/quant_weights.h" + +HWY_BEFORE_NAMESPACE(); +namespace jpegxl { +namespace tools { +namespace HWY_NAMESPACE { +void FillJPEGData(const jxl::Image3F& ycbcr, const jxl::PaddedBytes& icc, + const jxl::ImageF& quant_field, + const jxl::FrameDimensions& frame_dim, + jxl::jpeg::JPEGData* out) { + // JFIF + out->marker_order.push_back(0xE0); + out->app_data.emplace_back(std::vector<uint8_t>{ + 0xe0, // Marker + 0, 16, // Length + 'J', 'F', 'I', 'F', '\0', // ID + 1, 1, // Version (1.1) + 0, // No density units + 0, 1, 0, 1, // Pixel density 1 + 0, 0 // No thumbnail + }); + // ICC + if (!icc.empty()) { + out->marker_order.push_back(0xE2); + std::vector<uint8_t> icc_marker(17 + icc.size()); + icc_marker[0] = 0xe2; + icc_marker[1] = (icc_marker.size() - 1) >> 8; + icc_marker[2] = (icc_marker.size() - 1) & 0xFF; + memcpy(&icc_marker[3], "ICC_PROFILE", 12); + icc_marker[15] = 1; + icc_marker[16] = 1; + memcpy(&icc_marker[17], icc.data(), icc.size()); + out->app_data.push_back(std::move(icc_marker)); + } + + // DQT + out->marker_order.emplace_back(0xdb); + out->quant.resize(2); + out->quant[0].is_last = false; + out->quant[0].index = 0; + out->quant[1].is_last = true; + out->quant[1].index = 1; + jxl::DequantMatrices dequant; + + // mozjpeg q99 + int qluma[64] = { + 1, 1, 1, 1, 1, 1, 1, 2, // + 1, 1, 1, 1, 1, 1, 1, 2, // + 1, 1, 1, 1, 1, 1, 2, 3, // + 1, 1, 1, 1, 1, 1, 2, 3, // + 1, 1, 1, 1, 1, 2, 3, 4, // + 1, 1, 1, 1, 2, 2, 3, 5, // + 1, 1, 2, 2, 3, 3, 5, 6, // + 2, 2, 3, 3, 4, 5, 6, 8, // + }; + // mozjpeg q95 + int qchroma[64] = { + 2, 2, 2, 2, 3, 4, 6, 9, // + 2, 2, 2, 3, 3, 4, 5, 8, // + 2, 2, 2, 3, 4, 6, 9, 14, // + 2, 3, 3, 4, 5, 7, 11, 16, // + 3, 3, 4, 5, 7, 9, 13, 19, // + 4, 4, 6, 7, 9, 12, 17, 24, // + 6, 5, 9, 11, 13, 17, 23, 31, // + 9, 8, 14, 16, 19, 24, 31, 42, // + }; + // Disable quantization for now. + std::fill(std::begin(qluma), std::end(qluma), 1); + std::fill(std::begin(qchroma), std::end(qchroma), 1); + + memcpy(out->quant[0].values.data(), qluma, sizeof(qluma)); + memcpy(out->quant[1].values.data(), qchroma, sizeof(qchroma)); + + // SOF + out->marker_order.emplace_back(0xc2); + out->components.resize(3); + out->height = frame_dim.ysize; + out->width = frame_dim.xsize_padded; + out->components[0].id = 1; + out->components[1].id = 2; + out->components[2].id = 3; + out->components[0].h_samp_factor = out->components[1].h_samp_factor = + out->components[2].h_samp_factor = out->components[0].v_samp_factor = + out->components[1].v_samp_factor = out->components[2].v_samp_factor = + 1; + out->components[0].width_in_blocks = out->components[1].width_in_blocks = + out->components[2].width_in_blocks = frame_dim.xsize_blocks; + out->components[0].quant_idx = 0; + out->components[1].quant_idx = 1; + out->components[2].quant_idx = 1; + out->components[0].coeffs.resize(frame_dim.xsize_blocks * + frame_dim.ysize_blocks * 64); + out->components[1].coeffs.resize(frame_dim.xsize_blocks * + frame_dim.ysize_blocks * 64); + out->components[2].coeffs.resize(frame_dim.xsize_blocks * + frame_dim.ysize_blocks * 64); + + HWY_ALIGN float scratch_space[2 * 64]; + + for (size_t c = 0; c < 3; c++) { + int* qt = c == 0 ? qluma : qchroma; + for (size_t by = 0; by < frame_dim.ysize_blocks; by++) { + for (size_t bx = 0; bx < frame_dim.xsize_blocks; bx++) { + float deadzone = 0.5f / quant_field.Row(by)[bx]; + // Disable quantization for now. + deadzone = 0; + auto q = [&](float coeff, size_t x, size_t y) -> int { + size_t pos = x * 8 + y; + float scoeff = coeff / qt[pos]; + if (pos == 0) { + return std::round(scoeff); + } + if (std::abs(scoeff) < deadzone) return 0; + if (std::abs(scoeff) < 2 * deadzone && x + y >= 7) return 0; + return std::round(scoeff); + }; + HWY_ALIGN float dct[64]; + TransformFromPixels(jxl::AcStrategy::Type::DCT, + ycbcr.PlaneRow(c, 8 * by) + 8 * bx, + ycbcr.PixelsPerRow(), dct, scratch_space); + for (size_t iy = 0; iy < 8; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + float coeff = dct[iy * 8 + ix] * 2040; // not a typo + out->components[c] + .coeffs[(frame_dim.xsize_blocks * by + bx) * 64 + ix * 8 + iy] = + q(coeff, ix, iy); + } + } + } + } + } + + // DHT + // TODO: optimize + out->marker_order.emplace_back(0xC4); + out->huffman_code.resize(2); + out->huffman_code[0].slot_id = 0x00; // DC + out->huffman_code[0].counts = {{0, 0, 0, 0, 13}}; + std::iota(out->huffman_code[0].values.begin(), + out->huffman_code[0].values.end(), 0); + out->huffman_code[0].is_last = false; + + out->huffman_code[1].slot_id = 0x10; // AC + out->huffman_code[1].counts = {{0, 0, 0, 0, 0, 0, 0, 0, 255}}; + std::iota(out->huffman_code[1].values.begin(), + out->huffman_code[1].values.end(), 0); + out->huffman_code[1].is_last = true; + + // SOS + for (size_t _ = 0; _ < 7; _++) { + out->marker_order.emplace_back(0xDA); + } + out->scan_info.resize(7); + // DC + // comp id, DC tbl, AC tbl + out->scan_info[0].num_components = 3; + out->scan_info[0].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0}, + jxl::jpeg::JPEGComponentScanInfo{1, 0, 0}, + jxl::jpeg::JPEGComponentScanInfo{2, 0, 0}}}; + out->scan_info[0].Ss = 0; + out->scan_info[0].Se = 0; + out->scan_info[0].Ah = out->scan_info[0].Al = 0; + // AC 1 - highest bits + out->scan_info[1].num_components = 1; + out->scan_info[1].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0}}}; + out->scan_info[1].Ss = 1; + out->scan_info[1].Se = 63; + out->scan_info[1].Ah = 0; + out->scan_info[1].Al = 1; + + // Copy for X / B-Y + out->scan_info[2] = out->scan_info[1]; + out->scan_info[2].components[0].comp_idx = 1; + out->scan_info[3] = out->scan_info[1]; + out->scan_info[3].components[0].comp_idx = 2; + + // AC 2 - lowest bit + out->scan_info[4].num_components = 1; + out->scan_info[4].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0}}}; + out->scan_info[4].Ss = 1; + out->scan_info[4].Se = 63; + out->scan_info[4].Ah = 1; + out->scan_info[4].Al = 0; + + // Copy for X / B-Y + out->scan_info[5] = out->scan_info[4]; + out->scan_info[5].components[0].comp_idx = 1; + out->scan_info[6] = out->scan_info[4]; + out->scan_info[6].components[0].comp_idx = 2; + + // EOI + out->marker_order.push_back(0xd9); +} +} // namespace HWY_NAMESPACE +} // namespace tools +} // namespace jpegxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE + +namespace jpegxl { +namespace tools { + +HWY_EXPORT(FillJPEGData); + +int HBDJPEGMain(int argc, const char* argv[]) { + if (argc < 3) { + fprintf(stderr, "Usage: %s input output.jpg\n", argv[0]); + return 1; + } + fprintf(stderr, "Compressing %s to %s\n", argv[1], argv[2]); + jxl::CodecInOut io; + if (!jxl::SetFromFile(argv[1], jxl::extras::ColorHints{}, &io)) { + fprintf(stderr, "Failed to read image %s.\n", argv[1]); + return 1; + } + jxl::Image3F ycbcr(jxl::RoundUpToBlockDim(io.xsize()), + jxl::RoundUpToBlockDim(io.ysize())); + ycbcr.ShrinkTo(io.xsize(), io.ysize()); + jxl::FrameDimensions frame_dim; + frame_dim.Set(io.xsize(), io.ysize(), 0, 0, 0, false, 1); + for (size_t y = 0; y < ycbcr.ysize(); y++) { + for (size_t x = 0; x < ycbcr.xsize(); x++) { + float r = io.Main().color()->PlaneRow(0, y)[x]; + float g = io.Main().color()->PlaneRow(1, y)[x]; + float b = io.Main().color()->PlaneRow(2, y)[x]; + ycbcr.PlaneRow(0, y)[x] = + 0.299 * r + 0.587 * g + 0.114 * b - (128. / 255.); + ycbcr.PlaneRow(1, y)[x] = -0.168736 * r - 0.331264 * g + 0.5 * b; + ycbcr.PlaneRow(2, y)[x] = 0.5 * r - 0.418688 * g - 0.081312 * b; + } + } + jxl::Image3F rgb2(ycbcr.xsize(), ycbcr.ysize()); + jxl::Image3F ycbcr2(ycbcr.xsize(), ycbcr.ysize()); + for (size_t y = 0; y < ycbcr.ysize(); y++) { + for (size_t x = 0; x < ycbcr.xsize(); x++) { + ycbcr2.PlaneRow(0, y)[x] = ycbcr.PlaneRow(1, y)[x]; + ycbcr2.PlaneRow(1, y)[x] = ycbcr.PlaneRow(0, y)[x]; + ycbcr2.PlaneRow(2, y)[x] = ycbcr.PlaneRow(2, y)[x]; + } + } + jxl::YcbcrToRgb(ycbcr2, &rgb2, jxl::Rect(ycbcr)); + + PadImageToBlockMultipleInPlace(&ycbcr); + + jxl::Image3F opsin(jxl::RoundUpToBlockDim(io.xsize()), + jxl::RoundUpToBlockDim(io.ysize())); + opsin.ShrinkTo(io.xsize(), io.ysize()); + jxl::ToXYB(io.Main(), nullptr, &opsin, jxl::GetJxlCms()); + PadImageToBlockMultipleInPlace(&opsin); + jxl::ImageF mask; + jxl::ImageF qf = + InitialQuantField(1.0, opsin, frame_dim, nullptr, 1.0, &mask); + + jxl::CodecInOut out; + out.Main().jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>(); + HWY_DYNAMIC_DISPATCH(FillJPEGData) + (ycbcr, io.metadata.m.color_encoding.ICC(), qf, frame_dim, + out.Main().jpeg_data.get()); + jxl::PaddedBytes output; + if (!jxl::jpeg::EncodeImageJPGCoefficients(&out, &output)) { + return 1; + } + if (!jxl::WriteFile(output, argv[2])) { + fprintf(stderr, "Failed to write to \"%s\"\n", argv[2]); + return 1; + } + return 0; +} + +} // namespace tools +} // namespace jpegxl + +int main(int argc, const char** argv) { + return jpegxl::tools::HBDJPEGMain(argc, argv); +} +#endif diff --git a/media/libjxl/src/tools/cjxl.cc b/media/libjxl/src/tools/cjxl.cc new file mode 100644 index 0000000000..7d61feba64 --- /dev/null +++ b/media/libjxl/src/tools/cjxl.cc @@ -0,0 +1,769 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/cjxl.h" + +#include <math.h> +#include <stdint.h> +#include <stdio.h> + +#include <algorithm> +#include <string> +#include <utility> +#include <vector> + +#include "lib/extras/codec.h" +#include "lib/extras/time.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/cache_aligned.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "tools/args.h" +#include "tools/box/box.h" +#include "tools/speed_stats.h" + +namespace jpegxl { +namespace tools { +namespace { + +static inline bool ParseSpeedTier(const char* arg, jxl::SpeedTier* out) { + return jxl::ParseSpeedTier(arg, out); +} +static inline bool ParseColorTransform(const char* arg, + jxl::ColorTransform* out) { + size_t value = 0; + bool ret = ParseUnsigned(arg, &value); + if (ret && value > 2) ret = false; + if (ret) *out = jxl::ColorTransform(value); + return ret; +} +static inline bool ParseIntensityTarget(const char* arg, float* out) { + return ParseFloat(arg, out) && *out > 0; +} +static inline bool ParsePhotonNoiseParameter(const char* arg, float* out) { + return strncmp(arg, "ISO", 3) == 0 && ParseFloat(arg + 3, out) && *out > 0; +} + +// Proposes a distance to try for a given bpp target. This could depend +// on the entropy in the image, too, but let's start with something. +static double ApproximateDistanceForBPP(double bpp) { + return 1.704 * pow(bpp, -0.804); +} + +jxl::Status LoadSaliencyMap(const std::string& filename_heatmap, + jxl::ThreadPool* pool, jxl::ImageF* out_map) { + jxl::CodecInOut io_heatmap; + if (!SetFromFile(filename_heatmap, jxl::extras::ColorHints(), &io_heatmap, + pool)) { + return JXL_FAILURE("Could not load heatmap."); + } + *out_map = std::move(io_heatmap.Main().color()->Plane(0)); + return true; +} + +void SetParametersForSizeOrBitrate(jxl::ThreadPoolInternal* pool, + const size_t pixels, CompressArgs* args) { + CompressArgs s = *args; // Args for search. + + // If fixed size, convert to bitrate. + if (s.params.target_size > 0) { + s.params.target_bitrate = s.params.target_size * 8.0 / pixels; + s.params.target_size = 0; + } + const double target_size = s.params.target_bitrate * (1 / 8.) * pixels; + + double dist = ApproximateDistanceForBPP(s.params.target_bitrate); + s.params.target_bitrate = 0; + double best_dist = 1.0; + double best_loss = 1e99; + + jxl::CodecInOut io; + double decode_mps = 0; + if (!LoadAll(*args, pool, &io, &decode_mps)) { + s.params.butteraugli_distance = static_cast<float>(dist); + printf("couldn't load image\n"); + return; + } + + for (int i = 0; i < 7; ++i) { + s.params.butteraugli_distance = static_cast<float>(dist); + jxl::PaddedBytes candidate; + bool ok = + CompressJxl(io, decode_mps, pool, s, &candidate, /*print_stats=*/false); + if (!ok) { + printf( + "Compression error occurred during the search for best size. " + "Trying with butteraugli distance %.15g\n", + best_dist); + break; + } + printf("Butteraugli distance %.3f yields %6" PRIuS " bytes, %.3f bpp.\n", + dist, candidate.size(), candidate.size() * 8.0 / pixels); + const double ratio = static_cast<double>(candidate.size()) / target_size; + const double loss = std::max(ratio, 1.0 / std::max(ratio, 1e-30)); + if (best_loss > loss) { + best_dist = dist; + best_loss = loss; + } + dist *= ratio; + if (dist < 0.01) { + dist = 0.01; + } + if (dist >= 16.0) { + dist = 16.0; + } + } + args->params.butteraugli_distance = static_cast<float>(best_dist); + args->params.target_bitrate = 0; + args->params.target_size = 0; +} + +const char* ModeFromArgs(const CompressArgs& args) { + if (args.jpeg_transcode) return "JPEG"; + if (args.params.modular_mode) return "Modular"; + return "VarDCT"; +} + +std::string QualityFromArgs(const CompressArgs& args) { + char buf[100]; + if (args.jpeg_transcode) { + snprintf(buf, sizeof(buf), "lossless transcode"); + } else if (args.params.IsLossless()) { + snprintf(buf, sizeof(buf), "lossless"); + } else { + snprintf(buf, sizeof(buf), "d%.3f", args.params.butteraugli_distance); + } + return buf; +} + +void PrintMode(jxl::ThreadPoolInternal* pool, const jxl::CodecInOut& io, + const double decode_mps, const CompressArgs& args) { + const char* mode = ModeFromArgs(args); + const char* speed = SpeedTierName(args.params.speed_tier); + const std::string quality = QualityFromArgs(args); + fprintf(stderr, + "Read %" PRIuS "x%" PRIuS + " image, %.1f MP/s\n" + "Encoding [%s%s, %s, %s", + io.xsize(), io.ysize(), decode_mps, + (args.use_container ? "Container | " : ""), mode, quality.c_str(), + speed); + if (args.use_container) { + if (args.jpeg_transcode && args.store_jpeg_metadata) + fprintf(stderr, " | JPEG reconstruction data"); + if (!io.blobs.exif.empty()) + fprintf(stderr, " | %" PRIuS "-byte Exif", io.blobs.exif.size()); + if (!io.blobs.xmp.empty()) + fprintf(stderr, " | %" PRIuS "-byte XMP", io.blobs.xmp.size()); + if (!io.blobs.jumbf.empty()) + fprintf(stderr, " | %" PRIuS "-byte JUMBF", io.blobs.jumbf.size()); + } + fprintf(stderr, "], %" PRIuS " threads.\n", pool->NumWorkerThreads()); +} + +} // namespace + +void CompressArgs::AddCommandLineOptions(CommandLineParser* cmdline) { + // Positional arguments. + cmdline->AddPositionalOption("INPUT", /* required = */ true, + "the input can be " +#if JPEGXL_ENABLE_APNG + "PNG, APNG, " +#endif +#if JPEGXL_ENABLE_GIF + "GIF, " +#endif +#if JPEGXL_ENABLE_JPEG + "JPEG, " +#else + "JPEG (lossless recompression only), " +#endif +#if JPEGXL_ENABLE_EXR + "EXR, " +#endif + "PPM, PFM, or PGX", + &file_in); + cmdline->AddPositionalOption( + "OUTPUT", /* required = */ true, + "the compressed JXL output file (can be omitted for benchmarking)", + &file_out); + + // Flags. + // TODO(lode): also add options to add exif/xmp/other metadata in the + // container. + // TODO(lode): decide on good name for this flag: box, container, bmff, ... + cmdline->AddOptionFlag( + '\0', "container", + "Always encode using container format (default: only if needed)", + &use_container, &SetBooleanTrue, 1); + + cmdline->AddOptionFlag('\0', "strip", + "Do not encode using container format (strips " + "Exif/XMP/JPEG bitstream reconstruction data)", + &no_container, &SetBooleanTrue, 2); + + cmdline->AddOptionFlag('\0', "strip_jpeg_metadata", + "Do not encode JPEG bitstream reconstruction data", + &store_jpeg_metadata, &SetBooleanFalse, 2); + + // Target distance/size/bpp + opt_distance_id = cmdline->AddOptionValue( + 'd', "distance", "maxError", + ("Max. butteraugli distance, lower = higher quality. Range: 0 .. 25.\n" + " 0.0 = mathematically lossless. Default for already-lossy input " + "(JPEG/GIF).\n" + " 1.0 = visually lossless. Default for other input.\n" + " Recommended range: 0.5 .. 3.0."), + ¶ms.butteraugli_distance, &ParseFloat); + opt_target_size_id = cmdline->AddOptionValue( + '\0', "target_size", "N", + ("Aim at file size of N bytes.\n" + " Compresses to 1 % of the target size in ideal conditions.\n" + " Runs the same algorithm as --target_bpp"), + ¶ms.target_size, &ParseUnsigned, 1); + opt_target_bpp_id = cmdline->AddOptionValue( + '\0', "target_bpp", "BPP", + ("Aim at file size that has N bits per pixel.\n" + " Compresses to 1 % of the target BPP in ideal conditions."), + ¶ms.target_bitrate, &ParseFloat, 1); + + // High-level options + opt_quality_id = cmdline->AddOptionValue( + 'q', "quality", "QUALITY", + "Quality setting (is remapped to --distance). Range: -inf .. 100.\n" + " 100 = mathematically lossless. Default for already-lossy input " + "(JPEG/GIF).\n Positive quality values roughly match libjpeg quality.", + &quality, &ParseFloat); + + cmdline->AddOptionValue( + 'e', "effort", "EFFORT", + "Encoder effort setting. Range: 1 .. 9.\n" + " Default: 7. Higher number is more effort (slower).", + ¶ms.speed_tier, &ParseSpeedTier); + + cmdline->AddOptionValue('\0', "brotli_effort", "B_EFFORT", + "Brotli effort setting. Range: 0 .. 11.\n" + " Default: -1 (based on EFFORT). Higher number is " + "more effort (slower).", + ¶ms.brotli_effort, &ParseSigned, -1); + + cmdline->AddOptionValue( + 's', "speed", "ANIMAL", + "Deprecated synonym for --effort. Valid values are:\n" + " lightning (1), thunder, falcon, cheetah, hare, wombat, squirrel, " + "kitten, tortoise (9)\n" + " Default: squirrel. Values are in order from faster to slower.\n", + ¶ms.speed_tier, &ParseSpeedTier, 2); + + cmdline->AddOptionValue('\0', "faster_decoding", "AMOUNT", + "Favour higher decoding speed. 0 = default, higher " + "values give higher speed at the expense of quality", + ¶ms.decoding_speed_tier, &ParseUnsigned, 2); + + cmdline->AddOptionFlag('p', "progressive", + "Enable progressive/responsive decoding.", + &progressive, &SetBooleanTrue); + + cmdline->AddOptionFlag('\0', "premultiply", + "Force premultiplied (associated) alpha.", + &force_premultiplied, &SetBooleanTrue, 1); + cmdline->AddOptionValue('\0', "keep_invisible", "0|1", + "force disable/enable preserving color of invisible " + "pixels (default: 1 if lossless, 0 if lossy).", + ¶ms.keep_invisible, &ParseOverride, 1); + + cmdline->AddOptionFlag('\0', "centerfirst", + "Put center groups first in the compressed file.", + ¶ms.centerfirst, &SetBooleanTrue, 1); + + cmdline->AddOptionValue('\0', "center_x", "0..XSIZE", + "Put center groups first in the compressed file.", + ¶ms.center_x, &ParseUnsigned, 1); + cmdline->AddOptionValue('\0', "center_y", "0..YSIZE", + "Put center groups first in the compressed file.", + ¶ms.center_y, &ParseUnsigned, 1); + + // Flags. + cmdline->AddOptionFlag('\0', "progressive_ac", + "Use the progressive mode for AC.", + ¶ms.progressive_mode, &SetBooleanTrue, 1); + cmdline->AddOptionFlag('\0', "qprogressive_ac", + "Use the progressive mode for AC.", + ¶ms.qprogressive_mode, &SetBooleanTrue, 1); + cmdline->AddOptionValue('\0', "progressive_dc", "num_dc_frames", + "Use progressive mode for DC.", + ¶ms.progressive_dc, &ParseSigned, 1); + cmdline->AddOptionFlag('m', "modular", + "Use the modular mode (lossy / lossless).", + ¶ms.modular_mode, &SetBooleanTrue, 1); + cmdline->AddOptionFlag('\0', "use_new_heuristics", + "use new and not yet ready encoder heuristics", + ¶ms.use_new_heuristics, &SetBooleanTrue, 2); + + // JPEG modes: parallel Brunsli, pixels to JPEG, or JPEG to Brunsli + cmdline->AddOptionFlag('j', "jpeg_transcode", + "Do lossy transcode of input JPEG file (decode to " + "pixels instead of doing lossless transcode).", + &jpeg_transcode, &SetBooleanFalse, 1); + cmdline->AddOptionFlag('\0', "jpeg_transcode_disable_cfl", + "Disable CFL for lossless JPEG recompression", + ¶ms.force_cfl_jpeg_recompression, &SetBooleanFalse, + 2); + + cmdline->AddOptionValue('\0', "num_threads", "N", + "number of worker threads (zero = none).", + &num_threads, &ParseUnsigned, 1); + cmdline->AddOptionValue('\0', "num_reps", "N", "how many times to compress.", + &num_reps, &ParseUnsigned, 1); + + cmdline->AddOptionValue('\0', "noise", "0|1", + "force disable/enable noise generation.", + ¶ms.noise, &ParseOverride, 1); + cmdline->AddOptionValue( + '\0', "photon_noise", "ISO3200", + "Set the noise to approximately what it would be at a given nominal " + "exposure on a 35mm camera. For formats other than 35mm, or when the " + "whole sensor was not used, you can multiply the ISO value by the " + "equivalence ratio squared, for example by 2.25 for an APS-C camera.", + ¶ms.photon_noise_iso, &ParsePhotonNoiseParameter, 1); + cmdline->AddOptionValue('\0', "dots", "0|1", + "force disable/enable dots generation.", ¶ms.dots, + &ParseOverride, 1); + cmdline->AddOptionValue('\0', "patches", "0|1", + "force disable/enable patches generation.", + ¶ms.patches, &ParseOverride, 1); + cmdline->AddOptionValue( + '\0', "resampling", "-1|1|2|4|8", + "Subsample all color channels by this factor, or use -1 to choose the " + "resampling factor based on distance.", + ¶ms.resampling, &ParseSigned, 0); + cmdline->AddOptionValue( + '\0', "ec_resampling", "1|2|4|8", + "Subsample all extra channels by this factor. If this value is smaller " + "than the resampling of color channels, it will be increased to match.", + ¶ms.ec_resampling, &ParseSigned, 2); + cmdline->AddOptionFlag('\0', "already_downsampled", + "Do not downsample the given input before encoding, " + "but still signal that the decoder should upsample.", + ¶ms.already_downsampled, &SetBooleanTrue, 2); + + cmdline->AddOptionValue( + '\0', "epf", "-1..3", + "Edge preserving filter level (-1 = choose based on quality, default)", + ¶ms.epf, &ParseSigned, 1); + + cmdline->AddOptionValue('\0', "gaborish", "0|1", + "force disable/enable gaborish.", ¶ms.gaborish, + &ParseOverride, 1); + + opt_intensity_target_id = cmdline->AddOptionValue( + '\0', "intensity_target", "N", + ("Intensity target of monitor in nits, higher\n" + " results in higher quality image. Must be strictly positive.\n" + " Default is 255 for standard images, 4000 for input images known to\n" + " to have PQ or HLG transfer function."), + &intensity_target, &ParseIntensityTarget, 1); + + cmdline->AddOptionValue('\0', "saliency_num_progressive_steps", "N", nullptr, + ¶ms.saliency_num_progressive_steps, + &ParseUnsigned, 2); + cmdline->AddOptionValue('\0', "saliency_map_filename", "STRING", nullptr, + &saliency_map_filename, &ParseString, 2); + cmdline->AddOptionValue('\0', "saliency_threshold", "0..1", nullptr, + ¶ms.saliency_threshold, &ParseFloat, 2); + + cmdline->AddOptionValue( + 'x', "dec-hints", "key=value", + "color_space indicates the ColorEncoding, see Description();\n" + "icc_pathname refers to a binary file containing an ICC profile.", + &color_hints, &ParseAndAppendKeyValue, 1); + + cmdline->AddOptionValue( + '\0', "override_bitdepth", "0=use from image, 1-32=override", + "If nonzero, store the given bit depth in the JPEG XL file metadata" + " (1-32), instead of using the bit depth from the original input" + " image.", + &override_bitdepth, &ParseUnsigned, 2); + + opt_color_id = cmdline->AddOptionValue( + 'c', "colortransform", "0..2", "0=XYB, 1=None, 2=YCbCr", + ¶ms.color_transform, &ParseColorTransform, 2); + + // modular mode options + cmdline->AddOptionValue( + 'I', "iterations", "F", + "[modular encoding] fraction of pixels used to learn MA trees " + "(default=0.5, try 0 for no MA and fast decode)", + ¶ms.options.nb_repeats, &ParseFloat, 2); + + cmdline->AddOptionValue( + 'C', "colorspace", "K", + ("[modular encoding] color transform: 0=RGB, 1=YCoCg, " + "2-37=RCT (default: try several, depending on speed)"), + ¶ms.colorspace, &ParseSigned, 1); + + opt_m_group_size_id = cmdline->AddOptionValue( + 'g', "group-size", "K", + ("[modular encoding] set group size to 128 << K " + "(default: 1 or 2)"), + ¶ms.modular_group_size_shift, &ParseUnsigned, 1); + + cmdline->AddOptionValue( + 'P', "predictor", "K", + "[modular encoding] predictor(s) to use: 0=zero, " + "1=left, 2=top, 3=avg0, 4=select, 5=gradient, 6=weighted, " + "7=topright, 8=topleft, 9=leftleft, 10=avg1, 11=avg2, 12=avg3, " + "13=toptop predictive average " + "14=mix 5 and 6, 15=mix everything. Default 14, at slowest speed " + "default 15", + ¶ms.options.predictor, &ParsePredictor, 1); + + cmdline->AddOptionValue( + 'E', "extra-properties", "K", + "[modular encoding] number of extra MA tree properties to use", + ¶ms.options.max_properties, &ParseSigned, 2); + + cmdline->AddOptionValue('\0', "palette", "K", + "[modular encoding] use a palette if image has at " + "most K colors (default: 1024)", + ¶ms.palette_colors, &ParseSigned, 1); + + cmdline->AddOptionFlag( + '\0', "lossy-palette", + "[modular encoding] quantize to a palette that has fewer entries than " + "would be necessary for perfect preservation; for the time being, it is " + "recommended to set --palette=0 with this option to use the default " + "palette only", + ¶ms.lossy_palette, &SetBooleanTrue, 1); + + cmdline->AddOptionValue( + 'X', "pre-compact", "PERCENT", + ("[modular encoding] compact channels (globally) if ratio " + "used/range is below this (default: 80%)"), + ¶ms.channel_colors_pre_transform_percent, &ParseFloat, 2); + + cmdline->AddOptionValue( + 'Y', "post-compact", "PERCENT", + ("[modular encoding] compact channels (per-group) if ratio " + "used/range is below this (default: 80%)"), + ¶ms.channel_colors_percent, &ParseFloat, 2); + + cmdline->AddOptionValue('R', "responsive", "K", + "[modular encoding] do Squeeze transform, 0=false, " + "1=true (default: true if lossy, false if lossless)", + ¶ms.responsive, &ParseSigned, 1); + + cmdline->AddOptionFlag('V', "version", "Print version number and exit", + &version, &SetBooleanTrue, 1); + cmdline->AddOptionFlag('\0', "quiet", "Be more silent", &quiet, + &SetBooleanTrue, 1); + cmdline->AddOptionValue('\0', "print_profile", "0|1", + "Print timing information before exiting", + &print_profile, &ParseOverride, 1); + + cmdline->AddOptionFlag( + 'v', "verbose", + "Verbose output; can be repeated, also applies to help (!).", + ¶ms.verbose, &SetBooleanTrue); +} + +jxl::Status CompressArgs::ValidateArgs(const CommandLineParser& cmdline) { + params.file_in = file_in; + params.file_out = file_out; + + if (file_in == nullptr) { + fprintf(stderr, "Missing INPUT filename.\n"); + return false; + } + + bool got_distance = cmdline.GetOption(opt_distance_id)->matched(); + bool got_target_size = cmdline.GetOption(opt_target_size_id)->matched(); + bool got_target_bpp = cmdline.GetOption(opt_target_bpp_id)->matched(); + bool got_quality = cmdline.GetOption(opt_quality_id)->matched(); + bool got_intensity_target = + cmdline.GetOption(opt_intensity_target_id)->matched(); + + if (got_quality) { + default_settings = false; + if (quality < 100) jpeg_transcode = false; + if (quality < 7 || quality == 100 || params.modular_mode) { + if (jpeg_transcode == false) params.modular_mode = true; + } + // Quality settings roughly match libjpeg qualities. + if (quality >= 100) { + params.butteraugli_distance = 0; + } else if (quality >= 30) { + params.butteraugli_distance = 0.1 + (100 - quality) * 0.09; + } else { + params.butteraugli_distance = + 6.4 + pow(2.5, (30 - quality) / 5.0f) / 6.25f; + } + } + + if (params.resampling > 1 && !params.already_downsampled) + jpeg_transcode = false; + + if (progressive) { + params.qprogressive_mode = true; + params.responsive = 1; + default_settings = false; + } + if (got_target_size || got_target_bpp || got_intensity_target) { + default_settings = false; + } + + if (params.progressive_dc < -1 || params.progressive_dc > 2) { + fprintf(stderr, "Invalid/out of range progressive_dc (%d), try -1 to 2.\n", + params.progressive_dc); + return false; + } + + if (got_distance) { + constexpr float butteraugli_min_dist = 0.1f; + constexpr float butteraugli_max_dist = 25.0f; + if (!(0 <= params.butteraugli_distance && + params.butteraugli_distance <= butteraugli_max_dist)) { + fprintf(stderr, "Invalid/out of range distance, try 0 to %g.\n", + butteraugli_max_dist); + return false; + } + if (params.butteraugli_distance > 0) jpeg_transcode = false; + if (params.butteraugli_distance == 0) { + // Use modular for lossless. + if (jpeg_transcode == false) params.modular_mode = true; + } else if (params.butteraugli_distance < butteraugli_min_dist) { + params.butteraugli_distance = butteraugli_min_dist; + } + default_settings = false; + } + + if (got_target_bpp || got_target_size) { + jpeg_transcode = false; + } + if (params.brotli_effort > 11) { + fprintf(stderr, "Invalid --brotli_effort value\n"); + return false; + } + if (got_target_bpp + got_target_size + got_distance + got_quality > 1) { + fprintf(stderr, + "You can specify only one of '--distance', '-q', " + "'--target_bpp' and '--target_size'. They are all different ways" + " to specify the image quality. When in doubt, use --distance." + " It gives the most visually consistent results.\n"); + return false; + } + + if (!saliency_map_filename.empty()) { + if (!params.progressive_mode) { + saliency_map_filename.clear(); + fprintf(stderr, + "Warning: Specifying --saliency_map_filename only makes sense " + "for --progressive_ac mode.\n"); + } + } + + if (!params.file_in) { + fprintf(stderr, "Missing input filename.\n"); + return false; + } + + if (!cmdline.GetOption(opt_color_id)->matched()) { + // default to RGB for lossless modular + if (params.modular_mode) { + if (params.butteraugli_distance > 0.f) { + params.color_transform = jxl::ColorTransform::kXYB; + } else { + params.color_transform = jxl::ColorTransform::kNone; + } + } + } + + if (override_bitdepth > 32) { + fprintf(stderr, "override_bitdepth must be <= 32\n"); + return false; + } + + if (params.epf > 3) { + fprintf(stderr, "--epf must be in the 0..3 range\n"); + return false; + } + + return true; +} + +jxl::Status CompressArgs::ValidateArgsAfterLoad( + const CommandLineParser& cmdline, const jxl::CodecInOut& io) { + if (!ValidateArgs(cmdline)) return false; + bool got_m_group_size = cmdline.GetOption(opt_m_group_size_id)->matched(); + if (params.modular_mode && !got_m_group_size) { + // Default modular group size: set to 512 if 256 would be silly + const size_t kThinImageThr = 256 + 64; + const size_t kSmallImageThr = 256 + 128; + if (io.xsize() < kThinImageThr || io.ysize() < kThinImageThr || + (io.xsize() < kSmallImageThr && io.ysize() < kSmallImageThr)) { + params.modular_group_size_shift = 2; + } + } + if (!io.blobs.exif.empty() || !io.blobs.xmp.empty() || + !io.blobs.jumbf.empty() || !io.blobs.iptc.empty() || + (jpeg_transcode && store_jpeg_metadata)) { + use_container = true; + } + if (no_container) use_container = false; + if (jpeg_transcode && params.modular_mode) { + fprintf(stderr, + "Error: cannot do lossless JPEG transcode in modular mode.\n"); + return false; + } + if (jpeg_transcode) { + if (params.progressive_mode || params.qprogressive_mode || + params.progressive_dc > 0) { + fprintf(stderr, + "Error: progressive lossless JPEG transcode is not yet " + "implemented.\n"); + return false; + } + } + return true; +} + +jxl::Status LoadAll(CompressArgs& args, jxl::ThreadPoolInternal* pool, + jxl::CodecInOut* io, double* decode_mps) { + const double t0 = jxl::Now(); + + jxl::PaddedBytes encoded; + JXL_RETURN_IF_ERROR(jxl::ReadFile(args.params.file_in, &encoded)); + jxl::extras::Codec input_codec; + bool ok; + if (args.jpeg_transcode && encoded.size() >= 2 && encoded[0] == 0xFF && + encoded[1] == 0xD8) { + input_codec = jxl::extras::Codec::kJPG; + ok = jxl::jpeg::DecodeImageJPG(jxl::Span<const uint8_t>(encoded), io); + } else { + ok = jxl::SetFromBytes(jxl::Span<const uint8_t>(encoded), args.color_hints, + io, nullptr, &input_codec); + } + if (!ok) { + fprintf(stderr, "Failed to read image %s.\n", args.params.file_in); + return false; + } + if (args.intensity_target != 0) { + io->metadata.m.SetIntensityTarget(args.intensity_target); + } + if (input_codec != jxl::extras::Codec::kJPG) args.jpeg_transcode = false; + if (args.jpeg_transcode) args.params.butteraugli_distance = 0; + + if (input_codec == jxl::extras::Codec::kGIF && args.default_settings) { + args.params.modular_mode = true; + args.params.butteraugli_distance = 0; + } + if (args.override_bitdepth != 0) { + if (args.override_bitdepth == 32) { + io->metadata.m.SetFloat32Samples(); + } else { + io->metadata.m.SetUintSamples(args.override_bitdepth); + } + } + if (args.force_premultiplied) { + io->PremultiplyAlpha(); + } + + jxl::ImageF saliency_map; + if (!args.saliency_map_filename.empty()) { + if (!LoadSaliencyMap(args.saliency_map_filename, pool, &saliency_map)) { + fprintf(stderr, "Failed to read saliency map %s.\n", + args.saliency_map_filename.c_str()); + return false; + } + args.params.saliency_map = &saliency_map; + } + + const double t1 = jxl::Now(); + const size_t pixels = io->xsize() * io->ysize(); + *decode_mps = pixels * io->frames.size() * 1E-6 / (t1 - t0); + + return true; +} + +jxl::Status CompressJxl(jxl::CodecInOut& io, double decode_mps, + jxl::ThreadPoolInternal* pool, CompressArgs& args, + jxl::PaddedBytes* compressed, bool print_stats) { + JXL_CHECK(pool); + + const size_t pixels = io.xsize() * io.ysize(); + + if (args.params.target_size > 0 || args.params.target_bitrate > 0) { + // Slow iterative search for parameters that reach target bpp / size. + SetParametersForSizeOrBitrate(pool, pixels, &args); + } + + if (print_stats) PrintMode(pool, io, decode_mps, args); + + // Final/actual compression run (possibly repeated for benchmarking). + jxl::AuxOut aux_out; + if (args.inspector_image3f) { + aux_out.SetInspectorImage3F(args.inspector_image3f); + } + SpeedStats stats; + jxl::PassesEncoderState passes_encoder_state; + if (args.params.use_new_heuristics) { + passes_encoder_state.heuristics = + jxl::make_unique<jxl::FastEncoderHeuristics>(); + } + for (size_t i = 0; i < args.num_reps; ++i) { + const double t0 = jxl::Now(); + jxl::Status ok = false; + if (io.Main().IsJPEG()) { + // TODO(lode): automate this in the encoder. The encoder must in the + // beginning choose to either do all in xyb, or all in non-xyb, write + // that in the xyb_encoded header flag, and persistently keep that state + // to check if every frame uses an allowed color transform. + args.params.color_transform = io.Main().color_transform; + } + ok = EncodeFile(args.params, &io, &passes_encoder_state, compressed, + jxl::GetJxlCms(), &aux_out, pool); + if (!ok) { + fprintf(stderr, "Failed to compress to %s.\n", ModeFromArgs(args)); + return false; + } + const double t1 = jxl::Now(); + stats.NotifyElapsed(t1 - t0); + stats.SetImageSize(io.xsize(), io.ysize()); + } + + if (print_stats) { + const double bpp = + static_cast<double>(compressed->size() * jxl::kBitsPerByte) / pixels; + fprintf(stderr, "Compressed to %" PRIuS " bytes (%.3f bpp%s).\n", + compressed->size(), bpp / io.frames.size(), + io.frames.size() == 1 ? "" : "/frame"); + JXL_CHECK(stats.Print(args.num_threads)); + if (args.params.verbose) { + aux_out.Print(1); + } + } + + return true; +} + +} // namespace tools +} // namespace jpegxl diff --git a/media/libjxl/src/tools/cjxl.h b/media/libjxl/src/tools/cjxl.h new file mode 100644 index 0000000000..1be3500dfd --- /dev/null +++ b/media/libjxl/src/tools/cjxl.h @@ -0,0 +1,109 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_CJXL_H_ +#define TOOLS_CJXL_H_ + +#include <stddef.h> + +#include <string> +#include <thread> +#include <utility> + +#include "lib/extras/dec/color_hints.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/jxl_inspection.h" +#include "tools/cmdline.h" + +namespace jpegxl { +namespace tools { + +struct CompressArgs { + void SetInspectorImage3F(const jxl::InspectorImage3F& inspector) { + inspector_image3f = inspector; + } + + // Add all the command line options to the CommandLineParser. Note that the + // options are tied to the instance that this was called on. + void AddCommandLineOptions(CommandLineParser* cmdline); + + // Post-processes and validates the passed arguments, checking whether all + // passed options are compatible. Returns whether the validation was + // successful. + jxl::Status ValidateArgs(const CommandLineParser& cmdline); + + // Validates the arguments again, having loaded the input so sensible defaults + // can be chosen based on e.g. dimensions. + jxl::Status ValidateArgsAfterLoad(const CommandLineParser& cmdline, + const jxl::CodecInOut& io); + + // Common flags. + bool version = false; + bool use_container = false; + bool no_container = false; + bool quiet = false; + + const char* file_in = nullptr; + const char* file_out = nullptr; + jxl::Override print_profile = jxl::Override::kDefault; + + // Decoding source image flags + jxl::extras::ColorHints color_hints; + + // JXL flags + size_t override_bitdepth = 0; + jxl::CompressParams params; + size_t num_threads = std::thread::hardware_concurrency(); + size_t num_reps = 1; + float intensity_target = 0; + + // Filename for the user provided saliency-map. + std::string saliency_map_filename; + + // Whether to perform lossless transcoding with kVarDCT or kJPEG encoding. + // If true, attempts to load JPEG coefficients instead of pixels. + // Reset to false if input image is not a JPEG. + bool jpeg_transcode = true; + + bool store_jpeg_metadata = true; + + float quality = -1001.f; // Default to lossless if input is already lossy, + // or to VarDCT otherwise. + bool progressive = false; + bool default_settings = true; + bool force_premultiplied = false; + + // Will get passed on to AuxOut. + jxl::InspectorImage3F inspector_image3f; + + // References (ids) of specific options to check if they were matched. + CommandLineParser::OptionId opt_distance_id = -1; + CommandLineParser::OptionId opt_target_size_id = -1; + CommandLineParser::OptionId opt_target_bpp_id = -1; + CommandLineParser::OptionId opt_quality_id = -1; + CommandLineParser::OptionId opt_near_lossless_id = -1; + CommandLineParser::OptionId opt_intensity_target_id = -1; + CommandLineParser::OptionId opt_color_id = -1; + CommandLineParser::OptionId opt_m_group_size_id = -1; +}; + +jxl::Status LoadAll(CompressArgs& args, jxl::ThreadPoolInternal* pool, + jxl::CodecInOut* io, double* decode_mps); + +// The input image must already have been loaded into io using LoadAll. +jxl::Status CompressJxl(jxl::CodecInOut& io, double decode_mps, + jxl::ThreadPoolInternal* pool, CompressArgs& args, + jxl::PaddedBytes* compressed, bool print_stats = true); + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_CJXL_H_ diff --git a/media/libjxl/src/tools/cjxl_bisect_bpp b/media/libjxl/src/tools/cjxl_bisect_bpp new file mode 100755 index 0000000000..d7a1066e1c --- /dev/null +++ b/media/libjxl/src/tools/cjxl_bisect_bpp @@ -0,0 +1,40 @@ +#!/bin/sh +# +# Bisects JPEG XL encoding quality parameter to reach a given +# target bits-per-pixel value. +# (To be used directly, or as a template for tailored processing.) +# +# Usage: cjxl_bisect_size {input_filename} {output_filename} {target_bpp} + +# +# We take the `bisector` tool from $PATH, or, if not available, +# try to locate it in the same directory as the current script. +# The `get_bpp` helper is taken from the same directory as the current script. +# + +input_filename=$1 +output_filename=$2 +target_size=$3 + +script_dir=$(dirname $(readlink -f $0)) +bisect_tool=$(which bisector) +if [ -z $bisect_tool ] ; then + bisect_tool="${script_dir}/bisector" +fi +jxl_get_bpp_helper="${script_dir}/jxl_get_bpp_helper" +# If $CJXL_BIN is set, we use this instead of looking for `cjxl` on $PATH. + +cjxl_bin=${CJXL_BIN} +if [ -z $cjxl_bin ] ; then + cjxl_bin="cjxl" +fi + +# Using `identify` from ImageMagick here. +num_pixels=$(identify -format "%w*%h\n" /tmp/baseball.png|bc) + +# Allow 0.5% tolerance in size (--rtol=0.005). +exec $bisect_tool --var=BISECT --range=0.01,15.0 --target=$target_size \ + --rtol_val=0.005 \ + --cmd="$cjxl_bin --distance=\$BISECT ${input_filename} ${output_filename}_bisect_\$BISECT.jxl ; (find ${output_filename}_bisect_\$BISECT.jxl -printf \"scale=10;%s/$num_pixels\n\" | bc -l)" \ + --final="mv ${output_filename}_bisect_\$BISECT.jxl ${output_filename}; rm -f ${output_filename}_bisect_*.jxl" \ + --verbosity=1 diff --git a/media/libjxl/src/tools/cjxl_bisect_size b/media/libjxl/src/tools/cjxl_bisect_size new file mode 100755 index 0000000000..9cd88ea529 --- /dev/null +++ b/media/libjxl/src/tools/cjxl_bisect_size @@ -0,0 +1,36 @@ +#!/bin/sh +# +# Bisects JPEG XL encoding quality parameter to reach a given +# target byte-size. +# (To be used directly, or as a template for tailored processing.) +# +# Usage: cjxl_bisect_size {input_filename} {output_filename} {target_size} + +# +# We take the `bisector` tool from $PATH, or, if not available, +# try to locate it in the same directory as the current script. +# + +input_filename=$1 +output_filename=$2 +target_size=$3 + +script_dir=$(dirname $(readlink -f $0)) +bisect_tool=$(which bisector) +if [ -z $bisect_tool ] ; then + bisect_tool="${script_dir}/bisector" +fi + +# If $CJXL_BIN is set, we use this instead of looking for `cjxl` on $PATH. + +cjxl_bin=${CJXL_BIN} +if [-z $cjxl_bin ] ; then + cjxl_bin="cjxl" +fi + +# Allow 0.5% tolerance in size (--rtol=0.005). +exec $bisect_tool --var=BISECT --range=0.01,10.0 --target=$target_size \ + --rtol_val=0.005 \ + --cmd="$cjxl_bin --distance=\$BISECT ${input_filename} ${output_filename}_bisect_\$BISECT.jxl && wc -c ${output_filename}_bisect_\$BISECT.jxl" \ + --final="mv ${output_filename}_bisect_\$BISECT.jxl ${output_filename}; rm -f ${output_filename}_bisect_*.jxl" \ + --verbosity=1 diff --git a/media/libjxl/src/tools/cjxl_main.cc b/media/libjxl/src/tools/cjxl_main.cc new file mode 100644 index 0000000000..157400a13d --- /dev/null +++ b/media/libjxl/src/tools/cjxl_main.cc @@ -0,0 +1,151 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> + +#include "jxl/encode.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "tools/box/box.h" +#include "tools/cjxl.h" +#include "tools/codec_config.h" + +namespace jpegxl { +namespace tools { + +enum CjxlRetCode : int { + OK = 0, + ERR_PARSE, + ERR_INVALID_ARG, + ERR_LOAD_INPUT, + ERR_INVALID_INPUT, + ERR_ENCODING, + ERR_CONTAINER, + ERR_WRITE, + DROPPED_JBRD, +}; + +int CompressJpegXlMain(int argc, const char* argv[]) { + CommandLineParser cmdline; + CompressArgs args; + args.AddCommandLineOptions(&cmdline); + + if (!cmdline.Parse(argc, argv)) { + // Parse already printed the actual error cause. + fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); + return CjxlRetCode::ERR_PARSE; + } + + if (args.version) { + fprintf(stdout, "cjxl %s\n", + CodecConfigString(JxlEncoderVersion()).c_str()); + fprintf(stdout, "Copyright (c) the JPEG XL Project\n"); + return CjxlRetCode::OK; + } + + if (!args.quiet) { + fprintf(stderr, "JPEG XL encoder %s\n", + CodecConfigString(JxlEncoderVersion()).c_str()); + } + + if (cmdline.HelpFlagPassed()) { + cmdline.PrintHelp(); + return CjxlRetCode::OK; + } + + if (!args.ValidateArgs(cmdline)) { + // ValidateArgs already printed the actual error cause. + fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); + return CjxlRetCode::ERR_INVALID_ARG; + } + + jxl::PaddedBytes compressed; + + jxl::ThreadPoolInternal pool(args.num_threads); + jxl::CodecInOut io; + double decode_mps = 0; + if (!LoadAll(args, &pool, &io, &decode_mps)) { + return CjxlRetCode::ERR_LOAD_INPUT; + } + + // need to validate again because now we know the input + if (!args.ValidateArgsAfterLoad(cmdline, io)) { + fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); + return CjxlRetCode::ERR_INVALID_INPUT; + } + if (!args.file_out && !args.quiet) { + fprintf(stderr, + "No output file specified.\n" + "Encoding will be performed, but the result will be discarded.\n"); + } + if (!CompressJxl(io, decode_mps, &pool, args, &compressed, !args.quiet)) { + return CjxlRetCode::ERR_ENCODING; + } + + int ret = CjxlRetCode::OK; + if (args.use_container) { + JpegXlContainer container; + container.codestream = std::move(compressed); + if (!io.blobs.exif.empty()) { + container.exif = io.blobs.exif.data(); + container.exif_size = io.blobs.exif.size(); + } + auto append_xml = [&container](const std::vector<uint8_t>& bytes) { + if (bytes.empty()) return; + container.xml.emplace_back(bytes.data(), bytes.size()); + }; + append_xml(io.blobs.xmp); + if (!io.blobs.jumbf.empty()) { + container.jumb = io.blobs.jumbf.data(); + container.jumb_size = io.blobs.jumbf.size(); + } + jxl::PaddedBytes jpeg_data; + if (args.store_jpeg_metadata && io.Main().IsJPEG()) { + jxl::jpeg::JPEGData data_in = *io.Main().jpeg_data; + if (EncodeJPEGData(data_in, &jpeg_data, args.params)) { + container.jpeg_reconstruction = jpeg_data.data(); + container.jpeg_reconstruction_size = jpeg_data.size(); + } else { + fprintf(stderr, "Warning: failed to create JPEG reconstruction data\n"); + ret = CjxlRetCode::DROPPED_JBRD; + } + } + compressed = {}; + if (!EncodeJpegXlContainerOneShot(container, &compressed)) { + fprintf(stderr, "Failed to encode container format\n"); + return CjxlRetCode::ERR_CONTAINER; + } + if (!args.quiet) { + const size_t pixels = io.xsize() * io.ysize(); + const double bpp = + static_cast<double>(compressed.size() * jxl::kBitsPerByte) / pixels; + fprintf(stderr, "Including container: %llu bytes (%.3f bpp%s).\n", + static_cast<long long unsigned>(compressed.size()), + bpp / io.frames.size(), io.frames.size() == 1 ? "" : "/frame"); + } + } + if (args.file_out) { + if (!jxl::WriteFile(compressed, args.file_out)) { + fprintf(stderr, "Failed to write to \"%s\"\n", args.file_out); + return CjxlRetCode::ERR_WRITE; + } + } + + if (args.print_profile == jxl::Override::kOn) { + PROFILER_PRINT_RESULTS(); + } + if (!args.quiet && cmdline.verbosity > 0) { + jxl::CacheAligned::PrintStats(); + } + return ret; +} + +} // namespace tools +} // namespace jpegxl + +int main(int argc, const char** argv) { + return jpegxl::tools::CompressJpegXlMain(argc, argv); +} diff --git a/media/libjxl/src/tools/cjxl_ng_main.cc b/media/libjxl/src/tools/cjxl_ng_main.cc new file mode 100644 index 0000000000..403ee82cfd --- /dev/null +++ b/media/libjxl/src/tools/cjxl_ng_main.cc @@ -0,0 +1,922 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Note: This encoder binary does extensive flag-validity checking (in +// order to produce meaningful error messages), and on top of that +// checks all libjxl C API call return values. The downside of this +// vs. libjxl providing meaningful error messages is that a change to +// the accepted range of a flag-specified parameter in libjxl will +// also require a change to the range-check here. The advantage is +// that this minimizes the size of libjxl. + +#include <stdint.h> + +#include <cmath> +#include <cstdlib> +#include <functional> +#include <iostream> +#include <sstream> +#include <string> +#include <vector> + +#include "gflags/gflags.h" +#include "jxl/codestream_header.h" +#include "jxl/encode.h" +#include "jxl/encode_cxx.h" +#include "jxl/thread_parallel_runner.h" +#include "jxl/thread_parallel_runner_cxx.h" +#include "jxl/types.h" +#include "lib/extras/codec.h" +#include "lib/extras/dec/apng.h" +#include "lib/extras/dec/color_hints.h" +#include "lib/extras/dec/gif.h" +#include "lib/extras/dec/jpg.h" +#include "lib/extras/dec/pgx.h" +#include "lib/extras/dec/pnm.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/size_constraints.h" + +DECLARE_bool(help); +DECLARE_bool(helpshort); +// The flag --version is owned by gflags itself. +DEFINE_bool(encoder_version, false, + "Print encoder library version number and exit."); + +DEFINE_bool(lossless_jpeg, true, + "If the input is JPEG, use JxlEncoderAddJPEGFrame " + "to add a JPEG frame (i.e. losslessly transcoding JPEG), " + "rather than using JxlEncoderAddImageFrame to reencode pixels."); + +DEFINE_bool(jpeg_store_metadata, true, + "If --lossless_jpeg is set, store JPEG reconstruction " + "metadata in the JPEG XL container " + "(for lossless reconstruction of the JPEG codestream)."); + +DEFINE_bool(jpeg_reconstruction_cfl, true, + "Enable/disable chroma-from-luma (CFL) for lossless " + "JPEG reconstruction."); + +DEFINE_bool(container, false, + "Force using container format (default: use only if needed)."); + +DEFINE_bool(strip, false, + "Do not encode using container format (strips " + "Exif/XMP/JPEG bitstream reconstruction data)."); + +DEFINE_bool(responsive, false, "[modular encoding] Do Squeeze transform"); + +DEFINE_bool(progressive, false, "Enable progressive/responsive decoding."); + +DEFINE_bool(progressive_ac, false, "Use progressive mode for AC."); + +DEFINE_bool(qprogressive_ac, false, "Use progressive mode for AC."); + +DEFINE_bool(modular_lossy_palette, false, "Use delta-palette."); + +DEFINE_int32(premultiply, -1, + "Force premultiplied (associated) alpha. " + "-1 = Do what the input does, 0 = Do not premultiply, " + "1 = force premultiply."); + +DEFINE_bool(already_downsampled, false, + "Do not downsample the given input before encoding, " + "but still signal that the decoder should upsample."); + +DEFINE_bool( + modular, false, + "Use modular mode (not provided = encoder chooses, 0 = enforce VarDCT, " + "1 = enforce modular mode)."); + +DEFINE_bool(keep_invisible, false, + "Force disable/enable preserving color of invisible " + "pixels. (not provided = default, 0 = disable, 1 = enable)."); + +DEFINE_bool(dots, false, + "Force disable/enable dots generation. " + "(not provided = default, 0 = disable, 1 = enable)."); + +DEFINE_bool(patches, false, + "Force disable/enable patches generation. " + "(not provided = default, 0 = disable, 1 = enable)."); + +DEFINE_bool(gaborish, false, + "Force disable/enable the gaborish filter. " + "(not provided = default, 0 = disable, 1 = enable)."); + +DEFINE_bool( + group_order, false, + "Order in which 256x256 regions are stored " + "in the codestream for progressive rendering. " + "Value not provided means 'encoder default', 0 means 'scanline order', " + "1 means 'center-first order'."); + +DEFINE_double( + intensity_target, 0.0, + "Upper bound on the intensity level present in the image in nits. " + "Leaving this set to its default of 0 lets libjxl choose a sensible " + "default " + "value based on the color encoding."); + +// TODO(tfish): +// --dec-hints, -- NEED (passed to image decoders, via extras, tweaks decoding) +// --override_bitdepth, -- NEED + +DEFINE_int32(progressive_dc, -1, + "Progressive-DC setting. Valid values are: -1, 0, 1, 2."); + +DEFINE_int32(faster_decoding, 0, + "Favour higher decoding speed. 0 = default, higher " + "values give higher speed at the expense of quality"); + +DEFINE_int32( + resampling, -1, + "Resampling. Default of -1 applies resampling only for low quality. " + "Value 1 does no downsampling (1x1), 2 does 2x2 downsampling, " + "4 is for 4x4 downsampling, and 8 for 8x8 downsampling."); + +DEFINE_int32( + ec_resampling, -1, + "Resampling for extra channels. Default of -1 applies resampling only " + "for low quality. Value 1 does no downsampling (1x1), 2 does 2x2 " + "downsampling, 4 is for 4x4 downsampling, and 8 for 8x8 downsampling."); + +DEFINE_int32( + epf, -1, + "Edge preserving filter level, -1 to 3. " + "Value -1 means: default (encoder chooses), 0 to 3 set a strength."); + +DEFINE_int64( + center_x, -1, + "Determines the horizontal position of center for the center-first " + "group order. The value -1 means 'use the middle of the image', " + "other values [0..xsize) set this to a particular coordinate."); + +DEFINE_int64(center_y, -1, + "Determines the vertical position of center for the center-first " + "group order. The value -1 means 'use the middle of the image', " + "other values [0..ysize) set this to a particular coordinate."); + +DEFINE_int64(num_threads, -1, + "Number of worker threads (-1 == use machine default, " + "0 == do not use multithreading)."); + +DEFINE_int64(num_reps, 1, "How many times to compress. (For benchmarking)."); + +DEFINE_int32(modular_group_size, -1, + "[modular encoding] group size: -1 == default. 0 => 128, " + "1 => 256, 2 => 512, 3 => 1024"); + +DEFINE_int32(modular_predictor, -1, + "[modular encoding] predictor(s) to use: 0=zero, " + "1=left, 2=top, 3=avg0, 4=select, 5=gradient, 6=weighted, " + "7=topright, 8=topleft, 9=leftleft, 10=avg1, 11=avg2, 12=avg3, " + "13=toptop predictive average " + "14=mix 5 and 6, 15=mix everything. If unset, uses default 14, " + "at slowest speed default 15."); + +DEFINE_int32(modular_colorspace, -1, + "[modular encoding] color transform: 0=RGB, 1=YCoCg, " + "2-37=RCT (default: try several, depending on speed)"); + +DEFINE_int32( + modular_channel_colors_global_percent, -1, + "[modular encoding] Use Global channel palette if the number of " + "colors is smaller than this percentage of range. " + "Use 0-100 to set an explicit percentage, -1 to use the encoder default."); + +DEFINE_int32( + modular_channel_colors_group_percent, -1, + "[modular encoding] Use Local (per-group) channel palette if the number " + "of colors is smaller than this percentage of range. Use 0-100 to set " + "an explicit percentage, -1 to use the encoder default."); + +DEFINE_int32( + modular_palette_colors, -1, + "[modular encoding] Use color palette if number of colors is smaller " + "than or equal to this, or -1 to use the encoder default."); + +DEFINE_int32(modular_nb_prev_channels, -1, + "[modular encoding] number of extra MA tree properties to use"); + +DEFINE_int32(modular_ma_tree_learning_percent, -1, + "[modular encoding] Fraction of pixels used to learn MA trees as " + "a percentage. -1 = default, 0 = no MA and fast decode, 50 = " + "default value, 100 = all, values above 100 are also permitted. " + "Higher values use more encoder memory."); + +DEFINE_int32(photon_noise_iso, 0, + "Adds noise to the image emulating photographic film noise. " + "The higher the given number, the grainier the image will be. " + "As an example, a value of 100 gives low noise whereas a value " + "of 3200 gives a lot of noise. The default value is 0."); + +DEFINE_int32(codestream_level, 5, "The codestream level. Either `5` or `10`."); + +DEFINE_double( + distance, 1.0, + "Max. butteraugli distance, lower = higher quality.\n" + " 0.0 = mathematically lossless. Default for already-lossy input " + "(JPEG/GIF).\n" + " 1.0 = visually lossless. Default for other input.\n" + " Recommended range: 0.5 .. 3.0. Mutually exclusive with --quality."); + +DEFINE_double( + quality, 100.0, + "Quality setting (is remapped to --distance). Range: -inf .. 100.\n" + " 100 = mathematically lossless. Default for already-lossy input " + "(JPEG/GIF).\n" + " Other input gets encoded as per --distance default.\n" + " Positive quality values roughly match libjpeg quality.\n" + " Mutually exclusive with --distance."); + +DEFINE_int64(effort, 3, + "Encoder effort setting. Range: 1 .. 9.\n" + " Higher number is more effort (slower)."); + +DEFINE_int32(brotli_effort, 9, + "Brotli effort setting. Range: 0 .. 11.\n" + " Default: 9. Higher number is more effort (slower)."); + +DEFINE_string(frame_indexing, "", + // TODO(tfish): Add a more convenient vanilla alternative. + "If non-empty, a string matching '^[01]*$'. If this string has a " + "'1' in i-th position, then the i-th frame will be indexed in " + "the frame index box."); + +namespace { +/** + * Writes bytes to file. + */ +bool WriteFile(const std::vector<uint8_t>& bytes, const char* filename) { + FILE* file = fopen(filename, "wb"); + if (!file) { + std::cerr << "Could not open file: " << filename << " for writing" + << std::endl + << "Error: " << strerror(errno) << std::endl; + return false; + } + if (fwrite(bytes.data(), sizeof(uint8_t), bytes.size(), file) != + bytes.size()) { + std::cerr << "Could not write bytes to file: " << filename << std::endl + << "Error: " << strerror(errno) << std::endl; + return false; + } + if (fclose(file) != 0) { + std::cerr << "Could not close file: " << filename << std::endl + << "Error: " << strerror(errno) << std::endl; + return false; + } + return true; +} + +void SetFlagFrameOptionOrDie(const char* flag_name, int32_t flag_value, + JxlEncoderFrameSettings* frame_settings, + JxlEncoderFrameSettingId encoder_option) { + if (JXL_ENC_SUCCESS != JxlEncoderFrameSettingsSetOption( + frame_settings, encoder_option, flag_value)) { + std::cerr << "Setting encoder option from flag -- " << flag_name + << "failed." << std::endl; + exit(EXIT_FAILURE); + } +} + +void SetDistanceFromFlags(JxlEncoderFrameSettings* jxl_encoder_frame_settings, + const jxl::extras::Codec& codec) { + bool distance_set = + !gflags::GetCommandLineFlagInfoOrDie("distance").is_default; + bool quality_set = !gflags::GetCommandLineFlagInfoOrDie("quality").is_default; + + if (distance_set && quality_set) { + std::cerr << "Must not set both --distance and --quality." << std::endl; + exit(EXIT_FAILURE); + } + if (distance_set) { + if (JXL_ENC_SUCCESS != JxlEncoderSetFrameDistance( + jxl_encoder_frame_settings, FLAGS_distance)) { + std::cerr << "Setting --distance parameter failed." << std::endl; + exit(EXIT_FAILURE); + } + return; + } + if (quality_set) { + double distance = FLAGS_quality >= 100 ? 0.0 + : FLAGS_quality >= 30 + ? 0.1 + (100 - FLAGS_quality) * 0.09 + : 6.4 + pow(2.5, (30 - FLAGS_quality) / 5.0) / 6.25; + if (JXL_ENC_SUCCESS != + JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, distance)) { + std::cerr << "Setting --quality parameter failed." << std::endl; + exit(EXIT_FAILURE); + } + return; + } + // No flag set, but input is JPG or GIF: Use distance 0 default. + if (codec == jxl::extras::Codec::kJPG || codec == jxl::extras::Codec::kGIF) { + if (JXL_ENC_SUCCESS == + JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, 0.0)) { + std::cerr << "Setting 'lossless' default for GIF or JPEG input." + << std::endl; + } + } +} + +typedef std::function<std::string(int32_t)> flag_check_fn; + +bool IsJPG(const jxl::PaddedBytes& image_data) { + return (image_data.size() >= 2 && image_data[0] == 0xFF && + image_data[1] == 0xD8); +} + +void SetCodestreamLevel(JxlEncoder* jxl_encoder, bool for_lossless_jpeg) { + bool flag_set = + !gflags::GetCommandLineFlagInfoOrDie("codestream_level").is_default; + int32_t codestream_level = FLAGS_codestream_level; + auto set_codestream_level = [&jxl_encoder, &codestream_level]() { + if (JXL_ENC_SUCCESS != + JxlEncoderSetCodestreamLevel(jxl_encoder, codestream_level)) { + std::cerr << "Setting --codestream_level failed." << std::endl; + exit(EXIT_FAILURE); + } + }; + if (for_lossless_jpeg) { + if (!flag_set) { + set_codestream_level(); + } + } else { + if (!flag_set) { + codestream_level = static_cast<int32_t>( + JxlEncoderGetRequiredCodestreamLevel(jxl_encoder)); + if (codestream_level == -1) { + std::cerr << "No codestream_level supports the given image parameters." + << std::endl; + exit(EXIT_FAILURE); + } + } + set_codestream_level(); + } +} + +// TODO(tfish): Replace with non-C-API library function. +// Implementation is in extras/. +jxl::Status GetPixeldata(const jxl::PaddedBytes& image_data, + jxl::extras::PackedPixelFile& ppf, + jxl::extras::Codec& codec) { + // Any valid encoding is larger (ensures codecs can read the first few bytes). + constexpr size_t kMinBytes = 9; + + if (image_data.size() < kMinBytes) return JXL_FAILURE("Input too small."); + jxl::Span<const uint8_t> encoded(image_data); + + ppf.info.orientation = JXL_ORIENT_IDENTITY; + jxl::extras::ColorHints color_hints; + jxl::SizeConstraints size_constraints; + +#if JPEGXL_ENABLE_APNG + if (jxl::extras::DecodeImageAPNG(encoded, color_hints, size_constraints, + &ppf)) { + codec = jxl::extras::Codec::kPNG; + } else +#endif + if (jxl::extras::DecodeImagePGX(encoded, color_hints, size_constraints, + &ppf)) { + codec = jxl::extras::Codec::kPGX; + } else if (jxl::extras::DecodeImagePNM(encoded, color_hints, size_constraints, + &ppf)) { + codec = jxl::extras::Codec::kPNM; + } +#if JPEGXL_ENABLE_GIF + else if (jxl::extras::DecodeImageGIF(encoded, color_hints, size_constraints, + &ppf)) { + codec = jxl::extras::Codec::kGIF; + } +#endif +#if JPEGXL_ENABLE_JPEG + else if (jxl::extras::DecodeImageJPG(encoded, color_hints, size_constraints, + &ppf)) { + codec = jxl::extras::Codec::kJPG; + } +#endif + else { // TODO(tfish): Bring back EXR and PSD. + return JXL_FAILURE("Codecs failed to decode input."); + } + // TODO(tfish): Migrate this: + // if (!skip_ppf_conversion) { + // JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); + // } + return true; +} + +void set_usage_message_and_version(const char* argv0) { + gflags::SetUsageMessage( + "JPEG XL-encodes an image.\n" + " Input format can be one of: " +#if JPEGXL_ENABLE_APNG + "PNG, APNG, " +#endif +#if JPEGXL_ENABLE_GIF + "GIF, " +#endif +#if JPEGXL_ENABLE_JPEG + "JPEG, " +#endif + "PPM, PFM, PGX.\n Sample usage:\n" + + std::string(argv0) + " <source_image_filename> <target_image_filename>"); + uint32_t version = JxlEncoderVersion(); + + gflags::SetVersionString(std::to_string(version / 1000000) + "." + + std::to_string((version / 1000) % 1000) + "." + + std::to_string(version % 1000)); +} + +} // namespace + +int main(int argc, char** argv) { + std::cerr << "Warning: This is work in progress, consider using cjxl instead!" + << std::endl; + set_usage_message_and_version(argv[0]); + // TODO(firsching): rethink --help handling + gflags::ParseCommandLineNonHelpFlags(&argc, &argv, /*remove_flags=*/true); + if (FLAGS_help) { + FLAGS_help = false; + FLAGS_helpshort = true; + } + gflags::HandleCommandLineHelpFlags(); + + if (argc != 3) { + FLAGS_help = false; + FLAGS_helpshort = true; + gflags::HandleCommandLineHelpFlags(); + return EXIT_FAILURE; + } + const char* filename_in = argv[1]; + const char* filename_out = argv[2]; + + // Loading the input. + // Depending on flags-settings, we want to either load a JPEG and + // faithfully convert it to JPEG XL, or load (JPEG or non-JPEG) + // pixel data. For benchmarking, we want to be able to do + // N repetitions of image-compression, but the input should + // not get reloaded as part of that. + // Since we do not want to load the input before we decided that + // flag-settings are valid, we need a mechanism to lazy-load the image. + bool input_image_loaded = false; + jxl::PaddedBytes image_data; + jxl::extras::PackedPixelFile ppf; + jxl::extras::Codec codec = jxl::extras::Codec::kUnknown; + auto ensure_image_loaded = [&filename_in, &input_image_loaded, &image_data, + &ppf, &codec]() { + if (input_image_loaded) return; + if (!ReadFile(filename_in, &image_data)) { + std::cerr << "Reading image data failed." << std::endl; + exit(EXIT_FAILURE); + } + if (!(FLAGS_lossless_jpeg && IsJPG(image_data))) { + jxl::Status status = GetPixeldata(image_data, ppf, codec); + if (!status) { + std::cerr << "Getting pixel data." << std::endl; + exit(EXIT_FAILURE); + } + if (ppf.frames.size() < 1) { + std::cerr << "No frames on input file." << std::endl; + exit(EXIT_FAILURE); + } + } + input_image_loaded = true; + }; + + JxlEncoderPtr enc = JxlEncoderMake(/*memory_manager=*/nullptr); + JxlEncoder* jxl_encoder = enc.get(); + JxlThreadParallelRunnerPtr runner; + for (int num_rep = 0; num_rep < FLAGS_num_reps; ++num_rep) { + JxlEncoderReset(jxl_encoder); + if (FLAGS_num_threads != 0) { + size_t num_worker_threads = + JxlThreadParallelRunnerDefaultNumWorkerThreads(); + { + int64_t flag_num_worker_threads = FLAGS_num_threads; + if (flag_num_worker_threads != -1) { + num_worker_threads = flag_num_worker_threads; + } + } + if (runner == nullptr) { + runner = JxlThreadParallelRunnerMake( + /*memory_manager=*/nullptr, num_worker_threads); + } + if (JXL_ENC_SUCCESS != + JxlEncoderSetParallelRunner(jxl_encoder, JxlThreadParallelRunner, + runner.get())) { + std::cerr << "JxlEncoderSetParallelRunner failed." << std::endl; + return EXIT_FAILURE; + } + } + + JxlEncoderFrameSettings* jxl_encoder_frame_settings = + JxlEncoderFrameSettingsCreate(jxl_encoder, nullptr); + + auto process_flag = [&jxl_encoder_frame_settings]( + const char* flag_name, int32_t flag_value, + JxlEncoderFrameSettingId encoder_option, + flag_check_fn flag_check) { + gflags::CommandLineFlagInfo flag_info = + gflags::GetCommandLineFlagInfoOrDie(flag_name); + if (!flag_info.is_default) { + std::string error = flag_check(flag_value); + if (!error.empty()) { + std::cerr << "Invalid flag value for --" << flag_name << ": " << error + << std::endl; + exit(EXIT_FAILURE); + } + SetFlagFrameOptionOrDie(flag_name, flag_value, + jxl_encoder_frame_settings, encoder_option); + } + }; + + auto process_bool_flag = [&process_flag]( + const char* flag_name, int32_t flag_value, + JxlEncoderFrameSettingId encoder_option) { + process_flag(flag_name, static_cast<int32_t>(flag_value), encoder_option, + [](int32_t x) { return ""; }); + }; + + { // Processing tuning flags. + bool use_container = FLAGS_container; + // TODO(tfish): Set use_container according to need of encoded data. + // This will likely require moving this piece out of flags-processing. + if (FLAGS_strip) { + use_container = false; + } + JxlEncoderUseContainer(jxl_encoder, use_container); + + process_bool_flag("modular", FLAGS_modular, + JXL_ENC_FRAME_SETTING_MODULAR); + process_bool_flag("keep_invisible", FLAGS_keep_invisible, + JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE); + process_bool_flag("dots", FLAGS_dots, JXL_ENC_FRAME_SETTING_DOTS); + process_bool_flag("patches", FLAGS_patches, + JXL_ENC_FRAME_SETTING_PATCHES); + process_bool_flag("gaborish", FLAGS_gaborish, + JXL_ENC_FRAME_SETTING_GABORISH); + process_bool_flag("group_order", FLAGS_group_order, + JXL_ENC_FRAME_SETTING_GROUP_ORDER); + + if (!FLAGS_frame_indexing.empty()) { + bool must_be_all_zeros = FLAGS_frame_indexing[0] != '1'; + for (char c : FLAGS_frame_indexing) { + if (c == '1') { + if (must_be_all_zeros) { + std::cerr + << "Invalid --frame_indexing. If the first character is " + "'0', all must be '0'." + << std::endl; + return EXIT_FAILURE; + } + } else if (c != '0') { + std::cerr << "Invalid --frame_indexing. Must match the pattern " + "'^(0*|1[01]*)$'." + << std::endl; + return EXIT_FAILURE; + } + } + } + + process_flag( + "effort", FLAGS_effort, JXL_ENC_FRAME_SETTING_EFFORT, + [](int32_t x) -> std::string { + return (1 <= x && x <= 9) ? "" : "Valid range is {1, 2, ..., 9}."; + }); + process_flag( + "brotli_effort", FLAGS_brotli_effort, + JXL_ENC_FRAME_SETTING_BROTLI_EFFORT, [](int32_t x) -> std::string { + return (-1 <= x && x <= 11) ? "" + : "Valid range is {-1, 0, 1, ..., 11}."; + }); + process_flag("epf", FLAGS_epf, JXL_ENC_FRAME_SETTING_EPF, + [](int32_t x) -> std::string { + return (-1 <= x && x <= 3) + ? "" + : "Valid range is {-1, 0, 1, 2, 3}.\n"; + }); + process_flag( + "faster_decoding", FLAGS_faster_decoding, + JXL_ENC_FRAME_SETTING_DECODING_SPEED, [](int32_t x) -> std::string { + return (0 <= x && x <= 4) ? "" + : "Valid range is {0, 1, 2, 3, 4}.\n"; + }); + process_flag("resampling", FLAGS_resampling, + JXL_ENC_FRAME_SETTING_RESAMPLING, + [](int32_t x) -> std::string { + return (x == -1 || x == 1 || x == 4 || x == 8) + ? "" + : "Valid values are {-1, 1, 2, 4, 8}.\n"; + }); + process_flag("ec_resampling", FLAGS_ec_resampling, + JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, + [](int32_t x) -> std::string { + return (x == -1 || x == 1 || x == 4 || x == 8) + ? "" + : "Valid values are {-1, 1, 2, 4, 8}.\n"; + }); + process_flag("photon_noise_iso", FLAGS_photon_noise_iso, + JXL_ENC_FRAME_SETTING_PHOTON_NOISE, + [](int32_t x) -> std::string { + return x >= 0 ? "" : "Must be >= 0."; + }); + process_bool_flag("already_downsampled", FLAGS_already_downsampled, + JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED); + SetDistanceFromFlags(jxl_encoder_frame_settings, codec); + + if (!FLAGS_group_order && + (FLAGS_center_x != -1 || FLAGS_center_y != -1)) { + std::cerr + << "Invalid flag combination. Setting --center_x or --center_y " + << "requires setting --group_order=1" << std::endl; + return EXIT_FAILURE; + } + process_flag("center_x", FLAGS_center_x, + JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X, + [](int32_t x) -> std::string { + if (x < -1) { + return "Valid values are: -1 or [0 .. xsize)."; + } + return ""; + }); + process_flag("center_y", FLAGS_center_y, + JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y, + [](int32_t x) -> std::string { + if (x < -1) { + return "Valid values are: -1 or [0 .. ysize)."; + } + return ""; + }); + } + { // Progressive/responsive mode settings. + bool qprogressive_ac_set = + !gflags::GetCommandLineFlagInfoOrDie("qprogressive_ac").is_default; + int32_t qprogressive_ac = FLAGS_qprogressive_ac ? 1 : 0; + bool responsive_set = + !gflags::GetCommandLineFlagInfoOrDie("responsive").is_default; + int32_t responsive = FLAGS_responsive ? 1 : 0; + + process_flag( + "progressive_dc", FLAGS_progressive_dc, + JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, [](int32_t x) -> std::string { + return (-1 <= x && x <= 2) ? "" : "Valid range is {-1, 0, 1, 2}.\n"; + }); + process_bool_flag("progressive_ac", FLAGS_progressive_ac, + JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC); + + if (FLAGS_progressive) { + qprogressive_ac = 1; + qprogressive_ac_set = true; + responsive = 1; + responsive_set = true; + } + if (responsive_set) { + SetFlagFrameOptionOrDie("responsive", responsive, + jxl_encoder_frame_settings, + JXL_ENC_FRAME_SETTING_RESPONSIVE); + } + if (qprogressive_ac_set) { + SetFlagFrameOptionOrDie("qprogressive_ac", qprogressive_ac, + jxl_encoder_frame_settings, + JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC); + } + } + { // Modular mode related. + process_flag("modular_group_size", FLAGS_modular_group_size, + JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, + [](int32_t x) -> std::string { + return (-1 <= x && x <= 3) + ? "" + : "Invalid --modular_group_size. Valid " + "range is {-1, 0, 1, 2, 3}.\n"; + }); + process_flag("modular_predictor", FLAGS_modular_predictor, + JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, + [](int32_t x) -> std::string { + return (0 <= x && x <= 15) + ? "" + : "Invalid --modular_predictor. Valid " + "range is {-1, 0, 1, ..., 15}.\n"; + }); + process_flag( + "modular_colorspace", FLAGS_modular_colorspace, + JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, + [](int32_t x) -> std::string { + return (0 <= x && x <= 15) + ? "" + : "Invalid --modular_colorspace. Valid range is " + "{-1, 0, 1, ..., 37}.\n"; + }); + process_flag("modular_ma_tree_learning_percent", + FLAGS_modular_ma_tree_learning_percent, + JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, + [](int32_t x) -> std::string { + return (-1 <= x && x <= 100) + ? "" + : "Invalid --modular_ma_tree_learning_percent. " + "Valid range is {-1, 0, 1, ..., 100}.\n"; + }); + process_flag("modular_nb_prev_channels", FLAGS_modular_nb_prev_channels, + JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, + [](int32_t x) -> std::string { + return (-1 <= x && x <= 11) + ? "" + : "Invalid --modular_nb_prev_channels. Valid " + "range is {-1, 0, 1, ..., 11}.\n"; + }); + process_bool_flag("modular_lossy_palette", FLAGS_modular_lossy_palette, + JXL_ENC_FRAME_SETTING_LOSSY_PALETTE); + process_flag("modular_palette_colors", FLAGS_modular_palette_colors, + JXL_ENC_FRAME_SETTING_PALETTE_COLORS, + [](int32_t x) -> std::string { return ""; }); + process_flag( + "modular_channel_colors_global_percent", + FLAGS_modular_channel_colors_global_percent, + JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, + [](int32_t x) -> std::string { + return (-1 <= x && x <= 100) + ? "" + : "Invalid --modular_channel_colors_global_percent. " + "Valid " + "range is {-1, 0, 1, ..., 100}.\n"; + }); + process_flag( + "modular_channel_colors_group_percent", + FLAGS_modular_channel_colors_group_percent, + JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, + [](int32_t x) -> std::string { + return (-1 <= x && x <= 100) + ? "" + : "Invalid --modular_channel_colors_group_percent. " + "Valid " + "range is {-1, 0, 1, ..., 100}.\n"; + }); + } + ensure_image_loaded(); + if (FLAGS_lossless_jpeg && IsJPG(image_data)) { + if (gflags::GetCommandLineFlagInfoOrDie("lossless_jpeg").is_default) { + std::cerr << "Note: Implicit-default for JPEG is lossless-transcoding. " + << "To silence this message, set --lossless_jpeg=(1|0)." + << std::endl; + } + if (FLAGS_jpeg_store_metadata) { + if (JXL_ENC_SUCCESS != JxlEncoderStoreJPEGMetadata(jxl_encoder, true)) { + std::cerr << "Storing JPEG metadata failed. " << std::endl; + return EXIT_FAILURE; + } + } + process_bool_flag("jpeg_reconstruction_cfl", + FLAGS_jpeg_reconstruction_cfl, + JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL); + SetCodestreamLevel(jxl_encoder, /*for_lossless_jpeg=*/true); + if (JXL_ENC_SUCCESS != JxlEncoderAddJPEGFrame(jxl_encoder_frame_settings, + image_data.data(), + image_data.size())) { + std::cerr << "JxlEncoderAddJPEGFrame() failed." << std::endl; + return EXIT_FAILURE; + } + } else { // Do JxlEncoderAddImageFrame(). + size_t num_alpha_channels = 0; // Adjusted below. + { + JxlBasicInfo basic_info = ppf.info; + if (basic_info.alpha_bits > 0) num_alpha_channels = 1; + basic_info.intensity_target = + static_cast<float>(FLAGS_intensity_target); + basic_info.num_extra_channels = num_alpha_channels; + basic_info.num_color_channels = ppf.info.num_color_channels; + basic_info.uses_original_profile = JXL_FALSE; + if (JXL_ENC_SUCCESS != + JxlEncoderSetBasicInfo(jxl_encoder, &basic_info)) { + std::cerr << "JxlEncoderSetBasicInfo() failed." << std::endl; + return EXIT_FAILURE; + } + SetCodestreamLevel(jxl_encoder, /*for_lossless_jpeg=*/false); + } + + if (!ppf.icc.empty()) { + JxlEncoderSetICCProfile(jxl_encoder, ppf.icc.data(), ppf.icc.size()); + if (JXL_ENC_SUCCESS != JxlEncoderSetICCProfile(jxl_encoder, + ppf.icc.data(), + ppf.icc.size())) { + std::cerr << "JxlEncoderSetICCProfile() failed." << std::endl; + return EXIT_FAILURE; + } + } else { + if (JXL_ENC_SUCCESS != + JxlEncoderSetColorEncoding(jxl_encoder, &ppf.color_encoding)) { + std::cerr << "JxlEncoderSetColorEncoding() failed." << std::endl; + return EXIT_FAILURE; + } + } + + for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) { + const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame]; + const jxl::extras::PackedImage& pimage = pframe.color; + JxlPixelFormat ppixelformat = pimage.format; + { + if (JXL_ENC_SUCCESS != + JxlEncoderSetFrameHeader(jxl_encoder_frame_settings, + &pframe.frame_info)) { + std::cerr << "JxlEncoderSetFrameHeader() failed." << std::endl; + return EXIT_FAILURE; + } + } + if (num_frame < FLAGS_frame_indexing.size() && + FLAGS_frame_indexing[num_frame] == '1') { + if (JXL_ENC_SUCCESS != + JxlEncoderFrameSettingsSetOption(jxl_encoder_frame_settings, + JXL_ENC_FRAME_INDEX_BOX, 1)) { + std::cerr << "Setting option JXL_ENC_FRAME_INDEX_BOX failed." + << std::endl; + return EXIT_FAILURE; + } + } + jxl::Status enc_status(true); + { + if (num_alpha_channels > 0) { + JxlExtraChannelInfo extra_channel_info; + JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, + &extra_channel_info); + enc_status = JxlEncoderSetExtraChannelInfo(jxl_encoder, 0, + &extra_channel_info); + if (JXL_ENC_SUCCESS != enc_status) { + std::cerr << "JxlEncoderSetExtraChannelInfo() failed." + << std::endl; + return EXIT_FAILURE; + } + if (FLAGS_premultiply != -1) { + if (!(FLAGS_premultiply == 0 || FLAGS_premultiply == 1)) { + std::cerr << "Flag --premultiply must be one of: -1, 0, 1." + << std::endl; + return EXIT_FAILURE; + } + extra_channel_info.alpha_premultiplied = FLAGS_premultiply; + } + // We take the extra channel blend info frame_info, but don't do + // clamping. + JxlBlendInfo extra_channel_blend_info = + pframe.frame_info.layer_info.blend_info; + extra_channel_blend_info.clamp = JXL_FALSE; + JxlEncoderSetExtraChannelBlendInfo(jxl_encoder_frame_settings, 0, + &extra_channel_blend_info); + } + enc_status = + JxlEncoderAddImageFrame(jxl_encoder_frame_settings, &ppixelformat, + pimage.pixels(), pimage.pixels_size); + if (JXL_ENC_SUCCESS != enc_status) { + std::cerr << "JxlEncoderAddImageFrame() failed." << std::endl; + return EXIT_FAILURE; + } + // Only set extra channel buffer if is is provided non-interleaved. + if (!pframe.extra_channels.empty()) { + enc_status = JxlEncoderSetExtraChannelBuffer( + jxl_encoder_frame_settings, &ppixelformat, + pframe.extra_channels[0].pixels(), + pframe.extra_channels[0].stride * + pframe.extra_channels[0].ysize, + 0); + if (JXL_ENC_SUCCESS != enc_status) { + std::cerr << "JxlEncoderSetExtraChannelBuffer() failed." + << std::endl; + return EXIT_FAILURE; + } + } + } + } + } + JxlEncoderCloseInput(jxl_encoder); + } + // Reading compressed output + std::vector<uint8_t> compressed; + compressed.resize(4096); + uint8_t* next_out = compressed.data(); + size_t avail_out = compressed.size() - (next_out - compressed.data()); + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + process_result = + JxlEncoderProcessOutput(jxl_encoder, &next_out, &avail_out); + if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed.data(); + compressed.resize(compressed.size() * 2); + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + } + } + compressed.resize(next_out - compressed.data()); + if (JXL_ENC_SUCCESS != process_result) { + std::cerr << "JxlEncoderProcessOutput failed." << std::endl; + return EXIT_FAILURE; + } + + // TODO(firsching): print info about compressed size and other image stats + // here and in the beginning, like is done in current cjxl. + if (!WriteFile(compressed, filename_out)) { + std::cerr << "Could not write jxl file." << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/media/libjxl/src/tools/cmdline.cc b/media/libjxl/src/tools/cmdline.cc new file mode 100644 index 0000000000..f777c9469c --- /dev/null +++ b/media/libjxl/src/tools/cmdline.cc @@ -0,0 +1,95 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/cmdline.h" + +#include <memory> +#include <string> + +namespace jpegxl { +namespace tools { + +void CommandLineParser::PrintHelp() const { + // Use stdout, not stderr, so help can easily be grepped. + FILE* out = stdout; + fprintf(out, "Usage: %s", program_name_ ? program_name_ : "command"); + + for (const auto& option : options_) { + if (option->positional()) { + if (option->verbosity_level() > verbosity) continue; + if (option->required()) { + fprintf(out, " %s", option->help_flags().c_str()); + } else { + fprintf(out, " [%s]", option->help_flags().c_str()); + } + } + } + fprintf(out, " [OPTIONS...]\n"); + + bool showed_all = true; + for (const auto& option : options_) { + if (option->verbosity_level() > verbosity) { + showed_all = false; + continue; + } + fprintf(out, " %s\n", option->help_flags().c_str()); + const char* help_text = option->help_text(); + if (help_text) { + fprintf(out, " %s\n", help_text); + } + } + fprintf(out, " -h, --help\n Prints this help message%s.\n", + (showed_all ? "" : " (use -v to see more options)")); +} + +bool CommandLineParser::Parse(int argc, const char* argv[]) { + if (argc) program_name_ = argv[0]; + int i = 1; // argv[0] is the program name. + // if false, stop matching options and take only positional arguments + bool parse_options = true; + while (i < argc) { + if (!strcmp("-h", argv[i]) || !strcmp("--help", argv[i])) { + help_ = true; + i++; + continue; + } + if (!strcmp("-v", argv[i]) || !strcmp("--verbose", argv[i])) { + verbosity++; + } + // after "--", filenames starting with "-" can be used + if (!strcmp("--", argv[i])) { + parse_options = false; + i++; + continue; + } + // special case: "-" is a filename denoting stdin or stdout + bool parse_this_option = true; + if (!strcmp("-", argv[i])) { + parse_this_option = false; + } + bool found = false; + for (const auto& option : options_) { + if (option->Match(argv[i], parse_options && parse_this_option)) { + // Parsing advances the value i on success. + const char* arg = argv[i]; + if (!option->Parse(argc, argv, &i)) { + fprintf(stderr, "Error parsing flag %s\n", arg); + return false; + } + found = true; + break; + } + } + if (!found) { + // No option matched argv[i]. + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + return false; + } + } + return true; +} + +} // namespace tools +} // namespace jpegxl diff --git a/media/libjxl/src/tools/cmdline.h b/media/libjxl/src/tools/cmdline.h new file mode 100644 index 0000000000..ed5d847050 --- /dev/null +++ b/media/libjxl/src/tools/cmdline.h @@ -0,0 +1,322 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_CMDLINE_H_ +#define TOOLS_CMDLINE_H_ + +#include <stdio.h> +#include <string.h> + +#include <memory> +#include <string> +#include <vector> + +#include "lib/jxl/base/status.h" + +namespace jpegxl { +namespace tools { + +class CommandLineParser { + public: + typedef size_t OptionId; + + // An abstract class for defining command line options. + class CmdOptionInterface { + public: + CmdOptionInterface() = default; + virtual ~CmdOptionInterface() = default; + + // Return a string with the option name or available flags. + virtual std::string help_flags() const = 0; + + // Return the help string if any, or nullptr if no help string. + virtual const char* help_text() const = 0; + + // Return the verbosity level for this option + virtual int verbosity_level() const = 0; + + // Return whether the option was passed. + virtual bool matched() const = 0; + + // Returns whether this option matches the passed command line argument. + virtual bool Match(const char* arg, bool parse_options) const = 0; + + // Parses the option. The passed i points to the argument with the flag + // that matches either the short or the long name. + virtual bool Parse(int argc, const char* argv[], int* i) = 0; + + // Returns whether the option is positional, and therefore will be shown + // in the first command line representation of the help output. + virtual bool positional() const = 0; + + // Returns whether the option should be displayed as required in the help + // output. No effect on validation. + virtual bool required() const = 0; + }; + + // Add a positional argument. Returns the id of the added option or + // kOptionError on error. + // The "required" flag indicates whether the parameter is mandatory or + // optional, but is only used for how it is displayed in the command line + // help. + OptionId AddPositionalOption(const char* name, bool required, + const char* help_text, const char** storage, + int verbosity_level = 0) { + options_.emplace_back(new CmdOptionPositional(name, help_text, storage, + verbosity_level, required)); + return options_.size() - 1; + } + + // Add an option with a value of type T. The option can be passed as + // '-s <value>' or '--long value' or '--long=value'. The CommandLineParser + // parser will call the function parser with the string pointing to '<value>' + // in either case. Returns the id of the added option or kOptionError on + // error. + template <typename T> + OptionId AddOptionValue(char short_name, const char* long_name, + const char* metavar, const char* help_text, + T* storage, bool(parser)(const char*, T*), + int verbosity_level = 0) { + options_.emplace_back(new CmdOptionFlag<T>(short_name, long_name, metavar, + help_text, storage, parser, + verbosity_level)); + return options_.size() - 1; + } + + // Add a flag without a value. Returns the id of the added option or + // kOptionError on error. + template <typename T> + OptionId AddOptionFlag(char short_name, const char* long_name, + const char* help_text, T* storage, bool(parser)(T*), + int verbosity_level = 0) { + options_.emplace_back(new CmdOptionFlag<T>( + short_name, long_name, help_text, storage, parser, verbosity_level)); + return options_.size() - 1; + } + + const CmdOptionInterface* GetOption(OptionId id) const { + JXL_ASSERT(id < options_.size()); + return options_[id].get(); + } + + // Print the help message to stdout. + void PrintHelp() const; + + // Whether a help flag was specified + bool HelpFlagPassed() const { return help_; } + + int verbosity = 0; + + // Parse the command line. + bool Parse(int argc, const char* argv[]); + + // Return the remaining positional args + std::vector<const char*> PositionalArgs() const; + + private: + // A positional argument. + class CmdOptionPositional : public CmdOptionInterface { + public: + CmdOptionPositional(const char* name, const char* help_text, + const char** storage, int verbosity_level, + bool required) + : name_(name), + help_text_(help_text), + storage_(storage), + verbosity_level_(verbosity_level), + required_(required) {} + + std::string help_flags() const override { return name_; } + const char* help_text() const override { return help_text_; } + int verbosity_level() const override { return verbosity_level_; } + bool matched() const override { return matched_; } + + // Only match non-flag values. This means that you can't pass '-foo' as a + // positional argument, but it helps with detecting when passed a flag with + // a typo. After '--', option matching is disabled so positional arguments + // starting with '-' can be used. + bool Match(const char* arg, bool parse_options) const override { + return !matched_ && (!parse_options || arg[0] != '-'); + } + + bool Parse(const int argc, const char* argv[], int* i) override { + *storage_ = argv[*i]; + (*i)++; + matched_ = true; + return true; + } + + bool positional() const override { return true; } + + bool required() const override { return required_; } + + private: + const char* name_; + const char* help_text_; + const char** storage_; + const int verbosity_level_; + const bool required_; + + bool matched_{false}; + }; + + // A class for handling an option flag like '-v' or '--foo=bar'. + template <typename T> + class CmdOptionFlag : public CmdOptionInterface { + public: + // Construct a flag that doesn't take any value, for example '-v' or + // '--long'. Passing a value to it raises an error. + CmdOptionFlag(char short_name, const char* long_name, const char* help_text, + T* storage, bool(parser)(T*), int verbosity_level) + : short_name_(short_name), + long_name_(long_name), + long_name_len_(long_name ? strlen(long_name) : 0), + metavar_(nullptr), + help_text_(help_text), + storage_(storage), + verbosity_level_(verbosity_level) { + parser_.parser_no_value_ = parser; + } + + // Construct a flag that expects a value to be passed. + CmdOptionFlag(char short_name, const char* long_name, const char* metavar, + const char* help_text, T* storage, + bool(parser)(const char* arg, T*), int verbosity_level) + : short_name_(short_name), + long_name_(long_name), + long_name_len_(long_name ? strlen(long_name) : 0), + metavar_(metavar ? metavar : ""), + help_text_(help_text), + storage_(storage), + verbosity_level_(verbosity_level) { + parser_.parser_with_arg_ = parser; + } + + std::string help_flags() const override { + std::string ret; + if (short_name_) { + ret += std::string("-") + short_name_; + if (metavar_) ret += std::string(" ") + metavar_; + if (long_name_) ret += ", "; + } + if (long_name_) { + ret += std::string("--") + long_name_; + if (metavar_) ret += std::string("=") + metavar_; + } + return ret; + } + const char* help_text() const override { return help_text_; } + int verbosity_level() const override { return verbosity_level_; } + bool matched() const override { return matched_; } + + bool Match(const char* arg, bool parse_options) const override { + return parse_options && (MatchShort(arg) || MatchLong(arg)); + } + + bool Parse(const int argc, const char* argv[], int* i) override { + matched_ = true; + if (MatchLong(argv[*i])) { + const char* arg = argv[*i] + 2 + long_name_len_; + if (arg[0] == '=') { + if (metavar_) { + // Passed '--long_name=...'. + (*i)++; + // Skip over the '=' on the LongMatch. + arg += 1; + return (*parser_.parser_with_arg_)(arg, storage_); + } else { + fprintf(stderr, "--%s didn't expect any argument passed to it.\n", + argv[*i]); + return false; + } + } + } + // In any other case, it passed a -s or --long_name + (*i)++; + if (metavar_) { + if (argc <= *i) { + fprintf(stderr, "--%s expected an argument but none passed.\n", + argv[*i - 1]); + return false; + } + return (*parser_.parser_with_arg_)(argv[(*i)++], storage_); + } else { + return (*parser_.parser_no_value_)(storage_); + } + } + + bool positional() const override { return false; } + + bool required() const override { + // Only used for help display of positional arguments. + return false; + } + + private: + // Returns whether arg matches the short_name flag of this option. + bool MatchShort(const char* arg) const { + if (!short_name_ || arg[0] != '-') return false; + return arg[1] == short_name_ && arg[2] == 0; + } + + // Returns whether arg matches the long_name flag of this option, + // potentially with an argument passed to it. + bool MatchLong(const char* arg) const { + if (!long_name_ || arg[0] != '-' || arg[1] != '-') return false; + arg += 2; // Skips the '--' + if (strncmp(long_name_, arg, long_name_len_) != 0) return false; + arg += long_name_len_; + // Allow "--long_name=foo" and "--long_name" as long matches. + return arg[0] == 0 || arg[0] == '='; + } + + // A short option passed as '-X' where X is the char. A value of 0 means + // no short option. + const char short_name_; + + // A long option name passed as '--long' where 'long' is the name of the + // option. + const char* long_name_; + size_t long_name_len_; + + // The text to display when referring to the value passed to this flag, for + // example "N" in the flag '--value N'. If null, this flag accepts no value + // and therefore no value must be passed. + const char* metavar_; + + // The help string for this flag. + const char* help_text_; + + // The pointer to the storage of this flag used when parsing. + T* storage_; + + // At which verbosity level do we show this option? + int verbosity_level_; + + // The function to use to parse the value when matched. The function used is + // parser_with_arg_ when metavar_ is not null (and the value string will be + // used) or parser_no_value_ when metavar_ is null. + union { + bool (*parser_with_arg_)(const char*, T*); + bool (*parser_no_value_)(T*); + } parser_; + + // Whether this flag was matched. + bool matched_{false}; + }; + + const char* program_name_{nullptr}; + + std::vector<std::unique_ptr<CmdOptionInterface>> options_; + + // If true, help argument was given, so print help to stdout rather than + // stderr. + bool help_ = false; +}; + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_CMDLINE_H_ diff --git a/media/libjxl/src/tools/codec_config.cc b/media/libjxl/src/tools/codec_config.cc new file mode 100644 index 0000000000..9ed64a23c8 --- /dev/null +++ b/media/libjxl/src/tools/codec_config.cc @@ -0,0 +1,57 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/codec_config.h" + +#include <hwy/targets.h> + +#include "lib/jxl/base/status.h" +#include "tools/tool_version.h" + +namespace jpegxl { +namespace tools { + +std::string CodecConfigString(uint32_t lib_version) { + std::string config; + + if (lib_version != 0) { + char version_str[20]; + snprintf(version_str, sizeof(version_str), "v%d.%d.%d ", + lib_version / 1000000, (lib_version / 1000) % 1000, + lib_version % 1000); + config += version_str; + } + + std::string version = kJpegxlVersion; + if (version != "(unknown)") { + config += version + ' '; + } + +#if defined(ADDRESS_SANITIZER) + config += " asan "; +#elif defined(MEMORY_SANITIZER) + config += " msan "; +#elif defined(THREAD_SANITIZER) + config += " tsan "; +#else +#endif + + bool saw_target = false; + config += "["; + for (const uint32_t target : hwy::SupportedAndGeneratedTargets()) { + config += hwy::TargetName(target); + config += ','; + saw_target = true; + } + JXL_ASSERT(saw_target); + (void)saw_target; + config.resize(config.size() - 1); // remove trailing comma + config += "]"; + + return config; +} + +} // namespace tools +} // namespace jpegxl diff --git a/media/libjxl/src/tools/codec_config.h b/media/libjxl/src/tools/codec_config.h new file mode 100644 index 0000000000..729c96d4a8 --- /dev/null +++ b/media/libjxl/src/tools/codec_config.h @@ -0,0 +1,22 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_CODEC_CONFIG_H_ +#define TOOLS_CODEC_CONFIG_H_ + +#include <string> + +namespace jpegxl { +namespace tools { + +// Returns a short string describing the codec version (if known) and build +// settings such as sanitizers and SIMD targets. Used in the benchmark and +// command-line tools. +std::string CodecConfigString(uint32_t lib_version); + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_CODEC_CONFIG_H_ diff --git a/media/libjxl/src/tools/color_encoding_fuzzer.cc b/media/libjxl/src/tools/color_encoding_fuzzer.cc new file mode 100644 index 0000000000..087bd8ba1e --- /dev/null +++ b/media/libjxl/src/tools/color_encoding_fuzzer.cc @@ -0,0 +1,24 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <string> + +#include "lib/extras/dec/color_description.h" + +namespace jxl { + +int TestOneInput(const uint8_t* data, size_t size) { + std::string description(reinterpret_cast<const char*>(data), size); + JxlColorEncoding c; + (void)ParseDescription(description, &c); + + return 0; +} + +} // namespace jxl + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return jxl::TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/comparison_viewer/CMakeLists.txt b/media/libjxl/src/tools/comparison_viewer/CMakeLists.txt new file mode 100644 index 0000000000..b5b5fa742e --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/CMakeLists.txt @@ -0,0 +1,74 @@ +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +find_package(Qt5 QUIET COMPONENTS Concurrent Widgets) +if (NOT Qt5_FOUND) + message(WARNING "Qt5 was not found. The comparison tool will not be built.") + return() +endif () + +if (NOT TARGET icc_detect) + message(WARNING "icc_detect not built. The comparison tool will not be built.") + return () +endif () + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +add_library(image_loading STATIC + ../viewer/load_jxl.cc + ../viewer/load_jxl.h + image_loading.cc + image_loading.h +) +target_include_directories(image_loading PRIVATE + $<TARGET_PROPERTY:lcms2,INCLUDE_DIRECTORIES> +) +target_link_libraries(image_loading PUBLIC + Qt5::Widgets + jxl-static + jxl_threads-static + jxl_extras-static + lcms2 +) + +add_executable(compare_codecs WIN32 + codec_comparison_window.cc + codec_comparison_window.h + codec_comparison_window.ui + compare_codecs.cc + settings.cc + settings.h + settings.ui + split_image_renderer.cc + split_image_renderer.h + split_image_view.cc + split_image_view.h + split_image_view.ui +) +target_link_libraries(compare_codecs + image_loading + Qt5::Concurrent + Qt5::Widgets + icc_detect +) + +add_executable(compare_images WIN32 + compare_images.cc + settings.cc + settings.h + settings.ui + split_image_renderer.cc + split_image_renderer.h + split_image_view.cc + split_image_view.h + split_image_view.ui +) +target_link_libraries(compare_images + image_loading + Qt5::Widgets + icc_detect +) diff --git a/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.cc b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.cc new file mode 100644 index 0000000000..9bf6253ba6 --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.cc @@ -0,0 +1,316 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/comparison_viewer/codec_comparison_window.h" + +#include <stdlib.h> + +#include <QCollator> +#include <QComboBox> +#include <QDir> +#include <QFileInfo> +#include <QFlags> +#include <QIcon> +#include <QImage> +#include <QImageReader> +#include <QLabel> +#include <QList> +#include <QMap> +#include <QString> +#include <QStringList> +#include <QtConcurrent> +#include <algorithm> +#include <climits> +#include <functional> +#include <utility> + +#include "lib/extras/codec.h" +#include "tools/comparison_viewer/image_loading.h" +#include "tools/comparison_viewer/split_image_view.h" +#include "tools/icc_detect/icc_detect.h" + +namespace jxl { + +static constexpr char kPngSuffix[] = "png"; + +namespace { + +QVector<QPair<QComboBox*, QString>> currentCodecSelection( + const Ui::CodecComparisonWindow& ui) { + QVector<QPair<QComboBox*, QString>> result; + for (QComboBox* const comboBox : + {ui.codec1ComboBox, ui.codec2ComboBox, ui.compressionLevel1ComboBox, + ui.compressionLevel2ComboBox}) { + result << qMakePair(comboBox, comboBox->currentText()); + } + return result; +} + +void restoreCodecSelection( + const QVector<QPair<QComboBox*, QString>>& selection) { + for (const auto& comboBox : selection) { + const int index = comboBox.first->findText(comboBox.second); + if (index != -1) { + comboBox.first->setCurrentIndex(index); + } + } +} + +} // namespace + +CodecComparisonWindow::CodecComparisonWindow(const QString& directory, + const float intensityTarget, + QWidget* const parent) + : QMainWindow(parent), + intensityTarget_(intensityTarget), + monitorIccProfile_(GetMonitorIccProfile(this)) { + ui_.setupUi(this); + + connect(ui_.imageSetComboBox, &QComboBox::currentTextChanged, this, + &CodecComparisonWindow::handleImageSetSelection); + connect(ui_.imageComboBox, &QComboBox::currentTextChanged, this, + &CodecComparisonWindow::handleImageSelection); + + connect(ui_.codec1ComboBox, &QComboBox::currentTextChanged, + [this]() { handleCodecChange(Side::LEFT); }); + connect(ui_.codec2ComboBox, &QComboBox::currentTextChanged, + [this]() { handleCodecChange(Side::RIGHT); }); + + connect(ui_.compressionLevel1ComboBox, &QComboBox::currentTextChanged, + [this]() { updateSideImage(Side::LEFT); }); + connect(ui_.compressionLevel2ComboBox, &QComboBox::currentTextChanged, + [this]() { updateSideImage(Side::RIGHT); }); + + connect(ui_.match1Label, &QLabel::linkActivated, + [this]() { matchSize(Side::LEFT); }); + connect(ui_.match2Label, &QLabel::linkActivated, + [this]() { matchSize(Side::RIGHT); }); + + connect( + ui_.splitImageView, &SplitImageView::renderingModeChanged, + [this](const SplitImageRenderer::RenderingMode newMode) { + switch (newMode) { + case SplitImageRenderer::RenderingMode::LEFT: + case SplitImageRenderer::RenderingMode::RIGHT: { + QString codec, compressionLevel; + if (newMode == SplitImageRenderer::RenderingMode::LEFT) { + codec = ui_.codec1ComboBox->currentText(); + compressionLevel = ui_.compressionLevel1ComboBox->currentText(); + } else { + codec = ui_.codec2ComboBox->currentText(); + compressionLevel = ui_.compressionLevel2ComboBox->currentText(); + } + ui_.renderingModeLabel->setText(tr("Currently displaying: %1 @ %2") + .arg(codec) + .arg(compressionLevel)); + break; + } + + case SplitImageRenderer::RenderingMode::MIDDLE: + ui_.renderingModeLabel->setText( + tr("Currently displaying the original image.")); + break; + + default: + ui_.renderingModeLabel->clear(); + break; + } + }); + + loadDirectory(directory); +} + +void CodecComparisonWindow::handleImageSetSelection( + const QString& imageSetName) { + const auto selection = currentCodecSelection(ui_); + { + const QSignalBlocker blocker(ui_.imageComboBox); + ui_.imageComboBox->clear(); + } + const QStringList imageNames = imageSets_.value(imageSetName).keys(); + const std::function<QIcon(const QString&)> loadIcon = + [this, &imageSetName](const QString& imageName) { + return QIcon(pathToOriginalImage(imageSetName, imageName)); + }; + const QFuture<QIcon> thumbnails = QtConcurrent::mapped(imageNames, loadIcon); + int i = 0; + for (const QString& imageName : imageNames) { + ui_.imageComboBox->addItem(thumbnails.resultAt(i), imageName); + ++i; + } + restoreCodecSelection(selection); +} + +void CodecComparisonWindow::handleImageSelection(const QString& imageName) { + const QString imageSetName = ui_.imageSetComboBox->currentText(); + ui_.splitImageView->setMiddleImage( + loadImage(pathToOriginalImage(imageSetName, imageName), + monitorIccProfile_, intensityTarget_)); + + const auto selection = currentCodecSelection(ui_); + QStringList codecs = imageSets_.value(imageSetName).value(imageName).keys(); + for (QComboBox* const codecComboBox : + {ui_.codec1ComboBox, ui_.codec2ComboBox}) { + { + const QSignalBlocker blocker(codecComboBox); + codecComboBox->clear(); + } + codecComboBox->addItems(codecs); + } + restoreCodecSelection(selection); +} + +void CodecComparisonWindow::handleCodecChange(const Side side) { + const QComboBox* const codecComboBox = + side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox; + QComboBox* const compressionLevelComboBox = + side == Side::LEFT ? ui_.compressionLevel1ComboBox + : ui_.compressionLevel2ComboBox; + + QStringList compressionLevels = + imageSets_.value(ui_.imageSetComboBox->currentText()) + .value(ui_.imageComboBox->currentText()) + .value(codecComboBox->currentText()) + .keys(); + QCollator collator; + collator.setNumericMode(true); + std::sort(compressionLevels.begin(), compressionLevels.end(), collator); + + { + const QSignalBlocker blocker(compressionLevelComboBox); + compressionLevelComboBox->clear(); + } + compressionLevelComboBox->addItems(compressionLevels); + matchSize(side); +} + +void CodecComparisonWindow::updateSideImage(const Side side) { + const ComparableImage& imageInfo = currentlySelectedImage(side); + if (imageInfo.decodedImagePath.isEmpty()) return; + QImage image = loadImage(imageInfo.decodedImagePath, monitorIccProfile_, + intensityTarget_); + const int pixels = image.width() * image.height(); + QLabel* const sizeInfoLabel = + side == Side::LEFT ? ui_.size1Label : ui_.size2Label; + if (pixels == 0) { + sizeInfoLabel->setText(tr("Empty image.")); + } else { + const double bpp = + CHAR_BIT * static_cast<double>(imageInfo.byteSize) / pixels; + sizeInfoLabel->setText(tr("%L1bpp").arg(bpp)); + } + + if (side == Side::LEFT) { + ui_.splitImageView->setLeftImage(std::move(image)); + } else { + ui_.splitImageView->setRightImage(std::move(image)); + } +} + +QString CodecComparisonWindow::pathToOriginalImage( + const QString& imageSetName, const QString& imageName) const { + return baseDirectory_.absolutePath() + "/" + imageSetName + "/" + imageName + + "/original.png"; +} + +CodecComparisonWindow::ComparableImage +CodecComparisonWindow::currentlySelectedImage(const Side side) const { + const QComboBox* const codecComboBox = + side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox; + QComboBox* const compressionLevelComboBox = + side == Side::LEFT ? ui_.compressionLevel1ComboBox + : ui_.compressionLevel2ComboBox; + + return imageSets_.value(ui_.imageSetComboBox->currentText()) + .value(ui_.imageComboBox->currentText()) + .value(codecComboBox->currentText()) + .value(compressionLevelComboBox->currentText()); +} + +void CodecComparisonWindow::matchSize(const Side side) { + const Side otherSide = (side == Side::LEFT ? Side::RIGHT : Side::LEFT); + const qint64 otherSideSize = currentlySelectedImage(otherSide).byteSize; + if (otherSideSize == 0) return; + + const QComboBox* const codecComboBox = + side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox; + QComboBox* const compressionLevelComboBox = + side == Side::LEFT ? ui_.compressionLevel1ComboBox + : ui_.compressionLevel2ComboBox; + const Codec codec = imageSets_.value(ui_.imageSetComboBox->currentText()) + .value(ui_.imageComboBox->currentText()) + .value(codecComboBox->currentText()); + if (codec.empty()) return; + Codec::ConstIterator bestMatch = codec.begin(); + for (auto it = codec.begin(); it != codec.end(); ++it) { + if (std::abs(it->byteSize - otherSideSize) < + std::abs(bestMatch->byteSize - otherSideSize)) { + bestMatch = it; + } + } + compressionLevelComboBox->setCurrentText(bestMatch.key()); +} + +void CodecComparisonWindow::loadDirectory(const QString& directory) { + baseDirectory_.setPath(directory); + baseDirectory_.makeAbsolute(); + imageSets_.clear(); + visited_.clear(); + + browseDirectory(directory); + + { + const QSignalBlocker blocker(ui_.imageSetComboBox); + ui_.imageSetComboBox->clear(); + } + ui_.imageSetComboBox->addItems(imageSets_.keys()); +} + +void CodecComparisonWindow::browseDirectory(const QDir& directory, int depth) { + for (const QFileInfo& subdirectory : directory.entryInfoList( + QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks)) { + if (visited_.contains(subdirectory.absoluteFilePath())) continue; + visited_.insert(subdirectory.absoluteFilePath()); + browseDirectory(subdirectory.absoluteFilePath(), depth + 1); + } + + // Need at least image_name/codec_name/file. + if (depth < 2) return; + + for (const QFileInfo& file : directory.entryInfoList(QDir::Files)) { + if (file.suffix() == kPngSuffix) continue; + QString decodedImage; + if (canLoadImageWithExtension(file.suffix())) { + decodedImage = file.absoluteFilePath(); + } else { + QFileInfo png(file.absolutePath() + "/" + file.completeBaseName() + "." + + kPngSuffix); + if (png.exists()) { + decodedImage = png.absoluteFilePath(); + } + } + + if (decodedImage.isEmpty()) continue; + + const QString codec = file.absoluteDir().dirName(); + QDir imageDirectory = file.absoluteDir(); + if (!imageDirectory.cdUp()) return; + const QString imageName = imageDirectory.dirName(); + QDir imageSetDirectory = imageDirectory; + if (!imageSetDirectory.cdUp()) return; + QString imageSetPath = + baseDirectory_.relativeFilePath(imageSetDirectory.absolutePath()); + if (imageSetPath.isEmpty()) { + imageSetPath = "."; + } + + ComparableImage& image = + imageSets_[imageSetPath][imageName][codec][file.completeBaseName()]; + image.decodedImagePath = decodedImage; + image.byteSize = file.size(); + } +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.h b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.h new file mode 100644 index 0000000000..b157a5a9ef --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.h @@ -0,0 +1,77 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_COMPARISON_VIEWER_CODEC_COMPARISON_WINDOW_H_ +#define TOOLS_COMPARISON_VIEWER_CODEC_COMPARISON_WINDOW_H_ + +#include <QDir> +#include <QMainWindow> +#include <QMap> +#include <QSet> +#include <QString> + +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/common.h" +#include "tools/comparison_viewer/ui_codec_comparison_window.h" + +namespace jxl { + +class CodecComparisonWindow : public QMainWindow { + Q_OBJECT + + public: + explicit CodecComparisonWindow( + const QString& directory, float intensityTarget = kDefaultIntensityTarget, + QWidget* parent = nullptr); + ~CodecComparisonWindow() override = default; + + private slots: + void handleImageSetSelection(const QString& imageSetName); + void handleImageSelection(const QString& imageName); + + private: + struct ComparableImage { + // Absolute path to the decoded PNG (or an image that Qt can read). + QString decodedImagePath; + // Size of the encoded image (*not* the PNG). + qint64 byteSize = 0; + }; + // Keys are compression levels. + using Codec = QMap<QString, ComparableImage>; + // Keys are codec names. + using Codecs = QMap<QString, Codec>; + // Keys are image names (relative to the image set directory). + using ImageSet = QMap<QString, Codecs>; + // Keys are paths to image sets (relative to the base directory chosen by the + // user). + using ImageSets = QMap<QString, ImageSet>; + + enum class Side { LEFT, RIGHT }; + + QString pathToOriginalImage(const QString& imageSet, + const QString& imageName) const; + ComparableImage currentlySelectedImage(Side side) const; + + void handleCodecChange(Side side); + void updateSideImage(Side side); + void matchSize(Side side); + + void loadDirectory(const QString& directory); + // Recursive, called by loadDirectory. + void browseDirectory(const QDir& directory, int depth = 0); + + Ui::CodecComparisonWindow ui_; + + QDir baseDirectory_; + ImageSets imageSets_; + QSet<QString> visited_; + + const float intensityTarget_; + const QByteArray monitorIccProfile_; +}; + +} // namespace jxl + +#endif // TOOLS_COMPARISON_VIEWER_CODEC_COMPARISON_WINDOW_H_ diff --git a/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.ui b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.ui new file mode 100644 index 0000000000..1fbda6a1ca --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.ui @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <comment> + Copyright (c) the JPEG XL Project Authors. All rights reserved. + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. + </comment> + <class>CodecComparisonWindow</class> + <widget class="QMainWindow" name="CodecComparisonWindow"> + <property name="windowTitle"> + <string>Codec Comparison Tool</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,1"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0,1"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="1"> + <widget class="QComboBox" name="imageSetComboBox"/> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="imageSetLabel"> + <property name="text"> + <string>Image set:</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="imageLabel"> + <property name="text"> + <string>Image:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="imageComboBox"/> + </item> + </layout> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="1"> + <widget class="QComboBox" name="compressionLevel1ComboBox"/> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="codec1ComboBox"/> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="match1Label"> + <property name="text"> + <string><a href="#match1">Match →</a></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="0"> + <widget class="QLabel" name="match2Label"> + <property name="text"> + <string><a href="#match2">Match ←</a></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="compressionLevel2ComboBox"/> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="codec2ComboBox"/> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1,0,1,0"> + <item> + <widget class="QLabel" name="size1Label"> + <property name="text"> + <string>No image loaded.</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="renderingModeLabel"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="size2Label"> + <property name="text"> + <string>No image loaded.</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="jxl::SplitImageView" name="splitImageView" native="true"/> + </item> + </layout> + </widget> + </widget> + <customwidgets> + <customwidget> + <class>jxl::SplitImageView</class> + <extends>QWidget</extends> + <header>split_image_view.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/media/libjxl/src/tools/comparison_viewer/compare_codecs.cc b/media/libjxl/src/tools/comparison_viewer/compare_codecs.cc new file mode 100644 index 0000000000..932765e479 --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/compare_codecs.cc @@ -0,0 +1,75 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdlib.h> + +#include <QApplication> +#include <QCommandLineParser> +#include <QMessageBox> +#include <QString> +#include <QStringList> + +#include "tools/comparison_viewer/codec_comparison_window.h" + +int main(int argc, char** argv) { + QApplication application(argc, argv); + + QCommandLineParser parser; + parser.setApplicationDescription( + QCoreApplication::translate("compare_codecs", "Codec comparison tool")); + parser.addHelpOption(); + + QCommandLineOption intensityTargetOption( + {"intensity-target", "intensity_target", "i"}, + QCoreApplication::translate("compare_codecs", + "The peak luminance of the display."), + QCoreApplication::translate("compare_codecs", "nits"), + QString::number(jxl::kDefaultIntensityTarget)); + parser.addOption(intensityTargetOption); + + parser.addPositionalArgument( + "folders", QCoreApplication::translate("compare_codecs", "Image folders"), + "<folders>..."); + + parser.process(application); + + bool ok; + const float intensityTarget = + parser.value(intensityTargetOption).toFloat(&ok); + if (!ok) { + parser.showHelp(EXIT_FAILURE); + } + + QStringList folders = parser.positionalArguments(); + + if (folders.empty()) { + QMessageBox message; + message.setIcon(QMessageBox::Information); + message.setWindowTitle( + QCoreApplication::translate("CodecComparisonWindow", "Usage")); + message.setText(QCoreApplication::translate( + "CodecComparisonWindow", "Please specify a directory to use.")); + message.setDetailedText(QCoreApplication::translate( + "CodecComparisonWindow", + "That directory should contain images in the following layout:\n" + "- .../<image name>/original.png (optional)\n" + "- .../<image_name>/<codec_name>/<compression_level>.<ext>\n" + "- .../<image_name>/<codec_name>/<compression_level>.png (optional for " + "formats that Qt can load)\n" + "With arbitrary nesting allowed before that. (The \"...\" part is " + "referred to as an \"image set\" by the tool.")); + message.exec(); + return EXIT_FAILURE; + } + + for (const QString& folder : folders) { + auto* const window = + new jxl::CodecComparisonWindow(folder, intensityTarget); + window->setAttribute(Qt::WA_DeleteOnClose); + window->show(); + } + + return application.exec(); +} diff --git a/media/libjxl/src/tools/comparison_viewer/compare_images.cc b/media/libjxl/src/tools/comparison_viewer/compare_images.cc new file mode 100644 index 0000000000..cf39f88128 --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/compare_images.cc @@ -0,0 +1,128 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdlib.h> + +#include <QApplication> +#include <QCommandLineOption> +#include <QCommandLineParser> +#include <QFlags> +#include <QImage> +#include <QMessageBox> +#include <QStringList> + +#include "tools/comparison_viewer/image_loading.h" +#include "tools/comparison_viewer/split_image_view.h" +#include "tools/icc_detect/icc_detect.h" + +namespace { + +void displayLoadingError(const QString& path) { + QMessageBox message; + message.setIcon(QMessageBox::Critical); + message.setWindowTitle( + QCoreApplication::translate("SplitImageView", "Error")); + message.setText(QCoreApplication::translate("SplitImageView", + "Could not load image \"%1\".") + .arg(path)); + message.exec(); +} + +} // namespace + +int main(int argc, char** argv) { + QApplication application(argc, argv); + + QCommandLineParser parser; + parser.setApplicationDescription( + QCoreApplication::translate("compare_images", "Image comparison tool")); + parser.addHelpOption(); + parser.addPositionalArgument( + "left-image", + QCoreApplication::translate("compare_images", + "The image to display on the left."), + "<left-image>"); + parser.addPositionalArgument( + "right-image", + QCoreApplication::translate("compare_images", + "The image to display on the right."), + "<right-image>"); + parser.addPositionalArgument( + "middle-image", + QCoreApplication::translate( + "compare_images", "The image to display in the middle (optional)."), + "[<middle-image>]"); + + QCommandLineOption colorSpaceOption( + {"color-space", "color_space", "c"}, + QCoreApplication::translate( + "compare_images", + "The color space to use for untagged images (typically PNM)."), + QCoreApplication::translate("compare_images", "color-space")); + parser.addOption(colorSpaceOption); + + QCommandLineOption intensityTargetOption( + {"intensity-target", "intensity_target", "i"}, + QCoreApplication::translate("compare_images", + "The peak luminance of the display."), + QCoreApplication::translate("compare_images", "nits"), + QString::number(jxl::kDefaultIntensityTarget)); + parser.addOption(intensityTargetOption); + + parser.process(application); + + const QString colorSpaceHint = parser.value(colorSpaceOption); + + QStringList arguments = parser.positionalArguments(); + if (arguments.size() < 2 || arguments.size() > 3) { + parser.showHelp(EXIT_FAILURE); + } + + bool ok; + const float intensityTarget = + parser.value(intensityTargetOption).toFloat(&ok); + if (!ok) { + parser.showHelp(EXIT_FAILURE); + } + + jxl::SplitImageView view; + + const QByteArray monitorIccProfile = jxl::GetMonitorIccProfile(&view); + + const QString leftImagePath = arguments.takeFirst(); + QImage leftImage = jxl::loadImage(leftImagePath, monitorIccProfile, + intensityTarget, colorSpaceHint); + if (leftImage.isNull()) { + displayLoadingError(leftImagePath); + return EXIT_FAILURE; + } + view.setLeftImage(std::move(leftImage)); + + const QString rightImagePath = arguments.takeFirst(); + QImage rightImage = jxl::loadImage(rightImagePath, monitorIccProfile, + intensityTarget, colorSpaceHint); + if (rightImage.isNull()) { + displayLoadingError(rightImagePath); + return EXIT_FAILURE; + } + view.setRightImage(std::move(rightImage)); + + if (!arguments.empty()) { + const QString middleImagePath = arguments.takeFirst(); + QImage middleImage = jxl::loadImage(middleImagePath, monitorIccProfile, + intensityTarget, colorSpaceHint); + if (middleImage.isNull()) { + displayLoadingError(middleImagePath); + return EXIT_FAILURE; + } + view.setMiddleImage(std::move(middleImage)); + } + + view.setWindowFlags(view.windowFlags() | Qt::Window); + view.setWindowState(Qt::WindowMaximized); + view.show(); + + return application.exec(); +} diff --git a/media/libjxl/src/tools/comparison_viewer/image_loading.cc b/media/libjxl/src/tools/comparison_viewer/image_loading.cc new file mode 100644 index 0000000000..55bebb8a1a --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/image_loading.cc @@ -0,0 +1,111 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/comparison_viewer/image_loading.h" + +#include <QRgb> +#include <QThread> + +#include "lib/extras/codec.h" +#include "lib/extras/dec/color_hints.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/enc_color_management.h" +#include "tools/viewer/load_jxl.h" + +namespace jxl { + +namespace { + +Status loadFromFile(const QString& filename, + const extras::ColorHints& color_hints, + CodecInOut* const decoded, ThreadPool* const pool) { + PaddedBytes compressed; + JXL_RETURN_IF_ERROR(ReadFile(filename.toStdString(), &compressed)); + const Span<const uint8_t> compressed_span(compressed); + return SetFromBytes(compressed_span, color_hints, decoded, pool, nullptr); +} + +} // namespace + +bool canLoadImageWithExtension(QString extension) { + extension = extension.toLower(); + size_t bitsPerSampleUnused; + return extension == "jxl" || extension == "j" || extension == "brn" || + extras::CodecFromExtension("." + extension.toStdString(), + &bitsPerSampleUnused) != + jxl::extras::Codec::kUnknown; +} + +QImage loadImage(const QString& filename, const QByteArray& targetIccProfile, + const float intensityTarget, + const QString& sourceColorSpaceHint) { + qint64 elapsed; + QImage img = loadJxlImage(filename, targetIccProfile, &elapsed); + if (img.width() != 0 && img.height() != 0) { + return img; + } + static ThreadPoolInternal pool(QThread::idealThreadCount()); + + CodecInOut decoded; + extras::ColorHints color_hints; + if (!sourceColorSpaceHint.isEmpty()) { + color_hints.Add("color_space", sourceColorSpaceHint.toStdString()); + } + if (!loadFromFile(filename, color_hints, &decoded, &pool)) { + return QImage(); + } + decoded.metadata.m.SetIntensityTarget(intensityTarget); + const ImageBundle& ib = decoded.Main(); + + ColorEncoding targetColorSpace; + PaddedBytes icc; + icc.assign(reinterpret_cast<const uint8_t*>(targetIccProfile.data()), + reinterpret_cast<const uint8_t*>(targetIccProfile.data() + + targetIccProfile.size())); + if (!targetColorSpace.SetICC(std::move(icc))) { + targetColorSpace = ColorEncoding::SRGB(ib.IsGray()); + } + Image3F converted; + if (!ib.CopyTo(Rect(ib), targetColorSpace, GetJxlCms(), &converted, &pool)) { + return QImage(); + } + + QImage image(converted.xsize(), converted.ysize(), QImage::Format_ARGB32); + + const auto ScaleAndClamp = [](const float x) { + return Clamp1(x * 255 + .5f, 0.f, 255.f); + }; + + if (ib.HasAlpha()) { + for (int y = 0; y < image.height(); ++y) { + QRgb* const row = reinterpret_cast<QRgb*>(image.scanLine(y)); + const float* const alphaRow = ib.alpha().ConstRow(y); + const float* const redRow = converted.ConstPlaneRow(0, y); + const float* const greenRow = converted.ConstPlaneRow(1, y); + const float* const blueRow = converted.ConstPlaneRow(2, y); + for (int x = 0; x < image.width(); ++x) { + row[x] = qRgba(ScaleAndClamp(redRow[x]), ScaleAndClamp(greenRow[x]), + ScaleAndClamp(blueRow[x]), ScaleAndClamp(alphaRow[x])); + } + } + } else { + for (int y = 0; y < image.height(); ++y) { + QRgb* const row = reinterpret_cast<QRgb*>(image.scanLine(y)); + const float* const redRow = converted.ConstPlaneRow(0, y); + const float* const greenRow = converted.ConstPlaneRow(1, y); + const float* const blueRow = converted.ConstPlaneRow(2, y); + for (int x = 0; x < image.width(); ++x) { + row[x] = qRgb(ScaleAndClamp(redRow[x]), ScaleAndClamp(greenRow[x]), + ScaleAndClamp(blueRow[x])); + } + } + } + + return image; +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/comparison_viewer/image_loading.h b/media/libjxl/src/tools/comparison_viewer/image_loading.h new file mode 100644 index 0000000000..89b37d13b9 --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/image_loading.h @@ -0,0 +1,29 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_COMPARISON_VIEWER_IMAGE_LOADING_H_ +#define TOOLS_COMPARISON_VIEWER_IMAGE_LOADING_H_ + +#include <QByteArray> +#include <QImage> +#include <QString> + +#include "lib/jxl/common.h" + +namespace jxl { + +// `extension` should not include the dot. +bool canLoadImageWithExtension(QString extension); + +// Converts the loaded image to the given display profile, or sRGB if not +// specified. Thread-hostile. +QImage loadImage(const QString& filename, + const QByteArray& targetIccProfile = QByteArray(), + float intensityTarget = kDefaultIntensityTarget, + const QString& sourceColorSpaceHint = QString()); + +} // namespace jxl + +#endif // TOOLS_COMPARISON_VIEWER_IMAGE_LOADING_H_ diff --git a/media/libjxl/src/tools/comparison_viewer/settings.cc b/media/libjxl/src/tools/comparison_viewer/settings.cc new file mode 100644 index 0000000000..9ef117b0a7 --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/settings.cc @@ -0,0 +1,51 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/comparison_viewer/settings.h" + +namespace jxl { + +SettingsDialog::SettingsDialog(QWidget* const parent) + : QDialog(parent), settings_("JPEG XL project", "Comparison tool") { + ui_.setupUi(this); + + settings_.beginGroup("rendering"); + renderingSettings_.fadingMSecs = settings_.value("fadingMSecs", 300).toInt(); + settings_.beginGroup("gray"); + renderingSettings_.gray = settings_.value("enabled", false).toBool(); + renderingSettings_.grayMSecs = settings_.value("delayMSecs", 300).toInt(); + settings_.endGroup(); + settings_.endGroup(); + + settingsToUi(); +} + +SplitImageRenderingSettings SettingsDialog::renderingSettings() const { + return renderingSettings_; +} + +void SettingsDialog::on_SettingsDialog_accepted() { + renderingSettings_.fadingMSecs = ui_.fadingTime->value(); + renderingSettings_.gray = ui_.grayGroup->isChecked(); + renderingSettings_.grayMSecs = ui_.grayTime->value(); + + settings_.beginGroup("rendering"); + settings_.setValue("fadingMSecs", renderingSettings_.fadingMSecs); + settings_.beginGroup("gray"); + settings_.setValue("enabled", renderingSettings_.gray); + settings_.setValue("delayMSecs", renderingSettings_.grayMSecs); + settings_.endGroup(); + settings_.endGroup(); +} + +void SettingsDialog::on_SettingsDialog_rejected() { settingsToUi(); } + +void SettingsDialog::settingsToUi() { + ui_.fadingTime->setValue(renderingSettings_.fadingMSecs); + ui_.grayGroup->setChecked(renderingSettings_.gray); + ui_.grayTime->setValue(renderingSettings_.grayMSecs); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/comparison_viewer/settings.h b/media/libjxl/src/tools/comparison_viewer/settings.h new file mode 100644 index 0000000000..bd91f710aa --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/settings.h @@ -0,0 +1,40 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_COMPARISON_VIEWER_SETTINGS_H_ +#define TOOLS_COMPARISON_VIEWER_SETTINGS_H_ + +#include <QDialog> +#include <QSettings> + +#include "tools/comparison_viewer/split_image_renderer.h" +#include "tools/comparison_viewer/ui_settings.h" + +namespace jxl { + +class SettingsDialog : public QDialog { + Q_OBJECT + + public: + explicit SettingsDialog(QWidget* parent = nullptr); + ~SettingsDialog() override = default; + + SplitImageRenderingSettings renderingSettings() const; + + private slots: + void on_SettingsDialog_accepted(); + void on_SettingsDialog_rejected(); + + private: + void settingsToUi(); + + Ui::SettingsDialog ui_; + QSettings settings_; + SplitImageRenderingSettings renderingSettings_; +}; + +} // namespace jxl + +#endif // TOOLS_COMPARISON_VIEWER_SETTINGS_H_ diff --git a/media/libjxl/src/tools/comparison_viewer/settings.ui b/media/libjxl/src/tools/comparison_viewer/settings.ui new file mode 100644 index 0000000000..ca81a33aec --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/settings.ui @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <comment> + Copyright (c) the JPEG XL Project Authors. All rights reserved. + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. + </comment> + <class>SettingsDialog</class> + <widget class="QDialog" name="SettingsDialog"> + <property name="windowTitle"> + <string>Comparison tool settings</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0"> + <property name="sizeConstraint"> + <enum>QLayout::SetFixedSize</enum> + </property> + <item> + <layout class="QFormLayout" name="settingsLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="fadingTimePromptLabel"> + <property name="text"> + <string>Fading time:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="fadingTime"> + <property name="suffix"> + <string> ms</string> + </property> + <property name="maximum"> + <number>1000</number> + </property> + <property name="singleStep"> + <number>50</number> + </property> + <property name="value"> + <number>300</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="grayGroup"> + <property name="title"> + <string>Gray in between</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="1"> + <widget class="QSpinBox" name="grayTime"> + <property name="suffix"> + <string> ms</string> + </property> + <property name="minimum"> + <number>0</number> + </property> + <property name="maximum"> + <number>1000</number> + </property> + <property name="singleStep"> + <number>50</number> + </property> + <property name="value"> + <number>300</number> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="grayTimePromptLabel"> + <property name="text"> + <string>Time on gray:</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SettingsDialog</receiver> + <slot>accept()</slot> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SettingsDialog</receiver> + <slot>reject()</slot> + </connection> + </connections> +</ui> diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_renderer.cc b/media/libjxl/src/tools/comparison_viewer/split_image_renderer.cc new file mode 100644 index 0000000000..acade64d34 --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/split_image_renderer.cc @@ -0,0 +1,239 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/comparison_viewer/split_image_renderer.h" + +#include <algorithm> +#include <cmath> +#include <utility> + +#include <QEvent> +#include <QGuiApplication> +#include <QPainter> +#include <QPalette> +#include <QPen> +#include <QPoint> +#include <QRect> + +namespace jxl { + +SplitImageRenderer::SplitImageRenderer(QWidget* const parent) + : QWidget(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); + setMouseTracking(true); + setFocusPolicy(Qt::WheelFocus); + grabKeyboard(); + + connect(&fadingPoint_, &QVariantAnimation::valueChanged, + [this] { update(); }); +} + +void SplitImageRenderer::setLeftImage(QImage image) { + leftImage_ = QPixmap::fromImage(std::move(image)); + updateMinimumSize(); + update(); +} +void SplitImageRenderer::setRightImage(QImage image) { + rightImage_ = QPixmap::fromImage(std::move(image)); + updateMinimumSize(); + update(); +} +void SplitImageRenderer::setMiddleImage(QImage image) { + middleImage_ = QPixmap::fromImage(std::move(image)); + updateMinimumSize(); + update(); +} + +void SplitImageRenderer::setRenderingSettings( + const SplitImageRenderingSettings& settings) { + renderingSettings_ = settings; +} + +void SplitImageRenderer::setMiddleWidthPercent(const int percent) { + middleWidthPercent_ = percent; + update(); +} + +void SplitImageRenderer::setZoomLevel(double scale) { + scale_ = scale; + updateMinimumSize(); + update(); +} + +void SplitImageRenderer::keyPressEvent(QKeyEvent* const event) { + switch (event->key()) { + case Qt::Key_Left: + setRenderingMode(RenderingMode::LEFT); + break; + + case Qt::Key_Right: + setRenderingMode(RenderingMode::RIGHT); + break; + + case Qt::Key_Up: + case Qt::Key_Down: + setRenderingMode(RenderingMode::MIDDLE); + break; + + case Qt::Key_Escape: + QCoreApplication::quit(); + break; + + case Qt::Key_ZoomIn: + emit zoomLevelIncreaseRequested(); + break; + case Qt::Key_ZoomOut: + emit zoomLevelDecreaseRequested(); + break; + + default: + QWidget::keyPressEvent(event); + break; + } + update(); +} + +void SplitImageRenderer::mouseMoveEvent(QMouseEvent* const event) { + setRenderingMode(RenderingMode::SPLIT); + middleX_ = event->pos().x(); + update(); +} + +void SplitImageRenderer::wheelEvent(QWheelEvent* event) { + if (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) { + if (event->angleDelta().y() > 0) { + emit zoomLevelIncreaseRequested(); + return; + } else if (event->angleDelta().y() < 0) { + emit zoomLevelDecreaseRequested(); + return; + } + } + + event->ignore(); +} + +void SplitImageRenderer::paintEvent(QPaintEvent* const event) { + QRectF drawingArea(0., 0., minimumWidth(), minimumHeight()); + + QPainter painter(this); + painter.fillRect(rect(), QColor(119, 119, 119)); + painter.translate(QRectF(rect()).center() - drawingArea.center()); + painter.scale(scale_, scale_); + if (scale_ < 1.) { + painter.setRenderHint(QPainter::SmoothPixmapTransform); + } + + const auto drawSingleImage = [&](const RenderingMode mode) { + const QPixmap* image = nullptr; + switch (mode) { + case RenderingMode::LEFT: + image = &leftImage_; + break; + case RenderingMode::RIGHT: + image = &rightImage_; + break; + case RenderingMode::MIDDLE: + image = &middleImage_; + break; + + default: + return; + } + painter.drawPixmap(QPointF(0., 0.), *image); + }; + + if (mode_ != RenderingMode::SPLIT) { + if (fadingPoint_.state() != QAbstractAnimation::Running) { + drawSingleImage(mode_); + return; + } + + const float fadingPoint = fadingPoint_.currentValue().toFloat(); + if (renderingSettings_.gray) { + if (fadingPoint < renderingSettings_.fadingMSecs) { + painter.setOpacity((renderingSettings_.fadingMSecs - fadingPoint) / + renderingSettings_.fadingMSecs); + drawSingleImage(previousMode_); + } else if (fadingPoint > renderingSettings_.fadingMSecs + + renderingSettings_.grayMSecs) { + painter.setOpacity((fadingPoint - renderingSettings_.fadingMSecs - + renderingSettings_.grayMSecs) / + renderingSettings_.fadingMSecs); + drawSingleImage(mode_); + } + } else { + drawSingleImage(previousMode_); + painter.setOpacity(fadingPoint / renderingSettings_.fadingMSecs); + drawSingleImage(mode_); + } + + return; + } + + const qreal middleWidth = + std::min<qreal>((minimumWidth() / scale_) * middleWidthPercent_ / 100., + middleImage_.width()); + + const double transformedMiddleX = + painter.transform().inverted().map(QPointF(middleX_, 0.)).x(); + QRectF middleRect = middleImage_.rect(); + middleRect.setWidth(middleWidth); + middleRect.moveCenter(QPointF(transformedMiddleX, middleRect.center().y())); + middleRect.setLeft(std::round(middleRect.left())); + middleRect.setRight(std::round(middleRect.right())); + + QRectF leftRect = leftImage_.rect(); + leftRect.setRight(middleRect.left()); + + QRectF rightRect = rightImage_.rect(); + rightRect.setLeft(middleRect.right()); + + painter.drawPixmap(leftRect, leftImage_, leftRect); + painter.drawPixmap(rightRect, rightImage_, rightRect); + painter.drawPixmap(middleRect, middleImage_, middleRect); + + QPen middlePen; + middlePen.setStyle(Qt::DotLine); + painter.setPen(middlePen); + painter.drawLine(leftRect.topRight(), leftRect.bottomRight()); + painter.drawLine(rightRect.topLeft(), rightRect.bottomLeft()); +} + +void SplitImageRenderer::updateMinimumSize() { + const int imagesWidth = std::max( + std::max(leftImage_.width(), rightImage_.width()), middleImage_.width()); + const int imagesHeight = + std::max(std::max(leftImage_.height(), rightImage_.height()), + middleImage_.height()); + setMinimumSize(scale_ * QSize(imagesWidth, imagesHeight)); +} + +void SplitImageRenderer::setRenderingMode(const RenderingMode newMode) { + if (newMode == mode_) return; + previousMode_ = mode_; + mode_ = newMode; + if (previousMode_ == RenderingMode::SPLIT || mode_ == RenderingMode::SPLIT) { + fadingPoint_.stop(); + } else { + const int msecs = + renderingSettings_.gray + ? 2 * renderingSettings_.fadingMSecs + renderingSettings_.grayMSecs + : renderingSettings_.fadingMSecs; + const float startValue = fadingPoint_.state() == QAbstractAnimation::Running + ? fadingPoint_.endValue().toFloat() - + fadingPoint_.currentValue().toFloat() + : 0.f; + fadingPoint_.stop(); + fadingPoint_.setStartValue(startValue); + fadingPoint_.setEndValue(static_cast<float>(msecs)); + fadingPoint_.setDuration(fadingPoint_.endValue().toFloat() - + fadingPoint_.startValue().toFloat()); + fadingPoint_.start(); + } + emit renderingModeChanged(mode_); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_renderer.h b/media/libjxl/src/tools/comparison_viewer/split_image_renderer.h new file mode 100644 index 0000000000..decb407ff3 --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/split_image_renderer.h @@ -0,0 +1,90 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_RENDERER_H_ +#define TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_RENDERER_H_ + +#include <QImage> +#include <QKeyEvent> +#include <QMouseEvent> +#include <QPaintEvent> +#include <QPixmap> +#include <QVariantAnimation> +#include <QWheelEvent> +#include <QWidget> + +namespace jxl { + +struct SplitImageRenderingSettings { + int fadingMSecs; + bool gray; + int grayMSecs; +}; + +class SplitImageRenderer : public QWidget { + Q_OBJECT + + public: + enum class RenderingMode { + // The default mode when using the mouse: one (partial) image is shown on + // each side of the cursor, with a vertical band of the middle image if + // applicable. + SPLIT, + // Only show the left image (accessed by pressing the left arrow key when + // the renderer has focus). + LEFT, + // Only show the right image (accessed by pressing the right arrow key). + RIGHT, + // Only show the middle image (accessed by pressing the up or down arrow + // key). + MIDDLE, + }; + Q_ENUM(RenderingMode) + + explicit SplitImageRenderer(QWidget* parent = nullptr); + ~SplitImageRenderer() override = default; + + QSize sizeHint() const override { return minimumSize(); } + + void setLeftImage(QImage image); + void setRightImage(QImage image); + void setMiddleImage(QImage image); + + void setRenderingSettings(const SplitImageRenderingSettings& settings); + + public slots: + void setMiddleWidthPercent(int percent); + void setZoomLevel(double scale); + + signals: + void zoomLevelIncreaseRequested(); + void zoomLevelDecreaseRequested(); + + void renderingModeChanged(RenderingMode newMode); + + protected: + void keyPressEvent(QKeyEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + void paintEvent(QPaintEvent* event) override; + + private: + void updateMinimumSize(); + void setRenderingMode(RenderingMode newMode); + + QPixmap leftImage_, rightImage_, middleImage_; + RenderingMode mode_ = RenderingMode::SPLIT; + RenderingMode previousMode_ = RenderingMode::SPLIT; + SplitImageRenderingSettings renderingSettings_; + // Goes from 0 to the animation duration in milliseconds, as a float. + QVariantAnimation fadingPoint_; + int middleX_ = 0; + int middleWidthPercent_ = 10; + double scale_ = 1.; +}; + +} // namespace jxl + +#endif // TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_RENDERER_H_ diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_view.cc b/media/libjxl/src/tools/comparison_viewer/split_image_view.cc new file mode 100644 index 0000000000..76c8edca7c --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/split_image_view.cc @@ -0,0 +1,71 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/comparison_viewer/split_image_view.h" + +#include <utility> + +#include <QLabel> + +#include "tools/comparison_viewer/split_image_renderer.h" + +namespace jxl { + +SplitImageView::SplitImageView(QWidget* const parent) : QWidget(parent) { + ui_.setupUi(this); + + ui_.splitImageRenderer->setRenderingSettings(settings_.renderingSettings()); + + connect(ui_.middleWidthSlider, &QSlider::valueChanged, + [this](const int value) { + ui_.middleWidthDisplayLabel->setText(tr("%L1%").arg(value)); + }); + connect(ui_.middleWidthSlider, &QSlider::valueChanged, ui_.splitImageRenderer, + &SplitImageRenderer::setMiddleWidthPercent); + + connect(ui_.zoomLevelSlider, &QSlider::valueChanged, [this](const int value) { + if (value >= 0) { + ui_.zoomLevelDisplayLabel->setText(tr("×%L1").arg(1 << value)); + ui_.splitImageRenderer->setZoomLevel(1 << value); + } else { + ui_.zoomLevelDisplayLabel->setText(tr("×1/%L1").arg(1 << -value)); + ui_.splitImageRenderer->setZoomLevel(1. / (1 << -value)); + } + }); + + connect(ui_.splitImageRenderer, + &SplitImageRenderer::zoomLevelIncreaseRequested, [this]() { + ui_.zoomLevelSlider->triggerAction( + QAbstractSlider::SliderSingleStepAdd); + }); + connect(ui_.splitImageRenderer, + &SplitImageRenderer::zoomLevelDecreaseRequested, [this]() { + ui_.zoomLevelSlider->triggerAction( + QAbstractSlider::SliderSingleStepSub); + }); + + connect(ui_.splitImageRenderer, &SplitImageRenderer::renderingModeChanged, + this, &SplitImageView::renderingModeChanged); +} + +void SplitImageView::setLeftImage(QImage image) { + ui_.splitImageRenderer->setLeftImage(std::move(image)); +} + +void SplitImageView::setRightImage(QImage image) { + ui_.splitImageRenderer->setRightImage(std::move(image)); +} + +void SplitImageView::setMiddleImage(QImage image) { + ui_.splitImageRenderer->setMiddleImage(std::move(image)); +} + +void SplitImageView::on_settingsButton_clicked() { + if (settings_.exec()) { + ui_.splitImageRenderer->setRenderingSettings(settings_.renderingSettings()); + } +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_view.h b/media/libjxl/src/tools/comparison_viewer/split_image_view.h new file mode 100644 index 0000000000..4978750d1e --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/split_image_view.h @@ -0,0 +1,40 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_VIEW_H_ +#define TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_VIEW_H_ + +#include <QWidget> + +#include "tools/comparison_viewer/settings.h" +#include "tools/comparison_viewer/ui_split_image_view.h" + +namespace jxl { + +class SplitImageView : public QWidget { + Q_OBJECT + + public: + explicit SplitImageView(QWidget* parent = nullptr); + ~SplitImageView() override = default; + + void setLeftImage(QImage image); + void setRightImage(QImage image); + void setMiddleImage(QImage image); + + signals: + void renderingModeChanged(SplitImageRenderer::RenderingMode newMode); + + private slots: + void on_settingsButton_clicked(); + + private: + Ui::SplitImageView ui_; + SettingsDialog settings_; +}; + +} // namespace jxl + +#endif // TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_VIEW_H_ diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_view.ui b/media/libjxl/src/tools/comparison_viewer/split_image_view.ui new file mode 100644 index 0000000000..0755a58d18 --- /dev/null +++ b/media/libjxl/src/tools/comparison_viewer/split_image_view.ui @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <comment> + Copyright (c) the JPEG XL Project Authors. All rights reserved. + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. + </comment> + <class>SplitImageView</class> + <widget class="QWidget" name="SplitImageView"> + <property name="windowTitle"> + <string>Image Comparison Tool</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0"> + <item> + <widget class="QScrollArea" name="scrollArea"> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="jxl::SplitImageRenderer" name="splitImageRenderer"/> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0,0"> + <item> + <layout class="QFormLayout" name="zoomLevelFormLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="zoomLevelPromptLabel"> + <property name="text"> + <string>Zoom level:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QSlider" name="zoomLevelSlider"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimum"> + <number>-3</number> + </property> + <property name="maximum"> + <number>3</number> + </property> + <property name="pageStep"> + <number>2</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="zoomLevelDisplayLabel"> + <property name="text"> + <string>×1</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <layout class="QFormLayout" name="middleWidthFormLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="middleWidthPromptLabel"> + <property name="text"> + <string>Width of the central band:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QSlider" name="middleWidthSlider"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>10</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="middleWidthDisplayLabel"> + <property name="text"> + <string>10%</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="QToolButton" name="settingsButton"> + <property name="text"> + <string>Settings</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>jxl::SplitImageRenderer</class> + <extends>QWidget</extends> + <header>split_image_renderer.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/media/libjxl/src/tools/conformance/CMakeLists.txt b/media/libjxl/src/tools/conformance/CMakeLists.txt new file mode 100644 index 0000000000..9bc7e317f7 --- /dev/null +++ b/media/libjxl/src/tools/conformance/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +add_executable(djxl_conformance djxl_conformance.cc) +target_link_libraries(djxl_conformance jxl_dec) + +if(BUILD_TESTING AND CMAKE_EXECUTABLE_SUFFIX STREQUAL "") +# Script to validate the tooling. +find_program (BASH_PROGRAM bash) +if(BASH_PROGRAM) + add_test( + NAME conformance_tooling_test + COMMAND + ${BASH_PROGRAM} ${CMAKE_CURRENT_SOURCE_DIR}/tooling_test.sh + ${CMAKE_BINARY_DIR} ${JPEGXL_TEST_DATA_PATH}) + # Skip the test if dependencies are not available. + set_tests_properties(conformance_tooling_test PROPERTIES SKIP_RETURN_CODE 254) +endif() +endif() # BUILD_TESTING diff --git a/media/libjxl/src/tools/conformance/conformance.py b/media/libjxl/src/tools/conformance/conformance.py new file mode 100755 index 0000000000..b012716d2e --- /dev/null +++ b/media/libjxl/src/tools/conformance/conformance.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +"""JPEG XL conformance test runner. + +Tool to perform a conformance test for a decoder. +""" + +import argparse +import json +import numpy +import os +import subprocess +import sys +import tempfile + +import lcms2 + + +class ConformanceTestError(Exception): + """General conformance test error.""" + + +def CompareNPY(ref, ref_icc, dec, dec_icc, frame_idx, rmse, peak_error): + """Compare a decoded numpy against the reference one.""" + if ref.shape != dec.shape: + raise ConformanceTestError( + f'Expected shape {ref.shape} but found {dec.shape}') + ref_frame = ref[frame_idx] + dec_frame = dec[frame_idx] + num_channels = ref_frame.shape[2] + + if ref_icc != dec_icc: + # Transform colors before comparison. + if num_channels < 3: + raise ConformanceTestError(f"Only RGB images are supported") + ref_clr = ref_frame[:, :, 0:3] + dec_clr = dec_frame[:, :, 0:3] + dec_frame[:, :, 0:3] = lcms2.convert_pixels(dec_icc, ref_icc, dec_clr) + + error = numpy.abs(ref_frame - dec_frame) + for ch in range(num_channels): + error_ch = error[:, :, ch] + actual_rmse = numpy.sqrt(numpy.mean(error_ch * error_ch)) + if actual_rmse > rmse: + raise ConformanceTestError( + f"RMSE too large: {actual_rmse} > {rmse}") + + actual_peak_error = error.max() + if actual_peak_error > peak_error: + raise ConformanceTestError( + f"Peak error too large: {actual_peak_error} > {peak_error}") + + +def CompareBinaries(ref_bin, dec_bin): + """Compare a decoded binary file against the reference for exact contents.""" + with open(ref_bin, 'rb') as reff: + ref_data = reff.read() + + with open(dec_bin, 'rb') as decf: + dec_data = decf.read() + + if ref_data != dec_data: + raise ConformanceTestError( + f'Binary files mismatch: {ref_bin} {dec_bin}') + + +TEST_KEYS = set( + ['reconstructed_jpeg', 'original_icc', 'rms_error', 'peak_error']) + + +def CheckMeta(dec, ref): + if isinstance(ref, dict): + if not isinstance(dec, dict): + raise ConformanceTestError("Malformed metadata file") + for k, v in ref.items(): + if k in TEST_KEYS: + continue + if k not in dec: + raise ConformanceTestError( + f"Malformed metadata file: key {k} not found") + vv = dec[k] + CheckMeta(vv, v) + elif isinstance(ref, list): + if not isinstance(dec, list) or len(dec) != len(ref): + raise ConformanceTestError("Malformed metadata file") + for vv, v in zip(dec, ref): + CheckMeta(vv, v) + elif isinstance(ref, float): + if not isinstance(dec, float): + raise ConformanceTestError("Malformed metadata file") + if abs(dec - ref) > 0.0001: + raise ConformanceTestError( + f"Metadata: Expected {ref}, found {dec}") + elif dec != ref: + raise ConformanceTestError(f"Metadata: Expected {ref}, found {dec}") + + +def ConformanceTestRunner(args): + # We can pass either the .txt file or the directory which defaults to the + # full corpus. This is useful to run a subset of the corpus in other .txt + # files. + if os.path.isdir(args.corpus): + corpus_dir = args.corpus + corpus_txt = os.path.join(args.corpus, 'corpus.txt') + else: + corpus_dir = os.path.dirname(args.corpus) + corpus_txt = args.corpus + + with open(corpus_txt, 'r') as f: + for test_id in f: + test_id = test_id.rstrip('\n') + print('Testing %s' % test_id) + test_dir = os.path.join(corpus_dir, test_id) + + with open(os.path.join(test_dir, 'test.json'), 'r') as f: + descriptor = json.load(f) + if 'sha256sums' in descriptor: + del descriptor['sha256sums'] + + exact_tests = [] + + with tempfile.TemporaryDirectory(prefix=test_id) as work_dir: + cmd = [args.decoder, os.path.join(test_dir, 'input.jxl')] + # Select the parameters to run. + pixel_prefix = os.path.join(work_dir, 'decoded') + cmd.extend(['-p', pixel_prefix]) + if 'reconstructed_jpeg' in descriptor: + jpeg_filename = os.path.join(work_dir, 'reconstructed.jpg') + cmd.extend(['-j', jpeg_filename]) + exact_tests.append(('reconstructed.jpg', jpeg_filename)) + if 'original_icc' in descriptor: + decoded_original_icc = os.path.join( + work_dir, 'decoded_org.icc') + cmd.extend(['-i', decoded_original_icc]) + exact_tests.append(('original.icc', decoded_original_icc)) + meta_filename = os.path.join(work_dir, 'meta.json') + cmd.extend(['-m', meta_filename]) + + if subprocess.call(cmd) != 0: + raise ConformanceTestError( + 'Running the decoder (%s) returned error' % + ' '.join(cmd)) + + # Run validation of exact files. + for reference_basename, decoded_filename in exact_tests: + reference_filename = os.path.join(test_dir, + reference_basename) + CompareBinaries(reference_filename, decoded_filename) + + # Validate metadata. + with open(meta_filename, 'r') as f: + meta = json.load(f) + + CheckMeta(meta, descriptor) + + # Pixel data. + decoded_icc = pixel_prefix + '.icc' + with open(decoded_icc, 'rb') as f: + decoded_icc = f.read() + reference_icc = os.path.join(test_dir, "reference.icc") + with open(reference_icc, 'rb') as f: + reference_icc = f.read() + + reference_npy = os.path.join(test_dir, 'reference_image.npy') + decoded_npy = os.path.join(work_dir, 'decoded_image.npy') + + if not os.path.exists(decoded_npy): + raise ConformanceTestError( + 'File not decoded: decoded_image.npy') + + reference_npy = numpy.load(reference_npy) + decoded_npy = numpy.load(decoded_npy) + + for i, fd in enumerate(descriptor['frames']): + CompareNPY(reference_npy, reference_icc, decoded_npy, + decoded_icc, i, fd['rms_error'], + fd['peak_error']) + + if 'preview' in descriptor: + reference_npy = os.path.join(test_dir, + 'reference_preview.npy') + decoded_npy = os.path.join(work_dir, 'decoded_preview.npy') + + if not os.path.exists(decoded_npy): + raise ConformanceTestError( + 'File not decoded: decoded_preview.npy') + + reference_npy = numpy.load(reference_npy) + decoded_npy = numpy.load(decoded_npy) + CompareNPY(reference_npy, reference_icc, decoded_npy, + decoded_icc, 0, + descriptor['preview']['rms_error'], + descriptor['preview']['peak_error']) + + return True + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--decoder', + metavar='DECODER', + required=True, + help='path to the decoder binary under test.') + parser.add_argument( + '--corpus', + metavar='CORPUS', + required=True, + help=('path to the corpus directory or corpus descriptor' + ' text file.')) + args = parser.parse_args() + if not ConformanceTestRunner(args): + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/media/libjxl/src/tools/conformance/djxl_conformance.cc b/media/libjxl/src/tools/conformance/djxl_conformance.cc new file mode 100644 index 0000000000..77d0c68722 --- /dev/null +++ b/media/libjxl/src/tools/conformance/djxl_conformance.cc @@ -0,0 +1,669 @@ + +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <algorithm> +#include <fstream> +#include <iostream> +#include <iterator> +#include <memory> +#include <sstream> +#include <vector> + +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/thread_parallel_runner.h" +#include "jxl/thread_parallel_runner_cxx.h" + +namespace { + +struct DecodeOptions { + // Path to the input .jxl file. + const char* input = nullptr; + + // Prefix of the output path where to generate the pixel data or nullptr if + // no pixel data should be save to disk. + const char* pixel_prefix = nullptr; + + // Path to the original ICC profile to be generated, if requested. + const char* icc_path = nullptr; + + // Path to JPEG reconstruction file to be generated, if requested. + const char* jpeg_path = nullptr; + + // Path to the metadata text file to be generated, if requested. + const char* metadata_path = nullptr; +}; + +bool LoadFile(const char* filename, std::vector<uint8_t>* data) { + std::ifstream ifs(filename, std::ios::binary); + std::vector<uint8_t> contents((std::istreambuf_iterator<char>(ifs)), + std::istreambuf_iterator<char>()); + ifs.close(); + *data = std::move(contents); + return ifs.good(); +} + +bool SaveFile(const char* filename, std::vector<uint8_t> data) { + std::ofstream ofs(filename, std::ios::binary); + ofs.write(reinterpret_cast<const char*>(data.data()), data.size()); + ofs.close(); + return ofs.good(); +} + +struct ImageArray { + uint32_t xsize, ysize; + // amount of color channels: 1 for grayscale, 3 for RGB + uint32_t num_color_channels; + // amount of extra channels, including alpha channels, spot colors, ... + uint32_t num_extra_channels; + + // Both frames and ec_frames are filled in by the JXL decoder, and will be + // converted into a numpy array of the form (frame, ysize, xsize, channel) + + // Array of the color channels of the frames. This is an array of frames, + // where each frame is an array of pixels. The pixels in a frame are laid + // out per scanline, then per channel, and finally individual pixels as + // little endian 32-bit floating point. + std::vector<std::vector<uint8_t>> frames; + + // Array of the extra channels of the frames. This is an array of frames, + // where each frame is an array of extra channels. The pixels in an extra + // channel are laid out per scanline, then individual pixels as + // little endian 32-bit floating point. + std::vector<std::vector<std::vector<uint8_t>>> ec_frames; +}; + +// Saves an ImageArray as a numpy 4D ndarray in binary format. +bool SaveNPYArray(const char* filename, const ImageArray& arr) { + size_t image_size = + sizeof(float) * arr.xsize * arr.ysize * arr.num_color_channels; + size_t ec_size = sizeof(float) * arr.xsize * arr.ysize; + for (const auto& frame : arr.frames) { + if (frame.size() != image_size) { + fprintf(stderr, "Invalid frame size\n"); + return false; + } + } + for (const auto& frame : arr.ec_frames) { + if (frame.size() != arr.num_extra_channels) { + fprintf(stderr, "Invalid extra channel count\n"); + return false; + } + for (const auto& ch : frame) { + if (ch.size() != ec_size) { + fprintf(stderr, "Invalid extra channel size\n"); + return false; + } + } + } + + FILE* file = fopen(filename, "wb"); + if (!file) { + fprintf(stderr, "Could not open %s for writing", filename); + return false; + } +#define WRITE_TO_FILE(ptr, len) \ + do { \ + if (fwrite((ptr), (len), 1, file) != 1) { \ + fprintf(stderr, "Error writing " #ptr " to file %s\n", filename); \ + fclose(file); \ + return false; \ + } \ + } while (0) + + const uint8_t header[] = "\x93NUMPY\x01\x00"; + WRITE_TO_FILE(header, 8); + + { + uint32_t num_channels = arr.num_color_channels + arr.num_extra_channels; + std::stringstream ss; + ss << "{'descr': '<f4', 'fortran_order': False, 'shape': (" + << arr.frames.size() << ", " << arr.ysize << ", " << arr.xsize << ", " + << num_channels << "), }\n"; + // 16-bit little endian header length. + uint8_t header_len[2] = {static_cast<uint8_t>(ss.str().size() % 256), + static_cast<uint8_t>(ss.str().size() / 256)}; + WRITE_TO_FILE(header_len, 2); + WRITE_TO_FILE(ss.str().data(), ss.str().size()); + } + + // interleave the samples from color and extra channels + for (size_t f = 0; f < arr.frames.size(); ++f) { + size_t pos = 0; + for (size_t y = 0; y < arr.ysize; ++y) { + for (size_t x = 0; x < arr.xsize; ++x, pos += sizeof(float)) { + WRITE_TO_FILE(arr.frames[f].data() + pos * arr.num_color_channels, + arr.num_color_channels * sizeof(float)); + for (size_t i = 0; i < arr.num_extra_channels; i++) { + WRITE_TO_FILE(arr.ec_frames[f][i].data() + pos, sizeof(float)); + } + } + } + } + + return fclose(file) == 0; +#undef WRITE_TO_FILE +} + +// JSON value writing + +class JSONField { + public: + virtual ~JSONField() = default; + virtual void Write(std::ostream& o, uint32_t indent) const = 0; + + protected: + JSONField() = default; +}; + +class JSONValue : public JSONField { + public: + template <typename T> + explicit JSONValue(const T& value) : value_(std::to_string(value)) {} + + explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {} + + explicit JSONValue(bool value) : value_(value ? "true" : "false") {} + + void Write(std::ostream& o, uint32_t indent) const override { o << value_; } + + private: + std::string value_; +}; + +class JSONDict : public JSONField { + public: + JSONDict() = default; + + template <typename T> + T* AddEmpty(const std::string& key) { + static_assert(std::is_convertible<T*, JSONField*>::value, + "T must be a JSONField"); + T* ret = new T(); + values_.emplace_back( + key, std::unique_ptr<JSONField>(static_cast<JSONField*>(ret))); + return ret; + } + + template <typename T> + void Add(const std::string& key, const T& value) { + values_.emplace_back(key, std::unique_ptr<JSONField>(new JSONValue(value))); + } + + void Write(std::ostream& o, uint32_t indent) const override { + std::string indent_str(indent, ' '); + o << "{"; + bool is_first = true; + for (const auto& key_value : values_) { + if (!is_first) { + o << ","; + } + is_first = false; + o << std::endl << indent_str << " \"" << key_value.first << "\": "; + key_value.second->Write(o, indent + 2); + } + if (!values_.empty()) { + o << std::endl << indent_str; + } + o << "}"; + } + + private: + // Dictionary with order. + std::vector<std::pair<std::string, std::unique_ptr<JSONField>>> values_; +}; + +class JSONArray : public JSONField { + public: + JSONArray() = default; + + template <typename T> + T* AddEmpty() { + static_assert(std::is_convertible<T*, JSONField*>::value, + "T must be a JSONField"); + T* ret = new T(); + values_.emplace_back(ret); + return ret; + } + + template <typename T> + void Add(const T& value) { + values_.emplace_back(new JSONValue(value)); + } + + void Write(std::ostream& o, uint32_t indent) const override { + std::string indent_str(indent, ' '); + o << "["; + bool is_first = true; + for (const auto& value : values_) { + if (!is_first) { + o << ","; + } + is_first = false; + o << std::endl << indent_str << " "; + value->Write(o, indent + 2); + } + if (!values_.empty()) { + o << std::endl << indent_str; + } + o << "]"; + } + + private: + std::vector<std::unique_ptr<JSONField>> values_; +}; + +#define EXPECT_TRUE(X) \ + do { \ + if (!(X)) { \ + fprintf(stderr, "Failed: %s\n", #X); \ + return false; \ + } \ + } while (false) + +// Helper macro for decoder error checking. +#define EXPECT_SUCCESS(X) EXPECT_TRUE((X) == JXL_DEC_SUCCESS) + +// TODO(veluca): merge this back in DecodeJXL once/if the API supports decoding +// to JPEG and to pixels at the same time. +bool DecodeJXLToJpeg(const char* input_path, const char* output_path) { + // JPEG output buffer when reconstructing a JPEG file. + std::vector<uint8_t> jpeg_data; + std::vector<uint8_t> jpeg_data_chunk(16 * 1024); + auto dec = JxlDecoderMake(nullptr); + + uint32_t events = JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE; + EXPECT_SUCCESS(JxlDecoderSubscribeEvents(dec.get(), events)); + + // TODO(deymo): Consider using a multi-threading decoder for conformance + // testing as well. + + // Load and set input all at oncee. + std::vector<uint8_t> jxl_input; + EXPECT_TRUE(LoadFile(input_path, &jxl_input)); + EXPECT_SUCCESS( + JxlDecoderSetInput(dec.get(), jxl_input.data(), jxl_input.size())); + + bool has_jpeg_reconstruction = false; + + while (true) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + if (status == JXL_DEC_ERROR) { + fprintf(stderr, "Error decoding.\n"); + return false; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + fprintf(stderr, "Error decoding: expected more input.\n"); + return false; + } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { + has_jpeg_reconstruction = true; + // Decoding to JPEG. + EXPECT_SUCCESS(JxlDecoderSetJPEGBuffer(dec.get(), jpeg_data_chunk.data(), + jpeg_data_chunk.size())); + } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + // Decoded a chunk to JPEG. + size_t used_jpeg_output = + jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + jpeg_data.insert(jpeg_data.end(), jpeg_data_chunk.data(), + jpeg_data_chunk.data() + used_jpeg_output); + if (used_jpeg_output == 0) { + // Chunk is too small. + jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2); + } + EXPECT_SUCCESS(JxlDecoderSetJPEGBuffer(dec.get(), jpeg_data_chunk.data(), + jpeg_data_chunk.size())); + } else if (status == JXL_DEC_SUCCESS) { + break; + } else if (status == JXL_DEC_FULL_IMAGE) { + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + return true; + } else { + fprintf(stderr, "Error: unexpected status: %d\n", + static_cast<int>(status)); + return false; + } + } + + if (has_jpeg_reconstruction) { + size_t used_jpeg_output = + jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + jpeg_data.insert(jpeg_data.end(), jpeg_data_chunk.data(), + jpeg_data_chunk.data() + used_jpeg_output); + EXPECT_TRUE(SaveFile(output_path, jpeg_data)); + } + return true; +} + +bool DecodeJXL(const DecodeOptions& opts) { + auto dec = JxlDecoderMake(nullptr); + + uint32_t events = + JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_PREVIEW_IMAGE; + if (opts.pixel_prefix) events |= JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE; + // We need to output the frame header info in the metadata. + if (opts.metadata_path) events |= JXL_DEC_FRAME; + + if (opts.jpeg_path) { + EXPECT_TRUE(DecodeJXLToJpeg(opts.input, opts.jpeg_path)); + } + + EXPECT_SUCCESS(JxlDecoderSubscribeEvents(dec.get(), events)); + EXPECT_SUCCESS(JxlDecoderSetRenderSpotcolors(dec.get(), JXL_FALSE)); + + // TODO(deymo): Consider using a multi-threading decoder for conformance + // testing as well. + + // Load and set input all at oncee. + std::vector<uint8_t> jxl_input; + EXPECT_TRUE(LoadFile(opts.input, &jxl_input)); + EXPECT_SUCCESS( + JxlDecoderSetInput(dec.get(), jxl_input.data(), jxl_input.size())); + + JxlBasicInfo info{}; + + // Pixel data when decoding a frame or a preview frame. + std::vector<uint8_t> pixels; + std::vector<uint8_t> preview_pixels; + + std::vector<JxlExtraChannelInfo> extra_channels; + std::vector<std::vector<uint8_t>> extra_channel_pixels; + + std::vector<JxlFrameHeader> frame_headers; + std::vector<std::string> frame_names; + + JxlPixelFormat format; + + ImageArray image, preview; + + while (true) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + if (status == JXL_DEC_ERROR) { + fprintf(stderr, "Error decoding.\n"); + return false; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + fprintf(stderr, "Error decoding: expected more input.\n"); + return false; + } else if (status == JXL_DEC_BASIC_INFO) { + // Basic info. + EXPECT_SUCCESS(JxlDecoderGetBasicInfo(dec.get(), &info)); + extra_channels.resize(info.num_extra_channels); + for (uint32_t i = 0; i < info.num_extra_channels; ++i) { + EXPECT_SUCCESS( + JxlDecoderGetExtraChannelInfo(dec.get(), i, &extra_channels[i])); + std::vector<char> name(extra_channels[i].name_length + 1); + EXPECT_SUCCESS(JxlDecoderGetExtraChannelName(dec.get(), i, name.data(), + name.size())); + } + + // Select the output pixel format based on the basic info. + format = JxlPixelFormat{info.num_color_channels, JXL_TYPE_FLOAT, + JXL_LITTLE_ENDIAN, 0}; + image.num_color_channels = info.num_color_channels; + image.num_extra_channels = info.num_extra_channels; + image.xsize = info.xsize; + image.ysize = info.ysize; + + if (info.have_preview) { + preview.num_color_channels = info.num_color_channels; + preview.num_extra_channels = info.num_extra_channels; + preview.xsize = info.preview.xsize; + preview.ysize = info.preview.ysize; + } + } else if (status == JXL_DEC_COLOR_ENCODING) { + // ICC profiles. + if (opts.icc_path) { + // Store the original ICC if requested. + size_t icc_size; + EXPECT_SUCCESS(JxlDecoderGetICCProfileSize( + dec.get(), nullptr, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &icc_size)); + std::vector<uint8_t> icc_original(icc_size); + EXPECT_SUCCESS(JxlDecoderGetColorAsICCProfile( + dec.get(), nullptr, JXL_COLOR_PROFILE_TARGET_ORIGINAL, + icc_original.data(), icc_original.size())); + EXPECT_TRUE(SaveFile(opts.icc_path, icc_original)); + } + + if (opts.pixel_prefix) { + // Get the ICC color profile of the pixel data and store it. + size_t icc_size; + EXPECT_SUCCESS(JxlDecoderGetICCProfileSize( + dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)); + std::vector<uint8_t> icc_data(icc_size); + EXPECT_SUCCESS(JxlDecoderGetColorAsICCProfile( + dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, icc_data.data(), + icc_data.size())); + std::string icc_data_filename = std::string(opts.pixel_prefix) + ".icc"; + EXPECT_TRUE(SaveFile(icc_data_filename.c_str(), icc_data)); + } + } else if (status == JXL_DEC_FRAME) { + // Capture the frame header information. + JxlFrameHeader frame_header; + EXPECT_SUCCESS(JxlDecoderGetFrameHeader(dec.get(), &frame_header)); + std::vector<char> frame_name(frame_header.name_length + 1); + EXPECT_SUCCESS(JxlDecoderGetFrameName(dec.get(), frame_name.data(), + frame_name.size())); + EXPECT_TRUE(frame_name[frame_name.size() - 1] == '\0'); + frame_headers.emplace_back(frame_header); + frame_names.emplace_back(frame_name.begin(), frame_name.end() - 1); + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + // Set pixel output buffer. + size_t buffer_size; + EXPECT_SUCCESS( + JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)); + pixels.resize(buffer_size); + memset(pixels.data(), 0, pixels.size()); + EXPECT_SUCCESS(JxlDecoderSetImageOutBuffer(dec.get(), &format, + pixels.data(), pixels.size())); + extra_channel_pixels.resize(info.num_extra_channels); + for (uint32_t i = 0; i < info.num_extra_channels; ++i) { + EXPECT_SUCCESS(JxlDecoderExtraChannelBufferSize(dec.get(), &format, + &buffer_size, i)); + extra_channel_pixels[i].resize(buffer_size); + memset(extra_channel_pixels[i].data(), 0, + extra_channel_pixels[i].size()); + EXPECT_SUCCESS(JxlDecoderSetExtraChannelBuffer( + dec.get(), &format, extra_channel_pixels[i].data(), + extra_channel_pixels[i].size(), i)); + } + } else if (status == JXL_DEC_NEED_PREVIEW_OUT_BUFFER) { + // Set preview pixel output buffer. + size_t buffer_size; + EXPECT_SUCCESS( + JxlDecoderPreviewOutBufferSize(dec.get(), &format, &buffer_size)); + preview_pixels.resize(buffer_size); + memset(preview_pixels.data(), 0, preview_pixels.size()); + EXPECT_SUCCESS(JxlDecoderSetPreviewOutBuffer( + dec.get(), &format, preview_pixels.data(), preview_pixels.size())); + } else if (status == JXL_DEC_FULL_IMAGE) { + // Pixel output buffer is set. + if (opts.pixel_prefix) { + image.frames.emplace_back(); + swap(image.frames.back(), pixels); + image.ec_frames.emplace_back(); + for (uint32_t i = 0; i < info.num_extra_channels; ++i) { + image.ec_frames.back().emplace_back(); + swap(image.ec_frames.back().back(), extra_channel_pixels[i]); + } + } + + // TODO(deymo): Get the extra channel pixel data an store it. + } else if (status == JXL_DEC_PREVIEW_IMAGE) { + // Preview pixel output buffer is set. + if (opts.pixel_prefix && info.have_preview) { + preview.frames.emplace_back(); + swap(preview.frames.back(), preview_pixels); + } + } else if (status == JXL_DEC_SUCCESS) { + break; + } else { + fprintf(stderr, "Error: unexpected status: %d\n", + static_cast<int>(status)); + return false; + } + } + + if (opts.pixel_prefix) { + std::string name = std::string(opts.pixel_prefix) + "_image.npy"; + EXPECT_TRUE(SaveNPYArray(name.c_str(), image)); + } + + if (opts.pixel_prefix && info.have_preview) { + std::string name = std::string(opts.pixel_prefix) + "_preview.npy"; + EXPECT_TRUE(SaveNPYArray(name.c_str(), preview)); + } + + if (opts.metadata_path) { + JSONDict meta; + // Same order as in 18181-3 CD. + + // Frames. + auto* meta_frames = meta.AddEmpty<JSONArray>("frames"); + for (size_t i = 0; i < frame_headers.size(); i++) { + auto* frame_i = meta_frames->AddEmpty<JSONDict>(); + if (info.have_animation) { + frame_i->Add("duration", JSONValue(frame_headers[i].duration * 1.0f * + info.animation.tps_denominator / + info.animation.tps_numerator)); + } + + frame_i->Add("name", JSONValue(frame_names[i])); + + if (info.animation.have_timecodes) { + frame_i->Add("timecode", JSONValue(frame_headers[i].timecode)); + } + } + +#define METADATA(FIELD) meta.Add(#FIELD, info.FIELD) + + METADATA(intensity_target); + METADATA(min_nits); + METADATA(relative_to_max_display); + METADATA(linear_below); + + if (info.have_preview) { + meta.AddEmpty<JSONDict>("preview"); + // TODO(veluca): can we have duration/name/timecode here? + } + + { + auto ectype = meta.AddEmpty<JSONArray>("extra_channel_type"); + auto bps = meta.AddEmpty<JSONArray>("bits_per_sample"); + auto ebps = meta.AddEmpty<JSONArray>("exp_bits_per_sample"); + bps->Add(info.bits_per_sample); + ebps->Add(info.exponent_bits_per_sample); + for (size_t i = 0; i < extra_channels.size(); i++) { + switch (extra_channels[i].type) { + case JXL_CHANNEL_ALPHA: { + ectype->Add(std::string("Alpha")); + break; + } + case JXL_CHANNEL_DEPTH: { + ectype->Add(std::string("Depth")); + break; + } + case JXL_CHANNEL_SPOT_COLOR: { + ectype->Add(std::string("SpotColor")); + break; + } + case JXL_CHANNEL_SELECTION_MASK: { + ectype->Add(std::string("SelectionMask")); + break; + } + case JXL_CHANNEL_BLACK: { + ectype->Add(std::string("Black")); + break; + } + case JXL_CHANNEL_CFA: { + ectype->Add(std::string("CFA")); + break; + } + case JXL_CHANNEL_THERMAL: { + ectype->Add(std::string("Thermal")); + break; + } + default: { + ectype->Add(std::string("UNKNOWN")); + break; + } + } + bps->Add(extra_channels[i].bits_per_sample); + ebps->Add(extra_channels[i].exponent_bits_per_sample); + } + } + + std::ofstream ofs(opts.metadata_path); + meta.Write(ofs, 0); + ofs << std::endl; + ofs.close(); + EXPECT_TRUE(ofs.good()); + } + return true; +} + +int Usage(const char* program) { + fprintf( + stderr, + "Usage: %s INPUT_JXL [-i ORG_ICC] [-p PREFIX] [-m METADATA]\n" + "\n" + " INPUT_JXL: Path to the input .jxl file.\n" + " -i ORG_ICC: Path to the output \"original\" ICC profile.\n" + " -p PREFIX: Prefix path to generate the pixel numpy data image (with\n" + " suffix \".npy\") and ICC profile (with suffix \".icc\"). The \n" + " image data will be a 4D numpy array with dimensions (number of \n" + " frames, height, width, number of channels).\n" + " -j JPEG: Path to the output reconstructed JPEG file.\n" + " -m METADATA: Path to the output JSON text metadata file.\n", + program); + return 1; +} + +} // namespace + +// Helper macro to check that an extra argument was passed to ARG. +#define EXPECT_ARG(ARG) \ + if (optind >= argc) { \ + fprintf(stderr, "%s needs an argument value.\n", ARG); \ + return Usage(argv[0]); \ + } + +int main(int argc, char* argv[]) { + DecodeOptions opts; + + for (int optind = 1; optind < argc;) { + if (!strcmp(argv[optind], "-i")) { + optind++; + EXPECT_ARG("-i"); + opts.icc_path = argv[optind++]; + } else if (!strcmp(argv[optind], "-p")) { + optind++; + EXPECT_ARG("-p"); + opts.pixel_prefix = argv[optind++]; + } else if (!strcmp(argv[optind], "-j")) { + optind++; + EXPECT_ARG("-j"); + opts.jpeg_path = argv[optind++]; + } else if (!strcmp(argv[optind], "-m")) { + optind++; + EXPECT_ARG("-m"); + opts.metadata_path = argv[optind++]; + } else if (opts.input == nullptr) { + opts.input = argv[optind++]; + } else { + fprintf(stderr, "Unknown parameter: \"%s\".\n", argv[optind]); + return Usage(argv[0]); + } + } + if (!opts.input) { + fprintf(stderr, "JXL decoder for conformance testing.\n"); + return Usage(argv[0]); + } + + return DecodeJXL(opts) ? 0 : 1; +} diff --git a/media/libjxl/src/tools/conformance/generator.py b/media/libjxl/src/tools/conformance/generator.py new file mode 100755 index 0000000000..e230d48b3c --- /dev/null +++ b/media/libjxl/src/tools/conformance/generator.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +"""Tool for generating a conformance testing corpus from a set of .jxl files. + +This is not the JPEG XL conformance test runner. This is a tool to generate a +conformance testing corpus from a set of .jxl files. +""" + +import argparse +import itertools +import json +import os +import shutil +import subprocess +import sys + + +def GenerateConformanceCorpus(args): + """Generate the conformance test corpus for the given arguments.""" + files = [] + for jxl in args.inputs: + if os.path.isdir(jxl): + # Add all the .jxl files recursively. + for root, _, dir_files in os.walk(jxl): + files.extend( + os.path.join(root, filename) for filename in dir_files + if filename.lower().endswith('.jxl')) + else: + files.append(jxl) + + os.makedirs(args.output, 0o755, exist_ok=True) + + test_ids = [] + for jxl in files: + # Generate a unique test_id for this file based on the filename. + test_id = os.path.basename(jxl).lower() + if test_id.endswith('.jxl'): + test_id = test_id[:-4] + if test_id in test_ids: + for i in itertools.count(2): + candidate = test_id + '%02d' % i + if candidate not in test_ids: + test_id = candidate + break + test_ids.append(test_id) + + test_dir = os.path.join(args.output, test_id) + os.makedirs(test_dir, 0o755, exist_ok=True) + print('Generating %s' % (test_id, )) + input_file = os.path.join(test_dir, 'input.jxl') + shutil.copy(jxl, input_file) + + # The test descriptor file. + descriptor = {} + descriptor['jxl'] = 'input.jxl' + + cmd = [args.decoder, input_file] + original_icc_filename = os.path.join(test_dir, 'original.icc') + reconstructed_filename = os.path.join(test_dir, 'reconstructed.jpg') + pixel_prefix = os.path.join(test_dir, 'reference') + cmd.extend(['-p', pixel_prefix]) + cmd.extend(['-i', original_icc_filename]) + cmd.extend(['-j', reconstructed_filename]) + metadata_filename = os.path.join(test_dir, 'test.json') + cmd.extend(['-m', metadata_filename]) + + # Decode and generate the reference files. + subprocess.check_call(cmd) + + with open(metadata_filename, 'r') as f: + metadata = json.load(f) + + if os.path.exists(original_icc_filename): + metadata['original_icc'] = "original.icc" + + if os.path.exists(reconstructed_filename): + metadata['reconstructed_jpeg'] = "reconstructed.jpg" + + for frame in metadata['frames']: + frame['rms_error'] = args.rmse + frame['peak_error'] = args.peak_error + + if 'preview' in metadata: + metadata['preview']['rms_error'] = args.rmse + metadata['preview']['peak_error'] = args.peak_error + + # Create the test descriptor file. + with open(metadata_filename, 'w') as f: + json.dump(metadata, f, indent=2) + + # Generate a corpus descriptor with the list of the all the test_id names, + # one per line. + with open(os.path.join(args.output, 'corpus.txt'), 'w') as f: + f.write(''.join(line + '\n' for line in test_ids)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--decoder', + metavar='DECODER', + required=True, + help='path to the decoder binary under test.') + parser.add_argument('--output', + metavar='DIR', + required=True, + help='path to the output directory') + parser.add_argument('--peak_error', + metavar='PEAK_ERROR', + type=float, + required=True, + help='peak error for each testcase') + parser.add_argument('--rmse', + metavar='RMSE', + type=float, + required=True, + help='max RMSE for each testcase') + parser.add_argument('inputs', + metavar='JXL', + nargs='+', + help='path to input .jxl file(s)') + args = parser.parse_args() + GenerateConformanceCorpus(args) + + +if __name__ == '__main__': + main() diff --git a/media/libjxl/src/tools/conformance/lcms2.py b/media/libjxl/src/tools/conformance/lcms2.py new file mode 100644 index 0000000000..f8313cd6b4 --- /dev/null +++ b/media/libjxl/src/tools/conformance/lcms2.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +import ctypes +from numpy.ctypeslib import ndpointer +import numpy +import os + +lcms2_lib_path = os.getenv("LCMS2_LIB_PATH", "liblcms2.so.2") +lcms2_lib = ctypes.cdll.LoadLibrary(lcms2_lib_path) + +native_open_profile = lcms2_lib.cmsOpenProfileFromMem +native_open_profile.restype = ctypes.c_void_p +native_open_profile.argtypes = [ + ctypes.c_char_p, # MemPtr + ctypes.c_size_t # dwSize +] + +native_close_profile = lcms2_lib.cmsCloseProfile +native_close_profile.restype = ctypes.c_int +native_close_profile.argtypes = [ + ctypes.c_void_p # hProfile +] + +native_create_transform = lcms2_lib.cmsCreateTransform +native_create_transform.restype = ctypes.c_void_p +native_create_transform.argtypes = [ + ctypes.c_void_p, # Input + ctypes.c_uint32, # InputFormat + ctypes.c_void_p, # Output + ctypes.c_uint32, # OutputFormat + ctypes.c_uint32, # Intent + ctypes.c_uint32 # dwFlags +] + +native_delete_transform = lcms2_lib.cmsDeleteTransform +native_delete_transform.restype = None +native_delete_transform.argtypes = [ + ctypes.c_void_p # hTransform +] + +native_do_transform = lcms2_lib.cmsDoTransform +native_do_transform.restype = None +native_do_transform.argtypes = [ + ctypes.c_void_p, # Transform + ndpointer(ctypes.c_double, flags="C_CONTIGUOUS"), # InputBuffer + ndpointer(ctypes.c_double, flags="C_CONTIGUOUS"), # OutputBuffer + ctypes.c_uint32 # Size +] + + +def make_format( + bytes_per_sample=4, # float32 + num_channels=3, # RGB or XYZ + extra_channels=0, + swap_channels=0, + swap_endiannes=0, + planar=0, + flavor=0, + swap_first=0, + unused=0, + pixel_type=4, # RGB + optimized=0, + floating_point=1): + values = [bytes_per_sample, num_channels, extra_channels, swap_channels, + swap_endiannes, planar, flavor, swap_first, unused, pixel_type, + optimized, floating_point] + bit_width = [3, 4, 3, 1, 1, 1, 1, 1, 1, 5, 1, 1] + result = 0 + shift = 0 + for i in range(len(bit_width)): + result += values[i] << shift + shift += bit_width[i] + return result + + +def convert_pixels(from_icc, to_icc, from_pixels): + from_icc = bytearray(from_icc) + to_icc = bytearray(to_icc) + + if len(from_pixels.shape) != 3 or from_pixels.shape[2] != 3: + raise ValueError("Only WxHx3 shapes are supported") + from_pixels_plain = from_pixels.ravel().astype(numpy.float64) + num_pixels = len(from_pixels_plain) // 3 + to_pixels_plain = numpy.empty(num_pixels * 3, dtype=numpy.float64) + + from_icc = (ctypes.c_char * len(from_icc)).from_buffer(from_icc) + from_profile = native_open_profile( + ctypes.cast(ctypes.pointer(from_icc), ctypes.c_char_p), len(from_icc)) + + to_icc = (ctypes.c_char * len(to_icc)).from_buffer(to_icc) + to_profile = native_open_profile( + ctypes.cast(ctypes.pointer(to_icc), ctypes.c_char_p), len(to_icc)) + + # bytes_per_sample=0 actually means 8 bytes (but there are just 3 bits to + # encode the length of sample) + format_rgb_f64 = make_format(bytes_per_sample=0) + intent = 0 # INTENT_PERCEPTUAL + flags = 0 # default; no "no-optimization" + transform = native_create_transform( + from_profile, format_rgb_f64, to_profile, format_rgb_f64, intent, flags) + + native_do_transform( + transform, from_pixels_plain, to_pixels_plain, num_pixels) + + native_delete_transform(transform) + native_close_profile(to_profile) + native_close_profile(from_profile) + + # Return same shape and size as input + return to_pixels_plain.reshape(from_pixels.shape).astype(from_pixels.dtype) + +if __name__ == '__main__': + raise Exception("Not an executable") diff --git a/media/libjxl/src/tools/conformance/tooling_test.sh b/media/libjxl/src/tools/conformance/tooling_test.sh new file mode 100755 index 0000000000..4366599838 --- /dev/null +++ b/media/libjxl/src/tools/conformance/tooling_test.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# Conformance test tooling test. This is not the JPEG XL conformance test +# runner. This test that the tooling to generate the conformance test and the +# conformance test runner work together. + +MYDIR=$(dirname $(realpath "$0")) + +if [[ $# -eq 2 ]]; then + JPEGXL_TEST_DATA_PATH="$2" +else + JPEGXL_TEST_DATA_PATH="${MYDIR}/../../third_party/testdata" +fi + +set -eux + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -rf "${CLEANUP_FILES[@]}" + fi +} +trap 'retcode=$?; { set +x; } 2>/dev/null; cleanup' INT TERM EXIT + +main() { + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + + if ! python3 -c 'import numpy'; then + echo "Missing numpy, skipping test." >&2 + exit 254 # Signals ctest that we should mark this test as skipped. + fi + + local build_dir="${1:-}" + if [[ -z "${build_dir}" ]]; then + build_dir=$(realpath "${MYDIR}/../../build") + fi + + local decoder="${build_dir}/tools/conformance/djxl_conformance" + "${MYDIR}/generator.py" \ + --decoder="${decoder}" \ + --output="${tmpdir}" \ + --peak_error=0.001 \ + --rmse=0.001 \ + "${JPEGXL_TEST_DATA_PATH}/jxl/blending/cropped_traffic_light.jxl" + + # List the contents of the corpus dir. + tree "${tmpdir}" || true + + "${MYDIR}/conformance.py" \ + --decoder="${decoder}" \ + --corpus="${tmpdir}" +} + +main "$@" diff --git a/media/libjxl/src/tools/decode_and_encode.cc b/media/libjxl/src/tools/decode_and_encode.cc new file mode 100644 index 0000000000..59b1d6d3af --- /dev/null +++ b/media/libjxl/src/tools/decode_and_encode.cc @@ -0,0 +1,50 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> + +#include <string> + +#include "lib/extras/codec.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { +namespace { + +// Reads an input file (typically PNM) with color_space hint and writes to an +// output file (typically PNG) which supports all required metadata. +int Convert(int argc, char** argv) { + if (argc != 4 && argc != 5) { + fprintf(stderr, "Args: in colorspace_description out [bits]\n"); + return 1; + } + const std::string& pathname_in = argv[1]; + const std::string& desc = argv[2]; + const std::string& pathname_out = argv[3]; + + CodecInOut io; + extras::ColorHints color_hints; + ThreadPoolInternal pool(4); + color_hints.Add("color_space", desc); + if (!SetFromFile(pathname_in, color_hints, &io, &pool)) { + fprintf(stderr, "Failed to read %s\n", pathname_in.c_str()); + return 1; + } + + if (!EncodeToFile(io, pathname_out, &pool)) { + fprintf(stderr, "Failed to write %s\n", pathname_out.c_str()); + return 1; + } + + return 0; +} + +} // namespace +} // namespace jxl + +int main(int argc, char** argv) { return jxl::Convert(argc, argv); } diff --git a/media/libjxl/src/tools/decode_basic_info_fuzzer.cc b/media/libjxl/src/tools/decode_basic_info_fuzzer.cc new file mode 100644 index 0000000000..59f7089f65 --- /dev/null +++ b/media/libjxl/src/tools/decode_basic_info_fuzzer.cc @@ -0,0 +1,58 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdint.h> + +#include "jxl/decode.h" + +namespace jxl { + +int TestOneInput(const uint8_t* data, size_t size) { + JxlDecoderStatus status; + JxlDecoder* dec = JxlDecoderCreate(nullptr); + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING); + JxlDecoderSetInput(dec, data, size); + + status = JxlDecoderProcessInput(dec); + + if (status != JXL_DEC_BASIC_INFO) { + JxlDecoderDestroy(dec); + return 0; + } + + JxlBasicInfo info; + bool have_basic_info = !JxlDecoderGetBasicInfo(dec, &info); + + if (have_basic_info) { + if (info.alpha_bits != 0) { + for (int i = 0; i < info.num_extra_channels; ++i) { + JxlExtraChannelInfo extra; + JxlDecoderGetExtraChannelInfo(dec, 0, &extra); + } + } + } + status = JxlDecoderProcessInput(dec); + + if (status != JXL_DEC_COLOR_ENCODING) { + JxlDecoderDestroy(dec); + return 0; + } + + JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + JxlDecoderGetColorAsEncodedProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, nullptr); + size_t dec_profile_size; + JxlDecoderGetICCProfileSize(dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, + &dec_profile_size); + + JxlDecoderDestroy(dec); + return 0; +} + +} // namespace jxl + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return jxl::TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/demo_progressive_saliency_encoding.py b/media/libjxl/src/tools/demo_progressive_saliency_encoding.py new file mode 100755 index 0000000000..6eb5cadd54 --- /dev/null +++ b/media/libjxl/src/tools/demo_progressive_saliency_encoding.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 + +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Produces demos for how progressive-saliency encoding would look like. + +As long as we do not have a progressive decoder that allows showing images +generated from partially-available data, we can resort to building +animated gifs that show how progressive loading would look like. + +Method: + +1. JPEG-XL encode the image, but stop at the pre-final (2nd) step. +2. Use separate tool to compute a heatmap which shows where differences between + the pre-final and final image are expected to be perceptually worst. +3. Use this heatmap to JPEG-XL encode the image with the final step split into + 'salient parts only' and 'non-salient parts'. Generate a sequence of images + that stop decoding after the 1st, 2nd, 3rd, 4th step. JPEG-XL decode these + truncated images back to PNG. +4. Measure byte sizes of the truncated-encoded images. +5. Build an animated GIF with variable delays by calling ImageMagick's + `convert` command. + +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from six.moves import zip +import ast # For ast.literal_eval() only. +import os +import re +import shlex +import subprocess +import sys + +_BLOCKSIZE = 8 + +_CONF_PARSERS = dict( + keep_tempfiles=lambda s: bool(ast.literal_eval(s)), + heatmap_command=shlex.split, + simulated_progressive_loading_time_sec=float, + simulated_progressive_loading_delay_until_looparound_sec=float, + jpegxl_encoder=shlex.split, + jpegxl_decoder=shlex.split, + blurring=lambda s: s.split(), +) + + +def parse_config(config_filename): + """Parses the configuration file.""" + conf = {} + re_comment = re.compile(r'^\s*(?:#.*)?$') + re_param = re.compile(r'^(?P<option>\w+)\s*:\s*(?P<value>.*?)\s*$') + try: + with open(config_filename) as h: + for line in h: + if re_comment.match(line): + continue + m = re_param.match(line) + if not m: + raise ValueError('Syntax error') + conf[m.group('option')] = ( + _CONF_PARSERS[m.group('option')](m.group('value'))) + except Exception as exn: + raise ValueError('Bad Configuration line ({}): {}'.format(exn, line)) + missing_options = set(_CONF_PARSERS) - set(conf) + if missing_options: + raise ValueError('Missing configuration options: ' + ', '.join( + sorted(missing_options))) + return conf + + +def generate_demo_image(config, input_filename, output_filename): + tempfiles = [] + # + def encode_img(input_filename, output_filename, num_steps, + heatmap_filename=None): + replacements = { + '${INPUT}': input_filename, + '${OUTPUT}': output_filename, + '${STEPS}': str(num_steps), + # Heatmap argument will be provided in --param=value form. + '${HEATMAP_ARG}': ('--saliency_map_filename=' + heatmap_filename + if heatmap_filename is not None else '') + } + # Remove empty args. This removes the heatmap-argument if no heatmap + # is provided.. + cmd = [ + _f for _f in + [replacements.get(arg, arg) for arg in config['jpegxl_encoder']] if _f + ] + tempfiles.append(output_filename) + subprocess.call(cmd) + # + def decode_img(input_filename, output_filename): + replacements = {'${INPUT}': input_filename, '${OUTPUT}': output_filename} + cmd = [replacements.get(arg, arg) for arg in config['jpegxl_decoder']] + tempfiles.append(output_filename) + subprocess.call(cmd) + # + def generate_heatmap(orig_image_filename, coarse_grained_filename, + heatmap_filename): + cmd = config['heatmap_command'] + [ + str(_BLOCKSIZE), orig_image_filename, coarse_grained_filename, + heatmap_filename] + tempfiles.append(heatmap_filename) + subprocess.call(cmd) + # + try: + encode_img(input_filename, output_filename + '._step1.pik', 1) + decode_img(output_filename + '._step1.pik', output_filename + '._step1.png') + encode_img(input_filename, output_filename + '._step2.pik', 2) + decode_img(output_filename + '._step2.pik', output_filename + '._step2.png') + generate_heatmap(input_filename, output_filename + '._step2.png', + output_filename + '._heatmap.png') + encode_img(input_filename, + output_filename + '._step3.pik', 3, + output_filename + '._heatmap.png') + encode_img(input_filename, + output_filename + '._step4.pik', 4, + output_filename + '._heatmap.png') + decode_img(output_filename + '._step3.pik', output_filename + '._step3.png') + decode_img(output_filename + '._step4.pik', output_filename + '._step4.png') + data_sizes = [ + os.stat('{}._step{}.pik'.format(output_filename, num_step)).st_size + for num_step in (1, 2, 3, 4)] + time_offsets = [0] + [ + # Imagemagick's `convert` accepts delays in units of 1/100 sec. + round(100 * config['simulated_progressive_loading_time_sec'] * size / + data_sizes[-1]) for size in data_sizes] + time_delays = [t_next - t_prev + for t_next, t_prev in zip(time_offsets[1:], time_offsets)] + # Add a fake white initial image. As long as no usable image data is + # available, the user will see a white background. + subprocess.call(['convert', + output_filename + '._step1.png', + '-fill', 'white', '-colorize', '100%', + output_filename + '._step0.png']) + tempfiles.append(output_filename + '._step0.png') + subprocess.call( + ['convert', '-loop', '0', output_filename + '._step0.png'] + + [arg for args in [ + ['-delay', str(time_delays[n - 1]), + '-blur', config['blurring'][n - 1], + '{}._step{}.png'.format(output_filename, n)] + for n in (1, 2, 3, 4)] for arg in args] + + ['-delay', str(round(100 * config[ + 'simulated_progressive_loading_delay_until_looparound_sec'])), + output_filename + '._step4.png', + output_filename]) + finally: + if not config['keep_tempfiles']: + for filename in tempfiles: + try: + os.unlink(filename) + except OSError: + pass # May already have been deleted otherwise. + + +def main(): + if sys.version.startswith('2.'): + sys.exit('This is a python3-only script.') + if (len(sys.argv) != 4 or not sys.argv[-1].endswith('.gif') + or not sys.argv[-2].endswith('.png')): + sys.exit( + 'Usage: {} [config_options_file] [input.png] [output.gif]'.format( + sys.argv[0])) + try: + _, config_filename, input_filename, output_filename = sys.argv + config = parse_config(config_filename) + generate_demo_image(config, input_filename, output_filename) + except ValueError as exn: + sys.exit(exn) + + + +if __name__ == '__main__': + main() diff --git a/media/libjxl/src/tools/demo_vardct_select.sh b/media/libjxl/src/tools/demo_vardct_select.sh new file mode 100755 index 0000000000..414eacbbd2 --- /dev/null +++ b/media/libjxl/src/tools/demo_vardct_select.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# Produces a demo video showing VarDCT block type selection +# from very high quality to very low quality. + +# Assumes ImageMagick convert, ffmpeg, bc are available. + +set -eu + +MYDIR=$(dirname $(realpath "$0")) + +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + + + +main() { + local infile="${1:-}" + if [[ -z "${infile}" ]]; then + cat >&2 <<EOF +Use: $0 IMAGE [OUT.apng] + +Where IMAGE is an input image and OUT.apng is the output +EOF + exit 1 + fi + + shift + + local outfile="$@" + if [[ -z "${outfile}" ]]; then + # default output filename + outfile=vardct-select-demo.apng + fi + + if ! command -v benchmark_xl &>/dev/null 2>&1; then + PATH=$PATH:$MYDIR/../build/tools + if ! command -v benchmark_xl &>/dev/null 2>&1; then + echo "Could not find benchmark_xl, try building first" + exit + fi + fi + local b=benchmark_xl + + if ! command -v ffmpeg &>/dev/null 2>&1; then + echo "Could not find ffmpeg" + exit + fi + + if ! command -v convert &>/dev/null 2>&1; then + echo "Could not find ImageMagick (convert)" + exit + fi + + local tmp=$(mktemp -d --suffix=vardctdemo) + CLEANUP_FILES+=("${tmp}") + + cp $infile $tmp/orig + + local n=0 + local pixels="$(identify -format "(%w * %h)" $tmp/orig)" + for i in $(seq 0.2 0.2 2) $(seq 2.5 0.5 5) $(seq 6 1 10) $(seq 12 2 40); do + $b --input=$tmp/orig --codec=jxl:d$i --save_decompressed --save_compressed \ + --debug_image_dir=$tmp --output_dir=$tmp + convert $tmp/orig \( $tmp/orig.jxl:d$i.dbg/ac_strategy.png \ + -alpha set -channel A -evaluate set 66% \) \ + -composite $tmp/t.ppm + bytes=$(stat -c "%s" $tmp/orig.jxl_d$i) + bpp=$( echo "$bytes * 8 / $pixels " | bc -l | cut -b 1-6 ) + label="cjxl -d $i ($((bytes / 1000)) kb, bpp: $bpp)" + convert +append $tmp/t.ppm $tmp/orig.jxl_d$i.png $tmp/t2.ppm + convert $tmp/t2.ppm \ + -gravity north \ + -pointsize 32 \ + -stroke '#000C' -strokewidth 5 -annotate +0+12 "$label" \ + -stroke none -fill white -annotate +0+12 "$label" $tmp/frame-$n.png + + n=$((n+1)) + done + + ffmpeg -framerate 1 -i $tmp/frame-%d.png $outfile +} + +main "$@" diff --git a/media/libjxl/src/tools/djxl.cc b/media/libjxl/src/tools/djxl.cc new file mode 100644 index 0000000000..8487fcdf09 --- /dev/null +++ b/media/libjxl/src/tools/djxl.cc @@ -0,0 +1,329 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/djxl.h" + +#include <stdio.h> + +#include "lib/extras/codec.h" +#include "lib/extras/dec/color_description.h" +#include "lib/extras/time.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/alpha.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/jpeg/dec_jpeg_data_writer.h" +#include "tools/args.h" +#include "tools/box/box.h" + +namespace jpegxl { +namespace tools { + +static inline bool ParseLuminanceRange(const char* arg, + std::pair<float, float>* out) { + char* end; + out->first = static_cast<float>(strtod(arg, &end)); + if (*end == '\0') { + // That was actually the upper bound. + out->second = out->first; + out->first = 0; + return true; + } + if (*end != '-') { + fprintf(stderr, "Unable to interpret as luminance range: %s.\n", arg); + return JXL_FAILURE("Args"); + } + const char* second = end + 1; + out->second = static_cast<float>(strtod(second, &end)); + if (*end != '\0') { + fprintf(stderr, "Unable to interpret as luminance range: %s.\n", arg); + return JXL_FAILURE("Args"); + } + return true; +} + +void DecompressArgs::AddCommandLineOptions(CommandLineParser* cmdline) { + // Positional arguments. + cmdline->AddPositionalOption("INPUT", /* required = */ true, + "the compressed input file", &file_in); + + cmdline->AddPositionalOption( + "OUTPUT", /* required = */ true, + "the output can be PNG with ICC, JPG, or PPM/PFM.", &file_out); + + cmdline->AddOptionFlag('V', "version", "print version number and exit", + &version, &SetBooleanTrue); + + cmdline->AddOptionValue('\0', "num_reps", "N", nullptr, &num_reps, + &ParseUnsigned); + + cmdline->AddOptionValue('\0', "num_threads", "N", + "The number of threads to use", &num_threads, + &ParseUnsigned); + + cmdline->AddOptionValue('\0', "print_profile", "0|1", + "print timing information before exiting", + &print_profile, &ParseOverride); + + cmdline->AddOptionValue('\0', "bits_per_sample", "N", + "defaults to original (input) bit depth", + &bits_per_sample, &ParseUnsigned); + + cmdline->AddOptionFlag( + '\0', "tone_map", + "tone map the image to the luminance range indicated by --display_nits " + "instead of performing a naive 0-1 -> 0-1 conversion", + &tone_map, &SetBooleanTrue); + + cmdline->AddOptionValue('\0', "display_nits", "0.3-250", + "luminance range of the display to which to " + "tone-map; the lower bound can be omitted", + &display_nits, &ParseLuminanceRange); + cmdline->AddOptionValue( + '\0', "preserve_saturation", "0..1", + "with --tone_map, how much to favor saturation over luminance", + &preserve_saturation, &ParseFloat); + + cmdline->AddOptionValue('\0', "color_space", "RGB_D65_SRG_Rel_Lin", + "defaults to original (input) color space", + &color_space, &ParseString); + + cmdline->AddOptionValue('s', "downsampling", "1,2,4,8,16", + "maximum permissible downsampling factor (values " + "greater than 16 will return the LQIP if available)", + ¶ms.max_downsampling, &ParseUnsigned); + + cmdline->AddOptionFlag('\0', "allow_partial_files", + "allow decoding of truncated files", + ¶ms.allow_partial_files, &SetBooleanTrue); + + cmdline->AddOptionFlag('\0', "allow_more_progressive_steps", + "allow decoding more progressive steps in truncated " + "files. No effect without --allow_partial_files", + ¶ms.allow_more_progressive_steps, &SetBooleanTrue); + +#if JPEGXL_ENABLE_JPEG + cmdline->AddOptionFlag( + 'j', "pixels_to_jpeg", + "By default, if the input JPEG XL contains a recompressed JPEG file, " + "djxl " + "reconstructs the exact original JPEG file. This flag causes the decoder " + "to instead decode the image to pixels and encode a new (lossy) JPEG. " + "The output file if provided must be a .jpg or .jpeg file.", + &decode_to_pixels, &SetBooleanTrue); + + opt_jpeg_quality_id = + cmdline->AddOptionValue('q', "jpeg_quality", "N", + "JPEG output quality. Setting an output quality " + "implies --pixels_to_jpeg.", + &jpeg_quality, &ParseUnsigned); +#endif + +#if JPEGXL_ENABLE_SJPEG + cmdline->AddOptionFlag('\0', "use_sjpeg", + "use sjpeg instead of libjpeg for JPEG output", + &use_sjpeg, &SetBooleanTrue); +#endif + + cmdline->AddOptionFlag('\0', "print_read_bytes", + "print total number of decoded bytes", + &print_read_bytes, &SetBooleanTrue); + + cmdline->AddOptionFlag('\0', "quiet", "silence output (except for errors)", + &quiet, &SetBooleanTrue); +} + +jxl::Status DecompressArgs::ValidateArgs(const CommandLineParser& cmdline) { + if (file_in == nullptr) { + fprintf(stderr, "Missing INPUT filename.\n"); + return false; + } + +#if JPEGXL_ENABLE_JPEG + if (cmdline.GetOption(opt_jpeg_quality_id)->matched()) { + decode_to_pixels = true; + } +#endif + if (file_out) { + const std::string extension = jxl::Extension(file_out); + const jxl::extras::Codec codec = + jxl::extras::CodecFromExtension(extension, &bits_per_sample); + if (codec != jxl::extras::Codec::kJPG) { + // when decoding to anything-but-JPEG, we'll need pixels + decode_to_pixels = true; + } + } else { + decode_to_pixels = true; + } + return true; +} + +jxl::Status DecompressJxlToPixels(const jxl::Span<const uint8_t> compressed, + const jxl::DecompressParams& params, + jxl::ThreadPool* pool, + jxl::CodecInOut* JXL_RESTRICT io, + SpeedStats* JXL_RESTRICT stats) { + const double t0 = jxl::Now(); + if (!jxl::DecodeFile(params, compressed, io, pool)) { + fprintf(stderr, "Failed to decompress to pixels.\n"); + return false; + } + const double t1 = jxl::Now(); + stats->NotifyElapsed(t1 - t0); + stats->SetImageSize(io->xsize(), io->ysize()); + return true; +} + +jxl::Status DecompressJxlToJPEG(const JpegXlContainer& container, + const DecompressArgs& args, + jxl::ThreadPool* pool, jxl::PaddedBytes* output, + SpeedStats* JXL_RESTRICT stats) { + output->clear(); + const double t0 = jxl::Now(); + + jxl::Span<const uint8_t> compressed(container.codestream); + + JXL_RETURN_IF_ERROR(compressed.size() >= 2); + + // JXL case + // Decode to DCT when possible and generate a JPG file. + jxl::CodecInOut io; + // Set JPEG quality. + // TODO(deymo): We should probably fail to give a JPEG file if the + // original image can't be transcoded to a JPEG file without passing + // through pixels, or at least signal this to the user. + io.use_sjpeg = args.use_sjpeg; + io.jpeg_quality = args.jpeg_quality; + + if (!DecodeJpegXlToJpeg(args.params, container, &io, pool)) { + return JXL_FAILURE("Failed to decode JXL to JPEG"); + } + if (!jxl::jpeg::EncodeImageJPGCoefficients(&io, output)) { + return JXL_FAILURE("Failed to generate JPEG"); + } + stats->SetImageSize(io.xsize(), io.ysize()); + + const double t1 = jxl::Now(); + stats->NotifyElapsed(t1 - t0); + stats->SetFileSize(output->size()); + return true; +} + +jxl::Status WriteJxlOutput(const DecompressArgs& args, const char* file_out, + jxl::CodecInOut& io, jxl::ThreadPool* pool) { + // Can only write if we decoded and have an output filename. + // (Writing large PNGs is slow, so allow skipping it for benchmarks.) + if (file_out == nullptr) return true; + + // Stay in original color space unless something else is needed. + jxl::ColorEncoding c_out = io.metadata.m.color_encoding; + // Override original color space with sRGB if input is CMYK. + if (io.Main().HasBlack()) c_out = jxl::ColorEncoding::SRGB(false); + // Override original color space with arg if specified. + if (!args.color_space.empty()) { + bool color_space_applied = false; + JxlColorEncoding c_out_external; + if (jxl::ParseDescription(args.color_space, &c_out_external) && + ConvertExternalToInternalColorEncoding(c_out_external, &c_out) && + c_out.CreateICC()) { + color_space_applied = true; + } else { + jxl::PaddedBytes icc; + if (jxl::ReadFile(args.color_space, &icc) && + c_out.SetICC(std::move(icc))) { + color_space_applied = true; + } + } + + if (!color_space_applied) { + fprintf(stderr, "Failed to apply color_space.\n"); + return false; + } + } + + // Override original #bits with arg if specified. + size_t bits_per_sample = io.metadata.m.bit_depth.bits_per_sample; + if (args.bits_per_sample != 0) bits_per_sample = args.bits_per_sample; + + if (args.tone_map) { + jxl::Status status = jxl::ToneMapTo(args.display_nits, &io, pool); + if (!status) fprintf(stderr, "Failed to map tones.\n"); + JXL_RETURN_IF_ERROR(status); + status = jxl::GamutMap(&io, args.preserve_saturation, pool); + if (!status) fprintf(stderr, "Failed to map gamut.\n"); + JXL_RETURN_IF_ERROR(status); + if (c_out.tf.IsPQ() && args.color_space.empty()) { + // Prevent writing the tone-mapped image to PQ output unless explicitly + // requested. The result would look even dimmer than it would have without + // tone mapping. + c_out.tf.SetTransferFunction(jxl::TransferFunction::k709); + status = c_out.CreateICC(); + if (!status) fprintf(stderr, "Failed to create ICC\n"); + JXL_RETURN_IF_ERROR(c_out.CreateICC()); + } + } + + const char* extension = strrchr(file_out, '.'); + std::string base = extension == nullptr + ? std::string(file_out) + : std::string(file_out, extension - file_out); + if (extension == nullptr) extension = ""; + const jxl::extras::Codec codec = jxl::extras::CodecFromExtension(extension); + if (!io.metadata.m.have_animation || codec == jxl::extras::Codec::kPNG) { + bool ok; + if (io.Main().IsJPEG() && codec == jxl::extras::Codec::kJPG) { + jxl::PaddedBytes encoded; + ok = jxl::jpeg::EncodeImageJPGCoefficients(&io, &encoded) && + jxl::WriteFile(encoded, file_out); + } else { + ok = jxl::EncodeToFile(io, c_out, bits_per_sample, file_out, pool); + } + if (!ok) { + fprintf(stderr, "Failed to write decoded image.\n"); + return false; + } + } else { + const int digits = 1 + static_cast<int>(std::log10(std::max( + 1, static_cast<int>(io.frames.size() - 1)))); + std::vector<char> output_filename; + output_filename.resize(base.size() + 1 + digits + strlen(extension) + 1); + + for (size_t i = 0; i < io.frames.size(); ++i) { + jxl::CodecInOut frame_io; + frame_io.SetFromImage(jxl::CopyImage(*io.frames[i].color()), + io.frames[i].c_current()); + frame_io.metadata.m = *io.frames[i].metadata(); + frame_io.jpeg_quality = io.jpeg_quality; + frame_io.use_sjpeg = io.use_sjpeg; + if (io.frames[i].HasAlpha()) { + frame_io.Main().SetAlpha( + jxl::CopyImage(*io.frames[i].alpha()), + /*alpha_is_premultiplied=*/io.frames[i].AlphaIsPremultiplied()); + } + snprintf(output_filename.data(), output_filename.size(), "%s-%0*zu%s", + base.c_str(), digits, i, extension); + if (!EncodeToFile(frame_io, c_out, bits_per_sample, + output_filename.data(), pool)) { + fprintf(stderr, + "Failed to write decoded image for frame %" PRIuS "/%" PRIuS + ".\n", + i + 1, io.frames.size()); + } + } + } + return true; +} + +} // namespace tools +} // namespace jpegxl diff --git a/media/libjxl/src/tools/djxl.h b/media/libjxl/src/tools/djxl.h new file mode 100644 index 0000000000..d091ed7a3f --- /dev/null +++ b/media/libjxl/src/tools/djxl.h @@ -0,0 +1,91 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_DJXL_H_ +#define TOOLS_DJXL_H_ + +#include <stddef.h> + +#include <thread> + +#include "jxl/decode.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/dec_params.h" +#include "tools/args.h" +#include "tools/box/box.h" +#include "tools/cmdline.h" +#include "tools/speed_stats.h" + +namespace jpegxl { +namespace tools { + +// Common JPEG XL decompress arguments. +struct DecompressArgs { + // Initialize non-static default options. + DecompressArgs() = default; + + // Add all the command line options to the CommandLineParser. Note that the + // options are tied to the instance that this was called on. + void AddCommandLineOptions(CommandLineParser* cmdline); + + // Validate the passed arguments, checking whether all passed options are + // compatible. Returns whether the validation was successful. + jxl::Status ValidateArgs(const CommandLineParser& cmdline); + + // Common djxl parameters. + const char* file_in = nullptr; + const char* file_out = nullptr; + size_t num_threads = std::thread::hardware_concurrency(); + bool use_sjpeg = false; + size_t jpeg_quality = 95; + bool decode_to_pixels = false; + bool version = false; + jxl::Override print_profile = jxl::Override::kDefault; + + size_t num_reps = 1; + + // Format parameters: + + size_t bits_per_sample = 0; + bool tone_map = false; + std::pair<float, float> display_nits = {0.f, jxl::kDefaultIntensityTarget}; + float preserve_saturation = .1f; + std::string color_space; // description or path to ICC profile + + jxl::DecompressParams params; + + // If true, print the effective amount of bytes read from the bitstream. + bool print_read_bytes = false; + bool quiet = false; + + // References (ids) of specific options to check if they were matched. + CommandLineParser::OptionId opt_jpeg_quality_id = -1; +}; + +// Decompresses and notifies SpeedStats of elapsed time. +jxl::Status DecompressJxlToPixels(const jxl::Span<const uint8_t> compressed, + const jxl::DecompressParams& params, + jxl::ThreadPool* pool, + jxl::CodecInOut* JXL_RESTRICT io, + SpeedStats* JXL_RESTRICT stats); + +jxl::Status DecompressJxlToJPEG(const JpegXlContainer& container, + const DecompressArgs& args, + jxl::ThreadPool* pool, jxl::PaddedBytes* output, + SpeedStats* JXL_RESTRICT stats); + +jxl::Status WriteJxlOutput(const DecompressArgs& args, const char* file_out, + jxl::CodecInOut& io, + jxl::ThreadPool* pool = nullptr); + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_DJXL_H_ diff --git a/media/libjxl/src/tools/djxl_fuzzer.cc b/media/libjxl/src/tools/djxl_fuzzer.cc new file mode 100644 index 0000000000..a03472a58a --- /dev/null +++ b/media/libjxl/src/tools/djxl_fuzzer.cc @@ -0,0 +1,570 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <algorithm> +#include <map> +#include <mutex> +#include <random> +#include <vector> + +#include "hwy/targets.h" +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/thread_parallel_runner.h" +#include "jxl/thread_parallel_runner_cxx.h" + +namespace { + +// Externally visible value to ensure pixels are used in the fuzzer. +int external_code = 0; + +constexpr const size_t kStreamingTargetNumberOfChunks = 128; + +// Options for the fuzzing +struct FuzzSpec { + JxlDataType output_type; + JxlEndianness output_endianness; + size_t output_align; + bool get_alpha; + bool get_grayscale; + bool use_streaming; + bool jpeg_to_pixels; // decode to pixels even if it is JPEG-reconstructible + // Whether to use the callback mechanism for the output image or not. + bool use_callback; + bool keep_orientation; + bool decode_boxes; + bool coalescing; + // Used for random variation of chunk sizes, extra channels, ... to get + uint32_t random_seed; +}; + +template <typename It> +void Consume(const It& begin, const It& end) { + for (auto it = begin; it < end; ++it) { + if (*it == 0) { + external_code ^= ~0; + } else { + external_code ^= *it; + } + } +} + +template <typename T> +void Consume(const T& entry) { + const uint8_t* begin = reinterpret_cast<const uint8_t*>(&entry); + Consume(begin, begin + sizeof(T)); +} + +// use_streaming: if true, decodes the data in small chunks, if false, decodes +// it in one shot. +bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels, + const FuzzSpec& spec, std::vector<uint8_t>* pixels, + std::vector<uint8_t>* jpeg, size_t* xsize, size_t* ysize, + std::vector<uint8_t>* icc_profile) { + // Multi-threaded parallel runner. Limit to max 2 threads since the fuzzer + // itself is already multithreaded. + size_t num_threads = + std::min<size_t>(2, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + auto runner = JxlThreadParallelRunnerMake(nullptr, num_threads); + + std::mt19937 mt(spec.random_seed); + std::exponential_distribution<> dis_streaming(kStreamingTargetNumberOfChunks); + + auto dec = JxlDecoderMake(nullptr); + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents( + dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_EXTENSIONS | + JXL_DEC_COLOR_ENCODING | JXL_DEC_PREVIEW_IMAGE | + JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE | + JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_BOX)) { + return false; + } + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), + JxlThreadParallelRunner, + runner.get())) { + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetKeepOrientation(dec.get(), spec.keep_orientation)) { + abort(); + } + if (JXL_DEC_SUCCESS != JxlDecoderSetCoalescing(dec.get(), spec.coalescing)) { + abort(); + } + JxlBasicInfo info; + uint32_t channels = (spec.get_grayscale ? 1 : 3) + (spec.get_alpha ? 1 : 0); + JxlPixelFormat format = {channels, spec.output_type, spec.output_endianness, + spec.output_align}; + + if (!spec.use_streaming) { + // Set all input at once + JxlDecoderSetInput(dec.get(), jxl, size); + JxlDecoderCloseInput(dec.get()); + } + + bool seen_basic_info = false; + bool seen_extensions = false; + bool seen_color_encoding = false; + bool seen_preview = false; + bool seen_need_image_out = false; + bool seen_full_image = false; + bool seen_frame = false; + uint32_t num_frames = 0; + bool seen_jpeg_reconstruction = false; + bool seen_jpeg_need_more_output = false; + // If streaming and seen around half the input, test flushing + bool tested_flush = false; + + // Size made available for the streaming input, emulating a subset of the + // full input size. + size_t streaming_size = 0; + size_t leftover = size; + size_t preview_xsize = 0; + size_t preview_ysize = 0; + bool want_preview = false; + std::vector<uint8_t> preview_pixels; + + std::vector<uint8_t> extra_channel_pixels; + + // Callback function used when decoding with use_callback. + struct DecodeCallbackData { + JxlBasicInfo info; + size_t xsize = 0; + size_t ysize = 0; + std::mutex called_rows_mutex; + // For each row stores the segments of the row being called. For each row + // the sum of all the int values in the map up to [i] (inclusive) tell how + // many times a callback included the pixel i of that row. + std::vector<std::map<uint32_t, int>> called_rows; + + // Use the pixel values. + uint32_t value = 0; + }; + DecodeCallbackData decode_callback_data; + auto decode_callback = +[](void* opaque, size_t x, size_t y, + size_t num_pixels, const void* pixels) { + DecodeCallbackData* data = static_cast<DecodeCallbackData*>(opaque); + if (num_pixels > data->xsize) abort(); + if (x + num_pixels > data->xsize) abort(); + if (y >= data->ysize) abort(); + if (num_pixels && !pixels) abort(); + // Keep track of the segments being called by the callback. + { + const std::lock_guard<std::mutex> lock(data->called_rows_mutex); + data->called_rows[y][x]++; + data->called_rows[y][x + num_pixels]--; + data->value += *static_cast<const uint8_t*>(pixels); + } + }; + + JxlExtraChannelInfo extra_channel_info; + + std::vector<uint8_t> box_buffer; + + if (spec.decode_boxes && + JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(dec.get(), JXL_TRUE)) { + // error ignored, can still fuzz if it doesn't brotli-decompress brob boxes. + } + + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + if (status == JXL_DEC_ERROR) { + return false; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + if (spec.use_streaming) { + size_t remaining = JxlDecoderReleaseInput(dec.get()); + // move any remaining bytes to the front if necessary + size_t used = streaming_size - remaining; + jxl += used; + leftover -= used; + streaming_size -= used; + size_t chunk_size = std::max<size_t>( + 1, size * std::min<double>(1.0, dis_streaming(mt))); + size_t add_size = + std::min<size_t>(chunk_size, leftover - streaming_size); + if (add_size == 0) { + // End of the streaming data reached + return false; + } + streaming_size += add_size; + if (JXL_DEC_SUCCESS != + JxlDecoderSetInput(dec.get(), jxl, streaming_size)) { + return false; + } + if (leftover == streaming_size) { + // All possible input bytes given + JxlDecoderCloseInput(dec.get()); + } + + if (!tested_flush && seen_frame) { + // Test flush max once to avoid too slow fuzzer run + tested_flush = true; + JxlDecoderFlushImage(dec.get()); + } + } else { + return false; + } + } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + if (spec.jpeg_to_pixels) abort(); + if (!seen_jpeg_reconstruction) abort(); + seen_jpeg_need_more_output = true; + size_t used_jpeg_output = + jpeg->size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + jpeg->resize(std::max<size_t>(4096, jpeg->size() * 2)); + uint8_t* jpeg_buffer = jpeg->data() + used_jpeg_output; + size_t jpeg_buffer_size = jpeg->size() - used_jpeg_output; + + if (JXL_DEC_SUCCESS != + JxlDecoderSetJPEGBuffer(dec.get(), jpeg_buffer, jpeg_buffer_size)) { + return false; + } + } else if (status == JXL_DEC_BASIC_INFO) { + if (seen_basic_info) abort(); // already seen basic info + seen_basic_info = true; + + memset(&info, 0, sizeof(info)); + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &info)) { + return false; + } + Consume(info); + + *xsize = info.xsize; + *ysize = info.ysize; + decode_callback_data.info = info; + size_t num_pixels = *xsize * *ysize; + // num_pixels overflow + if (*xsize != 0 && num_pixels / *xsize != *ysize) return false; + // limit max memory of this fuzzer test + if (num_pixels > max_pixels) return false; + + if (info.have_preview) { + want_preview = true; + preview_xsize = info.preview.xsize; + preview_ysize = info.preview.ysize; + size_t preview_num_pixels = preview_xsize * preview_ysize; + // num_pixels overflow + if (preview_xsize != 0 && + preview_num_pixels / preview_xsize != preview_ysize) { + return false; + } + // limit max memory of this fuzzer test + if (preview_num_pixels > max_pixels) return false; + } + + for (size_t ec = 0; ec < info.num_extra_channels; ++ec) { + memset(&extra_channel_info, 0, sizeof(extra_channel_info)); + if (JXL_DEC_SUCCESS != + JxlDecoderGetExtraChannelInfo(dec.get(), ec, &extra_channel_info)) { + abort(); + } + Consume(extra_channel_info); + std::vector<char> ec_name(extra_channel_info.name_length + 1); + if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelName(dec.get(), ec, + ec_name.data(), + ec_name.size())) { + abort(); + } + Consume(ec_name.cbegin(), ec_name.cend()); + } + } else if (status == JXL_DEC_EXTENSIONS) { + if (!seen_basic_info) abort(); // expected basic info first + if (seen_color_encoding) abort(); // should happen after this + if (seen_extensions) abort(); // already seen extensions + seen_extensions = true; + // TODO(eustas): get extensions? + } else if (status == JXL_DEC_COLOR_ENCODING) { + if (!seen_basic_info) abort(); // expected basic info first + if (seen_color_encoding) abort(); // already seen color encoding + seen_color_encoding = true; + + // Get the ICC color profile of the pixel data + size_t icc_size; + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize( + dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) { + return false; + } + icc_profile->resize(icc_size); + if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile( + dec.get(), &format, + JXL_COLOR_PROFILE_TARGET_DATA, + icc_profile->data(), icc_profile->size())) { + return false; + } + if (want_preview) { + size_t preview_size; + if (JXL_DEC_SUCCESS != + JxlDecoderPreviewOutBufferSize(dec.get(), &format, &preview_size)) { + return false; + } + preview_pixels.resize(preview_size); + if (JXL_DEC_SUCCESS != JxlDecoderSetPreviewOutBuffer( + dec.get(), &format, preview_pixels.data(), + preview_pixels.size())) { + abort(); + } + } + } else if (status == JXL_DEC_PREVIEW_IMAGE) { + if (seen_preview) abort(); + if (!want_preview) abort(); + if (!seen_color_encoding) abort(); + want_preview = false; + seen_preview = true; + Consume(preview_pixels.cbegin(), preview_pixels.cend()); + } else if (status == JXL_DEC_FRAME || + status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + if (want_preview) abort(); // expected preview before frame + if (!seen_color_encoding) abort(); // expected color encoding first + if (status == JXL_DEC_FRAME) { + if (seen_frame) abort(); // already seen JXL_DEC_FRAME + seen_frame = true; + JxlFrameHeader frame_header; + memset(&frame_header, 0, sizeof(frame_header)); + if (JXL_DEC_SUCCESS != + JxlDecoderGetFrameHeader(dec.get(), &frame_header)) { + abort(); + } + decode_callback_data.xsize = frame_header.layer_info.xsize; + decode_callback_data.ysize = frame_header.layer_info.ysize; + if (!spec.coalescing) { + decode_callback_data.called_rows.clear(); + } + decode_callback_data.called_rows.resize(decode_callback_data.ysize); + Consume(frame_header); + std::vector<char> frame_name(frame_header.name_length + 1); + if (JXL_DEC_SUCCESS != JxlDecoderGetFrameName(dec.get(), + frame_name.data(), + frame_name.size())) { + abort(); + } + Consume(frame_name.cbegin(), frame_name.cend()); + // When not testing streaming, test that JXL_DEC_NEED_IMAGE_OUT_BUFFER + // occurs instead, so do not set buffer now. + if (!spec.use_streaming) continue; + } + if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + // expected JXL_DEC_FRAME instead + if (!seen_frame) abort(); + // already should have set buffer if streaming + if (spec.use_streaming) abort(); + // already seen need image out + if (seen_need_image_out) abort(); + seen_need_image_out = true; + } + + if (info.num_extra_channels > 0) { + std::uniform_int_distribution<> dis(0, info.num_extra_channels); + size_t ec_index = dis(mt); + // There is also a probability no extra channel is chosen + if (ec_index < info.num_extra_channels) { + size_t ec_index = info.num_extra_channels - 1; + size_t ec_size; + if (JXL_DEC_SUCCESS != JxlDecoderExtraChannelBufferSize( + dec.get(), &format, &ec_size, ec_index)) { + return false; + } + extra_channel_pixels.resize(ec_size); + if (JXL_DEC_SUCCESS != + JxlDecoderSetExtraChannelBuffer(dec.get(), &format, + extra_channel_pixels.data(), + ec_size, ec_index)) { + return false; + } + } + } + + if (spec.use_callback) { + if (JXL_DEC_SUCCESS != + JxlDecoderSetImageOutCallback(dec.get(), &format, decode_callback, + &decode_callback_data)) { + return false; + } + } else { + // Use the pixels output buffer. + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) { + return false; + } + pixels->resize(buffer_size); + void* pixels_buffer = (void*)pixels->data(); + size_t pixels_buffer_size = pixels->size(); + if (JXL_DEC_SUCCESS != + JxlDecoderSetImageOutBuffer(dec.get(), &format, pixels_buffer, + pixels_buffer_size)) { + return false; + } + } + } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { + if (want_preview) abort(); // expected preview before frame + if (seen_jpeg_reconstruction) abort(); + seen_jpeg_reconstruction = true; + if (!spec.jpeg_to_pixels) { + // Make sure buffer is allocated, but current size is too small to + // contain valid JPEG. + jpeg->resize(1); + uint8_t* jpeg_buffer = jpeg->data(); + size_t jpeg_buffer_size = jpeg->size(); + if (JXL_DEC_SUCCESS != + JxlDecoderSetJPEGBuffer(dec.get(), jpeg_buffer, jpeg_buffer_size)) { + return false; + } + } + } else if (status == JXL_DEC_FULL_IMAGE) { + if (want_preview) abort(); // expected preview before frame + if (!spec.jpeg_to_pixels && seen_jpeg_reconstruction) { + if (!seen_jpeg_need_more_output) abort(); + jpeg->resize(jpeg->size() - JxlDecoderReleaseJPEGBuffer(dec.get())); + } else { + // expected need image out or frame first + if (!seen_need_image_out && !seen_frame) abort(); + } + + seen_full_image = true; // there may be multiple if animated + + // There may be a next animation frame so expect those again: + seen_need_image_out = false; + seen_frame = false; + num_frames++; + + // "Use" all the pixels; MSAN needs a conditional to count as usage. + Consume(pixels->cbegin(), pixels->cend()); + Consume(jpeg->cbegin(), jpeg->cend()); + + // When not coalescing, check that the whole (possibly cropped) frame was + // sent + if (seen_need_image_out && spec.use_callback && spec.coalescing) { + // Check that the callback sent all the pixels + for (uint32_t y = 0; y < decode_callback_data.ysize; y++) { + // Check that each row was at least called once. + if (decode_callback_data.called_rows[y].empty()) abort(); + uint32_t last_idx = 0; + int calls = 0; + for (auto it : decode_callback_data.called_rows[y]) { + if (it.first > last_idx) { + if (static_cast<uint32_t>(calls) != 1) abort(); + } + calls += it.second; + last_idx = it.first; + } + } + } + // Nothing to do. Do not yet return. If the image is an animation, more + // full frames may be decoded. This example only keeps the last one. + } else if (status == JXL_DEC_SUCCESS) { + if (!seen_full_image) abort(); // expected full image before finishing + + // When decoding we may not get seen_need_image_out unless we were + // decoding the image to pixels. + if (seen_need_image_out && spec.use_callback && spec.coalescing) { + // Check that the callback sent all the pixels + for (uint32_t y = 0; y < decode_callback_data.ysize; y++) { + // Check that each row was at least called once. + if (decode_callback_data.called_rows[y].empty()) abort(); + uint32_t last_idx = 0; + int calls = 0; + for (auto it : decode_callback_data.called_rows[y]) { + if (it.first > last_idx) { + if (static_cast<uint32_t>(calls) != num_frames) abort(); + } + calls += it.second; + last_idx = it.first; + } + } + } + + // All decoding successfully finished. + // It's not required to call JxlDecoderReleaseInput(dec.get()) here since + // the decoder will be destroyed. + return true; + } else if (status == JXL_DEC_BOX) { + if (spec.decode_boxes) { + if (!box_buffer.empty()) { + size_t remaining = JxlDecoderReleaseBoxBuffer(dec.get()); + size_t box_size = box_buffer.size() - remaining; + if (box_size != 0) { + Consume(box_buffer.begin(), box_buffer.begin() + box_size); + box_buffer.clear(); + } + } + box_buffer.resize(64); + JxlDecoderSetBoxBuffer(dec.get(), box_buffer.data(), box_buffer.size()); + } + } else if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) { + if (!spec.decode_boxes) { + abort(); // Not expected when not setting output buffer + } + size_t remaining = JxlDecoderReleaseBoxBuffer(dec.get()); + size_t box_size = box_buffer.size() - remaining; + box_buffer.resize(box_buffer.size() * 2); + JxlDecoderSetBoxBuffer(dec.get(), box_buffer.data() + box_size, + box_buffer.size() - box_size); + } else { + return false; + } + } +} + +int TestOneInput(const uint8_t* data, size_t size) { + if (size < 4) return 0; + uint32_t flags = 0; + size_t used_flag_bits = 0; + memcpy(&flags, data + size - 4, 4); + size -= 4; + + const auto getFlag = [&flags, &used_flag_bits](size_t max_value) { + size_t limit = 1; + while (limit <= max_value) { + limit <<= 1; + used_flag_bits++; + if (used_flag_bits > 32) abort(); + } + uint32_t result = flags % limit; + flags /= limit; + return result % (max_value + 1); + }; + + FuzzSpec spec; + // Allows some different possible variations in the chunk sizes of the + // streaming case + spec.random_seed = flags ^ size; + spec.get_alpha = !!getFlag(1); + spec.get_grayscale = !!getFlag(1); + spec.use_streaming = !!getFlag(1); + spec.jpeg_to_pixels = !!getFlag(1); + spec.use_callback = !!getFlag(1); + spec.keep_orientation = !!getFlag(1); + spec.coalescing = !!getFlag(1); + spec.output_type = static_cast<JxlDataType>(getFlag(JXL_TYPE_FLOAT16)); + spec.output_endianness = static_cast<JxlEndianness>(getFlag(JXL_BIG_ENDIAN)); + spec.output_align = getFlag(16); + spec.decode_boxes = !!getFlag(1); + + std::vector<uint8_t> pixels; + std::vector<uint8_t> jpeg; + std::vector<uint8_t> icc; + size_t xsize, ysize; + size_t max_pixels = 1 << 21; + + const auto targets = hwy::SupportedAndGeneratedTargets(); + hwy::SetSupportedTargetsForTest(targets[getFlag(targets.size() - 1)]); + DecodeJpegXl(data, size, max_pixels, spec, &pixels, &jpeg, &xsize, &ysize, + &icc); + hwy::SetSupportedTargetsForTest(0); + + return 0; +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/djxl_main.cc b/media/libjxl/src/tools/djxl_main.cc new file mode 100644 index 0000000000..d1442d949c --- /dev/null +++ b/media/libjxl/src/tools/djxl_main.cc @@ -0,0 +1,195 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include <vector> + +#include "jxl/decode.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/cache_aligned.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "tools/box/box.h" +#include "tools/cmdline.h" +#include "tools/codec_config.h" +#include "tools/djxl.h" +#include "tools/speed_stats.h" + +namespace jpegxl { +namespace tools { +namespace { + +int DecompressMain(int argc, const char* argv[]) { + DecompressArgs args; + CommandLineParser cmdline; + args.AddCommandLineOptions(&cmdline); + + if (!cmdline.Parse(argc, argv)) { + // ValidateArgs already printed the actual error cause. + fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); + return 1; + } + + if (args.version) { + fprintf(stdout, "djxl %s\n", + CodecConfigString(JxlDecoderVersion()).c_str()); + fprintf(stdout, "Copyright (c) the JPEG XL Project\n"); + return 0; + } + if (!args.quiet) { + fprintf(stderr, "JPEG XL decoder %s\n", + CodecConfigString(JxlDecoderVersion()).c_str()); + } + + if (cmdline.HelpFlagPassed()) { + cmdline.PrintHelp(); + return 0; + } + + if (!args.ValidateArgs(cmdline)) { + // ValidateArgs already printed the actual error cause. + fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); + return 1; + } + + jxl::PaddedBytes compressed; + if (!jxl::ReadFile(args.file_in, &compressed)) { + fprintf(stderr, "Failed to read file: %s.\n", args.file_in); + return 1; + } + if (!args.quiet) { + fprintf(stderr, "Read %" PRIuS " compressed bytes.\n", compressed.size()); + } + + // If the file uses the box format container, unpack the boxes into + // `container`. Otherwise, fill `container.codestream` accordingly. + JpegXlContainer container; + if (IsContainerHeader(compressed.data(), compressed.size())) { + if (!DecodeJpegXlContainerOneShot(compressed.data(), compressed.size(), + &container)) { + fprintf(stderr, "Decoding container format failed.\n"); + return 1; + } + } else { + container.codestream = std::move(compressed); + } + + jxl::ThreadPoolInternal pool(args.num_threads); + SpeedStats stats; + + // Quick test that this looks like a valid JXL file. + JxlSignature signature = JxlSignatureCheck(container.codestream.data(), + container.codestream.size()); + if (signature == JXL_SIG_NOT_ENOUGH_BYTES || signature == JXL_SIG_INVALID) { + fprintf(stderr, "Unknown compressed image format (%u)\n", signature); + return 1; + } + + if (!args.file_out && !args.quiet) { + fprintf(stderr, + "No output file specified.\n" + "Decoding will be performed, but the result will be discarded.\n"); + } + + jxl::AuxOut aux_out; + + if (!args.decode_to_pixels) { + args.params.keep_dct = true; + + jxl::PaddedBytes jpg_output; + bool success = true; + for (size_t i = 0; i < args.num_reps; ++i) { + success = success && DecompressJxlToJPEG(container, args, &pool, + &jpg_output, &stats); + } + if (!args.quiet && success) fprintf(stderr, "Reconstructed to JPEG.\n"); + + if (success && args.file_out != nullptr) { + if (!jxl::WriteFile(jpg_output, args.file_out)) { + fprintf(stderr, "Failed to write to \"%s\"\n", args.file_out); + return 1; + } + } + if (!success) { + if (!args.quiet) { + fprintf(stderr, + "Warning: could not decode losslessly to JPEG. Retrying with " + "--pixels_to_jpeg...\n"); + } + args.decode_to_pixels = true; + } + } + if (args.decode_to_pixels) { + args.params.keep_dct = false; + jxl::CodecInOut io; + auto assign = [](const uint8_t* bytes, size_t size, + std::vector<uint8_t>& target) { + target.assign(bytes, bytes + size); + }; + if (container.exif_size) { + assign(container.exif, container.exif_size, io.blobs.exif); + } + if (!container.xml.empty()) { + assign(container.xml[0].first, container.xml[0].second, io.blobs.xmp); + } + if (container.xml.size() > 1) { + fprintf(stderr, + "Warning: more than one XML box found, assuming first one is XMP " + "and ignoring others\n"); + } + // Set JPEG quality. + // TODO(veluca): the decoder should set this value, and the argument should + // be an override. + // TODO(veluca): the decoder should directly produce a JPEG file, and this + // should not be necessary. + io.use_sjpeg = args.use_sjpeg; + io.jpeg_quality = args.jpeg_quality; + + // Decode to pixels. + for (size_t i = 0; i < args.num_reps; ++i) { + if (!DecompressJxlToPixels(jxl::Span<const uint8_t>(container.codestream), + args.params, &pool, &io, &stats)) { + // Error is already reported by DecompressJxlToPixels. + return 1; + } + } + if (!args.quiet) fprintf(stderr, "Decoded to pixels.\n"); + if (!WriteJxlOutput(args, args.file_out, io, &pool)) { + // Error is already reported by WriteJxlOutput. + return 1; + } + + if (args.print_read_bytes) { + fprintf(stderr, "Decoded bytes: %" PRIuS "\n", io.Main().decoded_bytes()); + } + } + + if (!args.quiet) JXL_CHECK(stats.Print(pool.NumWorkerThreads())); + + if (args.print_profile == jxl::Override::kOn) { + PROFILER_PRINT_RESULTS(); + } + if (!args.quiet) jxl::CacheAligned::PrintStats(); + return 0; +} + +} // namespace +} // namespace tools +} // namespace jpegxl + +int main(int argc, const char* argv[]) { + return jpegxl::tools::DecompressMain(argc, argv); +} diff --git a/media/libjxl/src/tools/djxl_ng_main.cc b/media/libjxl/src/tools/djxl_ng_main.cc new file mode 100644 index 0000000000..b2eec302a8 --- /dev/null +++ b/media/libjxl/src/tools/djxl_ng_main.cc @@ -0,0 +1,476 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <climits> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <string> +#include <vector> + +#include "gflags/gflags.h" +#include "jxl/codestream_header.h" +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/resizable_parallel_runner_cxx.h" +#include "jxl/thread_parallel_runner.h" +#include "jxl/thread_parallel_runner_cxx.h" +#include "jxl/types.h" +#include "lib/extras/dec/decode.h" +#include "lib/extras/enc/pnm.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/status.h" + +DECLARE_bool(help); +DECLARE_bool(helpshort); + +DEFINE_int64(num_reps, 1, "How many times to decompress."); + +DEFINE_int64(num_threads, 0, + // TODO(firsching): Sync with team about changed meaning of 0 - + // was: No multithreaded workers. Is: use default number. + "Number of worker threads (0 == use machine default)."); + +// TODO(firsching): wire this up. +DEFINE_int32(bits_per_sample, 0, "0 = original (input) bit depth"); + +// TODO(firsching): wire this up. +DEFINE_bool( + tone_map, true, + "tone map the image to the luminance range indicated by --display_nits " + "instead of performing a naive 0-1 -> 0-1 conversion"); + +// TODO(firsching): wire this up. +DEFINE_string(display_nits, "0.f-255.", + "luminance range of the display to which to " + "tone-map; the lower bound can be omitted"); + +// TODO(firsching): wire this up. +DEFINE_double(preserve_saturation, 0.1, + "with --tone_map, how much to favor saturation over luminance"); + +// TODO(firsching): wire this up; consider making empty string the default. +DEFINE_string(color_space, "RGB_D65_SRG_Rel_Lin", + "defaults to original (input) color space"); + +// TODO(firsching): wire this up. +DEFINE_uint32(downsampling, 0, + "maximum permissible downsampling factor (values " + "greater than 16 will return the LQIP if available"); + +// TODO(firsching): wire this up. +DEFINE_bool(allow_partial_files, false, "allow decoding of truncated files"); + +// TODO(firsching): wire this up. +DEFINE_bool(allow_more_progressive_steps, false, + "allow decoding more progressive steps in truncated " + "files. No effect without --allow_partial_files"); + +#if JPEGXL_ENABLE_JPEG +// TODO(firsching): wire this up. +DEFINE_bool( + pixels_to_jpeg, false, + "By default, if the input JPEG XL contains a recompressed JPEG file, djxl " + "reconstructs the exact original JPEG file. This flag causes the decoder " + "to instead decode the image to pixels and encode a new (lossy) JPEG. " + "The output file if provided must be a .jpg or .jpeg file."); + +// TODO(firsching): wire this up. +DEFINE_uint32(jpeg_quality, 95, + "JPEG output quality. Setting an output quality " + "implies --pixels_to_jpeg."); +#endif + +#if JPEGXL_ENABLE_SJPEG +// TODO(firsching): wire this up. +DEFINE_bool(use_sjpeg, false, "use sjpeg instead of libjpeg for JPEG output"); +#endif + +// TODO(firsching): wire this up. +DEFINE_bool(print_read_bytes, false, "print total number of decoded bytes"); + +// TODO(firsching): wire this up. +DEFINE_bool(quiet, false, "silence output (except for errors)"); + +bool ReadFile(const char* filename, std::vector<uint8_t>* out) { + FILE* file = fopen(filename, "rb"); + if (!file) { + return false; + } + + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return false; + } + + long size = ftell(file); + // Avoid invalid file or directory. + if (size >= LONG_MAX || size < 0) { + fclose(file); + return false; + } + + if (fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return false; + } + + out->resize(size); + size_t readsize = fread(out->data(), 1, size, file); + if (fclose(file) != 0) { + return false; + } + + return readsize == static_cast<size_t>(size); +} + +bool WriteFile(const char* filename, const std::vector<uint8_t>& bytes) { + FILE* file = fopen(filename, "wb"); + if (!file) { + fprintf(stderr, + "Could not open %s for writing\n" + "Error: %s", + filename, strerror(errno)); + return false; + } + if (fwrite(bytes.data(), 1, bytes.size(), file) != bytes.size()) { + fprintf(stderr, + "Could not write to file\n" + "Error: %s", + strerror(errno)); + return false; + } + if (fclose(file) != 0) { + fprintf(stderr, + "Could not close file\n" + "Error: %s", + strerror(errno)); + return false; + } + return true; +} + +int DecompressJxlReconstructJPEG(const std::vector<uint8_t>& compressed, + std::vector<uint8_t>& jpeg_bytes, + JxlDecoderPtr dec, + JxlThreadParallelRunnerPtr runner) { + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), + JxlThreadParallelRunner, + runner.get())) { + fprintf(stderr, "JxlEncoderSetParallelRunner failed\n"); + return EXIT_FAILURE; + } + + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents( + dec.get(), JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE)) { + fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); + return EXIT_FAILURE; + } + bool can_reconstruct_jpeg = false; + std::vector<uint8_t> jpeg_data_chunk(16384); + jpeg_bytes.resize(0); + if (JXL_DEC_SUCCESS != + JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size())) { + fprintf(stderr, "Decoder failed to set input\n"); + return EXIT_FAILURE; + } + JxlDecoderCloseInput(dec.get()); + + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + if (status == JXL_DEC_ERROR) { + fprintf(stderr, "Failed to decode image\n"); + return EXIT_FAILURE; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + fprintf(stderr, "Error, already provided all input\n"); + return EXIT_FAILURE; + } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { + can_reconstruct_jpeg = true; + // Decoding to JPEG. + if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec.get(), + jpeg_data_chunk.data(), + jpeg_data_chunk.size())) { + fprintf(stderr, "Decoder failed to set JPEG Buffer\n"); + return EXIT_FAILURE; + } + } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + // Decoded a chunk to JPEG. + size_t used_jpeg_output = + jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + jpeg_bytes.insert(jpeg_bytes.end(), jpeg_data_chunk.data(), + jpeg_data_chunk.data() + used_jpeg_output); + if (used_jpeg_output == 0) { + // Chunk is too small. + jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2); + } + if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec.get(), + jpeg_data_chunk.data(), + jpeg_data_chunk.size())) { + fprintf(stderr, "Decoder failed to set JPEG Buffer\n"); + return EXIT_FAILURE; + } + } else if (status == JXL_DEC_SUCCESS) { + // Decoding finished successfully. + break; + } else if (status == JXL_DEC_FULL_IMAGE) { + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + break; + } else { + fprintf(stderr, "Error: unexpected status: %d\n", + static_cast<int>(status)); + return EXIT_FAILURE; + } + } + if (!can_reconstruct_jpeg) return EXIT_FAILURE; + size_t used_jpeg_output = + jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + jpeg_bytes.insert(jpeg_bytes.end(), jpeg_data_chunk.data(), + jpeg_data_chunk.data() + used_jpeg_output); + return EXIT_SUCCESS; +} + +int DecompressJxlToPackedPixelFile(const std::vector<uint8_t>& compressed, + jxl::extras::PackedPixelFile& ppf, + JxlPixelFormat& format, JxlDecoderPtr dec, + JxlThreadParallelRunnerPtr runner) { + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), + JxlThreadParallelRunner, + runner.get())) { + fprintf(stderr, "JxlEncoderSetParallelRunner failed\n"); + return EXIT_FAILURE; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents(dec.get(), + JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | + JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)) { + fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); + return EXIT_FAILURE; + } + + // Reading compressed JPEG XL input and decoding to pixels + if (JXL_DEC_SUCCESS != + JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size())) { + fprintf(stderr, "Decoder failed to set input\n"); + return EXIT_FAILURE; + } + // TODO(firsching): handle boxes as well (exif, iptc, jumbf and xmp). + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + if (status == JXL_DEC_ERROR) { + fprintf(stderr, "Failed to decode image\n"); + return EXIT_FAILURE; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + fprintf(stderr, "Error, already provided all input\n"); + return EXIT_FAILURE; + } else if (status == JXL_DEC_BASIC_INFO) { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &ppf.info)) { + fprintf(stderr, "JxlDecoderGetBasicInfo failed\n"); + return EXIT_FAILURE; + } + // Make some modifications to the format if the decoded data requires it. + if (ppf.info.num_color_channels != format.num_channels) { + format.num_channels = ppf.info.num_color_channels; + } + if (ppf.info.bits_per_sample > 8 && + ppf.info.exponent_bits_per_sample == 0) { + format.data_type = JXL_TYPE_UINT16; + } + // TODO(firsching): handle extra channels + } else if (status == JXL_DEC_COLOR_ENCODING) { + size_t icc_size = 0; + // TODO(firsching) handle other targets as well. + JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_ORIGINAL; + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize(dec.get(), &format, target, &icc_size)) { + fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); + } + if (icc_size != 0) { + ppf.icc.resize(icc_size); + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsICCProfile(dec.get(), &format, target, + ppf.icc.data(), icc_size)) { + fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); + return EXIT_FAILURE; + } + } else { + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsEncodedProfile(dec.get(), &format, target, + &ppf.color_encoding)) { + fprintf(stderr, "JxlDecoderGetColorAsEncodedProfile failed\n"); + return EXIT_FAILURE; + } + } + } else if (status == JXL_DEC_FRAME) { + jxl::extras::PackedFrame frame(ppf.info.xsize, ppf.info.ysize, format); + ppf.frames.emplace_back(std::move(frame)); + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) { + fprintf(stderr, "JxlDecoderImageOutBufferSize failed\n"); + return EXIT_FAILURE; + } + if (buffer_size != ppf.frames.back().color.pixels_size) { + fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n", + buffer_size, ppf.frames.back().color.pixels_size); + return EXIT_FAILURE; + } + + auto callback = [](void* opaque, size_t x, size_t y, size_t num_pixels, + const void* pixels) { + jxl::extras::PackedPixelFile* ppf = + reinterpret_cast<jxl::extras::PackedPixelFile*>(opaque); + uint8_t* pixels_buffer = + reinterpret_cast<uint8_t*>(ppf->frames.back().color.pixels()); + size_t sample_size = ppf->frames.back().color.format.num_channels * + ppf->frames.back().color.BitsPerChannel( + ppf->frames.back().color.format.data_type) / + 8; + // TODO(firsching): take color profile into account and transform if + // needed here. + memcpy(pixels_buffer + + (ppf->frames.back().color.stride * y + sample_size * x), + pixels, num_pixels * sample_size); + }; + if (JXL_DEC_SUCCESS != + JxlDecoderSetImageOutCallback(dec.get(), &format, callback, &ppf)) { + fprintf(stderr, "JxlDecoderSetImageOutCallback failed\n"); + return EXIT_FAILURE; + } + } else if (status == JXL_DEC_SUCCESS) { + // Decoding finished successfully. + break; + } else if (status == JXL_DEC_FULL_IMAGE) { + } else { + fprintf(stderr, "Error: unexpected status: %d\n", + static_cast<int>(status)); + return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; +} + +int main(int argc, char** argv) { + std::cerr << "Warning: This is work in progress, consider using djxl " + "instead!\n"; + + gflags::SetUsageMessage("JPEG XL decoder"); + uint32_t version = JxlDecoderVersion(); + gflags::SetVersionString(std::to_string(version / 1000000) + "." + + std::to_string((version / 1000) % 1000) + "." + + std::to_string(version % 1000)); + // TODO(firsching): rethink --help handling + gflags::ParseCommandLineNonHelpFlags(&argc, &argv, /*remove_flags=*/true); + if (FLAGS_help) { + FLAGS_help = false; + FLAGS_helpshort = true; + } + gflags::HandleCommandLineHelpFlags(); + + if (argc != 3) { + FLAGS_help = false; + FLAGS_helpshort = true; + gflags::HandleCommandLineHelpFlags(); + return EXIT_FAILURE; + } + const char* filename_in = argv[1]; + const char* filename_out = argv[2]; + size_t num_reps = FLAGS_num_reps; + + const char* extension = strrchr(filename_out, '.'); + std::string base = extension == nullptr + ? std::string(filename_out) + : std::string(filename_out, extension - filename_out); + if (extension == nullptr) extension = ""; + const jxl::extras::Codec codec = jxl::extras::CodecFromExtension(extension); + + std::vector<uint8_t> compressed; + // Reading compressed JPEG XL input + if (!ReadFile(filename_in, &compressed)) { + fprintf(stderr, "couldn't load %s\n", filename_in); + return EXIT_FAILURE; + } + + size_t num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads(); + { + int64_t flag_num_worker_threads = FLAGS_num_threads; + if (flag_num_worker_threads != 0) { + num_worker_threads = flag_num_worker_threads; + } + } + auto dec = JxlDecoderMake(/*memory_manager=*/nullptr); + auto runner = JxlThreadParallelRunnerMake( + /*memory_manager=*/nullptr, num_worker_threads); + std::vector<uint8_t> bytes; + if (codec == jxl::extras::Codec::kJPG +#if JPEGXL_ENABLE_JPEG + && !FLAGS_pixels_to_jpeg +#endif + ) { + std::vector<uint8_t> bytes; + for (size_t i = 0; i < num_reps; ++i) { + if (DecompressJxlReconstructJPEG(compressed, bytes, std::move(dec), + std::move(runner)) != 0) { + return EXIT_FAILURE; + } + } + if (WriteFile(filename_out, bytes)) { + return EXIT_FAILURE; + }; + // TODO(firsching): handle non-reconstruct JPEG + } else if (codec == jxl::extras::Codec::kPNM) { + JxlDataType datatype = JXL_TYPE_UINT8; + uint32_t num_channels = 3; + if (std::string(extension) == ".pfm") { + datatype = JXL_TYPE_FLOAT; + } + if (std::string(extension) == ".pgm") { + num_channels = 1; + } + + JxlPixelFormat format = {num_channels, datatype, JXL_NATIVE_ENDIAN, 0}; + jxl::extras::PackedPixelFile ppf; + if (DecompressJxlToPackedPixelFile(compressed, ppf, format, std::move(dec), + std::move(runner)) != 0) { + return EXIT_FAILURE; + } + if (ppf.info.exponent_bits_per_sample != 0) { + if (num_channels == 1 && ppf.info.num_color_channels == 3) { + JXL_WARNING("For color images, the filename should end with .ppm.\n"); + } + if (num_channels == 3 && ppf.info.num_color_channels == 1) { + JXL_WARNING( + "For grayscale images, the filename should end with .pgm.\n"); + } + if (ppf.info.bits_per_sample > 16) { + JXL_WARNING("PPM only supports up to 16 bits per sample"); + } + } + const int digits = 1 + static_cast<int>(std::log10(std::max( + 1, static_cast<int>(ppf.frames.size() - 1)))); + std::vector<char> output_filename; + output_filename.resize(base.size() + 1 + digits + strlen(extension) + 1); + for (size_t i = 0; i < ppf.frames.size(); i++) { + JXL_RETURN_IF_ERROR(jxl::extras::EncodeImagePNM( + ppf, ppf.frames[i].color.BitsPerChannel(format.data_type), nullptr, i, + &bytes)); + snprintf(output_filename.data(), output_filename.size(), "%s-%0*zu%s", + base.c_str(), digits, i, extension); + if (!WriteFile( + ppf.frames.size() > 1 ? output_filename.data() : filename_out, + bytes)) { + return EXIT_FAILURE; + } + } + } else { + // TODO(firsching): handle other formats + } + return EXIT_SUCCESS; +} diff --git a/media/libjxl/src/tools/example_tree.txt b/media/libjxl/src/tools/example_tree.txt new file mode 100644 index 0000000000..c4df6d4089 --- /dev/null +++ b/media/libjxl/src/tools/example_tree.txt @@ -0,0 +1,50 @@ +RCT 1 /* YCoCg */ +GroupShift 3 /* Group size is 128 << 3 == 1024 */ +Width 1024 +Height 1024 +Bitdepth 8 +/* FloatExpBits 3 */ +/* Alpha */ +/* Squeeze */ +/* XYB */ +/* CbYCr */ + + +if c > 0 + /* Co, Cg: diagonal stripes */ + if W > 50 + - Set -50 + - W + 5 + /* Y: elementary cellular automaton */ + if y > 0 + if N > 0 + if NW-N > -1 + if N-NE > 0 + - Set 0 + - Set 255 + if N-NE > 0 + - Set 255 + - Set 0 + if NW-N > 0 + if N-NE > -1 + - Set 255 + - Set 0 + if N-NE > -1 + - Set 0 + - Set 255 + /* First row initialization */ + if x > 511 + - Set 255 + - Set 0 + +Everything after the end of the tree is ignored. + +The tree above represents a cellular automaton on a subtly striped background. + + + +List of properties: c, g, y, x, |N|, |W|, N, W, W-WW-NW+NWW, W+N-NW, W-NW, NW-N, N-NE, N-NN, W-WW, WGH, + PrevAbs, Prev, PrevAbsErr, PrevErr, PPrevAbs, PPrev, PPrevAbsErr, PPrevErr + +List of predictors: Set, W, N, AvgW+N, Select, Gradient, Weighted, NE, NW, WW, AvgW+NW, AvgN+NW, AvgN+NE, AvgAll + diff --git a/media/libjxl/src/tools/fields_fuzzer.cc b/media/libjxl/src/tools/fields_fuzzer.cc new file mode 100644 index 0000000000..87e143928b --- /dev/null +++ b/media/libjxl/src/tools/fields_fuzzer.cc @@ -0,0 +1,85 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdint.h> + +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/jpeg/jpeg_data.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +int TestOneInput(const uint8_t* data, size_t size) { + // Global parameters used by some headers. + CodecMetadata codec_metadata; + + // First byte controls which header to parse. + if (size == 0) return 0; + BitReader reader(Span<const uint8_t>(data + 1, size - 1)); +#define FUZZER_CASE_HEADER(number, classname, ...) \ + case number: { \ + classname header{__VA_ARGS__}; \ + (void)Bundle::Read(&reader, &header); \ + break; \ + } + switch (data[0]) { + case 0: { + SizeHeader size_header; + (void)ReadSizeHeader(&reader, &size_header); + break; + } + + case 1: { + ImageMetadata metadata; + (void)ReadImageMetadata(&reader, &metadata); + break; + } + + FUZZER_CASE_HEADER(2, FrameHeader, &codec_metadata) + FUZZER_CASE_HEADER(3, jpeg::JPEGData) + FUZZER_CASE_HEADER(4, AnimationFrame, &codec_metadata) + FUZZER_CASE_HEADER(5, AnimationHeader) + FUZZER_CASE_HEADER(6, BitDepth) + FUZZER_CASE_HEADER(7, BlendingInfo) + FUZZER_CASE_HEADER(8, ColorEncoding) + FUZZER_CASE_HEADER(9, CustomTransferFunction) + FUZZER_CASE_HEADER(10, Customxy) + FUZZER_CASE_HEADER(11, ExtraChannelInfo) + FUZZER_CASE_HEADER(12, GroupHeader) + FUZZER_CASE_HEADER(13, weighted::Header) + FUZZER_CASE_HEADER(14, LoopFilter) + FUZZER_CASE_HEADER(15, LZ77Params) + FUZZER_CASE_HEADER(16, OpsinInverseMatrix) + FUZZER_CASE_HEADER(17, Passes) + FUZZER_CASE_HEADER(18, PreviewHeader) + FUZZER_CASE_HEADER(19, QuantizerParams) + FUZZER_CASE_HEADER(20, SqueezeParams) + FUZZER_CASE_HEADER(21, ToneMapping) + FUZZER_CASE_HEADER(22, Transform) + FUZZER_CASE_HEADER(23, YCbCrChromaSubsampling) + + default: { + CustomTransformData transform_data; + transform_data.nonserialized_xyb_encoded = true; + (void)Bundle::Read(&reader, &transform_data); + break; + } + } + (void)reader.Close(); + + return 0; +} + +} // namespace jxl + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return jxl::TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/flicker_test/CMakeLists.txt b/media/libjxl/src/tools/flicker_test/CMakeLists.txt new file mode 100644 index 0000000000..efa4716a2e --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/CMakeLists.txt @@ -0,0 +1,38 @@ +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +find_package(Qt5 QUIET COMPONENTS Widgets) +if (NOT Qt5_FOUND) + message(WARNING "Qt5 was not found. The flicker test tool will not be built.") + return() +endif () + +if (NOT TARGET icc_detect OR NOT TARGET image_loading) + message(WARNING "Comparison tool not built. The flicker test tool will not be built.") + return() +endif () + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +add_executable(flicker_test WIN32 + main.cc + parameters.cc + parameters.h + setup.cc + setup.h + setup.ui + split_view.cc + split_view.h + test_window.cc + test_window.h + test_window.ui) + +target_link_libraries(flicker_test PUBLIC + Qt5::Widgets + image_loading + icc_detect +) diff --git a/media/libjxl/src/tools/flicker_test/main.cc b/media/libjxl/src/tools/flicker_test/main.cc new file mode 100644 index 0000000000..67985a9638 --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/main.cc @@ -0,0 +1,22 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <QApplication> + +#include "tools/flicker_test/setup.h" +#include "tools/flicker_test/test_window.h" + +int main(int argc, char** argv) { + QApplication application(argc, argv); + + jxl::FlickerTestWizard wizard; + if (wizard.exec()) { + jxl::FlickerTestWindow test_window(wizard.parameters()); + if (test_window.proceedWithTest()) { + test_window.showMaximized(); + return application.exec(); + } + } +} diff --git a/media/libjxl/src/tools/flicker_test/parameters.cc b/media/libjxl/src/tools/flicker_test/parameters.cc new file mode 100644 index 0000000000..575edb0832 --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/parameters.cc @@ -0,0 +1,87 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/flicker_test/parameters.h" + +namespace jxl { + +namespace { + +constexpr char kPathsGroup[] = "paths"; +constexpr char kOriginalFolderKey[] = "originalFolder"; +constexpr char kAlteredFolderKey[] = "alteredFolder"; +constexpr char kOutputFileKey[] = "outputFile"; + +constexpr char kTimingGroup[] = "timing"; +constexpr char kAdvanceTimeKey[] = "advanceTimeMSecs"; +constexpr char kViewingTimeKey[] = "viewingTimeSecs"; +constexpr char kBlankingTimeKey[] = "blankingTimeMSecs"; +constexpr char kGrayGroup[] = "gray"; +constexpr char kGrayKey[] = "enabled"; +constexpr char kGrayFadingTimeKey[] = "fadingTimeMSecs"; +constexpr char kGrayTimeKey[] = "timeMSecs"; + +constexpr char kDisplayGroup[] = "display"; +constexpr char kIntensityTargetKey[] = "intensityTarget"; +constexpr char kSpacingKey[] = "spacing"; + +} // namespace + +FlickerTestParameters FlickerTestParameters::loadFrom( + QSettings* const settings) { + FlickerTestParameters parameters; + + settings->beginGroup(kPathsGroup); + parameters.originalFolder = settings->value(kOriginalFolderKey).toString(); + parameters.alteredFolder = settings->value(kAlteredFolderKey).toString(); + parameters.outputFile = settings->value(kOutputFileKey).toString(); + settings->endGroup(); + + settings->beginGroup(kTimingGroup); + parameters.advanceTimeMSecs = settings->value(kAdvanceTimeKey, 100).toInt(); + parameters.viewingTimeSecs = settings->value(kViewingTimeKey, 4).toInt(); + parameters.blankingTimeMSecs = settings->value(kBlankingTimeKey, 250).toInt(); + settings->beginGroup(kGrayGroup); + parameters.gray = settings->value(kGrayKey, false).toBool(); + parameters.grayFadingTimeMSecs = + settings->value(kGrayFadingTimeKey, 100).toInt(); + parameters.grayTimeMSecs = settings->value(kGrayTimeKey, 300).toInt(); + settings->endGroup(); + settings->endGroup(); + + settings->beginGroup(kDisplayGroup); + parameters.intensityTarget = + settings->value(kIntensityTargetKey, 250).toInt(); + parameters.spacing = settings->value(kSpacingKey, 50).toInt(); + settings->endGroup(); + + return parameters; +} + +void FlickerTestParameters::saveTo(QSettings* const settings) const { + settings->beginGroup(kPathsGroup); + settings->setValue(kOriginalFolderKey, originalFolder); + settings->setValue(kAlteredFolderKey, alteredFolder); + settings->setValue(kOutputFileKey, outputFile); + settings->endGroup(); + + settings->beginGroup(kTimingGroup); + settings->setValue(kAdvanceTimeKey, advanceTimeMSecs); + settings->setValue(kViewingTimeKey, viewingTimeSecs); + settings->setValue(kBlankingTimeKey, blankingTimeMSecs); + settings->beginGroup(kGrayGroup); + settings->setValue(kGrayKey, gray); + settings->setValue(kGrayFadingTimeKey, grayFadingTimeMSecs); + settings->setValue(kGrayTimeKey, grayTimeMSecs); + settings->endGroup(); + settings->endGroup(); + + settings->beginGroup(kDisplayGroup); + settings->setValue(kIntensityTargetKey, intensityTarget); + settings->setValue(kSpacingKey, spacing); + settings->endGroup(); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/flicker_test/parameters.h b/media/libjxl/src/tools/flicker_test/parameters.h new file mode 100644 index 0000000000..a06399566d --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/parameters.h @@ -0,0 +1,32 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_FLICKER_TEST_PARAMETERS_H_ +#define TOOLS_FLICKER_TEST_PARAMETERS_H_ + +#include <QSettings> + +namespace jxl { + +struct FlickerTestParameters { + QString originalFolder; + QString alteredFolder; + QString outputFile; + int advanceTimeMSecs; + int viewingTimeSecs; + int blankingTimeMSecs; + bool gray; + int grayFadingTimeMSecs; + int grayTimeMSecs; + int intensityTarget; + int spacing; + + static FlickerTestParameters loadFrom(QSettings* settings); + void saveTo(QSettings* settings) const; +}; + +} // namespace jxl + +#endif // TOOLS_FLICKER_TEST_PARAMETERS_H_ diff --git a/media/libjxl/src/tools/flicker_test/setup.cc b/media/libjxl/src/tools/flicker_test/setup.cc new file mode 100644 index 0000000000..bfcddd5fc0 --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/setup.cc @@ -0,0 +1,151 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/flicker_test/setup.h" + +#include <QCompleter> +#include <QFileDialog> +#include <QFileSystemModel> +#include <QMessageBox> +#include <QPushButton> + +namespace jxl { + +FlickerTestWizard::FlickerTestWizard(QWidget* const parent) + : QWizard(parent), settings_("JPEG XL project", "Flickering test") { + ui_.setupUi(this); + + connect(ui_.grayFadingTime, SIGNAL(valueChanged(int)), this, + SLOT(updateTotalGrayTime())); + connect(ui_.grayTime, SIGNAL(valueChanged(int)), this, + SLOT(updateTotalGrayTime())); + + ui_.timingButtonBox->button(QDialogButtonBox::RestoreDefaults) + ->setText(tr("Restore ISO/IEC 29170-2:2015 parameters")); + + setButtonText(QWizard::FinishButton, tr("Start test")); + + QCompleter* const completer = new QCompleter(this); + QFileSystemModel* const model = new QFileSystemModel(completer); + model->setRootPath("/"); + model->setFilter(QDir::Dirs); + completer->setModel(model); + ui_.originalFolder->setCompleter(completer); + ui_.alteredFolder->setCompleter(completer); + + const auto parameters = FlickerTestParameters::loadFrom(&settings_); + ui_.originalFolder->setText(parameters.originalFolder); + ui_.alteredFolder->setText(parameters.alteredFolder); + ui_.outputFile->setText(parameters.outputFile); + ui_.advanceTime->setValue(parameters.advanceTimeMSecs); + ui_.viewingTime->setValue(parameters.viewingTimeSecs); + ui_.blankingTime->setValue(parameters.blankingTimeMSecs); + ui_.grayFlickering->setChecked(parameters.gray); + ui_.grayFadingTime->setValue(parameters.grayFadingTimeMSecs); + ui_.grayTime->setValue(parameters.grayTimeMSecs); + ui_.intensityTarget->setValue(parameters.intensityTarget); + ui_.spacing->setValue(parameters.spacing); + + QImage white(256, 256, QImage::Format_RGB32); + white.fill(Qt::white); + ui_.spacingDemo->setOriginalImage(white); + ui_.spacingDemo->setAlteredImage(white); + + connect(this, &QDialog::accepted, + [&] { this->parameters().saveTo(&settings_); }); +} + +FlickerTestParameters FlickerTestWizard::parameters() const { + FlickerTestParameters result; + result.originalFolder = ui_.originalFolder->text(); + result.alteredFolder = ui_.alteredFolder->text(); + result.outputFile = ui_.outputFile->text(); + result.advanceTimeMSecs = ui_.advanceTime->value(); + result.viewingTimeSecs = ui_.viewingTime->value(); + result.blankingTimeMSecs = ui_.blankingTime->value(); + result.gray = ui_.grayFlickering->isChecked(); + result.grayFadingTimeMSecs = ui_.grayFadingTime->value(); + result.grayTimeMSecs = ui_.grayTime->value(); + result.intensityTarget = ui_.intensityTarget->value(); + result.spacing = ui_.spacing->value(); + return result; +} + +void FlickerTestWizard::on_originalFolderBrowseButton_clicked() { + const QString path = QFileDialog::getExistingDirectory( + this, tr("Folder with original images"), ui_.originalFolder->text()); + if (!path.isEmpty()) { + ui_.originalFolder->setText(path); + } +} + +void FlickerTestWizard::on_alteredFolderBrowseButton_clicked() { + const QString path = QFileDialog::getExistingDirectory( + this, tr("Folder with altered images"), ui_.alteredFolder->text()); + if (!path.isEmpty()) { + ui_.alteredFolder->setText(path); + } +} + +void FlickerTestWizard::on_outputFileBrowseButton_clicked() { + // The overwrite check is disabled here because it is carried out in + // `validateCurrentPage` (called when the user clicks the "Next" button) so + // that it also applies to automatically-reloaded settings. + const QString path = QFileDialog::getSaveFileName( + this, tr("CSV file in which to save the results"), ui_.outputFile->text(), + tr("CSV files (*.csv)"), /*selectedFilter=*/nullptr, + QFileDialog::DontConfirmOverwrite); + if (!path.isEmpty()) { + ui_.outputFile->setText(path); + } +} + +void FlickerTestWizard::on_timingButtonBox_clicked( + QAbstractButton* const button) { + if (ui_.timingButtonBox->standardButton(button) == + QDialogButtonBox::RestoreDefaults) { + ui_.advanceTime->setValue(100); + ui_.viewingTime->setValue(4); + ui_.blankingTime->setValue(250); + ui_.grayFlickering->setChecked(false); + } +} + +void FlickerTestWizard::updateTotalGrayTime() { + ui_.totalGrayTimeLabel->setText( + tr("Total gray time: %L1 ms") + .arg(2 * ui_.grayFadingTime->value() + ui_.grayTime->value())); +} + +bool FlickerTestWizard::validateCurrentPage() { + if (currentPage() == ui_.pathsPage && QFile::exists(ui_.outputFile->text())) { + QMessageBox messageBox(this); + messageBox.setIcon(QMessageBox::Warning); + messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + messageBox.setWindowTitle(tr("Output file already exists")); + messageBox.setText(tr("The selected output file \"%1\" already exists.") + .arg(ui_.outputFile->text())); + messageBox.setInformativeText(tr("Do you wish to overwrite it?")); + if (messageBox.exec() == QMessageBox::Cancel) { + return false; + } + } else if (currentPage() == ui_.timesPage) { + if (ui_.grayFlickering->isChecked() && + 2 * ui_.grayFadingTime->value() + ui_.grayTime->value() > + ui_.advanceTime->value()) { + QMessageBox messageBox(this); + messageBox.setIcon(QMessageBox::Warning); + messageBox.setStandardButtons(QMessageBox::Ok); + messageBox.setWindowTitle(tr("Incompatible times selected")); + messageBox.setText( + tr("The total gray time is greater than the advance time.")); + messageBox.exec(); + return false; + } + } + return QWizard::validateCurrentPage(); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/flicker_test/setup.h b/media/libjxl/src/tools/flicker_test/setup.h new file mode 100644 index 0000000000..0da78d60c8 --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/setup.h @@ -0,0 +1,44 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_FLICKER_TEST_SETUP_H_ +#define TOOLS_FLICKER_TEST_SETUP_H_ + +#include <QWizard> + +#include "tools/flicker_test/parameters.h" +#include "tools/flicker_test/ui_setup.h" + +namespace jxl { + +class FlickerTestWizard : public QWizard { + Q_OBJECT + + public: + explicit FlickerTestWizard(QWidget* parent = nullptr); + ~FlickerTestWizard() override = default; + + FlickerTestParameters parameters() const; + + protected: + bool validateCurrentPage() override; + + private slots: + void on_originalFolderBrowseButton_clicked(); + void on_alteredFolderBrowseButton_clicked(); + void on_outputFileBrowseButton_clicked(); + + void on_timingButtonBox_clicked(QAbstractButton* button); + + void updateTotalGrayTime(); + + private: + Ui::FlickerTestWizard ui_; + QSettings settings_; +}; + +} // namespace jxl + +#endif // TOOLS_FLICKER_TEST_SETUP_H_ diff --git a/media/libjxl/src/tools/flicker_test/setup.ui b/media/libjxl/src/tools/flicker_test/setup.ui new file mode 100644 index 0000000000..055c7f750c --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/setup.ui @@ -0,0 +1,422 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <comment> + Copyright (c) the JPEG XL Project Authors. All rights reserved. + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. + </comment> + <class>FlickerTestWizard</class> + <widget class="QWizard" name="FlickerTestWizard"> + <property name="windowTitle"> + <string>New flicker test</string> + </property> + <property name="options"> + <set>QWizard::NoBackButtonOnStartPage</set> + </property> + <widget class="QWizardPage" name="pathsPage"> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="originalFolderPromptLabel"> + <property name="text"> + <string>Folder with the original images:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0"> + <item> + <widget class="QLineEdit" name="originalFolder"/> + </item> + <item> + <widget class="QToolButton" name="originalFolderBrowseButton"> + <property name="text"> + <string>Browse…</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="alteredFolderPromptLabel"> + <property name="text"> + <string>Folder with the altered images:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0"> + <item> + <widget class="QLineEdit" name="alteredFolder"/> + </item> + <item> + <widget class="QToolButton" name="alteredFolderBrowseButton"> + <property name="text"> + <string>Browse…</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="outputFilePromptLabel"> + <property name="text"> + <string>CSV file in which to save the results:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0"> + <item> + <widget class="QLineEdit" name="outputFile"/> + </item> + <item> + <widget class="QToolButton" name="outputFileBrowseButton"> + <property name="text"> + <string>Browse…</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWizardPage" name="timesPage"> + <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0,1"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,1"> + <item> + <layout class="QFormLayout" name="formLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="advanceTimePromptLabel"> + <property name="text"> + <string>Advance time:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="advanceTime"> + <property name="suffix"> + <string> ms</string> + </property> + <property name="minimum"> + <number>100</number> + </property> + <property name="maximum"> + <number>3000</number> + </property> + <property name="singleStep"> + <number>100</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="viewingTimePromptLabel"> + <property name="text"> + <string>Viewing time (t<sub>VIEW</sub>):</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="viewingTime"> + <property name="specialValueText"> + <string>no limit</string> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="minimum"> + <number>0</number> + </property> + <property name="maximum"> + <number>30</number> + </property> + <property name="value"> + <number>4</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="blankingTimePromptLabel"> + <property name="text"> + <string>Blanking time (t<sub>BLANK</sub>):</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="blankingTime"> + <property name="suffix"> + <string> ms</string> + </property> + <property name="minimum"> + <number>50</number> + </property> + <property name="maximum"> + <number>1000</number> + </property> + <property name="singleStep"> + <number>50</number> + </property> + <property name="value"> + <number>250</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="grayFlickering"> + <property name="title"> + <string>Gray flickering</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <layout class="QFormLayout" name="formLayout_4"> + <item row="0" column="0"> + <widget class="QLabel" name="grayFadingTimePromptLabel"> + <property name="text"> + <string>Fading time to and from gray:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="grayFadingTime"> + <property name="suffix"> + <string> ms</string> + </property> + <property name="maximum"> + <number>1000</number> + </property> + <property name="singleStep"> + <number>100</number> + </property> + <property name="value"> + <number>100</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="grayTimePromptLabel"> + <property name="text"> + <string>Time on gray:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="grayTime"> + <property name="suffix"> + <string> ms</string> + </property> + <property name="maximum"> + <number>1000</number> + </property> + <property name="singleStep"> + <number>100</number> + </property> + <property name="value"> + <number>300</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="totalGrayTimeLabel"> + <property name="text"> + <string>Total gray time: 500 ms</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="timingButtonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::RestoreDefaults</set> + </property> + <property name="centerButtons"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWizardPage" name="intensityTargetPage"> + <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="1,0,1"> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <layout class="QFormLayout" name="formLayout_5"> + <item row="0" column="0"> + <widget class="QLabel" name="intensityTargetPromptLabel"> + <property name="text"> + <string>Display peak luminance:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="intensityTarget"> + <property name="correctionMode"> + <enum>QAbstractSpinBox::CorrectToNearestValue</enum> + </property> + <property name="suffix"> + <string> cd/m²</string> + </property> + <property name="minimum"> + <number>20</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + <property name="stepType"> + <enum>QAbstractSpinBox::AdaptiveDecimalStepType</enum> + </property> + <property name="value"> + <number>250</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWizardPage" name="spacingPage"> + <layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0,0"> + <item> + <widget class="jxl::SplitView" name="spacingDemo" native="true"/> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </spacer> + </item> + <item> + <layout class="QFormLayout" name="formLayout_3"> + <item row="0" column="0"> + <widget class="QLabel" name="spacingPromptLabel"> + <property name="text"> + <string>Spacing between the images:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0"> + <item> + <widget class="QSlider" name="spacing"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>1000</number> + </property> + <property name="value"> + <number>50</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="spacingSpinBox"> + <property name="suffix"> + <string> px</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>1000</number> + </property> + <property name="value"> + <number>50</number> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + <customwidgets> + <customwidget> + <class>jxl::SplitView</class> + <extends>QWidget</extends> + <header>tools/flicker_test/split_view.h</header> + <container>1</container> + <slots> + <slot>setSpacing(int)</slot> + </slots> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>spacing</sender> + <signal>valueChanged(int)</signal> + <receiver>spacingDemo</receiver> + <slot>setSpacing(int)</slot> + </connection> + <connection> + <sender>spacing</sender> + <signal>valueChanged(int)</signal> + <receiver>spacingSpinBox</receiver> + <slot>setValue(int)</slot> + </connection> + <connection> + <sender>spacingSpinBox</sender> + <signal>valueChanged(int)</signal> + <receiver>spacing</receiver> + <slot>setValue(int)</slot> + </connection> + </connections> +</ui> diff --git a/media/libjxl/src/tools/flicker_test/split_view.cc b/media/libjxl/src/tools/flicker_test/split_view.cc new file mode 100644 index 0000000000..3455d70bd7 --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/split_view.cc @@ -0,0 +1,167 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/flicker_test/split_view.h" + +#include <QMouseEvent> +#include <QPainter> + +namespace jxl { + +SplitView::SplitView(QWidget* const parent) + : QWidget(parent), g_(std::random_device()()) { + blankingTimer_.setSingleShot(true); + blankingTimer_.setTimerType(Qt::PreciseTimer); + viewingTimer_.setSingleShot(true); + viewingTimer_.setTimerType(Qt::PreciseTimer); + flicker_.setLoopCount(-1); + connect(&blankingTimer_, &QTimer::timeout, this, &SplitView::startDisplaying); + connect(&flicker_, &QVariantAnimation::valueChanged, this, [&] { + if (gray_) { + update(); + } + }); + connect(&flicker_, &QAbstractAnimation::currentLoopChanged, [&] { + showingAltered_ = !showingAltered_; + update(); + }); + connect(&viewingTimer_, &QTimer::timeout, [&] { + flicker_.stop(); + original_.fill(Qt::black); + altered_.fill(Qt::black); + update(); + }); +} + +void SplitView::setOriginalImage(QImage image) { + original_ = QPixmap::fromImage(std::move(image)); + updateMinimumSize(); + update(); +} + +void SplitView::setAlteredImage(QImage image) { + altered_ = QPixmap::fromImage(std::move(image)); + updateMinimumSize(); + update(); +} + +void SplitView::setSpacing(int spacing) { + spacing_ = spacing; + updateMinimumSize(); + update(); +} + +void SplitView::startTest(QString imageName, const int blankingTimeMSecs, + const int viewingTimeSecs, const int advanceTimeMSecs, + const bool gray, const int grayFadingTimeMSecs, + const int grayTimeMSecs) { + imageName_ = std::move(imageName); + std::bernoulli_distribution bernoulli; + originalSide_ = bernoulli(g_) ? Side::kLeft : Side::kRight; + viewingTimer_.setInterval(1000 * viewingTimeSecs); + + flicker_.setDuration(advanceTimeMSecs); + gray_ = gray; + QVariantAnimation::KeyValues keyValues; + if (gray_) { + keyValues << QVariantAnimation::KeyValue(0., 0.f) + << QVariantAnimation::KeyValue( + static_cast<float>(grayFadingTimeMSecs) / advanceTimeMSecs, + 1.f) + << QVariantAnimation::KeyValue( + static_cast<float>(advanceTimeMSecs - grayTimeMSecs - + grayFadingTimeMSecs) / + advanceTimeMSecs, + 1.f) + << QVariantAnimation::KeyValue( + static_cast<float>(advanceTimeMSecs - grayTimeMSecs) / + advanceTimeMSecs, + 0.f) + << QVariantAnimation::KeyValue(1.f, 0.f); + } else { + keyValues << QVariantAnimation::KeyValue(0., 1.f) + << QVariantAnimation::KeyValue(1., 1.f); + } + flicker_.setKeyValues(keyValues); + + state_ = State::kBlanking; + blankingTimer_.start(blankingTimeMSecs); +} + +void SplitView::mousePressEvent(QMouseEvent* const event) { + if (state_ != State::kDisplaying) return; + + if (leftRect_.contains(event->pos())) { + clicking_ = true; + clickedSide_ = Side::kLeft; + } else if (rightRect_.contains(event->pos())) { + clicking_ = true; + clickedSide_ = Side::kRight; + } +} + +void SplitView::mouseReleaseEvent(QMouseEvent* const event) { + if (!clicking_) return; + clicking_ = false; + + const int clickDelayMSecs = viewingStartTime_.elapsed(); + + if ((clickedSide_ == Side::kLeft && !leftRect_.contains(event->pos())) || + (clickedSide_ == Side::kRight && !rightRect_.contains(event->pos()))) { + return; + } + + flicker_.stop(); + viewingTimer_.stop(); + state_ = State::kBlanking; + update(); + + emit testResult(imageName_, originalSide_, clickedSide_, clickDelayMSecs); +} + +void SplitView::paintEvent(QPaintEvent* const event) { + QPainter painter(this); + painter.fillRect(rect(), QColor(119, 119, 119)); + + if (state_ == State::kBlanking) return; + + if (gray_ && flicker_.state() == QAbstractAnimation::Running) { + painter.setOpacity(flicker_.currentValue().toFloat()); + } + + const auto imageForSide = [&](const Side side) { + if (side == originalSide_) return &original_; + return showingAltered_ ? &altered_ : &original_; + }; + + QPixmap* const leftImage = imageForSide(Side::kLeft); + QPixmap* const rightImage = imageForSide(Side::kRight); + + leftRect_ = leftImage->rect(); + leftRect_.moveCenter(rect().center()); + leftRect_.moveRight(rect().center().x() - spacing_ / 2 - spacing_ % 2); + painter.drawPixmap(leftRect_, *leftImage); + + rightRect_ = rightImage->rect(); + rightRect_.moveCenter(rect().center()); + rightRect_.moveLeft(rect().center().x() + 1 + spacing_ / 2); + painter.drawPixmap(rightRect_, *rightImage); +} + +void SplitView::startDisplaying() { + state_ = State::kDisplaying; + flicker_.start(); + viewingStartTime_.start(); + if (viewingTimer_.interval() > 0) { + viewingTimer_.start(); + } +} + +void SplitView::updateMinimumSize() { + setMinimumWidth(2 * std::max(original_.width(), altered_.width()) + spacing_); + setMinimumHeight(std::max(original_.height(), altered_.height())); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/flicker_test/split_view.h b/media/libjxl/src/tools/flicker_test/split_view.h new file mode 100644 index 0000000000..b4c7a1d8de --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/split_view.h @@ -0,0 +1,84 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_FLICKER_TEST_SPLIT_VIEW_H_ +#define TOOLS_FLICKER_TEST_SPLIT_VIEW_H_ + +#include <QElapsedTimer> +#include <QImage> +#include <QPixmap> +#include <QTimer> +#include <QVariantAnimation> +#include <QWidget> +#include <random> + +namespace jxl { + +class SplitView : public QWidget { + Q_OBJECT + + public: + enum class Side { + kLeft, + kRight, + }; + Q_ENUM(Side) + + explicit SplitView(QWidget* parent = nullptr); + ~SplitView() override = default; + + void setOriginalImage(QImage image); + void setAlteredImage(QImage image); + + signals: + void testResult(const QString& imageName, Side flickeringSide, + Side clickedSide, int clickDelayMSecs); + + public slots: + void setSpacing(int spacing); + void startTest(QString imageName, int blankingTimeMSecs, int viewingTimeSecs, + int advanceTimeMSecs, bool gray, int grayFadingTimeMSecs, + int grayTimeMSecs); + + protected: + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void paintEvent(QPaintEvent* event) override; + + private slots: + void startDisplaying(); + + private: + enum class State { + kBlanking, + kDisplaying, + }; + + void updateMinimumSize(); + + int spacing_ = 50; + + std::mt19937 g_; + + QString imageName_; + QPixmap original_, altered_; + Side originalSide_; + bool clicking_ = false; + Side clickedSide_; + QRect leftRect_, rightRect_; + State state_ = State::kDisplaying; + bool gray_ = false; + QTimer blankingTimer_; + QTimer viewingTimer_; + // Throughout each cycle, animates the opacity of the image being displayed + // between 0 and 1 if fading to gray is enabled. + QVariantAnimation flicker_; + bool showingAltered_ = true; + QElapsedTimer viewingStartTime_; +}; + +} // namespace jxl + +#endif // TOOLS_FLICKER_TEST_SPLIT_VIEW_H_ diff --git a/media/libjxl/src/tools/flicker_test/test_window.cc b/media/libjxl/src/tools/flicker_test/test_window.cc new file mode 100644 index 0000000000..f3827c56d0 --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/test_window.cc @@ -0,0 +1,184 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/flicker_test/test_window.h" + +#include <QDir> +#include <QMessageBox> +#include <QSet> +#include <algorithm> +#include <random> + +#include "tools/icc_detect/icc_detect.h" + +namespace jxl { + +FlickerTestWindow::FlickerTestWindow(FlickerTestParameters parameters, + QWidget* const parent) + : QMainWindow(parent), + monitorProfile_(GetMonitorIccProfile(this)), + parameters_(std::move(parameters)), + originalFolder_(parameters_.originalFolder, "*.png"), + alteredFolder_(parameters_.alteredFolder, "*.png"), + outputFile_(parameters_.outputFile) { + ui_.setupUi(this); + ui_.splitView->setSpacing(parameters_.spacing); + ui_.endLabel->setText( + tr("The test is complete and the results have been saved to \"%1\".") + .arg(parameters_.outputFile)); + connect(ui_.startButton, &QAbstractButton::clicked, [&] { + ui_.stackedView->setCurrentWidget(ui_.splitView); + nextImage(); + }); + connect(ui_.splitView, &SplitView::testResult, this, + &FlickerTestWindow::processTestResult); + + if (!outputFile_.open(QIODevice::WriteOnly)) { + QMessageBox messageBox; + messageBox.setIcon(QMessageBox::Critical); + messageBox.setStandardButtons(QMessageBox::Close); + messageBox.setWindowTitle(tr("Failed to open output file")); + messageBox.setInformativeText( + tr("Could not open \"%1\" for writing.").arg(outputFile_.fileName())); + messageBox.exec(); + proceed_ = false; + return; + } + outputStream_.setDevice(&outputFile_); + outputStream_ << "image name,original side,clicked side,click delay (ms)\n"; + + if (monitorProfile_.isEmpty()) { + QMessageBox messageBox; + messageBox.setIcon(QMessageBox::Warning); + messageBox.setStandardButtons(QMessageBox::Ok); + messageBox.setWindowTitle(tr("No monitor profile found")); + messageBox.setText( + tr("No ICC profile appears to be associated with the display. It will " + "be assumed to match sRGB.")); + messageBox.exec(); + } + + originalFolder_.setFilter(QDir::Files); + alteredFolder_.setFilter(QDir::Files); + +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + auto originalImages = QSet<QString>::fromList(originalFolder_.entryList()); + auto alteredImages = QSet<QString>::fromList(alteredFolder_.entryList()); +#else + const QStringList originalFolderEntries = originalFolder_.entryList(); + QSet<QString> originalImages(originalFolderEntries.begin(), + originalFolderEntries.end()); + const QStringList alteredFolderEntries = alteredFolder_.entryList(); + QSet<QString> alteredImages(alteredFolderEntries.begin(), + alteredFolderEntries.end()); +#endif + + auto onlyOriginal = originalImages - alteredImages, + onlyAltered = alteredImages - originalImages; + if (!onlyOriginal.isEmpty() || !onlyAltered.isEmpty()) { + QMessageBox messageBox; + messageBox.setIcon(QMessageBox::Warning); + messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + messageBox.setWindowTitle(tr("Image set mismatch")); + messageBox.setText( + tr("A mismatch has been detected between the original and altered " + "images.")); + messageBox.setInformativeText(tr("Proceed with the test?")); + QStringList detailedTextParagraphs; + const QString itemFormat = tr("— %1\n"); + if (!onlyOriginal.isEmpty()) { + QString originalList; + for (const QString& original : onlyOriginal) { + originalList += itemFormat.arg(original); + } + detailedTextParagraphs << tr("The following images were only found in " + "the originals folder:\n%1") + .arg(originalList); + } + if (!onlyAltered.isEmpty()) { + QString alteredList; + for (const QString& altered : onlyAltered) { + alteredList += itemFormat.arg(altered); + } + detailedTextParagraphs << tr("The following images were only found in " + "the altered images folder:\n%1") + .arg(alteredList); + } + messageBox.setDetailedText(detailedTextParagraphs.join("\n\n")); + if (messageBox.exec() == QMessageBox::Cancel) { + proceed_ = false; + return; + } + } + + remainingImages_ = originalImages.intersect(alteredImages).values(); + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(remainingImages_.begin(), remainingImages_.end(), g); +} + +void FlickerTestWindow::processTestResult(const QString& imageName, + const SplitView::Side originalSide, + const SplitView::Side clickedSide, + const int clickDelayMSecs) { + const auto sideToString = [](const SplitView::Side side) { + switch (side) { + case SplitView::Side::kLeft: + return "left"; + + case SplitView::Side::kRight: + return "right"; + } + return "unknown"; + }; + outputStream_ << imageName << "," << sideToString(originalSide) << "," + << sideToString(clickedSide) << "," << clickDelayMSecs << "\n"; + + nextImage(); +} + +void FlickerTestWindow::nextImage() { + if (remainingImages_.empty()) { + outputStream_.flush(); + ui_.stackedView->setCurrentWidget(ui_.finalPage); + return; + } + const QString image = remainingImages_.takeFirst(); +retry: + QImage originalImage = + loadImage(originalFolder_.absoluteFilePath(image), monitorProfile_, + parameters_.intensityTarget); + QImage alteredImage = loadImage(alteredFolder_.absoluteFilePath(image), + monitorProfile_, parameters_.intensityTarget); + if (originalImage.isNull() || alteredImage.isNull()) { + QMessageBox messageBox(this); + messageBox.setIcon(QMessageBox::Warning); + messageBox.setStandardButtons(QMessageBox::Retry | QMessageBox::Ignore | + QMessageBox::Abort); + messageBox.setWindowTitle(tr("Failed to load image")); + messageBox.setText(tr("Could not load image \"%1\".").arg(image)); + switch (messageBox.exec()) { + case QMessageBox::Retry: + goto retry; + + case QMessageBox::Ignore: + outputStream_ << image << ",,,\n"; + return nextImage(); + + case QMessageBox::Abort: + ui_.stackedView->setCurrentWidget(ui_.finalPage); + return; + } + } + + ui_.splitView->setOriginalImage(std::move(originalImage)); + ui_.splitView->setAlteredImage(std::move(alteredImage)); + ui_.splitView->startTest( + image, parameters_.blankingTimeMSecs, parameters_.viewingTimeSecs, + parameters_.advanceTimeMSecs, parameters_.gray, + parameters_.grayFadingTimeMSecs, parameters_.grayTimeMSecs); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/flicker_test/test_window.h b/media/libjxl/src/tools/flicker_test/test_window.h new file mode 100644 index 0000000000..1dfe5fca8b --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/test_window.h @@ -0,0 +1,50 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_FLICKER_TEST_TEST_WINDOW_H_ +#define TOOLS_FLICKER_TEST_TEST_WINDOW_H_ + +#include <QByteArray> +#include <QDir> +#include <QMainWindow> +#include <QStringList> +#include <QTextStream> + +#include "tools/comparison_viewer/image_loading.h" +#include "tools/flicker_test/parameters.h" +#include "tools/flicker_test/ui_test_window.h" + +namespace jxl { + +class FlickerTestWindow : public QMainWindow { + Q_OBJECT + + public: + explicit FlickerTestWindow(FlickerTestParameters parameters, + QWidget* parent = nullptr); + ~FlickerTestWindow() override = default; + + bool proceedWithTest() const { return proceed_; } + + private slots: + void processTestResult(const QString& imageName, SplitView::Side originalSide, + SplitView::Side clickedSide, int clickDelayMSecs); + + private: + void nextImage(); + + Ui::FlickerTestWindow ui_; + bool proceed_ = true; + const QByteArray monitorProfile_; + FlickerTestParameters parameters_; + QDir originalFolder_, alteredFolder_; + QFile outputFile_; + QTextStream outputStream_; + QStringList remainingImages_; +}; + +} // namespace jxl + +#endif // TOOLS_FLICKER_TEST_TEST_WINDOW_H_ diff --git a/media/libjxl/src/tools/flicker_test/test_window.ui b/media/libjxl/src/tools/flicker_test/test_window.ui new file mode 100644 index 0000000000..7eb26196fe --- /dev/null +++ b/media/libjxl/src/tools/flicker_test/test_window.ui @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <comment> + Copyright (c) the JPEG XL Project Authors. All rights reserved. + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. + </comment> + <class>FlickerTestWindow</class> + <widget class="QMainWindow" name="FlickerTestWindow"> + <property name="windowTitle"> + <string>Flicker test</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QStackedWidget" name="stackedView"> + <widget class="QWidget" name="startPage"> + <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,1"> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,1"> + <item> + <spacer name="spacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="startButton"> + <property name="text"> + <string>Start</string> + </property> + </widget> + </item> + <item> + <spacer name="spacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="jxl::SplitView" name="splitView"/> + <widget class="QWidget" name="finalPage"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0,1"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="endLabel"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignJustify|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </widget> + <customwidgets> + <customwidget> + <class>jxl::SplitView</class> + <extends>QWidget</extends> + <header>tools/flicker_test/split_view.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/media/libjxl/src/tools/fuzzer_corpus.cc b/media/libjxl/src/tools/fuzzer_corpus.cc new file mode 100644 index 0000000000..0d66bd816e --- /dev/null +++ b/media/libjxl/src/tools/fuzzer_corpus.cc @@ -0,0 +1,473 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#if defined(_WIN32) || defined(_WIN64) +#include "third_party/dirent.h" +#else +#include <dirent.h> +#include <unistd.h> +#endif + +#include <algorithm> +#include <functional> +#include <iostream> +#include <mutex> +#include <random> +#include <vector> + +#if JPEGXL_ENABLE_JPEG +#include "lib/extras/enc/jpg.h" +#endif +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/encode_internal.h" +#include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "lib/jxl/modular/encoding/context_predict.h" + +namespace { + +const size_t kMaxWidth = 50000; +const size_t kMaxHeight = 50000; +const size_t kMaxPixels = 20 * (1 << 20); // 20 MP +const size_t kMaxBitDepth = 24; // The maximum reasonable bit depth supported. + +std::mutex stderr_mutex; + +typedef std::function<uint8_t()> PixelGenerator; + +// ImageSpec needs to be a packed struct to allow us to use the raw memory of +// the struct for hashing to create a consistent. +#pragma pack(push, 1) +struct ImageSpec { + bool Validate() const { + if (width > kMaxWidth || height > kMaxHeight || + width * height > kMaxPixels) { + return false; + } + if (bit_depth > kMaxBitDepth || bit_depth == 0) return false; + if (num_frames == 0) return false; + // JPEG doesn't support all formats, so reconstructible JPEG isn't always + // valid. + if (is_reconstructible_jpeg && (bit_depth != 8 || num_channels != 3 || + alpha_bit_depth != 0 || num_frames != 1)) + return false; + return true; + } + + friend std::ostream& operator<<(std::ostream& o, const ImageSpec& spec) { + o << "ImageSpec<" + << "size=" << spec.width << "x" << spec.height + << " * chan=" << spec.num_channels << " depth=" << spec.bit_depth + << " alpha=" << spec.alpha_bit_depth + << " (premult=" << spec.alpha_is_premultiplied + << ") x frames=" << spec.num_frames << " seed=" << spec.seed + << ", speed=" << static_cast<int>(spec.params.speed_tier) + << ", butteraugli=" << spec.params.butteraugli_distance + << ", modular_mode=" << spec.params.modular_mode + << ", lossy_palette=" << spec.params.lossy_palette + << ", noise=" << spec.params.noise << ", preview=" << spec.params.preview + << ", fuzzer_friendly=" << spec.fuzzer_friendly + << ", is_reconstructible_jpeg=" << spec.is_reconstructible_jpeg + << ", orientation=" << static_cast<int>(spec.orientation) << ">"; + return o; + } + + void SpecHash(uint8_t hash[16]) const { + const uint8_t* from = reinterpret_cast<const uint8_t*>(this); + std::seed_seq hasher(from, from + sizeof(*this)); + uint32_t* to = reinterpret_cast<uint32_t*>(hash); + hasher.generate(to, to + 4); + } + + uint64_t width = 256; + uint64_t height = 256; + // Number of channels *not* including alpha. + uint64_t num_channels = 3; + uint64_t bit_depth = 8; + // Bit depth for the alpha channel. A value of 0 means no alpha channel. + uint64_t alpha_bit_depth = 8; + int32_t alpha_is_premultiplied = false; + + // Whether the ANS fuzzer friendly setting is currently enabled. + uint32_t fuzzer_friendly = false; + + // Number of frames, all the frames will have the same size. + uint64_t num_frames = 1; + + // The seed for the PRNG. + uint32_t seed = 7777; + + // Flags used for compression. These are mapped to the CompressedParams. + struct CjxlParams { + float butteraugli_distance = 1.f; + // Must not use Weighted - see force_no_wp + jxl::Predictor modular_predictor = jxl::Predictor::Gradient; + jxl::ColorTransform color_transform = jxl::ColorTransform::kXYB; + jxl::SpeedTier speed_tier = jxl::SpeedTier::kTortoise; + bool modular_mode = false; + bool lossy_palette = false; + bool noise = false; + bool preview = false; + // CjxlParams is packed; re-add padding when sum of sizes of members is not + // multiple of 4. + // uint8_t padding_[0] = {}; + } params; + + uint32_t is_reconstructible_jpeg = false; + // Use 0xFFFFFFFF if any random spec is good; otherwise set the desired value. + uint32_t override_decoder_spec = 0xFFFFFFFF; + // Orientation. + uint8_t orientation = 0; + uint8_t padding_[3] = {}; +}; +#pragma pack(pop) +static_assert(sizeof(ImageSpec) % 4 == 0, "Add padding to ImageSpec."); + +bool GenerateFile(const char* output_dir, const ImageSpec& spec, + bool regenerate, bool quiet) { + // Compute a checksum of the ImageSpec to name the file. This is just to keep + // the output of this program repeatable. + uint8_t checksum[16]; + spec.SpecHash(checksum); + std::string hash_str(sizeof(checksum) * 2, ' '); + static const char* hex_chars = "0123456789abcdef"; + for (size_t i = 0; i < sizeof(checksum); i++) { + hash_str[2 * i] = hex_chars[checksum[i] >> 4]; + hash_str[2 * i + 1] = hex_chars[checksum[i] % 0x0f]; + } + std::string output_fn = std::string(output_dir) + "/" + hash_str + ".jxl"; + + // Don't regenerate files if they already exist on disk to speed-up + // consecutive calls when --regenerate is not used. + struct stat st; + if (!regenerate && stat(output_fn.c_str(), &st) == 0 && S_ISREG(st.st_mode)) { + return true; + } + + if (!quiet) { + std::unique_lock<std::mutex> lock(stderr_mutex); + std::cerr << "Generating " << spec << " as " << hash_str << std::endl; + } + + jxl::CodecInOut io; + if (spec.bit_depth == 32) { + io.metadata.m.SetFloat32Samples(); + } else { + io.metadata.m.SetUintSamples(spec.bit_depth); + } + io.metadata.m.SetAlphaBits(spec.alpha_bit_depth, spec.alpha_is_premultiplied); + io.metadata.m.orientation = spec.orientation; + io.dec_pixels = spec.width * spec.height; + io.frames.clear(); + io.frames.reserve(spec.num_frames); + + jxl::ColorEncoding c; + if (spec.num_channels == 1) { + c = jxl::ColorEncoding::LinearSRGB(true); + } else if (spec.num_channels == 3) { + c = jxl::ColorEncoding::SRGB(); + } + + uint8_t hash[16]; + spec.SpecHash(hash); + std::mt19937 mt(spec.seed); + + // Compress the image. + jxl::PaddedBytes compressed; + + std::uniform_int_distribution<> dis(1, 6); + PixelGenerator gen = [&]() -> uint8_t { return dis(mt); }; + + for (uint32_t frame = 0; frame < spec.num_frames; frame++) { + jxl::ImageBundle ib(&io.metadata.m); + const bool has_alpha = spec.alpha_bit_depth != 0; + const size_t bytes_per_sample = + jxl::DivCeil(io.metadata.m.bit_depth.bits_per_sample, 8); + const size_t bytes_per_pixel = + bytes_per_sample * + (io.metadata.m.color_encoding.Channels() + has_alpha); + const size_t row_size = spec.width * bytes_per_pixel; + std::vector<uint8_t> img_data(row_size * spec.height, 0); + for (size_t y = 0; y < spec.height; y++) { + size_t pos = row_size * y; + for (size_t x = 0; x < spec.width; x++) { + for (size_t b = 0; b < bytes_per_pixel; b++) { + img_data[pos++] = gen(); + } + } + } + + const jxl::Span<const uint8_t> span(img_data.data(), img_data.size()); + JXL_RETURN_IF_ERROR(ConvertFromExternal( + span, spec.width, spec.height, io.metadata.m.color_encoding, + bytes_per_pixel / bytes_per_sample, + /*alpha_is_premultiplied=*/spec.alpha_is_premultiplied, + io.metadata.m.bit_depth.bits_per_sample, JXL_LITTLE_ENDIAN, + false /* flipped_y */, nullptr, &ib, /*float_in=*/false, /*align=*/0)); + io.frames.push_back(std::move(ib)); + } + + jxl::CompressParams params; + params.speed_tier = spec.params.speed_tier; + +#if JPEGXL_ENABLE_JPEG + if (spec.is_reconstructible_jpeg) { + // If this image is supposed to be a reconstructible JPEG, collect the JPEG + // metadata and encode it in the beginning of the compressed bytes. + jxl::PaddedBytes jpeg_bytes; + JXL_RETURN_IF_ERROR(EncodeImageJPG( + &io, jxl::extras::JpegEncoder::kLibJpeg, /*quality=*/70, + jxl::YCbCrChromaSubsampling(), /*pool=*/nullptr, &jpeg_bytes)); + JXL_RETURN_IF_ERROR(jxl::jpeg::DecodeImageJPG( + jxl::Span<const uint8_t>(jpeg_bytes.data(), jpeg_bytes.size()), &io)); + jxl::PaddedBytes jpeg_data; + JXL_RETURN_IF_ERROR( + EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data, params)); + std::vector<uint8_t> header; + header.insert(header.end(), jxl::kContainerHeader, + jxl::kContainerHeader + sizeof(jxl::kContainerHeader)); + jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false, + &header); + header.insert(header.end(), jpeg_data.data(), + jpeg_data.data() + jpeg_data.size()); + jxl::AppendBoxHeader(jxl::MakeBoxType("jxlc"), 0, true, &header); + compressed.append(header); + } +#endif + + params.modular_mode = spec.params.modular_mode; + params.color_transform = spec.params.color_transform; + params.butteraugli_distance = spec.params.butteraugli_distance; + params.options.predictor = {spec.params.modular_predictor}; + params.lossy_palette = spec.params.lossy_palette; + if (spec.params.preview) params.preview = jxl::Override::kOn; + if (spec.params.noise) params.noise = jxl::Override::kOn; + + jxl::AuxOut aux_out; + jxl::PassesEncoderState passes_encoder_state; + // EncodeFile replaces output; pass a temporary storage for it. + jxl::PaddedBytes compressed_image; + bool ok = + jxl::EncodeFile(params, &io, &passes_encoder_state, &compressed_image, + jxl::GetJxlCms(), &aux_out, nullptr); + if (!ok) return false; + compressed.append(compressed_image); + + // Append 4 bytes with the flags used by djxl_fuzzer to select the decoding + // output. + std::uniform_int_distribution<> dis256(0, 255); + if (spec.override_decoder_spec == 0xFFFFFFFF) { + for (size_t i = 0; i < 4; ++i) compressed.push_back(dis256(mt)); + } else { + for (size_t i = 0; i < 4; ++i) { + compressed.push_back(spec.override_decoder_spec >> (8 * i)); + } + } + + if (!jxl::WriteFile(compressed, output_fn)) return 1; + if (!quiet) { + std::unique_lock<std::mutex> lock(stderr_mutex); + std::cerr << "Stored " << output_fn << " size: " << compressed.size() + << std::endl; + } + + return true; +} + +std::vector<ImageSpec::CjxlParams> CompressParamsList() { + std::vector<ImageSpec::CjxlParams> ret; + + { + ImageSpec::CjxlParams params; + params.butteraugli_distance = 1.5; + ret.push_back(params); + } + + { + // Lossless + ImageSpec::CjxlParams params; + params.modular_mode = true; + params.color_transform = jxl::ColorTransform::kNone; + params.butteraugli_distance = 0.f; + params.modular_predictor = {jxl::Predictor::Weighted}; + ret.push_back(params); + } + + return ret; +} + +void Usage() { + fprintf(stderr, + "Use: fuzzer_corpus [-r] [-q] [-j THREADS] [output_dir]\n" + "\n" + " -r Regenerate files if already exist.\n" + " -q Be quiet.\n" + " -j THREADS Number of parallel jobs to run.\n"); +} + +} // namespace + +int main(int argc, const char** argv) { + const char* dest_dir = nullptr; + bool regenerate = false; + bool quiet = false; + int num_threads = std::thread::hardware_concurrency(); + for (int optind = 1; optind < argc;) { + if (!strcmp(argv[optind], "-r")) { + regenerate = true; + optind++; + } else if (!strcmp(argv[optind], "-q")) { + quiet = true; + optind++; + } else if (!strcmp(argv[optind], "-j")) { + optind++; + if (optind < argc) { + num_threads = atoi(argv[optind++]); + } else { + fprintf(stderr, "-j needs an argument value.\n"); + Usage(); + return 1; + } + } else if (dest_dir == nullptr) { + dest_dir = argv[optind++]; + } else { + fprintf(stderr, "Unknown parameter: \"%s\".\n", argv[optind]); + Usage(); + return 1; + } + } + if (!dest_dir) { + dest_dir = "corpus"; + } + + struct stat st; + memset(&st, 0, sizeof(st)); + if (stat(dest_dir, &st) != 0 || !S_ISDIR(st.st_mode)) { + fprintf(stderr, "Output path \"%s\" is not a directory.\n", dest_dir); + Usage(); + return 1; + } + + // Create the corpus directory if doesn't already exist. + std::mt19937 mt(77777); + + std::vector<std::pair<uint32_t, uint32_t>> image_sizes = { + {8, 8}, + {32, 32}, + {128, 128}, + // Degenerated cases. + {10000, 1}, + {10000, 2}, + {1, 10000}, + {2, 10000}, + // Large case. + {555, 256}, + {257, 513}, + }; + const std::vector<ImageSpec::CjxlParams> params_list = CompressParamsList(); + + ImageSpec spec; + // The ans_fuzzer_friendly setting is not thread safe and therefore done in + // an outer loop. This determines whether to use fuzzer-friendly ANS encoding. + for (uint32_t fuzzer_friendly = 0; fuzzer_friendly < 2; ++fuzzer_friendly) { + jxl::SetANSFuzzerFriendly(fuzzer_friendly); + spec.fuzzer_friendly = fuzzer_friendly; + + std::vector<ImageSpec> specs; + for (auto img_size : image_sizes) { + spec.width = img_size.first; + spec.height = img_size.second; + for (uint32_t bit_depth : {1, 2, 8, 16}) { + spec.bit_depth = bit_depth; + for (uint32_t num_channels : {1, 3}) { + spec.num_channels = num_channels; + for (uint32_t alpha_bit_depth : {0, 8, 16}) { + spec.alpha_bit_depth = alpha_bit_depth; + if (bit_depth == 16 && alpha_bit_depth == 8) { + // This mode is not supported in CopyTo(). + continue; + } + for (uint32_t num_frames : {1, 3}) { + spec.num_frames = num_frames; + for (uint32_t preview : {0, 1}) { +#if JPEGXL_ENABLE_JPEG + for (bool reconstructible_jpeg : {false, true}) { + spec.is_reconstructible_jpeg = reconstructible_jpeg; +#else // JPEGXL_ENABLE_JPEG + spec.is_reconstructible_jpeg = false; +#endif // JPEGXL_ENABLE_JPEG + for (const auto& params : params_list) { + spec.params = params; + + spec.params.preview = preview; + if (alpha_bit_depth) { + spec.alpha_is_premultiplied = mt() % 2; + } + if (spec.width * spec.height > 1000) { + // Increase the encoder speed for larger images. + spec.params.speed_tier = jxl::SpeedTier::kWombat; + } + spec.seed = mt() % 777777; + // Pick the orientation at random. It is orthogonal to all + // other features. Valid values are 1 to 8. + spec.orientation = 1 + (mt() % 8); + if (!spec.Validate()) { + if (!quiet) { + std::cerr << "Skipping " << spec << std::endl; + } + } else { + specs.push_back(spec); + } + } +#if JPEGXL_ENABLE_JPEG + } +#endif // JPEGXL_ENABLE_JPEG + } + } + } + } + } + } + + specs.emplace_back(ImageSpec()); + specs.back().params.lossy_palette = true; + specs.back().override_decoder_spec = 0; + + specs.emplace_back(ImageSpec()); + specs.back().params.noise = true; + specs.back().override_decoder_spec = 0; + + jxl::ThreadPoolInternal pool{num_threads}; + if (!RunOnPool( + &pool, 0, specs.size(), jxl::ThreadPool::NoInit, + [&specs, dest_dir, regenerate, quiet](const uint32_t task, + size_t /* thread */) { + const ImageSpec& spec = specs[task]; + GenerateFile(dest_dir, spec, regenerate, quiet); + }, + "FuzzerCorpus")) { + std::cerr << "Error generating fuzzer corpus" << std::endl; + return 1; + } + } + std::cerr << "Finished generating fuzzer corpus" << std::endl; + return 0; +} diff --git a/media/libjxl/src/tools/fuzzer_stub.cc b/media/libjxl/src/tools/fuzzer_stub.cc new file mode 100644 index 0000000000..f984c00d48 --- /dev/null +++ b/media/libjxl/src/tools/fuzzer_stub.cc @@ -0,0 +1,45 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <fstream> +#include <iostream> +#include <iterator> +#include <vector> + +#include "jxl/thread_parallel_runner.h" +#include "jxl/thread_parallel_runner_cxx.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size); + +void ProcessInput(const char* filename) { + std::ifstream ifs(filename, std::ios::binary); + std::vector<char> contents((std::istreambuf_iterator<char>(ifs)), + std::istreambuf_iterator<char>()); + ifs.close(); + std::cout << "Processing " << filename << std::endl; + LLVMFuzzerTestOneInput(reinterpret_cast<uint8_t*>(contents.data()), + contents.size()); +} + +// Read files listed in args and pass their contents to "fuzzer". +int main(int argc, const char* argv[]) { + if (argc == 2) { + // No threaded runner for single inputs. + ProcessInput(argv[1]); + } else if (argc > 2) { + auto runner = JxlThreadParallelRunnerMake( + nullptr, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + return JxlThreadParallelRunner( + runner.get(), argv, + /* init= */ +[](void*, size_t) -> JxlParallelRetCode { return 0; }, + /* func= */ + +[](void* opaque, uint32_t value, size_t) { + const char** proc_argv = static_cast<const char**>(opaque); + ProcessInput(proc_argv[value]); + }, + /* start_range= */ 1, /* end_range= */ argc); + } + return 0; +} diff --git a/media/libjxl/src/tools/git_version.cmake b/media/libjxl/src/tools/git_version.cmake new file mode 100644 index 0000000000..4d216e8f57 --- /dev/null +++ b/media/libjxl/src/tools/git_version.cmake @@ -0,0 +1,34 @@ +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# git_version.cmake is a script which creates tools_version_git.h in the build +# directory if building from a git repository. +find_package(Git QUIET) + +# Check that this script was invoked with the necessary arguments. +if(NOT IS_DIRECTORY "${JPEGXL_ROOT_DIR}") + message(FATAL_ERROR "JPEGXL_ROOT_DIR is invalid") +endif() + +execute_process( + COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD + OUTPUT_VARIABLE GIT_REV + WORKING_DIRECTORY "${JPEGXL_ROOT_DIR}" + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + +# The define line in the file. +set(JPEGXL_VERSION_DEFINE "#define JPEGXL_VERSION \"${GIT_REV}\"\n") + +# Update the header file only if needed. +if(EXISTS "${DST}") + file(READ "${DST}" ORIG_DST) + if(NOT ORIG_DST STREQUAL JPEGXL_VERSION_DEFINE) + message(STATUS "Changing JPEGXL_VERSION to ${GIT_REV}") + file(WRITE "${DST}" "${JPEGXL_VERSION_DEFINE}") + endif() +else() + file(WRITE "${DST}" "${JPEGXL_VERSION_DEFINE}") +endif() diff --git a/media/libjxl/src/tools/hdr/README.md b/media/libjxl/src/tools/hdr/README.md new file mode 100644 index 0000000000..227b22b3e4 --- /dev/null +++ b/media/libjxl/src/tools/hdr/README.md @@ -0,0 +1,137 @@ +# HDR tools + +This directory contains a small set of command-line tools for HDR conversions, +including to SDR. + +## Tone mapping + +`tools/tone_map` implements tone mapping as described in annex 5 of +[Report ITU-R BT.2408-4](https://www.itu.int/pub/R-REP-BT.2408-4-2021), more +specifically the YRGB variant. Since the result may contain out-of-gamut colors, +it additionally does very basic gamut mapping. The balance between preserving +saturation and preserving luminance can be controlled by passing a number +between 0 and 1 using `--preserve_saturation`. The default is 0.1. Hue is never +sacrificed. + +### Examples + +```shell +# Tone maps a PQ image for a 300 cd/m² display, and writes the result as an SDR +# (but still wide-gamut) image to be shown on such a display. +$ tools/tone_map -t 300 ClassE_507.png ClassE_507_tone_mapped_300.png + +# The result can also be written as a PQ image itself: +$ tools/tone_map -t 300 --pq ClassE_507.png ClassE_507_tone_mapped_300_pq.png + +# It is possible to specify the maximum luminance found in the image using +# `--max_nits`. For OpenEXR input, it will override the `whiteLuminance` tag +# which indicates the luminance of (1, 1, 1). For PQ, it will not affect the +# luminance calculated from the signal, but it will tell the tone mapping how +# much headroom to leave for highlights. +$ tools/tone_map -m 4000 -t 300 ClassE_507.png ClassE_507_tone_mapped_300.png +``` + +## PQ to HLG conversion + +`tools/pq_to_hlg` performs conversion of a PQ image to HLG as described in +section 6 of the aforementioned BT.2408-4. That is, the PQ image is first +limited to 1000 cd/m² using the tone mapping mentioned above, and the result is +treated as if it were the output of a reference 1000 cd/m² HLG display: such a +display would have a system gamma of 1.2, and therefore, we can apply the +HLG inverse OOTF with a gamma of 1.2 to get “back” to the linear scene-referred +signal that would have produced that output on that reference display (and then +encode it using the OETF). + +As with the tone mapping tool, the `--max_nits` and `--preserve_saturation` +options can be used to guide the 1000 cd/m² limiting. + +### Example + +```shell +$ tools/pq_to_hlg ClassE_507.png ClassE_507_hlg.png +``` + +## HLG rendering + +HLG is designed to look acceptable without specific processing on displays that +expect a “traditional” SDR signal. Nevertheless, it is possible to optimize the +appearance for specific viewing conditions by applying the HLG inverse OETF and +then the OOTF with an appropriate system gamma. Here, the system gamma is +computed using the extended model mentioned at the bottom of page 29 of +[Report ITU-R BT.2390-9](https://www.itu.int/pub/R-REP-BT.2390-9-2021). That +formula should work well over a wide range of display peak luminances. + +It is possible to specify not just the peak luminance of the target display +(using `--target_nits`) but also the ambient luminance of the viewing +environment using `--surround_nits`. + +As with the tone mapping tool, the result can be written as a PQ image. In that +case, it would make sense, in further usage of `tools/tone_map` or +`tools/pq_to_hlg`, to set `--max_nits` to the value that was passed as +`--target_nits` to this tool. This also applies to the tone mapping tool. + +### Examples + +```shell +# Renders an HLG image for a 300 cd/m² display in a 10 cd/m² room. +$ tools/render_hlg -t 300 -s 10 ClassE_507_hlg.png ClassE_507_hlg_300.png + +# Renders it for a reference 1000 cd/m² display and writes the result as a PQ +# image. +$ tools/render_hlg -t 1000 --pq ClassE_507_hlg.png ClassE_507_hlg_pq.png + +# Informing pq_to_hlg about that maximum luminance then ensures proper +# roundtripping as it will not needlessly tone map the highlights. +$ tools/pq_to_hlg -m 1000 ClassE_507_hlg_pq.png ClassE_507_hlg_pq_hlg.png +``` + +## Display light to HLG + +By applying the inverse OOTF to a display-referred image, it is possible to +compute the scene light, and from there the HLG signal, that would have +produced that output on that display: + +```shell +$ tools/display_to_hlg -m 600 -s 5 srgb_input.png hlg_output.png +``` + +This is the mathematical inverse of `tools/render_hlg`. Furthermore, +`tools/pq_to_hlg` is equivalent to `tools/tone_map -t 1000` followed by +`tools/display_to_hlg -m 1000`. + +# LUT generation + +There are additionally two tools that can be used to generate look-up tables +for use with e.g. FFmpeg, ReShade, or DaVinci Resolve. + +The first of the two tools gives a starting point: + +```shell +$ tools/generate_lut_template --lut_size=64 identity.ppm +``` + +From there, one can apply a chain of per-pixel transforms (including other +LUTs) that the final LUT is intended to represent: + +```shell +$ tools/pq_to_hlg identity.ppm pq_to_hlg.ppm +$ tools/render_hlg -t 400 pq_to_hlg.ppm pq_to_400nit_rec2020.png +$ convert pq_to_400nit_rec2020.png -profile /usr/share/color/icc/colord/Rec709.icc pq_to_400nit_rec709.png +``` + +From there, the PNG image can be used as-is with ReShade’s “LUT” shader +(provided that the correct LUT size is set), or it can be converted to a +[Cube](https://wwwimages2.adobe.com/content/dam/acom/en/products/speedgrade/cc/pdfs/cube-lut-specification-1.0.pdf) +file for use in other software such as FFmpeg’s [lut3d](https://ffmpeg.org/ffmpeg-filters.html#lut3d-1) +filter: + +```shell +$ tools/texture_to_cube pq_to_400nit_rec709.png pq_to_400nit_rec709.cube +$ ffmpeg -i pq_video.mkv -vf lut3d=pq_to_400nit_rec709.cube -colorspace bt709 -color_primaries bt709 -color_trc bt709 400nit_rec709_video.mkv +``` + +Note: instead of converting to a standard color space such as Rec. 709, it is +also possible to convert to the color space of the specific display on which +the content is to be shown, in which case the transformed content does not need +any specific tagging and should be displayed directly without color management +(for example using `ffplay`). diff --git a/media/libjxl/src/tools/hdr/display_to_hlg.cc b/media/libjxl/src/tools/hdr/display_to_hlg.cc new file mode 100644 index 0000000000..a2caef28c3 --- /dev/null +++ b/media/libjxl/src/tools/hdr/display_to_hlg.cc @@ -0,0 +1,85 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <stdlib.h> + +#include "lib/extras/codec.h" +#include "lib/extras/hlg.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/enc_color_management.h" +#include "tools/args.h" +#include "tools/cmdline.h" + +int main(int argc, const char** argv) { + jxl::ThreadPoolInternal pool; + + jpegxl::tools::CommandLineParser parser; + float max_nits = 0; + auto max_nits_option = parser.AddOptionValue( + 'm', "max_nits", "nits", "maximum luminance of the display", &max_nits, + &jpegxl::tools::ParseFloat, 0); + float surround_nits = 5; + parser.AddOptionValue( + 's', "surround_nits", "nits", + "surround luminance of the viewing environment (default: 5)", + &surround_nits, &jpegxl::tools::ParseFloat, 0); + float preserve_saturation = .1f; + parser.AddOptionValue( + '\0', "preserve_saturation", "0..1", + "to what extent to try and preserve saturation over luminance if an " + "inverse gamma < 1 generates out-of-gamut colors", + &preserve_saturation, &jpegxl::tools::ParseFloat, 0); + const char* input_filename = nullptr; + auto input_filename_option = parser.AddPositionalOption( + "input", true, "input image", &input_filename, 0); + const char* output_filename = nullptr; + auto output_filename_option = parser.AddPositionalOption( + "output", true, "output image", &output_filename, 0); + + if (!parser.Parse(argc, argv)) { + fprintf(stderr, "See -h for help.\n"); + return EXIT_FAILURE; + } + + if (parser.HelpFlagPassed()) { + parser.PrintHelp(); + return EXIT_SUCCESS; + } + + if (!parser.GetOption(max_nits_option)->matched()) { + fprintf(stderr, + "Missing required argument --max_nits.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + if (!parser.GetOption(input_filename_option)->matched()) { + fprintf(stderr, "Missing input filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + if (!parser.GetOption(output_filename_option)->matched()) { + fprintf(stderr, "Missing output filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + + jxl::CodecInOut image; + JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image, + &pool)); + image.metadata.m.SetIntensityTarget(max_nits); + JXL_CHECK(jxl::HlgInverseOOTF( + &image.Main(), jxl::GetHlgGamma(max_nits, surround_nits), &pool)); + JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool)); + image.metadata.m.SetIntensityTarget(301); + + jxl::ColorEncoding hlg; + hlg.SetColorSpace(jxl::ColorSpace::kRGB); + hlg.primaries = jxl::Primaries::k2100; + hlg.white_point = jxl::WhitePoint::kD65; + hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG); + JXL_CHECK(hlg.CreateICC()); + JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool)); + image.metadata.m.color_encoding = hlg; + JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); +} diff --git a/media/libjxl/src/tools/hdr/generate_lut_template.cc b/media/libjxl/src/tools/hdr/generate_lut_template.cc new file mode 100644 index 0000000000..45fc27ab56 --- /dev/null +++ b/media/libjxl/src/tools/hdr/generate_lut_template.cc @@ -0,0 +1,59 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <stdlib.h> + +#include "lib/extras/codec.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "tools/args.h" +#include "tools/cmdline.h" + +int main(int argc, const char** argv) { + jxl::ThreadPoolInternal pool; + + jpegxl::tools::CommandLineParser parser; + size_t N = 64; + parser.AddOptionValue('N', "lut_size", "N", "linear size of the LUT", &N, + &jpegxl::tools::ParseUnsigned, 0); + const char* output_filename = nullptr; + auto output_filename_option = parser.AddPositionalOption( + "output", true, "output LUT", &output_filename, 0); + + if (!parser.Parse(argc, argv)) { + fprintf(stderr, "See -h for help.\n"); + return EXIT_FAILURE; + } + + if (parser.HelpFlagPassed()) { + parser.PrintHelp(); + return EXIT_SUCCESS; + } + + if (!parser.GetOption(output_filename_option)->matched()) { + fprintf(stderr, "Missing output filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + + jxl::Image3F image(N * N, N); + JXL_CHECK(jxl::RunOnPool( + &pool, 0, N, jxl::ThreadPool::NoInit, + [&](const uint32_t y, size_t /* thread */) { + const float g = static_cast<float>(y) / (N - 1); + float* const JXL_RESTRICT rows[3] = { + image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)}; + for (size_t x = 0; x < N * N; ++x) { + rows[0][x] = static_cast<float>(x % N) / (N - 1); + rows[1][x] = g; + rows[2][x] = static_cast<float>(x / N) / (N - 1); + } + }, + "GenerateTemplate")); + + jxl::CodecInOut output; + output.SetFromImage(std::move(image), jxl::ColorEncoding::SRGB()); + JXL_CHECK(jxl::EncodeToFile(output, jxl::ColorEncoding::SRGB(), 16, + output_filename, &pool)); +} diff --git a/media/libjxl/src/tools/hdr/pq_to_hlg.cc b/media/libjxl/src/tools/hdr/pq_to_hlg.cc new file mode 100644 index 0000000000..3b2125bf08 --- /dev/null +++ b/media/libjxl/src/tools/hdr/pq_to_hlg.cc @@ -0,0 +1,80 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <stdlib.h> + +#include "lib/extras/codec.h" +#include "lib/extras/hlg.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/enc_color_management.h" +#include "tools/args.h" +#include "tools/cmdline.h" + +int main(int argc, const char** argv) { + jxl::ThreadPoolInternal pool; + + jpegxl::tools::CommandLineParser parser; + float max_nits = 0; + parser.AddOptionValue('m', "max_nits", "nits", + "maximum luminance in the image", &max_nits, + &jpegxl::tools::ParseFloat, 0); + float preserve_saturation = .1f; + parser.AddOptionValue( + 's', "preserve_saturation", "0..1", + "to what extent to try and preserve saturation over luminance", + &preserve_saturation, &jpegxl::tools::ParseFloat, 0); + const char* input_filename = nullptr; + auto input_filename_option = parser.AddPositionalOption( + "input", true, "input image", &input_filename, 0); + const char* output_filename = nullptr; + auto output_filename_option = parser.AddPositionalOption( + "output", true, "output image", &output_filename, 0); + + if (!parser.Parse(argc, argv)) { + fprintf(stderr, "See -h for help.\n"); + return EXIT_FAILURE; + } + + if (parser.HelpFlagPassed()) { + parser.PrintHelp(); + return EXIT_SUCCESS; + } + + if (!parser.GetOption(input_filename_option)->matched()) { + fprintf(stderr, "Missing input filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + if (!parser.GetOption(output_filename_option)->matched()) { + fprintf(stderr, "Missing output filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + + jxl::CodecInOut image; + jxl::extras::ColorHints color_hints; + color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ"); + JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool)); + if (max_nits > 0) { + image.metadata.m.SetIntensityTarget(max_nits); + } + JXL_CHECK(jxl::ToneMapTo({0, 1000}, &image, &pool)); + JXL_CHECK(jxl::HlgInverseOOTF(&image.Main(), 1.2f, &pool)); + JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool)); + // Peak luminance at which the system gamma is 1, since we are now in scene + // light, having applied the inverse OOTF ourselves to control the subsequent + // gamut mapping instead of leaving it to JxlCms below. + image.metadata.m.SetIntensityTarget(301); + + jxl::ColorEncoding hlg; + hlg.SetColorSpace(jxl::ColorSpace::kRGB); + hlg.primaries = jxl::Primaries::k2100; + hlg.white_point = jxl::WhitePoint::kD65; + hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG); + JXL_CHECK(hlg.CreateICC()); + JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool)); + image.metadata.m.color_encoding = hlg; + JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); +} diff --git a/media/libjxl/src/tools/hdr/render_hlg.cc b/media/libjxl/src/tools/hdr/render_hlg.cc new file mode 100644 index 0000000000..c8a239550f --- /dev/null +++ b/media/libjxl/src/tools/hdr/render_hlg.cc @@ -0,0 +1,94 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <stdlib.h> + +#include "lib/extras/codec.h" +#include "lib/extras/hlg.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/enc_color_management.h" +#include "tools/args.h" +#include "tools/cmdline.h" + +int main(int argc, const char** argv) { + jxl::ThreadPoolInternal pool; + + jpegxl::tools::CommandLineParser parser; + float target_nits = 0; + auto target_nits_option = parser.AddOptionValue( + 't', "target_nits", "nits", "peak luminance of the target display", + &target_nits, &jpegxl::tools::ParseFloat, 0); + float surround_nits = 5; + parser.AddOptionValue( + 's', "surround_nits", "nits", + "surround luminance of the viewing environment (default: 5)", + &surround_nits, &jpegxl::tools::ParseFloat, 0); + float preserve_saturation = .1f; + parser.AddOptionValue( + '\0', "preserve_saturation", "0..1", + "to what extent to try and preserve saturation over luminance if a gamma " + "< 1 generates out-of-gamut colors", + &preserve_saturation, &jpegxl::tools::ParseFloat, 0); + bool pq = false; + parser.AddOptionFlag('p', "pq", + "write the output with absolute luminance using PQ", &pq, + &jpegxl::tools::SetBooleanTrue, 0); + const char* input_filename = nullptr; + auto input_filename_option = parser.AddPositionalOption( + "input", true, "input image", &input_filename, 0); + const char* output_filename = nullptr; + auto output_filename_option = parser.AddPositionalOption( + "output", true, "output image", &output_filename, 0); + + if (!parser.Parse(argc, argv)) { + fprintf(stderr, "See -h for help.\n"); + return EXIT_FAILURE; + } + + if (parser.HelpFlagPassed()) { + parser.PrintHelp(); + return EXIT_SUCCESS; + } + + if (!parser.GetOption(target_nits_option)->matched()) { + fprintf(stderr, + "Missing required argument --target_nits.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + if (!parser.GetOption(input_filename_option)->matched()) { + fprintf(stderr, "Missing input filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + if (!parser.GetOption(output_filename_option)->matched()) { + fprintf(stderr, "Missing output filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + + jxl::CodecInOut image; + jxl::extras::ColorHints color_hints; + color_hints.Add("color_space", "RGB_D65_202_Rel_HLG"); + JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool)); + // Ensures that conversions to linear by JxlCms will not apply the OOTF as we + // apply it ourselves to control the subsequent gamut mapping. + image.metadata.m.SetIntensityTarget(301); + const float gamma = jxl::GetHlgGamma(target_nits, surround_nits); + fprintf(stderr, "Using a system gamma of %g\n", gamma); + JXL_CHECK(jxl::HlgOOTF(&image.Main(), gamma, &pool)); + JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool)); + image.metadata.m.SetIntensityTarget(target_nits); + + jxl::ColorEncoding c_out = image.metadata.m.color_encoding; + if (pq) { + c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ); + } else { + c_out.tf.SetTransferFunction(jxl::TransferFunction::k709); + } + JXL_CHECK(c_out.CreateICC()); + JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool)); + image.metadata.m.color_encoding = c_out; + JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); +} diff --git a/media/libjxl/src/tools/hdr/texture_to_cube.cc b/media/libjxl/src/tools/hdr/texture_to_cube.cc new file mode 100644 index 0000000000..a5e5af788d --- /dev/null +++ b/media/libjxl/src/tools/hdr/texture_to_cube.cc @@ -0,0 +1,71 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <stdlib.h> + +#include "lib/extras/codec.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "tools/args.h" +#include "tools/cmdline.h" + +int main(int argc, const char** argv) { + jxl::ThreadPoolInternal pool; + + jpegxl::tools::CommandLineParser parser; + const char* input_filename = nullptr; + auto input_filename_option = parser.AddPositionalOption( + "input", true, "input image", &input_filename, 0); + const char* output_filename = nullptr; + auto output_filename_option = parser.AddPositionalOption( + "output", true, "output Cube LUT", &output_filename, 0); + + if (!parser.Parse(argc, argv)) { + fprintf(stderr, "See -h for help.\n"); + return EXIT_FAILURE; + } + + if (parser.HelpFlagPassed()) { + parser.PrintHelp(); + return EXIT_SUCCESS; + } + + if (!parser.GetOption(input_filename_option)->matched()) { + fprintf(stderr, "Missing input filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + if (!parser.GetOption(output_filename_option)->matched()) { + fprintf(stderr, "Missing output filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + + jxl::CodecInOut image; + JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image, + &pool)); + + JXL_CHECK(image.xsize() == image.ysize() * image.ysize()); + const unsigned N = image.ysize(); + + FILE* const output = fopen(output_filename, "wb"); + JXL_CHECK(output); + + fprintf(output, "# Created by libjxl\n"); + fprintf(output, "LUT_3D_SIZE %u\n", N); + fprintf(output, "DOMAIN_MIN 0.0 0.0 0.0\nDOMAIN_MAX 1.0 1.0 1.0\n\n"); + + for (size_t b = 0; b < N; ++b) { + for (size_t g = 0; g < N; ++g) { + const size_t y = g; + const float* const JXL_RESTRICT rows[3] = { + image.Main().color()->ConstPlaneRow(0, y) + N * b, + image.Main().color()->ConstPlaneRow(1, y) + N * b, + image.Main().color()->ConstPlaneRow(2, y) + N * b}; + for (size_t r = 0; r < N; ++r) { + const size_t x = r; + fprintf(output, "%.6f %.6f %.6f\n", rows[0][x], rows[1][x], rows[2][x]); + } + } + } +} diff --git a/media/libjxl/src/tools/hdr/tone_map.cc b/media/libjxl/src/tools/hdr/tone_map.cc new file mode 100644 index 0000000000..1ef3823c2c --- /dev/null +++ b/media/libjxl/src/tools/hdr/tone_map.cc @@ -0,0 +1,89 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <stdlib.h> + +#include "lib/extras/codec.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/enc_color_management.h" +#include "tools/args.h" +#include "tools/cmdline.h" + +int main(int argc, const char** argv) { + jxl::ThreadPoolInternal pool; + + jpegxl::tools::CommandLineParser parser; + float max_nits = 0; + parser.AddOptionValue('m', "max_nits", "nits", + "maximum luminance in the image", &max_nits, + &jpegxl::tools::ParseFloat, 0); + float target_nits = 0; + auto target_nits_option = parser.AddOptionValue( + 't', "target_nits", "nits", + "peak luminance of the display for which to tone map", &target_nits, + &jpegxl::tools::ParseFloat, 0); + float preserve_saturation = .1f; + parser.AddOptionValue( + 's', "preserve_saturation", "0..1", + "to what extent to try and preserve saturation over luminance", + &preserve_saturation, &jpegxl::tools::ParseFloat, 0); + bool pq = false; + parser.AddOptionFlag('p', "pq", + "write the output with absolute luminance using PQ", &pq, + &jpegxl::tools::SetBooleanTrue, 0); + const char* input_filename = nullptr; + auto input_filename_option = parser.AddPositionalOption( + "input", true, "input image", &input_filename, 0); + const char* output_filename = nullptr; + auto output_filename_option = parser.AddPositionalOption( + "output", true, "output image", &output_filename, 0); + + if (!parser.Parse(argc, argv)) { + fprintf(stderr, "See -h for help.\n"); + return EXIT_FAILURE; + } + + if (parser.HelpFlagPassed()) { + parser.PrintHelp(); + return EXIT_SUCCESS; + } + + if (!parser.GetOption(target_nits_option)->matched()) { + fprintf(stderr, + "Missing required argument --target_nits.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + if (!parser.GetOption(input_filename_option)->matched()) { + fprintf(stderr, "Missing input filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + if (!parser.GetOption(output_filename_option)->matched()) { + fprintf(stderr, "Missing output filename.\nSee -h for help.\n"); + return EXIT_FAILURE; + } + + jxl::CodecInOut image; + jxl::extras::ColorHints color_hints; + color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ"); + JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool)); + if (max_nits > 0) { + image.metadata.m.SetIntensityTarget(max_nits); + } + JXL_CHECK(jxl::ToneMapTo({0, target_nits}, &image, &pool)); + JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool)); + + jxl::ColorEncoding c_out = image.metadata.m.color_encoding; + if (pq) { + c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ); + } else { + c_out.tf.SetTransferFunction(jxl::TransferFunction::k709); + } + JXL_CHECK(c_out.CreateICC()); + JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool)); + image.metadata.m.color_encoding = c_out; + JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); +} diff --git a/media/libjxl/src/tools/icc_codec_fuzzer.cc b/media/libjxl/src/tools/icc_codec_fuzzer.cc new file mode 100644 index 0000000000..0af805c71a --- /dev/null +++ b/media/libjxl/src/tools/icc_codec_fuzzer.cc @@ -0,0 +1,68 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/enc_icc_codec.h" +#include "lib/jxl/icc_codec.h" + +namespace jxl { + +int TestOneInput(const uint8_t* data, size_t size) { +#if defined(JXL_ICC_FUZZER_ONLY_WRITE) + bool read = false; +#elif defined(JXL_ICC_FUZZER_ONLY_READ) + bool read = true; +#else + // Decide whether to test the reader or the writer (both use parsing) + if (!size) return 0; + bool read = data[0] == 0; + data++; + size--; +#endif + +#ifdef JXL_ICC_FUZZER_SLOW_TEST + // Including JPEG XL LZ77 and ANS compression. These are already fuzzed + // separately, so it is better to disable JXL_ICC_FUZZER_SLOW_TEST to focus on + // the ICC parsing. + if (read) { + // Reading parses the compressed format. + BitReader br(Span<const uint8_t>(data, size)); + PaddedBytes result; + (void)ReadICC(&br, &result); + (void)br.Close(); + } else { + // Writing parses the original ICC profile. + PaddedBytes icc; + icc.assign(data, data + size); + BitWriter writer; + AuxOut aux; + // Writing should support any random bytestream so must succeed, make + // fuzzer fail if not. + JXL_ASSERT(WriteICC(icc, &writer, 0, &aux)); + } +#else // JXL_ICC_FUZZER_SLOW_TEST + if (read) { + // Reading (unpredicting) parses the compressed format. + PaddedBytes result; + (void)UnpredictICC(data, size, &result); + } else { + // Writing (predicting) parses the original ICC profile. + PaddedBytes result; + // Writing should support any random bytestream so must succeed, make + // fuzzer fail if not. + JXL_ASSERT(PredictICC(data, size, &result)); + PaddedBytes reconstructed; + JXL_ASSERT(UnpredictICC(result.data(), result.size(), &reconstructed)); + JXL_ASSERT(reconstructed.size() == size); + JXL_ASSERT(memcmp(data, reconstructed.data(), size) == 0); + } +#endif // JXL_ICC_FUZZER_SLOW_TEST + return 0; +} + +} // namespace jxl + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return jxl::TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/icc_detect/icc_detect.h b/media/libjxl/src/tools/icc_detect/icc_detect.h new file mode 100644 index 0000000000..9335d94e73 --- /dev/null +++ b/media/libjxl/src/tools/icc_detect/icc_detect.h @@ -0,0 +1,19 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_ICC_DETECT_ICC_DETECT_H_ +#define TOOLS_ICC_DETECT_ICC_DETECT_H_ + +#include <QByteArray> +#include <QWidget> + +namespace jxl { + +// Should be cached if possible. +QByteArray GetMonitorIccProfile(const QWidget* widget); + +} // namespace jxl + +#endif // TOOLS_ICC_DETECT_ICC_DETECT_H_ diff --git a/media/libjxl/src/tools/icc_detect/icc_detect_empty.cc b/media/libjxl/src/tools/icc_detect/icc_detect_empty.cc new file mode 100644 index 0000000000..abd4a953fa --- /dev/null +++ b/media/libjxl/src/tools/icc_detect/icc_detect_empty.cc @@ -0,0 +1,14 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/icc_detect/icc_detect.h" + +namespace jxl { + +QByteArray GetMonitorIccProfile(const QWidget* const /*widget*/) { + return QByteArray(); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/icc_detect/icc_detect_win32.cc b/media/libjxl/src/tools/icc_detect/icc_detect_win32.cc new file mode 100644 index 0000000000..39ac5eef48 --- /dev/null +++ b/media/libjxl/src/tools/icc_detect/icc_detect_win32.cc @@ -0,0 +1,64 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/icc_detect/icc_detect.h" + +#include <windows.h> + +#include <memory> +#include <type_traits> + +namespace jxl { + +namespace { + +struct HandleDeleter { + void operator()(const HANDLE handle) const { + if (handle != INVALID_HANDLE_VALUE) { + CloseHandle(handle); + } + } +}; +using HandleUniquePtr = + std::unique_ptr<std::remove_pointer<HANDLE>::type, HandleDeleter>; + +} // namespace + +QByteArray GetMonitorIccProfile(const QWidget* const widget) { + const HWND window = reinterpret_cast<HWND>(widget->effectiveWinId()); + const HDC dc = GetDC(window); + wchar_t profile_path[MAX_PATH]; + DWORD profile_path_size = MAX_PATH; + if (!GetICMProfileW(dc, &profile_path_size, profile_path)) { + ReleaseDC(window, dc); + return QByteArray(); + } + ReleaseDC(window, dc); + HandleUniquePtr file(CreateFileW(profile_path, GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, + FILE_FLAG_SEQUENTIAL_SCAN, nullptr)); + if (file.get() == INVALID_HANDLE_VALUE) { + return QByteArray(); + } + LARGE_INTEGER profile_size; + if (!GetFileSizeEx(file.get(), &profile_size)) { + return QByteArray(); + } + HandleUniquePtr mapping( + CreateFileMappingW(file.get(), nullptr, PAGE_READONLY, 0, 0, nullptr)); + if (mapping == nullptr) { + return QByteArray(); + } + const char* const view = reinterpret_cast<const char*>( + MapViewOfFile(mapping.get(), FILE_MAP_READ, 0, 0, 0)); + if (view == nullptr) { + return QByteArray(); + } + QByteArray profile(view, profile_size.QuadPart); + UnmapViewOfFile(view); + return profile; +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/icc_detect/icc_detect_x11.cc b/media/libjxl/src/tools/icc_detect/icc_detect_x11.cc new file mode 100644 index 0000000000..be1209e387 --- /dev/null +++ b/media/libjxl/src/tools/icc_detect/icc_detect_x11.cc @@ -0,0 +1,77 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/icc_detect/icc_detect.h" + +#include <stdint.h> +#include <stdlib.h> +#include <xcb/xcb.h> + +#include <QX11Info> +#include <algorithm> +#include <memory> + +namespace jxl { + +namespace { + +constexpr char kIccProfileAtomName[] = "_ICC_PROFILE"; +constexpr uint32_t kMaxIccProfileSize = 1 << 24; + +struct FreeDeleter { + void operator()(void* const p) const { std::free(p); } +}; +template <typename T> +using XcbUniquePtr = std::unique_ptr<T, FreeDeleter>; + +} // namespace + +QByteArray GetMonitorIccProfile(const QWidget* const widget) { + Q_UNUSED(widget) + xcb_connection_t* const connection = QX11Info::connection(); + if (connection == nullptr) { + return QByteArray(); + } + const int screen_number = QX11Info::appScreen(); + + const xcb_intern_atom_cookie_t atomRequest = + xcb_intern_atom(connection, /*only_if_exists=*/1, + sizeof kIccProfileAtomName - 1, kIccProfileAtomName); + const XcbUniquePtr<xcb_intern_atom_reply_t> atomReply( + xcb_intern_atom_reply(connection, atomRequest, nullptr)); + if (atomReply == nullptr) { + return QByteArray(); + } + const xcb_atom_t iccProfileAtom = atomReply->atom; + + const xcb_screen_t* screen = nullptr; + int i = 0; + for (xcb_screen_iterator_t it = + xcb_setup_roots_iterator(xcb_get_setup(connection)); + it.rem; xcb_screen_next(&it)) { + if (i == screen_number) { + screen = it.data; + break; + } + ++i; + } + if (screen == nullptr) { + return QByteArray(); + } + const xcb_get_property_cookie_t profileRequest = xcb_get_property( + connection, /*_delete=*/0, screen->root, iccProfileAtom, + XCB_GET_PROPERTY_TYPE_ANY, /*long_offset=*/0, kMaxIccProfileSize); + const XcbUniquePtr<xcb_get_property_reply_t> profile( + xcb_get_property_reply(connection, profileRequest, nullptr)); + if (profile == nullptr || profile->bytes_after > 0) { + return QByteArray(); + } + + return QByteArray( + reinterpret_cast<const char*>(xcb_get_property_value(profile.get())), + xcb_get_property_value_length(profile.get())); +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java new file mode 100644 index 0000000000..440ef6edab --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java @@ -0,0 +1,39 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package org.jpeg.jpegxl.wrapper; + +import java.nio.Buffer; +import java.nio.ByteBuffer; + +/** JPEG XL JNI decoder wrapper. */ +public class Decoder { + /** Utility library, disable object construction. */ + private Decoder() {} + + /** One-shot decoding. */ + public static ImageData decode(Buffer data, PixelFormat pixelFormat) { + StreamInfo basicInfo = DecoderJni.getBasicInfo(data, pixelFormat); + if (basicInfo.status != Status.OK) { + throw new IllegalStateException("Decoding failed"); + } + if (basicInfo.width < 0 || basicInfo.height < 0 || basicInfo.pixelsSize < 0 + || basicInfo.iccSize < 0) { + throw new IllegalStateException("JNI has returned negative size"); + } + Buffer pixels = ByteBuffer.allocateDirect(basicInfo.pixelsSize); + Buffer icc = ByteBuffer.allocateDirect(basicInfo.iccSize); + Status status = DecoderJni.getPixels(data, pixels, icc, pixelFormat); + if (status != Status.OK) { + throw new IllegalStateException("Decoding failed"); + } + return new ImageData(basicInfo.width, basicInfo.height, pixels, icc, pixelFormat); + } + + // TODO(eustas): accept byte-array as input. + public static StreamInfo decodeInfo(Buffer data) { + return DecoderJni.getBasicInfo(data, null); + } +} diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java new file mode 100644 index 0000000000..7a2f2bf7ed --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java @@ -0,0 +1,73 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package org.jpeg.jpegxl.wrapper; + +import java.nio.Buffer; + +/** + * Low level JNI wrapper. + * + * This class is package-private, should be only be used by high level wrapper. + */ +class DecoderJni { + private static native void nativeGetBasicInfo(int[] context, Buffer data); + private static native void nativeGetPixels(int[] context, Buffer data, Buffer pixels, Buffer icc); + + static Status makeStatus(int statusCode) { + switch (statusCode) { + case 0: + return Status.OK; + case -1: + return Status.INVALID_STREAM; + case 1: + return Status.NOT_ENOUGH_INPUT; + default: + throw new IllegalStateException("Unknown status code"); + } + } + + static StreamInfo makeStreamInfo(int[] context) { + StreamInfo result = new StreamInfo(); + result.status = makeStatus(context[0]); + result.width = context[1]; + result.height = context[2]; + result.pixelsSize = context[3]; + result.iccSize = context[4]; + result.alphaBits = context[5]; + return result; + } + + /** Decode stream information. */ + static StreamInfo getBasicInfo(Buffer data, PixelFormat pixelFormat) { + if (!data.isDirect()) { + throw new IllegalArgumentException("data must be direct buffer"); + } + int[] context = new int[6]; + context[0] = (pixelFormat == null) ? -1 : pixelFormat.ordinal(); + nativeGetBasicInfo(context, data); + return makeStreamInfo(context); + } + + /** One-shot decoding. */ + static Status getPixels(Buffer data, Buffer pixels, Buffer icc, PixelFormat pixelFormat) { + if (!data.isDirect()) { + throw new IllegalArgumentException("data must be direct buffer"); + } + if (!pixels.isDirect()) { + throw new IllegalArgumentException("pixels must be direct buffer"); + } + if (!icc.isDirect()) { + throw new IllegalArgumentException("icc must be direct buffer"); + } + int[] context = new int[1]; + context[0] = pixelFormat.ordinal(); + nativeGetPixels(context, data, pixels, icc); + return makeStatus(context[0]); + } + + /** Utility library, disable object construction. */ + private DecoderJni() {} +} diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java new file mode 100644 index 0000000000..44f038c789 --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java @@ -0,0 +1,127 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package org.jpeg.jpegxl.wrapper; + +import java.nio.ByteBuffer; + +public class DecoderTest { + static { + String jniLibrary = System.getProperty("org.jpeg.jpegxl.wrapper.lib"); + if (jniLibrary != null) { + try { + System.load(new java.io.File(jniLibrary).getAbsolutePath()); + } catch (UnsatisfiedLinkError ex) { + String message = + "If the nested exception message says that some standard library (stdc++, tcmalloc, etc.) was not found, " + + "it is likely that JDK discovered by the build system overrides library search path. " + + "Try specifying a different JDK via JAVA_HOME environment variable and doing a clean build."; + throw new RuntimeException(message, ex); + } + } + } + + private static final int SIMPLE_IMAGE_DIM = 1024; + // Base64: "/wr6H0GRCAYBAGAASzgkunkeVbaSBu95EXDn0e7ABz2ShAMA" + private static final byte[] SIMPLE_IMAGE_BYTES = {-1, 10, -6, 31, 65, -111, 8, 6, 1, 0, 96, 0, 75, + 56, 36, -70, 121, 30, 85, -74, -110, 6, -17, 121, 17, 112, -25, -47, -18, -64, 7, 61, -110, + -124, 3, 0}; + + private static final int PIXEL_IMAGE_DIM = 1; + // Base64: "/woAELASCBAQABwASxLFgoUkDA==" + private static final byte[] PIXEL_IMAGE_BYTES = { + -1, 10, 0, 16, -80, 18, 8, 16, 16, 0, 28, 0, 75, 18, -59, -126, -123, 36, 12}; + + static ByteBuffer makeByteBuffer(byte[] src, int length) { + ByteBuffer buffer = ByteBuffer.allocateDirect(length); + buffer.put(src, 0, length); + return buffer; + } + + static ByteBuffer makeSimpleImage() { + return makeByteBuffer(SIMPLE_IMAGE_BYTES, SIMPLE_IMAGE_BYTES.length); + } + + static void checkSimpleImageData(ImageData imageData) { + if (imageData.width != SIMPLE_IMAGE_DIM) { + throw new IllegalStateException("invalid width"); + } + if (imageData.height != SIMPLE_IMAGE_DIM) { + throw new IllegalStateException("invalid height"); + } + int iccSize = imageData.icc.capacity(); + // Do not expect ICC profile to be some exact size; currently it is 732 + if (iccSize < 300 || iccSize > 1000) { + throw new IllegalStateException("unexpected ICC profile size"); + } + } + + static void checkPixelFormat(PixelFormat pixelFormat, int bytesPerPixel) { + ImageData imageData = Decoder.decode(makeSimpleImage(), pixelFormat); + checkSimpleImageData(imageData); + if (imageData.pixels.limit() != SIMPLE_IMAGE_DIM * SIMPLE_IMAGE_DIM * bytesPerPixel) { + throw new IllegalStateException("Unexpected pixels size"); + } + } + + static void testRgba() { + checkPixelFormat(PixelFormat.RGBA_8888, 4); + } + + static void testRgbaF16() { + checkPixelFormat(PixelFormat.RGBA_F16, 8); + } + + static void testRgb() { + checkPixelFormat(PixelFormat.RGB_888, 3); + } + + static void testRgbF16() { + checkPixelFormat(PixelFormat.RGB_F16, 6); + } + + static void checkGetInfo(ByteBuffer data, int dim, int alphaBits) { + StreamInfo streamInfo = Decoder.decodeInfo(data); + if (streamInfo.status != Status.OK) { + throw new IllegalStateException("Unexpected decoding error"); + } + if (streamInfo.width != dim || streamInfo.height != dim) { + throw new IllegalStateException("Invalid width / height"); + } + if (streamInfo.alphaBits != alphaBits) { + throw new IllegalStateException("Invalid alphaBits"); + } + } + + static void testGetInfoNoAlpha() { + checkGetInfo(makeSimpleImage(), SIMPLE_IMAGE_DIM, 0); + } + + static void testGetInfoAlpha() { + checkGetInfo(makeByteBuffer(PIXEL_IMAGE_BYTES, PIXEL_IMAGE_BYTES.length), PIXEL_IMAGE_DIM, 8); + } + + static void testNotEnoughInput() { + for (int i = 0; i < 6; ++i) { + ByteBuffer jxlData = makeByteBuffer(SIMPLE_IMAGE_BYTES, i); + StreamInfo streamInfo = Decoder.decodeInfo(jxlData); + if (streamInfo.status != Status.NOT_ENOUGH_INPUT) { + throw new IllegalStateException( + "Expected 'not enough input', but got " + streamInfo.status + " " + i); + } + } + } + + // Simple executable to avoid extra dependencies. + public static void main(String[] args) { + testRgba(); + testRgbaF16(); + testRgb(); + testRgbF16(); + testGetInfoNoAlpha(); + testGetInfoAlpha(); + testNotEnoughInput(); + } +} diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java new file mode 100644 index 0000000000..a449529a5a --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java @@ -0,0 +1,25 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package org.jpeg.jpegxl.wrapper; + +import java.nio.Buffer; + +/** POJO that contains necessary image data (dimensions, pixels,...). */ +public class ImageData { + final int width; + final int height; + final Buffer pixels; + final Buffer icc; + final PixelFormat pixelFormat; + + ImageData(int width, int height, Buffer pixels, Buffer icc, PixelFormat pixelFormat) { + this.width = width; + this.height = height; + this.pixels = pixels; + this.icc = icc; + this.pixelFormat = pixelFormat; + } +} diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java new file mode 100644 index 0000000000..5df1225740 --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java @@ -0,0 +1,13 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package org.jpeg.jpegxl.wrapper; + +public enum PixelFormat { + RGBA_8888, // 0 + RGBA_F16, // 1 + RGB_888, // 2 + RGB_F16 // 3 +} diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Status.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Status.java new file mode 100644 index 0000000000..a87206a166 --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Status.java @@ -0,0 +1,17 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package org.jpeg.jpegxl.wrapper; + +public enum Status { + /** Operation was successful. */ + OK, + + /** So far stream was valid, but incomplete. */ + NOT_ENOUGH_INPUT, + + /** Stream is corrupted. */ + INVALID_STREAM +} diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java new file mode 100644 index 0000000000..2419b37f23 --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java @@ -0,0 +1,18 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package org.jpeg.jpegxl.wrapper; + +/** POJO that wraps some fields of JxlBasicInfo. */ +public class StreamInfo { + public Status status; + public int width; + public int height; + public int alphaBits; + + // package-private + int pixelsSize; + int iccSize; +} diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc new file mode 100644 index 0000000000..1b3847e078 --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc @@ -0,0 +1,276 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h" + +#include <jni.h> + +#include <cstdlib> + +#include "jxl/decode.h" +#include "jxl/thread_parallel_runner.h" +#include "lib/jxl/base/status.h" + +namespace { + +template <typename From, typename To> +bool StaticCast(const From& from, To* to) { + To tmp = static_cast<To>(from); + // Check sign is preserved. + if ((from < 0 && tmp > 0) || (from > 0 && tmp < 0)) return false; + // Check value is preserved. + if (from != static_cast<From>(tmp)) return false; + *to = tmp; + return true; +} + +bool BufferToSpan(JNIEnv* env, jobject buffer, uint8_t** data, size_t* size) { + if (buffer == nullptr) return true; + + *data = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer)); + if (*data == nullptr) return false; + return StaticCast(env->GetDirectBufferCapacity(buffer), size); +} + +int ToStatusCode(const jxl::Status& status) { + if (status) return 0; + if (status.IsFatalError()) return -1; + return 1; // Non-fatal -> not enough input. +} + +constexpr const size_t kLastPixelFormat = 3; +constexpr const size_t kNoPixelFormat = static_cast<size_t>(-1); + +JxlPixelFormat ToPixelFormat(size_t pixel_format) { + if (pixel_format == 0) { + // RGBA, 4 x byte per pixel, no scanline padding. + return {/*num_channels=*/4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, /*align=*/0}; + } else if (pixel_format == 1) { + // RGBA, 4 x float16 per pixel, no scanline padding. + return {/*num_channels=*/4, JXL_TYPE_FLOAT16, JXL_LITTLE_ENDIAN, + /*align=*/0}; + } else if (pixel_format == 2) { + // RGB, 4 x byte per pixel, no scanline padding. + return {/*num_channels=*/3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, /*align=*/0}; + } else if (pixel_format == 3) { + // RGB, 4 x float16 per pixel, no scanline padding. + return {/*num_channels=*/3, JXL_TYPE_FLOAT16, JXL_LITTLE_ENDIAN, + /*align=*/0}; + } else { + abort(); + return {0, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + } +} + +jxl::Status DoDecode(JNIEnv* env, jobject data_buffer, size_t* info_pixels_size, + size_t* info_icc_size, JxlBasicInfo* info, + size_t pixel_format, jobject pixels_buffer, + jobject icc_buffer) { + if (data_buffer == nullptr) return JXL_FAILURE("No data buffer"); + + uint8_t* data = nullptr; + size_t data_size = 0; + if (!BufferToSpan(env, data_buffer, &data, &data_size)) { + return JXL_FAILURE("Failed to access data buffer"); + } + + uint8_t* pixels = nullptr; + size_t pixels_size = 0; + if (!BufferToSpan(env, pixels_buffer, &pixels, &pixels_size)) { + return JXL_FAILURE("Failed to access pixels buffer"); + } + + uint8_t* icc = nullptr; + size_t icc_size = 0; + if (!BufferToSpan(env, icc_buffer, &icc, &icc_size)) { + return JXL_FAILURE("Failed to access ICC buffer"); + } + + JxlDecoder* dec = JxlDecoderCreate(NULL); + + constexpr size_t kNumThreads = 0; // Do everything in this thread. + void* runner = JxlThreadParallelRunnerCreate(NULL, kNumThreads); + + struct Defer { + JxlDecoder* dec; + void* runner; + ~Defer() { + JxlThreadParallelRunnerDestroy(runner); + JxlDecoderDestroy(dec); + } + } defer{dec, runner}; + + auto status = + JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner); + if (status != JXL_DEC_SUCCESS) { + return JXL_FAILURE("Failed to set parallel runner"); + } + status = JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE | JXL_DEC_COLOR_ENCODING); + if (status != JXL_DEC_SUCCESS) { + return JXL_FAILURE("Failed to subscribe for events"); + } + status = JxlDecoderSetInput(dec, data, data_size); + if (status != JXL_DEC_SUCCESS) { + return JXL_FAILURE("Failed to set input"); + } + status = JxlDecoderProcessInput(dec); + if (status == JXL_DEC_NEED_MORE_INPUT) { + return JXL_STATUS(jxl::StatusCode::kNotEnoughBytes, "Not enough input"); + } + if (status != JXL_DEC_BASIC_INFO) { + return JXL_FAILURE("Unexpected notification (want: basic info)"); + } + if (info_pixels_size) { + JxlPixelFormat format = ToPixelFormat(pixel_format); + status = JxlDecoderImageOutBufferSize(dec, &format, info_pixels_size); + if (status != JXL_DEC_SUCCESS) { + return JXL_FAILURE("Failed to get pixels size"); + } + } + if (info) { + status = JxlDecoderGetBasicInfo(dec, info); + if (status != JXL_DEC_SUCCESS) { + return JXL_FAILURE("Failed to get basic info"); + } + } + status = JxlDecoderProcessInput(dec); + if (status != JXL_DEC_COLOR_ENCODING) { + return JXL_FAILURE("Unexpected notification (want: color encoding)"); + } + if (info_icc_size) { + JxlPixelFormat format = ToPixelFormat(pixel_format); + status = JxlDecoderGetICCProfileSize( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, info_icc_size); + if (status != JXL_DEC_SUCCESS) *info_icc_size = 0; + } + if (icc && icc_size > 0) { + JxlPixelFormat format = ToPixelFormat(pixel_format); + status = JxlDecoderGetColorAsICCProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, icc, icc_size); + if (status != JXL_DEC_SUCCESS) { + return JXL_FAILURE("Failed to get ICC"); + } + } + if (pixels) { + JxlPixelFormat format = ToPixelFormat(pixel_format); + status = JxlDecoderProcessInput(dec); + if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + return JXL_FAILURE("Unexpected notification (want: need out buffer)"); + } + status = JxlDecoderSetImageOutBuffer(dec, &format, pixels, pixels_size); + if (status != JXL_DEC_SUCCESS) { + return JXL_FAILURE("Failed to set out buffer"); + } + status = JxlDecoderProcessInput(dec); + if (status != JXL_DEC_FULL_IMAGE) { + return JXL_FAILURE("Unexpected notification (want: full image)"); + } + status = JxlDecoderProcessInput(dec); + if (status != JXL_DEC_SUCCESS) { + return JXL_FAILURE("Unexpected notification (want: success)"); + } + } + + return true; +} + +#undef FAILURE + +} // namespace + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL +Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetBasicInfo( + JNIEnv* env, jobject /*jobj*/, jintArray ctx, jobject data_buffer) { + jint context[6] = {0}; + env->GetIntArrayRegion(ctx, 0, 1, context); + + JxlBasicInfo info = {}; + size_t pixels_size = 0; + size_t icc_size = 0; + size_t pixel_format = 0; + + jxl::Status status = true; + + if (status) { + pixel_format = context[0]; + if (pixel_format == kNoPixelFormat) { + // OK + } else if (pixel_format > kLastPixelFormat) { + status = JXL_FAILURE("Unrecognized pixel format"); + } + } + + if (status) { + bool want_output_size = (pixel_format != kNoPixelFormat); + if (want_output_size) { + status = DoDecode( + env, data_buffer, &pixels_size, &icc_size, &info, pixel_format, + /* pixels_buffer= */ nullptr, /* icc_buffer= */ nullptr); + } else { + status = + DoDecode(env, data_buffer, /* info_pixels_size= */ nullptr, + /* info_icc_size= */ nullptr, &info, pixel_format, + /* pixels_buffer= */ nullptr, /* icc_buffer= */ nullptr); + } + } + + if (status) { + bool ok = true; + ok &= StaticCast(info.xsize, context + 1); + ok &= StaticCast(info.ysize, context + 2); + ok &= StaticCast(pixels_size, context + 3); + ok &= StaticCast(icc_size, context + 4); + ok &= StaticCast(info.alpha_bits, context + 5); + if (!ok) status = JXL_FAILURE("Invalid value"); + } + + context[0] = ToStatusCode(status); + + env->SetIntArrayRegion(ctx, 0, 6, context); +} + +/** + * Get image pixel data. + * + * @param ctx {out_status} tuple + * @param data [in] Buffer with encoded JXL stream + * @param pixels [out] Buffer to place pixels to + */ +JNIEXPORT void JNICALL Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetPixels( + JNIEnv* env, jobject /* jobj */, jintArray ctx, jobject data_buffer, + jobject pixels_buffer, jobject icc_buffer) { + jint context[1] = {0}; + env->GetIntArrayRegion(ctx, 0, 1, context); + + size_t pixel_format = 0; + + jxl::Status status = true; + + if (status) { + // Unlike getBasicInfo, "no-pixel-format" is not supported. + pixel_format = context[0]; + if (pixel_format > kLastPixelFormat) { + status = JXL_FAILURE("Unrecognized pixel format"); + } + } + + if (status) { + status = DoDecode(env, data_buffer, /* info_pixels_size= */ nullptr, + /* info_icc_size= */ nullptr, /* info= */ nullptr, + pixel_format, pixels_buffer, icc_buffer); + } + + context[0] = ToStatusCode(status); + env->SetIntArrayRegion(ctx, 0, 1, context); +} + +#ifdef __cplusplus +} +#endif diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h new file mode 100644 index 0000000000..8237fc95a2 --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h @@ -0,0 +1,43 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_JNI_ORG_JPEG_JPEGXL_WRAPPER_DECODER_JNI +#define TOOLS_JNI_ORG_JPEG_JPEGXL_WRAPPER_DECODER_JNI + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Get basic image information (size, etc.) + * + * @param ctx {in_pixel_format_out_status, out_width, out_height, pixels_size, + * icc_size} tuple + * @param data [in] Buffer with encoded JXL stream + */ +JNIEXPORT void JNICALL +Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetBasicInfo(JNIEnv* env, + jobject /*jobj*/, + jintArray ctx, + jobject data_buffer); + +/** + * Get image pixel data. + * + * @param ctx {in_pixel_format_out_status} tuple + * @param data [in] Buffer with encoded JXL stream + * @param pixels [out] Buffer to place pixels to + */ +JNIEXPORT void JNICALL Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetPixels( + JNIEnv* env, jobject /*jobj*/, jintArray ctx, jobject data_buffer, + jobject pixels_buffer, jobject icc_buffer); + +#ifdef __cplusplus +} +#endif + +#endif // TOOLS_JNI_ORG_JPEG_JPEGXL_WRAPPER_DECODER_JNI
\ No newline at end of file diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc new file mode 100644 index 0000000000..c5e6ba3e0f --- /dev/null +++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc @@ -0,0 +1,52 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <jni.h> + +#include "tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h" + +#ifdef __cplusplus +extern "C" { +#endif + +static char* kGetBasicInfoName = const_cast<char*>("nativeGetBasicInfo"); +static char* kGetBasicInfoSig = const_cast<char*>("([ILjava/nio/Buffer;)V"); +static char* kGetPixelsName = const_cast<char*>("nativeGetPixels"); +static char* kGetPixelsInfoSig = const_cast<char*>( + "([ILjava/nio/Buffer;Ljava/nio/Buffer;Ljava/nio/Buffer;)V"); + +#define JXL_JNI_METHOD(NAME) \ + (reinterpret_cast<void*>( \ + Java_org_jpeg_jpegxl_wrapper_DecoderJni_native##NAME)) + +static const JNINativeMethod kDecoderMethods[] = { + {kGetBasicInfoName, kGetBasicInfoSig, JXL_JNI_METHOD(GetBasicInfo)}, + {kGetPixelsName, kGetPixelsInfoSig, JXL_JNI_METHOD(GetPixels)}}; + +static const size_t kNumDecoderMethods = 2; + +#undef JXL_JNI_METHOD + +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + + jclass clazz = env->FindClass("org/jpeg/jpegxl/wrapper/DecoderJni"); + if (clazz == nullptr) { + return -1; + } + + if (env->RegisterNatives(clazz, kDecoderMethods, kNumDecoderMethods) < 0) { + return -1; + } + + return JNI_VERSION_1_6; +} + +#ifdef __cplusplus +} +#endif diff --git a/media/libjxl/src/tools/jxl_emcc.cc b/media/libjxl/src/tools/jxl_emcc.cc new file mode 100644 index 0000000000..1951b30a5a --- /dev/null +++ b/media/libjxl/src/tools/jxl_emcc.cc @@ -0,0 +1,66 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <cstring> + +#include "lib/extras/codec.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/enc_file.h" + +extern "C" { + +/* NOTA BENE: see file history to uncover how to decode HDR JPEGs to pixels. */ + +/** Result: uint32_t 'size' followed by compressed image (JXL). */ +uint8_t* jxlCompress(const uint8_t* data, size_t size) { + jxl::PaddedBytes compressed; + jxl::CodecInOut io; + jxl::extras::Codec input_codec; + if (!jxl::SetFromBytes(jxl::Span<const uint8_t>(data, size), &io, nullptr, + &input_codec)) { + return nullptr; + } + jxl::CompressParams params; + jxl::PassesEncoderState passes_encoder_state; + if (!jxl::EncodeFile(params, &io, &passes_encoder_state, &compressed, + jxl::GetJxlCms(), nullptr, nullptr)) { + return nullptr; + } + size_t compressed_size = compressed.size(); + uint8_t* result = reinterpret_cast<uint8_t*>(malloc(compressed_size + 4)); + uint32_t* meta = reinterpret_cast<uint32_t*>(result); + meta[0] = compressed_size; + memcpy(result + 4, compressed.data(), compressed_size); + return result; +} + +/** Result: uint32_t 'size' followed by decompressed image (JPG). */ +uint8_t* jxlDecompress(const uint8_t* data, size_t size) { + jxl::PaddedBytes decompressed; + jxl::CodecInOut io; + jxl::DecompressParams params; + if (!jxl::DecodeFile(params, jxl::Span<const uint8_t>(data, size), &io, + nullptr)) { + return nullptr; + } + io.use_sjpeg = false; + io.jpeg_quality = 100; + if (!jxl::Encode(io, jxl::extras::Codec::kJPG, io.Main().c_current(), 8, + &decompressed, nullptr)) { + return nullptr; + } + size_t decompressed_size = decompressed.size(); + uint8_t* result = reinterpret_cast<uint8_t*>(malloc(decompressed_size + 4)); + uint32_t* meta = reinterpret_cast<uint32_t*>(result); + meta[0] = decompressed_size; + memcpy(result + 4, decompressed.data(), decompressed_size); + return result; +} + +} // extern "C" diff --git a/media/libjxl/src/tools/jxl_from_tree.cc b/media/libjxl/src/tools/jxl_from_tree.cc new file mode 100644 index 0000000000..aa85ff88bb --- /dev/null +++ b/media/libjxl/src/tools/jxl_from_tree.cc @@ -0,0 +1,506 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <string.h> + +#include <fstream> +#include <iostream> +#include <unordered_map> + +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_frame.h" +#include "lib/jxl/enc_heuristics.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/encoding/enc_debug_tree.h" +#include "lib/jxl/modular/encoding/enc_ma.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/splines.h" + +namespace jxl { + +namespace { +struct SplineData { + int32_t quantization_adjustment = 1; + std::vector<Spline> splines; +}; + +Splines SplinesFromSplineData(const SplineData& spline_data, + const ColorCorrelationMap& cmap) { + std::vector<QuantizedSpline> quantized_splines; + std::vector<Spline::Point> starting_points; + quantized_splines.reserve(spline_data.splines.size()); + starting_points.reserve(spline_data.splines.size()); + for (const Spline& spline : spline_data.splines) { + JXL_CHECK(!spline.control_points.empty()); + quantized_splines.emplace_back(spline, spline_data.quantization_adjustment, + cmap.YtoXRatio(0), cmap.YtoBRatio(0)); + starting_points.push_back(spline.control_points.front()); + } + return Splines(spline_data.quantization_adjustment, + std::move(quantized_splines), std::move(starting_points)); +} + +template <typename F> +bool ParseNode(F& tok, Tree& tree, SplineData& spline_data, + CompressParams& cparams, size_t& W, size_t& H, CodecInOut& io, + int& have_next, int& x0, int& y0) { + static const std::unordered_map<std::string, int> property_map = { + {"c", 0}, + {"g", 1}, + {"y", 2}, + {"x", 3}, + {"|N|", 4}, + {"|W|", 5}, + {"N", 6}, + {"W", 7}, + {"W-WW-NW+NWW", 8}, + {"W+N-NW", 9}, + {"W-NW", 10}, + {"NW-N", 11}, + {"N-NE", 12}, + {"N-NN", 13}, + {"W-WW", 14}, + {"WGH", 15}, + {"PrevAbs", 16}, + {"Prev", 17}, + {"PrevAbsErr", 18}, + {"PrevErr", 19}, + {"PPrevAbs", 20}, + {"PPrev", 21}, + {"PPrevAbsErr", 22}, + {"PPrevErr", 23}, + }; + static const std::unordered_map<std::string, Predictor> predictor_map = { + {"Set", Predictor::Zero}, + {"W", Predictor::Left}, + {"N", Predictor::Top}, + {"AvgW+N", Predictor::Average0}, + {"Select", Predictor::Select}, + {"Gradient", Predictor::Gradient}, + {"Weighted", Predictor::Weighted}, + {"NE", Predictor::TopRight}, + {"NW", Predictor::TopLeft}, + {"WW", Predictor::LeftLeft}, + {"AvgW+NW", Predictor::Average1}, + {"AvgN+NW", Predictor::Average2}, + {"AvgN+NE", Predictor::Average3}, + {"AvgAll", Predictor::Average4}, + }; + auto t = tok(); + if (t == "if") { + // Decision node. + int p; + t = tok(); + if (!property_map.count(t)) { + fprintf(stderr, "Unexpected property: %s\n", t.c_str()); + return false; + } + p = property_map.at(t); + if ((t = tok()) != ">") { + fprintf(stderr, "Expected >, found %s\n", t.c_str()); + return false; + } + t = tok(); + size_t num = 0; + int split = std::stoi(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid splitval: %s\n", t.c_str()); + return false; + } + size_t pos = tree.size(); + tree.emplace_back(PropertyDecisionNode::Split(p, split, pos + 1)); + JXL_RETURN_IF_ERROR(ParseNode(tok, tree, spline_data, cparams, W, H, io, + have_next, x0, y0)); + tree[pos].rchild = tree.size(); + } else if (t == "-") { + // Leaf + t = tok(); + Predictor p; + if (!predictor_map.count(t)) { + fprintf(stderr, "Unexpected predictor: %s\n", t.c_str()); + return false; + } + p = predictor_map.at(t); + t = tok(); + bool subtract = false; + if (t == "-") { + subtract = true; + t = tok(); + } else if (t == "+") { + t = tok(); + } + size_t num = 0; + int offset = std::stoi(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid offset: %s\n", t.c_str()); + return false; + } + if (subtract) offset = -offset; + tree.emplace_back(PropertyDecisionNode::Leaf(p, offset)); + return true; + } else if (t == "Width") { + t = tok(); + size_t num = 0; + W = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid width: %s\n", t.c_str()); + return false; + } + } else if (t == "Height") { + t = tok(); + size_t num = 0; + H = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid height: %s\n", t.c_str()); + return false; + } + } else if (t == "/*") { + t = tok(); + while (t != "*/" && t != "") t = tok(); + } else if (t == "Squeeze") { + cparams.responsive = true; + } else if (t == "GroupShift") { + t = tok(); + size_t num = 0; + cparams.modular_group_size_shift = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid GroupShift: %s\n", t.c_str()); + return false; + } + } else if (t == "XYB") { + cparams.color_transform = ColorTransform::kXYB; + } else if (t == "CbYCr") { + cparams.color_transform = ColorTransform::kYCbCr; + } else if (t == "RCT") { + t = tok(); + size_t num = 0; + cparams.colorspace = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid RCT: %s\n", t.c_str()); + return false; + } + } else if (t == "Orientation") { + t = tok(); + size_t num = 0; + io.metadata.m.orientation = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid Orientation: %s\n", t.c_str()); + return false; + } + } else if (t == "Alpha") { + io.metadata.m.SetAlphaBits(io.metadata.m.bit_depth.bits_per_sample); + ImageF alpha(W, H); + io.frames[0].SetAlpha(std::move(alpha), false); + } else if (t == "Bitdepth") { + t = tok(); + size_t num = 0; + io.metadata.m.bit_depth.bits_per_sample = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid Bitdepth: %s\n", t.c_str()); + return false; + } + } else if (t == "FloatExpBits") { + t = tok(); + size_t num = 0; + io.metadata.m.bit_depth.floating_point_sample = true; + io.metadata.m.bit_depth.exponent_bits_per_sample = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid FloatExpBits: %s\n", t.c_str()); + return false; + } + } else if (t == "FramePos") { + t = tok(); + size_t num = 0; + x0 = std::stoi(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid FramePos x0: %s\n", t.c_str()); + return false; + } + t = tok(); + y0 = std::stoi(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid FramePos y0: %s\n", t.c_str()); + return false; + } + } else if (t == "NotLast") { + have_next = 1; + } else if (t == "Upsample") { + t = tok(); + size_t num = 0; + cparams.resampling = std::stoul(t, &num); + if (num != t.size() || + (cparams.resampling != 1 && cparams.resampling != 2 && + cparams.resampling != 4 && cparams.resampling != 8)) { + fprintf(stderr, "Invalid Upsample: %s\n", t.c_str()); + return false; + } + } else if (t == "Upsample_EC") { + t = tok(); + size_t num = 0; + cparams.ec_resampling = std::stoul(t, &num); + if (num != t.size() || + (cparams.ec_resampling != 1 && cparams.ec_resampling != 2 && + cparams.ec_resampling != 4 && cparams.ec_resampling != 8)) { + fprintf(stderr, "Invalid Upsample_EC: %s\n", t.c_str()); + return false; + } + } else if (t == "Animation") { + io.metadata.m.have_animation = true; + io.metadata.m.animation.tps_numerator = 1000; + io.metadata.m.animation.tps_denominator = 1; + io.frames[0].duration = 100; + } else if (t == "AnimationFPS") { + t = tok(); + size_t num = 0; + io.metadata.m.animation.tps_numerator = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid numerator: %s\n", t.c_str()); + return false; + } + t = tok(); + num = 0; + io.metadata.m.animation.tps_denominator = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid denominator: %s\n", t.c_str()); + return false; + } + } else if (t == "Duration") { + t = tok(); + size_t num = 0; + io.frames[0].duration = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid Duration: %s\n", t.c_str()); + return false; + } + } else if (t == "BlendMode") { + t = tok(); + if (t == "kAdd") { + io.frames[0].blendmode = BlendMode::kAdd; + } else if (t == "kReplace") { + io.frames[0].blendmode = BlendMode::kReplace; + } else if (t == "kBlend") { + io.frames[0].blendmode = BlendMode::kBlend; + } else if (t == "kAlphaWeightedAdd") { + io.frames[0].blendmode = BlendMode::kAlphaWeightedAdd; + } else if (t == "kMul") { + io.frames[0].blendmode = BlendMode::kMul; + } else { + fprintf(stderr, "Invalid BlendMode: %s\n", t.c_str()); + return false; + } + } else if (t == "SplineQuantizationAdjustment") { + t = tok(); + size_t num = 0; + spline_data.quantization_adjustment = std::stoul(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid SplineQuantizationAdjustment: %s\n", t.c_str()); + return false; + } + } else if (t == "Spline") { + Spline spline; + const auto ParseFloat = [&t, &tok](float& output) { + t = tok(); + size_t num = 0; + output = std::stof(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid spline data: %s\n", t.c_str()); + return false; + } + return true; + }; + for (auto& dct : spline.color_dct) { + for (float& coefficient : dct) { + JXL_RETURN_IF_ERROR(ParseFloat(coefficient)); + } + } + for (float& coefficient : spline.sigma_dct) { + JXL_RETURN_IF_ERROR(ParseFloat(coefficient)); + } + + while (true) { + t = tok(); + if (t == "EndSpline") break; + size_t num = 0; + Spline::Point point; + point.x = std::stof(t, &num); + bool ok_x = num == t.size(); + auto t_y = tok(); + point.y = std::stof(t_y, &num); + if (!ok_x || num != t_y.size()) { + fprintf(stderr, "Invalid spline control point: %s %s\n", t.c_str(), + t_y.c_str()); + return false; + } + spline.control_points.push_back(point); + } + + if (spline.control_points.empty()) { + fprintf(stderr, "Spline with no control point\n"); + return false; + } + + spline_data.splines.push_back(std::move(spline)); + } else if (t == "Gaborish") { + cparams.gaborish = jxl::Override::kOn; + } else if (t == "DeltaPalette") { + cparams.lossy_palette = true; + cparams.palette_colors = 0; + } else if (t == "EPF") { + t = tok(); + size_t num = 0; + cparams.epf = std::stoul(t, &num); + if (num != t.size() || cparams.epf > 3) { + fprintf(stderr, "Invalid EPF: %s\n", t.c_str()); + return false; + } + } else if (t == "Noise") { + cparams.manual_noise.resize(8); + for (size_t i = 0; i < 8; i++) { + t = tok(); + size_t num = 0; + cparams.manual_noise[i] = std::stof(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid noise entry: %s\n", t.c_str()); + return false; + } + } + } else if (t == "XYBFactors") { + cparams.manual_xyb_factors.resize(3); + for (size_t i = 0; i < 3; i++) { + t = tok(); + size_t num = 0; + cparams.manual_xyb_factors[i] = std::stof(t, &num); + if (num != t.size()) { + fprintf(stderr, "Invalid XYB factor: %s\n", t.c_str()); + return false; + } + } + } else { + fprintf(stderr, "Unexpected node type: %s\n", t.c_str()); + return false; + } + JXL_RETURN_IF_ERROR( + ParseNode(tok, tree, spline_data, cparams, W, H, io, have_next, x0, y0)); + return true; +} + +class Heuristics : public DefaultEncoderHeuristics { + public: + bool CustomFixedTreeLossless(const jxl::FrameDimensions& frame_dim, + Tree* tree) override { + *tree = tree_; + return true; + } + + explicit Heuristics(Tree tree) : tree_(std::move(tree)) {} + + private: + Tree tree_; +}; +} // namespace + +int JxlFromTree(const char* in, const char* out, const char* tree_out) { + Tree tree; + SplineData spline_data; + CompressParams cparams = {}; + size_t width = 1024, height = 1024; + int x0 = 0, y0 = 0; + cparams.SetLossless(); + cparams.resampling = 1; + cparams.ec_resampling = 1; + cparams.modular_group_size_shift = 3; + CodecInOut io; + int have_next = 0; + + std::ifstream f(in); + auto tok = [&f]() { + std::string out; + f >> out; + return out; + }; + if (!ParseNode(tok, tree, spline_data, cparams, width, height, io, have_next, + x0, y0)) { + return 1; + } + + if (tree_out) { + PrintTree(tree, tree_out); + } + Image3F image(width, height); + io.SetFromImage(std::move(image), ColorEncoding::SRGB()); + io.SetSize((width + x0) * cparams.resampling, + (height + y0) * cparams.resampling); + io.metadata.m.color_encoding.DecideIfWantICC(); + cparams.options.zero_tokens = true; + cparams.palette_colors = 0; + cparams.channel_colors_pre_transform_percent = 0; + cparams.channel_colors_percent = 0; + cparams.patches = jxl::Override::kOff; + cparams.already_downsampled = true; + PaddedBytes compressed; + + io.CheckMetadata(); + BitWriter writer; + + std::unique_ptr<CodecMetadata> metadata = jxl::make_unique<CodecMetadata>(); + *metadata = io.metadata; + JXL_RETURN_IF_ERROR(metadata->size.Set(io.xsize(), io.ysize())); + + metadata->m.xyb_encoded = cparams.color_transform == ColorTransform::kXYB; + + JXL_RETURN_IF_ERROR(WriteHeaders(metadata.get(), &writer, nullptr)); + writer.ZeroPadToByte(); + + while (true) { + PassesEncoderState enc_state; + enc_state.heuristics = make_unique<Heuristics>(tree); + enc_state.shared.image_features.splines = + SplinesFromSplineData(spline_data, enc_state.shared.cmap); + + FrameInfo info; + info.is_last = !have_next; + if (!info.is_last) info.save_as_reference = 1; + + io.frames[0].origin.x0 = x0; + io.frames[0].origin.y0 = y0; + + JXL_RETURN_IF_ERROR(EncodeFrame(cparams, info, metadata.get(), io.frames[0], + &enc_state, GetJxlCms(), nullptr, &writer, + nullptr)); + if (!have_next) break; + tree.clear(); + spline_data.splines.clear(); + have_next = 0; + if (!ParseNode(tok, tree, spline_data, cparams, width, height, io, + have_next, x0, y0)) { + return 1; + } + Image3F image(width, height); + io.SetFromImage(std::move(image), ColorEncoding::SRGB()); + io.frames[0].blend = true; + } + + compressed = std::move(writer).TakeBytes(); + + if (!WriteFile(compressed, out)) { + fprintf(stderr, "Failed to write to \"%s\"\n", out); + return 1; + } + + return 0; +} +} // namespace jxl + +int main(int argc, char** argv) { + if ((argc != 3 && argc != 4) || !strcmp(argv[1], argv[2])) { + fprintf(stderr, "Usage: %s tree_in.txt out.jxl [tree_drawing]\n", argv[0]); + return 1; + } + return jxl::JxlFromTree(argv[1], argv[2], argc < 4 ? nullptr : argv[3]); +} diff --git a/media/libjxl/src/tools/jxlinfo.c b/media/libjxl/src/tools/jxlinfo.c new file mode 100644 index 0000000000..0cced1740c --- /dev/null +++ b/media/libjxl/src/tools/jxlinfo.c @@ -0,0 +1,461 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This example prints information from the main codestream header. + +#include <inttypes.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "jxl/decode.h" + +int PrintBasicInfo(FILE* file, int verbose) { + uint8_t* data = NULL; + size_t data_size = 0; + // In how large chunks to read from the file and try decoding the basic info. + const size_t chunk_size = 2048; + + JxlDecoder* dec = JxlDecoderCreate(NULL); + if (!dec) { + fprintf(stderr, "JxlDecoderCreate failed\n"); + return 0; + } + + JxlDecoderSetKeepOrientation(dec, 1); + JxlDecoderSetCoalescing(dec, JXL_FALSE); + + if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | + JXL_DEC_FRAME | JXL_DEC_BOX)) { + fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); + JxlDecoderDestroy(dec); + return 0; + } + + JxlBasicInfo info; + int seen_basic_info = 0; + JxlFrameHeader frame_header; + int framecount = 0; + float total_duration = 0.f; + + for (;;) { + // The first time, this will output JXL_DEC_NEED_MORE_INPUT because no + // input is set yet, this is ok since the input is set when handling this + // event. + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + + if (status == JXL_DEC_ERROR) { + fprintf(stderr, "Decoder error\n"); + break; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + // The first time there is nothing to release and it returns 0, but that + // is ok. + size_t remaining = JxlDecoderReleaseInput(dec); + // move any remaining bytes to the front if necessary + if (remaining != 0) { + memmove(data, data + data_size - remaining, remaining); + } + // resize the buffer to append one more chunk of data + // TODO(lode): avoid unnecessary reallocations + data = (uint8_t*)realloc(data, remaining + chunk_size); + // append bytes read from the file behind the remaining bytes + size_t read_size = fread(data + remaining, 1, chunk_size, file); + if (read_size == 0 && feof(file)) { + fprintf(stderr, "Unexpected EOF\n"); + break; + } + data_size = remaining + read_size; + JxlDecoderSetInput(dec, data, data_size); + if (feof(file)) JxlDecoderCloseInput(dec); + } else if (status == JXL_DEC_SUCCESS) { + // Finished all processing. + break; + } else if (status == JXL_DEC_BASIC_INFO) { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec, &info)) { + fprintf(stderr, "JxlDecoderGetBasicInfo failed\n"); + break; + } + + seen_basic_info = 1; + + printf("JPEG XL %s, %ux%u, %s", + info.have_animation ? "animation" : "image", info.xsize, + info.ysize, + info.uses_original_profile ? "(possibly) lossless" : "lossy"); + printf(", %d-bit ", info.bits_per_sample); + if (info.exponent_bits_per_sample) { + printf("float (%d exponent bits) ", info.exponent_bits_per_sample); + } + int cmyk = 0, alpha = 0; + const char* const ec_type_names[7] = {"Alpha", "Depth", "Spotcolor", + "Selection", "Black", "CFA", + "Thermal"}; + for (uint32_t i = 0; i < info.num_extra_channels; i++) { + JxlExtraChannelInfo extra; + if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &extra)) { + fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n"); + break; + } + if (extra.type == JXL_CHANNEL_BLACK) cmyk = 1; + if (extra.type == JXL_CHANNEL_ALPHA) alpha = 1; + } + if (info.num_color_channels == 1) + printf("Grayscale"); + else { + if (cmyk) { + printf("CMYK"); + cmyk = 0; + } else if (alpha) { + printf("RGBA"); + alpha = 0; + } else { + printf("RGB"); + } + } + for (uint32_t i = 0; i < info.num_extra_channels; i++) { + JxlExtraChannelInfo extra; + if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &extra)) { + fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n"); + break; + } + if (extra.type == JXL_CHANNEL_BLACK && cmyk == 0) { + cmyk = 1; + continue; + } + if (extra.type == JXL_CHANNEL_ALPHA && alpha == 0) { + alpha = 1; + continue; + } + + printf("+%s", (extra.type < 7 ? ec_type_names[extra.type] + : (extra.type == JXL_CHANNEL_OPTIONAL + ? "UnknownOptional" + : "Unknown(OUTDATED libjxl!)"))); + } + printf("\n"); + if (verbose) { + printf("num_color_channels: %d\n", info.num_color_channels); + printf("num_extra_channels: %d\n", info.num_extra_channels); + + for (uint32_t i = 0; i < info.num_extra_channels; i++) { + JxlExtraChannelInfo extra; + if (JXL_DEC_SUCCESS != + JxlDecoderGetExtraChannelInfo(dec, i, &extra)) { + fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n"); + break; + } + printf("extra channel %u:\n", i); + printf( + " type: %s\n", + (extra.type < 7 ? ec_type_names[extra.type] + : (extra.type == JXL_CHANNEL_OPTIONAL + ? "Unknown but can be ignored" + : "Unknown, please update your libjxl"))); + printf(" bits_per_sample: %u\n", extra.bits_per_sample); + if (extra.exponent_bits_per_sample > 0) { + printf(" float, with exponent_bits_per_sample: %u\n", + extra.exponent_bits_per_sample); + } + if (extra.dim_shift > 0) { + printf(" dim_shift: %u (upsampled %ux)\n", extra.dim_shift, + 1 << extra.dim_shift); + } + if (extra.name_length) { + char* name = malloc(extra.name_length + 1); + if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelName( + dec, i, name, extra.name_length + 1)) { + fprintf(stderr, "JxlDecoderGetExtraChannelName failed\n"); + free(name); + break; + } + printf(" name: %s\n", name); + free(name); + } + if (extra.type == JXL_CHANNEL_ALPHA) + printf(" alpha_premultiplied: %d (%s)\n", + extra.alpha_premultiplied, + extra.alpha_premultiplied ? "Premultiplied" + : "Non-premultiplied"); + if (extra.type == JXL_CHANNEL_SPOT_COLOR) { + printf(" spot_color: (%f, %f, %f) with opacity %f\n", + extra.spot_color[0], extra.spot_color[1], + extra.spot_color[2], extra.spot_color[3]); + } + if (extra.type == JXL_CHANNEL_CFA) + printf(" cfa_channel: %u\n", extra.cfa_channel); + } + } + + if (info.intensity_target != 255.f || info.min_nits != 0.f || + info.relative_to_max_display != 0 || + info.relative_to_max_display != 0.f) { + printf("intensity_target: %f nits\n", info.intensity_target); + printf("min_nits: %f\n", info.min_nits); + printf("relative_to_max_display: %d\n", info.relative_to_max_display); + printf("linear_below: %f\n", info.linear_below); + } + if (verbose) printf("have_preview: %d\n", info.have_preview); + if (info.have_preview) { + printf("Preview image: %ux%u\n", info.preview.xsize, + info.preview.ysize); + } + if (verbose) printf("have_animation: %d\n", info.have_animation); + if (verbose && info.have_animation) { + printf("ticks per second (numerator / denominator): %u / %u\n", + info.animation.tps_numerator, info.animation.tps_denominator); + printf("num_loops: %u\n", info.animation.num_loops); + printf("have_timecodes: %d\n", info.animation.have_timecodes); + } + if (info.xsize != info.intrinsic_xsize || + info.ysize != info.intrinsic_ysize || verbose) { + printf("Intrinsic dimensions: %ux%u\n", info.intrinsic_xsize, + info.intrinsic_ysize); + } + const char* const orientation_string[8] = { + "Normal", "Flipped horizontally", + "Upside down", "Flipped vertically", + "Transposed", "90 degrees clockwise", + "Anti-Transposed", "90 degrees counter-clockwise"}; + if (info.orientation > 0 && info.orientation < 9) { + if (verbose || info.orientation > 1) { + printf("Orientation: %d (%s)\n", info.orientation, + orientation_string[info.orientation - 1]); + } + } else { + fprintf(stderr, "Invalid orientation\n"); + } + } else if (status == JXL_DEC_COLOR_ENCODING) { + JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + printf("Color space: "); + + JxlColorEncoding color_encoding; + if (JXL_DEC_SUCCESS == + JxlDecoderGetColorAsEncodedProfile(dec, &format, + JXL_COLOR_PROFILE_TARGET_ORIGINAL, + &color_encoding)) { + const char* const cs_string[4] = {"RGB", "Grayscale", "XYB", "Unknown"}; + const char* const wp_string[12] = {"", "D65", "Custom", "", "", "", + "", "", "", "", "E", "P3"}; + const char* const pr_string[12] = { + "", "sRGB", "Custom", "", "", "", "", "", "", "Rec.2100", "", "P3"}; + const char* const tf_string[19] = { + "", "709", "Unknown", "", "", "", "", "", "Linear", "", + "", "", "", "sRGB", "", "", "PQ", "DCI", "HLG"}; + const char* const ri_string[4] = {"Perceptual", "Relative", + "Saturation", "Absolute"}; + printf("%s, ", cs_string[color_encoding.color_space]); + printf("%s, ", wp_string[color_encoding.white_point]); + if (color_encoding.white_point == JXL_WHITE_POINT_CUSTOM) { + printf("white_point(x=%f,y=%f), ", color_encoding.white_point_xy[0], + color_encoding.white_point_xy[1]); + } + if (color_encoding.color_space == JXL_COLOR_SPACE_RGB || + color_encoding.color_space == JXL_COLOR_SPACE_UNKNOWN) { + printf("%s primaries", pr_string[color_encoding.primaries]); + if (color_encoding.primaries == JXL_PRIMARIES_CUSTOM) { + printf(": red(x=%f,y=%f),", color_encoding.primaries_red_xy[0], + color_encoding.primaries_red_xy[1]); + printf(" green(x=%f,y=%f),", color_encoding.primaries_green_xy[0], + color_encoding.primaries_green_xy[1]); + printf(" blue(x=%f,y=%f)", color_encoding.primaries_blue_xy[0], + color_encoding.primaries_blue_xy[1]); + } else + printf(", "); + } + if (color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) { + printf("gamma(%f) transfer function, ", color_encoding.gamma); + } else { + printf("%s transfer function, ", + tf_string[color_encoding.transfer_function]); + } + printf("rendering intent: %s\n", + ri_string[color_encoding.rendering_intent]); + + } else { + // The profile is not in JPEG XL encoded form, get as ICC profile + // instead. + size_t profile_size; + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize(dec, &format, + JXL_COLOR_PROFILE_TARGET_ORIGINAL, + &profile_size)) { + fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); + continue; + } + printf("%" PRIu64 "-byte ICC profile, ", (uint64_t)profile_size); + if (profile_size < 132) { + fprintf(stderr, "ICC profile too small\n"); + continue; + } + uint8_t* profile = (uint8_t*)malloc(profile_size); + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsICCProfile(dec, &format, + JXL_COLOR_PROFILE_TARGET_ORIGINAL, + profile, profile_size)) { + fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); + free(profile); + continue; + } + printf("CMM type: \"%.4s\", ", profile + 4); + printf("color space: \"%.4s\", ", profile + 16); + printf("rendering intent: %d\n", (int)profile[67]); + free(profile); + } + } else if (status == JXL_DEC_FRAME) { + if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &frame_header)) { + fprintf(stderr, "JxlDecoderGetFrameHeader failed\n"); + break; + } + if (frame_header.duration == 0) { + if (frame_header.is_last && framecount == 0 && + frame_header.name_length == 0) + continue; + printf("layer: "); + } else { + printf("frame: "); + } + framecount++; + if (frame_header.layer_info.have_crop) { + printf("%ux%u at position (%i,%i)", frame_header.layer_info.xsize, + frame_header.layer_info.ysize, frame_header.layer_info.crop_x0, + frame_header.layer_info.crop_y0); + } else { + printf("full image size"); + } + + float ms = frame_header.duration * 1000.f * + info.animation.tps_denominator / info.animation.tps_numerator; + total_duration += ms; + if (info.have_animation) { + printf(", duration: %.1f ms", ms); + if (info.animation.have_timecodes) { + printf(", time code: %X", frame_header.timecode); + } + } + if (frame_header.name_length) { + char* name = malloc(frame_header.name_length + 1); + if (JXL_DEC_SUCCESS != + JxlDecoderGetFrameName(dec, name, frame_header.name_length + 1)) { + fprintf(stderr, "JxlDecoderGetFrameName failed\n"); + free(name); + break; + } + printf(", name: \"%s\"", name); + free(name); + } + printf("\n"); + } else if (status == JXL_DEC_BOX) { + JxlBoxType type; + uint64_t size; + JxlDecoderGetBoxType(dec, type, JXL_FALSE); + JxlDecoderGetBoxSizeRaw(dec, &size); + if (verbose) { + printf("box: type: \"%c%c%c%c\" size: %" PRIu64 "\n", type[0], type[1], + type[2], type[3], (uint64_t)size); + } + if (!strncmp(type, "JXL ", 4)) { + printf("JPEG XL file format container (ISO/IEC 18181-2)\n"); + } else if (!strncmp(type, "ftyp", 4)) { + } else if (!strncmp(type, "jxlc", 4)) { + } else if (!strncmp(type, "jxlp", 4)) { + } else if (!strncmp(type, "jxll", 4)) { + } else if (!strncmp(type, "jxli", 4)) { + printf("Frame index box present\n"); + } else if (!strncmp(type, "jbrd", 4)) { + printf("JPEG bitstream reconstruction data available\n"); + } else if (!strncmp(type, "jumb", 4) || !strncmp(type, "Exif", 4) || + !strncmp(type, "xml ", 4)) { + printf("Uncompressed %c%c%c%c metadata: %" PRIu64 " bytes\n", type[0], + type[1], type[2], type[3], (uint64_t)size); + + } else if (!strncmp(type, "brob", 4)) { + JxlDecoderGetBoxType(dec, type, JXL_TRUE); + printf("Brotli-compressed %c%c%c%c metadata: %" PRIu64 + " compressed bytes\n", + type[0], type[1], type[2], type[3], (uint64_t)size); + } else { + printf("unknown box: type: \"%c%c%c%c\" size: %" PRIu64 "\n", type[0], + type[1], type[2], type[3], (uint64_t)size); + } + } else { + fprintf(stderr, "Unexpected decoder status\n"); + break; + } + } + if (info.animation.num_loops > 1) total_duration *= info.animation.num_loops; + if (info.have_animation) { + printf("Animation length: %.3f seconds%s\n", total_duration * 0.001f, + (info.animation.num_loops ? "" : " (looping)")); + } + JxlDecoderDestroy(dec); + free(data); + + return seen_basic_info; +} + +static void print_usage(const char* name) { + fprintf(stderr, + "Usage: %s [-v] INPUT\n" + " INPUT input JPEG XL image filename(s)\n" + " -v more verbose output\n", + name); +} + +static int print_basic_info_filename(const char* jxl_filename, int verbose) { + FILE* file = fopen(jxl_filename, "rb"); + if (!file) { + fprintf(stderr, "Failed to read file: %s\n", jxl_filename); + return 1; + } + int status = PrintBasicInfo(file, verbose); + fclose(file); + if (!status) { + fprintf(stderr, "Error reading file: %s\n", jxl_filename); + return status; + } + + return 0; +} + +int main(int argc, char* argv[]) { + int verbose = 0, status = 0; + const char* const name = argv[0]; + + for (int i = 1; i < argc; i++) { + const char* const* help_opts = + (const char* const[]){"--help", "-h", "-?", NULL}; + while (*help_opts) { + if (!strcmp(*help_opts++, argv[i])) { + print_usage(name); + return 0; + } + } + } + + const char* const* verbose_opts = + (const char* const[]){"--verbose", "-v", NULL}; + /* argc >= 2 gate prevents segfault on argc = 1 */ + while (argc >= 2 && *verbose_opts) { + if (!strcmp(*verbose_opts++, argv[1])) { + verbose = 1; + argc--; + argv++; + break; + } + } + + if (argc < 2) { + print_usage(name); + return 2; + } + + while (argc-- >= 2) { + status |= print_basic_info_filename(*++argv, verbose); + } + + return status; +} diff --git a/media/libjxl/src/tools/libjxl_test.c b/media/libjxl/src/tools/libjxl_test.c new file mode 100644 index 0000000000..bb57c2d2b8 --- /dev/null +++ b/media/libjxl/src/tools/libjxl_test.c @@ -0,0 +1,17 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Program to test that we can link against the public API of libjpegxl from C. +// This links against the shared libjpegxl library which doesn't expose any of +// the internals of the jxl namespace. + +#include "jxl/decode.h" + +int main() { + if (!JxlDecoderVersion()) return 1; + JxlDecoder* dec = JxlDecoderCreate(NULL); + if (!dec) return 1; + JxlDecoderDestroy(dec); +} diff --git a/media/libjxl/src/tools/optimizer/simplex_fork.py b/media/libjxl/src/tools/optimizer/simplex_fork.py new file mode 100755 index 0000000000..20de4c95c6 --- /dev/null +++ b/media/libjxl/src/tools/optimizer/simplex_fork.py @@ -0,0 +1,255 @@ +#!/usr/bin/python +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Implementation of simplex search for an external process. + +The external process gets the input vector through environment variables. +Input of vector as setenv("VAR%dimension", val) +Getting the optimized function with regexp match from stdout +of the forked process. + +https://en.wikipedia.org/wiki/Nelder%E2%80%93Mead_method + +start as ./simplex_fork.py binary dimensions amount +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from six.moves import range +import copy +import os +import random +import re +import subprocess +import sys + +def Midpoint(simplex): + """Nelder-Mead-like simplex midpoint calculation.""" + simplex.sort() + dim = len(simplex) - 1 + retval = [None] + [0.0] * dim + for i in range(1, dim + 1): + for k in range(dim): + retval[i] += simplex[k][i] + retval[i] /= dim + return retval + + +def Subtract(a, b): + """Vector arithmetic, with [0] being ignored.""" + return [None if k == 0 else a[k] - b[k] for k in range(len(a))] + +def Add(a, b): + """Vector arithmetic, with [0] being ignored.""" + return [None if k == 0 else a[k] + b[k] for k in range(len(a))] + +def Average(a, b): + """Vector arithmetic, with [0] being ignored.""" + return [None if k == 0 else 0.5 * (a[k] + b[k]) for k in range(len(a))] + + +eval_hash = {} + +def EvalCacheForget(): + global eval_hash + eval_hash = {} + +def RandomizedJxlCodecs(): + retval = [] + minval = 0.5 + maxval = 3.3 + rangeval = maxval/minval + steps = 7 + for i in range(steps): + mul = minval * rangeval**(float(i)/(steps - 1)) + mul *= 0.99 + 0.05 * random.random() + retval.append("jxl:epf2:d%.3f" % mul) + steps = 7 + for i in range(steps - 1): + mul = minval * rangeval**(float(i+0.5)/(steps - 1)) + mul *= 0.99 + 0.05 * random.random() + retval.append("jxl:epf0:d%.3f" % mul) + return ",".join(retval) + +g_codecs = RandomizedJxlCodecs() + +def Eval(vec, binary_name, cached=True): + """Evaluates the objective function by forking a process. + + Args: + vec: [0] will be set to the objective function, [1:] will + contain the vector position for the objective function. + binary_name: the name of the binary that evaluates the value. + """ + global eval_hash + global g_codecs + key = "" + # os.environ["BUTTERAUGLI_OPTIMIZE"] = "1" + for i in range(300): + os.environ["VAR%d" % i] = "0" + for i in range(len(vec) - 1): + os.environ["VAR%d" % i] = str(vec[i + 1]) + key += str(vec[i + 1]) + ":" + if cached and (key in eval_hash): + vec[0] = eval_hash[key] + return + + process = subprocess.Popen( + (binary_name, + '--input', + '/usr/local/google/home/jyrki/mix_corpus/*.png', + '--error_pnorm=3', + '--more_columns', + '--codec', g_codecs), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=dict(os.environ)) + + # process.wait() + found_score = False + vec[0] = 1.0 + dct2 = 0.0 + dct4 = 0.0 + dct16 = 0.0 + dct32 = 0.0 + n = 0 + for line in process.communicate(input=None)[0].splitlines(): + print("BE", line) + sys.stdout.flush() + if line[0:3] == b'jxl': + bpp = line.split()[3] + dist_pnorm = line.split()[7] + vec[0] *= float(dist_pnorm) * float(bpp) / 16.0 + #vec[0] *= (float(dist_max) * float(bpp) / 16.0) ** 0.2 + n += 1 + found_score = True + distance = float(line.split()[0].split(b'd')[-1]) + #faultybpp = 1.0 + 0.43 * ((float(bpp) * distance ** 0.74) - 1.57) ** 2 + #vec[0] *= faultybpp + + print("eval: ", vec) + if (vec[0] <= 0.0): + vec[0] = 1e30 + if found_score: + eval_hash[key] = vec[0] + return + vec[0] = 1e33 + return + # sys.exit("awful things happened") + +def Reflect(simplex, binary): + """Main iteration step of Nelder-Mead optimization. Modifies `simplex`.""" + simplex.sort() + last = simplex[-1] + mid = Midpoint(simplex) + diff = Subtract(mid, last) + mirrored = Add(mid, diff) + Eval(mirrored, binary) + if mirrored[0] > simplex[-2][0]: + print("\nStill worst\n\n") + # Still the worst, shrink towards the best. + shrinking = Average(simplex[-1], simplex[0]) + Eval(shrinking, binary) + print("\nshrinking...\n\n") + simplex[-1] = shrinking + return + if mirrored[0] < simplex[0][0]: + # new best + print("\nNew Best\n\n") + even_further = Add(mirrored, diff) + Eval(even_further, binary) + if even_further[0] < mirrored[0]: + print("\nEven Further\n\n") + mirrored = even_further + simplex[-1] = mirrored + # try to extend + return + else: + # not a best, not a worst point + simplex[-1] = mirrored + + +def OneDimensionalSearch(simplex, shrink, index): + # last appended was better than the best so far, try to replace it + last_attempt = simplex[-1][:] + best = simplex[0] + if last_attempt[0] < best[0]: + # try expansion of the amount + diff = simplex[-1][index] - simplex[0][index] + simplex[-1][index] = simplex[0][index] + shrink * diff + Eval(simplex[-1], g_binary) + if simplex[-1][0] < last_attempt[0]: + # it got better + return True + elif last_attempt[0] >= 0: + diff = simplex[-1][index] - simplex[0][index] + simplex[-1][index] = simplex[0][index] - diff + Eval(simplex[-1], g_binary) + if simplex[-1][0] < last_attempt[0]: + # it got better + return True + simplex[-1] = last_attempt + return False + +def InitialSimplex(vec, dim, amount): + """Initialize the simplex at origin.""" + EvalCacheForget() + best = vec[:] + Eval(best, g_binary) + retval = [best] + comp_order = list(range(1, dim + 1)) + random.shuffle(comp_order) + + for i in range(dim): + index = comp_order[i] + best = retval[0][:] + best[index] += amount + Eval(best, g_binary) + retval.append(best) + do_shrink = True + while OneDimensionalSearch(retval, 2.0, index): + print("OneDimensionalSearch-Grow") + while OneDimensionalSearch(retval, 1.1, index): + print("OneDimensionalSearch-SlowGrow") + do_shrink = False + if do_shrink: + while OneDimensionalSearch(retval, 0.9, index): + print("OneDimensionalSearch-SlowShrinking") + retval.sort() + return retval + + +if len(sys.argv) != 4: + print("usage: ", sys.argv[0], "binary-name number-of-dimensions simplex-size") + exit(1) + +g_dim = int(sys.argv[2]) +g_amount = float(sys.argv[3]) +g_binary = sys.argv[1] +g_simplex = InitialSimplex([None] + [0.0] * g_dim, + g_dim, 7.0 * g_amount) +best = g_simplex[0][:] +g_codecs = RandomizedJxlCodecs() +g_simplex = InitialSimplex(best, g_dim, g_amount * 2.47) +best = g_simplex[0][:] +g_simplex = InitialSimplex(best, g_dim, g_amount) +best = g_simplex[0][:] +g_simplex = InitialSimplex(best, g_dim, g_amount * 0.33) +best = g_simplex[0][:] + +for restarts in range(99999): + for ii in range(g_dim * 2): + g_simplex.sort() + print("reflect", ii, g_simplex[0]) + Reflect(g_simplex, g_binary) + + mulli = 0.1 + 15 * random.random()**2.0 + g_codecs = RandomizedJxlCodecs() + print("\n\n\nRestart", restarts, "mulli", mulli) + g_simplex.sort() + best = g_simplex[0][:] + g_simplex = InitialSimplex(best, g_dim, g_amount * mulli) diff --git a/media/libjxl/src/tools/ossfuzz-build.sh b/media/libjxl/src/tools/ossfuzz-build.sh new file mode 100755 index 0000000000..b5fbb45b10 --- /dev/null +++ b/media/libjxl/src/tools/ossfuzz-build.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# Helper builder file to replace the /src/build.sh one in oss-fuzz/ + +if [[ -z "${FUZZING_ENGINE:-}" ]]; then + echo "Don't call this script directly. Use ./ci.sh ossfuzz_* commands" \ + "instead." >&2 + exit 1 +fi + +set -eux + +main() { + # Build the fuzzers in release mode but force the inclusion of JXL_DASSERT() + # checks. + build_args=( + -G Ninja + -DBUILD_TESTING=OFF + -DJPEGXL_ENABLE_BENCHMARK=OFF + -DJPEGXL_ENABLE_DEVTOOLS=ON + -DJPEGXL_ENABLE_EXAMPLES=OFF + -DJPEGXL_ENABLE_FUZZERS=ON + -DJPEGXL_ENABLE_MANPAGES=OFF + -DJPEGXL_ENABLE_SJPEG=OFF + -DJPEGXL_ENABLE_VIEWERS=OFF + -DCMAKE_BUILD_TYPE=Release + ) + export CXXFLAGS="${CXXFLAGS} -DJXL_IS_DEBUG_BUILD=1" + + mkdir -p ${WORK} + cd ${WORK} + cmake \ + "${build_args[@]}" \ + -DJPEGXL_FUZZER_LINK_FLAGS="${LIB_FUZZING_ENGINE}" \ + "${SRC}/libjxl" + + fuzzers=( + color_encoding_fuzzer + djxl_fuzzer + fields_fuzzer + icc_codec_fuzzer + rans_fuzzer + transforms_fuzzer + ) + if [[ -n "${JPEGXL_EXTRA_ARGS:-}" ]]; then + # Extra arguments passed to ci.sh ossfuzz commands are treated as ninja + # targets. The environment variable is split into individual targets here, + # which might break if passing paths with spaces, which is an unlikely use + # case. + fuzzers=(${JPEGXL_EXTRA_ARGS}) + echo "Building with targets: ${JPEGXL_EXTRA_ARGS}" + fi + ninja "${fuzzers[@]}" +} + +# Build as the regular user if not already running as that user. This avoids +# having root files in the build directory. +if [[ -n "${JPEGXL_UID:-}" && "${JPEGXL_UID}" != $(id -u) ]]; then + userspec="${JPEGXL_UID}:${JPEGXL_GID}" + unset JPEGXL_UID + unset JPEGXL_GID + chroot --skip-chdir --userspec="${userspec}" \ + / $(realpath "$0") "$@" + exit $? +fi + +main "$@" diff --git a/media/libjxl/src/tools/progressive_saliency.conf b/media/libjxl/src/tools/progressive_saliency.conf new file mode 100644 index 0000000000..987651a431 --- /dev/null +++ b/media/libjxl/src/tools/progressive_saliency.conf @@ -0,0 +1,32 @@ +# Configuration parameters for progressive-saliency encoding. +# (They are too many and too complex for command-line arguments.) + +# The total number of seconds for the simulated progressive-loading animation. +simulated_progressive_loading_time_sec: 8.0 + +# Time delay after the last progressive-loading step before the animation loops. +simulated_progressive_loading_delay_until_looparound_sec: 10.0 + +# The JPEG-XL encoding command, as one would pass it to the shell, +# but with parameters ${HEATMAP_ARG}, ${INPUT}, ${OUTPUT}, ${STEPS}. +jpegxl_encoder: cjpegxl pik ${INPUT} ${OUTPUT} --progressive --saliency_num_progressive_steps ${STEPS} --fast --saliency_threshold 0.8 ${HEATMAP_ARG} + +# The JPEG-XL encoding command, as one would pass it to the shell, +# but with parameters ${INPUT}, ${OUTPUT}. +jpegxl_decoder: djpegxl ${INPUT} ${OUTPUT} + +# The shell command to use for heatmap-generation. +# This must adhere the calling conventions stated below. +# +# When called as: +# {heatmap_command} {blocksize} {input_image_filename} {coarse_grained_input_filename} {output_heatmap_filename} +# This must produce: {output_heatmap_filename} in a format that is readable by the JPEG-XL encoder, and provides one +# grayscale value per image-block which encodes saliency - ideally in the form of block-percentiles. +heatmap_command: ml_get_high_level_saliency + +# How much to blur each of the four progressive stages. +blurring: 16x4 16x1.5 0x0 0x0 + +# Whether to keep tempfiles. +# Temporary files will be named by appending suffixes to the desired final output filename. +keep_tempfiles: True diff --git a/media/libjxl/src/tools/progressive_sizes.sh b/media/libjxl/src/tools/progressive_sizes.sh new file mode 100755 index 0000000000..a1e808d381 --- /dev/null +++ b/media/libjxl/src/tools/progressive_sizes.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + + +set -eu + +TMPDIR=$(mktemp -d) + +cleanup() { + rm -rf ${TMPDIR} +} + +trap cleanup EXIT + + +CJXL=$(realpath $(dirname "$0"))/../build/tools/cjxl +DJXL=$(realpath $(dirname "$0"))/../build/tools/djxl + +${CJXL} "$@" ${TMPDIR}/x.jxl &>/dev/null +S1=$(${DJXL} ${TMPDIR}/x.jxl --print_read_bytes -s 1 2>&1 | grep 'Decoded' | grep -o '[0-9]*') +S2=$(${DJXL} ${TMPDIR}/x.jxl --print_read_bytes -s 2 2>&1 | grep 'Decoded' | grep -o '[0-9]*') +S8=$(${DJXL} ${TMPDIR}/x.jxl --print_read_bytes -s 8 2>&1 | grep 'Decoded' | grep -o '[0-9]*') + +echo "8x: $S8 2x: $S2 1x: $S1" diff --git a/media/libjxl/src/tools/rans_fuzzer.cc b/media/libjxl/src/tools/rans_fuzzer.cc new file mode 100644 index 0000000000..7c78f0d1ca --- /dev/null +++ b/media/libjxl/src/tools/rans_fuzzer.cc @@ -0,0 +1,46 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/entropy_coder.h" + +namespace jxl { + +int TestOneInput(const uint8_t* data, size_t size) { + if (size < 2) return 0; + size_t numContexts = data[0] * 256 * data[1] + 1; + data += 2; + size -= 2; + + std::vector<uint8_t> context_map; + Status ret = true; + { + BitReader br(Span<const uint8_t>(data, size)); + BitReaderScopedCloser br_closer(&br, &ret); + ANSCode code; + JXL_RETURN_IF_ERROR( + DecodeHistograms(&br, numContexts, &code, &context_map)); + ANSSymbolReader ansreader(&code, &br); + + // Limit the maximum amount of reads to avoid (valid) infinite loops. + const size_t maxreads = size * 8; + size_t numreads = 0; + int context = 0; + while (DivCeil(br.TotalBitsConsumed(), kBitsPerByte) < size && + numreads <= maxreads) { + int code = ansreader.ReadHybridUint(context, &br, context_map); + context = code % numContexts; + numreads++; + } + } + + return 0; +} + +} // namespace jxl + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return jxl::TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/reference_zip.sh b/media/libjxl/src/tools/reference_zip.sh new file mode 100755 index 0000000000..6a87344d2d --- /dev/null +++ b/media/libjxl/src/tools/reference_zip.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# Tool to create the reference software .zip package with its required +# dependencies bundled. + +set -eu + +MYDIR=$(dirname $(realpath "$0")) + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap 'retcode=$?; { set +x; } 2>/dev/null; cleanup' INT TERM EXIT + + +main() { + # Run from the repo's top level directory. + cd "${MYDIR[@]}/.." + + local deps=( + third_party/brotli + third_party/highway + third_party/skcms + ) + + local ref_files=($(git ls-files)) + for dep in "${deps[@]}"; do + local dep_files=($(git -C "${dep}" ls-files)) + for dep_file in "${dep_files[@]}"; do + ref_files+=("${dep}/${dep_file}") + done + done + + echo "Packaging ${#ref_files[@]} files..." >&2 + local dest_zip="reference_package.zip" + rm -f "${dest_zip}" + printf '%s\n' "${ref_files[@]}" | zip -q -@ "${dest_zip}" + + if [[ "${1:-}" == "test" ]]; then + echo "Testing on docker..." >&2 + set -x + sudo docker run --rm -v "$(realpath ${dest_zip}):/home/pkg.zip:ro" \ + ubuntu:20.04 <<EOF +set -eux + +apt update +DEBIAN_FRONTEND=noninteractive apt install -y build-essential zip cmake + +cd /home/ +unzip -q pkg.zip +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_SJPEG=OFF .. +cmake --build . -- -j\$(nproc) + +tools/djxl ../third_party/testdata/jxl/blending/cropped_traffic_light.jxl test.png +tools/cjxl ../third_party/testdata/third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg test.jxl +tools/djxl test.jxl test.jpg +EOF + set +x + fi + echo "${dest_zip} ready." +} + +main "$@" diff --git a/media/libjxl/src/tools/set_from_bytes_fuzzer.cc b/media/libjxl/src/tools/set_from_bytes_fuzzer.cc new file mode 100644 index 0000000000..5eb9f750e0 --- /dev/null +++ b/media/libjxl/src/tools/set_from_bytes_fuzzer.cc @@ -0,0 +1,33 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stddef.h> +#include <stdint.h> + +#include "lib/extras/codec.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { + +int TestOneInput(const uint8_t* data, size_t size) { + CodecInOut io; + io.constraints.dec_max_xsize = 1u << 16; + io.constraints.dec_max_ysize = 1u << 16; + io.constraints.dec_max_pixels = 1u << 22; + ThreadPoolInternal pool(0); + + (void)SetFromBytes(Span<const uint8_t>(data, size), &io, &pool); + + return 0; +} + +} // namespace jxl + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return jxl::TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/speed_stats.cc b/media/libjxl/src/tools/speed_stats.cc new file mode 100644 index 0000000000..3ab271f964 --- /dev/null +++ b/media/libjxl/src/tools/speed_stats.cc @@ -0,0 +1,117 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/speed_stats.h" + +#include <inttypes.h> +#include <math.h> +#include <stddef.h> +#include <stdio.h> + +#include <algorithm> +#include <string> + +namespace jpegxl { +namespace tools { + +void SpeedStats::NotifyElapsed(double elapsed_seconds) { + JXL_ASSERT(elapsed_seconds > 0.0); + elapsed_.push_back(elapsed_seconds); +} + +jxl::Status SpeedStats::GetSummary(SpeedStats::Summary* s) { + if (elapsed_.empty()) return JXL_FAILURE("Didn't call NotifyElapsed"); + + s->min = *std::min_element(elapsed_.begin(), elapsed_.end()); + s->max = *std::max_element(elapsed_.begin(), elapsed_.end()); + + // Single rep + if (elapsed_.size() == 1) { + s->central_tendency = elapsed_[0]; + s->variability = 0.0; + s->type = ""; + return true; + } + + // Two: skip first (noisier) + if (elapsed_.size() == 2) { + s->central_tendency = elapsed_[1]; + s->variability = 0.0; + s->type = " second:"; + return true; + } + + // Prefer geomean unless numerically unreliable (too many reps) + if (pow(elapsed_[0], elapsed_.size()) < 1E100) { + double product = 1.0; + for (size_t i = 1; i < elapsed_.size(); ++i) { + product *= elapsed_[i]; + } + + s->central_tendency = pow(product, 1.0 / (elapsed_.size() - 1)); + s->variability = 0.0; + s->type = " geomean:"; + return true; + } + + // Else: median + std::sort(elapsed_.begin(), elapsed_.end()); + s->central_tendency = elapsed_.data()[elapsed_.size() / 2]; + std::vector<double> deviations(elapsed_.size()); + for (size_t i = 0; i < elapsed_.size(); i++) { + deviations[i] = fabs(elapsed_[i] - s->central_tendency); + } + std::nth_element(deviations.begin(), + deviations.begin() + deviations.size() / 2, + deviations.end()); + s->variability = deviations[deviations.size() / 2]; + s->type = "median: "; + return true; +} + +namespace { + +std::string SummaryStat(double value, const char* unit, + const SpeedStats::Summary& s) { + if (value == 0.) return ""; + + char stat_str[100] = {'\0'}; + const double value_tendency = value / s.central_tendency; + // Note flipped order: higher elapsed = lower mpps. + const double value_min = value / s.max; + const double value_max = value / s.min; + + int ret = snprintf(stat_str, sizeof(stat_str), ",%s %.2f %s/s [%.2f, %.2f]", + s.type, value_tendency, unit, value_min, value_max); + (void)ret; // ret is unused when JXL_ASSERT is disabled. + JXL_ASSERT(ret < static_cast<int>(sizeof(stat_str))); + return stat_str; +} + +} // namespace + +jxl::Status SpeedStats::Print(size_t worker_threads) { + Summary s; + JXL_RETURN_IF_ERROR(GetSummary(&s)); + std::string mps_stats = SummaryStat(xsize_ * ysize_ * 1e-6, "MP", s); + std::string mbs_stats = SummaryStat(file_size_ * 1e-6, "MB", s); + + char variability[20] = {'\0'}; + if (s.variability != 0.0) { + snprintf(variability, sizeof(variability), " (var %.2f)", s.variability); + } + + fprintf(stderr, + "%" PRIu64 " x %" PRIu64 "%s%s%s, %" PRIu64 " reps, %" PRIu64 + " threads.\n", + static_cast<uint64_t>(xsize_), static_cast<uint64_t>(ysize_), + mps_stats.c_str(), mbs_stats.c_str(), variability, + static_cast<uint64_t>(elapsed_.size()), + static_cast<uint64_t>(worker_threads)); + return true; +} + +} // namespace tools +} // namespace jpegxl diff --git a/media/libjxl/src/tools/speed_stats.h b/media/libjxl/src/tools/speed_stats.h new file mode 100644 index 0000000000..eec8a58586 --- /dev/null +++ b/media/libjxl/src/tools/speed_stats.h @@ -0,0 +1,63 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_SPEED_STATS_H_ +#define TOOLS_SPEED_STATS_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <vector> + +#include "lib/jxl/base/status.h" + +namespace jpegxl { +namespace tools { + +class SpeedStats { + public: + void NotifyElapsed(double elapsed_seconds); + + struct Summary { + // How central_tendency was computed - depends on number of reps. + const char* type; + + // Elapsed time + double central_tendency; + double min; + double max; + double variability; + }; + + // Non-const, may sort elapsed_. + jxl::Status GetSummary(Summary* summary); + + // Sets the image size to allow computing MP/s values. + void SetImageSize(size_t xsize, size_t ysize) { + xsize_ = xsize; + ysize_ = ysize; + } + + // Sets the file size to allow computing MB/s values. + void SetFileSize(size_t file_size) { file_size_ = file_size; } + + // Calls GetSummary and prints megapixels/sec. SetImageSize() must be called + // once before this can be used. + jxl::Status Print(size_t worker_threads); + + private: + std::vector<double> elapsed_; + size_t xsize_ = 0; + size_t ysize_ = 0; + + // Size of the source binary file, meaningful when decoding a recompressed + // JPEG. + size_t file_size_ = 0; +}; + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_SPEED_STATS_H_ diff --git a/media/libjxl/src/tools/ssimulacra.cc b/media/libjxl/src/tools/ssimulacra.cc new file mode 100644 index 0000000000..9ce61b9c74 --- /dev/null +++ b/media/libjxl/src/tools/ssimulacra.cc @@ -0,0 +1,331 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Re-implementation of //tools/ssimulacra.tct using jxl's +// ImageF library instead of opencv. + +#include "tools/ssimulacra.h" + +#include <cmath> + +#include "lib/jxl/gauss_blur.h" +#include "lib/jxl/image_ops.h" + +namespace ssimulacra { +namespace { + +using jxl::Image3F; +using jxl::ImageF; + +static const float kC1 = 0.0001f; +static const float kC2 = 0.0004f; +static const int kNumScales = 6; +// Premultiplied by chroma weight 0.2 +static const double kScaleWeights[kNumScales][3] = { + {0.04480, 0.00300, 0.00300}, {0.28560, 0.00896, 0.00896}, + {0.30010, 0.05712, 0.05712}, {0.23630, 0.06002, 0.06002}, + {0.13330, 0.06726, 0.06726}, {0.10000, 0.05000, 0.05000}, +}; +// Premultiplied by min weights 0.1, 0.005, 0.005 +const double kMinScaleWeights[kNumScales][3] = { + {0.02000, 0.00005, 0.00005}, {0.03000, 0.00025, 0.00025}, + {0.02500, 0.00100, 0.00100}, {0.02000, 0.00150, 0.00150}, + {0.01200, 0.00175, 0.00175}, {0.00500, 0.00175, 0.00175}, +}; +const double kEdgeWeight[3] = {1.5, 0.1, 0.1}; +const double kGridWeight[3] = {1.0, 0.1, 0.1}; + +inline void Rgb2Lab(float r, float g, float b, float* L, float* A, float* B) { + const float epsilon = 0.00885645167903563081f; + const float s = 0.13793103448275862068f; + const float k = 7.78703703703703703703f; + float fx = (r * 0.43393624408206207259f + g * 0.37619779063650710152f + + b * 0.18983429773803261441f); + float fy = (r * 0.2126729f + g * 0.7151522f + b * 0.0721750f); + float fz = (r * 0.01775381083562901744f + g * 0.10945087235996326905f + + b * 0.87263921028466483011f); + const float gamma = 1.0f / 3.0f; + float X = (fx > epsilon) ? powf(fx, gamma) - s : k * fx; + float Y = (fy > epsilon) ? powf(fy, gamma) - s : k * fy; + float Z = (fz > epsilon) ? powf(fz, gamma) - s : k * fz; + *L = Y * 1.16f; + *A = (0.39181818181818181818f + 2.27272727272727272727f * (X - Y)); + *B = (0.49045454545454545454f + 0.90909090909090909090f * (Y - Z)); +} + +Image3F Rgb2Lab(const Image3F& in) { + Image3F out(in.xsize(), in.ysize()); + for (size_t y = 0; y < in.ysize(); ++y) { + const float* JXL_RESTRICT row_in0 = in.PlaneRow(0, y); + const float* JXL_RESTRICT row_in1 = in.PlaneRow(1, y); + const float* JXL_RESTRICT row_in2 = in.PlaneRow(2, y); + float* JXL_RESTRICT row_out0 = out.PlaneRow(0, y); + float* JXL_RESTRICT row_out1 = out.PlaneRow(1, y); + float* JXL_RESTRICT row_out2 = out.PlaneRow(2, y); + + for (size_t x = 0; x < in.xsize(); ++x) { + Rgb2Lab(row_in0[x], row_in1[x], row_in2[x], &row_out0[x], &row_out1[x], + &row_out2[x]); + } + } + return out; +} + +Image3F Downsample(const Image3F& in, size_t fx, size_t fy) { + const size_t out_xsize = (in.xsize() + fx - 1) / fx; + const size_t out_ysize = (in.ysize() + fy - 1) / fy; + Image3F out(out_xsize, out_ysize); + const float normalize = 1.0f / (fx * fy); + for (size_t c = 0; c < 3; ++c) { + for (size_t oy = 0; oy < out_ysize; ++oy) { + float* JXL_RESTRICT row_out = out.PlaneRow(c, oy); + for (size_t ox = 0; ox < out_xsize; ++ox) { + float sum = 0.0f; + for (size_t iy = 0; iy < fy; ++iy) { + for (size_t ix = 0; ix < fx; ++ix) { + const size_t x = std::min(ox * fx + ix, in.xsize() - 1); + const size_t y = std::min(oy * fy + iy, in.ysize() - 1); + sum += in.PlaneRow(c, y)[x]; + } + } + row_out[ox] = sum * normalize; + } + } + } + return out; +} + +void Multiply(const Image3F& a, const Image3F& b, Image3F* mul) { + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < a.ysize(); ++y) { + const float* JXL_RESTRICT in1 = a.PlaneRow(c, y); + const float* JXL_RESTRICT in2 = b.PlaneRow(c, y); + float* JXL_RESTRICT out = mul->PlaneRow(c, y); + for (size_t x = 0; x < a.xsize(); ++x) { + out[x] = in1[x] * in2[x]; + } + } + } +} + +void RowColAvgP2(const ImageF& in, double* rp2, double* cp2) { + std::vector<double> ravg(in.ysize()); + std::vector<double> cavg(in.xsize()); + for (size_t y = 0; y < in.ysize(); ++y) { + auto row = in.Row(y); + for (size_t x = 0; x < in.xsize(); ++x) { + const float val = row[x]; + ravg[y] += val; + cavg[x] += val; + } + } + std::sort(ravg.begin(), ravg.end()); + std::sort(cavg.begin(), cavg.end()); + *rp2 = ravg[ravg.size() / 50] / in.xsize(); + *cp2 = cavg[cavg.size() / 50] / in.ysize(); +} + +class StreamingAverage { + public: + void Add(const float v) { + // Numerically stable method. + double delta = v - result_; + n_ += 1; + result_ += delta / n_; + } + + double Get() const { return result_; } + + private: + double result_ = 0.0; + size_t n_ = 0; +}; + +void EdgeDiffMap(const Image3F& img1, const Image3F& mu1, const Image3F& img2, + const Image3F& mu2, Image3F* out, double* plane_avg) { + for (size_t c = 0; c < 3; ++c) { + StreamingAverage avg; + for (size_t y = 0; y < img1.ysize(); ++y) { + const float* JXL_RESTRICT row1 = img1.PlaneRow(c, y); + const float* JXL_RESTRICT row2 = img2.PlaneRow(c, y); + const float* JXL_RESTRICT rowm1 = mu1.PlaneRow(c, y); + const float* JXL_RESTRICT rowm2 = mu2.PlaneRow(c, y); + float* JXL_RESTRICT row_out = out->PlaneRow(c, y); + for (size_t x = 0; x < img1.xsize(); ++x) { + float edgediff = std::max( + std::abs(row2[x] - rowm2[x]) - std::abs(row1[x] - rowm1[x]), 0.0f); + row_out[x] = 1.0f - edgediff; + avg.Add(row_out[x]); + } + } + plane_avg[c] = avg.Get(); + } +} + +// Temporary storage for Gaussian blur, reused for multiple images. +class Blur { + public: + Blur(const size_t xsize, const size_t ysize) + : rg_(jxl::CreateRecursiveGaussian(1.5)), temp_(xsize, ysize) {} + + void operator()(const ImageF& in, ImageF* JXL_RESTRICT out) { + jxl::ThreadPool* null_pool = nullptr; + FastGaussian(rg_, in, null_pool, &temp_, out); + } + + Image3F operator()(const Image3F& in) { + Image3F out(in.xsize(), in.ysize()); + operator()(in.Plane(0), &out.Plane(0)); + operator()(in.Plane(1), &out.Plane(1)); + operator()(in.Plane(2), &out.Plane(2)); + return out; + } + + // Allows reusing across scales. + void ShrinkTo(const size_t xsize, const size_t ysize) { + temp_.ShrinkTo(xsize, ysize); + } + + private: + hwy::AlignedUniquePtr<jxl::RecursiveGaussian> rg_; + ImageF temp_; +}; + +void SSIMMap(const Image3F& m1, const Image3F& m2, const Image3F& s11, + const Image3F& s22, const Image3F& s12, Image3F* out, + double* plane_averages) { + for (size_t c = 0; c < 3; ++c) { + StreamingAverage avg; + for (size_t y = 0; y < out->ysize(); ++y) { + const float* JXL_RESTRICT row_m1 = m1.PlaneRow(c, y); + const float* JXL_RESTRICT row_m2 = m2.PlaneRow(c, y); + const float* JXL_RESTRICT row_s11 = s11.PlaneRow(c, y); + const float* JXL_RESTRICT row_s22 = s22.PlaneRow(c, y); + const float* JXL_RESTRICT row_s12 = s12.PlaneRow(c, y); + float* JXL_RESTRICT row_out = out->PlaneRow(c, y); + for (size_t x = 0; x < out->xsize(); ++x) { + float mu1 = row_m1[x]; + float mu2 = row_m2[x]; + float mu11 = mu1 * mu1; + float mu22 = mu2 * mu2; + float mu12 = mu1 * mu2; + float nom_m = 2 * mu12 + kC1; + float nom_s = 2 * (row_s12[x] - mu12) + kC2; + float denom_m = mu11 + mu22 + kC1; + float denom_s = (row_s11[x] - mu11) + (row_s22[x] - mu22) + kC2; + row_out[x] = (nom_m * nom_s) / (denom_m * denom_s); + avg.Add(row_out[x]); + } + } + plane_averages[c] = avg.Get(); + } +} + +} // namespace + +double Ssimulacra::Score() const { + double ssim = 0.0; + double ssim_max = 0.0; + for (size_t c = 0; c < 3; ++c) { + for (size_t scale = 0; scale < scales.size(); ++scale) { + ssim += kScaleWeights[scale][c] * scales[scale].avg_ssim[c]; + ssim_max += kScaleWeights[scale][c]; + ssim += kMinScaleWeights[scale][c] * scales[scale].min_ssim[c]; + ssim_max += kMinScaleWeights[scale][c]; + } + if (!simple) { + ssim += kEdgeWeight[c] * avg_edgediff[c]; + ssim_max += kEdgeWeight[c]; + ssim += kGridWeight[c] * + (row_p2[0][c] + row_p2[1][c] + col_p2[0][c] + col_p2[1][c]); + ssim_max += 4.0 * kGridWeight[c]; + } + } + double dssim = ssim_max / ssim - 1.0; + return std::min(1.0, std::max(0.0, dssim)); +} + +inline void PrintItem(const char* name, int scale, const double* vals, + const double* w) { + printf("scale %d %s = [%.10f %.10f %.10f] w = [%.5f %.5f %.5f]\n", scale, + name, vals[0], vals[1], vals[2], w[0], w[1], w[2]); +} + +void Ssimulacra::PrintDetails() const { + for (size_t s = 0; s < scales.size(); ++s) { + if (s < kNumScales) { + PrintItem("avg ssim", s, scales[s].avg_ssim, kScaleWeights[s]); + PrintItem("min ssim", s, scales[s].min_ssim, kMinScaleWeights[s]); + } + if (s == 0 && !simple) { + PrintItem("avg edif", s, avg_edgediff, kEdgeWeight); + PrintItem("rp2 ssim", s, &row_p2[0][0], kGridWeight); + PrintItem("cp2 ssim", s, &col_p2[0][0], kGridWeight); + PrintItem("rp2 edif", s, &row_p2[1][0], kGridWeight); + PrintItem("cp2 edif", s, &col_p2[1][0], kGridWeight); + } + } +} + +Ssimulacra ComputeDiff(const Image3F& orig, const Image3F& distorted, + bool simple) { + Ssimulacra ssimulacra; + + ssimulacra.simple = simple; + Image3F img1 = Rgb2Lab(orig); + Image3F img2 = Rgb2Lab(distorted); + + Image3F mul(orig.xsize(), orig.ysize()); + Blur blur(img1.xsize(), img1.ysize()); + + for (int scale = 0; scale < kNumScales; scale++) { + if (img1.xsize() < 8 || img1.ysize() < 8) { + break; + } + if (scale) { + img1 = Downsample(img1, 2, 2); + img2 = Downsample(img2, 2, 2); + } + mul.ShrinkTo(img1.xsize(), img2.ysize()); + blur.ShrinkTo(img1.xsize(), img2.ysize()); + + Multiply(img1, img1, &mul); + Image3F sigma1_sq = blur(mul); + + Multiply(img2, img2, &mul); + Image3F sigma2_sq = blur(mul); + + Multiply(img1, img2, &mul); + Image3F sigma12 = blur(mul); + + Image3F mu1 = blur(img1); + Image3F mu2 = blur(img2); + // Reuse mul as "ssim_map". + SsimulacraScale sscale; + SSIMMap(mu1, mu2, sigma1_sq, sigma2_sq, sigma12, &mul, sscale.avg_ssim); + + const Image3F ssim_map = Downsample(mul, 4, 4); + for (size_t c = 0; c < 3; c++) { + float minval, maxval; + ImageMinMax(ssim_map.Plane(c), &minval, &maxval); + sscale.min_ssim[c] = static_cast<double>(minval); + } + ssimulacra.scales.push_back(sscale); + + if (scale == 0 && !simple) { + Image3F* edgediff = &sigma1_sq; // reuse + EdgeDiffMap(img1, mu1, img2, mu2, edgediff, ssimulacra.avg_edgediff); + for (size_t c = 0; c < 3; c++) { + RowColAvgP2(ssim_map.Plane(c), &ssimulacra.row_p2[0][c], + &ssimulacra.col_p2[0][c]); + RowColAvgP2(edgediff->Plane(c), &ssimulacra.row_p2[1][c], + &ssimulacra.col_p2[1][c]); + } + } + } + return ssimulacra; +} + +} // namespace ssimulacra diff --git a/media/libjxl/src/tools/ssimulacra.h b/media/libjxl/src/tools/ssimulacra.h new file mode 100644 index 0000000000..95fc9de903 --- /dev/null +++ b/media/libjxl/src/tools/ssimulacra.h @@ -0,0 +1,36 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_SSIMULACRA_H_ +#define TOOLS_SSIMULACRA_H_ + +#include <vector> + +#include "lib/jxl/image.h" + +namespace ssimulacra { + +struct SsimulacraScale { + double avg_ssim[3]; + double min_ssim[3]; +}; + +struct Ssimulacra { + std::vector<SsimulacraScale> scales; + double avg_edgediff[3]; + double row_p2[2][3]; + double col_p2[2][3]; + bool simple; + + double Score() const; + void PrintDetails() const; +}; + +Ssimulacra ComputeDiff(const jxl::Image3F& orig, const jxl::Image3F& distorted, + bool simple); + +} // namespace ssimulacra + +#endif // TOOLS_SSIMULACRA_H_ diff --git a/media/libjxl/src/tools/ssimulacra.txt b/media/libjxl/src/tools/ssimulacra.txt new file mode 100644 index 0000000000..cedda2ae13 --- /dev/null +++ b/media/libjxl/src/tools/ssimulacra.txt @@ -0,0 +1,382 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* + SSIMULACRA - Structural SIMilarity Unveiling Local And Compression Related Artifacts + + Cloudinary's variant of DSSIM, based on Philipp Klaus Krause's adaptation of Rabah Mehdi's SSIM implementation, + using ideas from Kornel Lesinski's DSSIM implementation as well as several new ideas. + + + + + Changes compared to Krause's SSIM implementation: + - Use C++ OpenCV API + - Convert sRGB to linear RGB and then to L*a*b*, to get a perceptually more accurate color space + - Multi-scale (6 scales) + - Extra penalty for specific kinds of artifacts: + - local artifacts + - grid-like artifacts (blockiness) + - introducing edges where the original is smooth (blockiness / color banding / ringing / mosquito noise) + + Known limitations: + - Color profiles are ignored; input images are assumed to be sRGB. + - Both input images need to have the same number of channels (Grayscale / RGB / RGBA) +*/ + +/* + This DSSIM program has been created by Philipp Klaus Krause based on + Rabah Mehdi's C++ implementation of SSIM (http://mehdi.rabah.free.fr/SSIM). + Originally it has been created for the VMV '09 paper + "ftc - floating precision texture compression" by Philipp Klaus Krause. + + The latest version of this program can probably be found somewhere at + http://www.colecovision.eu. + + It can be compiled using g++ -I/usr/include/opencv -lcv -lhighgui dssim.cpp + Make sure OpenCV is installed (e.g. for Debian/ubuntu: apt-get install + libcv-dev libhighgui-dev). + + DSSIM is described in + "Structural Similarity-Based Object Tracking in Video Sequences" by Loza et al. + however setting all Ci to 0 as proposed there results in numerical instabilities. + Thus this implementation used the Ci from the SSIM implementation. + SSIM is described in + "Image quality assessment: from error visibility to structural similarity" by Wang et al. +*/ + +/* + Copyright (c) 2005, Rabah Mehdi <mehdi.rabah@gmail.com> + + Feel free to use it as you want and to drop me a mail + if it has been useful to you. Please let me know if you enhance it. + I'm not responsible if this program destroy your life & blablabla :) + + Copyright (c) 2009, Philipp Klaus Krause <philipp@colecovision.eu> + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include <cv.hpp> +#include <highgui.h> +#include <stdio.h> +#include <set> + +// comment this in to produce debug images that show the differences at each scale +#define DEBUG_IMAGES 1 +using namespace std; +using namespace cv; + +// All of the constants below are more or less arbitrary. +// Some amount of tweaking/calibration was done, but there is certainly room for improvement. + +// SSIM constants. Original C2 was 0.0009, but a smaller value seems to work slightly better. +const double C1 = 0.0001, C2 = 0.0004; + +// Weight of each scale. Somewhat arbitrary. +// These are based on the values used in IW-SSIM and Kornel's DSSIM. +// It seems weird to give so little weight to the full-size scale, but then again, +// differences in more zoomed-out scales have more visual impact. +// Anyway, these weights seem to work. +// Added one more scale compared to IW-SSIM and Kornel's DSSIM. +// Weights for chroma are modified to give more weight to larger scales (similar to Kornel's subsampled chroma) +const float scale_weights[4][6] = { + // 1:1 1:2 1:4 1:8 1:16 1:32 + {0.0448, 0.2856, 0.3001, 0.2363, 0.1333, 0.1 }, + {0.015, 0.0448, 0.2856, 0.3001, 0.3363, 0.25 }, + {0.015, 0.0448, 0.2856, 0.3001, 0.3363, 0.25 }, + {0.0448, 0.2856, 0.3001, 0.2363, 0.1333, 0.1 }, + }; + +// higher value means more importance to chroma (weights above are multiplied by this factor for chroma and alpha) +const double chroma_weight = 0.2; + +// Weights for the worst-case (minimum) score at each scale. +// Higher value means more importance to worst artifacts, lower value means more importance to average artifacts. +const float mscale_weights[4][6] = { + // 1:4 1:8 1:16 1:32 1:64 1:128 + {0.2, 0.3, 0.25, 0.2, 0.12, 0.05}, + {0.01, 0.05, 0.2, 0.3, 0.35, 0.35}, + {0.01, 0.05, 0.2, 0.3, 0.35, 0.35}, + {0.2, 0.3, 0.25, 0.2, 0.12, 0.05}, + }; + + +// higher value means more importance to worst local artifacts +const double min_weight[4] = {0.1,0.005,0.005,0.005}; + +// higher value means more importance to artifact-edges (edges where original is smooth) +const double extra_edges_weight[4] = {1.5, 0.1, 0.1, 0.5}; + +// higher value means more importance to grid-like artifacts (blockiness) +const double worst_grid_weight[2][4] = + { {1.0, 0.1, 0.1, 0.5}, // on ssim heatmap + {1.0, 0.1, 0.1, 0.5} }; // on extra_edges heatmap + + +// Convert linear RGB to L*a*b* (all in 0..1 range) +inline void rgb2lab(Vec3f &p) __attribute__ ((hot)); +inline void rgb2lab(Vec3f &p) { + const float epsilon = 0.00885645167903563081f; + const float s = 0.13793103448275862068f; + const float k = 7.78703703703703703703f; + + // D65 adjustment included + float fx = (p[2] * 0.43393624408206207259f + p[1] * 0.37619779063650710152f + p[0] * .18983429773803261441f) ; + float fy = (p[2] * 0.2126729f + p[1] * 0.7151522f + p[0] * 0.0721750f); + float fz = (p[2] * 0.01775381083562901744f + p[1] * 0.10945087235996326905f + p[0] * 0.87263921028466483011f) ; + + float X = (fx > epsilon) ? powf(fx,1.0f/3.0f) - s : k * fx; + float Y = (fy > epsilon) ? powf(fy,1.0f/3.0f) - s : k * fy; + float Z = (fz > epsilon) ? powf(fz,1.0f/3.0f) - s : k * fz; + + p[0] = Y * 1.16f; + p[1] = (0.39181818181818181818f + 2.27272727272727272727f * (X - Y)); + p[2] = (0.49045454545454545454f + 0.90909090909090909090f * (Y - Z)); +} + + +int main(int argc, char** argv) { + + if(argc!=3) { + fprintf(stderr, "Usage: %s orig_image distorted_image\n", argv[0]); + fprintf(stderr, "Returns a value between 0 (images are identical) and 1 (images are very different)\n"); + fprintf(stderr, "If the value is above 0.1 (or so), the distortion is likely to be perceptible / annoying.\n"); + fprintf(stderr, "If the value is below 0.01 (or so), the distortion is likely to be imperceptible.\n"); + return(-1); + } + + Scalar sC1 = {C1,C1,C1,C1}, sC2 = {C2,C2,C2,C2}; + + Mat img1, img2, img1_img2, img1_temp, img2_temp, img1_sq, img2_sq, mu1, mu2, mu1_sq, mu2_sq, mu1_mu2, sigma1_sq, sigma2_sq, sigma12, ssim_map; + + // read and validate input images + + img1_temp = imread(argv[1],-1); + img2_temp = imread(argv[2],-1); + + int nChan = img1_temp.channels(); + if (nChan != img2_temp.channels()) { + fprintf(stderr, "Image file %s has %i channels, while\n", argv[1], nChan); + fprintf(stderr, "image file %s has %i channels. Can't compare.\n", argv[2], img2_temp.channels()); + return -1; + } + if (img1_temp.size() != img2_temp.size()) { + fprintf(stderr, "Image dimensions have to be identical.\n"); + return -1; + } + if (img1_temp.cols < 8 || img1_temp.rows < 8) { + fprintf(stderr, "Image is too small; need at least 8 rows and columns.\n"); + return -1; + } + int pixels = img1_temp.rows * img1_temp.cols; + if (nChan == 4) { + // blend to a gray background to have a fair comparison of semi-transparent RGB values + for( int i=0 ; i < pixels; i++ ) { + Vec4b & p = img1_temp.at<Vec4b>(i); + p[0] = (p[3]*p[0] + (255-p[3])*128 ) / 255; + p[1] = (p[3]*p[1] + (255-p[3])*128 ) / 255; + p[2] = (p[3]*p[2] + (255-p[3])*128 ) / 255; + } + for( int i=0 ; i < pixels; i++ ) { + Vec4b & p = img2_temp.at<Vec4b>(i); + p[0] = (p[3]*p[0] + (255-p[3])*128 ) / 255; + p[1] = (p[3]*p[1] + (255-p[3])*128 ) / 255; + p[2] = (p[3]*p[2] + (255-p[3])*128 ) / 255; + } + } + + + if (nChan > 1) { + // Create lookup table to convert 8-bit sRGB to linear RGB + Mat sRGB_gamma_LUT(1, 256, CV_32FC1); + for (int i = 0; i < 256; i++) { + float c = i / 255.0; + sRGB_gamma_LUT.at<float>(i) = (c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4)); + } + + // Convert from sRGB to linear RGB + LUT(img1_temp, sRGB_gamma_LUT, img1); + LUT(img2_temp, sRGB_gamma_LUT, img2); + } else { + img1 = Mat(img1_temp.rows, img1_temp.cols, CV_32FC1); + img2 = Mat(img1_temp.rows, img1_temp.cols, CV_32FC1); + } + + // Convert from linear RGB to Lab in a 0..1 range + if (nChan == 3) { + for( int i=0 ; i < pixels; i++ ) rgb2lab(img1.at<Vec3f>(i)); + for( int i=0 ; i < pixels; i++ ) rgb2lab(img2.at<Vec3f>(i)); + } else if (nChan == 4) { + for( int i=0 ; i < pixels; i++ ) { Vec3f p = {img1.at<Vec4f>(i)[0],img1.at<Vec4f>(i)[1],img1.at<Vec4f>(i)[2]}; rgb2lab(p); img1.at<Vec4f>(i)[0] = p[0]; img1.at<Vec4f>(i)[1] = p[1]; img1.at<Vec4f>(i)[2] = p[2];} + for( int i=0 ; i < pixels; i++ ) { Vec3f p = {img2.at<Vec4f>(i)[0],img2.at<Vec4f>(i)[1],img2.at<Vec4f>(i)[2]}; rgb2lab(p); img2.at<Vec4f>(i)[0] = p[0]; img2.at<Vec4f>(i)[1] = p[1]; img2.at<Vec4f>(i)[2] = p[2];} + } else if (nChan == 1) { + for( int i=0 ; i < pixels; i++ ) { img1.at<float>(i) = img1_temp.at<uchar>(i)/255.0;} + for( int i=0 ; i < pixels; i++ ) { img2.at<float>(i) = img2_temp.at<uchar>(i)/255.0;} + } else { + fprintf(stderr, "Can only deal with Grayscale, RGB or RGBA input.\n"); + return(-1); + } + + + double dssim=0, dssim_max=0; + + for (int scale = 0; scale < 6; scale++) { + + if (img1.cols < 8 || img1.rows < 8) break; + if (scale) { + // scale down 50% in each iteration. + resize(img1, img1, Size(), 0.5, 0.5, INTER_AREA); + resize(img2, img2, Size(), 0.5, 0.5, INTER_AREA); + } + + // Standard SSIM computation + cv::pow( img1, 2, img1_sq ); + cv::pow( img2, 2, img2_sq ); + + multiply( img1, img2, img1_img2, 1 ); + + GaussianBlur(img1, mu1, Size(11,11), 1.5); + GaussianBlur(img2, mu2, Size(11,11), 1.5); + + cv::pow( mu1, 2, mu1_sq ); + cv::pow( mu2, 2, mu2_sq ); + multiply( mu1, mu2, mu1_mu2, 1 ); + + GaussianBlur(img1_sq, sigma1_sq, Size(11,11), 1.5); + addWeighted( sigma1_sq, 1, mu1_sq, -1, 0, sigma1_sq ); + + GaussianBlur(img2_sq, sigma2_sq, Size(11,11), 1.5); + addWeighted( sigma2_sq, 1, mu2_sq, -1, 0, sigma2_sq ); + + GaussianBlur(img1_img2, sigma12, Size(11,11), 1.5); + addWeighted( sigma12, 1, mu1_mu2, -1, 0, sigma12 ); + + ssim_map = ((2*mu1_mu2 + sC1).mul(2*sigma12 + sC2))/((mu1_sq + mu2_sq + sC1).mul(sigma1_sq + sigma2_sq + sC2)); + + + // optional: write a nice debug image that shows the problematic areas +#ifdef DEBUG_IMAGES + Mat ssim_image; + ssim_map.convertTo(ssim_image,CV_8UC3,255); + for( int i=0 ; i < ssim_image.rows * ssim_image.cols; i++ ) { + Vec3b &p = ssim_image.at<Vec3b>(i); + p = {(uchar)(255-p[2]),(uchar)(255-p[0]),(uchar)(255-p[1])}; + } + imwrite("debug-scale"+to_string(scale)+".png",ssim_image); +#endif + + + // average ssim over the entire image + Scalar avg = mean( ssim_map ); + for(unsigned int i = 0; i < nChan; i++) { + printf("avg: %i %f\n",i,avg[i]); + dssim += (i>0?chroma_weight:1.0) * avg[i] * scale_weights[i][scale]; + dssim_max += (i>0?chroma_weight:1.0) * scale_weights[i][scale]; + } + +// resize(ssim_map, ssim_map, Size(), 0.5, 0.5, INTER_AREA); + + + // the edge/blockiness penalty is only done for the fullsize images + if (scale == 0) { + + // asymmetric: penalty for introducing edges where there are none (e.g. blockiness), no penalty for smoothing away edges + Mat edgediff = max(abs(img2 - mu2) - abs(img1 - mu1), 0); // positive if img2 has an edge where img1 is smooth + + // optional: write a nice debug image that shows the artifact edges +#ifdef DEBUG_IMAGES + Mat edgediff_image; + edgediff.convertTo(edgediff_image,CV_8UC3,5000); // multiplying by more than 255 to make things easier to see + for( int i=0 ; i < pixels; i++ ) { + Vec3b &p = edgediff_image.at<Vec3b>(i); + p = {(uchar)(p[1]+p[2]),p[0],p[0]}; + } + imwrite("debug-edgediff.png",edgediff_image); +#endif + + edgediff = Scalar(1.0,1.0,1.0,1.0) - edgediff; + + avg = mean(edgediff); + for(unsigned int i = 0; i < nChan; i++) { + printf("extra_edges: %i %f\n",i,avg[i]); + dssim += extra_edges_weight[i] * avg[i]; + dssim_max += extra_edges_weight[i]; + } + + // grid-like artifact detection + // do the things below twice: once for the SSIM map, once for the artifact-edge map + Mat errormap; + for(int twice=0; twice < 2; twice++) { + if (twice == 0) errormap = ssim_map; + else errormap = edgediff; + + // Find the 2nd percentile worst row. If the compression uses blocks, there will be artifacts around the block edges, + // so even with 32x32 blocks, the 2nd percentile will likely be one of the rows with block borders + multiset<double> row_scores[4]; + for (int y = 0; y < errormap.rows; y++) { + Mat roi = errormap(Rect(0,y,errormap.cols,1)); + Scalar ravg = mean(roi); + for (unsigned int i = 0; i < nChan; i++) row_scores[i].insert(ravg[i]); + } + for(unsigned int i = 0; i < nChan; i++) { + int k=0; for (const double& s : row_scores[i]) { if (k++ >= errormap.rows/50) { dssim += worst_grid_weight[twice][i] * s; + printf("grid row %s %i: %f\n",(twice?"edgediff":"ssimmap"),i,s); + + break; } } + dssim_max += worst_grid_weight[twice][i]; + } + // Find the 2nd percentile worst column. Same concept as above. + multiset<double> col_scores[4]; + for (int x = 0; x < errormap.cols; x++) { + Mat roi = errormap(Rect(x,0,1,errormap.rows)); + Scalar cavg = mean(roi); + for (unsigned int i = 0; i < nChan; i++) col_scores[i].insert(cavg[i]); + } + for(unsigned int i = 0; i < nChan; i++) { + int k=0; for (const double& s : col_scores[i]) { if (k++ >= errormap.cols/50) { dssim += worst_grid_weight[twice][i] * s; + printf("grid col %s %i: %f\n",(twice?"edgediff":"ssimmap"),i,s); + +break; } } + dssim_max += worst_grid_weight[twice][i]; + } + } + } + + // worst ssim in a particular 4x4 block (larger blocks are considered too because of multi-scale) + resize(ssim_map, ssim_map, Size(), 0.25, 0.25, INTER_AREA); +// resize(ssim_map, ssim_map, Size(), 0.5, 0.5, INTER_AREA); + + Mat ssim_map_c[4]; + split(ssim_map, ssim_map_c); + for (unsigned int i=0; i < nChan; i++) { + double minVal; + minMaxLoc(ssim_map_c[i], &minVal); + printf("worst %i: %f\n",i,minVal); + dssim += min_weight[i] * minVal * mscale_weights[i][scale]; + dssim_max += min_weight[i] * mscale_weights[i][scale]; + } + + } + + + dssim = dssim_max / dssim - 1; + if (dssim < 0) dssim = 0; // should not happen + if (dssim > 1) dssim = 1; // very different images + + printf("%.8f\n", dssim); + + return(0); +} diff --git a/media/libjxl/src/tools/ssimulacra_main.cc b/media/libjxl/src/tools/ssimulacra_main.cc new file mode 100644 index 0000000000..5b48fe22c7 --- /dev/null +++ b/media/libjxl/src/tools/ssimulacra_main.cc @@ -0,0 +1,67 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> + +#include "lib/extras/codec.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/enc_color_management.h" +#include "tools/ssimulacra.h" + +namespace ssimulacra { +namespace { + +int PrintUsage(char** argv) { + fprintf(stderr, "Usage: %s [-v] [-s] orig.png distorted.png\n", argv[0]); + return 1; +} + +int Run(int argc, char** argv) { + if (argc < 2) return PrintUsage(argv); + + bool verbose = false, simple = false; + int input_arg = 1; + if (!strcmp(argv[input_arg], "-v")) { + verbose = true; + input_arg++; + } + if (!strcmp(argv[input_arg], "-s")) { + simple = true; + input_arg++; + } + if (argc < input_arg + 2) return PrintUsage(argv); + + jxl::CodecInOut io1; + jxl::CodecInOut io2; + JXL_CHECK(SetFromFile(argv[input_arg], jxl::extras::ColorHints(), &io1)); + JXL_CHECK(SetFromFile(argv[input_arg + 1], jxl::extras::ColorHints(), &io2)); + JXL_CHECK(io1.TransformTo(jxl::ColorEncoding::LinearSRGB(io1.Main().IsGray()), + jxl::GetJxlCms())); + JXL_CHECK(io2.TransformTo(jxl::ColorEncoding::LinearSRGB(io2.Main().IsGray()), + jxl::GetJxlCms())); + + if (io1.xsize() != io2.xsize() || io1.ysize() != io2.ysize()) { + fprintf(stderr, "Image size mismatch\n"); + return 1; + } + if (io1.xsize() < 8 || io1.ysize() < 8) { + fprintf(stderr, "Minimum image size is 8x8 pixels\n"); + return 1; + } + + Ssimulacra ssimulacra = + ComputeDiff(*io1.Main().color(), *io2.Main().color(), simple); + + if (verbose) { + ssimulacra.PrintDetails(); + } + printf("%.8f\n", ssimulacra.Score()); + return 0; +} + +} // namespace +} // namespace ssimulacra + +int main(int argc, char** argv) { return ssimulacra::Run(argc, argv); } diff --git a/media/libjxl/src/tools/tool_version.cc b/media/libjxl/src/tools/tool_version.cc new file mode 100644 index 0000000000..152689dbe5 --- /dev/null +++ b/media/libjxl/src/tools/tool_version.cc @@ -0,0 +1,18 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/tool_version.h" + +#ifdef JPEGXL_VERSION_FROM_GIT +#include "tool_version_git.h" +#endif + +namespace jpegxl { +namespace tools { + +const char* kJpegxlVersion = JPEGXL_VERSION; + +} // namespace tools +} // namespace jpegxl diff --git a/media/libjxl/src/tools/tool_version.h b/media/libjxl/src/tools/tool_version.h new file mode 100644 index 0000000000..c6f7c16253 --- /dev/null +++ b/media/libjxl/src/tools/tool_version.h @@ -0,0 +1,22 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_TOOL_VERSION_H_ +#define TOOLS_TOOL_VERSION_H_ + +#include <string> + +namespace jpegxl { +namespace tools { + +// Package version as defined by the JPEGXL_VERSION macro. This is not the +// library semantic versioning number, but instead additional information on the +// tool version. +extern const char* kJpegxlVersion; + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_TOOL_VERSION_H_ diff --git a/media/libjxl/src/tools/transforms_fuzzer.cc b/media/libjxl/src/tools/transforms_fuzzer.cc new file mode 100644 index 0000000000..1ef08b2379 --- /dev/null +++ b/media/libjxl/src/tools/transforms_fuzzer.cc @@ -0,0 +1,146 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdint.h> + +#include "lib/jxl/base/random.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +namespace { +void FillChannel(Channel& ch, Rng& rng) { + auto p = &ch.plane; + const size_t w = ch.w; + const size_t h = ch.h; + for (size_t y = 0; y < h; ++y) { + pixel_type* row = p->Row(y); + for (size_t x = 0; x < w; ++x) { + row[x] = rng.UniformU(0, 0x80000000); + } + } +} +template <typename T> +void AssertEq(T a, T b) { + if (a != b) __builtin_trap(); +} +} // namespace + +int TestOneInput(const uint8_t* data, size_t size) { + static Status nevermind = true; + BitReader reader(Span<const uint8_t>(data, size)); + BitReaderScopedCloser reader_closer(&reader, &nevermind); + + Rng rng(reader.ReadFixedBits<56>()); + + // One of {0, 1, _2_, 3}; "2" will be filtered out soon. + size_t nb_chans = static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x3; + size_t nb_extra = static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x7; + // 1..32 + size_t bit_depth = + (static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x1F) + 1; + // {0, 1, 2, 3} + size_t log_upsampling = + (static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x3); + size_t upsampling = 1 << log_upsampling; + + size_t w_orig = static_cast<size_t>(reader.ReadFixedBits<16>()); + size_t h_orig = static_cast<size_t>(reader.ReadFixedBits<16>()); + size_t w = DivCeil(w_orig, upsampling); + size_t h = DivCeil(h_orig, upsampling); + + if ((nb_chans == 2) || ((nb_chans + nb_extra) == 0) || (w * h == 0) || + ((w_orig * h_orig * (nb_chans + nb_extra)) > (1 << 23))) { + return 0; + } + + std::vector<int> hshift; + std::vector<int> vshift; + std::vector<size_t> ec_upsampling; + + for (size_t c = 0; c < nb_chans; c++) { + hshift.push_back(static_cast<int>(reader.ReadFixedBits<8>()) & 1); + vshift.push_back(static_cast<int>(reader.ReadFixedBits<8>()) & 1); + } + + for (size_t ec = 0; ec < nb_extra; ec++) { + size_t log_ec_upsampling = + (static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x3); + log_ec_upsampling = std::max(log_ec_upsampling, log_upsampling); + ec_upsampling.push_back(1 << log_ec_upsampling); + } + + Image image(w, h, bit_depth, nb_chans + nb_extra); + + for (size_t c = 0; c < nb_chans; c++) { + Channel& ch = image.channel[c]; + ch.hshift = hshift[c]; + ch.vshift = vshift[c]; + ch.shrink(DivCeil(w, 1 << hshift[c]), DivCeil(h, 1 << vshift[c])); + } + + for (size_t ec = 0; ec < nb_extra; ec++) { + Channel& ch = image.channel[ec + nb_chans]; + size_t ch_up = ec_upsampling[ec]; + int up_level = CeilLog2Nonzero(ch_up) - CeilLog2Nonzero(upsampling); + ch.shrink(DivCeil(w_orig, ch_up), DivCeil(h_orig, ch_up)); + ch.hshift = ch.vshift = up_level; + } + + GroupHeader header; + if (!Bundle::Read(&reader, &header)) return 0; + weighted::Header w_header; + if (!Bundle::Read(&reader, &w_header)) return 0; + + // TODO(eustas): give it a try? + if (!reader.AllReadsWithinBounds()) return 0; + + image.transform = header.transforms; + for (Transform& transform : image.transform) { + if (!transform.MetaApply(image)) return 0; + } + if (image.error) return 0; + + ModularOptions options; + if (!ValidateChannelDimensions(image, options)) return 0; + + for (size_t i = 0; i < image.channel.size(); ++i) { + FillChannel(image.channel[i], rng); + } + + image.undo_transforms(w_header); + + AssertEq(image.error, false); + AssertEq<size_t>(image.nb_meta_channels, 0); + AssertEq(image.channel.size(), nb_chans + nb_extra); + + for (size_t c = 0; c < nb_chans; c++) { + const Channel& ch = image.channel[c]; + AssertEq(ch.hshift, hshift[c]); + AssertEq(ch.vshift, vshift[c]); + AssertEq(ch.w, DivCeil(w, 1 << hshift[c])); + AssertEq(ch.h, DivCeil(h, 1 << vshift[c])); + } + + for (size_t ec = 0; ec < nb_extra; ec++) { + const Channel& ch = image.channel[ec + nb_chans]; + size_t ch_up = ec_upsampling[ec]; + int up_level = CeilLog2Nonzero(ch_up) - CeilLog2Nonzero(upsampling); + AssertEq(ch.w, DivCeil(w_orig, ch_up)); + AssertEq(ch.h, DivCeil(h_orig, ch_up)); + AssertEq(ch.hshift, up_level); + AssertEq(ch.vshift, up_level); + } + + return 0; +} + +} // namespace jxl + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return jxl::TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/upscaling_coefficients/generate_upscaling_coefficients.py b/media/libjxl/src/tools/upscaling_coefficients/generate_upscaling_coefficients.py new file mode 100755 index 0000000000..17c404d1cd --- /dev/null +++ b/media/libjxl/src/tools/upscaling_coefficients/generate_upscaling_coefficients.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 + +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Generates coefficients used in upscaling. + +Given an upscaling factor which can be 2, 4 or 8, we generate coefficients and +indices for lib/jxl/image_metadata.cc in the format needed there. +""" + +import argparse +import itertools +import numpy as np + + +def compute_kernel(sigma): + """Gaussian-like kernel with standard deviation sigma.""" + # This controls the length of the kernel. + m = 2.5 + diff = int(max(1, m * abs(sigma))) + kernel = np.exp(-np.arange(-diff, diff + 1)**2 /(2 * sigma * sigma)) + return kernel + + +def convolution(pixels, kernel): + """Computes a horizontal convolution and transposes the result.""" + y, x = pixels.shape + kernel_len = len(kernel) + offset = kernel_len // 2 + scale = 1 / sum(kernel) + out_pixels = np.zeros(shape=(x, y), dtype=pixels.dtype) + for i, j in itertools.product(range(x), range(y)): + if kernel_len < i < x - kernel_len: + out_pixels[i, j] = scale * sum( + pixels[j, i - offset + k] * kernel[k] for k in range(kernel_len)) + else: + out_pixels[i, j] = pixels[j, i] + return out_pixels + + +def _super_sample(pixels, n): + return np.repeat(np.repeat(pixels, n, axis=0), n, axis=1) + + +def _sub_sample(pixels, n): + x, y = pixels.shape + assert x%n == 0 and y%n == 0 + return 1 / (n * n) * pixels.reshape(x // n, n, y // n, n).transpose( + [0, 2, 1, 3]).sum(axis=(2, 3)) + + +def smooth_4x4_corners(pixels): + """Generates a 4x4 upscaled image, to be smoothed afterwards.""" + overshoot = 3.5 + m = 1.0 / (4.0 - overshoot) + y_size, x_size = pixels.shape + for y, x in itertools.product(range(3, y_size - 3, 4), + range(3, x_size - 3, 4)): + ave = ( + pixels[y, x] + pixels[y, x + 1] + pixels[y + 1, x] + + pixels[y + 1, x + 1]) + off = 2 + other = (ave - overshoot * pixels[y, x]) * m + pixels[y - off, x - off] -= (other - pixels[y, x]) + pixels[y, x] = other + + other = (ave - overshoot * pixels[y, x + 1]) * m + pixels[y - off, x + off + 1] -= (other - pixels[y, x + 1]) + pixels[y, x + 1] = other + + other = (ave - overshoot * pixels[y + 1, x]) * m + pixels[y + off + 1, x - off] -= (other - pixels[y + 1, x]) + pixels[y + 1, x] = other + + other = (ave - overshoot * pixels[y + 1, x + 1]) * m + pixels[y + off + 1][x + off + 1] -= (other - pixels[y + 1, x + 1]) + pixels[y + 1, x + 1] = other + + return pixels + + +def smoothing(pixels): + new_pixels = smooth_4x4_corners(_super_sample(pixels, 4)) + my_kernel = compute_kernel(2.5) + smooth_image = convolution(convolution(new_pixels, my_kernel), my_kernel) + return smooth_image + + +upscaling = { + 2: lambda pixels: _sub_sample(smoothing(pixels), 2), + 4: smoothing, + 8: lambda pixels: _sub_sample(smoothing(smoothing(pixels)), 2) +} + + +def get_coeffs(upscaling_factor, kernel_size=5, normalized=True, dtype="float"): + """Returns 4-tensor of coefficients. + + Args: + upscaling_factor: 2, 4, or 8 + kernel_size: must be odd + normalized: if True, the kernel matrix adds to 1 + dtype: type of numpy array to return + + Returns: + A (upscaling_factor x upscaling_factor) matrix of + (kernel_size x kernel_size) matrices, describing the kernel for all pixels. + """ + + upscaling_method = upscaling[upscaling_factor] + patch_size = 2 * kernel_size + 1 + matrix_bases = np.eye( + patch_size * patch_size, dtype=dtype).reshape(patch_size, patch_size, + patch_size, patch_size) + + # takes some time... + smoothed_bases = np.array( + [[upscaling_method(matrix_bases[a, b]) + for a in range(patch_size)] + for b in range(patch_size)]) + + middle = patch_size // 2 + lower = middle - kernel_size // 2 + upper = middle + kernel_size // 2 + 1 + assert len(range(lower, upper)) == kernel_size + assert sum(range(lower, upper)) == kernel_size * middle + + coefficients = np.array([[[[ + smoothed_bases[i, j, upscaling_factor * middle + b, + upscaling_factor * middle + a] + for i in range(lower, upper) + ] + for j in range(lower, upper)] + for a in range(upscaling_factor)] + for b in range(upscaling_factor)]) + + if normalized: + return coefficients / coefficients.sum(axis=(2, 3))[..., np.newaxis, + np.newaxis] + else: + return coefficients + + +def indices_matrix(upscaling_factor, kernel_size=5): + """Matrix containing indices with all symmetries.""" + matrix = np.zeros( + shape=[upscaling_factor * kernel_size] * 2, dtype="int16") + # define a fundamental domain + counter = 1 + for i in range((kernel_size * upscaling_factor) // 2): + for j in range(i, (kernel_size * upscaling_factor) // 2): + matrix[i, j] = counter + counter += 1 + + matrix_with_transpose = matrix + (matrix.transpose()) * ( + matrix != matrix.transpose()) + matrix_vertical = matrix_with_transpose + ( + np.flip(matrix_with_transpose, axis=0) * + (matrix_with_transpose != np.flip(matrix_with_transpose, axis=0))) + matrix_horizontal = matrix_vertical + ( + np.flip(matrix_vertical, axis=1) * + (matrix_vertical != np.flip(matrix_vertical, axis=1))) - 1 + return matrix_horizontal + + +def format_indices_matrix(upscaling_factor, kernel_size=5): + """Returns string of commented out numbers-only matrices.""" + indices = indices_matrix(upscaling_factor) + output_str = [] + for i in range(upscaling_factor // 2): + for j in range(kernel_size): + output_str.append("//") + for a in range(upscaling_factor // 2): + for b in range(kernel_size): + output_str.append( + f"{'{:x}'.format(int(indices[kernel_size*i + j][kernel_size*a + b])).rjust(2)} " + ) + output_str.append(" ") + output_str.append("\n") + output_str.append("\n") + return "".join(output_str) + + +def weights_arrays(upscaling_factor, kernel_size=5): + """Returns string describing array of depth 4.""" + indices = indices_matrix(upscaling_factor) + return ( + f"kernel[{upscaling_factor}][{upscaling_factor}][{kernel_size}][{kernel_size}]" + f" = {{" + ", \n".join("{\n" + ", \n\n".join( + ("{" + ", \n".join("{" + ", ".join( + f"weights[{str(indices[kernel_size*i + j][kernel_size*a + b])}]" + for b in range(kernel_size)) + "}" + for j in range(kernel_size)) + "}" + for a in range(upscaling_factor // 2))) + "\n}" + for i in range(upscaling_factor // 2)) + "}\n") + + +def coefficients_list(upscaling_factor, kernel_size=5): + """Returns string describing coefficients.""" + coeff_tensor = get_coeffs(upscaling_factor, + kernel_size).transpose([0, 2, 1, 3]).reshape( + kernel_size * upscaling_factor, + kernel_size * upscaling_factor) + my_weights = [ + f'{"{:.8f}".format(coeff_tensor[i][j])}f' + for i in range((kernel_size * upscaling_factor) // 2) + for j in range(i, (kernel_size * upscaling_factor) // 2) + ] + return f"kWeights{upscaling_factor} = {{" + ", ".join(my_weights) + "};" + + +def print_all_output(upscaling_factor): + print(format_indices_matrix(upscaling_factor)) + print(coefficients_list(upscaling_factor), end="\n\n") + print(weights_arrays(upscaling_factor)) + + +def main(): + parser = argparse.ArgumentParser( + description="Generates coefficients used in upscaling.") + parser.add_argument( + "upscaling_factor", + type=int, + help="upscaling factor, must be 2, 4 or 8.", + nargs="?", + default=None) + + args = parser.parse_args() + upscaling_factor = args.upscaling_factor + if upscaling_factor: + print_all_output(upscaling_factor) + else: + for factor in [2, 4, 8]: + print(f"upscaling factor = {factor}") + print_all_output(factor) + + +if __name__ == "__main__": + main() diff --git a/media/libjxl/src/tools/upscaling_coefficients/upscaler_demo.py b/media/libjxl/src/tools/upscaling_coefficients/upscaler_demo.py new file mode 100644 index 0000000000..89f1320a74 --- /dev/null +++ b/media/libjxl/src/tools/upscaling_coefficients/upscaler_demo.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python3 + +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Demo for upscaling. + +Given an upscaling factor which can be 2, 4 or 8 we demo upscaling an image by +that factor. + +usage: upscaler_demo.py [-h] [--upscaling_factor N] input_filename output_filename + +Upscaling of an image by a factor of 2, 4 or 8. + +positional arguments: + input_filename of the PNG image to be upscaled. + output_filename where the upscaled image is written as PNG. + +optional arguments: + -h, --help show this help message and exit + --upscaling_factor N where N must be 2, 4 (default) or 8. +""" +from PIL import Image + +import argparse +import numpy as np + + +def convolution(pixels, kernel): + """ + Returns the convolution of `pixels` with `kernel`. + + Uses padding such that the shape of the returned convoluted array is the + same as the shape of `pixels`, scaled by the upscaling_factor implied by the + `kernel`. + + Args: + pixels: A [heigth, width]- or [height, width, num_channels]-array + representing an image. + + kernel: A [upscaling_factor, upscaling_factor, kernel_size, + kernel_size]-array used for the convolution. + + Returns: + A [upscaling_factor*heigth, upscaling_factor*width]- or + [upscaling_factor*height, upscaling_factor*width, num_channels]-array representing the + convoluted upscaled image. + """ + upscaling_factor, _, kernel_size, _ = kernel.shape + output_shape = list(pixels.shape) + output_shape[0] *= upscaling_factor + output_shape[1] *= upscaling_factor + shaped_pixels = pixels.reshape(pixels.shape[:2] + (-1,)) + pad_width = kernel_size//2 + padded_pixels = np.pad( + shaped_pixels, 2*[2*[pad_width]] + [[0, 0]], mode='edge') + x, y, _ = shaped_pixels.shape + convoluted = np.block([[np.einsum('rc...,RCrc->...RC', + padded_pixels[i - pad_width: i + pad_width + 1, + j - pad_width: j + pad_width + 1], + kernel) + for j in range(pad_width, pad_width + y)] + for i in range(pad_width, pad_width + x)]) + return np.moveaxis(convoluted, 0, -1).reshape(output_shape) + + +def main(): + parser = argparse.ArgumentParser( + description="Upscaling of an image by a factor of 2, 4 or 8.") + parser.add_argument( + "--upscaling_factor", + type=int, + help="where N must be 2, 4 (default) or 8.", + nargs=1, + default=[4], + metavar='N') + + parser.add_argument( + "input_filename", + type=str, + help="of the PNG image to be upscaled." + ) + parser.add_argument( + "output_filename", + type=str, + help="where the upscaled image is written as PNG." + ) + + args = parser.parse_args() + upscaling_factor = args.upscaling_factor[0] + kernel_size = 5 + if upscaling_factor not in (2, 4, 8): + raise ValueError("upscaling_factor must be 2, 4 or 8.") + kernel = np.array(_get_scaling_kernels()[upscaling_factor]) + assert kernel.shape == ( + upscaling_factor, upscaling_factor, kernel_size, kernel_size) + orig_raw = Image.open(args.input_filename) + orig = orig_raw.convert('RGB') if orig_raw.mode == 'P' else orig_raw + upscaled_float = convolution(np.array(orig), kernel) + + upscaled = Image.fromarray( + np.rint(np.clip(upscaled_float, 0, 255)).astype(np.uint8), orig.mode) + upscaled.save(args.output_filename) + + +def _get_scaling_kernels(): + return {2: [[[[-0.017162003089909145, -0.0345230259724203, -0.04022174342753632, + -0.029210135410064335, -0.006246448474415789], [-0.0345230259724203, + 0.14111091126932612, 0.28896754962953114, 0.0027871809188615613, + -0.016102674925096382], [-0.04022174342753632, 0.28896754962953114, + 0.5666155013385713, 0.037776067445408776, -0.01986694439461126], + [-0.029210135410064335, 0.0027871809188615535, 0.03777606744540877, + -0.031447310821961526, -0.011850679991269755], [-0.006246448474415788, + -0.01610267492509638, -0.019866944394611258, -0.011850679991269755, + -0.0021353894928012747]], [[-0.006246448474415787, -0.029210135410064328, + -0.040221743427536316, -0.034523025972420296, -0.01716200308990914], + [-0.01610267492509638, 0.0027871809188615582, 0.2889675496295311, + 0.1411109112693261, -0.034523025972420296], [-0.019866944394611254, + 0.037776067445408755, 0.5666155013385712, 0.2889675496295311, + -0.04022174342753631], [-0.011850679991269751, -0.03144731082196152, + 0.037776067445408755, 0.00278718091886156, -0.029210135410064324], + [-0.0021353894928012743, -0.011850679991269751, -0.01986694439461125, + -0.016102674925096375, -0.006246448474415786]]], [[[-0.006246448474415788, + -0.01610267492509638, -0.019866944394611258, -0.011850679991269755, + -0.0021353894928012747], [-0.02921013541006433, 0.002787180918861557, + 0.03777606744540876, -0.031447310821961526, -0.011850679991269755], + [-0.04022174342753632, 0.28896754962953114, 0.5666155013385712, + 0.037776067445408776, -0.019866944394611258], [-0.0345230259724203, + 0.14111091126932612, 0.28896754962953114, 0.0027871809188615595, + -0.016102674925096382], [-0.017162003089909145, -0.03452302597242031, + -0.04022174342753633, -0.029210135410064335, -0.006246448474415789]], + [[-0.0021353894928012747, -0.011850679991269755, -0.019866944394611258, + -0.01610267492509638, -0.006246448474415788], [-0.011850679991269755, + -0.031447310821961526, 0.03777606744540876, 0.002787180918861564, + -0.02921013541006433], [-0.019866944394611258, 0.037776067445408776, + 0.5666155013385712, 0.28896754962953114, -0.040221743427536316], + [-0.016102674925096382, 0.002787180918861556, 0.28896754962953114, + 0.14111091126932615, -0.0345230259724203], [-0.006246448474415789, + -0.029210135410064335, -0.04022174342753633, -0.0345230259724203, + -0.017162003089909145]]]], + 4: [[[[-0.024190672183733018, -0.03491987403959535, -0.036933511116288356, + -0.03094284535390427, -0.005297851729507614], [-0.03491987403959535, + 0.23651958284942343, 0.3339294481745815, -0.010735433431237009, + -0.013131808617501706], [-0.036933511116288356, 0.3339294481745815, + 0.4691419769580017, -0.0020927007975838127, -0.014845888917802386], + [-0.030942845353904277, -0.010735433431237024, -0.0020927007975838035, + -0.035516824721615874, -0.007548300818273063], [-0.005297851729507614, + -0.013131808617501708, -0.014845888917802386, -0.007548300818273063, + -0.0009165296078520004]], [[-0.01663431734052121, -0.03556862997573282, + -0.0388890539890255, -0.035168498619353575, -0.009894687488916538], + [-0.03556694367519552, 0.13048175192612746, 0.40103024797994685, + 0.03951149796198834, -0.02077584470399766], [-0.04064806042030105, + 0.18942529580147974, 0.5627989220290085, 0.06674400125646836, + -0.023354943007463536], [-0.0226791877794674, -0.023635779153108244, + 0.0031580414703133823, -0.03399097960642573, -0.013595188211470589], + [-0.003354666868160516, -0.011632944561351362, -0.016102939237729652, + -0.00974087766582541, -0.0019162161212866041]], [[-0.009894687488916542, + -0.03516849861935358, -0.03888905398902551, -0.035568629975732825, + -0.016634317340521215], [-0.020775844703997664, 0.03951149796198835, + 0.4010302479799469, 0.13048175192612743, -0.03556694367519553], + [-0.02335494300746354, 0.06674400125646836, 0.5627989220290086, + 0.18942529580147976, -0.04064806042030106], [-0.01359518821147059, + -0.033990979606425734, 0.003158041470313383, -0.02363577915310824, + -0.022679187779467407], [-0.0019162161212866043, -0.00974087766582541, + -0.016102939237729656, -0.011632944561351364, -0.0033546668681605166]], + [[-0.005297851729507613, -0.030942845353904264, -0.036933511116288356, + -0.034919874039595344, -0.024190672183733015], [-0.013131808617501703, + -0.010735433431237012, 0.33392944817458137, 0.23651958284942343, + -0.03491987403959533], [-0.014845888917802382, -0.0020927007975838153, + 0.4691419769580016, 0.33392944817458153, -0.03693351111628834], + [-0.007548300818273061, -0.03551682472161587, -0.0020927007975838053, + -0.010735433431237016, -0.030942845353904264], [-0.0009165296078520002, + -0.007548300818273061, -0.014845888917802382, -0.013131808617501704, + -0.005297851729507613]]], [[[-0.01663431734052122, -0.03556694367519555, + -0.040648060420301065, -0.02267918777946741, -0.0033546668681605175], + [-0.03556862997573284, 0.13048175192612746, 0.18942529580147982, + -0.023635779153108258, -0.011632944561351367], [-0.038889053989025514, + 0.401030247979947, 0.5627989220290087, 0.0031580414703133814, + -0.01610293923772966], [-0.03516849861935359, 0.03951149796198835, + 0.06674400125646837, -0.03399097960642574, -0.009740877665825412], + [-0.009894687488916542, -0.020775844703997664, -0.023354943007463547, + -0.01359518821147059, -0.0019162161212866046]], [[-0.01095445961681655, + -0.0319846366701879, -0.04455120920314033, -0.027997902912581793, + -0.006459118117528576], [-0.0319846366701879, 0.06390599280769027, + 0.22963887988104975, 0.006309810655924714, -0.018973492447769916], + [-0.04455120920314033, 0.2296388798810498, 0.67537268393182, + 0.08483369316914859, -0.025349935472536677], [-0.027997902912581786, + 0.006309810655924713, 0.08483369316914857, -0.02205197197850368, + -0.016679994683747115], [-0.006459118117528575, -0.018973492447769913, + -0.02534993547253667, -0.016679994683747115, -0.0038444335414517822]], + [[-0.006459118117528576, -0.02799790291258179, -0.04455120920314034, + -0.0319846366701879, -0.010954459616816552], [-0.01897349244776992, + 0.006309810655924714, 0.22963887988104978, 0.06390599280769028, + -0.03198463667018791], [-0.025349935472536677, 0.08483369316914859, + 0.6753726839318202, 0.22963887988104975, -0.04455120920314034], + [-0.016679994683747118, -0.022051971978503677, 0.08483369316914859, + 0.0063098106559247085, -0.02799790291258179], [-0.0038444335414517827, + -0.016679994683747115, -0.02534993547253667, -0.018973492447769916, + -0.006459118117528575]], [[-0.0033546668681605166, -0.022679187779467407, + -0.04064806042030106, -0.03556694367519554, -0.016634317340521218], + [-0.011632944561351364, -0.023635779153108254, 0.18942529580147976, + 0.13048175192612743, -0.035568629975732825], [-0.016102939237729656, + 0.0031580414703133762, 0.5627989220290086, 0.40103024797994696, + -0.038889053989025486], [-0.009740877665825409, -0.033990979606425734, + 0.06674400125646834, 0.03951149796198835, -0.035168498619353575], + [-0.0019162161212866041, -0.013595188211470589, -0.02335494300746354, + -0.02077584470399766, -0.00989468748891654]]], [[[-0.009894687488916542, + -0.020775844703997664, -0.023354943007463547, -0.01359518821147059, + -0.0019162161212866046], [-0.03516849861935359, 0.03951149796198835, + 0.06674400125646836, -0.03399097960642574, -0.009740877665825412], + [-0.03888905398902551, 0.401030247979947, 0.5627989220290087, + 0.0031580414703133814, -0.01610293923772966], [-0.03556862997573284, + 0.13048175192612746, 0.18942529580147982, -0.023635779153108258, + -0.011632944561351367], [-0.016634317340521218, -0.03556694367519555, + -0.040648060420301065, -0.022679187779467418, -0.0033546668681605175]], + [[-0.006459118117528575, -0.018973492447769916, -0.02534993547253667, + -0.016679994683747118, -0.0038444335414517827], [-0.02799790291258179, + 0.006309810655924723, 0.08483369316914856, -0.022051971978503684, + -0.016679994683747118], [-0.04455120920314034, 0.22963887988104978, + 0.6753726839318203, 0.0848336931691486, -0.02534993547253667], + [-0.03198463667018791, 0.06390599280769028, 0.22963887988104978, + 0.006309810655924709, -0.018973492447769923], [-0.010954459616816552, + -0.03198463667018791, -0.04455120920314034, -0.027997902912581796, + -0.006459118117528576]], [[-0.0038444335414517822, -0.01667999468374711, + -0.02534993547253667, -0.018973492447769913, -0.006459118117528575], + [-0.016679994683747115, -0.02205197197850368, 0.08483369316914854, + 0.006309810655924723, -0.027997902912581786], [-0.02534993547253667, + 0.08483369316914859, 0.6753726839318202, 0.22963887988104975, + -0.04455120920314033], [-0.01897349244776992, 0.006309810655924712, + 0.22963887988104975, 0.06390599280769027, -0.0319846366701879], + [-0.006459118117528576, -0.027997902912581786, -0.04455120920314033, + -0.0319846366701879, -0.01095445961681655]], [[-0.0019162161212866041, + -0.013595188211470589, -0.02335494300746354, -0.02077584470399766, + -0.00989468748891654], [-0.009740877665825409, -0.033990979606425734, + 0.06674400125646834, 0.03951149796198835, -0.03516849861935358], + [-0.016102939237729656, 0.0031580414703133762, 0.5627989220290086, + 0.40103024797994696, -0.03888905398902548], [-0.011632944561351364, + -0.023635779153108254, 0.18942529580147976, 0.1304817519261275, + -0.035568629975732825], [-0.0033546668681605166, -0.022679187779467414, + -0.04064806042030106, -0.03556694367519554, -0.016634317340521215]]], + [[[-0.005297851729507615, -0.013131808617501711, -0.01484588891780239, + -0.007548300818273065, -0.0009165296078520006], [-0.030942845353904277, + -0.010735433431237028, -0.0020927007975838087, -0.03551682472161588, + -0.007548300818273065], [-0.03693351111628837, 0.3339294481745815, + 0.4691419769580017, -0.002092700797583813, -0.01484588891780239], + [-0.03491987403959536, 0.23651958284942348, 0.33392944817458153, + -0.010735433431237012, -0.01313180861750171], [-0.024190672183733025, + -0.034919874039595365, -0.03693351111628837, -0.030942845353904277, + -0.005297851729507615]], [[-0.0033546668681605166, -0.011632944561351364, + -0.016102939237729656, -0.009740877665825412, -0.0019162161212866043], + [-0.022679187779467404, -0.023635779153108247, 0.003158041470313383, + -0.033990979606425734, -0.013595188211470589], [-0.04064806042030106, + 0.18942529580147982, 0.5627989220290085, 0.06674400125646837, + -0.023354943007463547], [-0.03556694367519553, 0.1304817519261275, + 0.4010302479799469, 0.03951149796198835, -0.020775844703997653], + [-0.016634317340521215, -0.035568629975732825, -0.038889053989025514, + -0.035168498619353575, -0.009894687488916542]], [[-0.0019162161212866048, + -0.009740877665825414, -0.01610293923772966, -0.011632944561351367, + -0.0033546668681605175], [-0.01359518821147059, -0.03399097960642574, + 0.0031580414703133836, -0.023635779153108254, -0.022679187779467407], + [-0.023354943007463554, 0.06674400125646839, 0.5627989220290086, + 0.18942529580147982, -0.040648060420301065], [-0.020775844703997657, + 0.03951149796198836, 0.401030247979947, 0.13048175192612746, + -0.035566943675195535], [-0.009894687488916544, -0.03516849861935359, + -0.03888905398902552, -0.03556862997573283, -0.016634317340521218]], + [[-0.0009165296078520004, -0.007548300818273063, -0.014845888917802386, + -0.013131808617501708, -0.005297851729507614], [-0.007548300818273063, + -0.035516824721615874, -0.0020927007975838083, -0.010735433431237009, + -0.03094284535390427], [-0.014845888917802386, -0.0020927007975838166, + 0.4691419769580016, 0.3339294481745815, -0.036933511116288356], + [-0.013131808617501706, -0.010735433431237014, 0.3339294481745815, + 0.23651958284942348, -0.03491987403959534], [-0.005297851729507614, + -0.03094284535390427, -0.03693351111628836, -0.03491987403959535, + -0.024190672183733018]]]], + 8: [[[[-0.029286133281073247, -0.03706352644207269, -0.0378381168526885, + -0.03324558280295302, -0.004476318148146651], [-0.0370635264420727, + 0.29895328454745274, 0.3575770812164143, -0.024475522375569658, + -0.010817484288013228], [-0.0378381168526885, 0.35757708121641435, + 0.42720050241527285, -0.0224893852885426, -0.01155272937910007], + [-0.03324558280295302, -0.024475522375569672, -0.022489385288542597, + -0.03680917952171095, -0.005422291349995999], [-0.00447631814814665, + -0.01081748428801323, -0.011552729379100074, -0.005422291349995998, + -0.00045072273860512197]], [[-0.02519406150475052, -0.037526010691823306, + -0.03901507994141054, -0.03663285147762567, -0.006466489422914399], + [-0.043145939817870266, 0.23903219477825294, 0.41119300519363017, + -0.005730455022054139, -0.014502394951723473], [-0.04562755195174026, + 0.28689495518965613, 0.4909386897413151, -7.890574314417001e-05, + -0.015459264122748742], [-0.029204772772557758, -0.02788574061911041, + -0.021181804710686657, -0.039424021044039116, -0.007755474877032563], + [-0.003601096394526256, -0.010202069931803576, -0.012319067611648214, + -0.006389875713059274, -0.0007159165805851706]], [[-0.020664074967504838, + -0.03838632575427139, -0.04002101086742024, -0.03900035414027985, + -0.009019734953997754], [-0.042468451339058966, 0.1756761813778118, + 0.45220642702382896, 0.02287757117854141, -0.019367833372750356], + [-0.045626588213857136, 0.21238920010551757, 0.5398093391410694, + 0.033694739393926816, -0.020702111700092594], [-0.024336140047717, + -0.03193943219458267, -0.020308275361446707, -0.04044013741654317, + -0.010740155274818487], [-0.002791220988040244, -0.009571146384946013, + -0.012883266171804216, -0.007309372111524051, -0.0010778269600400276]], + [[-0.016263925397518374, -0.039541478550530786, -0.04046620032608076, + -0.03979621423581153, -0.012244853215160445], [-0.03583254566206615, + 0.11572472115297627, 0.47416733354946305, 0.06284440084948137, + -0.026850659249274114], [-0.038669884759381434, 0.1422954970729258, + 0.5659339775075575, 0.08045180751196822, -0.028882977402423956], + [-0.01930821727497102, -0.03620398561701563, -0.019741250657301437, + -0.03919545281633189, -0.014560933634183603], [-0.0021015621671157305, + -0.008907053401106528, -0.013176682690936201, -0.008138951872408835, + -0.0015349087147535298]], [[-0.01224485321516044, -0.03979621423581152, + -0.04046620032608074, -0.03954147855053078, -0.016263925397518367], + [-0.0268506592492741, 0.06284440084948137, 0.47416733354946283, + 0.11572472115297619, -0.03583254566206614], [-0.028882977402423942, + 0.0804518075119682, 0.5659339775075574, 0.14229549707292571, + -0.03866988475938142], [-0.014560933634183596, -0.03919545281633188, + -0.01974125065730143, -0.03620398561701561, -0.019308217274971014], + [-0.0015349087147535291, -0.008138951872408828, -0.013176682690936196, + -0.008907053401106523, -0.002101562167115729]], [[-0.00901973495399775, + -0.039000354140279844, -0.040021010867420236, -0.03838632575427138, + -0.020664074967504838], [-0.019367833372750352, 0.02287757117854141, + 0.4522064270238289, 0.17567618137781174, -0.04246845133905896], + [-0.020702111700092587, 0.03369473939392681, 0.5398093391410693, + 0.21238920010551757, -0.04562658821385712], [-0.010740155274818485, + -0.04044013741654316, -0.020308275361446707, -0.031939432194582666, + -0.024336140047717], [-0.0010778269600400273, -0.007309372111524049, + -0.012883266171804212, -0.00957114638494601, -0.0027912209880402426]], + [[-0.006466489422914402, -0.03663285147762569, -0.03901507994141056, + -0.03752601069182331, -0.02519406150475053], [-0.014502394951723478, + -0.005730455022054147, 0.4111930051936302, 0.23903219477825297, + -0.04314593981787026], [-0.015459264122748746, -7.890574314417718e-05, + 0.4909386897413152, 0.2868949551896563, -0.045627551951740265], + [-0.007755474877032565, -0.03942402104403913, -0.021181804710686664, + -0.027885740619110408, -0.029204772772557765], [-0.0007159165805851706, + -0.006389875713059275, -0.012319067611648218, -0.01020206993180358, + -0.003601096394526257]], [[-0.00447631814814665, -0.03324558280295302, + -0.0378381168526885, -0.03706352644207268, -0.02928613328107324], + [-0.01081748428801323, -0.024475522375569672, 0.3575770812164142, + 0.2989532845474528, -0.03706352644207268], [-0.01155272937910007, + -0.02248938528854261, 0.42720050241527285, 0.35757708121641446, + -0.037838116852688494], [-0.005422291349995998, -0.03680917952171095, + -0.022489385288542604, -0.024475522375569658, -0.03324558280295301], + [-0.0004507227386051219, -0.005422291349995998, -0.01155272937910007, + -0.010817484288013232, -0.00447631814814665]]], [[[-0.025194061504750523, + -0.043145939817870266, -0.04562755195174026, -0.02920477277255776, + -0.0036010963945262565], [-0.037526010691823306, 0.23903219477825288, + 0.28689495518965624, -0.0278857406191104, -0.010202069931803576], + [-0.03901507994141054, 0.4111930051936302, 0.4909386897413151, + -0.021181804710686657, -0.01231906761164821], [-0.03663285147762567, + -0.005730455022054155, -7.890574314415865e-05, -0.039424021044039116, + -0.006389875713059272], [-0.0064664894229143986, -0.014502394951723471, + -0.015459264122748746, -0.00775547487703256, -0.0007159165805851706]], + [[-0.02128481178805433, -0.04173044153813555, -0.04831487472573022, + -0.03293190035303922, -0.005252595229206095], [-0.041730441538135564, + 0.18968272846778533, 0.3306368426789878, -0.013001053856678076, + -0.01372950329294693], [-0.04831487472573022, 0.3306368426789878, + 0.5640812622041927, 0.004583518760872409, -0.016482266055193047], + [-0.03293190035303923, -0.013001053856678086, 0.004583518760872414, + -0.040827417160105635, -0.009045186119473492], [-0.005252595229206096, + -0.01372950329294693, -0.016482266055193047, -0.00904518611947349, + -0.0011168422627331077]], [[-0.017203222289937238, -0.040527364499551265, + -0.050457063493932794, -0.036073170059570094, -0.007380297997922879], + [-0.0401746501391571, 0.13727831636454993, 0.3640223411093611, + 0.010278898793053761, -0.01832107424986819], [-0.04887867620762643, + 0.24585519478421125, 0.6202613509857569, 0.04314806591631964, + -0.022137366266623233], [-0.02790922286627615, -0.021178184193661707, + 0.007986619792820032, -0.039957113612285294, -0.012434273033433196], + [-0.00411203529942813, -0.012971303942569701, -0.01723725281482718, + -0.010225452530604957, -0.0016530642487971611]], [[-0.013417641633011908, + -0.03965629331558996, -0.051516162733405924, -0.038148863041386254, + -0.010058190693394595], [-0.03365072121724163, 0.08734505711506498, + 0.38194295165025005, 0.04338227748703876, -0.025259934728481214], + [-0.04158013952905281, 0.16637288777763284, 0.6502702298731253, + 0.0962163605307964, -0.031013880437287037], [-0.02231705358706074, + -0.02946265951499448, 0.009920547334197453, -0.03600283468483377, + -0.01684919502363355], [-0.003131096848010505, -0.012180160279609381, + -0.01763265975706309, -0.011256197301616299, -0.0023166274424323116]], + [[-0.01005819069339459, -0.03814886304138624, -0.0515161627334059, + -0.03965629331558995, -0.013417641633011903], [-0.025259934728481193, + 0.04338227748703876, 0.38194295165024994, 0.08734505711506492, + -0.033650721217241615], [-0.031013880437287013, 0.09621636053079634, + 0.6502702298731251, 0.1663728877776328, -0.04158013952905279], + [-0.016849195023633544, -0.03600283468483376, 0.009920547334197446, + -0.029462659514994466, -0.022317053587060723], [-0.0023166274424323103, + -0.01125619730161629, -0.017632659757063088, -0.012180160279609381, + -0.0031310968480105037]], [[-0.007380297997922879, -0.0360731700595701, + -0.0504570634939328, -0.040527364499551265, -0.01720322228993724], + [-0.01832107424986819, 0.010278898793053765, 0.36402234110936105, + 0.1372783163645499, -0.040174650139157095], [-0.022137366266623233, + 0.043148065916319624, 0.6202613509857569, 0.24585519478421133, + -0.04887867620762643], [-0.012434273033433196, -0.039957113612285294, + 0.007986619792820032, -0.0211781841936617, -0.027909222866276145], + [-0.0016530642487971611, -0.010225452530604957, -0.01723725281482718, + -0.012971303942569701, -0.004112035299428131]], [[-0.005252595229206095, + -0.03293190035303923, -0.04831487472573021, -0.04173044153813554, + -0.021284811788054327], [-0.013729503292946926, -0.013001053856678083, + 0.33063684267898774, 0.1896827284677853, -0.04173044153813554], + [-0.016482266055193047, 0.004583518760872396, 0.5640812622041926, + 0.33063684267898785, -0.0483148747257302], [-0.00904518611947349, + -0.04082741716010563, 0.0045835187608724145, -0.013001053856678076, + -0.03293190035303922], [-0.0011168422627331075, -0.009045186119473489, + -0.016482266055193043, -0.013729503292946926, -0.005252595229206093]], + [[-0.0036010963945262565, -0.029204772772557765, -0.04562755195174027, + -0.04314593981787027, -0.02519406150475053], [-0.01020206993180358, + -0.02788574061911041, 0.2868949551896562, 0.239032194778253, + -0.037526010691823306], [-0.012319067611648214, -0.02118180471068667, + 0.4909386897413152, 0.41119300519363033, -0.039015079941410534], + [-0.0063898757130592745, -0.03942402104403913, -7.890574314417213e-05, + -0.00573045502205414, -0.036632851477625676], [-0.0007159165805851707, + -0.007755474877032561, -0.015459264122748746, -0.014502394951723478, + -0.0064664894229143986]]], [[[-0.020664074967504838, + -0.042468451339058966, -0.04562658821385713, -0.024336140047717003, + -0.002791220988040243], [-0.03838632575427139, 0.1756761813778117, + 0.21238920010551754, -0.031939432194582666, -0.00957114638494601], + [-0.04002101086742023, 0.4522064270238289, 0.5398093391410693, + -0.020308275361446703, -0.012883266171804212], [-0.039000354140279844, + 0.022877571178541382, 0.03369473939392682, -0.04044013741654317, + -0.007309372111524049], [-0.00901973495399775, -0.019367833372750352, + -0.02070211170009259, -0.010740155274818482, -0.0010778269600400273]], + [[-0.017203222289937245, -0.040174650139157116, -0.04887867620762643, + -0.027909222866276156, -0.004112035299428132], [-0.040527364499551286, + 0.13727831636454993, 0.24585519478421136, -0.021178184193661704, + -0.01297130394256971], [-0.05045706349393281, 0.3640223411093611, + 0.6202613509857569, 0.007986619792820032, -0.017237252814827183], + [-0.03607317005957011, 0.010278898793053753, 0.04314806591631965, + -0.03995711361228531, -0.010225452530604962], [-0.007380297997922879, + -0.01832107424986819, -0.022137366266623236, -0.012434273033433195, + -0.0016530642487971616]], [[-0.013741489638205826, -0.037976197105406395, + -0.05142937279813894, -0.031173068184164848, -0.005819138225232018], + [-0.03797619710540639, 0.09628103622608752, 0.271299908889608, + -0.003537793416633379, -0.017341510615908634], [-0.05142937279813893, + 0.271299908889608, 0.6821432561027145, 0.050180479222290235, + -0.023208515458651935], [-0.031173068184164848, -0.003537793416633386, + 0.050180479222290235, -0.03637638898995256, -0.013943731217351598], + [-0.005819138225232015, -0.017341510615908627, -0.023208515458651935, + -0.013943731217351598, -0.002408535881464665]], [[-0.010640027531642969, + -0.03608088767126983, -0.05272168029782533, -0.03375669845324461, + -0.007955856657996018], [-0.03153980584721341, 0.05686230181726655, + 0.28500998074905703, 0.02230594207226229, -0.023749554287216885], + [-0.04383615619088237, 0.1845947431815049, 0.7151797455936234, + 0.10805612743320024, -0.03263677274980321], [-0.02511202585552835, + -0.017286364047844695, 0.054073310615547404, -0.028675684605006257, + -0.018931312997898533], [-0.004465109850519687, -0.01636186809305578, + -0.023770526019940293, -0.01522847594949364, -0.0033333443560800225]], + [[-0.007955856657996014, -0.033756698453244596, -0.052721680297825306, + -0.03608088767126982, -0.010640027531642969], [-0.023749554287216878, + 0.022305942072262296, 0.285009980749057, 0.056862301817266515, + -0.031539805847213394], [-0.0326367727498032, 0.10805612743320023, + 0.7151797455936234, 0.18459474318150482, -0.04383615619088237], + [-0.018931312997898533, -0.028675684605006243, 0.05407331061554739, + -0.0172863640478447, -0.025112025855528346], [-0.0033333443560800216, + -0.015228475949493633, -0.023770526019940286, -0.016361868093055777, + -0.004465109850519686]], [[-0.005819138225232017, -0.031173068184164845, + -0.05142937279813894, -0.037976197105406395, -0.013741489638205831], + [-0.01734151061590863, -0.003537793416633379, 0.27129990888960803, + 0.09628103622608748, -0.03797619710540639], [-0.023208515458651945, + 0.05018047922229022, 0.6821432561027146, 0.27129990888960803, + -0.05142937279813893], [-0.013943731217351596, -0.03637638898995256, + 0.050180479222290235, -0.003537793416633377, -0.03117306818416484], + [-0.0024085358814646654, -0.013943731217351596, -0.023208515458651942, + -0.01734151061590863, -0.005819138225232016]], [[-0.004112035299428132, + -0.02790922286627615, -0.04887867620762644, -0.04017465013915711, + -0.017203222289937245], [-0.012971303942569708, -0.021178184193661718, + 0.24585519478421136, 0.13727831636454993, -0.04052736449955127], + [-0.017237252814827183, 0.007986619792820018, 0.620261350985757, + 0.36402234110936116, -0.050457063493932794], [-0.01022545253060496, + -0.039957113612285315, 0.043148065916319644, 0.010278898793053767, + -0.0360731700595701], [-0.0016530642487971618, -0.012434273033433193, + -0.022137366266623233, -0.018321074249868192, -0.007380297997922878]], + [[-0.0027912209880402413, -0.02433614004771699, -0.04562658821385711, + -0.042468451339058945, -0.020664074967504827], [-0.009571146384946006, + -0.03193943219458265, 0.21238920010551743, 0.17567618137781163, + -0.038386325754271367], [-0.012883266171804209, -0.020308275361446707, + 0.5398093391410691, 0.4522064270238288, -0.0400210108674202], + [-0.007309372111524046, -0.040440137416543155, 0.033694739393926795, + 0.022877571178541393, -0.039000354140279817], [-0.001077826960040027, + -0.010740155274818476, -0.02070211170009258, -0.019367833372750345, + -0.009019734953997745]]], [[[-0.01626392539751837, -0.035832545662066145, + -0.03866988475938143, -0.01930821727497102, -0.0021015621671157296], + [-0.03954147855053079, 0.11572472115297622, 0.14229549707292574, + -0.03620398561701562, -0.008907053401106523], [-0.04046620032608075, + 0.47416733354946294, 0.5659339775075575, -0.019741250657301427, + -0.013176682690936197], [-0.039796214235811526, 0.06284440084948134, + 0.08045180751196822, -0.03919545281633188, -0.008138951872408828], + [-0.012244853215160443, -0.0268506592492741, -0.02888297740242396, + -0.014560933634183601, -0.0015349087147535293]], [[-0.013417641633011906, + -0.03365072121724163, -0.04158013952905282, -0.022317053587060733, + -0.003131096848010505], [-0.039656293315589966, 0.08734505711506495, + 0.16637288777763282, -0.029462659514994483, -0.012180160279609385], + [-0.051516162733405924, 0.3819429516502501, 0.6502702298731253, + 0.00992054733419745, -0.01763265975706309], [-0.03814886304138625, + 0.04338227748703875, 0.09621636053079638, -0.03600283468483378, + -0.011256197301616295], [-0.010058190693394593, -0.02525993472848121, + -0.03101388043728703, -0.016849195023633558, -0.0023166274424323116]], + [[-0.01064002753164297, -0.0315398058472134, -0.04383615619088237, + -0.02511202585552835, -0.004465109850519686], [-0.03608088767126983, + 0.05686230181726653, 0.18459474318150484, -0.0172863640478447, + -0.01636186809305578], [-0.05272168029782532, 0.2850099807490571, + 0.7151797455936235, 0.0540733106155474, -0.02377052601994029], + [-0.0337566984532446, 0.02230594207226228, 0.1080561274332002, + -0.028675684605006246, -0.01522847594949364], [-0.007955856657996014, + -0.02374955428721688, -0.03263677274980321, -0.018931312997898533, + -0.003333344356080022]], [[-0.008199753617852345, -0.02964168716094745, + -0.04499286779343149, -0.02745350495005966, -0.006124077091711166], + [-0.02964168716094745, 0.027274160377919326, 0.19446599876518117, + 0.0015983184753035505, -0.022324728394118268], [-0.04499286779343149, + 0.19446599876518125, 0.7498250634433566, 0.11452620166036631, + -0.03348047712449868], [-0.027453504950059663, 0.0015983184753035505, + 0.11452620166036631, -0.016056808817843177, -0.02070338975868157], + [-0.006124077091711163, -0.022324728394118268, -0.03348047712449867, + -0.02070338975868157, -0.004582234640385923]], [[-0.006124077091711165, + -0.027453504950059653, -0.04499286779343149, -0.02964168716094745, + -0.008199753617852345], [-0.022324728394118268, 0.0015983184753035479, + 0.19446599876518117, 0.027274160377919316, -0.02964168716094745], + [-0.03348047712449866, 0.11452620166036634, 0.7498250634433566, + 0.19446599876518117, -0.04499286779343149], [-0.020703389758681575, + -0.016056808817843174, 0.11452620166036631, 0.0015983184753035442, + -0.02745350495005966], [-0.004582234640385922, -0.020703389758681568, + -0.03348047712449866, -0.022324728394118268, -0.006124077091711163]], + [[-0.004465109850519687, -0.025112025855528342, -0.04383615619088236, + -0.03153980584721341, -0.01064002753164297], [-0.01636186809305578, + -0.017286364047844702, 0.18459474318150484, 0.056862301817266536, + -0.03608088767126984], [-0.023770526019940296, 0.054073310615547404, + 0.7151797455936236, 0.285009980749057, -0.05272168029782532], + [-0.015228475949493642, -0.028675684605006246, 0.10805612743320021, + 0.022305942072262292, -0.03375669845324461], [-0.003333344356080022, + -0.018931312997898533, -0.03263677274980321, -0.023749554287216885, + -0.007955856657996013]], [[-0.003131096848010504, -0.02231705358706072, + -0.04158013952905278, -0.03365072121724162, -0.013417641633011903], + [-0.01218016027960938, -0.029462659514994476, 0.16637288777763273, + 0.0873450571150649, -0.03965629331558995], [-0.017632659757063088, + 0.009920547334197435, 0.6502702298731252, 0.38194295165024994, + -0.051516162733405875], [-0.01125619730161629, -0.036002834684833764, + 0.09621636053079632, 0.04338227748703877, -0.03814886304138623], + [-0.0023166274424323103, -0.01684919502363354, -0.031013880437287016, + -0.025259934728481197, -0.010058190693394588]], [[-0.0021015621671157296, + -0.01930821727497101, -0.03866988475938142, -0.035832545662066145, + -0.016263925397518367], [-0.008907053401106521, -0.03620398561701562, + 0.14229549707292571, 0.11572472115297618, -0.03954147855053078], + [-0.013176682690936197, -0.019741250657301437, 0.5659339775075574, + 0.4741673335494629, -0.04046620032608073], [-0.008138951872408826, + -0.03919545281633188, 0.08045180751196819, 0.06284440084948135, + -0.039796214235811506], [-0.0015349087147535291, -0.014560933634183593, + -0.028882977402423952, -0.026850659249274097, -0.01224485321516044]]], + [[[-0.012244853215160442, -0.0268506592492741, -0.02888297740242396, + -0.0145609336341836, -0.0015349087147535293], [-0.039796214235811526, + 0.06284440084948134, 0.08045180751196819, -0.03919545281633189, + -0.008138951872408828], [-0.040466200326080747, 0.47416733354946294, + 0.5659339775075575, -0.019741250657301427, -0.013176682690936197], + [-0.03954147855053079, 0.1157247211529762, 0.14229549707292577, + -0.03620398561701562, -0.008907053401106523], [-0.016263925397518374, + -0.035832545662066145, -0.03866988475938143, -0.019308217274971024, + -0.00210156216711573]], [[-0.010058190693394592, -0.025259934728481204, + -0.031013880437287023, -0.016849195023633547, -0.0023166274424323103], + [-0.03814886304138625, 0.04338227748703876, 0.09621636053079634, + -0.036002834684833764, -0.011256197301616292], [-0.05151616273340591, + 0.38194295165025, 0.6502702298731253, 0.009920547334197446, + -0.017632659757063088], [-0.039656293315589966, 0.08734505711506492, + 0.16637288777763276, -0.029462659514994476, -0.012180160279609383], + [-0.013417641633011903, -0.03365072121724163, -0.041580139529052804, + -0.02231705358706074, -0.0031310968480105046]], [[-0.007955856657996016, + -0.02374955428721689, -0.03263677274980321, -0.01893131299789854, + -0.003333344356080023], [-0.03375669845324461, 0.02230594207226229, + 0.10805612743320021, -0.02867568460500625, -0.01522847594949364], + [-0.05272168029782533, 0.28500998074905703, 0.7151797455936236, + 0.05407331061554741, -0.0237705260199403], [-0.03608088767126984, + 0.05686230181726652, 0.18459474318150484, -0.017286364047844702, + -0.016361868093055783], [-0.01064002753164297, -0.03153980584721341, + -0.04383615619088236, -0.025112025855528356, -0.004465109850519687]], + [[-0.006124077091711165, -0.022324728394118264, -0.03348047712449866, + -0.020703389758681568, -0.004582234640385924], [-0.02745350495005966, + 0.0015983184753035565, 0.11452620166036628, -0.016056808817843167, + -0.020703389758681568], [-0.04499286779343149, 0.19446599876518122, + 0.7498250634433568, 0.1145262016603663, -0.03348047712449867], + [-0.02964168716094745, 0.027274160377919326, 0.1944659987651812, + 0.0015983184753035424, -0.022324728394118264], [-0.008199753617852345, + -0.02964168716094745, -0.04499286779343149, -0.027453504950059663, + -0.006124077091711164]], [[-0.004582234640385923, -0.02070338975868156, + -0.03348047712449866, -0.022324728394118268, -0.006124077091711164], + [-0.020703389758681568, -0.01605680881784317, 0.11452620166036626, + 0.0015983184753035535, -0.02745350495005966], [-0.03348047712449866, + 0.11452620166036631, 0.7498250634433566, 0.1944659987651812, + -0.04499286779343149], [-0.022324728394118268, 0.0015983184753035503, + 0.19446599876518122, 0.02727416037791932, -0.02964168716094745], + [-0.006124077091711164, -0.027453504950059653, -0.04499286779343149, + -0.02964168716094745, -0.008199753617852345]], [[-0.0033333443560800216, + -0.018931312997898523, -0.0326367727498032, -0.023749554287216878, + -0.007955856657996013], [-0.015228475949493635, -0.028675684605006243, + 0.10805612743320019, 0.022305942072262285, -0.0337566984532446], + [-0.02377052601994029, 0.05407331061554739, 0.7151797455936234, + 0.2850099807490569, -0.052721680297825306], [-0.016361868093055777, + -0.0172863640478447, 0.18459474318150482, 0.05686230181726653, + -0.03608088767126982], [-0.004465109850519686, -0.025112025855528346, + -0.04383615619088235, -0.03153980584721339, -0.010640027531642966]], + [[-0.002316627442432311, -0.01684919502363354, -0.031013880437287023, + -0.025259934728481207, -0.010058190693394592], [-0.011256197301616293, + -0.036002834684833764, 0.09621636053079634, 0.04338227748703875, + -0.03814886304138625], [-0.01763265975706309, 0.009920547334197437, + 0.6502702298731253, 0.38194295165025, -0.05151616273340589], + [-0.012180160279609383, -0.029462659514994483, 0.16637288777763276, + 0.08734505711506496, -0.03965629331558995], [-0.0031310968480105046, + -0.022317053587060733, -0.04158013952905281, -0.03365072121724162, + -0.013417641633011903]], [[-0.00153490871475353, -0.014560933634183603, + -0.028882977402423966, -0.02685065924927412, -0.012244853215160445], + [-0.008138951872408833, -0.039195452816331904, 0.08045180751196822, + 0.06284440084948137, -0.03979621423581154], [-0.013176682690936204, + -0.019741250657301448, 0.5659339775075575, 0.47416733354946317, + -0.04046620032608075], [-0.008907053401106528, -0.036203985617015634, + 0.1422954970729258, 0.1157247211529763, -0.0395414785505308], + [-0.002101562167115731, -0.01930821727497103, -0.03866988475938145, + -0.035832545662066166, -0.01626392539751838]]], [[[-0.00901973495399775, + -0.01936783337275035, -0.02070211170009259, -0.010740155274818482, + -0.001077826960040027], [-0.039000354140279844, 0.022877571178541386, + 0.033694739393926795, -0.04044013741654317, -0.007309372111524048], + [-0.040021010867420236, 0.452206427023829, 0.5398093391410694, + -0.020308275361446707, -0.01288326617180421], [-0.038386325754271394, + 0.1756761813778117, 0.21238920010551754, -0.031939432194582666, + -0.00957114638494601], [-0.020664074967504838, -0.042468451339058966, + -0.04562658821385713, -0.024336140047717003, -0.002791220988040243]], + [[-0.007380297997922877, -0.018321074249868192, -0.022137366266623233, + -0.012434273033433195, -0.0016530642487971611], [-0.0360731700595701, + 0.01027889879305376, 0.04314806591631964, -0.03995711361228531, + -0.010225452530604959], [-0.05045706349393281, 0.3640223411093611, + 0.6202613509857569, 0.007986619792820032, -0.017237252814827183], + [-0.04052736449955128, 0.13727831636454993, 0.24585519478421136, + -0.021178184193661704, -0.012971303942569708], [-0.017203222289937245, + -0.040174650139157116, -0.04887867620762645, -0.027909222866276163, + -0.004112035299428132]], [[-0.005819138225232015, -0.01734151061590863, + -0.02320851545865194, -0.0139437312173516, -0.0024085358814646654], + [-0.031173068184164845, -0.0035377934166333767, 0.05018047922229021, + -0.03637638898995257, -0.013943731217351596], [-0.05142937279813893, + 0.27129990888960803, 0.6821432561027146, 0.050180479222290235, + -0.023208515458651942], [-0.0379761971054064, 0.0962810362260875, + 0.27129990888960803, -0.0035377934166333793, -0.017341510615908634], + [-0.01374148963820583, -0.0379761971054064, -0.05142937279813894, + -0.031173068184164855, -0.005819138225232018]], [[-0.004465109850519687, + -0.016361868093055777, -0.023770526019940293, -0.015228475949493638, + -0.0033333443560800233], [-0.02511202585552834, -0.017286364047844695, + 0.054073310615547376, -0.028675684605006243, -0.018931312997898533], + [-0.04383615619088236, 0.1845947431815049, 0.7151797455936236, + 0.10805612743320021, -0.03263677274980321], [-0.03153980584721341, + 0.05686230181726654, 0.285009980749057, 0.02230594207226228, + -0.023749554287216885], [-0.010640027531642969, -0.03608088767126984, + -0.052721680297825334, -0.03375669845324461, -0.007955856657996016]], + [[-0.003333344356080022, -0.015228475949493635, -0.023770526019940282, + -0.016361868093055777, -0.004465109850519686], [-0.018931312997898526, + -0.02867568460500624, 0.054073310615547356, -0.01728636404784469, + -0.02511202585552834], [-0.0326367727498032, 0.10805612743320023, + 0.7151797455936234, 0.18459474318150484, -0.04383615619088235], + [-0.023749554287216878, 0.022305942072262296, 0.285009980749057, + 0.05686230181726651, -0.03153980584721339], [-0.007955856657996014, + -0.033756698453244596, -0.05272168029782531, -0.03608088767126983, + -0.010640027531642967]], [[-0.002408535881464665, -0.013943731217351592, + -0.023208515458651935, -0.017341510615908627, -0.005819138225232014], + [-0.013943731217351592, -0.03637638898995256, 0.050180479222290214, + -0.003537793416633366, -0.031173068184164845], [-0.02320851545865193, + 0.05018047922229023, 0.6821432561027146, 0.271299908889608, + -0.051429372798138924], [-0.017341510615908627, -0.003537793416633378, + 0.271299908889608, 0.0962810362260875, -0.03797619710540638], + [-0.005819138225232016, -0.03117306818416484, -0.05142937279813893, + -0.03797619710540639, -0.013741489638205826]], [[-0.0016530642487971614, + -0.012434273033433195, -0.022137366266623233, -0.01832107424986819, + -0.007380297997922878], [-0.01022545253060496, -0.03995711361228531, + 0.04314806591631963, 0.010278898793053765, -0.0360731700595701], + [-0.017237252814827183, 0.007986619792820022, 0.6202613509857569, + 0.3640223411093611, -0.05045706349393281], [-0.012971303942569706, + -0.021178184193661718, 0.24585519478421136, 0.13727831636454998, + -0.04052736449955127], [-0.004112035299428132, -0.027909222866276156, + -0.04887867620762645, -0.04017465013915711, -0.017203222289937245]], + [[-0.0010778269600400273, -0.010740155274818482, -0.02070211170009259, + -0.01936783337275035, -0.009019734953997749], [-0.007309372111524049, + -0.04044013741654317, 0.03369473939392679, 0.022877571178541403, + -0.039000354140279844], [-0.012883266171804212, -0.020308275361446713, + 0.5398093391410693, 0.4522064270238289, -0.04002101086742022], + [-0.00957114638494601, -0.031939432194582666, 0.2123892001055175, + 0.17567618137781177, -0.03838632575427138], [-0.002791220988040243, + -0.024336140047717003, -0.04562658821385713, -0.04246845133905897, + -0.020664074967504838]]], [[[-0.006466489422914399, -0.014502394951723476, + -0.015459264122748746, -0.007755474877032561, -0.0007159165805851706], + [-0.036632851477625676, -0.005730455022054154, -7.890574314417718e-05, + -0.03942402104403913, -0.006389875713059275], [-0.039015079941410555, + 0.4111930051936302, 0.4909386897413152, -0.021181804710686668, + -0.012319067611648218], [-0.03752601069182331, 0.239032194778253, + 0.2868949551896562, -0.027885740619110408, -0.010202069931803578], + [-0.025194061504750533, -0.04314593981787028, -0.04562755195174027, + -0.029204772772557768, -0.003601096394526257]], [[-0.005252595229206095, + -0.013729503292946928, -0.016482266055193047, -0.009045186119473492, + -0.0011168422627331079], [-0.03293190035303923, -0.013001053856678081, + 0.004583518760872412, -0.040827417160105635, -0.009045186119473489], + [-0.04831487472573022, 0.33063684267898774, 0.5640812622041926, + 0.004583518760872408, -0.01648226605519305], [-0.04173044153813555, + 0.18968272846778533, 0.3306368426789878, -0.01300105385667808, + -0.013729503292946928], [-0.02128481178805433, -0.041730441538135564, + -0.048314874725730234, -0.03293190035303923, -0.005252595229206095]], + [[-0.004112035299428132, -0.012971303942569708, -0.017237252814827186, + -0.010225452530604966, -0.0016530642487971618], [-0.027909222866276156, + -0.02117818419366171, 0.007986619792820025, -0.039957113612285315, + -0.0124342730334332], [-0.04887867620762645, 0.24585519478421144, + 0.620261350985757, 0.04314806591631966, -0.022137366266623243], + [-0.040174650139157116, 0.13727831636454998, 0.3640223411093612, + 0.010278898793053765, -0.018321074249868192], [-0.017203222289937245, + -0.04052736449955129, -0.05045706349393283, -0.03607317005957011, + -0.007380297997922882]], [[-0.0031310968480105046, -0.012180160279609381, + -0.017632659757063088, -0.011256197301616295, -0.0023166274424323108], + [-0.022317053587060723, -0.02946265951499447, 0.009920547334197437, + -0.036002834684833764, -0.016849195023633544], [-0.0415801395290528, + 0.16637288777763282, 0.6502702298731252, 0.09621636053079637, + -0.031013880437287027], [-0.03365072121724162, 0.08734505711506496, + 0.38194295165025005, 0.04338227748703875, -0.025259934728481197], + [-0.013417641633011903, -0.03965629331558996, -0.05151616273340592, + -0.03814886304138624, -0.010058190693394595]], [[-0.0023166274424323103, + -0.011256197301616293, -0.017632659757063088, -0.012180160279609383, + -0.0031310968480105046], [-0.016849195023633544, -0.036002834684833764, + 0.009920547334197428, -0.029462659514994466, -0.02231705358706073], + [-0.03101388043728702, 0.0962163605307964, 0.6502702298731252, + 0.1663728877776328, -0.041580139529052804], [-0.025259934728481197, + 0.043382277487038774, 0.38194295165025005, 0.08734505711506493, + -0.033650721217241615], [-0.010058190693394593, -0.03814886304138624, + -0.05151616273340592, -0.03965629331558995, -0.013417641633011903]], + [[-0.0016530642487971614, -0.01022545253060496, -0.017237252814827183, + -0.012971303942569706, -0.00411203529942813], [-0.012434273033433196, + -0.03995711361228531, 0.007986619792820018, -0.021178184193661697, + -0.027909222866276152], [-0.022137366266623233, 0.04314806591631965, + 0.6202613509857569, 0.24585519478421133, -0.04887867620762643], + [-0.018321074249868185, 0.010278898793053763, 0.3640223411093611, + 0.13727831636454993, -0.040174650139157095], [-0.00738029799792288, + -0.0360731700595701, -0.05045706349393282, -0.04052736449955128, + -0.01720322228993724]], [[-0.0011168422627331075, -0.009045186119473489, + -0.016482266055193047, -0.013729503292946922, -0.005252595229206093], + [-0.00904518611947349, -0.04082741716010563, 0.004583518760872401, + -0.01300105385667807, -0.032931900353039216], [-0.016482266055193047, + 0.004583518760872399, 0.5640812622041926, 0.33063684267898774, + -0.0483148747257302], [-0.013729503292946922, -0.01300105385667808, + 0.33063684267898774, 0.1896827284677853, -0.04173044153813553], + [-0.005252595229206094, -0.03293190035303922, -0.048314874725730206, + -0.04173044153813555, -0.021284811788054327]], [[-0.0007159165805851706, + -0.00775547487703256, -0.015459264122748746, -0.014502394951723471, + -0.006466489422914398], [-0.006389875713059273, -0.039424021044039116, + -7.890574314417675e-05, -0.0057304550220541334, -0.03663285147762567], + [-0.012319067611648212, -0.02118180471068667, 0.4909386897413151, + 0.4111930051936303, -0.03901507994141054], [-0.010202069931803573, + -0.02788574061911041, 0.28689495518965613, 0.23903219477825297, + -0.037526010691823306], [-0.0036010963945262557, -0.029204772772557758, + -0.04562755195174025, -0.043145939817870266, -0.025194061504750526]]], + [[[-0.004476318148146651, -0.010817484288013232, -0.011552729379100072, + -0.005422291349995998, -0.0004507227386051219], [-0.03324558280295301, + -0.02447552237556967, -0.022489385288542604, -0.03680917952171095, + -0.005422291349995998], [-0.0378381168526885, 0.35757708121641424, + 0.4272005024152728, -0.022489385288542604, -0.011552729379100074], + [-0.0370635264420727, 0.2989532845474528, 0.35757708121641435, + -0.024475522375569658, -0.010817484288013228], [-0.029286133281073247, + -0.037063526442072704, -0.037838116852688494, -0.03324558280295301, + -0.00447631814814665]], [[-0.0036010963945262574, -0.01020206993180358, + -0.012319067611648216, -0.006389875713059276, -0.0007159165805851708], + [-0.029204772772557765, -0.02788574061911041, -0.02118180471068666, + -0.03942402104403913, -0.007755474877032563], [-0.04562755195174027, + 0.2868949551896562, 0.490938689741315, -7.890574314416898e-05, + -0.015459264122748749], [-0.043145939817870266, 0.239032194778253, + 0.41119300519363033, -0.005730455022054142, -0.014502394951723474], + [-0.025194061504750533, -0.03752601069182331, -0.039015079941410555, + -0.036632851477625676, -0.0064664894229144]], [[-0.002791220988040242, + -0.009571146384946008, -0.012883266171804207, -0.007309372111524051, + -0.0010778269600400271], [-0.024336140047716993, -0.03193943219458266, + -0.020308275361446703, -0.040440137416543155, -0.010740155274818482], + [-0.04562658821385712, 0.2123892001055175, 0.5398093391410692, + 0.033694739393926816, -0.020702111700092587], [-0.042468451339058945, + 0.1756761813778117, 0.4522064270238289, 0.022877571178541396, + -0.019367833372750342], [-0.020664074967504824, -0.03838632575427138, + -0.040021010867420236, -0.03900035414027984, -0.00901973495399775]], + [[-0.0021015621671157296, -0.008907053401106525, -0.013176682690936196, + -0.008138951872408831, -0.0015349087147535293], [-0.019308217274971017, + -0.03620398561701561, -0.019741250657301434, -0.03919545281633189, + -0.014560933634183596], [-0.03866988475938143, 0.1422954970729258, + 0.5659339775075574, 0.0804518075119682, -0.028882977402423963], + [-0.035832545662066145, 0.11572472115297625, 0.47416733354946283, + 0.06284440084948135, -0.026850659249274097], [-0.01626392539751837, + -0.039541478550530786, -0.04046620032608076, -0.03979621423581152, + -0.012244853215160443]], [[-0.0015349087147535293, -0.008138951872408831, + -0.013176682690936201, -0.008907053401106528, -0.00210156216711573], + [-0.0145609336341836, -0.0391954528163319, -0.019741250657301437, + -0.03620398561701563, -0.01930821727497102], [-0.028882977402423963, + 0.08045180751196825, 0.5659339775075575, 0.14229549707292577, + -0.038669884759381434], [-0.0268506592492741, 0.06284440084948138, + 0.47416733354946305, 0.11572472115297622, -0.03583254566206615], + [-0.012244853215160445, -0.03979621423581153, -0.04046620032608077, + -0.03954147855053079, -0.016263925397518374]], [[-0.0010778269600400273, + -0.00730937211152405, -0.01288326617180421, -0.00957114638494601, + -0.002791220988040242], [-0.010740155274818482, -0.04044013741654317, + -0.020308275361446707, -0.031939432194582666, -0.024336140047717], + [-0.020702111700092594, 0.03369473939392682, 0.5398093391410693, + 0.21238920010551754, -0.04562658821385713], [-0.019367833372750342, + 0.022877571178541396, 0.452206427023829, 0.17567618137781177, + -0.04246845133905895], [-0.009019734953997752, -0.039000354140279844, + -0.040021010867420236, -0.03838632575427138, -0.02066407496750483]], + [[-0.0007159165805851705, -0.006389875713059274, -0.012319067611648212, + -0.010202069931803576, -0.0036010963945262565], [-0.007755474877032563, + -0.03942402104403912, -0.02118180471068666, -0.0278857406191104, + -0.029204772772557758], [-0.015459264122748742, -7.890574314417695e-05, + 0.4909386897413151, 0.2868949551896562, -0.04562755195174026], + [-0.01450239495172347, -0.0057304550220541465, 0.4111930051936302, + 0.23903219477825297, -0.04314593981787025], [-0.0064664894229144, + -0.03663285147762567, -0.03901507994141055, -0.037526010691823306, + -0.025194061504750526]], [[-0.0004507227386051219, -0.005422291349995998, + -0.01155272937910007, -0.01081748428801323, -0.00447631814814665], + [-0.005422291349995998, -0.03680917952171095, -0.022489385288542607, + -0.024475522375569655, -0.03324558280295302], [-0.011552729379100074, + -0.02248938528854261, 0.4272005024152728, 0.3575770812164143, + -0.0378381168526885], [-0.010817484288013228, -0.02447552237556967, + 0.3575770812164143, 0.29895328454745285, -0.03706352644207268], + [-0.00447631814814665, -0.03324558280295302, -0.0378381168526885, + -0.03706352644207269, -0.029286133281073243]]]] +} + +if __name__ == "__main__": + main()
\ No newline at end of file diff --git a/media/libjxl/src/tools/viewer/CMakeLists.txt b/media/libjxl/src/tools/viewer/CMakeLists.txt new file mode 100644 index 0000000000..7dbe5e3153 --- /dev/null +++ b/media/libjxl/src/tools/viewer/CMakeLists.txt @@ -0,0 +1,39 @@ +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +find_package(Qt5 QUIET COMPONENTS Widgets) +if (NOT Qt5_FOUND) + message(WARNING "Qt5 was not found. The directory viewer will not be built.") + return() +endif () + +if (NOT TARGET icc_detect) + message(WARNING "The directory viewer depends on the comparison tool and will also not be built.") + return () +endif () + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +add_executable(viewer WIN32 + load_jxl.cc + load_jxl.h + main.cc + viewer_window.cc + viewer_window.h + viewer_window.ui +) +target_include_directories(viewer PRIVATE + $<TARGET_PROPERTY:lcms2,INCLUDE_DIRECTORIES> + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries(viewer + Qt5::Widgets + icc_detect + jxl + jxl_threads + lcms2 +) diff --git a/media/libjxl/src/tools/viewer/load_jxl.cc b/media/libjxl/src/tools/viewer/load_jxl.cc new file mode 100644 index 0000000000..7fd35d8224 --- /dev/null +++ b/media/libjxl/src/tools/viewer/load_jxl.cc @@ -0,0 +1,174 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/viewer/load_jxl.h" + +#include <stdint.h> + +#include <QElapsedTimer> +#include <QFile> + +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/thread_parallel_runner_cxx.h" +#include "jxl/types.h" +#include "lcms2.h" + +namespace jxl { + +namespace { + +struct CmsProfileCloser { + void operator()(const cmsHPROFILE profile) const { + if (profile != nullptr) { + cmsCloseProfile(profile); + } + } +}; +using CmsProfileUniquePtr = + std::unique_ptr<std::remove_pointer<cmsHPROFILE>::type, CmsProfileCloser>; + +struct CmsTransformDeleter { + void operator()(const cmsHTRANSFORM transform) const { + if (transform != nullptr) { + cmsDeleteTransform(transform); + } + } +}; +using CmsTransformUniquePtr = + std::unique_ptr<std::remove_pointer<cmsHTRANSFORM>::type, + CmsTransformDeleter>; + +} // namespace + +QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile, + qint64* elapsed_ns, bool* usedRequestedProfile) { + auto runner = JxlThreadParallelRunnerMake( + nullptr, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + + auto dec = JxlDecoderMake(nullptr); + +#define EXPECT_TRUE(a) \ + do { \ + if (!(a)) { \ + fprintf(stderr, "Assertion failure (%d): %s\n", __LINE__, #a); \ + return QImage(); \ + } \ + } while (false) +#define EXPECT_EQ(a, b) \ + do { \ + int a_ = a; \ + int b_ = b; \ + if (a_ != b_) { \ + fprintf(stderr, "Assertion failure (%d): %s (%d) != %s (%d)\n", \ + __LINE__, #a, a_, #b, b_); \ + return QImage(); \ + } \ + } while (false) + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | + JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE)); + QFile jpegXlFile(filename); + if (!jpegXlFile.open(QIODevice::ReadOnly)) { + return QImage(); + } + const QByteArray jpegXlData = jpegXlFile.readAll(); + if (jpegXlData.size() < 4) { + return QImage(); + } + + QElapsedTimer timer; + timer.start(); + const uint8_t* jxl_data = reinterpret_cast<const uint8_t*>(jpegXlData.data()); + size_t jxl_size = jpegXlData.size(); + JxlDecoderSetInput(dec.get(), jxl_data, jxl_size); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec.get())); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec.get(), &info)); + size_t pixel_count = info.xsize * info.ysize; + + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec.get())); + static const JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, + 0}; + size_t icc_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize( + dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)); + std::vector<uint8_t> icc_profile(icc_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsICCProfile( + dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, + icc_profile.data(), icc_profile.size())); + + std::vector<float> float_pixels(pixel_count * 4); + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec.get())); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer(dec.get(), &format, float_pixels.data(), + pixel_count * 4 * sizeof(float))); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec.get())); + + std::vector<uint16_t> uint16_pixels(pixel_count * 4); + const thread_local cmsContext context = cmsCreateContext(nullptr, nullptr); + EXPECT_TRUE(context != nullptr); + const CmsProfileUniquePtr jxl_profile(cmsOpenProfileFromMemTHR( + context, icc_profile.data(), icc_profile.size())); + EXPECT_TRUE(jxl_profile != nullptr); + CmsProfileUniquePtr target_profile(cmsOpenProfileFromMemTHR( + context, targetIccProfile.data(), targetIccProfile.size())); + if (usedRequestedProfile != nullptr) { + *usedRequestedProfile = (target_profile != nullptr); + } + if (target_profile == nullptr) { + target_profile.reset(cmsCreate_sRGBProfileTHR(context)); + } + EXPECT_TRUE(target_profile != nullptr); + CmsTransformUniquePtr transform(cmsCreateTransformTHR( + context, jxl_profile.get(), TYPE_RGBA_FLT, target_profile.get(), + TYPE_RGBA_16, INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_COPY_ALPHA)); + EXPECT_TRUE(transform != nullptr); + cmsDoTransform(transform.get(), float_pixels.data(), uint16_pixels.data(), + pixel_count); + if (elapsed_ns != nullptr) *elapsed_ns = timer.nsecsElapsed(); + + QImage result(info.xsize, info.ysize, +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + info.alpha_premultiplied ? QImage::Format_RGBA64_Premultiplied + : QImage::Format_RGBA64 +#else + info.alpha_premultiplied ? QImage::Format_ARGB32_Premultiplied + : QImage::Format_ARGB32 +#endif + ); + + for (int y = 0; y < result.height(); ++y) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + QRgba64* const row = reinterpret_cast<QRgba64*>(result.scanLine(y)); +#else + QRgb* const row = reinterpret_cast<QRgb*>(result.scanLine(y)); +#endif + const uint16_t* const data = uint16_pixels.data() + result.width() * y * 4; + for (int x = 0; x < result.width(); ++x) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + row[x] = qRgba64(data[4 * x + 0], data[4 * x + 1], data[4 * x + 2], + data[4 * x + 3]) +#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0) + .toArgb32() +#endif + ; +#else + // Qt version older than 5.6 doesn't have a qRgba64. + row[x] = qRgba(data[4 * x + 0] * (255.f / 65535) + .5f, + data[4 * x + 1] * (255.f / 65535) + .5f, + data[4 * x + 2] * (255.f / 65535) + .5f, + data[4 * x + 3] * (255.f / 65535) + .5f); +#endif + } + } + return result; +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/viewer/load_jxl.h b/media/libjxl/src/tools/viewer/load_jxl.h new file mode 100644 index 0000000000..594f646f01 --- /dev/null +++ b/media/libjxl/src/tools/viewer/load_jxl.h @@ -0,0 +1,20 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_VIEWER_LOAD_JXL_H_ +#define TOOLS_VIEWER_LOAD_JXL_H_ + +#include <QByteArray> +#include <QImage> +#include <QString> + +namespace jxl { + +QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile, + qint64* elapsed, bool* usedRequestedProfile = nullptr); + +} // namespace jxl + +#endif // TOOLS_VIEWER_LOAD_JXL_H_ diff --git a/media/libjxl/src/tools/viewer/main.cc b/media/libjxl/src/tools/viewer/main.cc new file mode 100644 index 0000000000..d677888f61 --- /dev/null +++ b/media/libjxl/src/tools/viewer/main.cc @@ -0,0 +1,23 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <QApplication> + +#include "tools/viewer/viewer_window.h" + +int main(int argc, char** argv) { + QApplication application(argc, argv); + QStringList arguments = application.arguments(); + arguments.removeFirst(); + + jxl::ViewerWindow window; + window.show(); + + if (!arguments.empty()) { + window.loadFilesAndDirectories(arguments); + } + + return application.exec(); +} diff --git a/media/libjxl/src/tools/viewer/viewer_window.cc b/media/libjxl/src/tools/viewer/viewer_window.cc new file mode 100644 index 0000000000..530c2f0141 --- /dev/null +++ b/media/libjxl/src/tools/viewer/viewer_window.cc @@ -0,0 +1,130 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/viewer/viewer_window.h" + +#include <QElapsedTimer> +#include <QFileDialog> +#include <QFileInfo> +#include <QKeyEvent> +#include <QMessageBox> +#include <QSet> + +#include "tools/icc_detect/icc_detect.h" +#include "tools/viewer/load_jxl.h" + +namespace jxl { + +namespace { + +template <typename Output> +void recursivelyAddSubEntries(const QFileInfo& info, + QSet<QString>* const visited, + Output* const output) { + if (visited->contains(info.absoluteFilePath())) return; + *visited << info.absoluteFilePath(); + if (info.isDir()) { + QDir dir(info.absoluteFilePath()); + for (const QFileInfo& entry : dir.entryInfoList( + QStringList() << "*.jxl", + QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot)) { + recursivelyAddSubEntries(entry, visited, output); + } + } else { + *output << info.absoluteFilePath(); + } +} + +} // namespace + +ViewerWindow::ViewerWindow(QWidget* const parent) + : QMainWindow(parent), monitorProfile_(GetMonitorIccProfile(this)) { + ui_.setupUi(this); + ui_.actionOpen->setShortcut(QKeySequence::Open); + ui_.actionExit->setShortcut(QKeySequence::Quit); +} + +void ViewerWindow::loadFilesAndDirectories(QStringList entries) { + filenames_.clear(); + QSet<QString> visited; + for (const QString& entry : entries) { + recursivelyAddSubEntries(entry, &visited, &filenames_); + } + + const bool several = filenames_.size() > 1; + ui_.actionPreviousImage->setEnabled(several); + ui_.actionNextImage->setEnabled(several); + + currentFileIndex_ = 0; + refreshImage(); +} + +void ViewerWindow::on_actionOpen_triggered() { + QFileDialog dialog(this, tr("Select JPEG XL files to open…")); + dialog.setFileMode(QFileDialog::ExistingFiles); + dialog.setNameFilter(tr("JPEG XL images (*.jxl);;All files (*)")); + if (dialog.exec()) { + loadFilesAndDirectories(dialog.selectedFiles()); + } +} + +void ViewerWindow::on_actionPreviousImage_triggered() { + currentFileIndex_ = + (currentFileIndex_ - 1 + filenames_.size()) % filenames_.size(); + refreshImage(); +} + +void ViewerWindow::on_actionNextImage_triggered() { + currentFileIndex_ = (currentFileIndex_ + 1) % filenames_.size(); + refreshImage(); +} + +void ViewerWindow::refreshImage() { + if (currentFileIndex_ < 0 || currentFileIndex_ >= filenames_.size()) { + return; + } + + qint64 elapsed_ns; + bool usedRequestedProfile; + const QImage image = + loadJxlImage(filenames_[currentFileIndex_], monitorProfile_, &elapsed_ns, + &usedRequestedProfile); + if (image.isNull()) { + const QString message = + tr("Failed to load \"%1\".").arg(filenames_[currentFileIndex_]); + ui_.image->clear(); + ui_.statusBar->showMessage(message); + QMessageBox errorDialog(this); + errorDialog.setIcon(QMessageBox::Critical); + errorDialog.setWindowTitle(tr("Failed to load image")); + errorDialog.setText(message); + errorDialog.exec(); + return; + } + + ui_.image->setPixmap(QPixmap::fromImage(image)); + ui_.statusBar->showMessage( + tr("Loaded image %L1/%L2 (%3, %4×%5) in %L6ms (%L7 fps)") + .arg(currentFileIndex_ + 1) + .arg(filenames_.size()) + .arg(filenames_[currentFileIndex_]) + .arg(image.width()) + .arg(image.height()) + .arg(elapsed_ns / 1e6) + .arg(1e9 / elapsed_ns)); + + if (!usedRequestedProfile && !hasWarnedAboutMonitorProfile_) { + hasWarnedAboutMonitorProfile_ = true; + QMessageBox message(this); + message.setIcon(QMessageBox::Warning); + message.setWindowTitle(tr("No valid monitor profile found")); + message.setText( + tr("Failed to find a usable monitor profile. Images will be shown " + "assuming that the monitor's colorspace is sRGB.")); + message.exec(); + } +} + +} // namespace jxl diff --git a/media/libjxl/src/tools/viewer/viewer_window.h b/media/libjxl/src/tools/viewer/viewer_window.h new file mode 100644 index 0000000000..42de5bc267 --- /dev/null +++ b/media/libjxl/src/tools/viewer/viewer_window.h @@ -0,0 +1,41 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_VIEWER_VIEWER_WINDOW_H_ +#define TOOLS_VIEWER_VIEWER_WINDOW_H_ + +#include <QByteArray> +#include <QMainWindow> +#include <QStringList> + +#include "tools/viewer/ui_viewer_window.h" + +namespace jxl { + +class ViewerWindow : public QMainWindow { + Q_OBJECT + public: + explicit ViewerWindow(QWidget* parent = nullptr); + + public slots: + void loadFilesAndDirectories(QStringList entries); + + private slots: + void on_actionOpen_triggered(); + void on_actionPreviousImage_triggered(); + void on_actionNextImage_triggered(); + void refreshImage(); + + private: + const QByteArray monitorProfile_; + Ui::ViewerWindow ui_; + QStringList filenames_; + int currentFileIndex_ = 0; + bool hasWarnedAboutMonitorProfile_ = false; +}; + +} // namespace jxl + +#endif // TOOLS_VIEWER_VIEWER_WINDOW_H_ diff --git a/media/libjxl/src/tools/viewer/viewer_window.ui b/media/libjxl/src/tools/viewer/viewer_window.ui new file mode 100644 index 0000000000..9539890550 --- /dev/null +++ b/media/libjxl/src/tools/viewer/viewer_window.ui @@ -0,0 +1,125 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <comment> + Copyright (c) the JPEG XL Project Authors. All rights reserved. + + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. + </comment> + <class>ViewerWindow</class> + <widget class="QMainWindow" name="ViewerWindow"> + <property name="windowTitle"> + <string>JPEG XL Viewer</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QScrollArea" name="scrollArea"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QLabel" name="image"> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menuBar"> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>&File</string> + </property> + <addaction name="actionOpen"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <addaction name="menuFile"/> + </widget> + <widget class="QStatusBar" name="statusBar"/> + <widget class="QToolBar" name="toolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionOpen"/> + <addaction name="actionPreviousImage"/> + <addaction name="actionNextImage"/> + </widget> + <action name="actionOpen"> + <property name="icon"> + <iconset theme="document-open"/> + </property> + <property name="text"> + <string>&Open…</string> + </property> + <property name="menuRole"> + <enum>QAction::NoRole</enum> + </property> + </action> + <action name="actionExit"> + <property name="icon"> + <iconset theme="application-exit"/> + </property> + <property name="text"> + <string>E&xit</string> + </property> + <property name="menuRole"> + <enum>QAction::QuitRole</enum> + </property> + </action> + <action name="actionPreviousImage"> + <property name="icon"> + <iconset theme="go-previous"/> + </property> + <property name="text"> + <string>Previous image</string> + </property> + <property name="shortcut"> + <string>Left</string> + </property> + </action> + <action name="actionNextImage"> + <property name="icon"> + <iconset theme="go-next"/> + </property> + <property name="text"> + <string>Next image</string> + </property> + <property name="shortcut"> + <string>Right</string> + </property> + </action> + </widget> + <resources/> + <connections> + <connection> + <sender>actionExit</sender> + <signal>triggered()</signal> + <receiver>ViewerWindow</receiver> + <slot>close()</slot> + </connection> + </connections> +</ui> diff --git a/media/libjxl/src/tools/xyb_range.cc b/media/libjxl/src/tools/xyb_range.cc new file mode 100644 index 0000000000..1ce4882242 --- /dev/null +++ b/media/libjxl/src/tools/xyb_range.cc @@ -0,0 +1,80 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> + +#include <utility> + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { +namespace { + +void PrintXybRange() { + Image3F linear(1u << 16, 257); + for (int b = 0; b < 256; ++b) { + float* JXL_RESTRICT row0 = linear.PlaneRow(0, b + 1); + float* JXL_RESTRICT row1 = linear.PlaneRow(1, b + 1); + float* JXL_RESTRICT row2 = linear.PlaneRow(2, b + 1); + for (int r = 0; r < 256; ++r) { + for (int g = 0; g < 256; ++g) { + const int x = (r << 8) + g; + row0[x] = r; + row1[x] = g; + row2[x] = b; + } + } + } + CodecInOut io; + io.metadata.m.SetUintSamples(8); + io.metadata.m.color_encoding = ColorEncoding::LinearSRGB(); + io.SetFromImage(std::move(linear), io.metadata.m.color_encoding); + const ImageBundle& ib = io.Main(); + ThreadPool* null_pool = nullptr; + Image3F opsin(ib.xsize(), ib.ysize()); + (void)ToXYB(ib, null_pool, &opsin, GetJxlCms()); + for (size_t c = 0; c < 3; ++c) { + float minval = 1e10f; + float maxval = -1e10f; + int rgb_min = 0; + int rgb_max = 0; + for (int b = 0; b < 256; ++b) { + const float* JXL_RESTRICT row = opsin.PlaneRow(c, b); + for (int r = 0; r < 256; ++r) { + for (int g = 0; g < 256; ++g) { + float val = row[(r << 8) + g]; + if (val < minval) { + minval = val; + rgb_min = (r << 16) + (g << 8) + b; + } + if (val > maxval) { + maxval = val; + rgb_max = (r << 16) + (g << 8) + b; + } + } + } + } + printf("Opsin image plane %" PRIuS + " range: [%8.4f, %8.4f] " + "center: %.12f, range: %.12f (RGBmin=%06x, RGBmax=%06x)\n", + c, minval, maxval, 0.5 * (minval + maxval), 0.5 * (maxval - minval), + rgb_min, rgb_max); + // Ensure our constants are at least as wide as those obtained from sRGB. + } +} + +} // namespace +} // namespace jxl + +int main() { jxl::PrintXybRange(); } |