diff options
Diffstat (limited to 'tools')
167 files changed, 9419 insertions, 3911 deletions
diff --git a/tools/BUILD b/tools/BUILD new file mode 100644 index 0000000..58e0e9a --- /dev/null +++ b/tools/BUILD @@ -0,0 +1 @@ +package(default_visibility = ["//:__subpackages__"]) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 934ed89..7701e4d 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -6,39 +6,39 @@ # 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.") + find_package(Qt6 QUIET COMPONENTS Widgets) + if (NOT Qt6_FOUND) + message(WARNING "Qt6 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) + target_link_libraries(icc_detect PUBLIC Qt6::Widgets) if(JPEGXL_DEP_LICENSE_DIR) - configure_file("${JPEGXL_DEP_LICENSE_DIR}/libqt5widgets5/copyright" - ${PROJECT_BINARY_DIR}/LICENSE.libqt5widgets5 COPYONLY) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/libqt6widgets6/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.libqt6widgets6 COPYONLY) endif() # JPEGXL_DEP_LICENSE_DIR endif() elseif(APPLE) - find_package(Qt5 QUIET COMPONENTS Widgets) - if (Qt5_FOUND) + find_package(Qt6 QUIET COMPONENTS Widgets) + if (Qt6_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) + target_link_libraries(icc_detect PUBLIC Qt6::Widgets) else() - message(WARNING "APPLE: Qt5 was not found.") + message(WARNING "APPLE: Qt6 was not found.") endif() else() - find_package(Qt5 QUIET COMPONENTS Widgets X11Extras) + find_package(Qt6 QUIET COMPONENTS Widgets) find_package(ECM QUIET NO_MODULE) - if (NOT Qt5_FOUND OR NOT ECM_FOUND) - if (NOT Qt5_FOUND) - message(WARNING "Qt5 was not found.") + if (NOT Qt6_FOUND OR NOT ECM_FOUND) + if (NOT Qt6_FOUND) + message(WARNING "Qt6 was not found.") else() message(WARNING "extra-cmake-modules were not found.") endif() @@ -50,7 +50,7 @@ else() 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) + target_link_libraries(icc_detect PUBLIC jxl-internal Qt6::Widgets XCB::XCB) endif () endif() endif() @@ -60,17 +60,18 @@ endif() # JPEGXL_ENABLE_VIEWERS set(TOOL_BINARIES) # Tools that depend on jxl internal functions. set(INTERNAL_TOOL_BINARIES) +set(FUZZER_CORPUS_BINARIES) add_library(jxl_tool STATIC EXCLUDE_FROM_ALL cmdline.cc codec_config.cc speed_stats.cc - file_io.cc tool_version.cc + ${JXL_CMS_OBJECTS} ) target_compile_options(jxl_tool PUBLIC "${JPEGXL_INTERNAL_FLAGS}") target_include_directories(jxl_tool PUBLIC "${PROJECT_SOURCE_DIR}") -target_link_libraries(jxl_tool hwy) +target_link_libraries(jxl_tool PUBLIC hwy) # The JPEGXL_VERSION is set from the builders. if(NOT DEFINED JPEGXL_VERSION OR JPEGXL_VERSION STREQUAL "") @@ -125,7 +126,7 @@ if(JPEGXL_ENABLE_TOOLS) add_executable(cjxl cjxl_main.cc) target_link_libraries(cjxl jxl - jxl_extras_codec-static + jxl_extras_codec jxl_threads jxl_tool ) @@ -135,14 +136,19 @@ if(JPEGXL_ENABLE_TOOLS) add_executable(djxl djxl_main.cc) target_link_libraries(djxl jxl - jxl_extras_codec-static + jxl_extras_codec jxl_threads jxl_tool ) list(APPEND TOOL_BINARIES djxl) - add_executable(cjpeg_hdr cjpeg_hdr.cc) - list(APPEND INTERNAL_TOOL_BINARIES cjpeg_hdr) + if(JPEGXL_ENABLE_JPEGLI) + # Depends on parts of jxl_extras that are only built if libjpeg is found and + # jpegli is enabled. + add_executable(cjpegli cjpegli.cc) + add_executable(djpegli djpegli.cc) + list(APPEND INTERNAL_TOOL_BINARIES cjpegli djpegli) + endif() add_executable(jxlinfo jxlinfo.c) target_link_libraries(jxlinfo jxl) @@ -160,33 +166,53 @@ endif() # JPEGXL_ENABLE_TOOLS # Other developer tools. if(JPEGXL_ENABLE_DEVTOOLS) list(APPEND INTERNAL_TOOL_BINARIES - fuzzer_corpus butteraugli_main decode_and_encode display_to_hlg + exr_to_pq pq_to_hlg render_hlg + local_tone_map tone_map texture_to_cube generate_lut_template ssimulacra_main + ssimulacra2 xyb_range jxl_from_tree ) - add_executable(fuzzer_corpus fuzzer_corpus.cc) - add_executable(ssimulacra_main ssimulacra_main.cc ssimulacra.cc) + add_executable(ssimulacra2 ssimulacra2_main.cc ssimulacra2.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(exr_to_pq hdr/exr_to_pq.cc) add_executable(pq_to_hlg hdr/pq_to_hlg.cc) add_executable(render_hlg hdr/render_hlg.cc) + add_executable(local_tone_map hdr/local_tone_map.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) + + list(APPEND FUZZER_CORPUS_BINARIES djxl_fuzzer_corpus) + add_executable(djxl_fuzzer_corpus djxl_fuzzer_corpus.cc) + target_link_libraries(djxl_fuzzer_corpus + jxl_extras-internal + jxl_testlib-internal + jxl_tool + ) + if(JPEGXL_ENABLE_JPEGLI) + list(APPEND FUZZER_CORPUS_BINARIES jpegli_dec_fuzzer_corpus) + add_executable(jpegli_dec_fuzzer_corpus jpegli_dec_fuzzer_corpus.cc) + target_link_libraries(jpegli_dec_fuzzer_corpus + jpegli-static + jxl_tool + jxl_threads + ) + endif() endif() # JPEGXL_ENABLE_DEVTOOLS # Benchmark tools. @@ -205,8 +231,11 @@ if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS) benchmark/benchmark_utils.h benchmark/benchmark_codec_custom.cc benchmark/benchmark_codec_custom.h + benchmark/benchmark_codec_jpeg.cc + benchmark/benchmark_codec_jpeg.h benchmark/benchmark_codec_jxl.cc benchmark/benchmark_codec_jxl.h + ssimulacra2.cc ../third_party/dirent.cc ) target_link_libraries(benchmark_xl Threads::Threads) @@ -215,14 +244,6 @@ if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS) 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() @@ -231,6 +252,7 @@ if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS) "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_png.cc" "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_png.h" ) + target_compile_definitions(benchmark_xl PRIVATE -DBENCHMARK_PNG) endif() find_package(PkgConfig) @@ -245,11 +267,16 @@ if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS) # Use the static version of webp if available. find_library(WebP_STATIC_LINK_LIBRARY NAMES libwebp.a PATHS "${WebP_LIBDIR}") + find_library(SharpYuv_STATIC_LINK_LIBRARY NAMES libsharpyuv.a + PATHS "${WebP_LIBDIR}") if(NOT WebP_STATIC_LINK_LIBRARY) message(WARNING "Using dynamic libwebp") target_link_libraries(benchmark_xl PkgConfig::WebP) else() target_link_libraries(benchmark_xl "${WebP_STATIC_LINK_LIBRARY}") + if(SharpYuv_STATIC_LINK_LIBRARY) + target_link_libraries(benchmark_xl "${SharpYuv_STATIC_LINK_LIBRARY}") + endif() target_include_directories(benchmark_xl PRIVATE ${WebP_STATIC_INCLUDE_DIRS}) target_compile_options(benchmark_xl PRIVATE ${WebP_STATIC_CFLAGS_OTHER}) @@ -270,33 +297,46 @@ endif() # JPEGXL_ENABLE_BENCHMARK # All tool binaries depend on "jxl" library and the tool helpers. foreach(BINARY IN LISTS INTERNAL_TOOL_BINARIES) target_link_libraries("${BINARY}" - jxl_extras-static + jxl_extras-internal + jxl_threads jxl_tool ) endforeach() -list(APPEND TOOL_BINARIES ${INTERNAL_TOOL_BINARIES}) +list(APPEND TOOL_BINARIES ${INTERNAL_TOOL_BINARIES} ${FUZZER_CORPUS_BINARIES}) foreach(BINARY IN LISTS TOOL_BINARIES) - if(JPEGXL_EMSCRIPTEN) - set_target_properties(${BINARY} PROPERTIES LINK_FLAGS "-s USE_LIBPNG=1") + if(EMSCRIPTEN) + set(JXL_WASM_TOOLS_LINK_FLAGS "\ + -s USE_LIBPNG=1 \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s USE_PTHREADS=1 \ + -s PTHREAD_POOL_SIZE=16 \ + ") + set_target_properties(${BINARY} PROPERTIES LINK_FLAGS "${JXL_WASM_TOOLS_LINK_FLAGS}") 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 - cjxl_fuzzer - djxl_fuzzer - icc_codec_fuzzer - fields_fuzzer - rans_fuzzer - set_from_bytes_fuzzer - transforms_fuzzer -) +# djxl_fuzzer builds even when not JPEGXL_ENABLE_TOOLS +set(FUZZER_BINARIES djxl_fuzzer) +if(JPEGXL_ENABLE_TOOLS) + list(APPEND FUZZER_BINARIES + color_encoding_fuzzer + decode_basic_info_fuzzer + cjxl_fuzzer + icc_codec_fuzzer + fields_fuzzer + rans_fuzzer + set_from_bytes_fuzzer + transforms_fuzzer + ) +if(JPEGXL_ENABLE_JPEGLI) + list(APPEND FUZZER_BINARIES jpegli_dec_fuzzer) +endif() +endif() # Fuzzers. foreach(FUZZER IN LISTS FUZZER_BINARIES) @@ -314,12 +354,15 @@ foreach(FUZZER IN LISTS FUZZER_BINARIES) target_include_directories("${BINARY}" PRIVATE "${CMAKE_SOURCE_DIR}") if(FUZZER STREQUAL djxl_fuzzer) target_link_libraries("${BINARY}" - jxl_dec-static - jxl_threads-static + jxl_dec-internal + jxl_threads ) + elseif(FUZZER STREQUAL jpegli_dec_fuzzer) + target_link_libraries("${BINARY}" jpegli-static) else() target_link_libraries("${BINARY}" - jxl_extras-static + jxl_extras_nocodec-internal + jxl_testlib-internal jxl_tool ) endif() @@ -370,37 +413,8 @@ add_subdirectory(comparison_viewer) add_subdirectory(flicker_test) endif() -add_subdirectory(box) add_subdirectory(conformance) - - -if (JPEGXL_ENABLE_TOOLS AND JPEGXL_EMSCRIPTEN) -# WASM API facade. -add_executable(jxl_emcc jxl_emcc.cc) -target_link_libraries(jxl_emcc - jxl_extras-static -) -set_target_properties(jxl_emcc PROPERTIES LINK_FLAGS "\ - -O3\ - --closure 1 \ - -s TOTAL_MEMORY=75mb \ - -s USE_LIBPNG=1 \ - -s DISABLE_EXCEPTION_CATCHING=1 \ - -s MODULARIZE=1 \ - -s FILESYSTEM=0 \ - -s USE_PTHREADS=1 \ - -s PTHREAD_POOL_SIZE=4 \ - -s EXPORT_NAME=\"JxlCodecModule\"\ - -s \"EXPORTED_FUNCTIONS=[\ - _malloc,\ - _free,\ - _jxlCreateInstance,\ - _jxlDestroyInstance,\ - _jxlFlush,\ - _jxlProcessInput\ - ]\"\ -") -endif () # JPEGXL_ENABLE_TOOLS AND JPEGXL_EMSCRIPTEN +add_subdirectory(wasm_demo) if(JPEGXL_ENABLE_JNI) find_package(JNI QUIET) @@ -412,7 +426,7 @@ if (JNI_FOUND AND Java_FOUND) # 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) + target_link_libraries(jxl_jni PUBLIC jxl_dec-internal jxl_threads) if(NOT DEFINED JPEGXL_INSTALL_JNIDIR) set(JPEGXL_INSTALL_JNIDIR ${CMAKE_INSTALL_LIBDIR}) endif() @@ -455,15 +469,21 @@ endif() # JNI_FOUND & Java_FOUND endif() # JPEGXL_ENABLE_JNI # End-to-end tests for the tools -if(BUILD_TESTING AND JPEGXL_ENABLE_TOOLS AND JPEGXL_ENABLE_DEVTOOLS AND JPEGXL_ENABLE_TRANSCODE_JPEG AND (NOT JPEGXL_ENABLE_JNI)) +if(JPEGXL_TEST_TOOLS) find_program (BASH_PROGRAM bash) -if(BASH_PROGRAM AND $<TARGET_EXISTS:cjxl> AND $<TARGET_EXISTS:djxl> AND $<TARGET_EXISTS:ssimulacra_main>) - add_test( - NAME roundtrip_test - COMMAND ${BASH_PROGRAM} ${CMAKE_CURRENT_SOURCE_DIR}/roundtrip_test.sh - ${CMAKE_BINARY_DIR}) - if (CMAKE_CROSSCOMPILING_EMULATOR) - set_tests_properties(roundtrip_test PROPERTIES ENVIRONMENT "EMULATOR=${CMAKE_CROSSCOMPILING_EMULATOR}") +if (BASH_PROGRAM) + set(TEST_SCRIPTS) + find_package(JPEG) + if (JPEG_FOUND AND JPEGXL_ENABLE_TRANSCODE_JPEG) + list(APPEND TEST_SCRIPTS roundtrip_test) endif() -endif() -endif() # BUILD_TESTING + if (JPEG_FOUND AND JPEGXL_ENABLE_JPEGLI) + list(APPEND TEST_SCRIPTS jpegli_tools_test) + endif() + foreach(SCRIPT IN LISTS TEST_SCRIPTS) + add_test(NAME ${SCRIPT} + COMMAND ${BASH_PROGRAM} ${CMAKE_CURRENT_SOURCE_DIR}/scripts/${SCRIPT}.sh + ${CMAKE_BINARY_DIR}) + endforeach() +endif() # BASH_PROGRAM +endif() # JPEGXL_TEST_TOOLS diff --git a/tools/README.cjpeg_hdr.md b/tools/README.cjpeg_hdr.md deleted file mode 100644 index bd7c793..0000000 --- a/tools/README.cjpeg_hdr.md +++ /dev/null @@ -1,73 +0,0 @@ -# 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/tools/args.h b/tools/args.h index 7d04ce3..e34b75e 100644 --- a/tools/args.h +++ b/tools/args.h @@ -14,14 +14,12 @@ #include <string.h> #include <string> -#include <vector> +#include <utility> #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" +#include "tools/file_io.h" namespace jpegxl { namespace tools { @@ -54,8 +52,8 @@ static inline bool ParseFloatPair(const char* arg, return true; } -static inline bool ParseAndAppendKeyValue(const char* arg, - jxl::extras::ColorHints* out) { +template <typename Callback> +static inline bool ParseAndAppendKeyValue(const char* arg, Callback* cb) { const char* eq = strchr(arg, '='); if (!eq) { fprintf(stderr, "Expected argument as 'key=value' but received '%s'\n", @@ -63,26 +61,7 @@ static inline bool ParseAndAppendKeyValue(const char* 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; + return (*cb)(key, std::string(eq + 1)); } static inline bool ParseCString(const char* arg, const char** out) { @@ -95,6 +74,28 @@ static inline bool IncrementUnsigned(size_t* out) { return true; } +struct ColorHintsProxy { + jxl::extras::ColorHints target; + bool operator()(const std::string& key, const std::string& value) { + if (key == "icc_pathname") { + std::vector<uint8_t> icc; + JXL_RETURN_IF_ERROR(ReadFile(value, &icc)); + const char* data = reinterpret_cast<const char*>(icc.data()); + target.Add("icc", std::string(data, data + icc.size())); + } else if (key == "exif" || key == "xmp" || key == "jumbf") { + std::vector<uint8_t> metadata; + JXL_RETURN_IF_ERROR(ReadFile(value, &metadata)); + const char* data = reinterpret_cast<const char*>(metadata.data()); + target.Add(key, std::string(data, data + metadata.size())); + } else if (key == "strip") { + target.Add(value, ""); + } else { + target.Add(key, value); + } + return true; + } +}; + } // namespace tools } // namespace jpegxl diff --git a/tools/benchmark/benchmark_args.cc b/tools/benchmark/benchmark_args.cc index 2bd3eb8..cc2504a 100644 --- a/tools/benchmark/benchmark_args.cc +++ b/tools/benchmark/benchmark_args.cc @@ -16,12 +16,13 @@ #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_custom.h" // for AddCommand.. +#include "tools/benchmark/benchmark_codec_jpeg.h" // for AddCommand.. #include "tools/benchmark/benchmark_codec_jxl.h" -#if JPEGXL_ENABLE_APNG + +#ifdef BENCHMARK_PNG #include "tools/benchmark/benchmark_codec_png.h" -#endif +#endif // BENCHMARK_PNG #ifdef BENCHMARK_WEBP #include "tools/benchmark/benchmark_codec_webp.h" @@ -31,7 +32,8 @@ #include "tools/benchmark/benchmark_codec_avif.h" #endif // BENCHMARK_AVIF -namespace jxl { +namespace jpegxl { +namespace tools { std::vector<std::string> SplitString(const std::string& s, char c) { std::vector<std::string> result; @@ -128,6 +130,7 @@ Status BenchmarkArgs::AddCommandLineOptions() { AddDouble(&mul_output, "mul_output", "If nonzero, multiplies linear sRGB by this and clamps to 255", 0.0); + AddFlag(&save_heatmap, "save_heatmap", "Saves the heatmap images.", true); AddDouble(&heatmap_good, "heatmap_good", "If greater than zero, use this as the good " "threshold for creating heatmap images.", @@ -143,6 +146,11 @@ Status BenchmarkArgs::AddCommandLineOptions() { "Base64-encode the images in the HTML report rather than use " "external file names. May cause very large HTML data size.", false); + AddFlag(&html_report_use_decompressed, "html_report_use_decompressed", + "Show the compressed image as decompressed to --output_extension.", + true); + AddFlag(&html_report_add_heatmap, "html_report_add_heatmap", + "Add heatmaps to the image comparisons.", false); AddFlag( &markdown, "markdown", @@ -186,13 +194,6 @@ Status BenchmarkArgs::AddCommandLineOptions() { 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); @@ -210,13 +211,13 @@ Status BenchmarkArgs::AddCommandLineOptions() { "Distance numbers and compression speeds shown in the table are invalid.", false); + if (!AddCommandLineOptionsCustomCodec(this)) return false; if (!AddCommandLineOptionsJxlCodec(this)) return false; -#ifdef BENCHMARK_JPEG if (!AddCommandLineOptionsJPEGCodec(this)) return false; -#endif // BENCHMARK_JPEG -#if JPEGXL_ENABLE_APNG + +#ifdef BENCHMARK_PNG if (!AddCommandLineOptionsPNGCodec(this)) return false; -#endif +#endif // BENCHMARK_PNG #ifdef BENCHMARK_WEBP if (!AddCommandLineOptionsWebPCodec(this)) return false; #endif // BENCHMARK_WEBP @@ -228,13 +229,12 @@ Status BenchmarkArgs::AddCommandLineOptions() { } 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) { + if (jxl::extras::CodecFromPath(output_extension) == + jxl::extras::Codec::kUnknown) { JXL_WARNING("Unrecognized output_extension %s, try .png", output_extension.c_str()); return false; // already warned @@ -245,14 +245,13 @@ Status BenchmarkArgs::ValidateArgs() { 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)) { + if (!jxl::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(output_encoding.FromExternal(output_encoding_external)); + JXL_RETURN_IF_ERROR(!output_encoding.ICC().empty()); } JXL_RETURN_IF_ERROR(ValidateArgsJxlCodec(this)); @@ -278,4 +277,5 @@ Status BenchmarkArgs::ValidateArgs() { return true; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/benchmark/benchmark_args.h b/tools/benchmark/benchmark_args.h index bebc0ac..bdc385c 100644 --- a/tools/benchmark/benchmark_args.h +++ b/tools/benchmark/benchmark_args.h @@ -23,7 +23,12 @@ #include "tools/args.h" #include "tools/cmdline.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::ColorEncoding; +using ::jxl::Override; +using ::jxl::Status; std::vector<std::string> SplitString(const std::string& s, char c); @@ -108,7 +113,7 @@ struct BenchmarkArgs { bool silent_errors; bool save_compressed; bool save_decompressed; - std::string output_extension; // see CodecFromExtension + std::string output_extension; // see CodecFromPath std::string output_description; // see ParseDescription ColorEncoding output_encoding; // determined by output_description @@ -126,8 +131,11 @@ struct BenchmarkArgs { double heatmap_good; double heatmap_bad; + bool save_heatmap; bool write_html_report; bool html_report_self_contained; + bool html_report_use_decompressed; + bool html_report_add_heatmap; bool markdown; bool more_columns; @@ -143,9 +151,7 @@ struct BenchmarkArgs { int num_samples; int sample_dimensions; - ButteraugliParams ba_params; - bool profiler; double error_pnorm; bool show_progress; @@ -169,6 +175,7 @@ struct BenchmarkArgs { // Returns singleton BenchmarkArgs* Args(); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_ARGS_H_ diff --git a/tools/benchmark/benchmark_codec.cc b/tools/benchmark/benchmark_codec.cc index 230665b..c788aef 100644 --- a/tools/benchmark/benchmark_codec.cc +++ b/tools/benchmark/benchmark_codec.cc @@ -15,25 +15,23 @@ #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_PNG +#include "tools/benchmark/benchmark_codec_png.h" +#endif // BENCHMARK_PNG + #ifdef BENCHMARK_WEBP #include "tools/benchmark/benchmark_codec_webp.h" #endif // BENCHMARK_WEBP @@ -42,7 +40,10 @@ #include "tools/benchmark/benchmark_codec_avif.h" #endif // BENCHMARK_AVIF -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::Image3F; void ImageCodec::ParseParameters(const std::string& parameters) { params_ = parameters; @@ -75,26 +76,8 @@ Status ImageCodec::ParseParam(const std::string& param) { 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') { - ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry; bitrate_target_ = strtof(param.substr(1).c_str(), nullptr); return true; } @@ -108,10 +91,9 @@ class NoneCodec : public ImageCodec { Status ParseParam(const std::string& param) override { return true; } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, + ThreadPool* pool, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) override { - PROFILER_ZONE("NoneCompress"); - const double start = Now(); + const double start = jxl::Now(); // Encode image size so we "decompress" something of the same size, as // required by butteraugli. const uint32_t xsize = io->xsize(); @@ -119,17 +101,16 @@ class NoneCodec : public ImageCodec { compressed->resize(8); memcpy(compressed->data(), &xsize, 4); memcpy(compressed->data() + 4, &ysize, 4); - const double end = Now(); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start); return true; } Status Decompress(const std::string& filename, - const Span<const uint8_t> compressed, - ThreadPoolInternal* pool, CodecInOut* io, + const Span<const uint8_t> compressed, ThreadPool* pool, + CodecInOut* io, jpegxl::tools::SpeedStats* speed_stats) override { - PROFILER_ZONE("NoneDecompress"); - const double start = Now(); + const double start = jxl::Now(); JXL_ASSERT(compressed.size() == 8); uint32_t xsize, ysize; memcpy(&xsize, compressed.data(), 4); @@ -139,7 +120,7 @@ class NoneCodec : public ImageCodec { 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(); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start); return true; } @@ -162,14 +143,12 @@ ImageCodecPtr CreateImageCodec(const std::string& description) { } 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 +#ifdef BENCHMARK_PNG } else if (name == "png") { result.reset(CreateNewPNGCodec(*Args())); -#endif +#endif // BENCHMARK_PNG } else if (name == "none") { result.reset(new NoneCodec(*Args())); #ifdef BENCHMARK_WEBP @@ -180,7 +159,8 @@ ImageCodecPtr CreateImageCodec(const std::string& description) { } else if (name == "avif") { result.reset(CreateNewAvifCodec(*Args())); #endif // BENCHMARK_AVIF - } else { + } + if (!result.get()) { JXL_ABORT("Unknown image codec: %s", name.c_str()); } result->set_description(description); @@ -188,4 +168,5 @@ ImageCodecPtr CreateImageCodec(const std::string& description) { return result; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/benchmark/benchmark_codec.h b/tools/benchmark/benchmark_codec.h index e554fc2..eb19c35 100644 --- a/tools/benchmark/benchmark_codec.h +++ b/tools/benchmark/benchmark_codec.h @@ -12,12 +12,9 @@ #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" @@ -26,8 +23,13 @@ #include "tools/benchmark/benchmark_stats.h" #include "tools/cmdline.h" #include "tools/speed_stats.h" +#include "tools/thread_pool_internal.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::CodecInOut; +using ::jxl::Span; // Thread-compatible. class ImageCodec { @@ -43,36 +45,26 @@ class ImageCodec { 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, - std::vector<uint8_t>* compressed, + ThreadPool* pool, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) = 0; virtual Status Decompress(const std::string& filename, const Span<const uint8_t> compressed, - ThreadPoolInternal* pool, CodecInOut* io, + ThreadPool* pool, CodecInOut* io, jpegxl::tools::SpeedStats* speed_stats) = 0; virtual void GetMoreStats(BenchmarkStats* stats) {} + virtual bool IgnoreAlpha() const { return false; } + virtual Status CanRecompressJpeg() const { return false; } virtual Status RecompressJpeg(const std::string& filename, - const std::string& data, + const std::vector<uint8_t>& data, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) { return false; @@ -87,7 +79,6 @@ class ImageCodec { float butteraugli_target_; float q_target_; float bitrate_target_; - ButteraugliParams ba_params_; std::string error_message_; }; @@ -98,6 +89,7 @@ using ImageCodecPtr = std::unique_ptr<ImageCodec>; // then ParseParameters of the codec gets called with the part behind the colon. ImageCodecPtr CreateImageCodec(const std::string& description); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_H_ diff --git a/tools/benchmark/benchmark_codec_avif.cc b/tools/benchmark/benchmark_codec_avif.cc index fbe36b5..0ff1968 100644 --- a/tools/benchmark/benchmark_codec_avif.cc +++ b/tools/benchmark/benchmark_codec_avif.cc @@ -5,15 +5,15 @@ #include "tools/benchmark/benchmark_codec_avif.h" #include <avif/avif.h> +#include <jxl/cms.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" +#include "tools/thread_pool_internal.h" #define JXL_RETURN_IF_AVIF_ERROR(result) \ do { \ @@ -24,10 +24,32 @@ } \ } while (false) -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::Bytes; +using ::jxl::CodecInOut; +using ::jxl::IccBytes; +using ::jxl::ImageBundle; +using ::jxl::Primaries; +using ::jxl::Span; +using ::jxl::ThreadPool; +using ::jxl::TransferFunction; +using ::jxl::WhitePoint; namespace { +size_t GetNumThreads(ThreadPool* pool) { + size_t result = 0; + const auto count_threads = [&](const size_t num_threads) { + result = num_threads; + return true; + }; + const auto no_op = [&](const uint32_t /*task*/, size_t /*thread*/) {}; + (void)jxl::RunOnPool(pool, 0, 1, count_threads, no_op, "Compress"); + return result; +} + struct AvifArgs { avifPixelFormat chroma_subsampling = AVIF_PIXEL_FORMAT_YUV444; }; @@ -55,13 +77,13 @@ bool ParseChromaSubsampling(const char* arg, avifPixelFormat* subsampling) { } void SetUpAvifColor(const ColorEncoding& color, avifImage* const image) { - bool need_icc = color.white_point != WhitePoint::kD65; + bool need_icc = (color.GetWhitePointType() != WhitePoint::kD65); image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT709; if (!color.HasPrimaries()) { need_icc = true; } else { - switch (color.primaries) { + switch (color.GetPrimariesType()) { case Primaries::kSRGB: image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; break; @@ -76,7 +98,7 @@ void SetUpAvifColor(const ColorEncoding& color, avifImage* const image) { } } - switch (color.tf.GetTransferFunction()) { + switch (color.Tf().GetTransferFunction()) { case TransferFunction::kSRGB: image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; break; @@ -102,40 +124,41 @@ void SetUpAvifColor(const ColorEncoding& color, avifImage* const image) { Status ReadAvifColor(const avifImage* const image, ColorEncoding* const color) { if (image->icc.size != 0) { - PaddedBytes icc; + IccBytes icc; icc.assign(image->icc.data, image->icc.data + image->icc.size); - return color->SetICC(std::move(icc)); + return color->SetICC(std::move(icc), JxlGetDefaultCms()); } - color->white_point = WhitePoint::kD65; + JXL_RETURN_IF_ERROR(color->SetWhitePointType(WhitePoint::kD65)); switch (image->colorPrimaries) { case AVIF_COLOR_PRIMARIES_BT709: - color->primaries = Primaries::kSRGB; + JXL_RETURN_IF_ERROR(color->SetPrimariesType(Primaries::kSRGB)); break; case AVIF_COLOR_PRIMARIES_BT2020: - color->primaries = Primaries::k2100; + JXL_RETURN_IF_ERROR(color->SetPrimariesType(Primaries::k2100)); break; default: return JXL_FAILURE("unsupported avif primaries"); } + jxl::cms::CustomTransferFunction& tf = color->Tf(); switch (image->transferCharacteristics) { case AVIF_TRANSFER_CHARACTERISTICS_BT470M: - JXL_RETURN_IF_ERROR(color->tf.SetGamma(2.2)); + JXL_RETURN_IF_ERROR(tf.SetGamma(2.2)); break; case AVIF_TRANSFER_CHARACTERISTICS_BT470BG: - JXL_RETURN_IF_ERROR(color->tf.SetGamma(2.8)); + JXL_RETURN_IF_ERROR(tf.SetGamma(2.8)); break; case AVIF_TRANSFER_CHARACTERISTICS_LINEAR: - color->tf.SetTransferFunction(TransferFunction::kLinear); + tf.SetTransferFunction(TransferFunction::kLinear); break; case AVIF_TRANSFER_CHARACTERISTICS_SRGB: - color->tf.SetTransferFunction(TransferFunction::kSRGB); + tf.SetTransferFunction(TransferFunction::kSRGB); break; case AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084: - color->tf.SetTransferFunction(TransferFunction::kPQ); + tf.SetTransferFunction(TransferFunction::kPQ); break; case AVIF_TRANSFER_CHARACTERISTICS_HLG: - color->tf.SetTransferFunction(TransferFunction::kHLG); + tf.SetTransferFunction(TransferFunction::kHLG); break; default: return JXL_FAILURE("unsupported avif TRC"); @@ -213,10 +236,11 @@ class AvifCodec : public ImageCodec { } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, - jpegxl::tools::SpeedStats* speed_stats) override { + ThreadPool* pool, std::vector<uint8_t>* compressed, + SpeedStats* speed_stats) override { double elapsed_convert_image = 0; - const double start = Now(); + size_t max_threads = GetNumThreads(pool); + const double start = jxl::Now(); { const auto depth = std::min<int>(16, io->metadata.m.bit_depth.bits_per_sample); @@ -229,10 +253,15 @@ class AvifCodec : public ImageCodec { encoder->tileColsLog2 = log2_cols; encoder->tileRowsLog2 = log2_rows; encoder->speed = speed_; - encoder->maxThreads = pool->NumThreads(); + encoder->maxThreads = max_threads; for (const auto& opts : codec_specific_options_) { - avifEncoderSetCodecSpecificOption(encoder.get(), opts.first.c_str(), - opts.second.c_str()); +#if AVIF_VERSION_MAJOR >= 1 + JXL_RETURN_IF_AVIF_ERROR(avifEncoderSetCodecSpecificOption( + encoder.get(), opts.first.c_str(), opts.second.c_str())); +#else + (void)avifEncoderSetCodecSpecificOption( + encoder.get(), opts.first.c_str(), opts.second.c_str()); +#endif } avifAddImageFlags add_image_flags = AVIF_ADD_IMAGE_FLAG_SINGLE; if (io->metadata.m.have_animation) { @@ -258,14 +287,14 @@ class AvifCodec : public ImageCodec { avifRGBImageAllocatePixels(&rgb_image); std::unique_ptr<avifRGBImage, void (*)(avifRGBImage*)> pixels_freer( &rgb_image, &avifRGBImageFreePixels); - const double start_convert_image = Now(); + const double start_convert_image = jxl::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(); + const double end_convert_image = jxl::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( @@ -276,24 +305,23 @@ class AvifCodec : public ImageCodec { compressed->assign(buffer.data, buffer.data + buffer.size); avifRWDataFree(&buffer); } - const double end = Now(); + const double end = jxl::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 { + const Span<const uint8_t> compressed, ThreadPool* pool, + CodecInOut* io, SpeedStats* speed_stats) override { io->frames.clear(); - io->dec_pixels = 0; + size_t max_threads = GetNumThreads(pool); double elapsed_convert_image = 0; - const double start = Now(); + const double start = jxl::Now(); { std::unique_ptr<avifDecoder, void (*)(avifDecoder*)> decoder( avifDecoderCreate(), &avifDecoderDestroy); decoder->codecChoice = decoder_; - decoder->maxThreads = pool->NumThreads(); + decoder->maxThreads = max_threads; JXL_RETURN_IF_AVIF_ERROR(avifDecoderSetIOMemory( decoder.get(), compressed.data(), compressed.size())); JXL_RETURN_IF_AVIF_ERROR(avifDecoderParse(decoder.get())); @@ -316,27 +344,27 @@ class AvifCodec : public ImageCodec { 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(); + const double start_convert_image = jxl::Now(); { + JxlPixelFormat format = { + (has_alpha ? 4u : 3u), + (rgb_image.depth <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16), + JXL_NATIVE_ENDIAN, 0}; 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, pool, &ib, - /*float_in=*/false, /*align=*/0)); + Bytes(rgb_image.pixels, rgb_image.height * rgb_image.rowBytes), + rgb_image.width, rgb_image.height, color, rgb_image.depth, format, + pool, &ib)); io->frames.push_back(std::move(ib)); - io->dec_pixels += rgb_image.width * rgb_image.height; } - const double end_convert_image = Now(); + const double end_convert_image = jxl::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(); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start - elapsed_convert_image); return true; } @@ -355,4 +383,5 @@ ImageCodec* CreateNewAvifCodec(const BenchmarkArgs& args) { return new AvifCodec(args); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/benchmark/benchmark_codec_avif.h b/tools/benchmark/benchmark_codec_avif.h index b3dc38e..c3816cf 100644 --- a/tools/benchmark/benchmark_codec_avif.h +++ b/tools/benchmark/benchmark_codec_avif.h @@ -10,11 +10,13 @@ #include "tools/benchmark/benchmark_args.h" #include "tools/benchmark/benchmark_codec.h" -namespace jxl { +namespace jpegxl { +namespace tools { ImageCodec* CreateNewAvifCodec(const BenchmarkArgs& args); // Registers the avif-specific command line options. Status AddCommandLineOptionsAvifCodec(BenchmarkArgs* args); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_AVIF_H_ diff --git a/tools/benchmark/benchmark_codec_custom.cc b/tools/benchmark/benchmark_codec_custom.cc index eefae6e..87fc04c 100644 --- a/tools/benchmark/benchmark_codec_custom.cc +++ b/tools/benchmark/benchmark_codec_custom.cc @@ -13,35 +13,51 @@ #include <fstream> #include "lib/extras/codec.h" +#include "lib/extras/dec/color_description.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" +#include "tools/file_io.h" +#include "tools/thread_pool_internal.h" -namespace jxl { -namespace { +namespace jpegxl { +namespace tools { -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; +struct CustomCodecArgs { + std::string extension; + std::string colorspace; + bool quiet; +}; + +static CustomCodecArgs* const custom_args = new CustomCodecArgs; + +Status AddCommandLineOptionsCustomCodec(BenchmarkArgs* args) { + args->AddString( + &custom_args->extension, "custom_codec_extension", + "Converts input and output of codec to this file type (default: png).", + "png"); + args->AddString( + &custom_args->colorspace, "custom_codec_colorspace", + "If not empty, converts input and output of codec to this colorspace.", + ""); + args->AddFlag(&custom_args->quiet, "custom_codec_quiet", + "Whether stdin and stdout of custom codec should be shown.", + false); + return true; } +namespace { + // 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(); + const double start = jxl::Now(); JXL_RETURN_IF_ERROR(function()); - const double end = Now(); + const double end = jxl::Now(); const std::string time_filename = GetBaseName(std::move(output_filename)) + ".time"; std::ifstream time_stream(time_filename); @@ -64,21 +80,36 @@ class CustomCodec : public ImageCodec { explicit CustomCodec(const BenchmarkArgs& args) : ImageCodec(args) {} Status ParseParam(const std::string& param) override { + if (param_index_ == 0) { + description_ = ""; + } switch (param_index_) { case 0: extension_ = param; + description_ += param; break; - case 1: compress_command_ = param; + description_ += std::string(":"); + if (param.find_last_of('/') < param.size()) { + description_ += param.substr(param.find_last_of('/') + 1); + } else { + description_ += param; + } break; - case 2: decompress_command_ = param; break; - default: compress_args_.push_back(param); + description_ += std::string(":"); + if (param.size() > 2 && param[0] == '-' && param[1] == '-') { + description_ += param.substr(2); + } else if (param.size() > 2 && param[0] == '-') { + description_ += param.substr(1); + } else { + description_ += param; + } break; } ++param_index_; @@ -86,49 +117,68 @@ class CustomCodec : public ImageCodec { } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, + ThreadPool* pool, std::vector<uint8_t>* 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)); + TemporaryFile in_file(basename, custom_args->extension); + TemporaryFile encoded_file(basename, extension_); + std::string in_filename, encoded_filename; + JXL_RETURN_IF_ERROR(in_file.GetFileName(&in_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; - JXL_RETURN_IF_ERROR( - EncodeToFile(*io, io->Main().c_current(), bits, png_filename, pool)); + ColorEncoding c_enc = io->Main().c_current(); + if (!custom_args->colorspace.empty()) { + JxlColorEncoding colorspace; + JXL_RETURN_IF_ERROR( + jxl::ParseDescription(custom_args->colorspace, &colorspace)); + JXL_RETURN_IF_ERROR(c_enc.FromExternal(colorspace)); + } + std::vector<uint8_t> encoded; + JXL_RETURN_IF_ERROR(Encode(*io, c_enc, bits, in_filename, &encoded, pool)); + JXL_RETURN_IF_ERROR(WriteFile(in_filename, encoded)); std::vector<std::string> arguments = compress_args_; - arguments.push_back(png_filename); + arguments.push_back(in_filename); arguments.push_back(encoded_filename); JXL_RETURN_IF_ERROR(ReportCodecRunningTime( - [&, this] { return RunCommand(compress_command_, arguments); }, + [&, this] { + return RunCommand(compress_command_, arguments, custom_args->quiet); + }, 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, + const Span<const uint8_t> compressed, ThreadPool* 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; + TemporaryFile encoded_file(basename, extension_); + TemporaryFile out_file(basename, custom_args->extension); + std::string encoded_filename, out_filename; JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename)); - JXL_RETURN_IF_ERROR(png_file.GetFileName(&png_filename)); + JXL_RETURN_IF_ERROR(out_file.GetFileName(&out_filename)); - JXL_RETURN_IF_ERROR(WriteFile(compressed, encoded_filename)); + JXL_RETURN_IF_ERROR(WriteFile(encoded_filename, compressed)); JXL_RETURN_IF_ERROR(ReportCodecRunningTime( [&, this] { return RunCommand( decompress_command_, - std::vector<std::string>{encoded_filename, png_filename}); + std::vector<std::string>{encoded_filename, out_filename}, + custom_args->quiet); }, - png_filename, speed_stats)); + out_filename, speed_stats)); + jxl::extras::ColorHints hints; + if (!custom_args->colorspace.empty()) { + hints.Add("color_space", custom_args->colorspace); + } + std::vector<uint8_t> encoded; + JXL_RETURN_IF_ERROR(ReadFile(out_filename, &encoded)); JXL_RETURN_IF_ERROR( - SetFromFile(png_filename, extras::ColorHints(), io, pool)); + jxl::SetFromBytes(jxl::Bytes(encoded), hints, io, pool)); io->metadata.m.SetIntensityTarget(saved_intensity_target_); return true; } @@ -148,14 +198,18 @@ ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args) { return new CustomCodec(args); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl #else -namespace jxl { +namespace jpegxl { +namespace tools { ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args) { return nullptr; } +Status AddCommandLineOptionsCustomCodec(BenchmarkArgs* args) { return true; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // _MSC_VER diff --git a/tools/benchmark/benchmark_codec_custom.h b/tools/benchmark/benchmark_codec_custom.h index b2711cd..6e3d017 100644 --- a/tools/benchmark/benchmark_codec_custom.h +++ b/tools/benchmark/benchmark_codec_custom.h @@ -37,10 +37,13 @@ #include "tools/benchmark/benchmark_args.h" #include "tools/benchmark/benchmark_codec.h" -namespace jxl { +namespace jpegxl { +namespace tools { ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args); +Status AddCommandLineOptionsCustomCodec(BenchmarkArgs* args); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_CUSTOM_H_ diff --git a/tools/benchmark/benchmark_codec_jpeg.cc b/tools/benchmark/benchmark_codec_jpeg.cc index ae3215a..ae6abae 100644 --- a/tools/benchmark/benchmark_codec_jpeg.cc +++ b/tools/benchmark/benchmark_codec_jpeg.cc @@ -13,104 +13,363 @@ #include <numeric> // partial_sum #include <string> +#if JPEGXL_ENABLE_JPEGLI +#include "lib/extras/dec/jpegli.h" +#endif #include "lib/extras/dec/jpg.h" +#if JPEGXL_ENABLE_JPEGLI +#include "lib/extras/enc/jpegli.h" +#endif #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 "lib/jxl/image_bundle.h" +#include "tools/benchmark/benchmark_utils.h" #include "tools/cmdline.h" +#include "tools/file_io.h" +#include "tools/thread_pool_internal.h" -namespace jxl { - -namespace { +namespace jpegxl { +namespace tools { struct JPEGArgs { - std::string jpeg_encoder = "libjpeg"; - std::string chroma_subsampling = "444"; + std::string base_quant_fn; + float search_q_start; + float search_q_min; + float search_q_max; + float search_d_min; + float search_d_max; + int search_max_iters; + float search_tolerance; + float search_q_precision; + float search_first_iter_slope; }; -JPEGArgs* const jpegargs = new JPEGArgs; +static JPEGArgs* const jpegargs = new JPEGArgs; -} // namespace +#define SET_ENCODER_ARG(name) \ + if (jpegargs->name > 0) { \ + encoder->SetOption(#name, std::to_string(jpegargs->name)); \ + } Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args) { - args->cmdline.AddOptionValue( - '\0', "chroma_subsampling", "444/422/420/411", - "default JPEG chroma subsampling (default: 444).", - &jpegargs->chroma_subsampling, &jpegxl::tools::ParseString); + args->AddString(&jpegargs->base_quant_fn, "qtables", + "Custom base quantization tables."); + args->AddFloat(&jpegargs->search_q_start, "search_q_start", + "Starting quality for quality-to-target search", 0.0f); + args->AddFloat(&jpegargs->search_q_min, "search_q_min", + "Minimum quality for quality-to-target search", 0.0f); + args->AddFloat(&jpegargs->search_q_max, "search_q_max", + "Maximum quality for quality-to-target search", 0.0f); + args->AddFloat(&jpegargs->search_d_min, "search_d_min", + "Minimum distance for quality-to-target search", 0.0f); + args->AddFloat(&jpegargs->search_d_max, "search_d_max", + "Maximum distance for quality-to-target search", 0.0f); + args->AddFloat(&jpegargs->search_tolerance, "search_tolerance", + "Percentage value, if quality-to-target search result " + "relative error is within this, search stops.", + 0.0f); + args->AddFloat(&jpegargs->search_q_precision, "search_q_precision", + "If last quality change in quality-to-target search is " + "within this value, search stops.", + 0.0f); + args->AddFloat(&jpegargs->search_first_iter_slope, "search_first_iter_slope", + "Slope of first extrapolation step in quality-to-target " + "search.", + 0.0f); + args->AddSigned(&jpegargs->search_max_iters, "search_max_iters", + "Maximum search steps in quality-to-target search.", 0); return true; } class JPEGCodec : public ImageCodec { public: - explicit JPEGCodec(const BenchmarkArgs& args) : ImageCodec(args) { - jpeg_encoder_ = jpegargs->jpeg_encoder; - chroma_subsampling_ = jpegargs->chroma_subsampling; - } + explicit JPEGCodec(const BenchmarkArgs& args) : ImageCodec(args) {} Status ParseParam(const std::string& param) override { + if (param[0] == 'q' && ImageCodec::ParseParam(param)) { + enc_quality_set_ = true; + return true; + } if (ImageCodec::ParseParam(param)) { return true; } - if (param == "sjpeg") { + if (param == "sjpeg" || param.find("cjpeg") != std::string::npos) { jpeg_encoder_ = param; return true; } +#if JPEGXL_ENABLE_JPEGLI + if (param == "enc-jpegli") { + jpeg_encoder_ = "jpegli"; + return true; + } +#endif if (param.compare(0, 3, "yuv") == 0) { - if (param.size() != 6) return false; chroma_subsampling_ = param.substr(3); return true; } + if (param.compare(0, 4, "psnr") == 0) { + psnr_target_ = std::stof(param.substr(4)); + return true; + } + if (param[0] == 'p') { + progressive_id_ = strtol(param.substr(1).c_str(), nullptr, 10); + return true; + } + if (param == "fix") { + fix_codes_ = true; + return true; + } + if (param[0] == 'Q') { + libjpeg_quality_ = strtol(param.substr(1).c_str(), nullptr, 10); + return true; + } + if (param.compare(0, 3, "YUV") == 0) { + if (param.size() != 6) return false; + libjpeg_chroma_subsampling_ = param.substr(3); + return true; + } + if (param == "noaq") { + enable_adaptive_quant_ = false; + return true; + } +#if JPEGXL_ENABLE_JPEGLI + if (param == "xyb") { + xyb_mode_ = true; + return true; + } + if (param == "std") { + use_std_tables_ = true; + return true; + } + if (param == "dec-jpegli") { + jpeg_decoder_ = "jpegli"; + return true; + } + if (param.substr(0, 2) == "bd") { + bitdepth_ = strtol(param.substr(2).c_str(), nullptr, 10); + return true; + } + if (param.substr(0, 6) == "cquant") { + num_colors_ = strtol(param.substr(6).c_str(), nullptr, 10); + return true; + } +#endif return false; } + bool IgnoreAlpha() const override { return true; } + Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, + ThreadPool* pool, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) override { - extras::PackedPixelFile ppf; - JxlPixelFormat format = {0, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0}; + if (jpeg_encoder_.find("cjpeg") != std::string::npos) { +// 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) + const std::string basename = GetBaseName(filename); + TemporaryFile in_file(basename, "pnm"); + TemporaryFile encoded_file(basename, "jpg"); + std::string in_filename, encoded_filename; + JXL_RETURN_IF_ERROR(in_file.GetFileName(&in_filename)); + JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename)); + const size_t bits = io->metadata.m.bit_depth.bits_per_sample; + ColorEncoding c_enc = io->Main().c_current(); + std::vector<uint8_t> encoded; + JXL_RETURN_IF_ERROR( + Encode(*io, c_enc, bits, in_filename, &encoded, pool)); + JXL_RETURN_IF_ERROR(WriteFile(in_filename, encoded)); + std::string compress_command = jpeg_encoder_; + std::vector<std::string> arguments; + arguments.push_back("-outfile"); + arguments.push_back(encoded_filename); + arguments.push_back("-quality"); + arguments.push_back(std::to_string(static_cast<int>(q_target_))); + arguments.push_back("-sample"); + if (chroma_subsampling_ == "444") { + arguments.push_back("1x1"); + } else if (chroma_subsampling_ == "420") { + arguments.push_back("2x2"); + } else if (!chroma_subsampling_.empty()) { + return JXL_FAILURE("Unsupported chroma subsampling"); + } + arguments.push_back("-optimize"); + arguments.push_back(in_filename); + const double start = jxl::Now(); + JXL_RETURN_IF_ERROR(RunCommand(compress_command, arguments, false)); + const double end = jxl::Now(); + speed_stats->NotifyElapsed(end - start); + return ReadFile(encoded_filename, compressed); +#else + return JXL_FAILURE("Not supported on this build"); +#endif + } + + jxl::extras::PackedPixelFile ppf; + size_t bits_per_sample = io->metadata.m.bit_depth.bits_per_sample; + JxlPixelFormat format = { + 0, // num_channels is ignored by the converter + bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, JXL_BIG_ENDIAN, + 0}; JXL_RETURN_IF_ERROR(ConvertCodecInOutToPackedPixelFile( *io, format, io->metadata.m.color_encoding, pool, &ppf)); - extras::EncodedImage encoded; - std::unique_ptr<extras::Encoder> encoder = extras::GetJPEGEncoder(); - std::ostringstream os; - os << static_cast<int>(std::round(q_target_)); - encoder->SetOption("q", os.str()); - encoder->SetOption("jpeg_encoder", jpeg_encoder_); - encoder->SetOption("chroma_subsampling", chroma_subsampling_); - const double start = Now(); - JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool)); - const double end = Now(); - *compressed = encoded.bitstreams.back(); - speed_stats->NotifyElapsed(end - start); + double elapsed = 0.0; + if (jpeg_encoder_ == "jpegli") { +#if JPEGXL_ENABLE_JPEGLI + jxl::extras::JpegSettings settings; + settings.xyb = xyb_mode_; + if (!xyb_mode_) { + settings.use_std_quant_tables = use_std_tables_; + } + if (enc_quality_set_) { + settings.quality = q_target_; + } else { + settings.distance = butteraugli_target_; + } + if (progressive_id_ >= 0) { + settings.progressive_level = progressive_id_; + } + if (psnr_target_ > 0) { + settings.psnr_target = psnr_target_; + } + if (jpegargs->search_tolerance > 0) { + settings.search_tolerance = 0.01f * jpegargs->search_tolerance; + } + if (jpegargs->search_d_min > 0) { + settings.min_distance = jpegargs->search_d_min; + } + if (jpegargs->search_d_max > 0) { + settings.max_distance = jpegargs->search_d_max; + } + settings.chroma_subsampling = chroma_subsampling_; + settings.use_adaptive_quantization = enable_adaptive_quant_; + settings.libjpeg_quality = libjpeg_quality_; + settings.libjpeg_chroma_subsampling = libjpeg_chroma_subsampling_; + settings.optimize_coding = !fix_codes_; + const double start = jxl::Now(); + JXL_RETURN_IF_ERROR( + jxl::extras::EncodeJpeg(ppf, settings, pool, compressed)); + const double end = jxl::Now(); + elapsed = end - start; +#endif + } else { + jxl::extras::EncodedImage encoded; + std::unique_ptr<jxl::extras::Encoder> encoder = + jxl::extras::GetJPEGEncoder(); + if (!encoder) { + fprintf(stderr, "libjpeg codec is not supported\n"); + return false; + } + std::ostringstream os; + os << static_cast<int>(std::round(q_target_)); + encoder->SetOption("q", os.str()); + encoder->SetOption("jpeg_encoder", jpeg_encoder_); + if (!chroma_subsampling_.empty()) { + encoder->SetOption("chroma_subsampling", chroma_subsampling_); + } + if (progressive_id_ >= 0) { + encoder->SetOption("progressive", std::to_string(progressive_id_)); + } + if (libjpeg_quality_ > 0) { + encoder->SetOption("libjpeg_quality", std::to_string(libjpeg_quality_)); + } + if (!libjpeg_chroma_subsampling_.empty()) { + encoder->SetOption("libjpeg_chroma_subsampling", + libjpeg_chroma_subsampling_); + } + if (fix_codes_) { + encoder->SetOption("optimize", "OFF"); + } + if (!enable_adaptive_quant_) { + encoder->SetOption("adaptive_q", "OFF"); + } + if (psnr_target_ > 0) { + encoder->SetOption("psnr", std::to_string(psnr_target_)); + } + if (!jpegargs->base_quant_fn.empty()) { + encoder->SetOption("base_quant_fn", jpegargs->base_quant_fn); + } + SET_ENCODER_ARG(search_q_start); + SET_ENCODER_ARG(search_q_min); + SET_ENCODER_ARG(search_q_max); + SET_ENCODER_ARG(search_q_precision); + SET_ENCODER_ARG(search_tolerance); + SET_ENCODER_ARG(search_first_iter_slope); + SET_ENCODER_ARG(search_max_iters); + const double start = jxl::Now(); + JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool)); + const double end = jxl::Now(); + elapsed = end - start; + *compressed = encoded.bitstreams.back(); + } + speed_stats->NotifyElapsed(elapsed); return true; } Status Decompress(const std::string& filename, - const Span<const uint8_t> compressed, - ThreadPoolInternal* pool, CodecInOut* io, + const Span<const uint8_t> compressed, ThreadPool* 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)); + jxl::extras::PackedPixelFile ppf; + if (jpeg_decoder_ == "jpegli") { +#if JPEGXL_ENABLE_JPEGLI + std::vector<uint8_t> jpeg_bytes(compressed.data(), + compressed.data() + compressed.size()); + const double start = jxl::Now(); + jxl::extras::JpegDecompressParams dparams; + dparams.output_data_type = + bitdepth_ > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8; + dparams.num_colors = num_colors_; + JXL_RETURN_IF_ERROR( + jxl::extras::DecodeJpeg(jpeg_bytes, dparams, pool, &ppf)); + const double end = jxl::Now(); + speed_stats->NotifyElapsed(end - start); +#endif + } else { + const double start = jxl::Now(); + jxl::extras::JPGDecompressParams dparams; + dparams.num_colors = num_colors_; + JXL_RETURN_IF_ERROR( + jxl::extras::DecodeImageJPG(compressed, jxl::extras::ColorHints(), + &ppf, /*constraints=*/nullptr, &dparams)); + const double end = jxl::Now(); + speed_stats->NotifyElapsed(end - start); + } + JXL_RETURN_IF_ERROR( + jxl::extras::ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); return true; } protected: - std::string jpeg_encoder_; + // JPEG encoder and its parameters + std::string jpeg_encoder_ = "libjpeg"; std::string chroma_subsampling_; + int progressive_id_ = -1; + bool fix_codes_ = false; + float psnr_target_ = 0.0f; + bool enc_quality_set_ = false; + int libjpeg_quality_ = 0; + std::string libjpeg_chroma_subsampling_; +#if JPEGXL_ENABLE_JPEGLI + bool xyb_mode_ = false; + bool use_std_tables_ = false; +#endif + bool enable_adaptive_quant_ = true; + // JPEG decoder and its parameters + std::string jpeg_decoder_ = "libjpeg"; + int num_colors_ = 0; +#if JPEGXL_ENABLE_JPEGLI + size_t bitdepth_ = 8; +#endif }; ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args) { return new JPEGCodec(args); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/benchmark/benchmark_codec_jpeg.h b/tools/benchmark/benchmark_codec_jpeg.h index cd4b009..d9f0c35 100644 --- a/tools/benchmark/benchmark_codec_jpeg.h +++ b/tools/benchmark/benchmark_codec_jpeg.h @@ -10,11 +10,13 @@ #include "tools/benchmark/benchmark_args.h" #include "tools/benchmark/benchmark_codec.h" -namespace jxl { +namespace jpegxl { +namespace tools { ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args); // Registers the jpeg-specific command line options. Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_JPEG_H_ diff --git a/tools/benchmark/benchmark_codec_jxl.cc b/tools/benchmark/benchmark_codec_jxl.cc index 6557858..554115a 100644 --- a/tools/benchmark/benchmark_codec_jxl.cc +++ b/tools/benchmark/benchmark_codec_jxl.cc @@ -4,6 +4,9 @@ // license that can be found in the LICENSE file. #include "tools/benchmark/benchmark_codec_jxl.h" +#include <jxl/stats.h> +#include <jxl/thread_parallel_runner_cxx.h> + #include <cstdint> #include <cstdlib> #include <functional> @@ -12,46 +15,34 @@ #include <utility> #include <vector> -#include "jxl/thread_parallel_runner_cxx.h" #include "lib/extras/codec.h" #include "lib/extras/dec/jxl.h" -#if JPEGXL_ENABLE_JPEG +#include "lib/extras/enc/apng.h" +#include "lib/extras/enc/encode.h" #include "lib/extras/enc/jpg.h" -#endif +#include "lib/extras/enc/jxl.h" #include "lib/extras/packed_image_convert.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/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 { +namespace jpegxl { +namespace tools { -// 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; -} +using ::jxl::Image3F; +using ::jxl::extras::EncodedImage; +using ::jxl::extras::Encoder; +using ::jxl::extras::JXLCompressParams; +using ::jxl::extras::JXLDecompressParams; +using ::jxl::extras::PackedFrame; +using ::jxl::extras::PackedPixelFile; struct JxlArgs { - double xmul; - double quant_bias; - - bool use_ac_strategy; bool qprogressive; // progressive with shift-quantization. bool progressive; int progressive_dc; @@ -60,20 +51,12 @@ struct JxlArgs { Override dots; Override patches; - bool log_search_state; 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", @@ -88,9 +71,6 @@ Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args) { args->AddOverride(&jxlargs->patches, "patches", "Enable(1)/disable(0) patch dictionary."); - args->AddFlag(&jxlargs->log_search_state, "log_search_state", - "Print out debug info for tortoise mode AQ loop.", false); - args->AddString( &jxlargs->debug_image_dir, "debug_image_dir", "If not empty, saves debug images for each " @@ -101,37 +81,76 @@ Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args) { Status ValidateArgsJxlCodec(BenchmarkArgs* args) { return true; } +inline bool ParseEffort(const std::string& s, int* out) { + if (s == "lightning") { + *out = 1; + return true; + } else if (s == "thunder") { + *out = 2; + return true; + } else if (s == "falcon") { + *out = 3; + return true; + } else if (s == "cheetah") { + *out = 4; + return true; + } else if (s == "hare") { + *out = 5; + return true; + } else if (s == "fast" || s == "wombat") { + *out = 6; + return true; + } else if (s == "squirrel") { + *out = 7; + return true; + } else if (s == "kitten") { + *out = 8; + return true; + } else if (s == "guetzli" || s == "tortoise") { + *out = 9; + return true; + } else if (s == "glacier") { + *out = 10; + return true; + } + size_t st = static_cast<size_t>(strtoull(s.c_str(), nullptr, 0)); + if (st <= 10 && st >= 1) { + *out = st; + return true; + } + return false; +} + class JxlCodec : public ImageCodec { public: - explicit JxlCodec(const BenchmarkArgs& args) : ImageCodec(args) {} + explicit JxlCodec(const BenchmarkArgs& args) + : ImageCodec(args), stats_(nullptr, JxlEncoderStatsDestroy) {} 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="; - + int val; + float fval; if (param.substr(0, kResamplingPrefix.size()) == kResamplingPrefix) { std::istringstream parser(param.substr(kResamplingPrefix.size())); - parser >> cparams_.resampling; + int resampling; + parser >> resampling; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, resampling); } else if (param.substr(0, kEcResamplingPrefix.size()) == kEcResamplingPrefix) { std::istringstream parser(param.substr(kEcResamplingPrefix.size())); - parser >> cparams_.ec_resampling; + int ec_resampling; + parser >> ec_resampling; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, + ec_resampling); } else if (ImageCodec::ParseParam(param)) { - if (param[0] == 'd' && butteraugli_target_ == 0.0) { - cparams_.SetLossless(); - } + // 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[0] == 'D') { + cparams_.alpha_distance = strtof(param.substr(1).c_str(), nullptr); } else if (param.substr(0, kMaxPassesPrefix.size()) == kMaxPassesPrefix) { std::istringstream parser(param.substr(kMaxPassesPrefix.size())); parser >> dparams_.max_passes; @@ -139,159 +158,127 @@ class JxlCodec : public ImageCodec { kDownsamplingPrefix) { std::istringstream parser(param.substr(kDownsamplingPrefix.size())); parser >> dparams_.max_downsampling; - } else if (ParseSpeedTier(param, &cparams_.speed_tier)) { - // Nothing to do. + } else if (ParseEffort(param, &val)) { + cparams_.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, val); } else if (param[0] == 'X') { - cparams_.channel_colors_pre_transform_percent = - strtol(param.substr(1).c_str(), nullptr, 10); + fval = strtof(param.substr(1).c_str(), nullptr); + cparams_.AddFloatOption( + JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, fval); } else if (param[0] == 'Y') { - cparams_.channel_colors_percent = - strtol(param.substr(1).c_str(), nullptr, 10); + fval = strtof(param.substr(1).c_str(), nullptr); + cparams_.AddFloatOption( + JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, fval); } else if (param[0] == 'p') { - cparams_.palette_colors = strtol(param.substr(1).c_str(), nullptr, 10); + val = strtol(param.substr(1).c_str(), nullptr, 10); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_PALETTE_COLORS, val); } else if (param == "lp") { - cparams_.lossy_palette = true; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_LOSSY_PALETTE, 1); } else if (param[0] == 'C') { - cparams_.colorspace = strtol(param.substr(1).c_str(), nullptr, 10); + val = strtol(param.substr(1).c_str(), nullptr, 10); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, val); } else if (param[0] == 'c') { - cparams_.color_transform = - (jxl::ColorTransform)strtol(param.substr(1).c_str(), nullptr, 10); + val = strtol(param.substr(1).c_str(), nullptr, 10); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM, val); has_ctransform_ = true; } else if (param[0] == 'I') { - cparams_.options.nb_repeats = strtof(param.substr(1).c_str(), nullptr); + fval = strtof(param.substr(1).c_str(), nullptr); + cparams_.AddFloatOption( + JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, fval * 100.0); } else if (param[0] == 'E') { - cparams_.options.max_properties = - strtof(param.substr(1).c_str(), nullptr); + val = strtol(param.substr(1).c_str(), nullptr, 10); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, val); } else if (param[0] == 'P') { - cparams_.options.predictor = - static_cast<Predictor>(strtof(param.substr(1).c_str(), nullptr)); + val = strtol(param.substr(1).c_str(), nullptr, 10); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, val); } else if (param == "slow") { - cparams_.options.nb_repeats = 2; + cparams_.AddFloatOption( + JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, 50.0); } else if (param == "R") { - cparams_.responsive = 1; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, 1); } else if (param[0] == 'R') { - cparams_.responsive = strtol(param.substr(1).c_str(), nullptr, 10); + val = strtol(param.substr(1).c_str(), nullptr, 10); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, val); } else if (param == "m") { - cparams_.modular_mode = true; - cparams_.color_transform = jxl::ColorTransform::kNone; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR, 1); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM, 1); // kNone + modular_mode_ = true; } else if (param.substr(0, 3) == "gab") { - long gab = strtol(param.substr(3).c_str(), nullptr, 10); - if (gab != 0 && gab != 1) { + val = strtol(param.substr(3).c_str(), nullptr, 10); + if (val != 0 && val != 1) { return JXL_FAILURE("Invalid gab value"); } - cparams_.gaborish = static_cast<Override>(gab); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_GABORISH, val); } else if (param[0] == 'g') { - long gsize = strtol(param.substr(1).c_str(), nullptr, 10); - if (gsize < 0 || gsize > 3) { + val = strtol(param.substr(1).c_str(), nullptr, 10); + if (val < 0 || val > 3) { return JXL_FAILURE("Invalid group size shift value"); } - cparams_.modular_group_size_shift = gsize; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, val); } 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; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, 0); + cparams_.AddFloatOption( + JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, 0.0f); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, 0); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, 0); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, 0); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, + 0); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, 0); } else if (param.substr(0, 3) == "epf") { - cparams_.epf = strtol(param.substr(3).c_str(), nullptr, 10); - if (cparams_.epf > 3) { + val = strtol(param.substr(3).c_str(), nullptr, 10); + if (val > 3) { return JXL_FAILURE("Invalid epf value"); } - } else if (param.substr(0, 2) == "nr") { - normalize_bitrate_ = true; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_EPF, val); } else if (param.substr(0, 16) == "faster_decoding=") { - cparams_.decoding_speed_tier = - strtol(param.substr(16).c_str(), nullptr, 10); + val = strtol(param.substr(16).c_str(), nullptr, 10); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_DECODING_SPEED, val); } 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; - if (normalize_bitrate_) 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, std::vector<uint8_t>* compressed, + ThreadPool* pool, std::vector<uint8_t>* 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)); + PackedPixelFile ppf; + JxlPixelFormat format{0, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + JXL_RETURN_IF_ERROR(ConvertCodecInOutToPackedPixelFile( + *io, format, io->Main().c_current(), pool, &ppf)); + cparams_.runner = pool->runner(); + cparams_.runner_opaque = pool->runner_opaque(); + cparams_.distance = butteraugli_target_; + cparams_.AddOption(JXL_ENC_FRAME_SETTING_NOISE, (int)jxlargs->noise); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_DOTS, (int)jxlargs->dots); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_PATCHES, (int)jxlargs->patches); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, + jxlargs->progressive); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC, + jxlargs->qprogressive); + cparams_.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, + jxlargs->progressive_dc); + if (butteraugli_target_ > 0.f && modular_mode_ && !has_ctransform_) { + // Reset color transform to default XYB for lossy modular. + cparams_.AddOption(JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM, -1); } - 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; + std::string debug_prefix; + SetDebugImageCallback(filename, &debug_prefix, &cparams_); + if (args_.print_more_stats) { + stats_.reset(JxlEncoderStatsCreate()); + cparams_.stats = stats_.get(); } - - cparams_.log_search_state = jxlargs->log_search_state; - -#if JPEGXL_ENABLE_JPEG - if (normalize_bitrate_ && cparams_.butteraugli_distance > 0.0f) { - extras::PackedPixelFile ppf; - JxlPixelFormat format = {0, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0}; - JXL_RETURN_IF_ERROR(ConvertCodecInOutToPackedPixelFile( - *io, format, io->metadata.m.color_encoding, pool, &ppf)); - extras::EncodedImage encoded; - std::unique_ptr<extras::Encoder> encoder = extras::GetJPEGEncoder(); - encoder->SetOption("q", "95"); - JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool)); - float jpeg_bits = encoded.bitstreams.back().size() * kBitsPerByte; - float jpeg_bitrate = jpeg_bits / (io->xsize() * io->ysize()); - // Formula fitted on jyrki31 corpus for distances between 1.0 and 8.0. - cparams_.target_bitrate = (jpeg_bitrate * 0.36f / - (0.6f * cparams_.butteraugli_distance + 0.4f)); - } -#endif - - const double start = Now(); - PassesEncoderState passes_encoder_state; - PaddedBytes compressed_padded; - JXL_RETURN_IF_ERROR(EncodeFile(cparams_, io, &passes_encoder_state, - &compressed_padded, GetJxlCms(), &cinfo_, - pool)); - const double end = Now(); - compressed->assign(compressed_padded.begin(), compressed_padded.end()); + const double start = jxl::Now(); + JXL_RETURN_IF_ERROR(jxl::extras::EncodeImageJXL( + cparams_, ppf, /*jpeg_bytes=*/nullptr, compressed)); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start); return true; } Status Decompress(const std::string& filename, - const Span<const uint8_t> compressed, - ThreadPoolInternal* pool, CodecInOut* io, + const Span<const uint8_t> compressed, ThreadPool* pool, + CodecInOut* io, jpegxl::tools::SpeedStats* speed_stats) override { dparams_.runner = pool->runner(); dparams_.runner_opaque = pool->runner_opaque(); @@ -304,35 +291,67 @@ class JxlCodec : public ImageCodec { // originals, so we must set the option to keep the original orientation // instead. dparams_.keep_orientation = true; - extras::PackedPixelFile ppf; + PackedPixelFile ppf; size_t decoded_bytes; - const double start = Now(); - JXL_RETURN_IF_ERROR(DecodeImageJXL(compressed.data(), compressed.size(), - dparams_, &decoded_bytes, &ppf)); - const double end = Now(); + const double start = jxl::Now(); + JXL_RETURN_IF_ERROR(jxl::extras::DecodeImageJXL( + compressed.data(), compressed.size(), dparams_, &decoded_bytes, &ppf)); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start); JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); 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); + stats->jxl_stats.num_inputs += 1; + JxlEncoderStatsMerge(stats->jxl_stats.stats.get(), stats_.get()); } protected: - AuxOut cinfo_; - CompressParams cparams_; + JXLCompressParams cparams_; bool has_ctransform_ = false; - extras::JXLDecompressParams dparams_; + bool modular_mode_ = false; + JXLDecompressParams dparams_; bool uint8_ = false; - bool normalize_bitrate_ = false; + std::unique_ptr<JxlEncoderStats, decltype(JxlEncoderStatsDestroy)*> stats_; + + private: + void SetDebugImageCallback(const std::string& filename, + std::string* debug_prefix, + JXLCompressParams* cparams) { + if (jxlargs->debug_image_dir.empty()) return; + *debug_prefix = JoinPath(jxlargs->debug_image_dir, FileBaseName(filename)) + + ".jxl:" + params_ + ".dbg/"; + JXL_CHECK(MakeDir(*debug_prefix)); + cparams->debug_image_opaque = debug_prefix; + cparams->debug_image = [](void* opaque, const char* label, size_t xsize, + size_t ysize, const JxlColorEncoding* color, + const uint16_t* pixels) { + auto encoder = jxl::extras::GetAPNGEncoder(); + JXL_CHECK(encoder); + PackedPixelFile debug_ppf; + JxlPixelFormat format{3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + PackedFrame frame(xsize, ysize, format); + memcpy(frame.color.pixels(), pixels, 6 * xsize * ysize); + debug_ppf.frames.emplace_back(std::move(frame)); + debug_ppf.info.xsize = xsize; + debug_ppf.info.ysize = ysize; + debug_ppf.info.num_color_channels = 3; + debug_ppf.info.bits_per_sample = 16; + debug_ppf.color_encoding = *color; + EncodedImage encoded; + JXL_CHECK(encoder->Encode(debug_ppf, &encoded)); + JXL_CHECK(!encoded.bitstreams.empty()); + std::string* debug_prefix = reinterpret_cast<std::string*>(opaque); + std::string fn = *debug_prefix + std::string(label) + ".png"; + WriteFile(fn, encoded.bitstreams[0]); + }; + } }; ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args) { return new JxlCodec(args); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/benchmark/benchmark_codec_jxl.h b/tools/benchmark/benchmark_codec_jxl.h index 12e9fef..967be26 100644 --- a/tools/benchmark/benchmark_codec_jxl.h +++ b/tools/benchmark/benchmark_codec_jxl.h @@ -12,12 +12,14 @@ #include "tools/benchmark/benchmark_args.h" #include "tools/benchmark/benchmark_codec.h" -namespace jxl { +namespace jpegxl { +namespace tools { ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args); // Registers the jxl-specific command line options. Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args); Status ValidateArgsJxlCodec(BenchmarkArgs* args); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_JXL_H_ diff --git a/tools/benchmark/benchmark_codec_png.cc b/tools/benchmark/benchmark_codec_png.cc index b310b11..2886166 100644 --- a/tools/benchmark/benchmark_codec_png.cc +++ b/tools/benchmark/benchmark_codec_png.cc @@ -3,8 +3,6 @@ // 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> @@ -18,12 +16,14 @@ #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 "lib/jxl/image_bundle.h" +#include "lib/jxl/image_metadata.h" +#include "tools/thread_pool_internal.h" -namespace jxl { +namespace jpegxl { +namespace tools { struct PNGArgs { // Empty, no PNG-specific args currently. @@ -41,36 +41,43 @@ class PNGCodec : public ImageCodec { Status ParseParam(const std::string& param) override { return true; } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, + ThreadPool* pool, std::vector<uint8_t>* 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(Encode(*io, extras::Codec::kPNG, io->Main().c_current(), - bits, compressed, pool)); - const double end = Now(); + const double start = jxl::Now(); + JXL_RETURN_IF_ERROR(jxl::Encode(*io, jxl::extras::Codec::kPNG, + io->Main().c_current(), bits, compressed, + pool)); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start); return true; } Status Decompress(const std::string& /*filename*/, - const Span<const uint8_t> compressed, - ThreadPoolInternal* pool, CodecInOut* io, + const Span<const uint8_t> compressed, ThreadPool* 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(); + jxl::extras::PackedPixelFile ppf; + const double start = jxl::Now(); + JXL_RETURN_IF_ERROR(jxl::extras::DecodeImageAPNG( + compressed, jxl::extras::ColorHints(), &ppf)); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start); - JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); + JXL_RETURN_IF_ERROR( + jxl::extras::ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); return true; } }; ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args) { - return new PNGCodec(args); + if (jxl::extras::GetAPNGEncoder() && + jxl::extras::CanDecode(jxl::extras::Codec::kPNG)) { + return new PNGCodec(args); + } else { + return nullptr; + } } -} // namespace jxl +} // namespace tools +} // namespace jpegxl -#endif diff --git a/tools/benchmark/benchmark_codec_png.h b/tools/benchmark/benchmark_codec_png.h index 23d982e..8f29583 100644 --- a/tools/benchmark/benchmark_codec_png.h +++ b/tools/benchmark/benchmark_codec_png.h @@ -6,21 +6,19 @@ #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 { +namespace jpegxl { +namespace tools { ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args); // Registers the png-specific command line options. Status AddCommandLineOptionsPNGCodec(BenchmarkArgs* args); -} // namespace jxl - -#endif +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_ diff --git a/tools/benchmark/benchmark_codec_webp.cc b/tools/benchmark/benchmark_codec_webp.cc index 3b1bb26..926dee6 100644 --- a/tools/benchmark/benchmark_codec_webp.cc +++ b/tools/benchmark/benchmark_codec_webp.cc @@ -4,6 +4,7 @@ // license that can be found in the LICENSE file. #include "tools/benchmark/benchmark_codec_webp.h" +#include <jxl/cms.h> #include <stdint.h> #include <string.h> #include <webp/decode.h> @@ -15,35 +16,39 @@ #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/base/status.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" +#include "tools/thread_pool_internal.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::ImageBundle; +using ::jxl::ImageMetadata; +using ::jxl::ThreadPool; // 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 bool has_alpha, 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 size_t bits_per_sample = (is_16bit ? 2 : 1) * jxl::kBitsPerByte; + const uint32_t num_channels = (is_gray ? 1 : 3) + (has_alpha ? 1 : 0); + JxlDataType data_type = is_16bit ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8; + JxlPixelFormat format = {num_channels, data_type, endianness, 0}; 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, pool, ib, - /*float_in=*/false, /*align=*/0); + return ConvertFromExternal(span, xsize, ysize, c, bits_per_sample, format, + pool, ib); } struct WebPArgs { @@ -85,9 +90,9 @@ class WebPCodec : public ImageCodec { } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, + ThreadPool* pool, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) override { - const double start = Now(); + const double start = jxl::Now(); const ImageBundle& ib = io->Main(); if (ib.HasAlpha() && ib.metadata()->GetAlphaBits() > 8) { @@ -99,8 +104,8 @@ class WebPCodec : public ImageCodec { 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)); + JXL_RETURN_IF_ERROR(jxl::TransformIfNeeded( + ib, c_desired, *JxlGetDefaultCms(), pool, &store, &transformed)); size_t xsize = ib.oriented_xsize(); size_t ysize = ib.oriented_ysize(); size_t stride = xsize * num_chans; @@ -153,14 +158,14 @@ class WebPCodec : public ImageCodec { } else { return false; } - const double end = Now(); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start); return true; } Status Decompress(const std::string& filename, - const Span<const uint8_t> compressed, - ThreadPoolInternal* pool, CodecInOut* io, + const Span<const uint8_t> compressed, ThreadPool* pool, + CodecInOut* io, jpegxl::tools::SpeedStats* speed_stats) override { WebPDecoderConfig config; #ifdef MEMORY_SANITIZER @@ -177,11 +182,11 @@ class WebPCodec : public ImageCodec { buf->colorspace = MODE_RGBA; const uint8_t* webp_data = compressed.data(); const int webp_size = compressed.size(); - const double start = Now(); + const double start = jxl::Now(); if (WebPDecode(webp_data, webp_size, &config) != VP8_STATUS_OK) { return JXL_FAILURE("WebPDecode failed"); } - const double end = Now(); + const double end = jxl::Now(); speed_stats->NotifyElapsed(end - start); JXL_CHECK(buf->u.RGBA.stride == buf->width * 4); @@ -191,7 +196,7 @@ class WebPCodec : public ImageCodec { 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); + jxl::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. @@ -201,13 +206,11 @@ class WebPCodec : public ImageCodec { 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()); + const Status ok = FromSRGB(buf->width, buf->height, is_gray, has_alpha, + /*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; } @@ -228,7 +231,9 @@ class WebPCodec : public ImageCodec { std::vector<uint8_t>* compressed) { compressed->clear(); WebPConfig config; - WebPConfigInit(&config); + if (!WebPConfigInit(&config)) { + return JXL_FAILURE("WebPConfigInit failed"); + } JXL_ASSERT(!lossless_ || !near_lossless_); // can't have both config.lossless = lossless_; config.quality = quality; @@ -243,7 +248,9 @@ class WebPCodec : public ImageCodec { JXL_CHECK(WebPValidateConfig(&config)); WebPPicture pic; - WebPPictureInit(&pic); + if (!WebPPictureInit(&pic)) { + return JXL_FAILURE("WebPPictureInit failed"); + } pic.width = static_cast<int>(xsize); pic.height = static_cast<int>(ysize); pic.writer = &WebPStringWrite; @@ -251,9 +258,13 @@ class WebPCodec : public ImageCodec { pic.custom_ptr = compressed; if (num_chans == 3) { - WebPPictureImportRGB(&pic, srgb.data(), 3 * xsize); + if (!WebPPictureImportRGB(&pic, srgb.data(), 3 * xsize)) { + return JXL_FAILURE("WebPPictureImportRGB failed"); + } } else { - WebPPictureImportRGBA(&pic, srgb.data(), 4 * xsize); + if (!WebPPictureImportRGBA(&pic, srgb.data(), 4 * xsize)) { + return JXL_FAILURE("WebPPictureImportRGBA failed"); + } } // WebP encoding may fail, for example, if the image is more than 16384 @@ -262,7 +273,7 @@ class WebPCodec : public ImageCodec { WebPPictureFree(&pic); // Compressed image data is initialized by libwebp, which we are not // instrumenting with msan. - msan::UnpoisonMemory(compressed->data(), compressed->size()); + jxl::msan::UnpoisonMemory(compressed->data(), compressed->size()); return ok; } @@ -277,4 +288,5 @@ ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args) { return new WebPCodec(args); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/benchmark/benchmark_codec_webp.h b/tools/benchmark/benchmark_codec_webp.h index cd4c60f..37d3c58 100644 --- a/tools/benchmark/benchmark_codec_webp.h +++ b/tools/benchmark/benchmark_codec_webp.h @@ -13,11 +13,13 @@ #include "tools/benchmark/benchmark_args.h" #include "tools/benchmark/benchmark_codec.h" -namespace jxl { +namespace jpegxl { +namespace tools { ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args); // Registers the webp-specific command line options. Status AddCommandLineOptionsWebPCodec(BenchmarkArgs* args); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_WEBP_H_ diff --git a/tools/benchmark/benchmark_file_io.cc b/tools/benchmark/benchmark_file_io.cc index c5db02b..b8acbfb 100644 --- a/tools/benchmark/benchmark_file_io.cc +++ b/tools/benchmark/benchmark_file_io.cc @@ -38,7 +38,8 @@ #define GLOB_TILDE 0 #endif -namespace jxl { +namespace jpegxl { +namespace tools { const char kPathSeparator = '/'; @@ -229,4 +230,5 @@ Status MatchFiles(const std::string& pattern, std::vector<std::string>* list) { #endif // HAS_GLOB } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/benchmark/benchmark_file_io.h b/tools/benchmark/benchmark_file_io.h index ecb8359..3c68acc 100644 --- a/tools/benchmark/benchmark_file_io.h +++ b/tools/benchmark/benchmark_file_io.h @@ -12,10 +12,13 @@ #include <string> #include <vector> -#include "lib/jxl/base/file_io.h" #include "lib/jxl/base/status.h" +#include "tools/file_io.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::Status; // Checks if the file exists, either as file or as directory bool PathExists(const std::string& fname); @@ -48,6 +51,7 @@ Status MatchFiles(const std::string& pattern, std::vector<std::string>* list); std::string JoinPath(const std::string& first, const std::string& second); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_FILE_IO_H_ diff --git a/tools/benchmark/benchmark_stats.cc b/tools/benchmark/benchmark_stats.cc index f22e89c..87b9985 100644 --- a/tools/benchmark/benchmark_stats.cc +++ b/tools/benchmark/benchmark_stats.cc @@ -17,7 +17,55 @@ #include "lib/jxl/base/status.h" #include "tools/benchmark/benchmark_args.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +#define ADD_NAME(val, name) \ + case JXL_ENC_STAT_##val: \ + return name +const char* JxlStatsName(JxlEncoderStatsKey key) { + switch (key) { + ADD_NAME(HEADER_BITS, "Header bits"); + ADD_NAME(TOC_BITS, "TOC bits"); + ADD_NAME(DICTIONARY_BITS, "Patch dictionary bits"); + ADD_NAME(SPLINES_BITS, "Splines bits"); + ADD_NAME(NOISE_BITS, "Noise bits"); + ADD_NAME(QUANT_BITS, "Quantizer bits"); + ADD_NAME(MODULAR_TREE_BITS, "Modular tree bits"); + ADD_NAME(MODULAR_GLOBAL_BITS, "Modular global bits"); + ADD_NAME(DC_BITS, "DC bits"); + ADD_NAME(MODULAR_DC_GROUP_BITS, "Modular DC group bits"); + ADD_NAME(CONTROL_FIELDS_BITS, "Control field bits"); + ADD_NAME(COEF_ORDER_BITS, "Coeff order bits"); + ADD_NAME(AC_HISTOGRAM_BITS, "AC histogram bits"); + ADD_NAME(AC_BITS, "AC token bits"); + ADD_NAME(MODULAR_AC_GROUP_BITS, "Modular AC group bits"); + ADD_NAME(NUM_SMALL_BLOCKS, "Number of small blocks"); + ADD_NAME(NUM_DCT4X8_BLOCKS, "Number of 4x8 blocks"); + ADD_NAME(NUM_AFV_BLOCKS, "Number of AFV blocks"); + ADD_NAME(NUM_DCT8_BLOCKS, "Number of 8x8 blocks"); + ADD_NAME(NUM_DCT8X32_BLOCKS, "Number of 8x32 blocks"); + ADD_NAME(NUM_DCT16_BLOCKS, "Number of 16x16 blocks"); + ADD_NAME(NUM_DCT16X32_BLOCKS, "Number of 16x32 blocks"); + ADD_NAME(NUM_DCT32_BLOCKS, "Number of 32x32 blocks"); + ADD_NAME(NUM_DCT32X64_BLOCKS, "Number of 32x64 blocks"); + ADD_NAME(NUM_DCT64_BLOCKS, "Number of 64x64 blocks"); + ADD_NAME(NUM_BUTTERAUGLI_ITERS, "Butteraugli iters"); + default: + return ""; + }; + return ""; +} +#undef ADD_NAME + +void JxlStats::Print() const { + for (int i = 0; i < JXL_ENC_NUM_STATS; ++i) { + JxlEncoderStatsKey key = static_cast<JxlEncoderStatsKey>(i); + size_t value = JxlEncoderStatsGet(stats.get(), key); + if (value) printf("%-25s %10" PRIuS "\n", JxlStatsName(key), value); + } +} + namespace { // Computes longest codec name from Args()->codec, for table alignment. @@ -61,7 +109,7 @@ struct ColumnDescriptor { bool more; // Whether to print only if more_columns is enabled }; -static const ColumnDescriptor ExtraMetricDescriptor() { +static ColumnDescriptor ExtraMetricDescriptor() { ColumnDescriptor d{{"DO NOT USE"}, 12, 4, TYPE_POSITIVE_FLOAT, false}; return d; } @@ -81,21 +129,11 @@ std::vector<ColumnDescriptor> GetColumnDescriptors(size_t num_extra_metrics) { {{"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}, + {{"SSIMULACRA2"}, 13, 8, TYPE_POSITIVE_FLOAT, false}, + {{"PSNR"}, 7, 2, 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}, + {{"QABPP"}, 8, 3, TYPE_POSITIVE_FLOAT, false}, {{"Bugs"}, 7, 5, TYPE_COUNT, false}, }; // clang-format on @@ -124,7 +162,7 @@ static std::string FormatFloat(const ColumnDescriptor& label, double value) { 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); + result.resize(end); } } return result; @@ -148,9 +186,10 @@ void BenchmarkStats::Assimilate(const BenchmarkStats& victim) { 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); + max_distance += pow(victim.max_distance, 2.0) * victim.total_input_pixels; distance_p_norm += victim.distance_p_norm; - distance_2 += victim.distance_2; + ssimulacra2 += victim.ssimulacra2; + psnr += victim.psnr; distances.insert(distances.end(), victim.distances.begin(), victim.distances.end()); total_errors += victim.total_errors; @@ -166,13 +205,6 @@ void BenchmarkStats::Assimilate(const BenchmarkStats& victim) { void BenchmarkStats::PrintMoreStats() const { if (Args()->print_more_stats) { jxl_stats.Print(); - size_t total_bits = jxl_stats.aux_out.TotalBits(); - size_t compressed_bits = total_compressed_size * kBitsPerByte; - if (total_bits != compressed_bits) { - printf("Total layer bits: %" PRIuS " vs total compressed bits: %" PRIuS - " (%.2f%% accounted for)\n", - total_bits, compressed_bits, total_bits * 100.0 / compressed_bits); - } } if (Args()->print_distance_percentiles) { std::vector<float> sorted = distances; @@ -195,13 +227,12 @@ std::vector<ColumnValue> BenchmarkStats::ComputeColumns( 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; + const double psnr_avg = psnr / total_input_pixels; + const double p_norm_avg = distance_p_norm / total_input_pixels; + const double ssimulacra2_avg = ssimulacra2 / total_input_pixels; + const double bpp_p_norm = p_norm_avg * comp_bpp; + + const double max_distance_avg = sqrt(max_distance / total_input_pixels); std::vector<ColumnValue> values( GetColumnDescriptors(extra_metrics.size()).size()); @@ -212,40 +243,15 @@ std::vector<ColumnValue> BenchmarkStats::ComputeColumns( 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; + values[6].f = static_cast<double>(max_distance_avg); + values[7].f = ssimulacra2_avg; + values[8].f = psnr_avg; + values[9].f = p_norm_avg; + values[10].f = bpp_p_norm; + values[11].f = adj_comp_bpp; + values[12].i = total_errors; for (size_t i = 0; i < extra_metrics.size(); i++) { - values[23 + i].f = extra_metrics[i] / total_input_files; + values[13 + i].f = extra_metrics[i] / total_input_files; } return values; } @@ -373,4 +379,5 @@ std::string PrintAggregate( return PrintFormattedEntries(num_extra_metrics, result); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/benchmark/benchmark_stats.h b/tools/benchmark/benchmark_stats.h index a23c4a1..deca72a 100644 --- a/tools/benchmark/benchmark_stats.h +++ b/tools/benchmark/benchmark_stats.h @@ -6,31 +6,30 @@ #ifndef TOOLS_BENCHMARK_BENCHMARK_STATS_H_ #define TOOLS_BENCHMARK_BENCHMARK_STATS_H_ +#include <jxl/stats.h> #include <stddef.h> #include <stdint.h> +#include <memory> #include <string> #include <vector> -#include "lib/jxl/aux_out.h" - -namespace jxl { +namespace jpegxl { +namespace tools { std::string StringPrintf(const char* format, ...); struct JxlStats { - JxlStats() { - num_inputs = 0; - aux_out = AuxOut(); - } + JxlStats() + : num_inputs(0), stats(JxlEncoderStatsCreate(), JxlEncoderStatsDestroy) {} void Assimilate(const JxlStats& victim) { num_inputs += victim.num_inputs; - aux_out.Assimilate(victim.aux_out); + JxlEncoderStatsMerge(stats.get(), victim.stats.get()); } - void Print() const { aux_out.Print(num_inputs); } + void Print() const; size_t num_inputs; - AuxOut aux_out; + std::unique_ptr<JxlEncoderStats, decltype(JxlEncoderStatsDestroy)*> stats; }; // The value of an entry in the table. Depending on the ColumnType, the string, @@ -61,8 +60,8 @@ struct BenchmarkStats { 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; + double psnr = 0.0; + double ssimulacra2 = 0.0; std::vector<float> distances; size_t total_errors = 0; JxlStats jxl_stats; @@ -76,6 +75,7 @@ std::string PrintAggregate( size_t num_extra_metrics, const std::vector<std::vector<ColumnValue>>& aggregate); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_STATS_H_ diff --git a/tools/benchmark/benchmark_utils.cc b/tools/benchmark/benchmark_utils.cc index 4b53131..11753f2 100644 --- a/tools/benchmark/benchmark_utils.cc +++ b/tools/benchmark/benchmark_utils.cc @@ -21,13 +21,13 @@ #include <fstream> -#include "lib/jxl/base/file_io.h" -#include "lib/jxl/codec_in_out.h" #include "lib/jxl/image_bundle.h" +#include "tools/file_io.h" extern char** environ; -namespace jxl { +namespace jpegxl { +namespace tools { TemporaryFile::TemporaryFile(std::string basename, std::string extension) { const auto extension_size = 1 + extension.size(); temp_filename_ = std::move(basename) + "_XXXXXX." + std::move(extension); @@ -50,8 +50,18 @@ Status TemporaryFile::GetFileName(std::string* const output) const { return true; } +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; +} + Status RunCommand(const std::string& command, - const std::vector<std::string>& arguments) { + const std::vector<std::string>& arguments, bool quiet) { std::vector<char*> args; args.reserve(arguments.size() + 2); args.push_back(const_cast<char*>(command.c_str())); @@ -60,18 +70,27 @@ Status RunCommand(const std::string& command, } args.push_back(nullptr); pid_t pid; - JXL_RETURN_IF_ERROR(posix_spawnp(&pid, command.c_str(), nullptr, nullptr, - args.data(), environ) == 0); + posix_spawn_file_actions_t file_actions; + posix_spawn_file_actions_init(&file_actions); + if (quiet) { + posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO); + posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO); + } + JXL_RETURN_IF_ERROR(posix_spawnp(&pid, command.c_str(), &file_actions, + nullptr, args.data(), environ) == 0); int wstatus; waitpid(pid, &wstatus, 0); + posix_spawn_file_actions_destroy(&file_actions); return WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == EXIT_SUCCESS; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl #else -namespace jxl { +namespace jpegxl { +namespace tools { TemporaryFile::TemporaryFile(std::string basename, std::string extension) {} TemporaryFile::~TemporaryFile() {} @@ -80,11 +99,14 @@ Status TemporaryFile::GetFileName(std::string* const output) const { return JXL_FAILURE("Not supported on this build"); } +std::string GetBaseName(std::string filename) { return filename; } + Status RunCommand(const std::string& command, - const std::vector<std::string>& arguments) { + const std::vector<std::string>& arguments, bool quiet) { return JXL_FAILURE("Not supported on this build"); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // _MSC_VER diff --git a/tools/benchmark/benchmark_utils.h b/tools/benchmark/benchmark_utils.h index 027fa08..5df2bec 100644 --- a/tools/benchmark/benchmark_utils.h +++ b/tools/benchmark/benchmark_utils.h @@ -11,7 +11,10 @@ #include "lib/jxl/base/status.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::Status; class TemporaryFile final { public: @@ -27,9 +30,13 @@ class TemporaryFile final { std::string temp_filename_; }; +std::string GetBaseName(std::string filename); + Status RunCommand(const std::string& command, - const std::vector<std::string>& arguments); + const std::vector<std::string>& arguments, + bool quiet = false); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_BENCHMARK_BENCHMARK_UTILS_H_ diff --git a/tools/benchmark/benchmark_xl.cc b/tools/benchmark/benchmark_xl.cc index fed5e9b..86d06a3 100644 --- a/tools/benchmark/benchmark_xl.cc +++ b/tools/benchmark/benchmark_xl.cc @@ -3,13 +3,15 @@ // 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 <jxl/cms.h> +#include <jxl/decode.h> #include <algorithm> +#include <cmath> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> #include <memory> #include <mutex> #include <numeric> @@ -17,27 +19,22 @@ #include <utility> #include <vector> -#include "jxl/decode.h" #include "lib/extras/codec.h" #include "lib/extras/dec/color_hints.h" +#include "lib/extras/enc/apng.h" +#include "lib/extras/metrics.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/cache_aligned.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" @@ -48,32 +45,65 @@ #include "tools/benchmark/benchmark_stats.h" #include "tools/benchmark/benchmark_utils.h" #include "tools/codec_config.h" +#include "tools/file_io.h" #include "tools/speed_stats.h" +#include "tools/ssimulacra2.h" +#include "tools/thread_pool_internal.h" -namespace jxl { +namespace jpegxl { +namespace tools { namespace { +using ::jxl::ButteraugliParams; +using ::jxl::Bytes; +using ::jxl::CodecInOut; +using ::jxl::ColorEncoding; +using ::jxl::Image3F; +using ::jxl::ImageBundle; +using ::jxl::ImageF; +using ::jxl::Rng; +using ::jxl::Status; +using ::jxl::ThreadPool; + 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); + std::vector<uint8_t> encoded; + return Encode(io, filename, &encoded, pool) && WriteFile(filename, encoded); } Status ReadPNG(const std::string& filename, Image3F* image) { CodecInOut io; - JXL_CHECK(SetFromFile(filename, extras::ColorHints(), &io)); - *image = CopyImage(*io.Main().color()); + std::vector<uint8_t> encoded; + JXL_CHECK(ReadFile(filename, &encoded)); + JXL_CHECK( + jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(), &io)); + *image = Image3F(io.xsize(), io.ysize()); + CopyImageTo(*io.Main().color(), image); return true; } +std::string CodecToExtension(std::string codec_name, char sep) { + std::string result; + // Add in the parameters of the codec_name in reverse order, so that the + // name of the file format (e.g. jxl) is last. + int pos = static_cast<int>(codec_name.size()) - 1; + while (pos > 0) { + int prev = codec_name.find_last_of(sep, pos); + if (prev > pos) prev = -1; + result += '.' + codec_name.substr(prev + 1, pos - prev); + pos = prev - 1; + } + return result; +} + void DoCompress(const std::string& filename, const CodecInOut& io, const std::vector<std::string>& extra_metrics_commands, - ImageCodec* codec, ThreadPoolInternal* inner_pool, + ImageCodec* codec, ThreadPool* inner_pool, std::vector<uint8_t>* compressed, BenchmarkStats* s) { - PROFILER_FUNC; ++s->total_input_files; if (io.frames.size() != 1) { @@ -104,7 +134,7 @@ void DoCompress(const std::string& filename, const CodecInOut& io, 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; + std::vector<uint8_t> data_in; JXL_CHECK(ReadFile(filename, &data_in)); JXL_CHECK( codec->RecompressJpeg(filename, data_in, compressed, &speed_stats)); @@ -142,8 +172,8 @@ void DoCompress(const std::string& filename, const CodecInOut& io, 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 (!codec->Decompress(filename, Bytes(*compressed), inner_pool, &io2, + &speed_stats)) { if (!Args()->silent_errors) { fprintf(stderr, "%s failed to decompress encoded image. Original source:" @@ -152,10 +182,13 @@ void DoCompress(const std::string& filename, const CodecInOut& io, } 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; + // TODO(veluca): this is a hack. codec->Decompress should set the bitdepth + // correctly, but for jxl it currently sets it from the pixel format (i.e. + // 32-bit float). + io2.metadata.m.bit_depth = io.metadata.m.bit_depth; + } + for (const auto& frame : io2.frames) { + s->total_input_pixels += frame.color().xsize() * frame.color().ysize(); } JXL_CHECK(speed_stats.GetSummary(&summary)); s->total_time_decode += summary.central_tendency; @@ -180,9 +213,7 @@ void DoCompress(const std::string& filename, const CodecInOut& io, valid = false; } - bool lossless = codec->IsJpegTranscoder(); - bool skip_butteraugli = - Args()->skip_butteraugli || Args()->decode_only || lossless; + bool skip_butteraugli = Args()->skip_butteraugli || Args()->decode_only; ImageF distmap; float max_distance = 1.0f; @@ -193,10 +224,9 @@ void DoCompress(const std::string& filename, const CodecInOut& io, ImageBundle& ib2 = io2.frames[i]; // Verify output - PROFILER_ZONE("Benchmark stats"); float distance; if (SameSize(ib1, ib2)) { - ButteraugliParams params = codec->BaParams(); + ButteraugliParams params; if (ib1.metadata()->IntensityTarget() != ib2.metadata()->IntensityTarget()) { fprintf(stderr, @@ -210,21 +240,24 @@ void DoCompress(const std::string& filename, const CodecInOut& io, 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()); + distance = + ButteraugliDistance(ib1, ib2, params, *JxlGetDefaultCms(), &distmap, + inner_pool, codec->IgnoreAlpha()); } 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->psnr += + compressed->empty() + ? 0 + : jxl::ComputePSNR(ib1, ib2, *JxlGetDefaultCms()) * input_pixels; s->distance_p_norm += - ComputeDistanceP(distmap, Args()->ba_params, Args()->error_pnorm) * + ComputeDistanceP(distmap, ButteraugliParams(), Args()->error_pnorm) * input_pixels; + s->ssimulacra2 += ComputeSSIMULACRA2(ib1, ib2).Score() * input_pixels; s->max_distance = std::max(s->max_distance, distance); s->distances.push_back(distance); max_distance = std::max(max_distance, distance); @@ -265,48 +298,44 @@ void DoCompress(const std::string& filename, const CodecInOut& io, std::string dir = FileDirName(filename); std::string outdir = Args()->output_dir.empty() ? dir + "/out" : Args()->output_dir; - std::string compressed_fn = outdir + "/" + name; - // Add in the parameters of the codec_name in reverse order, so that the - // name of the file format (e.g. jxl) is last. - int pos = static_cast<int>(codec_name.size()) - 1; - while (pos > 0) { - int prev = codec_name.find_last_of(':', pos); - if (prev > pos) prev = -1; - compressed_fn += '.' + codec_name.substr(prev + 1, pos - prev); - pos = prev - 1; - } + std::string compressed_fn = + outdir + "/" + name + CodecToExtension(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 + std::string heatmap_fn; + if (jxl::extras::GetAPNGEncoder()) { + heatmap_fn = compressed_fn + ".heatmap.png"; + } else { + heatmap_fn = compressed_fn + ".heatmap.ppm"; + } 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)); + JXL_CHECK(WriteFile(compressed_fn, *compressed)); } 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)); + *JxlGetDefaultCms(), 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)); + std::vector<uint8_t> encoded; + JXL_CHECK(Encode(io2, *c_desired, + ib2.metadata()->bit_depth.bits_per_sample, + decompressed_fn, &encoded)); + JXL_CHECK(WriteFile(decompressed_fn, encoded)); 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)); + float good = Args()->heatmap_good > 0.0f + ? Args()->heatmap_good + : jxl::ButteraugliFuzzyInverse(1.5); + float bad = Args()->heatmap_bad > 0.0f + ? Args()->heatmap_bad + : jxl::ButteraugliFuzzyInverse(0.5); + if (Args()->save_heatmap) { + JXL_CHECK(WriteImage(CreateHeatMapImage(distmap, good, bad), + inner_pool, heatmap_fn)); + } } } } @@ -324,10 +353,13 @@ void DoCompress(const std::string& filename, const CodecInOut& io, // 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)); + std::vector<uint8_t> encoded; + JXL_CHECK(Encode(io, c_desired, io.metadata.m.bit_depth.bits_per_sample, + tmp_in_fn, &encoded)); + JXL_CHECK(WriteFile(tmp_in_fn, encoded)); + JXL_CHECK(Encode(io2, c_desired, io.metadata.m.bit_depth.bits_per_sample, + tmp_out_fn, &encoded)); + JXL_CHECK(WriteFile(tmp_out_fn, encoded)); if (io.metadata.m.IntensityTarget() != io2.metadata.m.IntensityTarget()) { fprintf(stderr, "WARNING: original and decoded have different intensity targets " @@ -371,7 +403,7 @@ void DoCompress(const std::string& filename, const CodecInOut& io, // Makes a base64 data URI for embedded image in HTML std::string Base64Image(const std::string& filename) { - PaddedBytes bytes; + std::vector<uint8_t> bytes; if (!ReadFile(filename, &bytes)) { return ""; } @@ -406,12 +438,13 @@ 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) { + bool add_heatmap, bool self_contained) { std::string toggle_js = "<script type=\"text/javascript\">\n" " var codecname = '" + codec_desc + "';\n"; - toggle_js += R"( + if (add_heatmap) { + 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.'; @@ -435,7 +468,7 @@ void WriteHtmlReport(const std::string& codec_desc, hm.style.display = 'block'; } } - function toggle3(i) { + function toggle(i) { for (index = counter.length; index <= i; index++) { counter.push(1); } @@ -460,6 +493,48 @@ void WriteHtmlReport(const std::string& codec_desc, }; </script> )"; + } else { + toggle_js += R"( + var maintitle = codecname + ' - click images to toggle, press space to' + + ' toggle all. 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); + if (s == 0) { + preview.style.display = 'none'; + orig.style.display = 'block'; + } else if (s == 1) { + preview.style.display = 'block'; + orig.style.display = 'none'; + } + } + function toggle(i) { + for (index = counter.length; index <= i; index++) { + counter.push(1); + } + setState(i, counter[i]); + counter[i] = 1 - counter[i]; + document.title = maintitle; + } + var toggleall_state = 1; + document.body.onkeydown = function(e) { + // space (32) to toggle orig/compr + if (e.keyCode == 32) { + var divs = document.getElementsByTagName('div'); + toggleall_state = 1 - toggleall_state; + document.title = codecname + ' - ' + (toggleall_state == 0 ? + 'originals' : 'compressed'); + 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"; @@ -471,8 +546,12 @@ void WriteHtmlReport(const std::string& codec_desc, 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 name_out = name + CodecToExtension(codec_name, '_'); + if (Args()->html_report_use_decompressed) { + name_out += Args()->output_extension; + } + std::string heatmap_out = + name + CodecToExtension(codec_name, '_') + ".heatmap.png"; std::string fname_orig = fnames[i]; std::string fname_out = outdir + "/" + name_out; @@ -500,28 +579,24 @@ void WriteHtmlReport(const std::string& codec_desc, 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 + + out_html += "<div onclick=\"toggle(" + 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 += "\"" + url_out + "\"style=\"display:block;\"/>\n"; + out_html += " <img title=\"original\" id=\"orig" + number + "\" src="; + out_html += "\"" + url_orig + "\"style=\"display:none;\"/>\n"; + if (add_heatmap) { + out_html = " <img title=\"heatmap\" id=\"hm" + number + "\" src="; + out_html += "\"" + url_heatmap + "\"style=\"display:none;\"/>\n"; + } + out_html += "</div>\n"; } out_html += "</body>\n"; out_html += toggle_js; - JXL_CHECK(WriteFile(out_html, outdir + "/index." + codec_name + ".html")); + JXL_CHECK(WriteFile(outdir + "/index." + codec_name + ".html", out_html)); } // Prints the detailed and aggregate statistics, in the correct order but as @@ -552,7 +627,6 @@ struct StatPrinter { } 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) { @@ -603,17 +677,13 @@ struct StatPrinter { 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 psnr = t.stats.psnr / t.stats.total_input_pixels; + double ssimulacra2 = t.stats.ssimulacra2 / 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 = @@ -646,10 +716,11 @@ struct StatPrinter { 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 ", + " psnr:%10.8f ssimulacra2:%.2f 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); + dec_mps, comp_bpp, t.stats.max_distance, psnr, ssimulacra2, 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]); @@ -660,7 +731,6 @@ struct StatPrinter { } 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; @@ -680,6 +750,7 @@ struct StatPrinter { if (Args()->write_html_report) { WriteHtmlReport(method, *fnames_, tasks, images, + Args()->save_heatmap && Args()->html_report_add_heatmap, Args()->html_report_self_contained); } @@ -741,27 +812,21 @@ class Benchmark { 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::vector<Task> tasks = CreateTasks(methods, fnames); std::unique_ptr<ThreadPoolInternal> pool; std::vector<std::unique_ptr<ThreadPoolInternal>> inner_pools; - InitThreads(static_cast<int>(tasks.size()), &pool, &inner_pools); + InitThreads(tasks.size(), &pool, &inner_pools); - const std::vector<CodecInOut> loaded_images = LoadImages( - fnames, all_color_aware, jpeg_transcoding_requested, pool.get()); + const std::vector<CodecInOut> loaded_images = LoadImages(fnames, &*pool); if (RunTasks(methods, extra_metrics_names, extra_metrics_commands, fnames, - loaded_images, pool.get(), inner_pools, &tasks) != 0) { + loaded_images, &*pool, inner_pools, &tasks) != 0) { ret = EXIT_FAILURE; if (!Args()->silent_errors) { fprintf(stderr, "There were error(s) in the benchmark.\n"); @@ -769,24 +834,23 @@ class Benchmark { } } - // Must have exited profiler zone above before calling. - if (Args()->profiler) { - PROFILER_PRINT_RESULTS(); - } - CacheAligned::PrintStats(); + jxl::CacheAligned::PrintStats(); return ret; } private: - static int NumOuterThreads(const int num_hw_threads, const int num_tasks) { - int num_threads = Args()->num_threads; + static size_t NumOuterThreads(const size_t num_hw_threads, + const size_t num_tasks) { // Default to #cores - if (num_threads < 0) num_threads = num_hw_threads; + size_t num_threads = num_hw_threads; + if (Args()->num_threads >= 0) { + num_threads = static_cast<size_t>(Args()->num_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()); + std::min<size_t>(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); @@ -797,14 +861,21 @@ class Benchmark { return num_threads; } - static int NumInnerThreads(const int num_hw_threads, const int num_threads) { - int num_inner = Args()->inner_threads; + static int NumInnerThreads(const size_t num_hw_threads, + const size_t num_threads) { + size_t num_inner; // 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; + if (Args()->inner_threads < 0) { + if (num_threads == 0) { + num_inner = num_hw_threads; + } else if (num_hw_threads <= num_threads) { + num_inner = 1; + } else { + num_inner = (num_hw_threads - num_threads) / num_threads; + } + } else { + num_inner = static_cast<size_t>(Args()->inner_threads); } // Just one thread is counterproductive. @@ -814,20 +885,21 @@ class Benchmark { } static void InitThreads( - const int num_tasks, std::unique_ptr<ThreadPoolInternal>* pool, + size_t 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); + const size_t num_hw_threads = std::thread::hardware_concurrency(); + const size_t num_threads = NumOuterThreads(num_hw_threads, num_tasks); + const size_t num_inner = NumInnerThreads(num_hw_threads, num_threads); fprintf(stderr, - "%d total threads, %d tasks, %d threads, %d inner threads\n", + "%" PRIuS " total threads, %" PRIuS " tasks, %" PRIuS + " threads, %" PRIuS " 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) { + for (size_t i = 0; i < std::max<size_t>(num_threads, 1); ++i) { inner_pools->emplace_back(new ThreadPoolInternal(num_inner)); } } @@ -938,76 +1010,55 @@ class Benchmark { } // (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; + static std::vector<CodecInOut> LoadImages(const StringVec& fnames, + ThreadPool* pool) { 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()); - } - } + const auto process_image = [&](const uint32_t task, size_t /*thread*/) { + const size_t i = static_cast<size_t>(task); + Status ok = true; + + if (!Args()->decode_only) { + std::vector<uint8_t> encoded; + ok = ReadFile(fnames[i], &encoded); + if (ok) { + ok = jxl::SetFromBytes(Bytes(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 && 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")); + 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); + } + } + }; + JXL_CHECK(jxl::RunOnPool(pool, 0, static_cast<uint32_t>(fnames.size()), + ThreadPool::NoInit, process_image, "Load images")); return loaded_images; } static std::vector<Task> CreateTasks(const StringVec& methods, - const StringVec& fnames, - bool* all_color_aware, - bool* jpeg_transcoding_requested) { + const StringVec& fnames) { 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. @@ -1021,10 +1072,9 @@ class Benchmark { 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<CodecInOut>& loaded_images, ThreadPool* 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 @@ -1038,7 +1088,7 @@ class Benchmark { } std::vector<uint64_t> errors_thread; - JXL_CHECK(RunOnPool( + JXL_CHECK(jxl::RunOnPool( pool, 0, tasks->size(), [&](const size_t num_threads) { // Reduce false sharing by only writing every 8th slot (64 bytes). @@ -1051,14 +1101,15 @@ class Benchmark { t.image = ℑ std::vector<uint8_t> compressed; DoCompress(fnames[t.idx_image], image, extra_metrics_commands, - t.codec.get(), inner_pools[thread].get(), &compressed, + t.codec.get(), &*inner_pools[thread], &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); + return std::accumulate(errors_thread.begin(), errors_thread.end(), + size_t(0)); } }; @@ -1085,6 +1136,9 @@ int BenchmarkMain(int argc, const char** argv) { } } // namespace -} // namespace jxl +} // namespace tools +} // namespace jpegxl -int main(int argc, const char** argv) { return jxl::BenchmarkMain(argc, argv); } +int main(int argc, const char** argv) { + return jpegxl::tools::BenchmarkMain(argc, argv); +} diff --git a/tools/box/CMakeLists.txt b/tools/box/CMakeLists.txt deleted file mode 100644 index c79add0..0000000 --- a/tools/box/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# 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/tools/box/box.cc b/tools/box/box.cc deleted file mode 100644 index db73c7c..0000000 --- a/tools/box/box.cc +++ /dev/null @@ -1,285 +0,0 @@ -// 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); - pos += 4; - 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; - } - memcpy(box->type, in + pos, 4); - pos += 4; - 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. - -} // namespace tools -} // namespace jpegxl diff --git a/tools/box/box.h b/tools/box/box.h deleted file mode 100644 index 4cc3058..0000000 --- a/tools/box/box.h +++ /dev/null @@ -1,113 +0,0 @@ -// 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/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); - -} // namespace tools -} // namespace jpegxl - -#endif // TOOLS_BOX_BOX_H_ diff --git a/tools/box/box_list_main.cc b/tools/box/box_list_main.cc deleted file mode 100644 index 40ca910..0000000 --- a/tools/box/box_list_main.cc +++ /dev/null @@ -1,90 +0,0 @@ -// 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/tools/box/box_test.cc b/tools/box/box_test.cc deleted file mode 100644 index 3146bcf..0000000 --- a/tools/box/box_test.cc +++ /dev/null @@ -1,76 +0,0 @@ -// 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/tools/build_cleaner.py b/tools/build_cleaner.py deleted file mode 100755 index 76857d7..0000000 --- a/tools/build_cleaner.py +++ /dev/null @@ -1,317 +0,0 @@ -#!/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_image.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. - ]) - # 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/tools/butteraugli_main.cc b/tools/butteraugli_main.cc index 247ade8..436d290 100644 --- a/tools/butteraugli_main.cc +++ b/tools/butteraugli_main.cc @@ -3,6 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/cms.h> #include <stdint.h> #include <stdio.h> @@ -11,57 +12,70 @@ #include "lib/extras/codec.h" #include "lib/extras/dec/color_hints.h" +#include "lib/extras/metrics.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/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/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" +#include "tools/file_io.h" +#include "tools/thread_pool_internal.h" -namespace jxl { namespace { +using jpegxl::tools::ThreadPoolInternal; +using jxl::ButteraugliParams; +using jxl::CodecInOut; +using jxl::ColorEncoding; +using jxl::Image3F; +using jxl::ImageF; +using jxl::Status; + 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); + + std::vector<uint8_t> encoded; + return jxl::Encode(io, filename, &encoded, &pool) && + jpegxl::tools::WriteFile(filename, encoded); } Status RunButteraugli(const char* pathname1, const char* pathname2, const std::string& distmap_filename, + const std::string& raw_distmap_filename, const std::string& colorspace_hint, double p, float intensity_target) { - extras::ColorHints color_hints; + jxl::extras::ColorHints color_hints; if (!colorspace_hint.empty()) { color_hints.Add("color_space", colorspace_hint); } - CodecInOut io1; + const char* pathname[2] = {pathname1, pathname2}; + CodecInOut io[2]; 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; + for (size_t i = 0; i < 2; ++i) { + std::vector<uint8_t> encoded; + if (!jpegxl::tools::ReadFile(pathname[i], &encoded)) { + fprintf(stderr, "Failed to read image from %s\n", pathname[i]); + return false; + } + if (!jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &io[i], &pool)) { + fprintf(stderr, "Failed to decode image from %s\n", pathname[i]); + return false; + } } + CodecInOut& io1 = io[0]; + CodecInOut& io2 = io[1]; if (io1.xsize() != io2.xsize()) { fprintf(stderr, "Width mismatch: %" PRIuS " %" PRIuS "\n", io1.xsize(), io2.xsize()); @@ -75,33 +89,43 @@ Status RunButteraugli(const char* pathname1, const char* pathname2, ImageF distmap; ButteraugliParams ba_params; - ba_params.hf_asymmetry = 0.8f; + ba_params.hf_asymmetry = 1.0f; ba_params.xmul = 1.0f; ba_params.intensity_target = intensity_target; - const float distance = ButteraugliDistance(io1.Main(), io2.Main(), ba_params, - GetJxlCms(), &distmap, &pool); + const float distance = jxl::ButteraugliDistance( + io1.Main(), io2.Main(), ba_params, *JxlGetDefaultCms(), &distmap, &pool); printf("%.10f\n", distance); - double pnorm = ComputeDistanceP(distmap, ba_params, p); + double pnorm = jxl::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)); + float good = jxl::ButteraugliFuzzyInverse(1.5); + float bad = jxl::ButteraugliFuzzyInverse(0.5); + JXL_CHECK(WriteImage(jxl::CreateHeatMapImage(distmap, good, bad), + distmap_filename)); + } + if (!raw_distmap_filename.empty()) { + FILE* out = fopen(raw_distmap_filename.c_str(), "wb"); + JXL_CHECK(out != nullptr); + fprintf(out, "Pf\n%" PRIuS " %" PRIuS "\n-1.0\n", distmap.xsize(), + distmap.ysize()); + for (size_t y = distmap.ysize(); y-- > 0;) { + fwrite(distmap.Row(y), 4, distmap.xsize(), out); + } + fclose(out); } return true; } } // namespace -} // namespace jxl int main(int argc, char** argv) { if (argc < 3) { fprintf(stderr, "Usage: %s <reference> <distorted>\n" " [--distmap <distmap>]\n" + " [--rawdistmap <distmap.pfm>]\n" " [--intensity_target <intensity_target>]\n" " [--colorspace <colorspace_hint>]\n" " [--pnorm <pth norm>]\n" @@ -114,12 +138,15 @@ int main(int argc, char** argv) { return 1; } std::string distmap; + std::string raw_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]) == "--rawdistmap" && i + 1 < argc) { + raw_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) { @@ -137,8 +164,6 @@ int main(int argc, char** argv) { } } - return jxl::RunButteraugli(argv[1], argv[2], distmap, colorspace, p, - intensity_target) - ? 0 - : 1; + return !RunButteraugli(argv[1], argv[2], distmap, raw_distmap, colorspace, p, + intensity_target); } diff --git a/tools/cjpeg_hdr.cc b/tools/cjpeg_hdr.cc deleted file mode 100644 index cfe272e..0000000 --- a/tools/cjpeg_hdr.cc +++ /dev/null @@ -1,306 +0,0 @@ -// 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/tools/cjpegli.cc b/tools/cjpegli.cc new file mode 100644 index 0000000..4088e27 --- /dev/null +++ b/tools/cjpegli.cc @@ -0,0 +1,270 @@ +// 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 <vector> + +#include "lib/extras/dec/decode.h" +#include "lib/extras/enc/jpegli.h" +#include "lib/extras/time.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/span.h" +#include "tools/args.h" +#include "tools/cmdline.h" +#include "tools/file_io.h" +#include "tools/speed_stats.h" + +namespace jpegxl { +namespace tools { +namespace { + +struct Args { + void AddCommandLineOptions(CommandLineParser* cmdline) { + std::string input_help("the input can be "); + if (jxl::extras::CanDecode(jxl::extras::Codec::kPNG)) { + input_help.append("PNG, APNG, "); + } + if (jxl::extras::CanDecode(jxl::extras::Codec::kGIF)) { + input_help.append("GIF, "); + } + if (jxl::extras::CanDecode(jxl::extras::Codec::kEXR)) { + input_help.append("EXR, "); + } + input_help.append("PPM, PFM, or PGX"); + cmdline->AddPositionalOption("INPUT", /* required = */ true, input_help, + &file_in); + cmdline->AddPositionalOption("OUTPUT", /* required = */ true, + "the compressed JPG output file", &file_out); + + cmdline->AddOptionFlag('\0', "disable_output", + "No output file will be written (for benchmarking)", + &disable_output, &SetBooleanTrue, 1); + + 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_proxy, &ParseAndAppendKeyValue<ColorHintsProxy>, 1); + + opt_distance_id = cmdline->AddOptionValue( + 'd', "distance", "maxError", + "Max. butteraugli distance, lower = higher quality.\n" + " 1.0 = visually lossless (default).\n" + " Recommended range: 0.5 .. 3.0. Allowed range: 0.0 ... 25.0.\n" + " Mutually exclusive with --quality and --target_size.", + &settings.distance, &ParseFloat); + + opt_quality_id = cmdline->AddOptionValue( + 'q', "quality", "QUALITY", + "Quality setting (is remapped to --distance)." + " Default is quality 90.\n" + " Quality values roughly match libjpeg quality.\n" + " Recommended range: 68 .. 96. Allowed range: 1 .. 100.\n" + " Mutually exclusive with --distance and --target_size.", + &quality, &ParseSigned); + + cmdline->AddOptionValue('\0', "chroma_subsampling", "444|440|422|420", + "Chroma subsampling setting.", + &settings.chroma_subsampling, &ParseString); + + cmdline->AddOptionValue( + 'p', "progressive_level", "N", + "Progressive level setting. Range: 0 .. 2.\n" + " Default: 2. Higher number is more scans, 0 means sequential.", + &settings.progressive_level, &ParseSigned); + + cmdline->AddOptionFlag('\0', "xyb", "Convert to XYB colorspace", + &settings.xyb, &SetBooleanTrue, 1); + + cmdline->AddOptionFlag( + '\0', "std_quant", + "Use quantization tables based on Annex K of the JPEG standard.", + &settings.use_std_quant_tables, &SetBooleanTrue, 1); + + cmdline->AddOptionFlag( + '\0', "noadaptive_quantization", "Disable adaptive quantization.", + &settings.use_adaptive_quantization, &SetBooleanFalse, 1); + + cmdline->AddOptionFlag( + '\0', "fixed_code", + "Disable Huffman code optimization. Must be used together with -p 0.", + &settings.optimize_coding, &SetBooleanFalse, 1); + + cmdline->AddOptionValue( + '\0', "target_size", "N", + "If non-zero, set target size in bytes. This is useful for image \n" + " quality comparisons, but makes encoding speed up to 20x slower.\n" + " Mutually exclusive with --distance and --quality.", + &settings.target_size, &ParseUnsigned, 2); + + cmdline->AddOptionValue('\0', "num_reps", "N", + "How many times to compress. (For benchmarking).", + &num_reps, &ParseUnsigned, 1); + + cmdline->AddOptionFlag('\0', "quiet", "Suppress informative output", &quiet, + &SetBooleanTrue, 1); + + cmdline->AddOptionFlag( + 'v', "verbose", + "Verbose output; can be repeated, also applies to help (!).", &verbose, + &SetBooleanTrue); + } + + const char* file_in = nullptr; + const char* file_out = nullptr; + bool disable_output = false; + ColorHintsProxy color_hints_proxy; + jxl::extras::JpegSettings settings; + int quality = 90; + size_t num_reps = 1; + bool quiet = false; + bool verbose = false; + // References (ids) of specific options to check if they were matched. + CommandLineParser::OptionId opt_distance_id = -1; + CommandLineParser::OptionId opt_quality_id = -1; +}; + +bool ValidateArgs(const Args& args) { + const jxl::extras::JpegSettings& settings = args.settings; + if (settings.distance < 0.0 || settings.distance > 25.0) { + fprintf(stderr, "Invalid --distance argument\n"); + return false; + } + if (args.quality <= 0 || args.quality > 100) { + fprintf(stderr, "Invalid --quality argument\n"); + return false; + } + std::string cs = settings.chroma_subsampling; + if (!cs.empty() && cs != "444" && cs != "440" && cs != "422" && cs != "420") { + fprintf(stderr, "Invalid --chroma_subsampling argument\n"); + return false; + } + if (settings.progressive_level < 0 || settings.progressive_level > 2) { + fprintf(stderr, "Invalid --progressive_level argument\n"); + return false; + } + if (settings.progressive_level > 0 && !settings.optimize_coding) { + fprintf(stderr, "--fixed_code must be used together with -p 0\n"); + return false; + } + return true; +} + +bool SetDistance(const Args& args, const CommandLineParser& cmdline, + jxl::extras::JpegSettings* settings) { + bool distance_set = cmdline.GetOption(args.opt_distance_id)->matched(); + bool quality_set = cmdline.GetOption(args.opt_quality_id)->matched(); + int num_quality_settings = (distance_set ? 1 : 0) + (quality_set ? 1 : 0) + + (args.settings.target_size > 0 ? 1 : 0); + if (num_quality_settings > 1) { + fprintf( + stderr, + "Only one of --distance, --quality, or --target_size can be set.\n"); + return false; + } + if (quality_set) { + settings->quality = args.quality; + } + return true; +} + +int CJpegliMain(int argc, const char* argv[]) { + Args args; + CommandLineParser cmdline; + args.AddCommandLineOptions(&cmdline); + + if (!cmdline.Parse(argc, const_cast<const char**>(argv))) { + // Parse already printed the actual error cause. + fprintf(stderr, "Use '%s -h' for more information.\n", argv[0]); + return EXIT_FAILURE; + } + + if (cmdline.HelpFlagPassed() || !args.file_in) { + cmdline.PrintHelp(); + return EXIT_SUCCESS; + } + + if (!args.file_out && !args.disable_output) { + fprintf(stderr, + "No output file specified and --disable_output flag not passed.\n"); + return EXIT_FAILURE; + } + + if (args.disable_output && !args.quiet) { + fprintf(stderr, + "Encoding will be performed, but the result will be discarded.\n"); + } + + std::vector<uint8_t> input_bytes; + if (!ReadFile(args.file_in, &input_bytes)) { + fprintf(stderr, "Failed to read input image %s\n", args.file_in); + return EXIT_FAILURE; + } + + jxl::extras::PackedPixelFile ppf; + if (!jxl::extras::DecodeBytes(jxl::Bytes(input_bytes), + args.color_hints_proxy.target, &ppf)) { + fprintf(stderr, "Failed to decode input image %s\n", args.file_in); + return EXIT_FAILURE; + } + + if (!args.quiet) { + fprintf(stderr, "Read %ux%u image, %" PRIuS " bytes.\n", ppf.info.xsize, + ppf.info.ysize, input_bytes.size()); + } + + if (!ValidateArgs(args) || !SetDistance(args, cmdline, &args.settings)) { + return EXIT_FAILURE; + } + + if (!args.quiet) { + const jxl::extras::JpegSettings& s = args.settings; + fprintf(stderr, "Encoding [%s%s d%.3f%s %sAQ p%d %s]\n", + s.xyb ? "XYB" : "YUV", s.chroma_subsampling.c_str(), s.distance, + s.use_std_quant_tables ? " StdQuant" : "", + s.use_adaptive_quantization ? "" : "no", s.progressive_level, + s.optimize_coding ? "OPT" : "FIX"); + } + + jpegxl::tools::SpeedStats stats; + std::vector<uint8_t> jpeg_bytes; + for (size_t num_rep = 0; num_rep < args.num_reps; ++num_rep) { + const double t0 = jxl::Now(); + if (!jxl::extras::EncodeJpeg(ppf, args.settings, nullptr, &jpeg_bytes)) { + fprintf(stderr, "jpegli encoding failed\n"); + return EXIT_FAILURE; + } + const double t1 = jxl::Now(); + stats.NotifyElapsed(t1 - t0); + stats.SetImageSize(ppf.info.xsize, ppf.info.ysize); + } + + if (args.file_out && !args.disable_output) { + if (!WriteFile(args.file_out, jpeg_bytes)) { + fprintf(stderr, "Could not write jpeg to %s\n", args.file_out); + return EXIT_FAILURE; + } + } + if (!args.quiet) { + fprintf(stderr, "Compressed to %" PRIuS " bytes ", jpeg_bytes.size()); + const size_t num_pixels = ppf.info.xsize * ppf.info.ysize; + const double bpp = + static_cast<double>(jpeg_bytes.size() * jxl::kBitsPerByte) / num_pixels; + fprintf(stderr, "(%.3f bpp).\n", bpp); + stats.Print(1); + } + return EXIT_SUCCESS; +} + +} // namespace +} // namespace tools +} // namespace jpegxl + +int main(int argc, const char** argv) { + return jpegxl::tools::CJpegliMain(argc, argv); +} diff --git a/tools/cjxl_fuzzer.cc b/tools/cjxl_fuzzer.cc index f3a1d9f..4577143 100644 --- a/tools/cjxl_fuzzer.cc +++ b/tools/cjxl_fuzzer.cc @@ -3,22 +3,21 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/encode.h> +#include <jxl/encode_cxx.h> +#include <jxl/thread_parallel_runner.h> +#include <jxl/thread_parallel_runner_cxx.h> #include <limits.h> #include <stdint.h> -#include <stdio.h> #include <stdlib.h> #include <string.h> #include <algorithm> #include <functional> +#include <hwy/targets.h> #include <random> #include <vector> -#include "hwy/targets.h" -#include "jxl/encode.h" -#include "jxl/encode_cxx.h" -#include "jxl/thread_parallel_runner.h" -#include "jxl/thread_parallel_runner_cxx.h" #include "lib/jxl/base/status.h" #include "lib/jxl/test_image.h" @@ -120,7 +119,7 @@ bool EncodeJpegXl(const FuzzSpec& spec) { // Reading compressed output JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { - std::vector<uint8_t> buf(spec.output_buffer_size); + std::vector<uint8_t> buf(spec.output_buffer_size + 32); uint8_t* next_out = buf.data(); size_t avail_out = buf.size(); process_result = JxlEncoderProcessOutput(enc, &next_out, &avail_out); diff --git a/tools/cjxl_main.cc b/tools/cjxl_main.cc index e43bb27..de1e118 100644 --- a/tools/cjxl_main.cc +++ b/tools/cjxl_main.cc @@ -11,36 +11,41 @@ // also require a change to the range-check here. The advantage is // that this minimizes the size of libjxl. -#include <stdint.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 <algorithm> +#include <cerrno> #include <cmath> +#include <cstdint> +#include <cstdio> #include <cstdlib> +#include <cstring> #include <functional> #include <iostream> +#include <memory> #include <sstream> #include <string> #include <thread> #include <type_traits> #include <vector> -#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/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/decode.h" #include "lib/extras/dec/pnm.h" +#include "lib/extras/enc/jxl.h" #include "lib/extras/time.h" +#include "lib/jxl/base/common.h" #include "lib/jxl/base/override.h" #include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/span.h" #include "lib/jxl/base/status.h" #include "lib/jxl/exif.h" -#include "lib/jxl/size_constraints.h" #include "tools/args.h" #include "tools/cmdline.h" #include "tools/codec_config.h" @@ -52,12 +57,11 @@ namespace tools { namespace { inline bool ParsePhotonNoiseParameter(const char* arg, float* out) { - return strncmp(arg, "ISO", 3) == 0 && ParseFloat(arg + 3, out) && *out > 0; + return ParseFloat(arg, out) && *out >= 0; } inline bool ParseIntensityTarget(const char* arg, float* out) { return ParseFloat(arg, out) && *out > 0; } - } // namespace enum CjxlRetCode : int { @@ -75,141 +79,116 @@ enum CjxlRetCode : int { struct CompressArgs { // CompressArgs() = default; void AddCommandLineOptions(CommandLineParser* cmdline) { + std::string input_help("the input can be "); + if (jxl::extras::CanDecode(jxl::extras::Codec::kPNG)) { + input_help.append("PNG, APNG, "); + } + if (jxl::extras::CanDecode(jxl::extras::Codec::kGIF)) { + input_help.append("GIF, "); + } + if (jxl::extras::CanDecode(jxl::extras::Codec::kJPG)) { + input_help.append("JPEG, "); + } else { + input_help.append("JPEG (lossless recompression only), "); + } + if (jxl::extras::CanDecode(jxl::extras::Codec::kEXR)) { + input_help.append("EXR, "); + } + input_help.append("PPM, PFM, PAM, PGX, or JXL"); // 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", + cmdline->AddPositionalOption("INPUT", /* required = */ true, input_help, &file_in); - cmdline->AddPositionalOption( - "OUTPUT", /* required = */ true, - "the compressed JXL output file (can be omitted for benchmarking)", - &file_out); + cmdline->AddPositionalOption("OUTPUT", /* required = */ true, + "the compressed JXL output file", &file_out); // Flags. - // TODO(lode): also add options to add exif/xmp/other metadata in the - // container. - cmdline->AddOptionValue('\0', "container", "0|1", - "0 = Do not encode using container format (strip " - "Exif/XMP/JPEG bitstream reconstruction data)." - "1 = Force using container format \n" - "(default: use only if needed).\n", - &container, &ParseOverride, 1); - cmdline->AddOptionValue( - '\0', "jpeg_store_metadata", "0|1", - ("If --lossless_jpeg=1, store JPEG reconstruction " - "metadata in the JPEG XL container " - "(for lossless reconstruction of the JPEG codestream)." - "(default: 1)"), - &jpeg_store_metadata, &ParseUnsigned, 2); + cmdline->AddHelpText("\nBasic options:", 0); // Target distance/size/bpp opt_distance_id = cmdline->AddOptionValue( - 'd', "distance", "maxError", - "Max. butteraugli distance, lower = higher quality.\n" + 'd', "distance", "DISTANCE", + "Target visual distance in JND units, 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.", + " Recommended range: 0.5 .. 3.0. Allowed range: 0.0 ... 25.0. " + "Mutually exclusive with --quality.", &distance, &ParseFloat); // 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" - " Other input gets encoded as per --distance default.\n" - " Positive quality values roughly match libjpeg quality.\n" - " Mutually exclusive with --distance.", + "Quality setting, higher value = higher quality. This is internally " + "mapped to --distance.\n" + " 100 = mathematically lossless. 90 = visually lossless.\n" + " Quality values roughly match libjpeg quality.\n" + " Recommended range: 68 .. 96. Allowed range: 0 .. 100. Mutually " + "exclusive with --distance.", &quality, &ParseFloat); cmdline->AddOptionValue( 'e', "effort", "EFFORT", "Encoder effort setting. Range: 1 .. 9.\n" - " Default: 7. Higher number is more effort (slower).", - &effort, &ParseUnsigned, -1); + " Default: 7. Higher numbers allow more computation " + "at the expense of time.\n" + " For lossless, generally it will produce smaller files.\n" + " For lossy, higher effort should more accurately reach " + "the target quality.", + &effort, &ParseUnsigned); - cmdline->AddOptionValue( - '\0', "brotli_effort", "B_EFFORT", - "Brotli effort setting. Range: 0 .. 11.\n" - " Default: 9. Higher number is more effort (slower).", - &brotli_effort, &ParseUnsigned, -1); - - cmdline->AddOptionValue( - '\0', "faster_decoding", "0|1|2|3|4", - "Favour higher decoding speed. 0 = default, higher " - "values give higher speed at the expense of quality", - &faster_decoding, &ParseUnsigned, 2); + cmdline->AddOptionFlag('V', "version", + "Print encoder library version number and exit.", + &version, &SetBooleanTrue); + cmdline->AddOptionFlag('\0', "quiet", "Be more silent", &quiet, + &SetBooleanTrue); + cmdline->AddOptionFlag('v', "verbose", + "Verbose output; can be repeated and also applies " + "to help (!).", + &verbose, &SetBooleanTrue); + + cmdline->AddHelpText("\nAdvanced options:", 1); + + opt_alpha_distance_id = cmdline->AddOptionValue( + 'a', "alpha_distance", "A_DISTANCE", + "Target visual distance for the alpha channel, lower = higher " + "quality.\n" + " 0.0 = mathematically lossless. 1.0 = visually lossless.\n" + " Default is to use the same value as for the color image.\n" + " Recommended range: 0.5 .. 3.0. Allowed range: 0.0 ... 25.0.", + &alpha_distance, &ParseFloat, 1); cmdline->AddOptionFlag('p', "progressive", - "Enable progressive/responsive decoding.", - &progressive, &SetBooleanTrue); - - cmdline->AddOptionValue('\0', "premultiply", "-1|0|1", - "Force premultiplied (associated) alpha.", - &premultiply, &ParseSigned, 1); - - cmdline->AddOptionValue( - '\0', "keep_invisible", "0|1", - "force disable/enable preserving color of invisible " - "pixels (default: 1 if lossless, 0 if lossy).", - &keep_invisible, &ParseOverride, 1); + "Enable (more) progressive/responsive decoding.", + &progressive, &SetBooleanTrue, 1); cmdline->AddOptionValue( '\0', "group_order", "0|1", "Order in which 256x256 groups are stored " - "in the codestream for progressive rendering. " - "Value not provided means 'encoder default', 0 means 'scanline order', " - "1 means 'center-first order'.", + "in the codestream for progressive rendering.\n" + " 0 = scanline order, 1 = center-first order. Default: 0.", &group_order, &ParseOverride, 1); cmdline->AddOptionValue( - '\0', "center_x", "0..XSIZE", - "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.", - ¢er_x, &ParseInt64, 1); + '\0', "container", "0|1", + "0 = Avoid the container format unless it is needed (default)\n" + " 1 = Force using the container format even if it is not needed.", + &container, &ParseOverride, 1); - cmdline->AddOptionValue( - '\0', "center_y", "0..YSIZE", - "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.", - ¢er_y, &ParseInt64, 1); - - // Flags. - cmdline->AddOptionFlag('\0', "progressive_ac", - "Use the progressive mode for AC.", &progressive_ac, - &SetBooleanTrue, 1); - - opt_qprogressive_ac_id = cmdline->AddOptionFlag( - '\0', "qprogressive_ac", - "Use the progressive mode for AC with shift quantization.", - &qprogressive_ac, &SetBooleanTrue, 1); + cmdline->AddOptionValue('\0', "compress_boxes", "0|1", + "Disable/enable Brotli compression for metadata " + "boxes. Default is 1 (enabled).", + &compress_boxes, &ParseOverride, 1); cmdline->AddOptionValue( - '\0', "progressive_dc", "num_dc_frames", - "Progressive-DC setting. Valid values are: -1, 0, 1, 2.", - &progressive_dc, &ParseSigned, 1); + '\0', "brotli_effort", "B_EFFORT", + "Brotli effort setting. Range: 0 .. 11.\n" + " Default: 9. Higher number is more effort (slower).", + &brotli_effort, &ParseUnsigned, 1); cmdline->AddOptionValue( 'm', "modular", "0|1", - "Use modular mode (not provided = encoder chooses, 0 = enforce VarDCT, " + "Use modular mode (default = encoder chooses, 0 = enforce VarDCT, " "1 = enforce modular mode).", &modular, &ParseOverride, 1); @@ -217,206 +196,288 @@ struct CompressArgs { opt_lossless_jpeg_id = cmdline->AddOptionValue( 'j', "lossless_jpeg", "0|1", "If the input is JPEG, losslessly transcode JPEG, " - "rather than using reencode pixels.", + "rather than using reencode pixels. Default is 1 (losslessly " + "transcode)", &lossless_jpeg, &ParseUnsigned, 1); cmdline->AddOptionValue( - '\0', "jpeg_reconstruction_cfl", "0|1", - "Enable/disable chroma-from-luma (CFL) for lossless " - "JPEG reconstruction.", - &jpeg_reconstruction_cfl, &ParseOverride, 2); - - cmdline->AddOptionValue( '\0', "num_threads", "N", "Number of worker threads (-1 == use machine default, " "0 == do not use multithreading).", &num_threads, &ParseSigned, 1); - cmdline->AddOptionValue('\0', "num_reps", "N", - "How many times to compress. (For benchmarking).", - &num_reps, &ParseUnsigned, 1); - cmdline->AddOptionValue( - '\0', "photon_noise", "ISO3200", - "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.", + '\0', "photon_noise_iso", "ISO_FILM_SPEED", + "Adds noise to the image emulating photographic film or sensor noise.\n" + " Higher number = grainier image, e.g. 100 gives a low amount of " + "noise,\n" + " 3200 gives a lot of noise. Default is 0.", &photon_noise_iso, &ParsePhotonNoiseParameter, 1); cmdline->AddOptionValue( - '\0', "dots", "0|1", - "Force disable/enable dots generation. " - "(not provided = default, 0 = disable, 1 = enable).", - &dots, &ParseOverride, 1); + '\0', "intensity_target", "N", + "Upper bound on the intensity level present in the image, in nits.\n" + " Default is 0, which means 'choose a sensible default " + "value based on the color encoding.", + &intensity_target, &ParseIntensityTarget, 1); cmdline->AddOptionValue( - '\0', "patches", "0|1", - "Force disable/enable patches generation. " - "(not provided = default, 0 = disable, 1 = enable).", - &patches, &ParseOverride, 1); + 'x', "dec-hints", "key=value", + "This is useful for 'raw' formats like PPM that cannot store " + "colorspace information\n" + " and metadata, or to strip or modify metadata in formats that do.\n" + " The key 'color_space' indicates an enumerated ColorEncoding, for " + "example:\n" + " -x color_space=RGB_D65_SRG_Per_SRG is sRGB with perceptual " + "rendering intent\n" + " -x color_space=RGB_D65_202_Rel_PeQ is Rec.2100 PQ with relative " + "rendering intent\n" + " The key 'icc_pathname' refers to a binary file containing an ICC " + "profile.\n" + " The keys 'exif', 'xmp', and 'jumbf' refer to a binary file " + "containing metadata;\n" + " existing metadata of the same type will be overwritten.\n" + " Specific metadata can be stripped using e.g. -x strip=exif", + &color_hints_proxy, &ParseAndAppendKeyValue<ColorHintsProxy>, 1); + + cmdline->AddHelpText("\nExpert options:", 2); + + cmdline->AddOptionValue( + '\0', "jpeg_store_metadata", "0|1", + ("If --lossless_jpeg=1, store JPEG reconstruction " + "metadata in the JPEG XL container.\n" + " This allows reconstruction of the JPEG codestream. Default: 1."), + &jpeg_store_metadata, &ParseUnsigned, 2); + + cmdline->AddOptionValue('\0', "codestream_level", "K", + "The codestream level. Either `-1`, `5` or `10`.", + &codestream_level, &ParseInt64, 2); + + cmdline->AddOptionValue('\0', "faster_decoding", "0|1|2|3|4", + "0 = default, higher values improve decode speed " + "at the expense of quality or density.", + &faster_decoding, &ParseUnsigned, 2); + + cmdline->AddOptionValue('\0', "premultiply", "-1|0|1", + "Force premultiplied (associated) alpha.", + &premultiply, &ParseSigned, 2); + + cmdline->AddOptionValue('\0', "keep_invisible", "0|1", + "disable/enable preserving color of invisible " + "pixels (default: 1 if lossless, 0 if lossy).", + &keep_invisible, &ParseOverride, 2); cmdline->AddOptionValue( - '\0', "resampling", "-1|1|2|4|8", - "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.", - &resampling, &ParseSigned, 0); + '\0', "center_x", "-1..XSIZE", + "Determines the horizontal position of center for the center-first " + "group order.\n" + " Default -1 means 'middle of the image', " + "values [0..xsize) set this to a particular coordinate.", + ¢er_x, &ParseInt64, 2); + + cmdline->AddOptionValue( + '\0', "center_y", "-1..YSIZE", + "Determines the vertical position of center for the center-first " + "group order.\n" + " Default -1 means 'middle of the image', " + "values [0..ysize) set this to a particular coordinate.", + ¢er_y, &ParseInt64, 2); + + // Flags. + cmdline->AddOptionFlag('\0', "progressive_ac", + "Use the progressive mode for AC.", &progressive_ac, + &SetBooleanTrue, 2); + + cmdline->AddOptionFlag( + '\0', "qprogressive_ac", + "Use the progressive mode for AC with shift quantization.", + &qprogressive_ac, &SetBooleanTrue, 2); cmdline->AddOptionValue( - '\0', "ec_resampling", "-1|1|2|4|8", - "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.", - &ec_resampling, &ParseSigned, 2); + '\0', "progressive_dc", "num_dc_frames", + "Progressive-DC setting. Valid values are: -1, 0, 1, 2.", + &progressive_dc, &ParseInt64, 2); + + cmdline->AddOptionValue('\0', "resampling", "-1|1|2|4|8", + "Resampling for color channels. Default of -1 " + "applies resampling only for very low quality.\n" + " 1 = downsampling (1x1), 2 = 2x2 downsampling, " + "4 = 4x4 downsampling, 8 = 8x8 downsampling.", + &resampling, &ParseInt64, 2); + + cmdline->AddOptionValue('\0', "ec_resampling", "-1|1|2|4|8", + "Resampling for extra channels. Same as " + "--resampling but for extra channels like alpha.", + &ec_resampling, &ParseInt64, 2); cmdline->AddOptionFlag('\0', "already_downsampled", - "Do not downsample the given input before encoding, " + "Do not downsample before encoding, " "but still signal that the decoder should upsample.", &already_downsampled, &SetBooleanTrue, 2); cmdline->AddOptionValue( + '\0', "upsampling_mode", "-1|0|1", + "Upsampling mode the decoder should use. Mostly useful in combination " + "with --already_downsampled. Value -1 means default (non-separable " + "upsampling), 0 means nearest neighbor (useful for pixel art)", + &upsampling_mode, &ParseInt64, 2); + + cmdline->AddOptionValue( '\0', "epf", "-1|0|1|2|3", - "Edge preserving filter level, -1 to 3. " - "Value -1 means: default (encoder chooses), 0 to 3 set a strength.", - &epf, &ParseSigned, 1); + "Edge preserving filter level, 0-3. " + "Default -1 means encoder chooses, 0-3 set a strength.", + &epf, &ParseInt64, 2); + + cmdline->AddOptionValue('\0', "gaborish", "0|1", + "Force disable/enable the gaborish filter. Default " + "is 'encoder chooses'", + &gaborish, &ParseOverride, 2); + + cmdline->AddOptionValue('\0', "override_bitdepth", "BITDEPTH", + "Default is zero (use the input image bit depth); " + "if nonzero, override the bit depth", + &override_bitdepth, &ParseUnsigned, 2); + + cmdline->AddHelpText("\nOptions for experimentation / benchmarking:", 3); + + cmdline->AddOptionValue('\0', "noise", "0|1", + "Force disable/enable adaptive noise generation " + "(experimental). Default " + "is 'encoder chooses'", + &noise, &ParseOverride, 3); cmdline->AddOptionValue( - '\0', "gaborish", "0|1", - "Force disable/enable the gaborish filter. " - "(not provided = default, 0 = disable, 1 = enable).", - &gaborish, &ParseOverride, 1); + '\0', "jpeg_reconstruction_cfl", "0|1", + "Enable/disable chroma-from-luma (CFL) for lossless " + "JPEG reconstruction.", + &jpeg_reconstruction_cfl, &ParseOverride, 3); + + cmdline->AddOptionValue('\0', "num_reps", "N", + "How many times to compress. (For benchmarking).", + &num_reps, &ParseUnsigned, 3); + + cmdline->AddOptionFlag('\0', "streaming_input", + "Enable streaming processing of the input file " + "(works only for PPM and PGM input files).", + &streaming_input, &SetBooleanTrue, 3); + cmdline->AddOptionFlag('\0', "streaming_output", + "Enable incremental writing of the output file.", + &streaming_output, &SetBooleanTrue, 3); + cmdline->AddOptionFlag('\0', "disable_output", + "No output file will be written (for benchmarking)", + &disable_output, &SetBooleanTrue, 3); cmdline->AddOptionValue( - '\0', "intensity_target", "N", - "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.", - &intensity_target, &ParseIntensityTarget, 1); + '\0', "dots", "0|1", + "Force disable/enable dots generation. " + "(not provided = default, 0 = disable, 1 = enable).", + &dots, &ParseOverride, 3); 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); + '\0', "patches", "0|1", + "Force disable/enable patches generation. " + "(not provided = default, 0 = disable, 1 = enable).", + &patches, &ParseOverride, 3); 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); + '\0', "frame_indexing", "INDICES", + // TODO(tfish): Add a more convenient vanilla alternative. + "INDICES is of the form '^(0*|1[01]*)'. The i-th position indicates " + "whether the\n" + " i-th frame will be indexed in the frame index box.", + &frame_indexing, &ParseString, 3); + + cmdline->AddOptionFlag('\0', "allow_expert_options", + "Allow specifying advanced options; this allows " + "setting effort to 10, for\n" + " somewhat better lossless compression at the " + "cost of a massive speed hit.", + &allow_expert_options, &SetBooleanTrue, 3); + + cmdline->AddHelpText("\nModular mode options:", 4); // modular mode options cmdline->AddOptionValue( - 'I', "iterations", "F", - "[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." - "Higher values use more encoder memory.", - &modular_ma_tree_learning_percent, &ParseFloat, 2); + 'I', "iterations", "PERCENT", + "Percentage of pixels used to learn MA trees. Higher values use\n" + " more encoder memory and can result in better compression. Default " + "of -1 means\n" + " the encoder chooses. Zero means no MA trees are used.", + &modular_ma_tree_learning_percent, &ParseFloat, 4); cmdline->AddOptionValue( 'C', "modular_colorspace", "K", - ("[modular encoding] color transform: -1=default, 0=RGB (none), " - "1-41=RCT (6=YCoCg, default: try several, depending on speed)"), - &modular_colorspace, &ParseSigned, 1); + ("Color transform: -1 = default (try several per group, depending\n" + " on effort), 0 = RGB (none), 1-41 = fixed RCT (6 = YCoCg)."), + &modular_colorspace, &ParseInt64, 4); opt_modular_group_size_id = cmdline->AddOptionValue( 'g', "modular_group_size", "K", - "[modular encoding] group size: -1 == default. 0 => 128, " - "1 => 256, 2 => 512, 3 => 1024", - &modular_group_size, &ParseSigned, 1); + "Group size: -1 = default (let the encoder choose),\n" + " 0 = 128x128, 1 = 256x256, 2 = 512x512, 3 = 1024x1024.", + &modular_group_size, &ParseInt64, 4); cmdline->AddOptionValue( 'P', "modular_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. If unset, uses default 14, " - "at slowest speed default 15.", - &modular_predictor, &ParseSigned, 1); + "Predictor(s) to use: 0=zero, 1=left, 2=top, 3=avg0, 4=select,\n" + " 5=gradient, 6=weighted, 7=topright, 8=topleft, 9=leftleft, " + "10=avg1, 11=avg2, 12=avg3,\n" + " 13=toptop predictive average, 14=mix 5 and 6, 15=mix everything.\n" + " Default is 14 at effort < 9 and 15 at effort 9.", + &modular_predictor, &ParseInt64, 4); cmdline->AddOptionValue( 'E', "modular_nb_prev_channels", "K", - "[modular encoding] number of extra MA tree properties to use", - &modular_nb_prev_channels, &ParseSigned, 2); + "Number of extra (previous-channel) MA tree properties to use.", + &modular_nb_prev_channels, &ParseInt64, 4); cmdline->AddOptionValue( '\0', "modular_palette_colors", "K", - "[modular encoding] Use color palette if number of colors is smaller " - "than or equal to this, or -1 to use the encoder default.", - &modular_palette_colors, &ParseSigned, 1); + "Use palette if number of colors is smaller than or equal to this.", + &modular_palette_colors, &ParseInt64, 4); cmdline->AddOptionFlag( '\0', "modular_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", - &modular_lossy_palette, &SetBooleanTrue, 1); - - cmdline->AddOptionValue( - 'X', "pre-compact", "PERCENT", - "[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.", - &modular_channel_colors_global_percent, &ParseFloat, 2); + "Use delta palette in a lossy way; it is recommended to also\n" + " set --modular_palette_colors=0 with this " + "option to use the default palette only.", + &modular_lossy_palette, &SetBooleanTrue, 4); + + cmdline->AddOptionValue('X', "pre-compact", "PERCENT", + "Use global channel palette if the number of " + "sample values is smaller\n" + " than this percentage of the nominal range. ", + &modular_channel_colors_global_percent, &ParseFloat, + 4); cmdline->AddOptionValue( 'Y', "post-compact", "PERCENT", - "[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.", - &modular_channel_colors_group_percent, &ParseFloat, 2); - - cmdline->AddOptionValue('\0', "codestream_level", "K", - "The codestream level. Either `-1`, `5` or `10`.", - &codestream_level, &ParseSigned, 2); - - opt_responsive_id = cmdline->AddOptionValue( - 'R', "responsive", "K", - "[modular encoding] do Squeeze transform, 0=false, " - "1=true (default: true if lossy, false if lossless)", - &responsive, &ParseSigned, 1); - - cmdline->AddOptionFlag('V', "version", - "Print encoder library version number and exit.", - &version, &SetBooleanTrue, 1); - - cmdline->AddOptionFlag('\0', "quiet", "Be more silent", &quiet, - &SetBooleanTrue, 1); - - cmdline->AddOptionValue( - '\0', "frame_indexing", "string", - // TODO(tfish): Add a more convenient vanilla alternative. - "If non-empty, a string matching '^(0*|1[01]*)'. If this string has a " - "'1' in i-th position, then the i-th frame will be indexed in " - "the frame index box.", - &frame_indexing, &ParseString, 1); - - cmdline->AddOptionFlag( - 'v', "verbose", - "Verbose output; can be repeated, also applies to help (!).", &verbose, - &SetBooleanTrue); + "Use local (per-group) channel palette if the " + "number of sample values is\n" + " smaller than this percentage of the nominal range.", + &modular_channel_colors_group_percent, &ParseFloat, 4); + + opt_responsive_id = + cmdline->AddOptionValue('R', "responsive", "K", + "Do the Squeeze transform, 0=false, " + "1=true (default: 1 if lossy, 0 if lossless)", + &responsive, &ParseInt64, 4); } // Common flags. bool version = false; jxl::Override container = jxl::Override::kDefault; bool quiet = false; + bool disable_output = false; const char* file_in = nullptr; const char* file_out = nullptr; jxl::Override print_profile = jxl::Override::kDefault; + bool streaming_input = false; + bool streaming_output = false; // Decoding source image flags - jxl::extras::ColorHints color_hints; + ColorHintsProxy color_hints_proxy; // JXL flags size_t override_bitdepth = 0; @@ -424,9 +485,6 @@ struct CompressArgs { 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. @@ -440,10 +498,11 @@ struct CompressArgs { bool progressive = false; bool progressive_ac = false; bool qprogressive_ac = false; - int32_t progressive_dc = -1; + int64_t progressive_dc = -1; bool modular_lossy_palette = false; int32_t premultiply = -1; bool already_downsampled = false; + int64_t upsampling_mode = -1; jxl::Override jpeg_reconstruction_cfl = jxl::Override::kDefault; jxl::Override modular = jxl::Override::kDefault; jxl::Override keep_invisible = jxl::Override::kDefault; @@ -451,38 +510,40 @@ struct CompressArgs { jxl::Override patches = jxl::Override::kDefault; jxl::Override gaborish = jxl::Override::kDefault; jxl::Override group_order = jxl::Override::kDefault; + jxl::Override compress_boxes = jxl::Override::kDefault; + jxl::Override noise = jxl::Override::kDefault; size_t faster_decoding = 0; - int32_t resampling = -1; - int32_t ec_resampling = -1; - int32_t epf = -1; + int64_t resampling = -1; + int64_t ec_resampling = -1; + int64_t epf = -1; int64_t center_x = -1; int64_t center_y = -1; - int32_t modular_group_size = -1; - int32_t modular_predictor = -1; - int32_t modular_colorspace = -1; + int64_t modular_group_size = -1; + int64_t modular_predictor = -1; + int64_t modular_colorspace = -1; float modular_channel_colors_global_percent = -1.f; float modular_channel_colors_group_percent = -1.f; - int32_t modular_palette_colors = -1; - int32_t modular_nb_prev_channels = -1; + int64_t modular_palette_colors = -1; + int64_t modular_nb_prev_channels = -1; float modular_ma_tree_learning_percent = -1.f; float photon_noise_iso = 0; - int32_t codestream_level = -1; - int32_t responsive = -1; + int64_t codestream_level = -1; + int64_t responsive = -1; float distance = 1.0; + float alpha_distance = 1.0; size_t effort = 7; size_t brotli_effort = 9; std::string frame_indexing; - // Will get passed on to AuxOut. - // jxl::InspectorImage3F inspector_image3f; + bool allow_expert_options = false; // References (ids) of specific options to check if they were matched. CommandLineParser::OptionId opt_lossless_jpeg_id = -1; CommandLineParser::OptionId opt_responsive_id = -1; CommandLineParser::OptionId opt_distance_id = -1; + CommandLineParser::OptionId opt_alpha_distance_id = -1; CommandLineParser::OptionId opt_quality_id = -1; - CommandLineParser::OptionId opt_qprogressive_ac_id = -1; CommandLineParser::OptionId opt_modular_group_size_id = -1; }; @@ -506,144 +567,430 @@ std::string DistanceFromArgs(const CompressArgs& args) { } void PrintMode(jxl::extras::PackedPixelFile& ppf, const double decode_mps, - size_t num_bytes, const CompressArgs& args) { + size_t num_bytes, const CompressArgs& args, + jpegxl::tools::CommandLineParser& cmdline) { const char* mode = ModeFromArgs(args); const std::string distance = DistanceFromArgs(args); if (args.lossless_jpeg) { - fprintf(stderr, "Read JPEG image with %" PRIuS " bytes.\n", num_bytes); + cmdline.VerbosePrintf(1, "Read JPEG image with %" PRIuS " bytes.\n", + num_bytes); } else { - fprintf(stderr, - "Read %" PRIuS "x%" PRIuS " image, %" PRIuS " bytes, %.1f MP/s\n", - static_cast<size_t>(ppf.info.xsize), - static_cast<size_t>(ppf.info.ysize), num_bytes, decode_mps); + cmdline.VerbosePrintf( + 1, "Read %" PRIuS "x%" PRIuS " image, %" PRIuS " bytes, %.1f MP/s\n", + static_cast<size_t>(ppf.info.xsize), + static_cast<size_t>(ppf.info.ysize), num_bytes, decode_mps); } - fprintf(stderr, "Encoding [%s%s, %s, effort: %" PRIuS, - (args.container == jxl::Override::kOn ? "Container | " : ""), mode, - distance.c_str(), args.effort); + cmdline.VerbosePrintf( + 0, "Encoding [%s%s, %s, effort: %" PRIuS, + (args.container == jxl::Override::kOn ? "Container | " : ""), mode, + distance.c_str(), args.effort); if (args.container == jxl::Override::kOn) { if (args.lossless_jpeg && args.jpeg_store_metadata) - fprintf(stderr, " | JPEG reconstruction data"); + cmdline.VerbosePrintf(0, " | JPEG reconstruction data"); if (!ppf.metadata.exif.empty()) - fprintf(stderr, " | %" PRIuS "-byte Exif", ppf.metadata.exif.size()); + cmdline.VerbosePrintf(0, " | %" PRIuS "-byte Exif", + ppf.metadata.exif.size()); if (!ppf.metadata.xmp.empty()) - fprintf(stderr, " | %" PRIuS "-byte XMP", ppf.metadata.xmp.size()); + cmdline.VerbosePrintf(0, " | %" PRIuS "-byte XMP", + ppf.metadata.xmp.size()); if (!ppf.metadata.jumbf.empty()) - fprintf(stderr, " | %" PRIuS "-byte JUMBF", ppf.metadata.jumbf.size()); + cmdline.VerbosePrintf(0, " | %" PRIuS "-byte JUMBF", + ppf.metadata.jumbf.size()); } - fprintf(stderr, "], \n"); + cmdline.VerbosePrintf(0, "]\n"); } -} // namespace tools -} // namespace jpegxl +bool IsJPG(const std::vector<uint8_t>& image_data) { + return (image_data.size() >= 2 && image_data[0] == 0xFF && + image_data[1] == 0xD8); +} -namespace { +using flag_check_fn = std::function<std::string(int64_t)>; +using flag_check_float_fn = std::function<std::string(float)>; template <typename T> -void SetFlagFrameOptionOrDie(const char* flag_name, T flag_value, - JxlEncoderFrameSettings* frame_settings, - JxlEncoderFrameSettingId encoder_option) { - if (JXL_ENC_SUCCESS != - (std::is_same<T, float>::value - ? JxlEncoderFrameSettingsSetFloatOption(frame_settings, - encoder_option, flag_value) - : JxlEncoderFrameSettingsSetOption(frame_settings, encoder_option, - flag_value))) { - std::cerr << "Setting encoder option from flag --" << flag_name - << " failed." << std::endl; +void ProcessFlag( + const char* flag_name, T flag_value, + JxlEncoderFrameSettingId encoder_option, + jxl::extras::JXLCompressParams* params, + flag_check_fn flag_check = [](T x) { return std::string(); }) { + std::string error = flag_check(flag_value); + if (!error.empty()) { + std::cerr << "Invalid flag value for --" << flag_name << ": " << error + << std::endl; exit(EXIT_FAILURE); } + params->options.emplace_back( + jxl::extras::JXLOption(encoder_option, flag_value, 0)); } -void SetDistanceFromFlags(JxlEncoderFrameSettings* jxl_encoder_frame_settings, - jpegxl::tools::CommandLineParser* cmdline, - jpegxl::tools::CompressArgs* args, +void ProcessBoolFlag(jxl::Override flag_value, + JxlEncoderFrameSettingId encoder_option, + jxl::extras::JXLCompressParams* params) { + if (flag_value != jxl::Override::kDefault) { + int64_t value = flag_value == jxl::Override::kOn ? 1 : 0; + params->options.emplace_back( + jxl::extras::JXLOption(encoder_option, value, 0)); + } +} + +void SetDistanceFromFlags(CommandLineParser* cmdline, CompressArgs* args, + jxl::extras::JXLCompressParams* params, const jxl::extras::Codec& codec) { bool distance_set = cmdline->GetOption(args->opt_distance_id)->matched(); + bool alpha_distance_set = + cmdline->GetOption(args->opt_alpha_distance_id)->matched(); bool quality_set = cmdline->GetOption(args->opt_quality_id)->matched(); + if ((distance_set && (args->distance != 0.0)) && args->lossless_jpeg) { + std::cerr << "Must not set non-zero distance in combination with " + "--lossless_jpeg=1, which is set by default." + << std::endl; + exit(EXIT_FAILURE); + } + if ((quality_set && (args->quality != 100)) && args->lossless_jpeg) { + std::cerr << "Must not set quality below 100 in combination with " + "--lossless_jpeg=1, which is set by default" + << std::endl; + exit(EXIT_FAILURE); + } if (quality_set) { if (distance_set) { std::cerr << "Must not set both --distance and --quality." << std::endl; exit(EXIT_FAILURE); } - double distance = args->quality >= 100 ? 0.0 - : args->quality >= 30 - ? 0.1 + (100 - args->quality) * 0.09 - : 6.4 + pow(2.5, (30 - args->quality) / 5.0) / 6.25; - args->distance = distance; + args->distance = JxlEncoderDistanceFromQuality(args->quality); distance_set = true; } + if (!distance_set) { bool lossy_input = (codec == jxl::extras::Codec::kJPG || codec == jxl::extras::Codec::kGIF); args->distance = lossy_input ? 0.0 : 1.0; + } else if (args->distance > 0) { + args->lossless_jpeg = 0; } - if (JXL_ENC_SUCCESS != - JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, args->distance)) { - std::cerr << "Setting frame distance failed." << std::endl; + params->distance = args->distance; + params->alpha_distance = + alpha_distance_set ? args->alpha_distance : params->distance; +} + +void ProcessFlags(const jxl::extras::Codec codec, + const jxl::extras::PackedPixelFile& ppf, + const std::vector<uint8_t>* jpeg_bytes, + CommandLineParser* cmdline, CompressArgs* args, + jxl::extras::JXLCompressParams* params) { + // Tuning flags. + ProcessBoolFlag(args->modular, JXL_ENC_FRAME_SETTING_MODULAR, params); + ProcessBoolFlag(args->keep_invisible, JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE, + params); + ProcessBoolFlag(args->dots, JXL_ENC_FRAME_SETTING_DOTS, params); + ProcessBoolFlag(args->patches, JXL_ENC_FRAME_SETTING_PATCHES, params); + ProcessBoolFlag(args->gaborish, JXL_ENC_FRAME_SETTING_GABORISH, params); + ProcessBoolFlag(args->group_order, JXL_ENC_FRAME_SETTING_GROUP_ORDER, params); + ProcessBoolFlag(args->noise, JXL_ENC_FRAME_SETTING_NOISE, params); + + params->allow_expert_options = args->allow_expert_options; + + if (!args->frame_indexing.empty()) { + bool must_be_all_zeros = args->frame_indexing[0] != '1'; + for (char c : args->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; + exit(EXIT_FAILURE); + } + } else if (c != '0') { + std::cerr << "Invalid --frame_indexing. Must match the pattern " + "'^(0*|1[01]*)$'." + << std::endl; + exit(EXIT_FAILURE); + } + } + } + + ProcessFlag( + "effort", static_cast<int64_t>(args->effort), + JXL_ENC_FRAME_SETTING_EFFORT, params, [args](int64_t x) -> std::string { + if (args->allow_expert_options) { + return (1 <= x && x <= 10) ? "" : "Valid range is {1, 2, ..., 10}."; + } else { + return (1 <= x && x <= 9) ? "" : "Valid range is {1, 2, ..., 9}."; + } + }); + ProcessFlag("brotli_effort", static_cast<int64_t>(args->brotli_effort), + JXL_ENC_FRAME_SETTING_BROTLI_EFFORT, params, + [](int64_t x) -> std::string { + return (-1 <= x && x <= 11) + ? "" + : "Valid range is {-1, 0, 1, ..., 11}."; + }); + ProcessFlag( + "epf", args->epf, JXL_ENC_FRAME_SETTING_EPF, params, + [](int64_t x) -> std::string { + return (-1 <= x && x <= 3) ? "" : "Valid range is {-1, 0, 1, 2, 3}.\n"; + }); + ProcessFlag("faster_decoding", static_cast<int64_t>(args->faster_decoding), + JXL_ENC_FRAME_SETTING_DECODING_SPEED, params, + [](int64_t x) -> std::string { + return (0 <= x && x <= 4) ? "" + : "Valid range is {0, 1, 2, 3, 4}.\n"; + }); + ProcessFlag("resampling", args->resampling, JXL_ENC_FRAME_SETTING_RESAMPLING, + params, [](int64_t x) -> std::string { + return (x == -1 || x == 1 || x == 2 || x == 4 || x == 8) + ? "" + : "Valid values are {-1, 1, 2, 4, 8}.\n"; + }); + ProcessFlag("ec_resampling", args->ec_resampling, + JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, params, + [](int64_t x) -> std::string { + return (x == -1 || x == 1 || x == 2 || x == 4 || x == 8) + ? "" + : "Valid values are {-1, 1, 2, 4, 8}.\n"; + }); + ProcessFlag("photon_noise_iso", args->photon_noise_iso, + JXL_ENC_FRAME_SETTING_PHOTON_NOISE, params); + ProcessFlag("already_downsampled", + static_cast<int64_t>(args->already_downsampled), + JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED, params); + if (args->already_downsampled) params->already_downsampled = args->resampling; + + SetDistanceFromFlags(cmdline, args, params, codec); + + if (args->group_order != jxl::Override::kOn && + (args->center_x != -1 || args->center_y != -1)) { + std::cerr << "Invalid flag combination. Setting --center_x or --center_y " + << "requires setting --group_order=1" << std::endl; exit(EXIT_FAILURE); } -} + ProcessFlag("center_x", args->center_x, + JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X, params, + [](int64_t x) -> std::string { + if (x < -1) { + return "Valid values are: -1 or [0 .. xsize)."; + } + return ""; + }); + ProcessFlag("center_y", args->center_y, + JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y, params, + [](int64_t x) -> std::string { + if (x < -1) { + return "Valid values are: -1 or [0 .. ysize)."; + } + return ""; + }); + + // Progressive/responsive mode settings. + bool responsive_set = cmdline->GetOption(args->opt_responsive_id)->matched(); + + ProcessFlag("progressive_dc", args->progressive_dc, + JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, params, + [](int64_t x) -> std::string { + return (-1 <= x && x <= 2) ? "" + : "Valid range is {-1, 0, 1, 2}.\n"; + }); + ProcessFlag("progressive_ac", static_cast<int64_t>(args->progressive_ac), + JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, params); + + if (args->progressive) { + args->qprogressive_ac = true; + args->responsive = 1; + responsive_set = true; + } + if (responsive_set) { + ProcessFlag("responsive", args->responsive, + JXL_ENC_FRAME_SETTING_RESPONSIVE, params); + } + if (args->qprogressive_ac) { + ProcessFlag("qprogressive_ac", static_cast<int64_t>(1), + JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC, params); + } -using flag_check_fn = std::function<std::string(int64_t)>; -using flag_check_float_fn = std::function<std::string(float)>; + // Modular mode related. + ProcessFlag("modular_group_size", args->modular_group_size, + JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, params, + [](int64_t x) -> std::string { + return (-1 <= x && x <= 3) + ? "" + : "Invalid --modular_group_size. Valid " + "range is {-1, 0, 1, 2, 3}.\n"; + }); + ProcessFlag("modular_predictor", args->modular_predictor, + JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, params, + [](int64_t x) -> std::string { + return (-1 <= x && x <= 15) + ? "" + : "Invalid --modular_predictor. Valid " + "range is {-1, 0, 1, ..., 15}.\n"; + }); + ProcessFlag("modular_colorspace", args->modular_colorspace, + JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, params, + [](int64_t x) -> std::string { + return (-1 <= x && x <= 41) + ? "" + : "Invalid --modular_colorspace. Valid range is " + "{-1, 0, 1, ..., 41}.\n"; + }); + ProcessFlag("modular_ma_tree_learning_percent", + args->modular_ma_tree_learning_percent, + JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, params, + [](float x) -> std::string { + return -1 <= x && x <= 100 + ? "" + : "Invalid --modular_ma_tree_learning_percent, Valid" + "rang is [-1, 100].\n"; + }); + ProcessFlag("modular_nb_prev_channels", args->modular_nb_prev_channels, + JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, params, + [](int64_t x) -> std::string { + return (-1 <= x && x <= 11) + ? "" + : "Invalid --modular_nb_prev_channels. Valid " + "range is {-1, 0, 1, ..., 11}.\n"; + }); + if (args->modular_lossy_palette) { + if (args->progressive || args->qprogressive_ac) { + fprintf(stderr, + "WARNING: --modular_lossy_palette is ignored in " + "progressive mode.\n"); + args->modular_lossy_palette = false; + } + } + ProcessFlag("modular_lossy_palette", + static_cast<int64_t>(args->modular_lossy_palette), + JXL_ENC_FRAME_SETTING_LOSSY_PALETTE, params); + ProcessFlag("modular_palette_colors", args->modular_palette_colors, + JXL_ENC_FRAME_SETTING_PALETTE_COLORS, params, + [](int64_t x) -> std::string { + return -1 <= x ? "" + : "Invalid --modular_palette_colors, must " + "be -1 or non-negative\n"; + }); + ProcessFlag("modular_channel_colors_global_percent", + args->modular_channel_colors_global_percent, + JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, params, + [](float x) -> std::string { + return (-1 <= x && x <= 100) + ? "" + : "Invalid --modular_channel_colors_global_percent. " + "Valid " + "range is [-1, 100].\n"; + }); + ProcessFlag("modular_channel_colors_group_percent", + args->modular_channel_colors_group_percent, + JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, params, + [](float x) -> std::string { + return (-1 <= x && x <= 100) + ? "" + : "Invalid --modular_channel_colors_group_percent. " + "Valid " + "range is [-1, 100].\n"; + }); + + if (args->num_threads < -1) { + std::cerr + << "Invalid flag value for --num_threads: must be -1, 0 or positive." + << std::endl; + exit(EXIT_FAILURE); + } + // JPEG specific options. + if (jpeg_bytes) { + ProcessBoolFlag(args->jpeg_reconstruction_cfl, + JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL, params); + ProcessBoolFlag(args->compress_boxes, + JXL_ENC_FRAME_SETTING_JPEG_COMPRESS_BOXES, params); + } + // Set per-frame options. + for (size_t num_frame = 0; num_frame < ppf.num_frames(); ++num_frame) { + if (num_frame < args->frame_indexing.size() && + args->frame_indexing[num_frame] == '1') { + int64_t value = 1; + params->options.emplace_back( + jxl::extras::JXLOption(JXL_ENC_FRAME_INDEX_BOX, value, num_frame)); + } + } + // Copy over the rest of the non-option params. + params->use_container = args->container == jxl::Override::kOn; + params->jpeg_store_metadata = args->jpeg_store_metadata; + params->intensity_target = args->intensity_target; + params->override_bitdepth = args->override_bitdepth; + params->codestream_level = args->codestream_level; + params->premultiply = args->premultiply; + params->compress_boxes = args->compress_boxes != jxl::Override::kOff; + params->upsampling_mode = args->upsampling_mode; + if (codec == jxl::extras::Codec::kPNM && + ppf.info.exponent_bits_per_sample == 0) { + params->input_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM; + } -bool IsJPG(const std::vector<uint8_t>& image_data) { - return (image_data.size() >= 2 && image_data[0] == 0xFF && - image_data[1] == 0xD8); + // If a metadata field is set to an empty value, it is stripped. + // Make sure we also strip it when the input image is read with AddJPEGFrame + (void)args->color_hints_proxy.target.Foreach( + [¶ms](const std::string& key, + const std::string& value) -> jxl::Status { + if (value == "") { + if (key == "exif") params->jpeg_strip_exif = true; + if (key == "xmp") params->jpeg_strip_xmp = true; + if (key == "jumbf") params->jpeg_strip_jumbf = true; + } + return true; + }); } -// TODO(tfish): Replace with non-C-API library function. -// Implementation is in extras/. -jxl::Status GetPixeldata(const std::vector<uint8_t>& image_data, - const jxl::extras::ColorHints& color_hints, - 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::SizeConstraints size_constraints; - - const auto choose_codec = [&]() { -#if JPEGXL_ENABLE_APNG - if (jxl::extras::DecodeImageAPNG(encoded, color_hints, size_constraints, - &ppf)) { - return jxl::extras::Codec::kPNG; - } -#endif - if (jxl::extras::DecodeImagePGX(encoded, color_hints, size_constraints, - &ppf)) { - return jxl::extras::Codec::kPGX; - } else if (jxl::extras::DecodeImagePNM(encoded, color_hints, - size_constraints, &ppf)) { - return jxl::extras::Codec::kPNM; +struct JxlOutputProcessor { + bool SetOutputPath(const std::string& path) { + outfile.reset(new FileWrapper(path, "wb")); + if (!*outfile) { + fprintf(stderr, + "Could not open %s for writing\n" + "Error: %s", + path.c_str(), strerror(errno)); + return false; } -#if JPEGXL_ENABLE_GIF - if (jxl::extras::DecodeImageGIF(encoded, color_hints, size_constraints, - &ppf)) { - return jxl::extras::Codec::kGIF; + return true; + } + + JxlEncoderOutputProcessor GetOutputProcessor() { + return JxlEncoderOutputProcessor{this, GetBuffer, ReleaseBuffer, Seek, + SetFinalizedPosition}; + } + + static void* GetBuffer(void* opaque, size_t* size) { + JxlOutputProcessor* self = reinterpret_cast<JxlOutputProcessor*>(opaque); + self->output.resize(*size); + return self->output.data(); + } + + static void ReleaseBuffer(void* opaque, size_t written_bytes) { + JxlOutputProcessor* self = reinterpret_cast<JxlOutputProcessor*>(opaque); + if (*self->outfile && fwrite(self->output.data(), 1, written_bytes, + *self->outfile) != written_bytes) { + JXL_WARNING("Failed to write %" PRIuS " bytes to output", written_bytes); } -#endif -#if JPEGXL_ENABLE_JPEG - if (jxl::extras::DecodeImageJPG(encoded, color_hints, size_constraints, - &ppf)) { - return jxl::extras::Codec::kJPG; + self->output.clear(); + } + + static void Seek(void* opaque, uint64_t position) { + JxlOutputProcessor* self = reinterpret_cast<JxlOutputProcessor*>(opaque); + if (*self->outfile && fseek(*self->outfile, position, SEEK_SET) != 0) { + JXL_WARNING("Failed to seek output."); } -#endif - // TODO(tfish): Bring back EXR and PSD. - return jxl::extras::Codec::kUnknown; - }; - codec = choose_codec(); - if (codec == jxl::extras::Codec::kUnknown) { - return JXL_FAILURE("Codecs failed to decode input."); } - return true; -} -} // namespace + static void SetFinalizedPosition(void* opaque, uint64_t finalized_position) { + JxlOutputProcessor* self = reinterpret_cast<JxlOutputProcessor*>(opaque); + self->finalized_position = finalized_position; + } + + std::vector<uint8_t> output; + size_t finalized_position = 0; + std::unique_ptr<FileWrapper> outfile; +}; + +} // namespace tools +} // namespace jpegxl int main(int argc, char** argv) { std::string version = jpegxl::tools::CodecConfigString(JxlEncoderVersion()); @@ -672,9 +1019,15 @@ int main(int argc, char** argv) { return jpegxl::tools::CjxlRetCode::OK; } - if (!args.file_out && !args.quiet) { + if (!args.file_out && !args.disable_output) { + std::cerr + << "No output file specified and --disable_output flag not passed." + << std::endl; + exit(EXIT_FAILURE); + } + + if (args.file_out && args.disable_output && !args.quiet) { fprintf(stderr, - "No output file specified.\n" "Encoding will be performed, but the result will be discarded.\n"); } @@ -682,533 +1035,151 @@ int main(int argc, char** argv) { // 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. - std::vector<uint8_t> image_data; - jxl::extras::PackedPixelFile ppf; - jxl::extras::Codec codec = jxl::extras::Codec::kUnknown; - double decode_mps = 0; - size_t pixels = 0; - if (!jpegxl::tools::ReadFile(args.file_in, &image_data)) { + jpegxl::tools::FileWrapper f(args.file_in, "rb"); + if (!f) { std::cerr << "Reading image data failed." << std::endl; exit(EXIT_FAILURE); } - if (!IsJPG(image_data)) args.lossless_jpeg = 0; - if (!args.lossless_jpeg) { - const double t0 = jxl::Now(); - jxl::Status status = GetPixeldata(image_data, args.color_hints, ppf, codec); - if (!status) { - std::cerr << "Getting pixel data." << std::endl; + jxl::extras::JXLCompressParams params; + jxl::extras::PackedPixelFile ppf; + jxl::extras::Codec codec = jxl::extras::Codec::kUnknown; + std::vector<uint8_t> image_data; + std::vector<uint8_t>* jpeg_bytes = nullptr; + jxl::extras::ChunkedPNMDecoder pnm_dec; + size_t pixels = 0; + if (args.streaming_input) { + pnm_dec.f = f; + if (!DecodeImagePNM(&pnm_dec, args.color_hints_proxy.target, &ppf)) { + std::cerr << "PNM decoding failed." << std::endl; exit(EXIT_FAILURE); } - if (ppf.frames.empty()) { - std::cerr << "No frames on input file." << std::endl; + codec = jxl::extras::Codec::kPNM; + args.lossless_jpeg = 0; + pixels = ppf.info.xsize * ppf.info.ysize; + } else { + double decode_mps = 0; + if (!jpegxl::tools::ReadFile(f, &image_data)) { + std::cerr << "Reading image data failed." << std::endl; exit(EXIT_FAILURE); } + if (!jpegxl::tools::IsJPG(image_data)) args.lossless_jpeg = 0; + ProcessFlags(codec, ppf, jpeg_bytes, &cmdline, &args, ¶ms); + if (!args.lossless_jpeg) { + const double t0 = jxl::Now(); + jxl::Status status = jxl::extras::DecodeBytes( + jxl::Bytes(image_data), args.color_hints_proxy.target, &ppf, nullptr, + &codec); - const double t1 = jxl::Now(); - pixels = ppf.info.xsize * ppf.info.ysize; - decode_mps = pixels * ppf.info.num_color_channels * 1E-6 / (t1 - t0); - } - - JxlEncoderPtr enc = JxlEncoderMake(/*memory_manager=*/nullptr); - JxlEncoder* jxl_encoder = enc.get(); - JxlThreadParallelRunnerPtr runner; - std::vector<uint8_t> compressed; - size_t num_worker_threads; - jpegxl::tools::SpeedStats stats; - for (size_t num_rep = 0; num_rep < args.num_reps; ++num_rep) { - const double t0 = jxl::Now(); - JxlEncoderReset(jxl_encoder); - if (args.num_threads != 0) { - num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads(); - { - int64_t flag_num_worker_threads = args.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, int64_t flag_value, - JxlEncoderFrameSettingId encoder_option, - const flag_check_fn& flag_check) { - std::string error = flag_check(flag_value); - if (!error.empty()) { - std::cerr << "Invalid flag value for --" << flag_name << ": " << error - << std::endl; + if (!status) { + std::cerr << "Getting pixel data failed." << std::endl; exit(EXIT_FAILURE); } - SetFlagFrameOptionOrDie(flag_name, flag_value, jxl_encoder_frame_settings, - encoder_option); - }; - auto process_float_flag = [&jxl_encoder_frame_settings]( - const char* flag_name, float flag_value, - JxlEncoderFrameSettingId encoder_option, - const flag_check_float_fn& flag_check) { - std::string error = flag_check(flag_value); - if (!error.empty()) { - std::cerr << "Invalid flag value for --" << flag_name << ": " << error - << std::endl; + if (ppf.frames.empty()) { + std::cerr << "No frames on input file." << std::endl; exit(EXIT_FAILURE); } - SetFlagFrameOptionOrDie(flag_name, flag_value, jxl_encoder_frame_settings, - encoder_option); - }; - - auto process_bool_flag = [&jxl_encoder_frame_settings]( - const char* flag_name, - jxl::Override flag_value, - JxlEncoderFrameSettingId encoder_option) { - if (flag_value != jxl::Override::kDefault) { - SetFlagFrameOptionOrDie(flag_name, - flag_value == jxl::Override::kOn ? 1 : 0, - jxl_encoder_frame_settings, encoder_option); - } - }; - - { // Processing tuning flags. - process_bool_flag("modular", args.modular, JXL_ENC_FRAME_SETTING_MODULAR); - process_bool_flag("keep_invisible", args.keep_invisible, - JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE); - process_bool_flag("dots", args.dots, JXL_ENC_FRAME_SETTING_DOTS); - process_bool_flag("patches", args.patches, JXL_ENC_FRAME_SETTING_PATCHES); - process_bool_flag("gaborish", args.gaborish, - JXL_ENC_FRAME_SETTING_GABORISH); - process_bool_flag("group_order", args.group_order, - JXL_ENC_FRAME_SETTING_GROUP_ORDER); - - if (!args.frame_indexing.empty()) { - bool must_be_all_zeros = args.frame_indexing[0] != '1'; - for (char c : args.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", args.effort, JXL_ENC_FRAME_SETTING_EFFORT, - [](int64_t x) -> std::string { - return (1 <= x && x <= 9) ? "" : "Valid range is {1, 2, ..., 9}."; - }); - process_flag( - "brotli_effort", args.brotli_effort, - JXL_ENC_FRAME_SETTING_BROTLI_EFFORT, [](int64_t x) -> std::string { - return (-1 <= x && x <= 11) ? "" - : "Valid range is {-1, 0, 1, ..., 11}."; - }); - process_flag("epf", args.epf, JXL_ENC_FRAME_SETTING_EPF, - [](int64_t x) -> std::string { - return (-1 <= x && x <= 3) - ? "" - : "Valid range is {-1, 0, 1, 2, 3}.\n"; - }); - process_flag( - "faster_decoding", args.faster_decoding, - JXL_ENC_FRAME_SETTING_DECODING_SPEED, [](int64_t x) -> std::string { - return (0 <= x && x <= 4) ? "" - : "Valid range is {0, 1, 2, 3, 4}.\n"; - }); - process_flag("resampling", args.resampling, - JXL_ENC_FRAME_SETTING_RESAMPLING, - [](int64_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", args.ec_resampling, - JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, - [](int64_t x) -> std::string { - return (x == -1 || x == 1 || x == 4 || x == 8) - ? "" - : "Valid values are {-1, 1, 2, 4, 8}.\n"; - }); - SetFlagFrameOptionOrDie("photon_noise_iso", args.photon_noise_iso, - jxl_encoder_frame_settings, - JXL_ENC_FRAME_SETTING_PHOTON_NOISE); - SetFlagFrameOptionOrDie("already_downsampled", - static_cast<int32_t>(args.already_downsampled), - jxl_encoder_frame_settings, - JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED); - SetDistanceFromFlags(jxl_encoder_frame_settings, &cmdline, &args, codec); - - if (args.group_order != jxl::Override::kOn && - (args.center_x != -1 || args.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", args.center_x, - JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X, - [](int64_t x) -> std::string { - if (x < -1) { - return "Valid values are: -1 or [0 .. xsize)."; - } - return ""; - }); - process_flag("center_y", args.center_y, - JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y, - [](int64_t x) -> std::string { - if (x < -1) { - return "Valid values are: -1 or [0 .. ysize)."; - } - return ""; - }); + pixels = ppf.info.xsize * ppf.info.ysize; + const double t1 = jxl::Now(); + decode_mps = pixels * ppf.info.num_color_channels * 1E-6 / (t1 - t0); } - { // Progressive/responsive mode settings. - bool qprogressive_ac_set = - cmdline.GetOption(args.opt_qprogressive_ac_id)->matched(); - int32_t qprogressive_ac = args.qprogressive_ac ? 1 : 0; - bool responsive_set = - cmdline.GetOption(args.opt_responsive_id)->matched(); - int32_t responsive = args.responsive ? 1 : 0; - - process_flag( - "progressive_dc", args.progressive_dc, - JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, [](int64_t x) -> std::string { - return (-1 <= x && x <= 2) ? "" : "Valid range is {-1, 0, 1, 2}.\n"; - }); - SetFlagFrameOptionOrDie( - "progressive_ac", static_cast<int32_t>(args.progressive_ac), - jxl_encoder_frame_settings, JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC); - - if (args.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. - // TODO(firsching): consider doing more validation after image size is - // known, i.e. set to 512 if 256 would be silly using - // opt_modular_group_size_id. - process_flag("modular_group_size", args.modular_group_size, - JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, - [](int64_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", args.modular_predictor, - JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, - [](int64_t x) -> std::string { - return (-1 <= x && x <= 15) - ? "" - : "Invalid --modular_predictor. Valid " - "range is {-1, 0, 1, ..., 15}.\n"; - }); - process_flag( - "modular_colorspace", args.modular_colorspace, - JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, - [](int64_t x) -> std::string { - return (-1 <= x && x <= 41) - ? "" - : "Invalid --modular_colorspace. Valid range is " - "{-1, 0, 1, ..., 41}.\n"; - }); - process_float_flag( - "modular_ma_tree_learning_percent", - args.modular_ma_tree_learning_percent, - JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, - [](float x) -> std::string { - return -1 <= x && x <= 100 - ? "" - : "Invalid --modular_ma_tree_learning_percent, Valid" - "rang is [-1, 100].\n"; - }); - process_flag("modular_nb_prev_channels", args.modular_nb_prev_channels, - JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, - [](int64_t x) -> std::string { - return (-1 <= x && x <= 11) - ? "" - : "Invalid --modular_nb_prev_channels. Valid " - "range is {-1, 0, 1, ..., 11}.\n"; - }); - SetFlagFrameOptionOrDie("modular_lossy_palette", - static_cast<int32_t>(args.modular_lossy_palette), - jxl_encoder_frame_settings, - JXL_ENC_FRAME_SETTING_LOSSY_PALETTE); - process_flag("modular_palette_colors", args.modular_palette_colors, - JXL_ENC_FRAME_SETTING_PALETTE_COLORS, - [](int64_t x) -> std::string { - return -1 <= x ? "" - : "Invalid --modular_palette_colors, must " - "be -1 or non-negative\n"; - }); - process_float_flag( - "modular_channel_colors_global_percent", - args.modular_channel_colors_global_percent, - JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, - [](float x) -> std::string { - return (-1 <= x && x <= 100) - ? "" - : "Invalid --modular_channel_colors_global_percent. " - "Valid " - "range is [-1, 100].\n"; - }); - process_float_flag( - "modular_channel_colors_group_percent", - args.modular_channel_colors_group_percent, - JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, - [](float x) -> std::string { - return (-1 <= x && x <= 100) - ? "" - : "Invalid --modular_channel_colors_group_percent. " - "Valid " - "range is [-1, 100].\n"; - }); - } - - bool use_container = args.container == jxl::Override::kOn; - if (!ppf.metadata.exif.empty() || !ppf.metadata.xmp.empty() || - !ppf.metadata.jumbf.empty() || !ppf.metadata.iptc.empty() || - (args.lossless_jpeg && args.jpeg_store_metadata)) { - use_container = true; - } - if (use_container) args.container = jxl::Override::kOn; - - if (!ppf.metadata.exif.empty()) { - jxl::InterpretExif(ppf.metadata.exif, &ppf.info.orientation); - } - - if (JXL_ENC_SUCCESS != - JxlEncoderUseContainer(jxl_encoder, static_cast<int>(use_container))) { - std::cerr << "JxlEncoderUseContainer failed." << std::endl; - return EXIT_FAILURE; + if (!args.quiet) { + PrintMode(ppf, decode_mps, image_data.size(), args, cmdline); } - if (num_rep == 0 && !args.quiet) - PrintMode(ppf, decode_mps, image_data.size(), args); - - if (args.lossless_jpeg && IsJPG(image_data)) { + if (args.lossless_jpeg && jpegxl::tools::IsJPG(image_data)) { if (!cmdline.GetOption(args.opt_lossless_jpeg_id)->matched()) { std::cerr << "Note: Implicit-default for JPEG is lossless-transcoding. " << "To silence this message, set --lossless_jpeg=(1|0)." << std::endl; } - if (args.jpeg_store_metadata) { - if (JXL_ENC_SUCCESS != - JxlEncoderStoreJPEGMetadata(jxl_encoder, JXL_TRUE)) { - std::cerr << "Storing JPEG metadata failed. " << std::endl; - return EXIT_FAILURE; - } - } - process_bool_flag("jpeg_reconstruction_cfl", args.jpeg_reconstruction_cfl, - JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL); - 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 = args.intensity_target; - basic_info.num_extra_channels = num_alpha_channels; - basic_info.num_color_channels = ppf.info.num_color_channels; - const bool lossless = args.distance == 0; - basic_info.uses_original_profile = lossless; - if (args.override_bitdepth != 0) { - basic_info.bits_per_sample = args.override_bitdepth; - basic_info.exponent_bits_per_sample = - args.override_bitdepth == 32 ? 8 : 0; - } - if (JXL_ENC_SUCCESS != - JxlEncoderSetCodestreamLevel(jxl_encoder, args.codestream_level)) { - std::cerr << "Setting --codestream_level failed." << std::endl; - return EXIT_FAILURE; - } - if (JXL_ENC_SUCCESS != - JxlEncoderSetBasicInfo(jxl_encoder, &basic_info)) { - std::cerr << "JxlEncoderSetBasicInfo() failed." << std::endl; - return EXIT_FAILURE; - } - if (lossless && - JXL_ENC_SUCCESS != JxlEncoderSetFrameLossless( - jxl_encoder_frame_settings, JXL_TRUE)) { - std::cerr << "JxlEncoderSetFrameLossless() failed." << std::endl; - return EXIT_FAILURE; - } - } + jpeg_bytes = &image_data; + } + } - if (!ppf.icc.empty()) { - 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; - } - } + ProcessFlags(codec, ppf, jpeg_bytes, &cmdline, &args, ¶ms); - 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 < args.frame_indexing.size() && - args.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; - } - } - JxlEncoderStatus enc_status; - { - 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 (args.premultiply != -1) { - if (args.premultiply != 0 && args.premultiply != 1) { - std::cerr << "Flag --premultiply must be one of: -1, 0, 1." - << std::endl; - return EXIT_FAILURE; - } - extra_channel_info.alpha_premultiplied = args.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; - } - } - } - } + if (!ppf.metadata.exif.empty()) { + jxl::InterpretExif(ppf.metadata.exif, &ppf.info.orientation); + } + + if (!ppf.metadata.exif.empty() || !ppf.metadata.xmp.empty() || + !ppf.metadata.jumbf.empty() || !ppf.metadata.iptc.empty() || + (args.lossless_jpeg && args.jpeg_store_metadata)) { + if (args.container == jxl::Override::kDefault) { + args.container = jxl::Override::kOn; + } else if (args.container == jxl::Override::kOff) { + cmdline.VerbosePrintf( + 1, "Stripping all metadata due to explicit container=0\n"); + ppf.metadata.exif.clear(); + ppf.metadata.xmp.clear(); + ppf.metadata.jumbf.clear(); + ppf.metadata.iptc.clear(); + args.jpeg_store_metadata = 0; } - JxlEncoderCloseInput(jxl_encoder); - // Reading compressed output - compressed.clear(); - 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; - } + } + + size_t num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads(); + int64_t flag_num_worker_threads = args.num_threads; + if (flag_num_worker_threads > -1) { + num_worker_threads = flag_num_worker_threads; + } + JxlThreadParallelRunnerPtr runner = JxlThreadParallelRunnerMake( + /*memory_manager=*/nullptr, num_worker_threads); + params.runner = JxlThreadParallelRunner; + params.runner_opaque = runner.get(); + + jpegxl::tools::SpeedStats stats; + jpegxl::tools::JxlOutputProcessor output_processor; + if (args.streaming_output) { + if (args.file_out && !args.disable_output && + !output_processor.SetOutputPath(args.file_out)) { + return EXIT_FAILURE; } - compressed.resize(next_out - compressed.data()); - if (JXL_ENC_SUCCESS != process_result) { - std::cerr << "JxlEncoderProcessOutput failed." << std::endl; + params.output_processor = output_processor.GetOutputProcessor(); + } + std::vector<uint8_t> compressed; + for (size_t num_rep = 0; num_rep < args.num_reps; ++num_rep) { + const double t0 = jxl::Now(); + if (!EncodeImageJXL(params, ppf, jpeg_bytes, + args.streaming_output ? nullptr : &compressed)) { + fprintf(stderr, "EncodeImageJXL() failed.\n"); return EXIT_FAILURE; } - const double t1 = jxl::Now(); stats.NotifyElapsed(t1 - t0); stats.SetImageSize(ppf.info.xsize, ppf.info.ysize); } + size_t compressed_size = args.streaming_output + ? output_processor.finalized_position + : compressed.size(); - if (args.file_out) { + if (!args.streaming_output && args.file_out && !args.disable_output) { if (!jpegxl::tools::WriteFile(args.file_out, compressed)) { std::cerr << "Could not write jxl file." << std::endl; return EXIT_FAILURE; } } if (!args.quiet) { - const double bpp = - static_cast<double>(compressed.size() * jxl::kBitsPerByte) / pixels; - fprintf(stderr, "Compressed to %" PRIuS " bytes ", compressed.size()); + if (compressed_size < 100000) { + cmdline.VerbosePrintf(0, "Compressed to %" PRIuS " bytes ", + compressed_size); + } else { + cmdline.VerbosePrintf(0, "Compressed to %.1f kB ", + compressed_size * 0.001); + } // For lossless jpeg-reconstruction, we don't print some stats, since we // don't have easy access to the image dimensions. if (args.container == jxl::Override::kOn) { - fprintf(stderr, "including container "); + cmdline.VerbosePrintf(0, "including container "); } if (!args.lossless_jpeg) { - fprintf(stderr, "(%.3f bpp%s).\n", bpp / ppf.frames.size(), - ppf.frames.size() == 1 ? "" : "/frame"); + const double bpp = + static_cast<double>(compressed_size * jxl::kBitsPerByte) / pixels; + cmdline.VerbosePrintf(0, "(%.3f bpp%s).\n", bpp / ppf.num_frames(), + ppf.num_frames() == 1 ? "" : "/frame"); JXL_CHECK(stats.Print(num_worker_threads)); } else { - fprintf(stderr, "\n"); + cmdline.VerbosePrintf(0, "\n"); } } return EXIT_SUCCESS; diff --git a/tools/cmdline.cc b/tools/cmdline.cc index f777c94..29e4da8 100644 --- a/tools/cmdline.cc +++ b/tools/cmdline.cc @@ -29,19 +29,30 @@ void CommandLineParser::PrintHelp() const { fprintf(out, " [OPTIONS...]\n"); bool showed_all = true; + int max_verbosity = 0; for (const auto& option : options_) { + max_verbosity = std::max(option->verbosity_level(), max_verbosity); if (option->verbosity_level() > verbosity) { showed_all = false; continue; } + if (option->help_only()) { + fprintf(out, "%s\n", option->help_text()); + 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)")); + fprintf(out, "\n -h, --help\n Prints this help message. "); + if (showed_all) { + fprintf(out, "All options are shown above.\n"); + } else { + fprintf(out, "Add -v (up to a total of %i times) to see more options.\n", + max_verbosity); + } } bool CommandLineParser::Parse(int argc, const char* argv[]) { @@ -91,5 +102,15 @@ bool CommandLineParser::Parse(int argc, const char* argv[]) { return true; } +void CommandLineParser::VerbosePrintf(int min_verbosity, const char* format, + ...) const { + if (min_verbosity > verbosity) return; + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + fflush(stderr); + va_end(args); +} + } // namespace tools } // namespace jpegxl diff --git a/tools/cmdline.h b/tools/cmdline.h index 9b730e6..994341d 100644 --- a/tools/cmdline.h +++ b/tools/cmdline.h @@ -6,6 +6,7 @@ #ifndef TOOLS_CMDLINE_H_ #define TOOLS_CMDLINE_H_ +#include <stdarg.h> #include <stdio.h> #include <string.h> @@ -19,7 +20,7 @@ namespace tools { class CommandLineParser { public: - typedef size_t OptionId; + typedef int OptionId; // An abstract class for defining command line options. class CmdOptionInterface { @@ -53,16 +54,24 @@ class CommandLineParser { // Returns whether the option should be displayed as required in the help // output. No effect on validation. virtual bool required() const = 0; + + // Returns whether the option is not really an option but just help text + virtual bool help_only() const = 0; }; + // Add help text + void AddHelpText(const char* help_text, int verbosity_level = 0) { + options_.emplace_back(new CmdHelpText(help_text, verbosity_level)); + } + // 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) { + const std::string& 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; @@ -113,11 +122,44 @@ class CommandLineParser { // Return the remaining positional args std::vector<const char*> PositionalArgs() const; + // Conditionally print a message to stderr + void VerbosePrintf(int min_verbosity, const char* format, ...) const; + private: + // Help text only. + class CmdHelpText : public CmdOptionInterface { + public: + CmdHelpText(const char* help_text, int verbosity_level) + : help_text_(help_text), verbosity_level_(verbosity_level) {} + + std::string help_flags() const override { return ""; } + const char* help_text() const override { return help_text_; } + int verbosity_level() const override { return verbosity_level_; } + bool matched() const override { return false; } + + bool Match(const char* arg, bool parse_options) const override { + return false; + } + + bool Parse(const int argc, const char* argv[], int* i) override { + return true; + } + + bool positional() const override { return false; } + + bool required() const override { return false; } + + bool help_only() const override { return true; } + + private: + const char* help_text_; + const int verbosity_level_; + }; + // A positional argument. class CmdOptionPositional : public CmdOptionInterface { public: - CmdOptionPositional(const char* name, const char* help_text, + CmdOptionPositional(const char* name, const std::string& help_text, const char** storage, int verbosity_level, bool required) : name_(name), @@ -127,7 +169,7 @@ class CommandLineParser { required_(required) {} std::string help_flags() const override { return name_; } - const char* help_text() const override { return help_text_; } + const char* help_text() const override { return help_text_.c_str(); } int verbosity_level() const override { return verbosity_level_; } bool matched() const override { return matched_; } @@ -150,9 +192,11 @@ class CommandLineParser { bool required() const override { return required_; } + bool help_only() const override { return false; } + private: const char* name_; - const char* help_text_; + const std::string help_text_; const char** storage_; const int verbosity_level_; const bool required_; @@ -252,6 +296,8 @@ class CommandLineParser { return false; } + bool help_only() const override { return false; } + private: // Returns whether arg matches the short_name flag of this option. bool MatchShort(const char* arg) const { diff --git a/tools/codec_config.h b/tools/codec_config.h index a4f79a6..8d1c73f 100644 --- a/tools/codec_config.h +++ b/tools/codec_config.h @@ -7,6 +7,7 @@ #define TOOLS_CODEC_CONFIG_H_ #include <stdint.h> + #include <string> namespace jpegxl { diff --git a/tools/color_encoding_fuzzer.cc b/tools/color_encoding_fuzzer.cc index 087bd8b..d73dc4f 100644 --- a/tools/color_encoding_fuzzer.cc +++ b/tools/color_encoding_fuzzer.cc @@ -7,18 +7,20 @@ #include "lib/extras/dec/color_description.h" -namespace jxl { +namespace jpegxl { +namespace tools { int TestOneInput(const uint8_t* data, size_t size) { std::string description(reinterpret_cast<const char*>(data), size); JxlColorEncoding c; - (void)ParseDescription(description, &c); + (void)jxl::ParseDescription(description, &c); return 0; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - return jxl::TestOneInput(data, size); + return jpegxl::tools::TestOneInput(data, size); } diff --git a/tools/comparison_viewer/CMakeLists.txt b/tools/comparison_viewer/CMakeLists.txt index b5b5fa7..3c548d0 100644 --- a/tools/comparison_viewer/CMakeLists.txt +++ b/tools/comparison_viewer/CMakeLists.txt @@ -3,9 +3,9 @@ # 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.") +find_package(Qt6 QUIET COMPONENTS Concurrent Widgets) +if (NOT Qt6_FOUND) + message(WARNING "Qt6 was not found. The comparison tool will not be built.") return() endif () @@ -28,10 +28,10 @@ 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 + Qt6::Widgets + jxl-internal + jxl_threads + jxl_extras-internal lcms2 ) @@ -51,8 +51,8 @@ add_executable(compare_codecs WIN32 ) target_link_libraries(compare_codecs image_loading - Qt5::Concurrent - Qt5::Widgets + Qt6::Concurrent + Qt6::Widgets icc_detect ) @@ -69,6 +69,6 @@ add_executable(compare_images WIN32 ) target_link_libraries(compare_images image_loading - Qt5::Widgets + Qt6::Widgets icc_detect ) diff --git a/tools/comparison_viewer/codec_comparison_window.cc b/tools/comparison_viewer/codec_comparison_window.cc index 9bf6253..0ecd579 100644 --- a/tools/comparison_viewer/codec_comparison_window.cc +++ b/tools/comparison_viewer/codec_comparison_window.cc @@ -31,7 +31,8 @@ #include "tools/comparison_viewer/split_image_view.h" #include "tools/icc_detect/icc_detect.h" -namespace jxl { +namespace jpegxl { +namespace tools { static constexpr char kPngSuffix[] = "png"; @@ -313,4 +314,5 @@ void CodecComparisonWindow::browseDirectory(const QDir& directory, int depth) { } } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/comparison_viewer/codec_comparison_window.h b/tools/comparison_viewer/codec_comparison_window.h index b157a5a..bb23314 100644 --- a/tools/comparison_viewer/codec_comparison_window.h +++ b/tools/comparison_viewer/codec_comparison_window.h @@ -12,18 +12,19 @@ #include <QSet> #include <QString> -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/common.h" +#include "lib/jxl/base/common.h" #include "tools/comparison_viewer/ui_codec_comparison_window.h" -namespace jxl { +namespace jpegxl { +namespace tools { class CodecComparisonWindow : public QMainWindow { Q_OBJECT public: explicit CodecComparisonWindow( - const QString& directory, float intensityTarget = kDefaultIntensityTarget, + const QString& directory, + float intensityTarget = jxl::kDefaultIntensityTarget, QWidget* parent = nullptr); ~CodecComparisonWindow() override = default; @@ -72,6 +73,7 @@ class CodecComparisonWindow : public QMainWindow { const QByteArray monitorIccProfile_; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_COMPARISON_VIEWER_CODEC_COMPARISON_WINDOW_H_ diff --git a/tools/comparison_viewer/codec_comparison_window.ui b/tools/comparison_viewer/codec_comparison_window.ui index 1fbda6a..85ba810 100644 --- a/tools/comparison_viewer/codec_comparison_window.ui +++ b/tools/comparison_viewer/codec_comparison_window.ui @@ -152,14 +152,14 @@ </layout> </item> <item> - <widget class="jxl::SplitImageView" name="splitImageView" native="true"/> + <widget class="jpegxl::tools::SplitImageView" name="splitImageView" native="true"/> </item> </layout> </widget> </widget> <customwidgets> <customwidget> - <class>jxl::SplitImageView</class> + <class>jpegxl::tools::SplitImageView</class> <extends>QWidget</extends> <header>split_image_view.h</header> <container>1</container> diff --git a/tools/comparison_viewer/compare_codecs.cc b/tools/comparison_viewer/compare_codecs.cc index 932765e..3ab4c8d 100644 --- a/tools/comparison_viewer/compare_codecs.cc +++ b/tools/comparison_viewer/compare_codecs.cc @@ -66,7 +66,7 @@ int main(int argc, char** argv) { for (const QString& folder : folders) { auto* const window = - new jxl::CodecComparisonWindow(folder, intensityTarget); + new jpegxl::tools::CodecComparisonWindow(folder, intensityTarget); window->setAttribute(Qt::WA_DeleteOnClose); window->show(); } diff --git a/tools/comparison_viewer/compare_images.cc b/tools/comparison_viewer/compare_images.cc index cf39f88..321b2c4 100644 --- a/tools/comparison_viewer/compare_images.cc +++ b/tools/comparison_viewer/compare_images.cc @@ -87,13 +87,14 @@ int main(int argc, char** argv) { parser.showHelp(EXIT_FAILURE); } - jxl::SplitImageView view; + jpegxl::tools::SplitImageView view; - const QByteArray monitorIccProfile = jxl::GetMonitorIccProfile(&view); + const QByteArray monitorIccProfile = + jpegxl::tools::GetMonitorIccProfile(&view); const QString leftImagePath = arguments.takeFirst(); - QImage leftImage = jxl::loadImage(leftImagePath, monitorIccProfile, - intensityTarget, colorSpaceHint); + QImage leftImage = jpegxl::tools::loadImage(leftImagePath, monitorIccProfile, + intensityTarget, colorSpaceHint); if (leftImage.isNull()) { displayLoadingError(leftImagePath); return EXIT_FAILURE; @@ -101,8 +102,8 @@ int main(int argc, char** argv) { view.setLeftImage(std::move(leftImage)); const QString rightImagePath = arguments.takeFirst(); - QImage rightImage = jxl::loadImage(rightImagePath, monitorIccProfile, - intensityTarget, colorSpaceHint); + QImage rightImage = jpegxl::tools::loadImage( + rightImagePath, monitorIccProfile, intensityTarget, colorSpaceHint); if (rightImage.isNull()) { displayLoadingError(rightImagePath); return EXIT_FAILURE; @@ -111,8 +112,8 @@ int main(int argc, char** argv) { if (!arguments.empty()) { const QString middleImagePath = arguments.takeFirst(); - QImage middleImage = jxl::loadImage(middleImagePath, monitorIccProfile, - intensityTarget, colorSpaceHint); + QImage middleImage = jpegxl::tools::loadImage( + middleImagePath, monitorIccProfile, intensityTarget, colorSpaceHint); if (middleImage.isNull()) { displayLoadingError(middleImagePath); return EXIT_FAILURE; diff --git a/tools/comparison_viewer/image_loading.cc b/tools/comparison_viewer/image_loading.cc index 55bebb8..4a44dec 100644 --- a/tools/comparison_viewer/image_loading.cc +++ b/tools/comparison_viewer/image_loading.cc @@ -5,39 +5,56 @@ #include "tools/comparison_viewer/image_loading.h" +#include <jxl/cms.h> + #include <QRgb> #include <QThread> +#include <cstdint> +#include <vector> #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 "lib/jxl/image_bundle.h" +#include "lib/jxl/image_metadata.h" +#include "tools/file_io.h" +#include "tools/thread_pool_internal.h" #include "tools/viewer/load_jxl.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using jxl::CodecInOut; +using jxl::ColorEncoding; +using jxl::IccBytes; +using jxl::Image3F; +using jxl::ImageBundle; +using jxl::Rect; +using jxl::Span; +using jxl::Status; +using jxl::ThreadPool; +using jxl::extras::ColorHints; namespace { -Status loadFromFile(const QString& filename, - const extras::ColorHints& color_hints, +Status loadFromFile(const QString& filename, const ColorHints& color_hints, CodecInOut* const decoded, ThreadPool* const pool) { - PaddedBytes compressed; - JXL_RETURN_IF_ERROR(ReadFile(filename.toStdString(), &compressed)); + std::vector<uint8_t> compressed; + JXL_RETURN_IF_ERROR( + jpegxl::tools::ReadFile(filename.toStdString(), &compressed)); const Span<const uint8_t> compressed_span(compressed); - return SetFromBytes(compressed_span, color_hints, decoded, pool, nullptr); + return jxl::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; + if (extension == "jxl" || extension == "j" || extension == "brn") { + return true; + } + const auto codec = jxl::extras::CodecFromPath("." + extension.toStdString()); + return codec != jxl::extras::Codec::kUnknown; } QImage loadImage(const QString& filename, const QByteArray& targetIccProfile, @@ -51,7 +68,7 @@ QImage loadImage(const QString& filename, const QByteArray& targetIccProfile, static ThreadPoolInternal pool(QThread::idealThreadCount()); CodecInOut decoded; - extras::ColorHints color_hints; + ColorHints color_hints; if (!sourceColorSpaceHint.isEmpty()) { color_hints.Add("color_space", sourceColorSpaceHint.toStdString()); } @@ -62,22 +79,28 @@ QImage loadImage(const QString& filename, const QByteArray& targetIccProfile, 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))) { + bool use_fallback_profile = true; + if (!targetIccProfile.isEmpty()) { + IccBytes icc; + icc.assign(reinterpret_cast<const uint8_t*>(targetIccProfile.data()), + reinterpret_cast<const uint8_t*>(targetIccProfile.data() + + targetIccProfile.size())); + use_fallback_profile = + !targetColorSpace.SetICC(std::move(icc), JxlGetDefaultCms()); + } + if (use_fallback_profile) { targetColorSpace = ColorEncoding::SRGB(ib.IsGray()); } Image3F converted; - if (!ib.CopyTo(Rect(ib), targetColorSpace, GetJxlCms(), &converted, &pool)) { + if (!ib.CopyTo(Rect(ib), targetColorSpace, *JxlGetDefaultCms(), &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); + return jxl::Clamp1(x * 255 + .5f, 0.f, 255.f); }; if (ib.HasAlpha()) { @@ -108,4 +131,5 @@ QImage loadImage(const QString& filename, const QByteArray& targetIccProfile, return image; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/comparison_viewer/image_loading.h b/tools/comparison_viewer/image_loading.h index 89b37d1..37baaef 100644 --- a/tools/comparison_viewer/image_loading.h +++ b/tools/comparison_viewer/image_loading.h @@ -10,9 +10,10 @@ #include <QImage> #include <QString> -#include "lib/jxl/common.h" +#include "lib/jxl/base/common.h" -namespace jxl { +namespace jpegxl { +namespace tools { // `extension` should not include the dot. bool canLoadImageWithExtension(QString extension); @@ -21,9 +22,10 @@ bool canLoadImageWithExtension(QString extension); // specified. Thread-hostile. QImage loadImage(const QString& filename, const QByteArray& targetIccProfile = QByteArray(), - float intensityTarget = kDefaultIntensityTarget, + float intensityTarget = jxl::kDefaultIntensityTarget, const QString& sourceColorSpaceHint = QString()); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_COMPARISON_VIEWER_IMAGE_LOADING_H_ diff --git a/tools/comparison_viewer/settings.cc b/tools/comparison_viewer/settings.cc index 9ef117b..ca5c0c9 100644 --- a/tools/comparison_viewer/settings.cc +++ b/tools/comparison_viewer/settings.cc @@ -5,7 +5,8 @@ #include "tools/comparison_viewer/settings.h" -namespace jxl { +namespace jpegxl { +namespace tools { SettingsDialog::SettingsDialog(QWidget* const parent) : QDialog(parent), settings_("JPEG XL project", "Comparison tool") { @@ -48,4 +49,5 @@ void SettingsDialog::settingsToUi() { ui_.grayTime->setValue(renderingSettings_.grayMSecs); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/comparison_viewer/settings.h b/tools/comparison_viewer/settings.h index bd91f71..a54cd87 100644 --- a/tools/comparison_viewer/settings.h +++ b/tools/comparison_viewer/settings.h @@ -12,7 +12,8 @@ #include "tools/comparison_viewer/split_image_renderer.h" #include "tools/comparison_viewer/ui_settings.h" -namespace jxl { +namespace jpegxl { +namespace tools { class SettingsDialog : public QDialog { Q_OBJECT @@ -35,6 +36,7 @@ class SettingsDialog : public QDialog { SplitImageRenderingSettings renderingSettings_; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_COMPARISON_VIEWER_SETTINGS_H_ diff --git a/tools/comparison_viewer/split_image_renderer.cc b/tools/comparison_viewer/split_image_renderer.cc index acade64..911229c 100644 --- a/tools/comparison_viewer/split_image_renderer.cc +++ b/tools/comparison_viewer/split_image_renderer.cc @@ -5,10 +5,6 @@ #include "tools/comparison_viewer/split_image_renderer.h" -#include <algorithm> -#include <cmath> -#include <utility> - #include <QEvent> #include <QGuiApplication> #include <QPainter> @@ -16,8 +12,12 @@ #include <QPen> #include <QPoint> #include <QRect> +#include <algorithm> +#include <cmath> +#include <utility> -namespace jxl { +namespace jpegxl { +namespace tools { SplitImageRenderer::SplitImageRenderer(QWidget* const parent) : QWidget(parent) { @@ -32,16 +32,19 @@ SplitImageRenderer::SplitImageRenderer(QWidget* const parent) void SplitImageRenderer::setLeftImage(QImage image) { leftImage_ = QPixmap::fromImage(std::move(image)); + leftImage_.setDevicePixelRatio(devicePixelRatio()); updateMinimumSize(); update(); } void SplitImageRenderer::setRightImage(QImage image) { rightImage_ = QPixmap::fromImage(std::move(image)); + rightImage_.setDevicePixelRatio(devicePixelRatio()); updateMinimumSize(); update(); } void SplitImageRenderer::setMiddleImage(QImage image) { middleImage_ = QPixmap::fromImage(std::move(image)); + middleImage_.setDevicePixelRatio(devicePixelRatio()); updateMinimumSize(); update(); } @@ -181,7 +184,8 @@ void SplitImageRenderer::paintEvent(QPaintEvent* const event) { painter.transform().inverted().map(QPointF(middleX_, 0.)).x(); QRectF middleRect = middleImage_.rect(); middleRect.setWidth(middleWidth); - middleRect.moveCenter(QPointF(transformedMiddleX, middleRect.center().y())); + middleRect.moveCenter(QPointF(transformedMiddleX * devicePixelRatio(), + middleRect.center().y())); middleRect.setLeft(std::round(middleRect.left())); middleRect.setRight(std::round(middleRect.right())); @@ -191,24 +195,30 @@ void SplitImageRenderer::paintEvent(QPaintEvent* const event) { QRectF rightRect = rightImage_.rect(); rightRect.setLeft(middleRect.right()); - painter.drawPixmap(leftRect, leftImage_, leftRect); - painter.drawPixmap(rightRect, rightImage_, rightRect); - painter.drawPixmap(middleRect, middleImage_, middleRect); + painter.drawPixmap(QPointF(), leftImage_, leftRect); + painter.drawPixmap(middleRect.topLeft() / devicePixelRatio(), middleImage_, + middleRect); + painter.drawPixmap(rightRect.topLeft() / devicePixelRatio(), rightImage_, + rightRect); QPen middlePen; middlePen.setStyle(Qt::DotLine); painter.setPen(middlePen); - painter.drawLine(leftRect.topRight(), leftRect.bottomRight()); - painter.drawLine(rightRect.topLeft(), rightRect.bottomLeft()); + painter.drawLine(leftRect.topRight() / devicePixelRatio(), + leftRect.bottomRight() / devicePixelRatio()); + painter.drawLine(rightRect.topLeft() / devicePixelRatio(), + rightRect.bottomLeft() / devicePixelRatio()); } 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)); + const QSizeF leftSize = leftImage_.deviceIndependentSize(); + const QSizeF rightSize = rightImage_.deviceIndependentSize(); + const QSizeF middleSize = middleImage_.deviceIndependentSize(); + const qreal imagesWidth = std::max( + std::max(leftSize.width(), rightSize.width()), middleSize.width()); + const qreal imagesHeight = std::max( + std::max(leftSize.height(), rightSize.height()), middleSize.height()); + setMinimumSize((scale_ * QSizeF(imagesWidth, imagesHeight)).toSize()); } void SplitImageRenderer::setRenderingMode(const RenderingMode newMode) { @@ -236,4 +246,5 @@ void SplitImageRenderer::setRenderingMode(const RenderingMode newMode) { emit renderingModeChanged(mode_); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/comparison_viewer/split_image_renderer.h b/tools/comparison_viewer/split_image_renderer.h index decb407..5d3029a 100644 --- a/tools/comparison_viewer/split_image_renderer.h +++ b/tools/comparison_viewer/split_image_renderer.h @@ -15,7 +15,8 @@ #include <QWheelEvent> #include <QWidget> -namespace jxl { +namespace jpegxl { +namespace tools { struct SplitImageRenderingSettings { int fadingMSecs; @@ -85,6 +86,7 @@ class SplitImageRenderer : public QWidget { double scale_ = 1.; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_RENDERER_H_ diff --git a/tools/comparison_viewer/split_image_view.cc b/tools/comparison_viewer/split_image_view.cc index 76c8edc..9c27f46 100644 --- a/tools/comparison_viewer/split_image_view.cc +++ b/tools/comparison_viewer/split_image_view.cc @@ -11,7 +11,8 @@ #include "tools/comparison_viewer/split_image_renderer.h" -namespace jxl { +namespace jpegxl { +namespace tools { SplitImageView::SplitImageView(QWidget* const parent) : QWidget(parent) { ui_.setupUi(this); @@ -68,4 +69,5 @@ void SplitImageView::on_settingsButton_clicked() { } } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/comparison_viewer/split_image_view.h b/tools/comparison_viewer/split_image_view.h index 4978750..b9c3536 100644 --- a/tools/comparison_viewer/split_image_view.h +++ b/tools/comparison_viewer/split_image_view.h @@ -11,7 +11,8 @@ #include "tools/comparison_viewer/settings.h" #include "tools/comparison_viewer/ui_split_image_view.h" -namespace jxl { +namespace jpegxl { +namespace tools { class SplitImageView : public QWidget { Q_OBJECT @@ -35,6 +36,7 @@ class SplitImageView : public QWidget { SettingsDialog settings_; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_VIEW_H_ diff --git a/tools/comparison_viewer/split_image_view.ui b/tools/comparison_viewer/split_image_view.ui index 0755a58..f3b80c9 100644 --- a/tools/comparison_viewer/split_image_view.ui +++ b/tools/comparison_viewer/split_image_view.ui @@ -17,7 +17,7 @@ <property name="widgetResizable"> <bool>true</bool> </property> - <widget class="jxl::SplitImageRenderer" name="splitImageRenderer"/> + <widget class="jpegxl::tools::SplitImageRenderer" name="splitImageRenderer"/> </widget> </item> <item> @@ -130,7 +130,7 @@ </widget> <customwidgets> <customwidget> - <class>jxl::SplitImageRenderer</class> + <class>jpegxl::tools::SplitImageRenderer</class> <extends>QWidget</extends> <header>split_image_renderer.h</header> <container>1</container> diff --git a/tools/conformance/conformance.py b/tools/conformance/conformance.py index 15158bc..e4be865 100755 --- a/tools/conformance/conformance.py +++ b/tools/conformance/conformance.py @@ -12,6 +12,7 @@ import argparse import json import numpy import os +import shutil import subprocess import sys import tempfile @@ -166,7 +167,13 @@ def ConformanceTestRunner(args): for reference_basename, decoded_filename in exact_tests: reference_filename = os.path.join(test_dir, reference_basename) - ok = ok & CompareBinaries(reference_filename, decoded_filename) + binary_ok = CompareBinaries(reference_filename, + decoded_filename) + if not binary_ok and args.update_on_failure: + os.unlink(reference_filename) + shutil.copy2(decoded_filename, reference_filename) + binary_ok = True + ok = ok & binary_ok # Validate metadata. with open(meta_filename, 'r') as f: @@ -182,36 +189,50 @@ def ConformanceTestRunner(args): 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') + reference_npy_fn = os.path.join(test_dir, 'reference_image.npy') + decoded_npy_fn = os.path.join(work_dir, 'decoded_image.npy') - if not os.path.exists(decoded_npy): + if not os.path.exists(decoded_npy_fn): ok = Failure('File not decoded: decoded_image.npy') continue - reference_npy = numpy.load(reference_npy) - decoded_npy = numpy.load(decoded_npy) + reference_npy = numpy.load(reference_npy_fn) + decoded_npy = numpy.load(decoded_npy_fn) + frames_ok = True for i, fd in enumerate(descriptor['frames']): - ok = ok & CompareNPY(reference_npy, reference_icc, decoded_npy, - decoded_icc, i, fd['rms_error'], - fd['peak_error']) + frames_ok = frames_ok & CompareNPY( + reference_npy, reference_icc, decoded_npy, + decoded_icc, i, fd['rms_error'], + fd['peak_error']) + + if not frames_ok and args.update_on_failure: + os.unlink(reference_npy_fn) + shutil.copy2(decoded_npy_fn, reference_npy_fn) + frames_ok = True + ok = ok & frames_ok if 'preview' in descriptor: - reference_npy = os.path.join(test_dir, - 'reference_preview.npy') - decoded_npy = os.path.join(work_dir, 'decoded_preview.npy') + reference_npy_fn = os.path.join(test_dir, + 'reference_preview.npy') + decoded_npy_fn = os.path.join(work_dir, + 'decoded_preview.npy') - if not os.path.exists(decoded_npy): + if not os.path.exists(decoded_npy_fn): ok = Failure( 'File not decoded: decoded_preview.npy') - reference_npy = numpy.load(reference_npy) - decoded_npy = numpy.load(decoded_npy) - ok = ok & CompareNPY(reference_npy, reference_icc, decoded_npy, - decoded_icc, 0, - descriptor['preview']['rms_error'], - descriptor['preview']['peak_error']) + reference_npy = numpy.load(reference_npy_fn) + decoded_npy = numpy.load(decoded_npy_fn) + preview_ok = CompareNPY(reference_npy, reference_icc, + decoded_npy, decoded_icc, 0, + descriptor['preview']['rms_error'], + descriptor['preview']['peak_error']) + if not preview_ok & args.update_on_failure: + os.unlink(reference_npy_fn) + shutil.copy2(decoded_npy_fn, reference_npy_fn) + preview_ok = True + ok = ok & preview_ok return ok @@ -228,6 +249,9 @@ def main(): required=True, help=('path to the corpus directory or corpus descriptor' ' text file.')) + parser.add_argument( + '--update_on_failure', action='store_true', + help='If set, updates reference files on failing checks.') args = parser.parse_args() if not ConformanceTestRunner(args): sys.exit(1) diff --git a/tools/conformance/lcms2.py b/tools/conformance/lcms2.py index f8313cd..09f6334 100644 --- a/tools/conformance/lcms2.py +++ b/tools/conformance/lcms2.py @@ -8,8 +8,12 @@ import ctypes from numpy.ctypeslib import ndpointer import numpy import os +import platform -lcms2_lib_path = os.getenv("LCMS2_LIB_PATH", "liblcms2.so.2") +IS_OSX = (platform.system() == "Darwin") + +default_libcms2_lib_path = ["liblcms2.so.2", "liblcms2.2.dylib"][IS_OSX] +lcms2_lib_path = os.getenv("LCMS2_LIB_PATH", default_libcms2_lib_path) lcms2_lib = ctypes.cdll.LoadLibrary(lcms2_lib_path) native_open_profile = lcms2_lib.cmsOpenProfileFromMem diff --git a/tools/decode_and_encode.cc b/tools/decode_and_encode.cc index 59b1d6d..3c1d8e9 100644 --- a/tools/decode_and_encode.cc +++ b/tools/decode_and_encode.cc @@ -9,11 +9,12 @@ #include "lib/extras/codec.h" #include "lib/jxl/base/data_parallel.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/file_io.h" +#include "tools/thread_pool_internal.h" -namespace jxl { namespace { // Reads an input file (typically PNM) with color_space hint and writes to an @@ -27,16 +28,26 @@ int Convert(int argc, char** argv) { const std::string& desc = argv[2]; const std::string& pathname_out = argv[3]; - CodecInOut io; - extras::ColorHints color_hints; - ThreadPoolInternal pool(4); + std::vector<uint8_t> encoded_in; + if (!jpegxl::tools::ReadFile(pathname_in, &encoded_in)) { + fprintf(stderr, "Failed to read image from %s\n", pathname_in.c_str()); + return 1; + } + jxl::CodecInOut io; + jxl::extras::ColorHints color_hints; + jpegxl::tools::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()); + if (!jxl::SetFromBytes(jxl::Bytes(encoded_in), color_hints, &io, &pool)) { + fprintf(stderr, "Failed to decode %s\n", pathname_in.c_str()); return 1; } - if (!EncodeToFile(io, pathname_out, &pool)) { + std::vector<uint8_t> encoded_out; + if (!jxl::Encode(io, pathname_out, &encoded_out, &pool)) { + fprintf(stderr, "Failed to encode %s\n", pathname_out.c_str()); + return 1; + } + if (!jpegxl::tools::WriteFile(pathname_out, encoded_out)) { fprintf(stderr, "Failed to write %s\n", pathname_out.c_str()); return 1; } @@ -45,6 +56,5 @@ int Convert(int argc, char** argv) { } } // namespace -} // namespace jxl -int main(int argc, char** argv) { return jxl::Convert(argc, argv); } +int main(int argc, char** argv) { return Convert(argc, argv); } diff --git a/tools/decode_basic_info_fuzzer.cc b/tools/decode_basic_info_fuzzer.cc index 59f7089..8e97ff6 100644 --- a/tools/decode_basic_info_fuzzer.cc +++ b/tools/decode_basic_info_fuzzer.cc @@ -3,11 +3,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/decode.h> #include <stdint.h> -#include "jxl/decode.h" - -namespace jxl { +namespace jpegxl { +namespace tools { int TestOneInput(const uint8_t* data, size_t size) { JxlDecoderStatus status; @@ -40,19 +40,19 @@ int TestOneInput(const uint8_t* data, size_t size) { return 0; } - JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; - JxlDecoderGetColorAsEncodedProfile( - dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, nullptr); + JxlDecoderGetColorAsEncodedProfile(dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL, + nullptr); size_t dec_profile_size; - JxlDecoderGetICCProfileSize(dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, + JxlDecoderGetICCProfileSize(dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &dec_profile_size); JxlDecoderDestroy(dec); return 0; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - return jxl::TestOneInput(data, size); + return jpegxl::tools::TestOneInput(data, size); } diff --git a/tools/djpegli.cc b/tools/djpegli.cc new file mode 100644 index 0000000..bac55e1 --- /dev/null +++ b/tools/djpegli.cc @@ -0,0 +1,197 @@ +// 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> +#include <vector> + +#include "lib/extras/dec/jpegli.h" +#include "lib/extras/enc/apng.h" +#include "lib/extras/enc/encode.h" +#include "lib/extras/time.h" +#include "lib/jxl/base/printf_macros.h" +#include "tools/cmdline.h" +#include "tools/file_io.h" +#include "tools/speed_stats.h" + +namespace jpegxl { +namespace tools { +namespace { + +struct Args { + void AddCommandLineOptions(CommandLineParser* cmdline) { + std::string output_help("The output can be "); + if (jxl::extras::GetAPNGEncoder()) { + output_help.append("PNG, "); + } + output_help.append("PFM or PPM/PGM/PNM"); + cmdline->AddPositionalOption("INPUT", /* required = */ true, + "The JPG input file.", &file_in); + + cmdline->AddPositionalOption("OUTPUT", /* required = */ true, output_help, + &file_out); + cmdline->AddOptionFlag('\0', "disable_output", + "No output file will be written (for benchmarking)", + &disable_output, &SetBooleanTrue); + + cmdline->AddOptionValue('\0', "bitdepth", "8|16", + "Sets the output bitdepth for integer based " + "formats, can be 8 (default) " + "or 16. Has no impact on PFM output.", + &bitdepth, &ParseUnsigned); + + cmdline->AddOptionValue('\0', "num_reps", "N", + "Sets the number of times to decompress the image. " + "Used for benchmarking, the default is 1.", + &num_reps, &ParseUnsigned); + + cmdline->AddOptionFlag('\0', "quiet", "Silence output (except for errors).", + &quiet, &SetBooleanTrue); + } + + const char* file_in = nullptr; + const char* file_out = nullptr; + bool disable_output = false; + size_t bitdepth = 8; + size_t num_reps = 1; + bool quiet = false; +}; + +bool ValidateArgs(const Args& args) { + if (args.bitdepth != 8 && args.bitdepth != 16) { + fprintf(stderr, "Invalid --bitdepth argument\n"); + return false; + } + return true; +} + +void SetDecompressParams(const Args& args, const std::string& extension, + jxl::extras::JpegDecompressParams* params) { + if (extension == ".pfm") { + params->output_data_type = JXL_TYPE_FLOAT; + params->output_endianness = JXL_BIG_ENDIAN; + } else if (args.bitdepth == 16) { + params->output_data_type = JXL_TYPE_UINT16; + params->output_endianness = JXL_BIG_ENDIAN; + } + if (extension == ".pgm") { + params->force_grayscale = true; + } else if (extension == ".ppm") { + params->force_rgb = true; + } +} + +int DJpegliMain(int argc, const char* argv[]) { + Args args; + CommandLineParser cmdline; + args.AddCommandLineOptions(&cmdline); + + if (!cmdline.Parse(argc, const_cast<const char**>(argv))) { + // Parse already printed the actual error cause. + fprintf(stderr, "Use '%s -h' for more information.\n", argv[0]); + return EXIT_FAILURE; + } + + if (cmdline.HelpFlagPassed() || !args.file_in) { + cmdline.PrintHelp(); + return EXIT_SUCCESS; + } + + if (!args.file_out && !args.disable_output) { + fprintf(stderr, + "No output file specified and --disable_output flag not passed.\n"); + return EXIT_FAILURE; + } + + if (args.disable_output && !args.quiet) { + fprintf(stderr, + "Decoding will be performed, but the result will be discarded.\n"); + } + + if (!ValidateArgs(args)) { + return EXIT_FAILURE; + } + + std::vector<uint8_t> jpeg_bytes; + if (!ReadFile(args.file_in, &jpeg_bytes)) { + fprintf(stderr, "Failed to read input image %s\n", args.file_in); + return EXIT_FAILURE; + } + + if (!args.quiet) { + fprintf(stderr, "Read %" PRIuS " compressed bytes.\n", jpeg_bytes.size()); + } + + std::string filename_out; + std::string extension; + if (args.file_out) { + filename_out = std::string(args.file_out); + size_t pos = filename_out.find_last_of('.'); + if (pos >= filename_out.size()) { + fprintf(stderr, "Unrecognized output extension.\n"); + return EXIT_FAILURE; + } + extension = filename_out.substr(pos); + } + + jxl::extras::JpegDecompressParams dparams; + SetDecompressParams(args, extension, &dparams); + + jxl::extras::PackedPixelFile ppf; + jpegxl::tools::SpeedStats stats; + for (size_t num_rep = 0; num_rep < args.num_reps; ++num_rep) { + const double t0 = jxl::Now(); + if (!jxl::extras::DecodeJpeg(jpeg_bytes, dparams, nullptr, &ppf)) { + fprintf(stderr, "jpegli decoding failed\n"); + return EXIT_FAILURE; + } + const double t1 = jxl::Now(); + stats.NotifyElapsed(t1 - t0); + stats.SetImageSize(ppf.info.xsize, ppf.info.ysize); + } + + if (!args.quiet) { + stats.Print(1); + } + + if (args.disable_output) { + return EXIT_SUCCESS; + } + + if (extension == ".pnm") { + extension = ppf.info.num_color_channels == 3 ? ".ppm" : ".pgm"; + } + + std::unique_ptr<jxl::extras::Encoder> encoder = + jxl::extras::Encoder::FromExtension(extension); + if (encoder == nullptr) { + fprintf(stderr, "Can't decode to the file extension '%s'\n", + extension.c_str()); + return EXIT_FAILURE; + } + jxl::extras::EncodedImage encoded_image; + if (!encoder->Encode(ppf, &encoded_image) || + encoded_image.bitstreams.empty()) { + fprintf(stderr, "Encode failed\n"); + return EXIT_FAILURE; + } + if (!WriteFile(filename_out, encoded_image.bitstreams[0])) { + fprintf(stderr, "Failed to write output file %s\n", filename_out.c_str()); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +} // namespace +} // namespace tools +} // namespace jpegxl + +int main(int argc, const char* argv[]) { + return jpegxl::tools::DJpegliMain(argc, argv); +} diff --git a/tools/djxl_fuzzer.cc b/tools/djxl_fuzzer.cc index a03472a..4691eb4 100644 --- a/tools/djxl_fuzzer.cc +++ b/tools/djxl_fuzzer.cc @@ -3,24 +3,22 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/decode.h> +#include <jxl/decode_cxx.h> +#include <jxl/thread_parallel_runner.h> +#include <jxl/thread_parallel_runner_cxx.h> #include <limits.h> #include <stdint.h> -#include <stdio.h> #include <stdlib.h> #include <string.h> #include <algorithm> +#include <hwy/targets.h> #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. @@ -81,10 +79,10 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels, 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)) { + dec.get(), JXL_DEC_BASIC_INFO | 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(), @@ -111,7 +109,6 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels, } bool seen_basic_info = false; - bool seen_extensions = false; bool seen_color_encoding = false; bool seen_preview = false; bool seen_need_image_out = false; @@ -213,6 +210,7 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels, return false; } } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + if (want_preview) abort(); // expected preview before frame if (spec.jpeg_to_pixels) abort(); if (!seen_jpeg_reconstruction) abort(); seen_jpeg_need_more_output = true; @@ -274,12 +272,6 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels, } 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 @@ -288,14 +280,13 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels, // 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)) { + JxlDecoderGetICCProfileSize(dec.get(), 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, + dec.get(), JXL_COLOR_PROFILE_TARGET_DATA, icc_profile->data(), icc_profile->size())) { return false; } @@ -313,6 +304,7 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels, } } } else if (status == JXL_DEC_PREVIEW_IMAGE) { + // TODO(eustas): test JXL_DEC_NEED_PREVIEW_OUT_BUFFER if (seen_preview) abort(); if (!want_preview) abort(); if (!seen_color_encoding) abort(); @@ -404,7 +396,10 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels, } } } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { - if (want_preview) abort(); // expected preview before frame + // Do not check preview precedence here, since this event only declares + // that JPEG is going to be decoded; though, when first byte of JPEG + // arrives (JXL_DEC_JPEG_NEED_MORE_OUTPUT) it is certain that preview + // should have been produced already. if (seen_jpeg_reconstruction) abort(); seen_jpeg_reconstruction = true; if (!spec.jpeg_to_pixels) { diff --git a/tools/fuzzer_corpus.cc b/tools/djxl_fuzzer_corpus.cc index 159256c..73c7eae 100644 --- a/tools/fuzzer_corpus.cc +++ b/tools/djxl_fuzzer_corpus.cc @@ -15,6 +15,8 @@ #include <unistd.h> #endif +#include <jxl/cms.h> + #include <algorithm> #include <functional> #include <iostream> @@ -22,25 +24,21 @@ #include <random> #include <vector> -#if JPEGXL_ENABLE_JPEG #include "lib/extras/codec.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" +#include "lib/jxl/test_utils.h" // TODO(eustas): cut this dependency +#include "tools/file_io.h" +#include "tools/thread_pool_internal.h" namespace { @@ -175,7 +173,6 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec, } 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); @@ -214,46 +211,43 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec, } } } - + uint32_t num_channels = bytes_per_pixel / bytes_per_sample; + JxlDataType data_type = + bytes_per_sample == 1 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16; + JxlPixelFormat format = {num_channels, data_type, JXL_LITTLE_ENDIAN, 0}; 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, nullptr, - &ib, /*float_in=*/false, /*align=*/0)); + io.metadata.m.bit_depth.bits_per_sample, format, nullptr, &ib)); 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. std::vector<uint8_t> jpeg_bytes; io.jpeg_quality = 70; - JXL_RETURN_IF_ERROR(jxl::Encode(io, jxl::extras::Codec::kJPG, - io.metadata.m.color_encoding, - /*bits_per_sample=*/8, &jpeg_bytes, - /*pool=*/nullptr)); + JXL_QUIET_RETURN_IF_ERROR(jxl::Encode(io, jxl::extras::Codec::kJPG, + io.metadata.m.color_encoding, + /*bits_per_sample=*/8, &jpeg_bytes, + /*pool=*/nullptr)); JXL_RETURN_IF_ERROR(jxl::jpeg::DecodeImageJPG( - jxl::Span<const uint8_t>(jpeg_bytes.data(), jpeg_bytes.size()), &io)); - jxl::PaddedBytes jpeg_data; + jxl::Bytes(jpeg_bytes.data(), jpeg_bytes.size()), &io)); + std::vector<uint8_t> 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)); + header.insert(header.end(), jxl::kContainerHeader.begin(), + jxl::kContainerHeader.end()); jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false, &header); - header.insert(header.end(), jpeg_data.data(), - jpeg_data.data() + jpeg_data.size()); + jxl::Bytes(jpeg_data).AppendTo(&header); 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; @@ -263,13 +257,11 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec, 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); + std::vector<uint8_t> compressed_image; + bool ok = jxl::test::EncodeFile(params, &io, &passes_encoder_state, + &compressed_image); if (!ok) return false; compressed.append(compressed_image); @@ -284,7 +276,7 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec, } } - if (!jxl::WriteFile(compressed, output_fn)) return 1; + if (!jpegxl::tools::WriteFile(output_fn, compressed)) return 1; if (!quiet) { std::unique_lock<std::mutex> lock(stderr_mutex); std::cerr << "Stored " << output_fn << " size: " << compressed.size() @@ -331,7 +323,7 @@ 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(); + size_t num_threads = std::thread::hardware_concurrency(); for (int optind = 1; optind < argc;) { if (!strcmp(argv[optind], "-r")) { regenerate = true; @@ -410,12 +402,8 @@ int main(int argc, const char** argv) { 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; @@ -439,9 +427,7 @@ int main(int argc, const char** argv) { specs.push_back(spec); } } -#if JPEGXL_ENABLE_JPEG } -#endif // JPEGXL_ENABLE_JPEG } } } @@ -457,15 +443,14 @@ int main(int argc, const char** argv) { 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")) { + jpegxl::tools::ThreadPoolInternal pool{num_threads}; + const auto generate = [&specs, dest_dir, regenerate, quiet]( + const uint32_t task, size_t /* thread */) { + const ImageSpec& spec = specs[task]; + GenerateFile(dest_dir, spec, regenerate, quiet); + }; + if (!RunOnPool(&pool, 0, specs.size(), jxl::ThreadPool::NoInit, generate, + "FuzzerCorpus")) { std::cerr << "Error generating fuzzer corpus" << std::endl; return 1; } diff --git a/tools/djxl_fuzzer_test.cc b/tools/djxl_fuzzer_test.cc index e5b35c9..1b16584 100644 --- a/tools/djxl_fuzzer_test.cc +++ b/tools/djxl_fuzzer_test.cc @@ -3,15 +3,16 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/thread_parallel_runner.h> +#include <jxl/thread_parallel_runner_cxx.h> + +#include <cstdint> #include <sstream> #include <string> #include <vector> -#include "gtest/gtest.h" -#include "jxl/thread_parallel_runner.h" -#include "jxl/thread_parallel_runner_cxx.h" #include "lib/jxl/test_utils.h" -#include "lib/jxl/testdata.h" +#include "lib/jxl/testing.h" extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size); @@ -39,6 +40,6 @@ TEST_P(DjxlFuzzerTest, TestOne) { std::ostringstream os; os << "oss-fuzz/clusterfuzz-testcase-minimized-djxl_fuzzer-" << id; printf("Testing %s\n", os.str().c_str()); - const jxl::PaddedBytes input = jxl::ReadTestData(os.str()); + const std::vector<uint8_t> input = jxl::test::ReadTestData(os.str()); LLVMFuzzerTestOneInput(input.data(), input.size()); } diff --git a/tools/djxl_main.cc b/tools/djxl_main.cc index 44971c0..9abe0b6 100644 --- a/tools/djxl_main.cc +++ b/tools/djxl_main.cc @@ -3,7 +3,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/decode.h> +#include <jxl/thread_parallel_runner.h> +#include <jxl/thread_parallel_runner_cxx.h> +#include <jxl/types.h> + #include <climits> +#include <cmath> #include <cstddef> #include <cstdint> #include <cstdio> @@ -14,13 +20,14 @@ #include <string> #include <vector> -#include "jxl/decode.h" -#include "jxl/thread_parallel_runner.h" -#include "jxl/thread_parallel_runner_cxx.h" -#include "jxl/types.h" +#include "lib/extras/alpha_blend.h" +#include "lib/extras/codec.h" #include "lib/extras/dec/decode.h" #include "lib/extras/dec/jxl.h" +#include "lib/extras/enc/apng.h" #include "lib/extras/enc/encode.h" +#include "lib/extras/enc/exr.h" +#include "lib/extras/enc/jpg.h" #include "lib/extras/enc/pnm.h" #include "lib/extras/packed_image.h" #include "lib/extras/time.h" @@ -37,115 +44,178 @@ struct DecompressArgs { DecompressArgs() = default; void AddCommandLineOptions(CommandLineParser* cmdline) { - cmdline->AddPositionalOption("INPUT", /* required = */ true, - "The compressed input file.", &file_in); - - cmdline->AddPositionalOption("OUTPUT", /* required = */ true, - "The output can be (A)PNG with ICC, JPG, or " - "PPM/PFM.", + std::string output_help("The output format can be "); + if (jxl::extras::GetAPNGEncoder()) { + output_help.append("PNG, APNG, "); + } + if (jxl::extras::GetJPEGEncoder()) { + output_help.append("JPEG, "); + } else { + output_help.append("JPEG (lossless reconstruction only), "); + } + if (jxl::extras::GetEXREncoder()) { + output_help.append("EXR, "); + } + output_help.append( + "PGM (for greyscale input), PPM (for color input), PNM, PFM, or PAM.\n" + " To extract metadata, use output format EXIF, XMP, or JUMBF.\n" + " The format is selected based on extension ('filename.png') or " + "prefix ('png:filename').\n" + " Use '-' for output to stdout (e.g. 'ppm:-')"); + cmdline->AddPositionalOption( + "INPUT", /* required = */ true, + "The compressed input file (JXL). Use '-' for input from stdin.", + &file_in); + + cmdline->AddPositionalOption("OUTPUT", /* required = */ true, output_help, &file_out); + cmdline->AddHelpText("\nBasic options:", 0); + cmdline->AddOptionFlag('V', "version", "Print version number and exit.", - &version, &SetBooleanTrue); + &version, &SetBooleanTrue, 0); + cmdline->AddOptionFlag('\0', "quiet", "Silence output (except for errors).", + &quiet, &SetBooleanTrue, 0); + cmdline->AddOptionFlag('v', "verbose", + "Verbose output; can be repeated and also applies " + "to help (!).", + &verbose, &SetBooleanTrue); - cmdline->AddOptionValue('\0', "num_reps", "N", - "Sets the number of times to decompress the image. " - "Used for benchmarking, the default is 1.", - &num_reps, &ParseUnsigned); + cmdline->AddHelpText("\nAdvanced options:", 1); cmdline->AddOptionValue('\0', "num_threads", "N", - "Sets the number of threads to use. The default 0 " - "value means the machine default.", - &num_threads, &ParseUnsigned); - - cmdline->AddOptionValue('\0', "bits_per_sample", "N", - "Sets the output bit depth. The default 0 value " - "means the original (input) bit depth.", - &bits_per_sample, &ParseUnsigned); + "Number of worker threads (-1 == use machine " + "default, 0 == do not use multithreading).", + &num_threads, &ParseSigned, 1); + + opt_bits_per_sample_id = cmdline->AddOptionValue( + '\0', "bits_per_sample", "N", + "Sets the output bit depth. The value 0 (default for PNM) " + "means the original (input) bit depth.\n" + " The value -1 (default for other codecs) means it depends on the " + "output format capabilities\n" + " and the input bit depth (e.g. decoding a 12-bit image to PNG will " + "produce a 16-bit PNG).", + &bits_per_sample, &ParseSigned, 1); cmdline->AddOptionValue('\0', "display_nits", "N", "If set to a non-zero value, tone maps the image " "the given peak display luminance.", - &display_nits, &ParseDouble); - - cmdline->AddOptionValue('\0', "color_space", "COLORSPACE_DESC", - "Sets the output color space of the image. This " - "flag has no effect if the image is not XYB " - "encoded.", - &color_space, &ParseString); - - cmdline->AddOptionValue('s', "downsampling", "N", - "If set and the input JXL stream is progressive " - "and contains hints for target downsampling " - "ratios, the decoder will skip any progressive " - "passes that are not needed to produce a partially " - "decoded image intended for this downsampling " - "ratio.", - &downsampling, &ParseUint32); + &display_nits, &ParseDouble, 1); + + cmdline->AddOptionValue( + '\0', "color_space", "COLORSPACE_DESC", + "Sets the desired output color space of the image. For example:\n" + " --color_space=RGB_D65_SRG_Per_SRG is sRGB with perceptual " + "rendering intent\n" + " --color_space=RGB_D65_202_Rel_PeQ is Rec.2100 PQ with relative " + "rendering intent", + &color_space, &ParseString, 1); + + cmdline->AddOptionValue('s', "downsampling", "1|2|4|8", + "If the input JXL stream is contains hints for " + "target downsampling ratios,\n" + " only decode what is needed to produce an " + "image intended for this downsampling ratio.", + &downsampling, &ParseUint32, 1); cmdline->AddOptionFlag('\0', "allow_partial_files", "Allow decoding of truncated files.", - &allow_partial_files, &SetBooleanTrue); + &allow_partial_files, &SetBooleanTrue, 1); + + if (jxl::extras::GetJPEGEncoder()) { + cmdline->AddOptionFlag( + 'j', "pixels_to_jpeg", + "By default, if the input JXL is a recompressed JPEG file, " + "djxl reconstructs that JPEG file.\n" + " This flag causes the decoder to instead decode to pixels and " + "encode a new (lossy) JPEG.", + &pixels_to_jpeg, &SetBooleanTrue, 1); + + opt_jpeg_quality_id = cmdline->AddOptionValue( + 'q', "jpeg_quality", "N", + "Sets the JPEG output quality, default is 95. " + "Setting this option implies --pixels_to_jpeg.", + &jpeg_quality, &ParseUnsigned, 1); + } + + cmdline->AddHelpText("\nOptions for experimentation / benchmarking:", 2); + + cmdline->AddOptionValue('\0', "num_reps", "N", + "Sets the number of times to decompress the image. " + "Useful for benchmarking. Default is 1.", + &num_reps, &ParseUnsigned, 2); + + cmdline->AddOptionFlag('\0', "disable_output", + "No output file will be written (for benchmarking)", + &disable_output, &SetBooleanTrue, 2); + + cmdline->AddOptionFlag('\0', "output_extra_channels", + "If set, all extra channels will be written either " + "as part of the main output file (e.g. alpha " + "channel in png) or as separate output files with " + "suffix -ecN in their names. If not set, the " + "(first) alpha channel will only be written when " + "the output format supports alpha channels and all " + "other extra channels won't be decoded. Files are " + "concatenated when outputting to stdout.", + &output_extra_channels, &SetBooleanTrue, 2); -#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.", - &pixels_to_jpeg, &SetBooleanTrue); - - opt_jpeg_quality_id = cmdline->AddOptionValue( - 'q', "jpeg_quality", "N", - "Sets the JPEG output quality, default is 95. Setting an output " - "quality implies --pixels_to_jpeg.", - &jpeg_quality, &ParseUnsigned); -#endif - -#if JPEGXL_ENABLE_SJPEG + '\0', "output_frames", + "If set, all frames will be written either as part of the main output " + "file if that supports animation, or as separate output files with " + "suffix -N in their names. Files are concatenated when outputting to " + "stdout.", + &output_frames, &SetBooleanTrue, 2); + cmdline->AddOptionFlag('\0', "use_sjpeg", "Use sjpeg instead of libjpeg for JPEG output.", - &use_sjpeg, &SetBooleanTrue); -#endif + &use_sjpeg, &SetBooleanTrue, 2); cmdline->AddOptionFlag('\0', "norender_spotcolors", - "Disables rendering spot colors.", - &render_spotcolors, &SetBooleanFalse); + "Disables rendering of spot colors.", + &render_spotcolors, &SetBooleanFalse, 2); cmdline->AddOptionValue('\0', "preview_out", "FILENAME", "If specified, writes the preview image to this " "file.", - &preview_out, &ParseString); + &preview_out, &ParseString, 2); cmdline->AddOptionValue( '\0', "icc_out", "FILENAME", "If specified, writes the ICC profile of the decoded image to " "this file.", - &icc_out, &ParseString); + &icc_out, &ParseString, 2); cmdline->AddOptionValue( '\0', "orig_icc_out", "FILENAME", "If specified, writes the ICC profile of the original image to " - "this file. This can be different from the ICC profile of the " - "decoded image if --color_space was specified, or if the image " - "was XYB encoded and the color conversion to the original " - "profile was not supported by the decoder.", - &orig_icc_out, &ParseString); - - cmdline->AddOptionValue( - '\0', "metadata_out", "FILENAME", - "If specified, writes decoded metadata info to this file in " - "JSON format. Used by the conformance test script", - &metadata_out, &ParseString); + "this file\n" + " This can be different from the ICC profile of the " + "decoded image if --color_space was specified.", + &orig_icc_out, &ParseString, 2); + + cmdline->AddOptionValue('\0', "metadata_out", "FILENAME", + "If specified, writes metadata info to a JSON " + "file. Used by the conformance test script", + &metadata_out, &ParseString, 2); + + cmdline->AddOptionValue('\0', "background", "#NNNNNN", + "Specifies the background color for the " + "--alpha_blend option. Recognized values are " + "'black', 'white' (default), or '#NNNNNN'", + &background_spec, &ParseString, 2); + + cmdline->AddOptionFlag('\0', "alpha_blend", + "Blends alpha channel with the color image using " + "background color specified by --background " + "(default is white).", + &alpha_blend, &SetBooleanTrue, 2); 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); + &print_read_bytes, &SetBooleanTrue, 2); } // Validate the passed arguments, checking whether all passed options are @@ -155,15 +225,23 @@ struct DecompressArgs { fprintf(stderr, "Missing INPUT filename.\n"); return false; } + if (num_threads < -1) { + fprintf( + stderr, + "Invalid flag value for --num_threads: must be -1, 0 or positive.\n"); + return false; + } return true; } const char* file_in = nullptr; const char* file_out = nullptr; bool version = false; + bool verbose = false; size_t num_reps = 1; - size_t num_threads = 0; - size_t bits_per_sample = 0; + bool disable_output = false; + int32_t num_threads = -1; + int bits_per_sample = -1; double display_nits = 0.0; std::string color_space; uint32_t downsampling = 0; @@ -172,13 +250,18 @@ struct DecompressArgs { size_t jpeg_quality = 95; bool use_sjpeg = false; bool render_spotcolors = true; + bool output_extra_channels = false; + bool output_frames = false; std::string preview_out; std::string icc_out; std::string orig_icc_out; std::string metadata_out; + std::string background_spec = "white"; + bool alpha_blend = false; bool print_read_bytes = false; bool quiet = false; // References (ids) of specific options to check if they were matched. + CommandLineParser::OptionId opt_bits_per_sample_id = -1; CommandLineParser::OptionId opt_jpeg_quality_id = -1; }; @@ -192,20 +275,21 @@ bool WriteOptionalOutput(const std::string& filename, if (filename.empty() || bytes.empty()) { return true; } - return jpegxl::tools::WriteFile(filename.data(), bytes); + return jpegxl::tools::WriteFile(filename, bytes); } -std::string Filename(const std::string& base, const std::string& extension, +std::string Filename(const std::string& filename, const std::string& extension, int layer_index, int frame_index, int num_layers, int num_frames) { + if (filename == "-") return "-"; auto digits = [](int n) { return 1 + static_cast<int>(std::log10(n)); }; - std::string out = base; + std::string out = filename; if (num_frames > 1) { std::vector<char> buf(2 + digits(num_frames)); snprintf(buf.data(), buf.size(), "-%0*d", digits(num_frames), frame_index); out.append(buf.data()); } - if (num_layers > 1) { + if (num_layers > 1 && layer_index > 0) { std::vector<char> buf(4 + digits(num_layers)); snprintf(buf.data(), buf.size(), "-ec%0*d", digits(num_layers), layer_index); @@ -213,12 +297,49 @@ std::string Filename(const std::string& base, const std::string& extension, } if (extension == ".ppm" && layer_index > 0) { out.append(".pgm"); - } else { + } else if ((num_frames > 1) || (num_layers > 1 && layer_index > 0)) { out.append(extension); } return out; } +void AddFormatsWithAlphaChannel(std::vector<JxlPixelFormat>* formats) { + auto add_format = [&](JxlPixelFormat format) { + for (auto f : *formats) { + if (memcmp(&f, &format, sizeof(format)) == 0) return; + } + formats->push_back(format); + }; + size_t num_formats = formats->size(); + for (size_t i = 0; i < num_formats; ++i) { + JxlPixelFormat format = (*formats)[i]; + if (format.num_channels == 1 || format.num_channels == 3) { + ++format.num_channels; + add_format(format); + } + } +} + +bool ParseBackgroundColor(const std::string& background_desc, + float background[3]) { + if (background_desc == "black") { + background[0] = background[1] = background[2] = 0.0f; + return true; + } + if (background_desc == "white") { + background[0] = background[1] = background[2] = 1.0f; + return true; + } + if (background_desc.size() != 7 || background_desc[0] != '#') { + return false; + } + uint32_t color = std::stoi(background_desc.substr(1), nullptr, 16); + background[0] = ((color >> 16) & 0xff) * (1.0f / 255); + background[1] = ((color >> 8) & 0xff) * (1.0f / 255); + background[2] = (color & 0xff) * (1.0f / 255); + return true; +} + bool DecompressJxlReconstructJPEG(const jpegxl::tools::DecompressArgs& args, const std::vector<uint8_t>& compressed, void* runner, @@ -227,6 +348,7 @@ bool DecompressJxlReconstructJPEG(const jpegxl::tools::DecompressArgs& args, const double t0 = jxl::Now(); jxl::extras::PackedPixelFile ppf; // for JxlBasicInfo jxl::extras::JXLDecompressParams dparams; + dparams.allow_partial_input = args.allow_partial_files; dparams.runner = JxlThreadParallelRunner; dparams.runner_opaque = runner; if (!jxl::extras::DecodeImageJXL(compressed.data(), compressed.size(), @@ -257,6 +379,13 @@ bool DecompressJxlToPackedPixelFile( dparams.runner = JxlThreadParallelRunner; dparams.runner_opaque = runner; dparams.allow_partial_input = args.allow_partial_files; + dparams.need_icc = !args.icc_out.empty(); + if (args.bits_per_sample == 0) { + dparams.output_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM; + } else if (args.bits_per_sample > 0) { + dparams.output_bitdepth.type = JXL_BIT_DEPTH_CUSTOM; + dparams.output_bitdepth.bits_per_sample = args.bits_per_sample; + } const double t0 = jxl::Now(); if (!jxl::extras::DecodeImageJXL(compressed.data(), compressed.size(), dparams, decoded_bytes, ppf)) { @@ -293,7 +422,7 @@ int main(int argc, const char* argv[]) { fprintf(stderr, "JPEG XL decoder %s\n", version.c_str()); } - if (cmdline.HelpFlagPassed()) { + if (cmdline.HelpFlagPassed() || !args.file_in) { cmdline.PrintHelp(); return EXIT_SUCCESS; } @@ -311,29 +440,31 @@ int main(int argc, const char* argv[]) { return EXIT_FAILURE; } if (!args.quiet) { - fprintf(stderr, "Read %" PRIuS " compressed bytes.\n", compressed.size()); + cmdline.VerbosePrintf(1, "Read %" PRIuS " compressed bytes.\n", + compressed.size()); } - if (!args.file_out && !args.quiet) { + if (!args.file_out && !args.disable_output) { + std::cerr + << "No output file specified and --disable_output flag not passed." + << std::endl; + return EXIT_FAILURE; + } + + if (args.file_out && args.disable_output && !args.quiet) { fprintf(stderr, - "No output file specified.\n" "Decoding will be performed, but the result will be discarded.\n"); } std::string filename_out; - std::string base; + std::string filename; std::string extension; - if (args.file_out) { + jxl::extras::Codec codec = jxl::extras::Codec::kUnknown; + if (args.file_out && !args.disable_output) { filename_out = std::string(args.file_out); - size_t pos = filename_out.find_last_of('.'); - if (pos < filename_out.size()) { - base = filename_out.substr(0, pos); - extension = filename_out.substr(pos); - } else { - base = filename_out; - } + codec = jxl::extras::CodecFromPath( + filename_out, /* bits_per_sample */ nullptr, &filename, &extension); } - const jxl::extras::Codec codec = jxl::extras::CodecFromExtension(extension); if (codec == jxl::extras::Codec::kEXR) { std::string force_colorspace = "RGB_D65_SRG_Rel_Lin"; if (!args.color_space.empty() && args.color_space != force_colorspace) { @@ -341,12 +472,17 @@ int main(int argc, const char* argv[]) { } args.color_space = force_colorspace; } + if (codec == jxl::extras::Codec::kPNM && extension != ".pfm" && + (args.opt_jpeg_quality_id < 0 || + !cmdline.GetOption(args.opt_jpeg_quality_id)->matched())) { + args.bits_per_sample = 0; + } jpegxl::tools::SpeedStats stats; size_t num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads(); { int64_t flag_num_worker_threads = args.num_threads; - if (flag_num_worker_threads != 0) { + if (flag_num_worker_threads > -1) { num_worker_threads = flag_num_worker_threads; } } @@ -354,12 +490,11 @@ int main(int argc, const char* argv[]) { /*memory_manager=*/nullptr, num_worker_threads); bool decode_to_pixels = (codec != jxl::extras::Codec::kJPG); -#if JPEGXL_ENABLE_JPEG - if (args.pixels_to_jpeg || - cmdline.GetOption(args.opt_jpeg_quality_id)->matched()) { + if (args.opt_jpeg_quality_id >= 0 && + (args.pixels_to_jpeg || + cmdline.GetOption(args.opt_jpeg_quality_id)->matched())) { decode_to_pixels = true; } -#endif size_t num_reps = args.num_reps; if (!decode_to_pixels) { @@ -380,9 +515,8 @@ int main(int argc, const char* argv[]) { } } if (!bytes.empty()) { - if (!args.quiet) fprintf(stderr, "Reconstructed to JPEG.\n"); - if (!filename_out.empty() && - !jpegxl::tools::WriteFile(filename_out.c_str(), bytes)) { + if (!args.quiet) cmdline.VerbosePrintf(0, "Reconstructed to JPEG.\n"); + if (!filename_out.empty() && !jpegxl::tools::WriteFile(filename, bytes)) { return EXIT_FAILURE; } } @@ -398,6 +532,9 @@ int main(int argc, const char* argv[]) { return EXIT_FAILURE; } accepted_formats = encoder->AcceptedFormats(); + if (args.alpha_blend) { + AddFormatsWithAlphaChannel(&accepted_formats); + } } jxl::extras::PackedPixelFile ppf; size_t decoded_bytes = 0; @@ -409,57 +546,63 @@ int main(int argc, const char* argv[]) { return EXIT_FAILURE; } } - if (!args.quiet) fprintf(stderr, "Decoded to pixels.\n"); + if (!args.quiet) cmdline.VerbosePrintf(0, "Decoded to pixels.\n"); if (args.print_read_bytes) { fprintf(stderr, "Decoded bytes: %" PRIuS "\n", decoded_bytes); } - if (extension == ".pfm") { - ppf.info.bits_per_sample = 32; - } else if (args.bits_per_sample > 0) { - ppf.info.bits_per_sample = args.bits_per_sample; - } -#if JPEGXL_ENABLE_JPEG + // When --disable_output was parsed, `filename_out` is empty and we don't + // need to write files. if (encoder) { + if (args.alpha_blend) { + float background[3]; + if (!ParseBackgroundColor(args.background_spec, background)) { + fprintf(stderr, "Invalid background color %s\n", + args.background_spec.c_str()); + } + AlphaBlend(&ppf, background); + } std::ostringstream os; os << args.jpeg_quality; encoder->SetOption("q", os.str()); - } -#endif -#if JPEGXL_ENABLE_SJPEG - if (encoder && args.use_sjpeg) { - encoder->SetOption("jpeg_encoder", "sjpeg"); - } -#endif - jxl::extras::EncodedImage encoded_image; - if (encoder) { + if (args.use_sjpeg) { + encoder->SetOption("jpeg_encoder", "sjpeg"); + } + jxl::extras::EncodedImage encoded_image; + if (!args.quiet) cmdline.VerbosePrintf(2, "Encoding decoded image\n"); if (!encoder->Encode(ppf, &encoded_image)) { fprintf(stderr, "Encode failed\n"); return EXIT_FAILURE; } - } - size_t nlayers = 1 + encoded_image.extra_channel_bitstreams.size(); - size_t nframes = encoded_image.bitstreams.size(); - for (size_t i = 0; i < nlayers; ++i) { - for (size_t j = 0; j < nframes; ++j) { - const std::vector<uint8_t>& bitstream = - (i == 0 ? encoded_image.bitstreams[j] - : encoded_image.extra_channel_bitstreams[i - 1][j]); - std::string fn = Filename(base, extension, i, j, nlayers, nframes); - if (!jpegxl::tools::WriteFile(fn.c_str(), bitstream)) { - return EXIT_FAILURE; + size_t nlayers = args.output_extra_channels + ? 1 + encoded_image.extra_channel_bitstreams.size() + : 1; + size_t nframes = args.output_frames ? encoded_image.bitstreams.size() : 1; + for (size_t i = 0; i < nlayers; ++i) { + for (size_t j = 0; j < nframes; ++j) { + const std::vector<uint8_t>& bitstream = + (i == 0 ? encoded_image.bitstreams[j] + : encoded_image.extra_channel_bitstreams[i - 1][j]); + std::string fn = + Filename(filename, extension, i, j, nlayers, nframes); + if (!jpegxl::tools::WriteFile(fn.c_str(), bitstream)) { + return EXIT_FAILURE; + } + if (!args.quiet) + cmdline.VerbosePrintf(1, "Wrote output to %s\n", fn.c_str()); } } - } - if (!WriteOptionalOutput(args.preview_out, - encoded_image.preview_bitstream) || - !WriteOptionalOutput(args.icc_out, ppf.icc) || - !WriteOptionalOutput(args.orig_icc_out, ppf.orig_icc) || - !WriteOptionalOutput(args.metadata_out, encoded_image.metadata)) { - return EXIT_FAILURE; + if (!WriteOptionalOutput(args.preview_out, + encoded_image.preview_bitstream) || + !WriteOptionalOutput(args.icc_out, ppf.icc) || + !WriteOptionalOutput(args.orig_icc_out, ppf.orig_icc) || + !WriteOptionalOutput(args.metadata_out, encoded_image.metadata)) { + return EXIT_FAILURE; + } } } if (!args.quiet) { stats.Print(num_worker_threads); } + return EXIT_SUCCESS; } diff --git a/tools/fast_lossless/.gitignore b/tools/fast_lossless/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/tools/fast_lossless/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/tools/fast_lossless/README.md b/tools/fast_lossless/README.md new file mode 100644 index 0000000..5f99c13 --- /dev/null +++ b/tools/fast_lossless/README.md @@ -0,0 +1,10 @@ +# Fast-lossless +This is a script to compile a standalone version of a JXL encoder that supports +lossless compression, up to 16 bits, of 1- to 4-channel images and animations; it is +very fast and compression is slightly worse than PNG for 8-bit nonphoto content +and better or much better than PNG for all other situations. + +The main encoder is made out of two files, `lib/jxl/enc_fast_lossless.{cc,h}`; +it automatically selects and runs a SIMD implementation supported by your CPU. + +This folder contains an example build script and `main` file. diff --git a/tools/fast_lossless/build-android.sh b/tools/fast_lossless/build-android.sh new file mode 100755 index 0000000..c155b21 --- /dev/null +++ b/tools/fast_lossless/build-android.sh @@ -0,0 +1,27 @@ +#!/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 -e + +DIR=$(realpath "$(dirname "$0")") + +mkdir -p /tmp/build-android +cd /tmp/build-android + +CXX="$ANDROID_NDK"/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang++ +if ! command -v "$CXX" >/dev/null ; then + printf >&2 '%s: Android C++ compiler not found, is ANDROID_NDK set properly?\n' "${0##*/}" + exit 1 +fi + +[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp' +[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h' +[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c + +"$CXX" -O3 \ + -I. lodepng.o \ + -I"${DIR}"/../../ \ + "${DIR}"/../../lib/jxl/enc_fast_lossless.cc "${DIR}"/fast_lossless_main.cc \ + -o fast_lossless diff --git a/tools/fast_lossless/build.sh b/tools/fast_lossless/build.sh new file mode 100755 index 0000000..e2c0aa3 --- /dev/null +++ b/tools/fast_lossless/build.sh @@ -0,0 +1,27 @@ +#!/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 -e + +DIR=$(realpath "$(dirname "$0")") +mkdir -p "$DIR"/build +cd "$DIR"/build + +# set CXX to clang++ if not set in the environment +CXX="${CXX-clang++}" +if ! command -v "$CXX" >/dev/null ; then + printf >&2 '%s: C++ compiler not found\n' "${0##*/}" + exit 1 +fi + +[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp' +[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h' +[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c + +"$CXX" -O3 \ + -I. -g lodepng.o \ + -I"$DIR"/../../ \ + "$DIR"/../../lib/jxl/enc_fast_lossless.cc "$DIR"/fast_lossless_main.cc \ + -o fast_lossless diff --git a/tools/fast_lossless/cross_compile_aarch64.sh b/tools/fast_lossless/cross_compile_aarch64.sh new file mode 100755 index 0000000..a5e6aa2 --- /dev/null +++ b/tools/fast_lossless/cross_compile_aarch64.sh @@ -0,0 +1,26 @@ +#!/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 -e + +DIR=$(realpath "$(dirname "$0")") +mkdir -p "$DIR"/build-aarch64 +cd "$DIR"/build-aarch64 + +CXX="${CXX-aarch64-linux-gnu-c++}" +if ! command -v "$CXX" >/dev/null ; then + printf >&2 '%s: C++ compiler not found\n' "${0##*/}" + exit 1 +fi + +[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp' +[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h' +[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c + +"$CXX" -O3 -static \ + -I. lodepng.o \ + -I"$DIR"/../../ \ + "$DIR"/../../lib/jxl/enc_fast_lossless.cc "$DIR"/fast_lossless_main.cc \ + -o fast_lossless diff --git a/tools/fast_lossless/fast_lossless_main.cc b/tools/fast_lossless/fast_lossless_main.cc new file mode 100644 index 0000000..b59051d --- /dev/null +++ b/tools/fast_lossless/fast_lossless_main.cc @@ -0,0 +1,113 @@ +// 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 <string.h> + +#include <atomic> +#include <chrono> +#include <thread> +#include <vector> + +#include "lib/jxl/enc_fast_lossless.h" +#include "lodepng.h" +#include "pam-input.h" + +int main(int argc, char** argv) { + if (argc < 3) { + fprintf(stderr, + "Usage: %s in.png out.jxl [effort] [num_reps] [num_threads]\n", + argv[0]); + return 1; + } + + const char* in = argv[1]; + const char* out = argv[2]; + int effort = argc >= 4 ? atoi(argv[3]) : 2; + size_t num_reps = argc >= 5 ? atoi(argv[4]) : 1; + size_t num_threads = argc >= 6 ? atoi(argv[5]) : 0; + + if (effort < 0 || effort > 127) { + fprintf( + stderr, + "Effort should be between 0 and 127 (default is 2, more is slower)\n"); + return 1; + } + + unsigned char* png; + unsigned w, h; + size_t nb_chans = 4, bitdepth = 8; + + unsigned error = lodepng_decode32_file(&png, &w, &h, in); + + size_t width = w, height = h; + if (error && !DecodePAM(in, &png, &width, &height, &nb_chans, &bitdepth)) { + fprintf(stderr, "lodepng error %u: %s\n", error, lodepng_error_text(error)); + return 1; + } + + auto parallel_runner = [](void* num_threads_ptr, void* opaque, + void fun(void*, size_t), size_t count) { + size_t num_threads = *(size_t*)num_threads_ptr; + if (num_threads == 0) { + num_threads = std::thread::hardware_concurrency(); + } + if (num_threads > count) { + num_threads = count; + } + if (num_threads == 1) { + for (size_t i = 0; i < count; i++) { + fun(opaque, i); + } + } else { + std::atomic<int> task{0}; + std::vector<std::thread> threads; + for (size_t i = 0; i < num_threads; i++) { + threads.push_back(std::thread([count, opaque, fun, &task]() { + while (true) { + int t = task++; + if (t >= count) break; + fun(opaque, t); + } + })); + } + for (auto& t : threads) t.join(); + } + }; + + size_t encoded_size = 0; + unsigned char* encoded = nullptr; + size_t stride = width * nb_chans * (bitdepth > 8 ? 2 : 1); + + auto start = std::chrono::high_resolution_clock::now(); + for (size_t _ = 0; _ < num_reps; _++) { + free(encoded); + encoded_size = JxlFastLosslessEncode( + png, width, stride, height, nb_chans, bitdepth, + /*big_endian=*/true, effort, &encoded, &num_threads, +parallel_runner); + } + auto stop = std::chrono::high_resolution_clock::now(); + if (num_reps > 1) { + float us = + std::chrono::duration_cast<std::chrono::microseconds>(stop - start) + .count(); + size_t pixels = size_t{width} * size_t{height} * num_reps; + float mps = pixels / us; + fprintf(stderr, "%10.3f MP/s\n", mps); + fprintf(stderr, "%10.3f bits/pixel\n", + encoded_size * 8.0 / float(width) / float(height)); + } + + FILE* o = fopen(out, "wb"); + if (!o) { + fprintf(stderr, "error opening %s: %s\n", out, strerror(errno)); + return 1; + } + if (fwrite(encoded, 1, encoded_size, o) != encoded_size) { + fprintf(stderr, "error writing to %s: %s\n", out, strerror(errno)); + } + fclose(o); +} diff --git a/tools/fast_lossless/pam-input.h b/tools/fast_lossless/pam-input.h new file mode 100644 index 0000000..b5a0233 --- /dev/null +++ b/tools/fast_lossless/pam-input.h @@ -0,0 +1,292 @@ +// 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 <stdlib.h> +#include <string.h> + +bool error_msg(const char* message) { + fprintf(stderr, "%s\n", message); + return false; +} +#define return_on_error(X) \ + if (!X) return false; + +size_t Log2(uint32_t value) { return 31 - __builtin_clz(value); } + +struct HeaderPNM { + size_t xsize; + size_t ysize; + bool is_gray; // PGM + bool has_alpha; // PAM + size_t bits_per_sample; +}; + +class Parser { + public: + explicit Parser(uint8_t* data, size_t length) + : pos_(data), end_(data + length) {} + + // Sets "pos" to the first non-header byte/pixel on success. + bool ParseHeader(HeaderPNM* header, const uint8_t** pos) { + // codec.cc ensures we have at least two bytes => no range check here. + if (pos_[0] != 'P') return false; + const uint8_t type = pos_[1]; + pos_ += 2; + + switch (type) { + case '5': + header->is_gray = true; + return ParseHeaderPNM(header, pos); + + case '6': + header->is_gray = false; + return ParseHeaderPNM(header, pos); + + case '7': + return ParseHeaderPAM(header, pos); + } + return false; + } + + // Exposed for testing + bool ParseUnsigned(size_t* number) { + if (pos_ == end_) return error_msg("PNM: reached end before number"); + if (!IsDigit(*pos_)) return error_msg("PNM: expected unsigned number"); + + *number = 0; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number *= 10; + *number += *pos_ - '0'; + ++pos_; + } + + return true; + } + + bool ParseSigned(double* number) { + if (pos_ == end_) return error_msg("PNM: reached end before signed"); + + if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) { + return error_msg("PNM: expected signed number"); + } + + // Skip sign + const bool is_neg = *pos_ == '-'; + if (is_neg || *pos_ == '+') { + ++pos_; + if (pos_ == end_) return error_msg("PNM: reached end before digits"); + } + + // Leading digits + *number = 0.0; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number *= 10; + *number += *pos_ - '0'; + ++pos_; + } + + // Decimal places? + if (pos_ < end_ && *pos_ == '.') { + ++pos_; + double place = 0.1; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number += (*pos_ - '0') * place; + place *= 0.1; + ++pos_; + } + } + + if (is_neg) *number = -*number; + return true; + } + + private: + static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; } + static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; } + static bool IsWhitespace(const uint8_t c) { + return IsLineBreak(c) || c == '\t' || c == ' '; + } + + bool SkipBlank() { + if (pos_ == end_) return error_msg("PNM: reached end before blank"); + const uint8_t c = *pos_; + if (c != ' ' && c != '\n') return error_msg("PNM: expected blank"); + ++pos_; + return true; + } + + bool SkipSingleWhitespace() { + if (pos_ == end_) return error_msg("PNM: reached end before whitespace"); + if (!IsWhitespace(*pos_)) return error_msg("PNM: expected whitespace"); + ++pos_; + return true; + } + + bool SkipWhitespace() { + if (pos_ == end_) return error_msg("PNM: reached end before whitespace"); + if (!IsWhitespace(*pos_) && *pos_ != '#') { + return error_msg("PNM: expected whitespace/comment"); + } + + while (pos_ < end_ && IsWhitespace(*pos_)) { + ++pos_; + } + + // Comment(s) + while (pos_ != end_ && *pos_ == '#') { + while (pos_ != end_ && !IsLineBreak(*pos_)) { + ++pos_; + } + // Newline(s) + while (pos_ != end_ && IsLineBreak(*pos_)) pos_++; + } + + while (pos_ < end_ && IsWhitespace(*pos_)) { + ++pos_; + } + return true; + } + + bool MatchString(const char* keyword) { + const uint8_t* ppos = pos_; + while (*keyword) { + if (ppos >= end_) return error_msg("PAM: unexpected end of input"); + if (*keyword != *ppos) return false; + ppos++; + keyword++; + } + pos_ = ppos; + return_on_error(SkipWhitespace()); + return true; + } + + bool ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) { + size_t num_channels = 3; + size_t max_val = 255; + while (!MatchString("ENDHDR")) { + return_on_error(SkipWhitespace()); + if (MatchString("WIDTH")) { + return_on_error(ParseUnsigned(&header->xsize)); + } else if (MatchString("HEIGHT")) { + return_on_error(ParseUnsigned(&header->ysize)); + } else if (MatchString("DEPTH")) { + return_on_error(ParseUnsigned(&num_channels)); + } else if (MatchString("MAXVAL")) { + return_on_error(ParseUnsigned(&max_val)); + } else if (MatchString("TUPLTYPE")) { + if (MatchString("RGB_ALPHA")) { + header->has_alpha = true; + } else if (MatchString("RGB")) { + } else if (MatchString("GRAYSCALE_ALPHA")) { + header->has_alpha = true; + header->is_gray = true; + } else if (MatchString("GRAYSCALE")) { + header->is_gray = true; + } else if (MatchString("BLACKANDWHITE_ALPHA")) { + header->has_alpha = true; + header->is_gray = true; + max_val = 1; + } else if (MatchString("BLACKANDWHITE")) { + header->is_gray = true; + max_val = 1; + } else { + return error_msg("PAM: unknown TUPLTYPE"); + } + } else { + return error_msg("PAM: unknown header keyword"); + } + } + if (num_channels != + (header->has_alpha ? 1 : 0) + (header->is_gray ? 1 : 3)) { + return error_msg("PAM: bad DEPTH"); + } + if (max_val == 0 || max_val >= 65536) { + return error_msg("PAM: bad MAXVAL"); + } + header->bits_per_sample = Log2(max_val + 1); + + *pos = pos_; + return true; + } + + bool ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) { + return_on_error(SkipWhitespace()); + return_on_error(ParseUnsigned(&header->xsize)); + + return_on_error(SkipWhitespace()); + return_on_error(ParseUnsigned(&header->ysize)); + + return_on_error(SkipWhitespace()); + size_t max_val; + return_on_error(ParseUnsigned(&max_val)); + if (max_val == 0 || max_val >= 65536) { + return error_msg("PNM: bad MaxVal"); + } + header->bits_per_sample = Log2(max_val + 1); + + return_on_error(SkipSingleWhitespace()); + + *pos = pos_; + return true; + } + + const uint8_t* pos_; + const uint8_t* const end_; +}; + +bool load_file(unsigned char** out, size_t* outsize, const char* filename) { + FILE* file; + file = fopen(filename, "rb"); + if (!file) return false; + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return false; + } + *outsize = ftell(file); + if (*outsize == LONG_MAX || *outsize < 9 || fseek(file, 0, SEEK_SET)) { + fclose(file); + return false; + } + *out = (unsigned char*)malloc(*outsize); + if (!(*out)) { + fclose(file); + return false; + } + size_t readsize; + readsize = fread(*out, 1, *outsize, file); + fclose(file); + if (readsize != *outsize) return false; + return true; +} + +bool DecodePAM(const char* filename, uint8_t** buffer, size_t* w, size_t* h, + size_t* nb_chans, size_t* bitdepth) { + unsigned char* in_file; + size_t in_size; + if (!load_file(&in_file, &in_size, filename)) + return error_msg("Could not read input file"); + Parser parser(in_file, in_size); + HeaderPNM header = {}; + const uint8_t* pos = nullptr; + if (!parser.ParseHeader(&header, &pos)) return false; + + if (header.bits_per_sample == 0 || header.bits_per_sample > 16) { + return error_msg("PNM: bits_per_sample invalid (can do at most 16-bit)"); + } + *w = header.xsize; + *h = header.ysize; + *bitdepth = header.bits_per_sample; + *nb_chans = (header.is_gray ? 1 : 3) + (header.has_alpha ? 1 : 0); + + size_t pnm_remaining_size = in_file + in_size - pos; + size_t buffer_size = *w * *h * *nb_chans * (*bitdepth > 8 ? 2 : 1); + if (pnm_remaining_size < buffer_size) { + return error_msg("PNM file too small"); + } + *buffer = (uint8_t*)malloc(buffer_size); + memcpy(*buffer, pos, buffer_size); + return true; +} diff --git a/tools/fields_fuzzer.cc b/tools/fields_fuzzer.cc index 87e1439..09ea89c 100644 --- a/tools/fields_fuzzer.cc +++ b/tools/fields_fuzzer.cc @@ -5,6 +5,7 @@ #include <stdint.h> +#include "lib/jxl/dec_ans.h" #include "lib/jxl/dec_bit_reader.h" #include "lib/jxl/frame_header.h" #include "lib/jxl/headers.h" @@ -15,7 +16,15 @@ #include "lib/jxl/modular/encoding/encoding.h" #include "lib/jxl/modular/transform/transform.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::BitReader; +using ::jxl::Bytes; +using ::jxl::CodecMetadata; +using ::jxl::CustomTransformData; +using ::jxl::ImageMetadata; +using ::jxl::SizeHeader; int TestOneInput(const uint8_t* data, size_t size) { // Global parameters used by some headers. @@ -23,23 +32,23 @@ int TestOneInput(const uint8_t* data, size_t size) { // First byte controls which header to parse. if (size == 0) return 0; - BitReader reader(Span<const uint8_t>(data + 1, size - 1)); + BitReader reader(Bytes(data + 1, size - 1)); #define FUZZER_CASE_HEADER(number, classname, ...) \ case number: { \ - classname header{__VA_ARGS__}; \ - (void)Bundle::Read(&reader, &header); \ + ::jxl::classname header{__VA_ARGS__}; \ + (void)jxl::Bundle::Read(&reader, &header); \ break; \ } switch (data[0]) { case 0: { SizeHeader size_header; - (void)ReadSizeHeader(&reader, &size_header); + (void)jxl::ReadSizeHeader(&reader, &size_header); break; } case 1: { ImageMetadata metadata; - (void)ReadImageMetadata(&reader, &metadata); + (void)jxl::ReadImageMetadata(&reader, &metadata); break; } @@ -69,7 +78,7 @@ int TestOneInput(const uint8_t* data, size_t size) { default: { CustomTransformData transform_data; transform_data.nonserialized_xyb_encoded = true; - (void)Bundle::Read(&reader, &transform_data); + (void)jxl::Bundle::Read(&reader, &transform_data); break; } } @@ -78,8 +87,9 @@ int TestOneInput(const uint8_t* data, size_t size) { return 0; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - return jxl::TestOneInput(data, size); + return jpegxl::tools::TestOneInput(data, size); } diff --git a/tools/file_io.cc b/tools/file_io.cc deleted file mode 100644 index bc7f3b1..0000000 --- a/tools/file_io.cc +++ /dev/null @@ -1,75 +0,0 @@ -// 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/file_io.h" - -#include <errno.h> -#include <limits.h> -#include <stdio.h> -#include <string.h> - -namespace jpegxl { -namespace tools { - -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; -} - -} // namespace tools -} // namespace jpegxl diff --git a/tools/file_io.h b/tools/file_io.h index 959b79d..7d9f15d 100644 --- a/tools/file_io.h +++ b/tools/file_io.h @@ -6,16 +6,146 @@ #ifndef TOOLS_FILE_IO_H_ #define TOOLS_FILE_IO_H_ +#include <errno.h> +#include <limits.h> #include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <list> +#include <string> #include <vector> +#include "lib/jxl/base/compiler_specific.h" + namespace jpegxl { namespace tools { -bool ReadFile(const char* filename, std::vector<uint8_t>* out); +namespace { + +// RAII, ensures files are closed even when returning early. +class FileWrapper { + public: + FileWrapper(const FileWrapper& other) = delete; + FileWrapper& operator=(const FileWrapper& other) = delete; + + explicit FileWrapper(const std::string& pathname, const char* mode) + : file_(pathname == "-" ? (mode[0] == 'r' ? stdin : stdout) + : fopen(pathname.c_str(), mode)), + close_on_delete_(pathname != "-") { +#ifdef _WIN32 + struct __stat64 s = {}; + const int err = _stat64(pathname.c_str(), &s); + const bool is_file = (s.st_mode & S_IFREG) != 0; +#else + struct stat s = {}; + const int err = stat(pathname.c_str(), &s); + const bool is_file = S_ISREG(s.st_mode); +#endif + if (err == 0 && is_file) { + size_ = s.st_size; + } + } + + ~FileWrapper() { + if (file_ != nullptr && close_on_delete_) { + const int err = fclose(file_); + if (err) { + fprintf(stderr, + "Could not close file\n" + "Error: %s", + strerror(errno)); + } + } + } + + // We intend to use FileWrapper as a replacement of FILE. + // NOLINTNEXTLINE(google-explicit-constructor) + operator FILE*() const { return file_; } + + int64_t size() { return size_; } + + private: + FILE* const file_; + bool close_on_delete_ = true; + int64_t size_ = -1; +}; + +} // namespace + +template <typename ContainerType> +static inline bool ReadFile(FileWrapper& f, ContainerType* JXL_RESTRICT bytes) { + if (!f) return false; + + // Get size of file in bytes + const int64_t size = f.size(); + if (size < 0) { + // Size is unknown, loop reading chunks until EOF. + bytes->clear(); + std::list<std::vector<uint8_t>> chunks; + + size_t total_size = 0; + while (true) { + std::vector<uint8_t> chunk(16 * 1024); + const size_t bytes_read = fread(chunk.data(), 1, chunk.size(), f); + if (ferror(f) || bytes_read > chunk.size()) { + return false; + } + + chunk.resize(bytes_read); + total_size += bytes_read; + if (bytes_read != 0) { + chunks.emplace_back(std::move(chunk)); + } + if (feof(f)) { + break; + } + } + bytes->resize(total_size); + size_t pos = 0; + for (const auto& chunk : chunks) { + memcpy(bytes->data() + pos, chunk.data(), chunk.size()); + pos += chunk.size(); + } + } else { + // Size is known, read the file directly. + bytes->resize(static_cast<size_t>(size)); + + const size_t bytes_read = fread(bytes->data(), 1, bytes->size(), f); + if (bytes_read != static_cast<size_t>(size)) return false; + } + + return true; +} + +template <typename ContainerType> +static inline bool ReadFile(const std::string& filename, + ContainerType* JXL_RESTRICT bytes) { + FileWrapper f(filename, "rb"); + return ReadFile(f, bytes); +} -bool WriteFile(const char* filename, const std::vector<uint8_t>& bytes); +template <typename ContainerType> +static inline bool WriteFile(const std::string& filename, + const ContainerType& bytes) { + FileWrapper file(filename, "wb"); + if (!file) { + fprintf(stderr, + "Could not open %s for writing\n" + "Error: %s", + filename.c_str(), 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; + } + return true; +} } // namespace tools } // namespace jpegxl diff --git a/tools/flicker_test/CMakeLists.txt b/tools/flicker_test/CMakeLists.txt index efa4716..427a34f 100644 --- a/tools/flicker_test/CMakeLists.txt +++ b/tools/flicker_test/CMakeLists.txt @@ -3,9 +3,9 @@ # 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.") +find_package(Qt6 QUIET COMPONENTS Widgets) +if (NOT Qt6_FOUND) + message(WARNING "Qt6 was not found. The flicker test tool will not be built.") return() endif () @@ -32,7 +32,7 @@ add_executable(flicker_test WIN32 test_window.ui) target_link_libraries(flicker_test PUBLIC - Qt5::Widgets + Qt6::Widgets image_loading icc_detect ) diff --git a/tools/flicker_test/main.cc b/tools/flicker_test/main.cc index 67985a9..9617765 100644 --- a/tools/flicker_test/main.cc +++ b/tools/flicker_test/main.cc @@ -11,12 +11,13 @@ int main(int argc, char** argv) { QApplication application(argc, argv); - jxl::FlickerTestWizard wizard; + jpegxl::tools::FlickerTestWizard wizard; if (wizard.exec()) { - jxl::FlickerTestWindow test_window(wizard.parameters()); + jpegxl::tools::FlickerTestWindow test_window(wizard.parameters()); if (test_window.proceedWithTest()) { test_window.showMaximized(); return application.exec(); } } + return 0; } diff --git a/tools/flicker_test/parameters.cc b/tools/flicker_test/parameters.cc index 575edb0..460867b 100644 --- a/tools/flicker_test/parameters.cc +++ b/tools/flicker_test/parameters.cc @@ -5,7 +5,8 @@ #include "tools/flicker_test/parameters.h" -namespace jxl { +namespace jpegxl { +namespace tools { namespace { @@ -84,4 +85,5 @@ void FlickerTestParameters::saveTo(QSettings* const settings) const { settings->endGroup(); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/flicker_test/parameters.h b/tools/flicker_test/parameters.h index a063995..777d479 100644 --- a/tools/flicker_test/parameters.h +++ b/tools/flicker_test/parameters.h @@ -8,7 +8,8 @@ #include <QSettings> -namespace jxl { +namespace jpegxl { +namespace tools { struct FlickerTestParameters { QString originalFolder; @@ -27,6 +28,7 @@ struct FlickerTestParameters { void saveTo(QSettings* settings) const; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_FLICKER_TEST_PARAMETERS_H_ diff --git a/tools/flicker_test/setup.cc b/tools/flicker_test/setup.cc index bfcddd5..ff17286 100644 --- a/tools/flicker_test/setup.cc +++ b/tools/flicker_test/setup.cc @@ -11,7 +11,8 @@ #include <QMessageBox> #include <QPushButton> -namespace jxl { +namespace jpegxl { +namespace tools { FlickerTestWizard::FlickerTestWizard(QWidget* const parent) : QWizard(parent), settings_("JPEG XL project", "Flickering test") { @@ -148,4 +149,5 @@ bool FlickerTestWizard::validateCurrentPage() { return QWizard::validateCurrentPage(); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/flicker_test/setup.h b/tools/flicker_test/setup.h index 0da78d6..e034e28 100644 --- a/tools/flicker_test/setup.h +++ b/tools/flicker_test/setup.h @@ -11,7 +11,8 @@ #include "tools/flicker_test/parameters.h" #include "tools/flicker_test/ui_setup.h" -namespace jxl { +namespace jpegxl { +namespace tools { class FlickerTestWizard : public QWizard { Q_OBJECT @@ -39,6 +40,7 @@ class FlickerTestWizard : public QWizard { QSettings settings_; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_FLICKER_TEST_SETUP_H_ diff --git a/tools/flicker_test/setup.ui b/tools/flicker_test/setup.ui index 055c7f7..44b850c 100644 --- a/tools/flicker_test/setup.ui +++ b/tools/flicker_test/setup.ui @@ -11,6 +11,9 @@ <property name="windowTitle"> <string>New flicker test</string> </property> + <property name="wizardStyle"> + <enum>QWizard::ClassicStyle</enum> + </property> <property name="options"> <set>QWizard::NoBackButtonOnStartPage</set> </property> @@ -328,7 +331,7 @@ <widget class="QWizardPage" name="spacingPage"> <layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0,0"> <item> - <widget class="jxl::SplitView" name="spacingDemo" native="true"/> + <widget class="jpegxl::tools::SplitView" name="spacingDemo" native="true"/> </item> <item> <spacer name="verticalSpacer_2"> @@ -389,7 +392,7 @@ </widget> <customwidgets> <customwidget> - <class>jxl::SplitView</class> + <class>jpegxl::tools::SplitView</class> <extends>QWidget</extends> <header>tools/flicker_test/split_view.h</header> <container>1</container> diff --git a/tools/flicker_test/split_view.cc b/tools/flicker_test/split_view.cc index 3455d70..87df95e 100644 --- a/tools/flicker_test/split_view.cc +++ b/tools/flicker_test/split_view.cc @@ -8,7 +8,8 @@ #include <QMouseEvent> #include <QPainter> -namespace jxl { +namespace jpegxl { +namespace tools { SplitView::SplitView(QWidget* const parent) : QWidget(parent), g_(std::random_device()()) { @@ -37,12 +38,14 @@ SplitView::SplitView(QWidget* const parent) void SplitView::setOriginalImage(QImage image) { original_ = QPixmap::fromImage(std::move(image)); + original_.setDevicePixelRatio(devicePixelRatio()); updateMinimumSize(); update(); } void SplitView::setAlteredImage(QImage image) { altered_ = QPixmap::fromImage(std::move(image)); + altered_.setDevicePixelRatio(devicePixelRatio()); updateMinimumSize(); update(); } @@ -139,15 +142,17 @@ void SplitView::paintEvent(QPaintEvent* const event) { QPixmap* const leftImage = imageForSide(Side::kLeft); QPixmap* const rightImage = imageForSide(Side::kRight); - leftRect_ = leftImage->rect(); + leftRect_ = QRectF(QPoint(), leftImage->deviceIndependentSize()); leftRect_.moveCenter(rect().center()); - leftRect_.moveRight(rect().center().x() - spacing_ / 2 - spacing_ % 2); - painter.drawPixmap(leftRect_, *leftImage); + leftRect_.moveRight(rect().center().x() - + (spacing_ / 2 + spacing_ % 2) / devicePixelRatio()); + painter.drawPixmap(leftRect_.topLeft(), *leftImage); - rightRect_ = rightImage->rect(); + rightRect_ = QRectF(QPoint(), rightImage->deviceIndependentSize()); rightRect_.moveCenter(rect().center()); - rightRect_.moveLeft(rect().center().x() + 1 + spacing_ / 2); - painter.drawPixmap(rightRect_, *rightImage); + rightRect_.moveLeft(rect().center().x() + + (spacing_ / 2) / devicePixelRatio()); + painter.drawPixmap(rightRect_.topLeft(), *rightImage); } void SplitView::startDisplaying() { @@ -160,8 +165,12 @@ void SplitView::startDisplaying() { } void SplitView::updateMinimumSize() { - setMinimumWidth(2 * std::max(original_.width(), altered_.width()) + spacing_); - setMinimumHeight(std::max(original_.height(), altered_.height())); + setMinimumWidth(2 * std::max(original_.deviceIndependentSize().width(), + altered_.deviceIndependentSize().width()) + + spacing_ / devicePixelRatio()); + setMinimumHeight(std::max(original_.deviceIndependentSize().height(), + altered_.deviceIndependentSize().height())); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/flicker_test/split_view.h b/tools/flicker_test/split_view.h index b4c7a1d..37c5f7e 100644 --- a/tools/flicker_test/split_view.h +++ b/tools/flicker_test/split_view.h @@ -14,7 +14,8 @@ #include <QWidget> #include <random> -namespace jxl { +namespace jpegxl { +namespace tools { class SplitView : public QWidget { Q_OBJECT @@ -67,7 +68,7 @@ class SplitView : public QWidget { Side originalSide_; bool clicking_ = false; Side clickedSide_; - QRect leftRect_, rightRect_; + QRectF leftRect_, rightRect_; State state_ = State::kDisplaying; bool gray_ = false; QTimer blankingTimer_; @@ -79,6 +80,7 @@ class SplitView : public QWidget { QElapsedTimer viewingStartTime_; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_FLICKER_TEST_SPLIT_VIEW_H_ diff --git a/tools/flicker_test/test_window.cc b/tools/flicker_test/test_window.cc index f3827c5..c21ca6f 100644 --- a/tools/flicker_test/test_window.cc +++ b/tools/flicker_test/test_window.cc @@ -13,7 +13,8 @@ #include "tools/icc_detect/icc_detect.h" -namespace jxl { +namespace jpegxl { +namespace tools { FlickerTestWindow::FlickerTestWindow(FlickerTestParameters parameters, QWidget* const parent) @@ -181,4 +182,5 @@ retry: parameters_.grayFadingTimeMSecs, parameters_.grayTimeMSecs); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/flicker_test/test_window.h b/tools/flicker_test/test_window.h index 1dfe5fc..ad712af 100644 --- a/tools/flicker_test/test_window.h +++ b/tools/flicker_test/test_window.h @@ -16,7 +16,8 @@ #include "tools/flicker_test/parameters.h" #include "tools/flicker_test/ui_test_window.h" -namespace jxl { +namespace jpegxl { +namespace tools { class FlickerTestWindow : public QMainWindow { Q_OBJECT @@ -45,6 +46,7 @@ class FlickerTestWindow : public QMainWindow { QStringList remainingImages_; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_FLICKER_TEST_TEST_WINDOW_H_ diff --git a/tools/flicker_test/test_window.ui b/tools/flicker_test/test_window.ui index 7eb2619..bd42873 100644 --- a/tools/flicker_test/test_window.ui +++ b/tools/flicker_test/test_window.ui @@ -64,7 +64,7 @@ </item> </layout> </widget> - <widget class="jxl::SplitView" name="splitView"/> + <widget class="jpegxl::tools::SplitView" name="splitView"/> <widget class="QWidget" name="finalPage"> <layout class="QVBoxLayout" name="verticalLayout_3"> <item> @@ -104,7 +104,7 @@ </widget> <customwidgets> <customwidget> - <class>jxl::SplitView</class> + <class>jpegxl::tools::SplitView</class> <extends>QWidget</extends> <header>tools/flicker_test/split_view.h</header> <container>1</container> diff --git a/tools/fuzzer_stub.cc b/tools/fuzzer_stub.cc index f984c00..2f30e9e 100644 --- a/tools/fuzzer_stub.cc +++ b/tools/fuzzer_stub.cc @@ -3,14 +3,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/thread_parallel_runner.h> +#include <jxl/thread_parallel_runner_cxx.h> + #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) { diff --git a/tools/hdr/README.md b/tools/hdr/README.md index 227b22b..85eb1bd 100644 --- a/tools/hdr/README.md +++ b/tools/hdr/README.md @@ -99,6 +99,22 @@ 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`. +## OpenEXR to PQ + +`tools/exr_to_pq` converts an OpenEXR image into a Rec. 2020 + PQ image, which +can be saved as a PNG or PPM file. Luminance information is taken from the +`whiteLuminance` tag if the input has it, and otherwise defaults to treating +(1, 1, 1) as 100 cd/m². It is also possible to override this using the +`--luminance` (`-l`) flag, in two different ways: + +```shell +# Specifies that the brightest pixel in the image happens to be 1500 cd/m². +$ tools/exr_to_pq --luminance='max=1500' input.exr output.png + +# Specifies that (1, 1, 1) in the input file is 203 cd/m². +$ tools/exr_to_pq --luminance='white=203' input.exr output.png +``` + # LUT generation There are additionally two tools that can be used to generate look-up tables diff --git a/tools/hdr/display_to_hlg.cc b/tools/hdr/display_to_hlg.cc index a2caef2..8fa8fde 100644 --- a/tools/hdr/display_to_hlg.cc +++ b/tools/hdr/display_to_hlg.cc @@ -9,13 +9,15 @@ #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 "lib/jxl/base/span.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/file_io.h" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; float max_nits = 0; @@ -64,9 +66,11 @@ int main(int argc, const char** argv) { return EXIT_FAILURE; } + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); jxl::CodecInOut image; - JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image, - &pool)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(), + &image, &pool)); image.metadata.m.SetIntensityTarget(max_nits); JXL_CHECK(jxl::HlgInverseOOTF( &image.Main(), jxl::GetHlgGamma(max_nits, surround_nits), &pool)); @@ -75,11 +79,12 @@ int main(int argc, const char** argv) { 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.SetPrimariesType(jxl::Primaries::k2100)); + JXL_CHECK(hlg.SetWhitePointType(jxl::WhitePoint::kD65)); + hlg.Tf().SetTransferFunction(jxl::TransferFunction::kHLG); JXL_CHECK(hlg.CreateICC()); - JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool)); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, hlg, &pool)); image.metadata.m.color_encoding = hlg; - JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/hdr/exr_to_pq.cc b/tools/hdr/exr_to_pq.cc new file mode 100644 index 0000000..c7ce1b7 --- /dev/null +++ b/tools/hdr/exr_to_pq.cc @@ -0,0 +1,158 @@ +// 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/dec/decode.h" +#include "lib/extras/packed_image_convert.h" +#include "lib/jxl/cms/jxl_cms_internal.h" +#include "lib/jxl/image_bundle.h" +#include "tools/cmdline.h" +#include "tools/file_io.h" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" + +namespace { + +struct LuminanceInfo { + enum class Kind { kWhite, kMaximum }; + Kind kind = Kind::kWhite; + float luminance = 100.f; +}; + +bool ParseLuminanceInfo(const char* argument, LuminanceInfo* luminance_info) { + if (strncmp(argument, "white=", 6) == 0) { + luminance_info->kind = LuminanceInfo::Kind::kWhite; + argument += 6; + } else if (strncmp(argument, "max=", 4) == 0) { + luminance_info->kind = LuminanceInfo::Kind::kMaximum; + argument += 4; + } else { + fprintf(stderr, + "Invalid prefix for luminance info, expected white= or max=\n"); + return false; + } + return jpegxl::tools::ParseFloat(argument, &luminance_info->luminance); +} + +} // namespace + +int main(int argc, const char** argv) { + jpegxl::tools::ThreadPoolInternal pool; + + jpegxl::tools::CommandLineParser parser; + LuminanceInfo luminance_info; + auto luminance_option = + parser.AddOptionValue('l', "luminance", "<max|white=N>", + "luminance information (defaults to whiteLuminance " + "header if present, otherwise to white=100)", + &luminance_info, &ParseLuminanceInfo, 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::extras::PackedPixelFile ppf; + std::vector<uint8_t> input_bytes; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &input_bytes)); + JXL_CHECK(jxl::extras::DecodeBytes(jxl::Bytes(input_bytes), + jxl::extras::ColorHints(), &ppf)); + + jxl::CodecInOut image; + JXL_CHECK( + jxl::extras::ConvertPackedPixelFileToCodecInOut(ppf, &pool, &image)); + image.metadata.m.bit_depth.exponent_bits_per_sample = 0; + jxl::ColorEncoding linear_rec_2020 = image.Main().c_current(); + JXL_CHECK(linear_rec_2020.SetPrimariesType(jxl::Primaries::k2100)); + linear_rec_2020.Tf().SetTransferFunction(jxl::TransferFunction::kLinear); + JXL_CHECK(linear_rec_2020.CreateICC()); + JXL_CHECK( + jpegxl::tools::TransformCodecInOutTo(image, linear_rec_2020, &pool)); + + float primaries_xyz[9]; + const jxl::PrimariesCIExy p = image.Main().c_current().GetPrimaries(); + const jxl::CIExy wp = image.Main().c_current().GetWhitePoint(); + JXL_CHECK(jxl::PrimariesToXYZ(p.r.x, p.r.y, p.g.x, p.g.y, p.b.x, p.b.y, wp.x, + wp.y, primaries_xyz)); + + float max_value = 0.f; + float max_relative_luminance = 0.f; + float white_luminance = ppf.info.intensity_target != 0 && + !parser.GetOption(luminance_option)->matched() + ? ppf.info.intensity_target + : luminance_info.kind == LuminanceInfo::Kind::kWhite + ? luminance_info.luminance + : 0.f; + bool out_of_gamut = false; + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const rows[3] = {image.Main().color()->ConstPlaneRow(0, y), + image.Main().color()->ConstPlaneRow(1, y), + image.Main().color()->ConstPlaneRow(2, y)}; + for (size_t x = 0; x < image.xsize(); ++x) { + if (!out_of_gamut && + (rows[0][x] < 0 || rows[1][x] < 0 || rows[2][x] < 0)) { + out_of_gamut = true; + fprintf(stderr, + "WARNING: found colors outside of the Rec. 2020 gamut.\n"); + } + max_value = std::max( + max_value, std::max(rows[0][x], std::max(rows[1][x], rows[2][x]))); + const float luminance = primaries_xyz[1] * rows[0][x] + + primaries_xyz[4] * rows[1][x] + + primaries_xyz[7] * rows[2][x]; + if (luminance_info.kind == LuminanceInfo::Kind::kMaximum && + luminance > max_relative_luminance) { + max_relative_luminance = luminance; + white_luminance = luminance_info.luminance / luminance; + } + } + } + jxl::ScaleImage(1.f / max_value, image.Main().color()); + white_luminance *= max_value; + image.metadata.m.SetIntensityTarget(white_luminance); + if (white_luminance > 10000) { + fprintf(stderr, + "WARNING: the image is too bright for PQ (would need (1, 1, 1) to " + "be %g cd/m^2).\n", + white_luminance); + } else { + fprintf(stderr, + "The resulting image should be compressed with " + "--intensity_target=%g.\n", + white_luminance); + } + + jxl::ColorEncoding pq = image.Main().c_current(); + pq.Tf().SetTransferFunction(jxl::TransferFunction::kPQ); + JXL_CHECK(pq.CreateICC()); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, pq, &pool)); + image.metadata.m.color_encoding = pq; + std::vector<uint8_t> encoded; + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); +} diff --git a/tools/hdr/generate_lut_template.cc b/tools/hdr/generate_lut_template.cc index 626d54f..da8ecee 100644 --- a/tools/hdr/generate_lut_template.cc +++ b/tools/hdr/generate_lut_template.cc @@ -7,12 +7,13 @@ #include <stdlib.h> #include "lib/extras/codec.h" -#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/image_metadata.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; size_t N = 64; @@ -55,6 +56,8 @@ int main(int argc, const char** argv) { jxl::CodecInOut output; output.metadata.m.bit_depth.bits_per_sample = 16; output.SetFromImage(std::move(image), jxl::ColorEncoding::SRGB()); - JXL_CHECK(jxl::EncodeToFile(output, jxl::ColorEncoding::SRGB(), 16, - output_filename, &pool)); + std::vector<uint8_t> encoded; + JXL_CHECK(jxl::Encode(output, jxl::ColorEncoding::SRGB(), 16, output_filename, + &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/hdr/image_utils.h b/tools/hdr/image_utils.h new file mode 100644 index 0000000..901c2b6 --- /dev/null +++ b/tools/hdr/image_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_HDR_IMAGE_UTILS_H_ +#define TOOLS_HDR_IMAGE_UTILS_H_ + +#include <jxl/cms.h> +#include <jxl/cms_interface.h> + +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/image_bundle.h" + +namespace jpegxl { +namespace tools { + +static inline jxl::Status TransformCodecInOutTo( + jxl::CodecInOut& io, const jxl::ColorEncoding& c_desired, + jxl::ThreadPool* pool) { + const JxlCmsInterface& cms = *JxlGetDefaultCms(); + if (io.metadata.m.have_preview) { + JXL_RETURN_IF_ERROR(io.preview_frame.TransformTo(c_desired, cms, pool)); + } + for (jxl::ImageBundle& ib : io.frames) { + JXL_RETURN_IF_ERROR(ib.TransformTo(c_desired, cms, pool)); + } + return true; +} + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_HDR_IMAGE_UTILS_H_ diff --git a/tools/hdr/local_tone_map.cc b/tools/hdr/local_tone_map.cc new file mode 100644 index 0000000..b6582a6 --- /dev/null +++ b/tools/hdr/local_tone_map.cc @@ -0,0 +1,541 @@ +// 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 <jxl/cms.h> +#include <stdio.h> +#include <stdlib.h> + +#include "lib/extras/codec.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/enc_gamma_correct.h" +#include "lib/jxl/image_bundle.h" +#include "tools/args.h" +#include "tools/cmdline.h" +#include "tools/thread_pool_internal.h" + +namespace jxl { +namespace { + +constexpr WeightsSeparable5 kPyramidFilter = { + {HWY_REP4(.375f), HWY_REP4(.25f), HWY_REP4(.0625f)}, + {HWY_REP4(.375f), HWY_REP4(.25f), HWY_REP4(.0625f)}}; + +template <typename Tin, typename Tout> +void Subtract(const Image3<Tin>& image1, const Image3<Tin>& image2, + Image3<Tout>* out) { + const size_t xsize = image1.xsize(); + const size_t ysize = image1.ysize(); + JXL_CHECK(xsize == image2.xsize()); + JXL_CHECK(ysize == image2.ysize()); + + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const Tin* const JXL_RESTRICT row1 = image1.ConstPlaneRow(c, y); + const Tin* const JXL_RESTRICT row2 = image2.ConstPlaneRow(c, y); + Tout* const JXL_RESTRICT row_out = out->PlaneRow(c, y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row1[x] - row2[x]; + } + } + } +} + +// Adds `what` of the size of `rect` to `to` in the position of `rect`. +template <typename Tin, typename Tout> +void AddTo(const Rect& rect, const Image3<Tin>& what, Image3<Tout>* to) { + const size_t xsize = what.xsize(); + const size_t ysize = what.ysize(); + JXL_ASSERT(xsize == rect.xsize()); + JXL_ASSERT(ysize == rect.ysize()); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const Tin* JXL_RESTRICT row_what = what.ConstPlaneRow(c, y); + Tout* JXL_RESTRICT row_to = rect.PlaneRow(to, c, y); + for (size_t x = 0; x < xsize; ++x) { + row_to[x] += row_what[x]; + } + } + } +} + +template <typename T> +Plane<T> Product(const Plane<T>& a, const Plane<T>& b) { + Plane<T> c(a.xsize(), a.ysize()); + for (size_t y = 0; y < a.ysize(); ++y) { + const T* const JXL_RESTRICT row_a = a.Row(y); + const T* const JXL_RESTRICT row_b = b.Row(y); + T* const JXL_RESTRICT row_c = c.Row(y); + for (size_t x = 0; x < a.xsize(); ++x) { + row_c[x] = row_a[x] * row_b[x]; + } + } + return c; +} + +// Expects sRGB input. +// Will call consumer(x, y, contrast) for each pixel. +template <typename Consumer> +void Contrast(const jxl::Image3F& image, const Consumer& consumer, + ThreadPool* const pool) { + static constexpr WeightsSymmetric3 kLaplacianWeights = { + {HWY_REP4(-4)}, {HWY_REP4(1)}, {HWY_REP4(0)}}; + ImageF grayscale(image.xsize(), image.ysize()); + static constexpr float kLuminances[3] = {0.2126, 0.7152, 0.0722}; + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const JXL_RESTRICT input_rows[3] = { + image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)}; + float* const JXL_RESTRICT row = grayscale.Row(y); + + for (size_t x = 0; x < image.xsize(); ++x) { + row[x] = LinearToSrgb8Direct( + kLuminances[0] * Srgb8ToLinearDirect(input_rows[0][x]) + + kLuminances[1] * Srgb8ToLinearDirect(input_rows[1][x]) + + kLuminances[2] * Srgb8ToLinearDirect(input_rows[2][x])); + } + } + + ImageF laplacian(image.xsize(), image.ysize()); + Symmetric3(grayscale, Rect(grayscale), kLaplacianWeights, pool, &laplacian); + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const JXL_RESTRICT row = laplacian.ConstRow(y); + for (size_t x = 0; x < image.xsize(); ++x) { + consumer(x, y, std::abs(row[x])); + } + } +} + +template <typename Consumer> +void Saturation(const jxl::Image3F& image, const Consumer& consumer) { + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const JXL_RESTRICT rows[3] = { + image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)}; + for (size_t x = 0; x < image.xsize(); ++x) { + // TODO(sboukortt): experiment with other methods of computing the + // saturation, e.g. C*/L* in LUV/LCh. + const float mean = (1.f / 3) * (rows[0][x] + rows[1][x] + rows[2][x]); + const float deviations[3] = {rows[0][x] - mean, rows[1][x] - mean, + rows[2][x] - mean}; + consumer(x, y, + std::sqrt((1.f / 3) * (deviations[0] * deviations[0] + + deviations[1] * deviations[1] + + deviations[2] * deviations[2]))); + } + } +} + +template <typename Consumer> +void MidToneness(const jxl::Image3F& image, const float sigma, + const Consumer& consumer) { + const float inv_sigma_squared = 1.f / (sigma * sigma); + const auto Gaussian = [inv_sigma_squared](const float x) { + return std::exp(-.5f * (x - .5f) * (x - .5f) * inv_sigma_squared); + }; + for (size_t y = 0; y < image.ysize(); ++y) { + const float* const JXL_RESTRICT rows[3] = { + image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)}; + for (size_t x = 0; x < image.xsize(); ++x) { + consumer( + x, y, + Gaussian(rows[0][x]) * Gaussian(rows[1][x]) * Gaussian(rows[2][x])); + } + } +} + +ImageF ComputeWeights(const jxl::Image3F& image, const float contrast_weight, + const float saturation_weight, + const float midtoneness_weight, + const float midtoneness_sigma, ThreadPool* const pool) { + ImageF log_weights(image.xsize(), image.ysize()); + ZeroFillImage(&log_weights); + + if (contrast_weight > 0) { + Contrast( + image, + [&log_weights, contrast_weight](const size_t x, const size_t y, + const float weight) { + log_weights.Row(y)[x] = contrast_weight * std::log(weight); + }, + pool); + } + + if (saturation_weight > 0) { + Saturation(image, [&log_weights, saturation_weight]( + const size_t x, const size_t y, const float weight) { + log_weights.Row(y)[x] += saturation_weight * std::log(weight); + }); + } + + if (midtoneness_weight > 0) { + MidToneness(image, midtoneness_sigma, + [&log_weights, midtoneness_weight]( + const size_t x, const size_t y, const float weight) { + log_weights.Row(y)[x] += + midtoneness_weight * std::log(weight); + }); + } + + ImageF weights = std::move(log_weights); + + for (size_t y = 0; y < weights.ysize(); ++y) { + float* const JXL_RESTRICT row = weights.Row(y); + for (size_t x = 0; x < weights.xsize(); ++x) { + row[x] = std::exp(row[x]); + } + } + + return weights; +} + +std::vector<ImageF> ComputeWeights(const std::vector<Image3F>& images, + const float contrast_weight, + const float saturation_weight, + const float midtoneness_weight, + const float midtoneness_sigma, + ThreadPool* const pool) { + std::vector<ImageF> weights; + weights.reserve(images.size()); + for (const Image3F& image : images) { + if (image.xsize() != images.front().xsize() || + image.ysize() != images.front().ysize()) { + return {}; + } + weights.push_back(ComputeWeights(image, contrast_weight, saturation_weight, + midtoneness_weight, midtoneness_sigma, + pool)); + } + + std::vector<float*> rows(images.size()); + for (size_t y = 0; y < images.front().ysize(); ++y) { + for (size_t i = 0; i < images.size(); ++i) { + rows[i] = weights[i].Row(y); + } + for (size_t x = 0; x < images.front().xsize(); ++x) { + float sum = 1e-9f; + for (size_t i = 0; i < images.size(); ++i) { + sum += rows[i][x]; + } + const float ratio = 1.f / sum; + for (size_t i = 0; i < images.size(); ++i) { + rows[i][x] *= ratio; + } + } + } + + return weights; +} + +ImageF Downsample(const ImageF& image, ThreadPool* const pool) { + ImageF filtered(image.xsize(), image.ysize()); + Separable5(image, Rect(image), kPyramidFilter, pool, &filtered); + ImageF result(DivCeil(image.xsize(), 2), DivCeil(image.ysize(), 2)); + for (size_t y = 0; y < result.ysize(); ++y) { + const float* const JXL_RESTRICT filtered_row = filtered.ConstRow(2 * y); + float* const JXL_RESTRICT row = result.Row(y); + for (size_t x = 0; x < result.xsize(); ++x) { + row[x] = filtered_row[2 * x]; + } + } + return result; +} + +Image3F Downsample(const Image3F& image, ThreadPool* const pool) { + return Image3F(Downsample(image.Plane(0), pool), + Downsample(image.Plane(1), pool), + Downsample(image.Plane(2), pool)); +} + +Image3F PadImageMirror(const Image3F& in, const size_t xborder, + const size_t yborder) { + size_t xsize = in.xsize(); + size_t ysize = in.ysize(); + Image3F out(xsize + 2 * xborder, ysize + 2 * yborder); + if (xborder > xsize || yborder > ysize) { + for (size_t c = 0; c < 3; c++) { + for (int32_t y = 0; y < static_cast<int32_t>(out.ysize()); y++) { + float* row_out = out.PlaneRow(c, y); + const float* row_in = in.PlaneRow( + c, Mirror(y - static_cast<int32_t>(yborder), in.ysize())); + for (int32_t x = 0; x < static_cast<int32_t>(out.xsize()); x++) { + int32_t xin = Mirror(x - static_cast<int32_t>(xborder), in.xsize()); + row_out[x] = row_in[xin]; + } + } + } + return out; + } + CopyImageTo(Rect(in), in, Rect(xborder, yborder, xsize, ysize), &out); + for (size_t c = 0; c < 3; c++) { + // Horizontal pad. + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xborder; x++) { + out.PlaneRow(c, y + yborder)[x] = + in.ConstPlaneRow(c, y)[xborder - x - 1]; + out.PlaneRow(c, y + yborder)[x + xsize + xborder] = + in.ConstPlaneRow(c, y)[xsize - 1 - x]; + } + } + // Vertical pad. + for (size_t y = 0; y < yborder; y++) { + memcpy(out.PlaneRow(c, y), out.ConstPlaneRow(c, 2 * yborder - 1 - y), + out.xsize() * sizeof(float)); + memcpy(out.PlaneRow(c, y + ysize + yborder), + out.ConstPlaneRow(c, ysize + yborder - 1 - y), + out.xsize() * sizeof(float)); + } + } + return out; +} + +Image3F Upsample(const Image3F& image, const bool odd_width, + const bool odd_height, ThreadPool* const pool) { + const Image3F padded = PadImageMirror(image, 1, 1); + Image3F upsampled(2 * padded.xsize(), 2 * padded.ysize()); + ZeroFillImage(&upsampled); + for (int c = 0; c < 3; ++c) { + for (size_t y = 0; y < padded.ysize(); ++y) { + const float* const JXL_RESTRICT padded_row = padded.ConstPlaneRow(c, y); + float* const JXL_RESTRICT row = upsampled.PlaneRow(c, 2 * y); + for (size_t x = 0; x < padded.xsize(); ++x) { + row[2 * x] = 4 * padded_row[x]; + } + } + } + Image3F filtered(upsampled.xsize(), upsampled.ysize()); + for (int c = 0; c < 3; ++c) { + Separable5(upsampled.Plane(c), Rect(upsampled), kPyramidFilter, pool, + &filtered.Plane(c)); + } + Image3F result(2 * image.xsize() - (odd_width ? 1 : 0), + 2 * image.ysize() - (odd_height ? 1 : 0)); + CopyImageTo(Rect(2, 2, result.xsize(), result.ysize()), filtered, + Rect(result), &result); + return result; +} + +std::vector<ImageF> GaussianPyramid(ImageF image, int num_levels, + ThreadPool* pool) { + std::vector<ImageF> pyramid(num_levels); + for (int i = 0; i < num_levels - 1; ++i) { + ImageF downsampled = Downsample(image, pool); + pyramid[i] = std::move(image); + image = std::move(downsampled); + } + pyramid[num_levels - 1] = std::move(image); + return pyramid; +} + +std::vector<Image3F> LaplacianPyramid(Image3F image, int num_levels, + ThreadPool* pool) { + std::vector<Image3F> pyramid(num_levels); + for (int i = 0; i < num_levels - 1; ++i) { + Image3F downsampled = Downsample(image, pool); + const bool odd_width = image.xsize() % 2 != 0; + const bool odd_height = image.ysize() % 2 != 0; + Subtract(image, Upsample(downsampled, odd_width, odd_height, pool), &image); + pyramid[i] = std::move(image); + image = std::move(downsampled); + } + pyramid[num_levels - 1] = std::move(image); + return pyramid; +} + +Image3F ReconstructFromLaplacianPyramid(std::vector<Image3F> pyramid, + ThreadPool* const pool) { + Image3F result = std::move(pyramid.back()); + pyramid.pop_back(); + for (auto it = pyramid.rbegin(); it != pyramid.rend(); ++it) { + const bool odd_width = it->xsize() % 2 != 0; + const bool odd_height = it->ysize() % 2 != 0; + result = Upsample(result, odd_width, odd_height, pool); + AddTo(Rect(result), *it, &result); + } + return result; +} + +// Exposure fusion algorithm as described in: +// https://mericam.github.io/exposure_fusion/ +// +// That is, given n images of identical size: for each pixel coordinate, one +// weight per input image is computed, indicating how much each input image will +// contribute to the result. There are therefore n weight maps, the sum of which +// is 1 at every pixel. +// +// Those weights are then applied at various scales rather than directly at full +// resolution. To understand how, it helps to familiarize oneself with Laplacian +// and Gaussian pyramids, as described in "The Laplacian Pyramid as a Compact +// Image Code" by P. Burt and E. Adelson: +// http://persci.mit.edu/pub_pdfs/pyramid83.pdf +// +// A Gaussian pyramid of k levels is a sequence of k images in which the first +// image is the original image and each following level is a low-pass-filtered +// version of the previous one. A Laplacian pyramid is obtained from a Gaussian +// pyramid by: +// +// laplacian_pyramid[i] = gaussian_pyramid[i] − gaussian_pyramid[i + 1]. +// (The last item of the Laplacian pyramid is just the last one from the +// Gaussian pyramid without subtraction.) +// +// From there, the original image can be reconstructed by adding all the images +// from the Laplacian pyramid together. (If desired, the Gaussian pyramid can be +// reconstructed as well by storing the cumulative sums starting from the end.) +// +// Having established that, the application of the weight images is done by +// constructing a Laplacian pyramid for each input image, as well as a Gaussian +// pyramid for each weight image, and then constructing a Laplacian pyramid such +// that: +// +// pyramid[i] = sum(laplacian_pyramids[j][i] .* weight_gaussian_pyramids[j][i] +// for j in 1..n) +// +// And then reconstructing an image from the pyramid thus obtained. +Image3F ExposureFusion(std::vector<Image3F> images, int num_levels, + const float contrast_weight, + const float saturation_weight, + const float midtoneness_weight, + const float midtoneness_sigma, ThreadPool* const pool) { + std::vector<ImageF> weights = + ComputeWeights(images, contrast_weight, saturation_weight, + midtoneness_weight, midtoneness_sigma, pool); + + std::vector<Image3F> pyramid(num_levels); + for (size_t i = 0; i < images.size(); ++i) { + const std::vector<ImageF> weight_pyramid = + GaussianPyramid(std::move(weights[i]), num_levels, pool); + const std::vector<Image3F> image_pyramid = + LaplacianPyramid(std::move(images[i]), num_levels, pool); + + for (int k = 0; k < num_levels; ++k) { + Image3F product(Product(weight_pyramid[k], image_pyramid[k].Plane(0)), + Product(weight_pyramid[k], image_pyramid[k].Plane(1)), + Product(weight_pyramid[k], image_pyramid[k].Plane(2))); + if (pyramid[k].xsize() == 0) { + pyramid[k] = std::move(product); + } else { + AddTo(Rect(product), product, &pyramid[k]); + } + } + } + + return ReconstructFromLaplacianPyramid(std::move(pyramid), pool); +} + +} // namespace +} // namespace jxl + +int main(int argc, const char** argv) { + jpegxl::tools::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); + int64_t num_levels = -1; + parser.AddOptionValue('l', "num_levels", "1..", + "number of levels in the pyramid", &num_levels, + &jpegxl::tools::ParseInt64, 0); + float contrast_weight = 0.f; + parser.AddOptionValue('c', "contrast_weight", "0..", + "importance of contrast when computing weights", + &contrast_weight, &jpegxl::tools::ParseFloat, 0); + float saturation_weight = .2f; + parser.AddOptionValue('a', "saturation_weight", "0..", + "importance of saturation when computing weights", + &saturation_weight, &jpegxl::tools::ParseFloat, 0); + float midtoneness_weight = 1.f; + parser.AddOptionValue('t', "midtoneness_weight", "0..", + "importance of \"midtoneness\" when computing weights", + &midtoneness_weight, &jpegxl::tools::ParseFloat, 0); + float midtoneness_sigma = .2f; + parser.AddOptionValue('g', "midtoneness_sigma", "0..", + "spread of the function that computes midtoneness", + &midtoneness_sigma, &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"); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool)); + + if (max_nits > 0) { + image.metadata.m.SetIntensityTarget(max_nits); + } else { + max_nits = image.metadata.m.IntensityTarget(); + } + + std::vector<jxl::Image3F> input_images; + + if (max_nits <= 4 * jxl::kDefaultIntensityTarget) { + jxl::CodecInOut sRGB_image; + jxl::Image3F color(image.xsize(), image.ysize()); + CopyImageTo(*image.Main().color(), &color); + sRGB_image.SetFromImage(std::move(color), image.Main().c_current()); + JXL_CHECK(sRGB_image.Main().TransformTo(jxl::ColorEncoding::SRGB(), + *JxlGetDefaultCms(), &pool)); + input_images.push_back(std::move(*sRGB_image.Main().color())); + } + + for (int i = 0; i < 4; ++i) { + const float target = std::ldexp(jxl::kDefaultIntensityTarget, 2 - i); + if (target >= max_nits) continue; + jxl::CodecInOut tone_mapped_image; + jxl::Image3F color(image.xsize(), image.ysize()); + CopyImageTo(*image.Main().color(), &color); + tone_mapped_image.SetFromImage(std::move(color), image.Main().c_current()); + tone_mapped_image.metadata.m.SetIntensityTarget( + image.metadata.m.IntensityTarget()); + JXL_CHECK(jxl::ToneMapTo({0, target}, &tone_mapped_image, &pool)); + JXL_CHECK(jxl::GamutMap(&tone_mapped_image, preserve_saturation, &pool)); + JXL_CHECK(tone_mapped_image.Main().TransformTo(jxl::ColorEncoding::SRGB(), + *JxlGetDefaultCms(), &pool)); + input_images.push_back(std::move(*tone_mapped_image.Main().color())); + } + + if (num_levels < 1) { + num_levels = jxl::FloorLog2Nonzero(std::min(image.xsize(), image.ysize())); + } + + jxl::Image3F fused = jxl::ExposureFusion( + std::move(input_images), num_levels, contrast_weight, saturation_weight, + midtoneness_weight, midtoneness_sigma, &pool); + + jxl::CodecInOut output; + output.SetFromImage(std::move(fused), jxl::ColorEncoding::SRGB()); + + JXL_CHECK(jxl::Encode(output, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); +} diff --git a/tools/hdr/pq_to_hlg.cc b/tools/hdr/pq_to_hlg.cc index 3b2125b..ea47a6b 100644 --- a/tools/hdr/pq_to_hlg.cc +++ b/tools/hdr/pq_to_hlg.cc @@ -9,13 +9,13 @@ #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" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; float max_nits = 0; @@ -56,10 +56,14 @@ int main(int argc, const char** argv) { 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)); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool)); if (max_nits > 0) { image.metadata.m.SetIntensityTarget(max_nits); } + const jxl::Primaries original_primaries = + image.Main().c_current().GetPrimariesType(); 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)); @@ -70,11 +74,12 @@ int main(int argc, const char** argv) { 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.SetPrimariesType(original_primaries)); + JXL_CHECK(hlg.SetWhitePointType(jxl::WhitePoint::kD65)); + hlg.Tf().SetTransferFunction(jxl::TransferFunction::kHLG); JXL_CHECK(hlg.CreateICC()); - JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool)); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, hlg, &pool)); image.metadata.m.color_encoding = hlg; - JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/hdr/render_hlg.cc b/tools/hdr/render_hlg.cc index c8a2395..cca43b1 100644 --- a/tools/hdr/render_hlg.cc +++ b/tools/hdr/render_hlg.cc @@ -9,13 +9,13 @@ #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" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; float target_nits = 0; @@ -71,7 +71,9 @@ int main(int argc, const char** argv) { 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)); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), 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); @@ -82,13 +84,12 @@ int main(int argc, const char** argv) { 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::cms::TransferFunction tf = + pq ? jxl::TransferFunction::kPQ : jxl::TransferFunction::kSRGB; + c_out.Tf().SetTransferFunction(tf); JXL_CHECK(c_out.CreateICC()); - JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool)); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, c_out, &pool)); image.metadata.m.color_encoding = c_out; - JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/hdr/texture_to_cube.cc b/tools/hdr/texture_to_cube.cc index a5e5af7..0d9f731 100644 --- a/tools/hdr/texture_to_cube.cc +++ b/tools/hdr/texture_to_cube.cc @@ -7,12 +7,13 @@ #include <stdlib.h> #include "lib/extras/codec.h" -#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/image_bundle.h" #include "tools/args.h" #include "tools/cmdline.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; const char* input_filename = nullptr; @@ -42,8 +43,10 @@ int main(int argc, const char** argv) { } jxl::CodecInOut image; - JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image, - &pool)); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(), + &image, &pool)); JXL_CHECK(image.xsize() == image.ysize() * image.ysize()); const unsigned N = image.ysize(); diff --git a/tools/hdr/tone_map.cc b/tools/hdr/tone_map.cc index 1ef3823..67fea48 100644 --- a/tools/hdr/tone_map.cc +++ b/tools/hdr/tone_map.cc @@ -8,13 +8,14 @@ #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" +#include "tools/file_io.h" +#include "tools/hdr/image_utils.h" +#include "tools/thread_pool_internal.h" int main(int argc, const char** argv) { - jxl::ThreadPoolInternal pool; + jpegxl::tools::ThreadPoolInternal pool; jpegxl::tools::CommandLineParser parser; float max_nits = 0; @@ -69,7 +70,9 @@ int main(int argc, const char** argv) { 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)); + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool)); if (max_nits > 0) { image.metadata.m.SetIntensityTarget(max_nits); } @@ -77,13 +80,18 @@ int main(int argc, const char** argv) { 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::cms::TransferFunction tf = + pq ? jxl::TransferFunction::kPQ : jxl::TransferFunction::kSRGB; + + if (jxl::extras::CodecFromPath(output_filename) == jxl::extras::Codec::kEXR) { + tf = jxl::TransferFunction::kLinear; + image.metadata.m.SetFloat16Samples(); } + c_out.Tf().SetTransferFunction(tf); + JXL_CHECK(c_out.CreateICC()); - JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool)); + JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, c_out, &pool)); image.metadata.m.color_encoding = c_out; - JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool)); + JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool)); + JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded)); } diff --git a/tools/icc_codec_fuzzer.cc b/tools/icc_codec_fuzzer.cc index 0af805c..410ece1 100644 --- a/tools/icc_codec_fuzzer.cc +++ b/tools/icc_codec_fuzzer.cc @@ -6,7 +6,15 @@ #include "lib/jxl/enc_icc_codec.h" #include "lib/jxl/icc_codec.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::PaddedBytes; + +#ifdef JXL_ICC_FUZZER_SLOW_TEST +using ::jxl::BitReader; +using ::jxl::Span; +#endif int TestOneInput(const uint8_t* data, size_t size) { #if defined(JXL_ICC_FUZZER_ONLY_WRITE) @@ -27,33 +35,32 @@ int TestOneInput(const uint8_t* data, size_t size) { // the ICC parsing. if (read) { // Reading parses the compressed format. - BitReader br(Span<const uint8_t>(data, size)); - PaddedBytes result; - (void)ReadICC(&br, &result); + BitReader br(Bytes(data, size)); + std::vector<uint8_t> result; + (void)jxl::test::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)); + JXL_ASSERT(jxl::WriteICC(icc, &writer, 0, nullptr)); } #else // JXL_ICC_FUZZER_SLOW_TEST if (read) { // Reading (unpredicting) parses the compressed format. PaddedBytes result; - (void)UnpredictICC(data, size, &result); + (void)jxl::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)); + JXL_ASSERT(jxl::PredictICC(data, size, &result)); PaddedBytes reconstructed; - JXL_ASSERT(UnpredictICC(result.data(), result.size(), &reconstructed)); + JXL_ASSERT(jxl::UnpredictICC(result.data(), result.size(), &reconstructed)); JXL_ASSERT(reconstructed.size() == size); JXL_ASSERT(memcmp(data, reconstructed.data(), size) == 0); } @@ -61,8 +68,9 @@ int TestOneInput(const uint8_t* data, size_t size) { return 0; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - return jxl::TestOneInput(data, size); + return jpegxl::tools::TestOneInput(data, size); } diff --git a/tools/icc_detect/icc_detect.h b/tools/icc_detect/icc_detect.h index 9335d94..deca6d7 100644 --- a/tools/icc_detect/icc_detect.h +++ b/tools/icc_detect/icc_detect.h @@ -9,11 +9,13 @@ #include <QByteArray> #include <QWidget> -namespace jxl { +namespace jpegxl { +namespace tools { // Should be cached if possible. QByteArray GetMonitorIccProfile(const QWidget* widget); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_ICC_DETECT_ICC_DETECT_H_ diff --git a/tools/icc_detect/icc_detect_empty.cc b/tools/icc_detect/icc_detect_empty.cc index abd4a95..421ac50 100644 --- a/tools/icc_detect/icc_detect_empty.cc +++ b/tools/icc_detect/icc_detect_empty.cc @@ -5,10 +5,12 @@ #include "tools/icc_detect/icc_detect.h" -namespace jxl { +namespace jpegxl { +namespace tools { QByteArray GetMonitorIccProfile(const QWidget* const /*widget*/) { return QByteArray(); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/icc_detect/icc_detect_win32.cc b/tools/icc_detect/icc_detect_win32.cc index 39ac5ee..f06e688 100644 --- a/tools/icc_detect/icc_detect_win32.cc +++ b/tools/icc_detect/icc_detect_win32.cc @@ -10,7 +10,8 @@ #include <memory> #include <type_traits> -namespace jxl { +namespace jpegxl { +namespace tools { namespace { @@ -61,4 +62,5 @@ QByteArray GetMonitorIccProfile(const QWidget* const widget) { return profile; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/icc_detect/icc_detect_x11.cc b/tools/icc_detect/icc_detect_x11.cc index be1209e..e67b30e 100644 --- a/tools/icc_detect/icc_detect_x11.cc +++ b/tools/icc_detect/icc_detect_x11.cc @@ -3,17 +3,23 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// clang-format off #include "tools/icc_detect/icc_detect.h" +// clang-format on #include <stdint.h> #include <stdlib.h> #include <xcb/xcb.h> -#include <QX11Info> -#include <algorithm> #include <memory> -namespace jxl { +// clang-format off +#include <QApplication> +#include <X11/Xlib.h> +// clang-format on + +namespace jpegxl { +namespace tools { namespace { @@ -30,11 +36,17 @@ using XcbUniquePtr = std::unique_ptr<T, FreeDeleter>; QByteArray GetMonitorIccProfile(const QWidget* const widget) { Q_UNUSED(widget) - xcb_connection_t* const connection = QX11Info::connection(); + auto* const qX11App = + qGuiApp->nativeInterface<QNativeInterface::QX11Application>(); + if (qX11App == nullptr) { + return QByteArray(); + } + xcb_connection_t* const connection = qX11App->connection(); if (connection == nullptr) { return QByteArray(); } - const int screen_number = QX11Info::appScreen(); + + const int screenNumber = DefaultScreen(qX11App->display()); const xcb_intern_atom_cookie_t atomRequest = xcb_intern_atom(connection, /*only_if_exists=*/1, @@ -51,7 +63,7 @@ QByteArray GetMonitorIccProfile(const QWidget* const widget) { for (xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(connection)); it.rem; xcb_screen_next(&it)) { - if (i == screen_number) { + if (i == screenNumber) { screen = it.data; break; } @@ -74,4 +86,5 @@ QByteArray GetMonitorIccProfile(const QWidget* const widget) { xcb_get_property_value_length(profile.get())); } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java b/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java index 440ef6e..7bdd6a7 100644 --- a/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java +++ b/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java @@ -32,7 +32,14 @@ public class Decoder { return new ImageData(basicInfo.width, basicInfo.height, pixels, icc, pixelFormat); } - // TODO(eustas): accept byte-array as input. + public static StreamInfo decodeInfo(byte[] data) { + return decodeInfo(ByteBuffer.wrap(data)); + } + + public static StreamInfo decodeInfo(byte[] data, int offset, int length) { + return decodeInfo(ByteBuffer.wrap(data, offset, length)); + } + public static StreamInfo decodeInfo(Buffer data) { return DecoderJni.getBasicInfo(data, null); } diff --git a/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc b/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc index 1b3847e..d61464e 100644 --- a/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc +++ b/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc @@ -6,13 +6,11 @@ #include "tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h" #include <jni.h> +#include <jxl/decode.h> +#include <jxl/thread_parallel_runner.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> @@ -34,11 +32,11 @@ bool BufferToSpan(JNIEnv* env, jobject buffer, uint8_t** data, size_t* size) { 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. -} +enum class Status { OK = 0, FATAL_ERROR = -1, NOT_ENOUGH_INPUT = 1 }; + +bool IsOk(Status status) { return status == Status::OK; } + +#define FAILURE(M) Status::FATAL_ERROR constexpr const size_t kLastPixelFormat = 3; constexpr const size_t kNoPixelFormat = static_cast<size_t>(-1); @@ -64,28 +62,27 @@ JxlPixelFormat ToPixelFormat(size_t pixel_format) { } } -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"); +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 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"); + return 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"); + return 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"); + return FAILURE("Failed to access ICC buffer"); } JxlDecoder* dec = JxlDecoderCreate(NULL); @@ -105,80 +102,76 @@ jxl::Status DoDecode(JNIEnv* env, jobject data_buffer, size_t* info_pixels_size, auto status = JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner); if (status != JXL_DEC_SUCCESS) { - return JXL_FAILURE("Failed to set parallel runner"); + return 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"); + return FAILURE("Failed to subscribe for events"); } status = JxlDecoderSetInput(dec, data, data_size); if (status != JXL_DEC_SUCCESS) { - return JXL_FAILURE("Failed to set input"); + return FAILURE("Failed to set input"); } status = JxlDecoderProcessInput(dec); if (status == JXL_DEC_NEED_MORE_INPUT) { - return JXL_STATUS(jxl::StatusCode::kNotEnoughBytes, "Not enough input"); + return Status::NOT_ENOUGH_INPUT; } if (status != JXL_DEC_BASIC_INFO) { - return JXL_FAILURE("Unexpected notification (want: basic info)"); + return 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"); + return 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"); + return FAILURE("Failed to get basic info"); } } status = JxlDecoderProcessInput(dec); if (status != JXL_DEC_COLOR_ENCODING) { - return JXL_FAILURE("Unexpected notification (want: color encoding)"); + return 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); + status = JxlDecoderGetICCProfileSize(dec, 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); + status = JxlDecoderGetColorAsICCProfile(dec, JXL_COLOR_PROFILE_TARGET_DATA, + icc, icc_size); if (status != JXL_DEC_SUCCESS) { - return JXL_FAILURE("Failed to get ICC"); + return 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)"); + return 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"); + return FAILURE("Failed to set out buffer"); } status = JxlDecoderProcessInput(dec); if (status != JXL_DEC_FULL_IMAGE) { - return JXL_FAILURE("Unexpected notification (want: full image)"); + return FAILURE("Unexpected notification (want: full image)"); } status = JxlDecoderProcessInput(dec); if (status != JXL_DEC_SUCCESS) { - return JXL_FAILURE("Unexpected notification (want: success)"); + return FAILURE("Unexpected notification (want: success)"); } } - return true; + return Status::OK; } -#undef FAILURE - } // namespace #ifdef __cplusplus @@ -196,18 +189,18 @@ Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetBasicInfo( size_t icc_size = 0; size_t pixel_format = 0; - jxl::Status status = true; + Status status = Status::OK; - if (status) { + if (IsOk(status)) { pixel_format = context[0]; if (pixel_format == kNoPixelFormat) { // OK } else if (pixel_format > kLastPixelFormat) { - status = JXL_FAILURE("Unrecognized pixel format"); + status = FAILURE("Unrecognized pixel format"); } } - if (status) { + if (IsOk(status)) { bool want_output_size = (pixel_format != kNoPixelFormat); if (want_output_size) { status = DoDecode( @@ -221,17 +214,17 @@ Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetBasicInfo( } } - if (status) { + if (IsOk(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"); + if (!ok) status = FAILURE("Invalid value"); } - context[0] = ToStatusCode(status); + context[0] = static_cast<int>(status); env->SetIntArrayRegion(ctx, 0, 6, context); } @@ -251,26 +244,28 @@ JNIEXPORT void JNICALL Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetPixels( size_t pixel_format = 0; - jxl::Status status = true; + Status status = Status::OK; - if (status) { + if (IsOk(status)) { // Unlike getBasicInfo, "no-pixel-format" is not supported. pixel_format = context[0]; if (pixel_format > kLastPixelFormat) { - status = JXL_FAILURE("Unrecognized pixel format"); + status = FAILURE("Unrecognized pixel format"); } } - if (status) { + if (IsOk(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); + context[0] = static_cast<int>(status); env->SetIntArrayRegion(ctx, 0, 1, context); } +#undef FAILURE + #ifdef __cplusplus } #endif diff --git a/tools/jpegli_dec_fuzzer.cc b/tools/jpegli_dec_fuzzer.cc new file mode 100644 index 0000000..12464c6 --- /dev/null +++ b/tools/jpegli_dec_fuzzer.cc @@ -0,0 +1,212 @@ +// 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 <setjmp.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include <hwy/targets.h> +#include <vector> + +#include "lib/jpegli/decode.h" + +namespace { + +// Externally visible value to ensure pixels are used in the fuzzer. +int external_code = 0; + +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; + } + } +} + +// Options for the fuzzing +struct FuzzSpec { + size_t chunk_size; + JpegliDataType output_type; + JpegliEndianness output_endianness; + int crop_output; +}; + +static constexpr uint8_t kFakeEoiMarker[2] = {0xff, 0xd9}; +static constexpr size_t kNumSourceBuffers = 4; + +class SourceManager { + public: + SourceManager(const uint8_t* data, size_t len, size_t max_chunk_size) + : data_(data), len_(len), max_chunk_size_(max_chunk_size) { + pub_.skip_input_data = skip_input_data; + pub_.resync_to_restart = jpegli_resync_to_restart; + pub_.term_source = term_source; + pub_.init_source = init_source; + pub_.fill_input_buffer = fill_input_buffer; + if (max_chunk_size_ == 0) max_chunk_size_ = len; + buffers_.resize(kNumSourceBuffers, std::vector<uint8_t>(max_chunk_size_)); + Reset(); + } + + void Reset() { + pub_.next_input_byte = nullptr; + pub_.bytes_in_buffer = 0; + pos_ = 0; + chunk_idx_ = 0; + } + + private: + jpeg_source_mgr pub_; + const uint8_t* data_; + size_t len_; + size_t chunk_idx_; + size_t pos_; + size_t max_chunk_size_; + std::vector<std::vector<uint8_t>> buffers_; + + static void init_source(j_decompress_ptr cinfo) {} + + static boolean fill_input_buffer(j_decompress_ptr cinfo) { + auto src = reinterpret_cast<SourceManager*>(cinfo->src); + if (src->pos_ < src->len_) { + size_t remaining = src->len_ - src->pos_; + size_t chunk_size = std::min(remaining, src->max_chunk_size_); + size_t next_idx = ++src->chunk_idx_ % kNumSourceBuffers; + // Larger number of chunks causes fuzzer timuout. + if (src->chunk_idx_ >= (1u << 15)) { + chunk_size = remaining; + next_idx = src->buffers_.size(); + src->buffers_.emplace_back(chunk_size); + } + uint8_t* next_buffer = src->buffers_[next_idx].data(); + memcpy(next_buffer, src->data_ + src->pos_, chunk_size); + src->pub_.next_input_byte = next_buffer; + src->pub_.bytes_in_buffer = chunk_size; + } else { + src->pub_.next_input_byte = kFakeEoiMarker; + src->pub_.bytes_in_buffer = 2; + src->len_ += 2; + } + src->pos_ += src->pub_.bytes_in_buffer; + return TRUE; + } + + static void skip_input_data(j_decompress_ptr cinfo, long num_bytes) { + auto src = reinterpret_cast<SourceManager*>(cinfo->src); + if (num_bytes <= 0) { + return; + } + if (src->pub_.bytes_in_buffer >= static_cast<size_t>(num_bytes)) { + src->pub_.bytes_in_buffer -= num_bytes; + src->pub_.next_input_byte += num_bytes; + } else { + src->pos_ += num_bytes - src->pub_.bytes_in_buffer; + src->pub_.bytes_in_buffer = 0; + } + } + + static void term_source(j_decompress_ptr cinfo) {} +}; + +bool DecodeJpeg(const uint8_t* data, size_t size, size_t max_pixels, + const FuzzSpec& spec, std::vector<uint8_t>* pixels, + size_t* xsize, size_t* ysize) { + SourceManager src(data, size, spec.chunk_size); + jpeg_decompress_struct cinfo; + const auto try_catch_block = [&]() -> bool { + jpeg_error_mgr jerr; + jmp_buf env; + cinfo.err = jpegli_std_error(&jerr); + if (setjmp(env)) { + return false; + } + cinfo.client_data = reinterpret_cast<void*>(&env); + cinfo.err->error_exit = [](j_common_ptr cinfo) { + jmp_buf* env = reinterpret_cast<jmp_buf*>(cinfo->client_data); + jpegli_destroy(cinfo); + longjmp(*env, 1); + }; + cinfo.err->emit_message = [](j_common_ptr cinfo, int msg_level) {}; + jpegli_create_decompress(&cinfo); + cinfo.src = reinterpret_cast<jpeg_source_mgr*>(&src); + jpegli_read_header(&cinfo, TRUE); + *xsize = cinfo.image_width; + *ysize = cinfo.image_height; + size_t num_pixels = *xsize * *ysize; + if (num_pixels > max_pixels) return false; + jpegli_set_output_format(&cinfo, spec.output_type, spec.output_endianness); + jpegli_start_decompress(&cinfo); + if (spec.crop_output) { + JDIMENSION xoffset = cinfo.output_width / 3; + JDIMENSION xsize_cropped = cinfo.output_width / 3; + jpegli_crop_scanline(&cinfo, &xoffset, &xsize_cropped); + } + + size_t bytes_per_sample = jpegli_bytes_per_sample(spec.output_type); + size_t stride = + bytes_per_sample * cinfo.output_components * cinfo.output_width; + size_t buffer_size = *ysize * stride; + pixels->resize(buffer_size); + for (size_t y = 0; y < *ysize; ++y) { + JSAMPROW rows[] = {pixels->data() + y * stride}; + jpegli_read_scanlines(&cinfo, rows, 1); + } + Consume(pixels->cbegin(), pixels->cend()); + jpegli_finish_decompress(&cinfo); + return true; + }; + bool success = try_catch_block(); + jpegli_destroy_decompress(&cinfo); + return success; +} + +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; + spec.output_type = static_cast<JpegliDataType>(getFlag(JPEGLI_TYPE_UINT16)); + spec.output_endianness = + static_cast<JpegliEndianness>(getFlag(JPEGLI_BIG_ENDIAN)); + uint32_t chunks = getFlag(15); + spec.chunk_size = chunks ? 1u << (chunks - 1) : 0; + spec.crop_output = getFlag(1); + + std::vector<uint8_t> pixels; + size_t xsize, ysize; + size_t max_pixels = 1 << 21; + + const auto targets = hwy::SupportedAndGeneratedTargets(); + hwy::SetSupportedTargetsForTest(targets[getFlag(targets.size() - 1)]); + DecodeJpeg(data, size, max_pixels, spec, &pixels, &xsize, &ysize); + hwy::SetSupportedTargetsForTest(0); + + return 0; +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return TestOneInput(data, size); +} diff --git a/tools/jpegli_dec_fuzzer_corpus.cc b/tools/jpegli_dec_fuzzer_corpus.cc new file mode 100644 index 0000000..0963e66 --- /dev/null +++ b/tools/jpegli_dec_fuzzer_corpus.cc @@ -0,0 +1,365 @@ +// 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 <setjmp.h> +#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 <iostream> +#include <mutex> +#include <random> +#include <vector> + +#include "lib/jpegli/encode.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/random.h" +#include "tools/file_io.h" +#include "tools/thread_pool_internal.h" + +namespace { + +const size_t kMaxWidth = 50000; +const size_t kMaxHeight = 50000; +const size_t kMaxPixels = 20 * (1 << 20); // 20 MP + +std::mutex stderr_mutex; + +std::vector<uint8_t> GetSomeTestImage(size_t xsize, size_t ysize, + size_t num_channels, uint16_t seed) { + // Cause more significant image difference for successive seeds. + jxl::Rng generator(seed); + + // Returns random integer in interval [0, max_value) + auto rng = [&generator](size_t max_value) -> size_t { + return generator.UniformU(0, max_value); + }; + + // Dark background gradient color + uint16_t r0 = rng(32768); + uint16_t g0 = rng(32768); + uint16_t b0 = rng(32768); + uint16_t r1 = rng(32768); + uint16_t g1 = rng(32768); + uint16_t b1 = rng(32768); + + // Circle with different color + size_t circle_x = rng(xsize); + size_t circle_y = rng(ysize); + size_t circle_r = rng(std::min(xsize, ysize)); + + // Rectangle with random noise + size_t rect_x0 = rng(xsize); + size_t rect_y0 = rng(ysize); + size_t rect_x1 = rng(xsize); + size_t rect_y1 = rng(ysize); + if (rect_x1 < rect_x0) std::swap(rect_x0, rect_y1); + if (rect_y1 < rect_y0) std::swap(rect_y0, rect_y1); + + size_t num_pixels = xsize * ysize; + std::vector<uint8_t> pixels(num_pixels * num_channels); + // Create pixel content to test. + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xsize; x++) { + uint16_t r = r0 * (ysize - y - 1) / ysize + r1 * y / ysize; + uint16_t g = g0 * (ysize - y - 1) / ysize + g1 * y / ysize; + uint16_t b = b0 * (ysize - y - 1) / ysize + b1 * y / ysize; + // put some shape in there for visual debugging + if ((x - circle_x) * (x - circle_x) + (y - circle_y) * (y - circle_y) < + circle_r * circle_r) { + r = (65535 - x * y) ^ seed; + g = (x << 8) + y + seed; + b = (y << 8) + x * seed; + } else if (x > rect_x0 && x < rect_x1 && y > rect_y0 && y < rect_y1) { + r = rng(65536); + g = rng(65536); + b = rng(65536); + } + size_t i = (y * xsize + x) * num_channels; + pixels[i + 0] = (r >> 8); + if (num_channels == 3) { + pixels[i + 1] = (g >> 8); + pixels[i + 2] = (b >> 8); + } + } + } + return pixels; +} + +// ImageSpec needs to be a packed struct to allow us to use the raw memory of +// the struct for hashing to create a consistent id. +#pragma pack(push, 1) +struct ImageSpec { + bool Validate() const { + if (width > kMaxWidth || height > kMaxHeight || + width * height > kMaxPixels) { + 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 << " q=" << spec.quality + << " p=" << spec.progressive_level << " r=" << spec.restart_interval + << ">"; + 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); + } + + uint32_t width = 256; + uint32_t height = 256; + uint32_t num_channels = 3; + uint32_t quality = 90; + uint32_t sampling = 0x11111111; + uint32_t progressive_level = 2; + uint32_t restart_interval = 0; + uint32_t fraction = 100; + // The seed for the PRNG. + uint32_t seed = 7777; +}; +#pragma pack(pop) +static_assert(sizeof(ImageSpec) % 4 == 0, "Add padding to ImageSpec."); + +bool EncodeWithJpegli(const ImageSpec& spec, const std::vector<uint8_t>& pixels, + std::vector<uint8_t>* compressed) { + uint8_t* buffer = nullptr; + unsigned long buffer_size = 0; + jpeg_compress_struct cinfo; + const auto try_catch_block = [&]() -> bool { + jpeg_error_mgr jerr; + jmp_buf env; + cinfo.err = jpegli_std_error(&jerr); + if (setjmp(env)) { + return false; + } + cinfo.client_data = reinterpret_cast<void*>(&env); + cinfo.err->error_exit = [](j_common_ptr cinfo) { + (*cinfo->err->output_message)(cinfo); + jmp_buf* env = reinterpret_cast<jmp_buf*>(cinfo->client_data); + jpegli_destroy(cinfo); + longjmp(*env, 1); + }; + jpegli_create_compress(&cinfo); + jpegli_mem_dest(&cinfo, &buffer, &buffer_size); + cinfo.image_width = spec.width; + cinfo.image_height = spec.height; + cinfo.input_components = spec.num_channels; + cinfo.in_color_space = spec.num_channels == 1 ? JCS_GRAYSCALE : JCS_RGB; + jpegli_set_defaults(&cinfo); + jpegli_set_quality(&cinfo, spec.quality, TRUE); + uint32_t sampling = spec.sampling; + for (int c = 0; c < cinfo.num_components; ++c) { + cinfo.comp_info[c].h_samp_factor = sampling & 0xf; + cinfo.comp_info[c].v_samp_factor = (sampling >> 4) & 0xf; + sampling >>= 8; + } + jpegli_set_progressive_level(&cinfo, spec.progressive_level); + cinfo.restart_interval = spec.restart_interval; + jpegli_start_compress(&cinfo, TRUE); + size_t stride = cinfo.image_width * cinfo.input_components; + std::vector<uint8_t> row_bytes(stride); + for (size_t y = 0; y < cinfo.image_height; ++y) { + memcpy(&row_bytes[0], &pixels[y * stride], stride); + JSAMPROW row[] = {row_bytes.data()}; + jpegli_write_scanlines(&cinfo, row, 1); + } + jpegli_finish_compress(&cinfo); + return true; + }; + bool success = try_catch_block(); + jpegli_destroy_compress(&cinfo); + if (success) { + buffer_size = buffer_size * spec.fraction / 100; + compressed->assign(buffer, buffer + buffer_size); + } + if (buffer) std::free(buffer); + return success; +} + +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 + ".jpg"; + + // 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; + } + + uint8_t hash[16]; + spec.SpecHash(hash); + std::mt19937 mt(spec.seed); + + std::vector<uint8_t> pixels = + GetSomeTestImage(spec.width, spec.height, spec.num_channels, spec.seed); + std::vector<uint8_t> compressed; + JXL_CHECK(EncodeWithJpegli(spec, pixels, &compressed)); + + // Append 4 bytes with the flags used by jpegli_dec_fuzzer to select the + // decoding output. + std::uniform_int_distribution<> dis256(0, 255); + for (size_t i = 0; i < 4; ++i) { + compressed.push_back(dis256(mt)); + } + + if (!jpegxl::tools::WriteFile(output_fn, compressed)) { + return false; + } + if (!quiet) { + std::unique_lock<std::mutex> lock(stderr_mutex); + std::cerr << "Stored " << output_fn << " size: " << compressed.size() + << std::endl; + } + + return true; +} + +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; + size_t 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; + } + + std::mt19937 mt(77777); + + std::vector<std::pair<uint32_t, uint32_t>> image_sizes = { + {8, 8}, {32, 32}, {128, 128}, {10000, 1}, {10000, 2}, {1, 10000}, + {2, 10000}, {555, 256}, {257, 513}, {512, 265}, {264, 520}, + }; + std::vector<uint32_t> sampling_ratios = { + 0x11111111, // 444 + 0x11111112, // 422 + 0x11111121, // 440 + 0x11111122, // 420 + 0x11222211, // luma subsampling + }; + + ImageSpec spec; + std::vector<ImageSpec> specs; + for (auto img_size : image_sizes) { + spec.width = img_size.first; + spec.height = img_size.second; + for (uint32_t num_channels : {1, 3}) { + spec.num_channels = num_channels; + for (uint32_t sampling : sampling_ratios) { + spec.sampling = sampling; + if (num_channels == 1 && sampling != 0x11111111) continue; + for (uint32_t restart : {0, 1, 1024}) { + spec.restart_interval = restart; + for (uint32_t prog_level : {0, 1, 2}) { + spec.progressive_level = prog_level; + for (uint32_t quality : {10, 90, 100}) { + spec.quality = quality; + for (uint32_t fraction : {10, 70, 100}) { + spec.fraction = fraction; + spec.seed = mt() % 777777; + if (!spec.Validate()) { + if (!quiet) { + std::cerr << "Skipping " << spec << std::endl; + } + } else { + specs.push_back(spec); + } + } + } + } + } + } + } + } + + jpegxl::tools::ThreadPoolInternal pool{num_threads}; + const auto generate = [&specs, dest_dir, regenerate, quiet]( + const uint32_t task, size_t /* thread */) { + const ImageSpec& spec = specs[task]; + GenerateFile(dest_dir, spec, regenerate, quiet); + }; + if (!RunOnPool(&pool, 0, specs.size(), jxl::ThreadPool::NoInit, generate, + "FuzzerCorpus")) { + std::cerr << "Error generating fuzzer corpus" << std::endl; + return 1; + } + std::cerr << "Finished generating fuzzer corpus" << std::endl; + return 0; +} diff --git a/tools/jxl_from_tree.cc b/tools/jxl_from_tree.cc index aa85ff8..92a0874 100644 --- a/tools/jxl_from_tree.cc +++ b/tools/jxl_from_tree.cc @@ -3,17 +3,18 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/cms.h> #include <stdio.h> #include <string.h> #include <fstream> #include <iostream> +#include <istream> #include <unordered_map> -#include "lib/jxl/base/file_io.h" +#include "lib/jxl/codec_in_out.h" #include "lib/jxl/enc_cache.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_fields.h" #include "lib/jxl/enc_frame.h" #include "lib/jxl/enc_heuristics.h" #include "lib/jxl/modular/encoding/context_predict.h" @@ -21,8 +22,33 @@ #include "lib/jxl/modular/encoding/enc_ma.h" #include "lib/jxl/modular/encoding/encoding.h" #include "lib/jxl/splines.h" +#include "lib/jxl/test_utils.h" // TODO(eustas): cut this dependency +#include "tools/file_io.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::BitWriter; +using ::jxl::BlendMode; +using ::jxl::CodecInOut; +using ::jxl::CodecMetadata; +using ::jxl::ColorCorrelationMap; +using ::jxl::ColorEncoding; +using ::jxl::ColorTransform; +using ::jxl::CompressParams; +using ::jxl::DefaultEncoderHeuristics; +using ::jxl::FrameDimensions; +using ::jxl::FrameInfo; +using ::jxl::Image3F; +using ::jxl::ImageF; +using ::jxl::PaddedBytes; +using ::jxl::PassesEncoderState; +using ::jxl::Predictor; +using ::jxl::PropertyDecisionNode; +using ::jxl::QuantizedSpline; +using ::jxl::Spline; +using ::jxl::Splines; +using ::jxl::Tree; namespace { struct SplineData { @@ -196,7 +222,7 @@ bool ParseNode(F& tok, Tree& tree, SplineData& spline_data, } 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); + io.frames[0].SetAlpha(std::move(alpha)); } else if (t == "Bitdepth") { t = tok(); size_t num = 0; @@ -392,7 +418,7 @@ bool ParseNode(F& tok, Tree& tree, SplineData& spline_data, class Heuristics : public DefaultEncoderHeuristics { public: - bool CustomFixedTreeLossless(const jxl::FrameDimensions& frame_dim, + bool CustomFixedTreeLossless(const FrameDimensions& frame_dim, Tree* tree) override { *tree = tree_; return true; @@ -412,16 +438,24 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) { size_t width = 1024, height = 1024; int x0 = 0, y0 = 0; cparams.SetLossless(); + cparams.responsive = false; cparams.resampling = 1; cparams.ec_resampling = 1; cparams.modular_group_size_shift = 3; CodecInOut io; int have_next = 0; - std::ifstream f(in); + std::istream* f = &std::cin; + std::ifstream file; + + if (strcmp(in, "-")) { + file.open(in, std::ifstream::in); + f = &file; + } + auto tok = [&f]() { std::string out; - f >> out; + *f >> out; return out; }; if (!ParseNode(tok, tree, spline_data, cparams, width, height, io, have_next, @@ -436,7 +470,7 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) { io.SetFromImage(std::move(image), ColorEncoding::SRGB()); io.SetSize((width + x0) * cparams.resampling, (height + y0) * cparams.resampling); - io.metadata.m.color_encoding.DecideIfWantICC(); + io.metadata.m.color_encoding.DecideIfWantICC(*JxlGetDefaultCms()); cparams.options.zero_tokens = true; cparams.palette_colors = 0; cparams.channel_colors_pre_transform_percent = 0; @@ -452,14 +486,14 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) { *metadata = io.metadata; JXL_RETURN_IF_ERROR(metadata->size.Set(io.xsize(), io.ysize())); - metadata->m.xyb_encoded = cparams.color_transform == ColorTransform::kXYB; + metadata->m.xyb_encoded = (cparams.color_transform == ColorTransform::kXYB); - JXL_RETURN_IF_ERROR(WriteHeaders(metadata.get(), &writer, nullptr)); + JXL_RETURN_IF_ERROR(WriteCodestreamHeaders(metadata.get(), &writer, nullptr)); writer.ZeroPadToByte(); while (true) { PassesEncoderState enc_state; - enc_state.heuristics = make_unique<Heuristics>(tree); + enc_state.heuristics = jxl::make_unique<Heuristics>(tree); enc_state.shared.image_features.splines = SplinesFromSplineData(spline_data, enc_state.shared.cmap); @@ -469,14 +503,16 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) { io.frames[0].origin.x0 = x0; io.frames[0].origin.y0 = y0; + info.clamp = false; - JXL_RETURN_IF_ERROR(EncodeFrame(cparams, info, metadata.get(), io.frames[0], - &enc_state, GetJxlCms(), nullptr, &writer, - nullptr)); + JXL_RETURN_IF_ERROR(jxl::EncodeFrame( + cparams, info, metadata.get(), io.frames[0], &enc_state, + *JxlGetDefaultCms(), nullptr, &writer, nullptr)); if (!have_next) break; tree.clear(); spline_data.splines.clear(); have_next = 0; + cparams.manual_noise.clear(); if (!ParseNode(tok, tree, spline_data, cparams, width, height, io, have_next, x0, y0)) { return 1; @@ -488,19 +524,22 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) { compressed = std::move(writer).TakeBytes(); - if (!WriteFile(compressed, out)) { + if (!WriteFile(out, compressed)) { fprintf(stderr, "Failed to write to \"%s\"\n", out); return 1; } return 0; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl int main(int argc, char** argv) { - if ((argc != 3 && argc != 4) || !strcmp(argv[1], argv[2])) { + if ((argc != 3 && argc != 4) || + (strcmp(argv[1], "-") && !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]); + return jpegxl::tools::JxlFromTree(argv[1], argv[2], + argc < 4 ? nullptr : argv[3]); } diff --git a/tools/jxlinfo.c b/tools/jxlinfo.c index d8d67e7..e7d23ee 100644 --- a/tools/jxlinfo.c +++ b/tools/jxlinfo.c @@ -6,13 +6,12 @@ // This example prints information from the main codestream header. #include <inttypes.h> +#include <jxl/decode.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; @@ -90,7 +89,7 @@ int PrintBasicInfo(FILE* file, int verbose) { if (info.exponent_bits_per_sample) { printf("float (%d exponent bits) ", info.exponent_bits_per_sample); } - int cmyk = 0, alpha = 0; + int cmyk = 0; const char* const ec_type_names[] = { "Alpha", "Depth", "Spotcolor", "Selection", "Black", "CFA", "Thermal", "Reserved0", "Reserved1", "Reserved2", @@ -105,17 +104,12 @@ int PrintBasicInfo(FILE* file, int verbose) { 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; + printf("CMY"); } else { printf("RGB"); } @@ -126,15 +120,6 @@ int PrintBasicInfo(FILE* file, int verbose) { 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 < ec_type_names_size ? ec_type_names[extra.type] : "Unknown, please update your libjxl")); @@ -229,14 +214,12 @@ int PrintBasicInfo(FILE* file, int verbose) { 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)) { + JxlDecoderGetColorAsEncodedProfile( + dec, 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"}; @@ -280,8 +263,7 @@ int PrintBasicInfo(FILE* file, int verbose) { // instead. size_t profile_size; if (JXL_DEC_SUCCESS != - JxlDecoderGetICCProfileSize(dec, &format, - JXL_COLOR_PROFILE_TARGET_ORIGINAL, + JxlDecoderGetICCProfileSize(dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &profile_size)) { fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); continue; @@ -292,10 +274,9 @@ int PrintBasicInfo(FILE* file, int verbose) { continue; } uint8_t* profile = (uint8_t*)malloc(profile_size); - if (JXL_DEC_SUCCESS != - JxlDecoderGetColorAsICCProfile(dec, &format, - JXL_COLOR_PROFILE_TARGET_ORIGINAL, - profile, profile_size)) { + if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile( + dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL, + profile, profile_size)) { fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); free(profile); continue; @@ -326,11 +307,11 @@ int PrintBasicInfo(FILE* file, int verbose) { } 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) { + float ms = frame_header.duration * 1000.f * + info.animation.tps_denominator / + info.animation.tps_numerator; + total_duration += ms; printf(", duration: %.1f ms", ms); if (info.animation.have_timecodes) { printf(", time code: %X", frame_header.timecode); diff --git a/tools/libjxl_test.c b/tools/libjxl_test.c index bb57c2d..f56a1fa 100644 --- a/tools/libjxl_test.c +++ b/tools/libjxl_test.c @@ -7,9 +7,9 @@ // This links against the shared libjpegxl library which doesn't expose any of // the internals of the jxl namespace. -#include "jxl/decode.h" +#include <jxl/decode.h> -int main() { +int main(void) { if (!JxlDecoderVersion()) return 1; JxlDecoder* dec = JxlDecoderCreate(NULL); if (!dec) return 1; diff --git a/tools/optimizer/apply_simplex.py b/tools/optimizer/apply_simplex.py new file mode 100755 index 0000000..273305b --- /dev/null +++ b/tools/optimizer/apply_simplex.py @@ -0,0 +1,111 @@ +#!/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. + +"""apply_simplex.py: Updates constants based on results of simplex search. + +To use this tool, the simplex search parameters must we wrapped in a bias(n) +function call that returns the value of the VARn environment variable. The +tool reads a text file containing the simplex definition that simplex_fork.py +has written, and updates the target source files by substituting the bias(n) +function calls with the (n+1)th coordinate of the simplex vector, and also +simplifies these expressions by evaluating them to a sinlge floating point +literal. + +The tool recognizes and evaluates the following expressions: + <constant> + bias(n), + <constant> * bias(n), + <constant> + <coeff> * bias(n). + +The --keep_bias command-line flag can be used to continue an aborted simplex +search. This will keep the same bias(n) terms in the code, but would update the +surronding constants. + +The --index_min and --index_max flags can be used to update only a subset of the +bias(n) parameters. +""" + +import argparse +import re +import sys + +def ParseSimplex(fn): + """Returns the simplex definition written by simplex_fork.py""" + + with open(fn, "r") as f: + line = f.readline() + vec = eval(line) + return vec + + +def PythonExpr(c_expr): + """Removes the f at the end of float literals""" + + def repl(m): + return m.group(1) + + return re.sub("(\d+)f", repl, c_expr) + + +def UpdateSourceFile(fn, vec, keep_bias, id_min, id_max, minval): + """Updates expressions containing a bias(N) term.""" + + with open(fn, "r") as f: + lines_in = f.readlines() + lines_out = [] + rbias = "(bias\((\d+)\))" + r = " -?\d+\.\d+f?( (\+|-|\*) (\d+\.\d+f? \* )?" + rbias + ")" + for line in lines_in: + line_out = line + x = re.search(r, line) + if x: + id = int(x.group(5)) + if id >= id_min and id <= id_max: + expr = re.sub(rbias, str(vec[id + 1]), x.group(0)) + val = eval(PythonExpr(expr)) + if minval and val < minval: + val = minval + expr_out = " " + str(val) + "f" + if keep_bias: + expr_out += x.group(1) + line_out = re.sub(r, expr_out, line) + lines_out.append(line_out) + + with open(fn, "w") as f: + f.writelines(lines_out) + f.close() + + +def ApplySimplex(args): + """Main entry point of the program after parsing parameters.""" + + vec = ParseSimplex(args.simplex) + for fn in args.target: + UpdateSourceFile(fn, vec, args.keep_bias, args.index_min, args.index_max, + args.minval) + return 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('target', type=str, nargs='+', + help='source file(s) to update') + parser.add_argument('--simplex', default='best_simplex.txt', + help='simplex to apply to the code') + parser.add_argument('--keep_bias', default=False, action='store_true', + help='keep the bias term in the code, can be used to ' + + 'continue simplex search') + parser.add_argument('--index_min', type=int, default=0, + help='start index of the simplex to apply') + parser.add_argument('--index_max', type=int, default=9999, + help='last index of the simplex to apply') + parser.add_argument('--minval', type=float, default=None, + help='apply a minimum to expression results') + args = parser.parse_args() + sys.exit(ApplySimplex(args)) + + +if __name__ == '__main__': + main() diff --git a/tools/optimizer/simplex_fork.py b/tools/optimizer/simplex_fork.py index 20de4c9..f29e190 100755 --- a/tools/optimizer/simplex_fork.py +++ b/tools/optimizer/simplex_fork.py @@ -53,6 +53,7 @@ def Average(a, b): eval_hash = {} +g_best_val = None def EvalCacheForget(): global eval_hash @@ -60,19 +61,18 @@ def EvalCacheForget(): def RandomizedJxlCodecs(): retval = [] - minval = 0.5 - maxval = 3.3 + minval = 0.2 + maxval = 9.3 rangeval = maxval/minval - steps = 7 + steps = 13 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 + retval.append("jxl:d%.4f" % mul) 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) + retval.append("jxl:d%.4f" % mul) return ",".join(retval) g_codecs = RandomizedJxlCodecs() @@ -87,6 +87,7 @@ def Eval(vec, binary_name, cached=True): """ global eval_hash global g_codecs + global g_best_val key = "" # os.environ["BUTTERAUGLI_OPTIMIZE"] = "1" for i in range(300): @@ -101,8 +102,8 @@ def Eval(vec, binary_name, cached=True): process = subprocess.Popen( (binary_name, '--input', - '/usr/local/google/home/jyrki/mix_corpus/*.png', - '--error_pnorm=3', + '/usr/local/google/home/jyrki/newcorpus/split/*.png', + '--error_pnorm=3.0', '--more_columns', '--codec', g_codecs), stdout=subprocess.PIPE, @@ -122,20 +123,26 @@ def Eval(vec, binary_name, cached=True): sys.stdout.flush() if line[0:3] == b'jxl': bpp = line.split()[3] - dist_pnorm = line.split()[7] + dist_pnorm = line.split()[9] + dist_max = line.split()[6] vec[0] *= float(dist_pnorm) * float(bpp) / 16.0 - #vec[0] *= (float(dist_max) * float(bpp) / 16.0) ** 0.2 + #vec[0] *= (float(dist_max) * float(bpp) / 16.0) ** 0.01 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 + faultybpp = 1.0 + 0.43 * ((float(bpp) * distance ** 0.69) - 1.64) ** 2 + vec[0] *= faultybpp print("eval: ", vec) if (vec[0] <= 0.0): vec[0] = 1e30 if found_score: eval_hash[key] = vec[0] + if not g_best_val or vec[0] < g_best_val: + g_best_val = vec[0] + print("\nSaving best simplex\n") + with open("best_simplex.txt", "w") as f: + print(vec, file=f) return vec[0] = 1e33 return @@ -242,7 +249,7 @@ 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): + for ii in range(g_dim * 5): g_simplex.sort() print("reflect", ii, g_simplex[0]) Reflect(g_simplex, g_binary) diff --git a/tools/optimizer/update_jpegli_global_scale.py b/tools/optimizer/update_jpegli_global_scale.py new file mode 100755 index 0000000..1a57c59 --- /dev/null +++ b/tools/optimizer/update_jpegli_global_scale.py @@ -0,0 +1,103 @@ +#!/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. + +"""Script to update jpegli global scale after a change affecting quality. + +start as ./update_jpegli_global_scale.py build <corpus-dir> +""" + +import os +import re +import subprocess +import sys + +def SourceFileName(): + return "lib/jpegli/quant.cc" + +def ScalePattern(scale_type): + return "constexpr float kGlobalScale" + scale_type + " = "; + +def CodecName(scale_type): + if scale_type == "YCbCr": + return "jpeg:enc-jpegli:q90" + elif scale_type == "XYB": + return "jpeg:enc-jpegli:xyb:q90" + else: + raise Exception("Unknown scale type %s" % scale_type) + +def ReadGlobalScale(scale_type): + pattern = ScalePattern(scale_type) + with open(SourceFileName()) as f: + for line in f.read().splitlines(): + if line.startswith(pattern): + return float(line[len(pattern):-2]) + raise Exception("Global scale %s not found." % scale_type) + + +def UpdateGlobalScale(scale_type, new_val): + pattern = ScalePattern(scale_type) + found_pattern = False + fdata = "" + with open(SourceFileName()) as f: + for line in f.read().splitlines(): + if line.startswith(pattern): + fdata += pattern + "%.8ff;\n" % new_val + found_pattern = True + else: + fdata += line + "\n" + if not found_pattern: + raise Exception("Global scale %s not found." % scale_type) + with open(SourceFileName(), "w") as f: + f.write(fdata) + f.close() + +def EvalPnorm(build_dir, corpus_dir, codec): + compile_args = ["ninja", "-C", build_dir, "tools/benchmark_xl"] + try: + subprocess.check_output(compile_args) + except: + subprocess.check_call(compile_args) + process = subprocess.Popen( + (os.path.join(build_dir, "tools/benchmark_xl"), + "--input", os.path.join(corpus_dir, "*.png"), + "--codec", codec), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (out, err) = process.communicate(input=None) + for line in out.splitlines(): + if line.startswith(codec): + return float(line.split()[8]) + raise Exception("Unexpected benchmark output:\n%sstderr:\n%s" % (out, err)) + + +if len(sys.argv) != 3: + print("usage: ", sys.argv[0], "build-dir corpus-dir") + exit(1) + +build_dir = sys.argv[1] +corpus_dir = sys.argv[2] + +jpeg_pnorm = EvalPnorm(build_dir, corpus_dir, "jpeg:q90") + +print("Libjpeg pnorm: %.8f" % jpeg_pnorm) + +for scale_type in ["YCbCr", "XYB"]: + scale = ReadGlobalScale(scale_type) + best_scale = scale + best_rel_error = 100.0 + for i in range(10): + jpegli_pnorm = EvalPnorm(build_dir, corpus_dir, CodecName(scale_type)) + rel_error = abs(jpegli_pnorm / jpeg_pnorm - 1) + print("[%-5s] scale: %.8f pnorm: %.8f error: %.8f" % + (scale_type, scale, jpegli_pnorm, rel_error)) + if rel_error < best_rel_error: + best_rel_error = rel_error + best_scale = scale + if rel_error < 0.0001: + break + scale = scale * jpeg_pnorm / jpegli_pnorm + UpdateGlobalScale(scale_type, scale) + UpdateGlobalScale(scale_type, best_scale) diff --git a/tools/rans_fuzzer.cc b/tools/rans_fuzzer.cc index 7c78f0d..16fa99a 100644 --- a/tools/rans_fuzzer.cc +++ b/tools/rans_fuzzer.cc @@ -3,10 +3,21 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" #include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" #include "lib/jxl/entropy_coder.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::ANSCode; +using ::jxl::ANSSymbolReader; +using ::jxl::BitReader; +using ::jxl::BitReaderScopedCloser; +using ::jxl::Bytes; +using ::jxl::Status; int TestOneInput(const uint8_t* data, size_t size) { if (size < 2) return 0; @@ -17,7 +28,7 @@ int TestOneInput(const uint8_t* data, size_t size) { std::vector<uint8_t> context_map; Status ret = true; { - BitReader br(Span<const uint8_t>(data, size)); + BitReader br(Bytes(data, size)); BitReaderScopedCloser br_closer(&br, &ret); ANSCode code; JXL_RETURN_IF_ERROR( @@ -28,7 +39,7 @@ int TestOneInput(const uint8_t* data, size_t size) { const size_t maxreads = size * 8; size_t numreads = 0; int context = 0; - while (DivCeil(br.TotalBitsConsumed(), kBitsPerByte) < size && + while (jxl::DivCeil(br.TotalBitsConsumed(), jxl::kBitsPerByte) < size && numreads <= maxreads) { int code = ansreader.ReadHybridUint(context, &br, context_map); context = code % numContexts; @@ -39,8 +50,9 @@ int TestOneInput(const uint8_t* data, size_t size) { return 0; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - return jxl::TestOneInput(data, size); + return jpegxl::tools::TestOneInput(data, size); } diff --git a/tools/bisector b/tools/scripts/bisector index 2552045..b6a82d0 100755 --- a/tools/bisector +++ b/tools/scripts/bisector @@ -1,4 +1,10 @@ #!/usr/bin/env 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. + r"""General-purpose bisector Prints a space-separated list of values to stdout: diff --git a/tools/scripts/build_cleaner.py b/tools/scripts/build_cleaner.py new file mode 100755 index 0000000..0185fc5 --- /dev/null +++ b/tools/scripts/build_cleaner.py @@ -0,0 +1,270 @@ +#!/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 locale +import os +import re +import subprocess +import sys +import tempfile + + +HEAD = """# 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 file is generated, do not modify by manually. +# Run `tools/scripts/build_cleaner.py --update` to regenerate it. +""" + + +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 Check(condition, msg): + if not condition: + print(msg) + sys.exit(2) + + +def ContainsFn(*parts): + return lambda path: any(part in path for part in parts) + + +def HasPrefixFn(*prefixes): + return lambda path: any(path.startswith(prefix) for prefix in prefixes) + + +def HasSuffixFn(*suffixes): + return lambda path: any(path.endswith(suffix) for suffix in suffixes) + + +def Filter(src, fn): + yes_list = [] + no_list = [] + for item in src: + (yes_list if fn(item) else no_list).append(item) + return yes_list, no_list + + +def SplitLibFiles(repo_files): + """Splits the library files into the different groups.""" + + srcs_base = 'lib/' + srcs, _ = Filter(repo_files, HasPrefixFn(srcs_base)) + srcs = [path[len(srcs_base):] for path in srcs] + srcs, _ = Filter(srcs, HasSuffixFn('.cc', '.h', '.ui')) + srcs.sort() + + # Let's keep Jpegli sources a bit separate for a while. + jpegli_srcs, srcs = Filter(srcs, HasPrefixFn('jpegli')) + # TODO(eustas): move to tools? + _, srcs = Filter(srcs, HasSuffixFn('gbench_main.cc')) + + # First pick files scattered across directories. + tests, srcs = Filter(srcs, HasSuffixFn('_test.cc')) + jpegli_tests, jpegli_srcs = Filter(jpegli_srcs, HasSuffixFn('_test.cc')) + # TODO(eustas): move to separate list? + _, srcs = Filter(srcs, ContainsFn('testing.h')) + _, jpegli_srcs = Filter(jpegli_srcs, ContainsFn('testing.h')) + testlib_files, srcs = Filter(srcs, ContainsFn('test')) + jpegli_testlib_files, jpegli_srcs = Filter(jpegli_srcs, ContainsFn('test')) + jpegli_libjpeg_helper_files, jpegli_testlib_files = Filter( + jpegli_testlib_files, ContainsFn('libjpeg_test_util')) + gbench_sources, srcs = Filter(srcs, HasSuffixFn('_gbench.cc')) + + extras_sources, srcs = Filter(srcs, HasPrefixFn('extras/')) + lib_srcs, srcs = Filter(srcs, HasPrefixFn('jxl/')) + public_headers, srcs = Filter(srcs, HasPrefixFn('include/jxl/')) + threads_sources, srcs = Filter(srcs, HasPrefixFn('threads/')) + + Check(len(srcs) == 0, 'Orphan source files: ' + str(srcs)) + + base_sources, lib_srcs = Filter(lib_srcs, HasPrefixFn('jxl/base/')) + + jpegli_wrapper_sources, jpegli_srcs = Filter( + jpegli_srcs, HasSuffixFn('libjpeg_wrapper.cc')) + jpegli_sources = jpegli_srcs + + threads_public_headers, public_headers = Filter( + public_headers, ContainsFn('_parallel_runner')) + + codec_names = ['apng', 'exr', 'gif', 'jpegli', 'jpg', 'jxl', 'npy', 'pgx', + 'pnm'] + codecs = {} + for codec in codec_names: + codec_sources, extras_sources = Filter(extras_sources, HasPrefixFn( + f'extras/dec/{codec}', f'extras/enc/{codec}')) + codecs[f'codec_{codec}_sources'] = codec_sources + + # TODO(eustas): move to separate folder? + extras_for_tools_sources, extras_sources = Filter(extras_sources, ContainsFn( + '/codec', '/hlg', '/metrics', '/packed_image_convert', '/render_hdr', + '/tone_mapping')) + + # Source files only needed by the encoder or by tools (including decoding + # tools), but not by the decoder library. + # TODO(eustas): investigate the status of codec_in_out.h + # TODO(eustas): rename butteraugli_wrapper.cc to butteraugli.cc? + # TODO(eustas): is it possible to make butteraugli more standalone? + enc_sources, lib_srcs = Filter(lib_srcs, ContainsFn('/enc_', '/butteraugli', + 'jxl/encode.cc', 'jxl/encode_internal.h' + )) + + # The remaining of the files are in the dec_library. + dec_jpeg_sources, dec_sources = Filter(lib_srcs, HasPrefixFn('jxl/jpeg/', + 'jxl/decode_to_jpeg.cc', 'jxl/decode_to_jpeg.h')) + dec_box_sources, dec_sources = Filter(dec_sources, HasPrefixFn( + 'jxl/box_content_decoder.cc', 'jxl/box_content_decoder.h')) + cms_sources, dec_sources = Filter(dec_sources, HasPrefixFn('jxl/cms/')) + + # TODO(lode): further prune dec_srcs: only those files that the decoder + # absolutely needs, and or not only for encoding, should be listed here. + + return codecs | {'base_sources': base_sources, + 'cms_sources': cms_sources, + 'dec_box_sources': dec_box_sources, + 'dec_jpeg_sources': dec_jpeg_sources, + 'dec_sources': dec_sources, + 'enc_sources': enc_sources, + 'extras_for_tools_sources': extras_for_tools_sources, + 'extras_sources': extras_sources, + 'gbench_sources': gbench_sources, + 'jpegli_sources': jpegli_sources, + 'jpegli_testlib_files': jpegli_testlib_files, + 'jpegli_libjpeg_helper_files': jpegli_libjpeg_helper_files, + 'jpegli_tests': jpegli_tests, + 'jpegli_wrapper_sources' : jpegli_wrapper_sources, + 'public_headers': public_headers, + 'testlib_files': testlib_files, + 'tests': tests, + 'threads_public_headers': threads_public_headers, + 'threads_sources': threads_sources, + } + + +def MaybeUpdateFile(args, filename, new_text): + """Optionally replace file with new contents. + + If args.update is set, it will update the file with the new contents, + otherwise it will return True when no changes were needed. + """ + filepath = os.path.join(args.src_dir, filename) + with open(filepath, 'r') as f: + src_text = f.read() + + 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: + prefix = os.path.basename(filename) + with tempfile.NamedTemporaryFile(mode='w', prefix=prefix) 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 FormatList(items, prefix, suffix): + return ''.join(f'{prefix}{item}{suffix}\n' for item in items) + + +def FormatGniVar(name, var): + if type(var) is list: + contents = FormatList(var, ' "', '",') + return f'{name} = [\n{contents}]\n' + else: # TODO(eustas): do we need scalar strings? + return f'{name} = {var}\n' + + +def FormatCMakeVar(name, var): + if type(var) is list: + contents = FormatList(var, ' ', '') + return f'set({name}\n{contents})\n' + else: # TODO(eustas): do we need scalar strings? + return f'set({name} {var})\n' + + +def GetJpegLibVersion(src_dir): + with open(os.path.join(src_dir, 'CMakeLists.txt'), 'r') as f: + cmake_text = f.read() + m = re.search(r'set\(JPEGLI_LIBJPEG_LIBRARY_SOVERSION "([0-9]+)"', + cmake_text) + version = m.group(1) + if len(version) == 1: + version += "0" + return version + + +def BuildCleaner(args): + repo_files = RepoFiles(args.src_dir) + + with open(os.path.join(args.src_dir, 'lib/CMakeLists.txt'), 'r') as f: + cmake_text = f.read() + version = {'major_version': '', 'minor_version': '', 'patch_version': ''} + for var in version.keys(): + cmake_var = f'JPEGXL_{var.upper()}' + # TODO(eustas): use `cmake -L` + # Regexp: + # set(_varname_ _capture_decimal_) + match = re.search(r'set\(' + cmake_var + r' ([0-9]+)\)', cmake_text) + version[var] = match.group(1) + + version['jpegli_lib_version'] = GetJpegLibVersion(args.src_dir) + + lists = SplitLibFiles(repo_files) + + cmake_chunks = [HEAD] + cmake_parts = lists + for var in sorted(cmake_parts): + cmake_chunks.append(FormatCMakeVar( + 'JPEGXL_INTERNAL_' + var.upper(), cmake_parts[var])) + + gni_chunks = [HEAD] + gni_parts = version | lists + for var in sorted(gni_parts): + gni_chunks.append(FormatGniVar('libjxl_' + var, gni_parts[var])) + + okay = [ + MaybeUpdateFile(args, 'lib/jxl_lists.cmake', '\n'.join(cmake_chunks)), + MaybeUpdateFile(args, 'lib/jxl_lists.bzl', '\n'.join(gni_chunks)), + ] + return all(okay) + + +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() + Check(BuildCleaner(args), 'Build files need update.') + + +if __name__ == '__main__': + main() diff --git a/tools/build_stats.py b/tools/scripts/build_stats.py index b1dc1ea..63265e2 100755 --- a/tools/build_stats.py +++ b/tools/scripts/build_stats.py @@ -20,6 +20,7 @@ import collections import itertools import json import os +import platform import re import struct import subprocess @@ -29,6 +30,7 @@ import tempfile # Ignore functions with stack size smaller than this value. MIN_STACK_SIZE = 32 +IS_OSX = (platform.system() == 'Darwin') Symbol = collections.namedtuple('Symbol', ['address', 'size', 'typ', 'name']) @@ -55,7 +57,10 @@ RAM_SIZE = 'dbs' # u - symbols imported from some other library # a - absolute address symbols -IGNORE_SYMBOLS = 'ua' +# c - common symbol +# i - indirect symbol +# - - debugger symbol table entries +IGNORE_SYMBOLS = 'uaci-' SIMD_NAMESPACES = [ 'N_SCALAR', 'N_WASM', 'N_NEON', 'N_PPC8', 'N_SSE4', 'N_AVX2', 'N_AVX3'] @@ -65,17 +70,30 @@ def LoadSymbols(filename): ret = [] nmout = subprocess.check_output(['nm', '--format=posix', filename]) for line in nmout.decode('utf-8').splitlines(): - if line.rstrip().endswith(':'): + line = line.rstrip() + if len(line) == 0: + # OSX nm produces extra crlf at the end + continue + if line.endswith(':'): # Ignore object names. continue + line = re.sub(' +', ' ', line) # symbol_name, symbol_type, (optional) address, (optional) size symlist = line.rstrip().split(' ') - assert 2 <= len(symlist) <= 4 + col_count = len(symlist) + assert 2 <= col_count <= 4 ret.append(Symbol( - int(symlist[2], 16) if len(symlist) > 2 else None, - int(symlist[3], 16) if len(symlist) > 3 else None, + int(symlist[2], 16) if col_count > 2 else None, + int(symlist[3], 16) if col_count > 3 else None, symlist[1], symlist[0])) + if IS_OSX: + ret = sorted(ret, key=lambda sym: sym.address) + for i in range(len(ret) - 1): + size = ret[i + 1].address - ret[i].address + if size > (1 << 30): + continue + ret[i] = ret[i]._replace(size=size) return ret def LoadTargetCommand(target, build_dir): @@ -145,8 +163,9 @@ def LoadStackSizes(filename, binutils=''): section, which can be done by compiling with -fstack-size-section in clang. """ with tempfile.NamedTemporaryFile() as stack_sizes_sec: + objcopy = ['objcopy', 'gobjcopy'][IS_OSX] subprocess.check_call( - [binutils + 'objcopy', '-O', 'binary', '--only-section=.stack_sizes', + [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() @@ -157,10 +176,11 @@ def LoadStackSizes(filename, binutils=''): # dynamic stack allocations are not included. # Get the pointer format based on the ELF file. + objdump = ['objdump', 'gobjdump'][IS_OSX] output = subprocess.check_output( - [binutils + 'objdump', '-a', filename]).decode('utf-8') + [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': + if elf_format.startswith('elf64-little') or elf_format.endswith('-x86-64') or elf_format.endswith('-arm64'): pointer_fmt = '<Q' elif elf_format.startswith('elf32-little') or elf_format == 'elf32-i386': pointer_fmt = '<I' @@ -234,7 +254,7 @@ def PrintStats(stats): 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, + name, bin_size, (100. * bin_size / mx_bin_size) if mx_bin_size else 0, ram_size, (100. * ram_size / mx_ram_size) if mx_ram_size else 0)) print() diff --git a/tools/check_author.py b/tools/scripts/check_author.py index ae1c279..7e16859 100755 --- a/tools/check_author.py +++ b/tools/scripts/check_author.py @@ -73,6 +73,8 @@ def CheckAuthor(args): print("User %s <%s> not found, please add yourself to the AUTHORS file" % ( args.name, args.email), file=sys.stderr) + print("Hint: to override author in PR run:\n" + " git commit --amend --author=\"Your Name <ldap@corp.com>\" --no-edit") sorted_alphabetically = IndividualsInAlphabeticOrder(authors_path) if not sorted_alphabetically: @@ -90,7 +92,7 @@ def main(): help='name of the commit author to check') parser.add_argument( '--source-dir', - default=os.path.dirname(os.path.dirname(os.path.realpath(__file__))), + default=os.path.dirname(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') diff --git a/tools/cjxl_bisect_bpp b/tools/scripts/cjxl_bisect_bpp index d7a1066..13a908c 100755 --- a/tools/cjxl_bisect_bpp +++ b/tools/scripts/cjxl_bisect_bpp @@ -1,5 +1,10 @@ #!/bin/sh # +# 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. +# # 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.) diff --git a/tools/cjxl_bisect_size b/tools/scripts/cjxl_bisect_size index 9cd88ea..c0945d9 100755 --- a/tools/cjxl_bisect_size +++ b/tools/scripts/cjxl_bisect_size @@ -1,5 +1,10 @@ #!/bin/sh # +# 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. +# # Bisects JPEG XL encoding quality parameter to reach a given # target byte-size. # (To be used directly, or as a template for tailored processing.) diff --git a/tools/demo_progressive_saliency_encoding.py b/tools/scripts/demo_progressive_saliency_encoding.py index 6eb5cad..6eb5cad 100755 --- a/tools/demo_progressive_saliency_encoding.py +++ b/tools/scripts/demo_progressive_saliency_encoding.py diff --git a/tools/scripts/jpegli_tools_test.sh b/tools/scripts/jpegli_tools_test.sh new file mode 100644 index 0000000..96df3b0 --- /dev/null +++ b/tools/scripts/jpegli_tools_test.sh @@ -0,0 +1,287 @@ +#!/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. + +# End-to-end roundtrip tests for cjpegli and djpegli tools, and other linux +# tools linked with the jpegli library. + +set -eux + +MYDIR=$(dirname $(realpath "$0")) +JPEGXL_TEST_DATA_PATH="${MYDIR}/../../testdata" + +# 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 + +verify_ssimulacra2() { + local score="$("${ssimulacra2}" "${1}" "${2}")" + python3 -c "import sys; sys.exit(not ${score} >= ${3})" +} + +verify_max_bpp() { + local infn="$1" + local jpgfn="$2" + local maxbpp="$3" + local size="$(wc -c "${jpgfn}" | cut -d' ' -f1)" + local pixels=$(( "$(identify "${infn}" | cut -d' ' -f3 | tr 'x' '*')" )) + python3 -c "import sys; sys.exit(not ${size} * 8 <= ${maxbpp} * ${pixels})" +} + +# Test that jpeg files created with cjpegli can be decoded with normal djpeg. +cjpegli_test() { + local infn="${JPEGXL_TEST_DATA_PATH}/$1" + local encargs="$2" + local minscore="$3" + local maxbpp="$4" + local jpgfn="$(mktemp -p "${tmpdir}")" + local outfn="$(mktemp -p "${tmpdir}").ppm" + + "${cjpegli}" "${infn}" "${jpgfn}" $encargs + djpeg -outfile "${outfn}" "${jpgfn}" + + verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}" + verify_max_bpp "${infn}" "${jpgfn}" "${maxbpp}" +} + +# Test full cjpegli/djpegli roundtrip. +cjpegli_djpegli_test() { + local infn="${JPEGXL_TEST_DATA_PATH}/$1" + local encargs="$2" + local minscore="$3" + local maxbpp="$4" + local jpgfn="$(mktemp -p "${tmpdir}")" + local outfn="$(mktemp -p "${tmpdir}").png" + + "${cjpegli}" "${infn}" "${jpgfn}" $encargs + "${djpegli}" "${jpgfn}" "${outfn}" + + verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}" + verify_max_bpp "${infn}" "${jpgfn}" "${maxbpp}" +} + +# Test the --target_size command line argument of cjpegli. +cjpegli_test_target_size() { + local infn="${JPEGXL_TEST_DATA_PATH}/$1" + local encargs="$2" + local target_size="$3" + local jpgfn="$(mktemp -p "$tmpdir")" + + "${cjpegli}" "${infn}" "${jpgfn}" $encargs --target_size "${target_size}" + local size="$(wc -c "${jpgfn}" | cut -d' ' -f1)" + python3 -c "import sys; sys.exit(not ${target_size} * 0.996 <= ${size})" + python3 -c "import sys; sys.exit(not ${target_size} * 1.004 >= ${size})" +} + +# Test that jpeg files created with cjpeg binary + jpegli library can be decoded +# with normal libjpeg. +cjpeg_test() { + local infn="${JPEGXL_TEST_DATA_PATH}/$1" + local encargs="$2" + local minscore="$3" + local maxbpp="$4" + local jpgfn="$(mktemp -p "$tmpdir")" + local outfn="$(mktemp -p "${tmpdir}").png" + + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + cjpeg $encargs -outfile "${jpgfn}" "${infn}" + djpeg -outfile "${outfn}" "${jpgfn}" + + verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}" + verify_max_bpp "${infn}" "${jpgfn}" "${maxbpp}" +} + +# Test decoding of jpeg files with the djpegli binary. +djpegli_test() { + local infn="${JPEGXL_TEST_DATA_PATH}/$1" + local encargs="$2" + local minscore="$3" + local jpgfn="$(mktemp -p "$tmpdir")" + + cjpeg $encargs -outfile "${jpgfn}" "${infn}" + + # Test that disabling output works. + "${djpegli}" "${jpgfn}" --disable_output + for ext in png pgm ppm pfm pnm baz; do + "${djpegli}" "${jpgfn}" /foo/bar.$ext --disable_output + done + + # Test decoding to PNG, PPM, PNM, PFM + for ext in png ppm pnm pfm; do + local outfn="$(mktemp -p "${tmpdir}").${ext}" + "${djpegli}" "${jpgfn}" "${outfn}" --num_reps 2 + verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}" + done + + # Test decoding to PGM (for grayscale input) + if [[ "${infn: -6}" == ".g.png" ]]; then + local outfn="$(mktemp -p "${tmpdir}").pgm" + "${djpegli}" "${jpgfn}" "${outfn}" --quiet + verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}" + fi + + # Test decoding to 16 bit + for ext in png pnm; do + local outfn8="$(mktemp -p "${tmpdir}").8.${ext}" + local outfn16="$(mktemp -p "${tmpdir}").16.${ext}" + "${djpegli}" "${jpgfn}" "${outfn8}" + "${djpegli}" "${jpgfn}" "${outfn16}" --bitdepth 16 + local score8="$("${ssimulacra2}" "${infn}" "${outfn8}")" + local score16="$("${ssimulacra2}" "${infn}" "${outfn16}")" + python3 -c "import sys; sys.exit(not ${score16} > ${score8})" + done +} + +# Test decoding of jpeg files with the djpeg binary + jpegli library. +djpeg_test() { + local infn="${JPEGXL_TEST_DATA_PATH}/$1" + local encargs="$2" + local minscore="$3" + local jpgfn="$(mktemp -p "$tmpdir")" + + cjpeg $encargs -outfile "${jpgfn}" "${infn}" + + # Test default behaviour. + local outfn="$(mktemp -p "${tmpdir}").pnm" + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + djpeg -outfile "${outfn}" "${jpgfn}" + verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}" + + # Test color quantization. + local outfn="$(mktemp -p "${tmpdir}").pnm" + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + djpeg -outfile "${outfn}" -colors 128 "${jpgfn}" + verify_ssimulacra2 "${infn}" "${outfn}" 48 + + local outfn="$(mktemp -p "${tmpdir}").pnm" + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + djpeg -outfile "${outfn}" -colors 128 -onepass -dither fs "${jpgfn}" + verify_ssimulacra2 "${infn}" "${outfn}" 30 + + local outfn="$(mktemp -p "${tmpdir}").pnm" + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + djpeg -outfile "${outfn}" -colors 128 -onepass -dither ordered "${jpgfn}" + verify_ssimulacra2 "${infn}" "${outfn}" 30 + + # Test -grayscale flag. + local outfn="$(mktemp -p "${tmpdir}").pgm" + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + djpeg -outfile "${outfn}" -grayscale "${jpgfn}" + local outfn2="$(mktemp -p "${tmpdir}").pgm" + convert "${infn}" -set colorspace Gray "${outfn2}" + # JPEG color conversion is in gamma-compressed space, so it will not match + # the correct grayscale version very well. + verify_ssimulacra2 "${outfn2}" "${outfn}" 60 + + # Test -rgb flag. + local outfn="$(mktemp -p "${tmpdir}").ppm" + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + djpeg -outfile "${outfn}" -rgb "${jpgfn}" + verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}" + + # Test -crop flag. + for geometry in 256x256+128+128 256x127+128+117; do + local outfn="$(mktemp -p "${tmpdir}").pnm" + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + djpeg -outfile "${outfn}" -crop "${geometry}" "${jpgfn}" + local outfn2="$(mktemp -p "${tmpdir}").pnm" + convert "${infn}" -crop "${geometry}" "${outfn2}" + verify_ssimulacra2 "${outfn2}" "${outfn}" "${minscore}" + done + + # Test output scaling. + for scale in 1/4 3/8 1/2 5/8 9/8; do + local scalepct="$(python3 -c "print(100.0*${scale})")%" + local geometry=96x128+0+0 + local outfn="$(mktemp -p "${tmpdir}").pnm" + LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \ + djpeg -outfile "${outfn}" -scale "${scale}" -crop "${geometry}" "${jpgfn}" + local outfn2="$(mktemp -p "${tmpdir}").pnm" + convert "${infn}" -scale "${scalepct}" -crop "${geometry}" "${outfn2}" + verify_ssimulacra2 "${outfn2}" "${outfn}" 80 + done +} + +main() { + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + + local build_dir="${1:-}" + if [[ -z "${build_dir}" ]]; then + build_dir=$(realpath "${MYDIR}/../../build") + fi + + local cjpegli="${build_dir}/tools/cjpegli" + local djpegli="${build_dir}/tools/djpegli" + local ssimulacra2="${build_dir}/tools/ssimulacra2" + local rgb_in="jxl/flower/flower_small.rgb.png" + local gray_in="jxl/flower/flower_small.g.png" + local ppm_rgb="jxl/flower/flower_small.rgb.depth8.ppm" + local ppm_gray="jxl/flower/flower_small.g.depth8.pgm" + + cjpegli_test "${rgb_in}" "" 88.5 1.7 + cjpegli_test "${rgb_in}" "-q 80" 84 1.2 + cjpegli_test "${rgb_in}" "-q 95" 91.5 2.4 + cjpegli_test "${rgb_in}" "-d 0.5" 92 2.6 + cjpegli_test "${rgb_in}" "--chroma_subsampling 420" 87 1.5 + cjpegli_test "${rgb_in}" "--chroma_subsampling 440" 87 1.6 + cjpegli_test "${rgb_in}" "--chroma_subsampling 422" 87 1.6 + cjpegli_test "${rgb_in}" "--std_quant" 91 2.2 + cjpegli_test "${rgb_in}" "--noadaptive_quantization" 88.5 1.85 + cjpegli_test "${rgb_in}" "-p 1" 88.5 1.72 + cjpegli_test "${rgb_in}" "-p 0" 88.5 1.75 + cjpegli_test "${rgb_in}" "-p 0 --fixed_code" 88.5 1.8 + cjpegli_test "${gray_in}" "" 92 1.4 + + cjpegli_test_target_size "${rgb_in}" "" 10000 + cjpegli_test_target_size "${rgb_in}" "" 50000 + cjpegli_test_target_size "${rgb_in}" "" 100000 + cjpegli_test_target_size "${rgb_in}" "--chroma_subsampling 420" 20000 + cjpegli_test_target_size "${rgb_in}" "--xyb" 20000 + cjpegli_test_target_size "${rgb_in}" "-p 0 --fixed_code" 20000 + + cjpegli_test "jxl/flower/flower_small.rgb.depth8.ppm" "" 88.5 1.7 + cjpegli_test "jxl/flower/flower_small.rgb.depth16.ppm" "" 89 1.7 + cjpegli_test "jxl/flower/flower_small.g.depth8.pgm" "" 89 1.7 + cjpegli_test "jxl/flower/flower_small.g.depth16.pgm" "" 89 1.7 + + cjpegli_djpegli_test "${rgb_in}" "" 89 1.7 + cjpegli_djpegli_test "${rgb_in}" "--xyb" 87 1.5 + + djpegli_test "${ppm_rgb}" "-q 95" 92 + djpegli_test "${ppm_rgb}" "-q 95 -sample 1x1" 93 + djpegli_test "${ppm_gray}" "-q 95 -gray" 94 + + cjpeg_test "${ppm_rgb}" "" 89 1.9 + cjpeg_test "${ppm_rgb}" "-optimize" 89 1.85 + cjpeg_test "${ppm_rgb}" "-optimize -progressive" 89 1.8 + cjpeg_test "${ppm_rgb}" "-sample 2x2" 87 1.65 + cjpeg_test "${ppm_rgb}" "-sample 1x2" 88 1.75 + cjpeg_test "${ppm_rgb}" "-sample 2x1" 88 1.75 + cjpeg_test "${ppm_rgb}" "-grayscale" -50 1.45 + cjpeg_test "${ppm_rgb}" "-rgb" 92 4.5 + cjpeg_test "${ppm_rgb}" "-restart 1" 89 1.9 + cjpeg_test "${ppm_rgb}" "-restart 1024B" 89 1.9 + cjpeg_test "${ppm_rgb}" "-smooth 30" 88 1.75 + cjpeg_test "${ppm_gray}" "-grayscale" 92 1.45 + # The -q option works differently on v62 vs. v8 cjpeg binaries, so we have to + # have looser bounds than would be necessary if we sticked to a particular + # cjpeg version. + cjpeg_test "${ppm_rgb}" "-q 50" 76 0.95 + cjpeg_test "${ppm_rgb}" "-q 80" 84 1.6 + cjpeg_test "${ppm_rgb}" "-q 90" 89 2.35 + cjpeg_test "${ppm_rgb}" "-q 100" 95 7.45 + + djpeg_test "${ppm_rgb}" "-q 95" 92 + djpeg_test "${ppm_rgb}" "-q 95 -sample 1x1" 93 + djpeg_test "${ppm_gray}" "-q 95 -gray" 94 +} + +main "$@" diff --git a/tools/scripts/jxl-eval.sh b/tools/scripts/jxl-eval.sh new file mode 100755 index 0000000..138aac8 --- /dev/null +++ b/tools/scripts/jxl-eval.sh @@ -0,0 +1,124 @@ +#!/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 + +GSROOT="${GSROOT:-gs://jxl-quality}" +URLROOT="${URLROOT:-https://storage.googleapis.com/jxl-quality}" +BUILD_DIR="${BUILD_DIR:-./build}" +BUILD_MODE="${BUILD_MODE:-opt}" +DESC="${DESC:-exp}" + +build_libjxl() { + export BUILD_DIR="${BUILD_DIR}" + export SKIP_TEST=1 + ./ci.sh "${BUILD_MODE}" +} + +build_mozjpeg() { + if [[ ! -d "${HOME}/mozjpeg" ]]; then + (cd "${HOME}" + git clone https://github.com/mozilla/mozjpeg.git + ) + fi + (cd "${HOME}/mozjpeg" + mkdir -p build + cmake -GNinja -B build + ninja -C build + ) +} + +download_corpus() { + local corpus="$1" + local localdir="${HOME}/corpora/${corpus}" + local remotedir="${GSROOT}/corpora/${corpus}" + if [[ ! -d "${localdir}" ]]; then + mkdir -p "${localdir}" + fi + gsutil -m rsync "${remotedir}" "${localdir}" +} + +create_report() { + local corpus="$1" + local codec="$2" + shift 2 + local rev="$(git rev-parse --short HEAD)" + local originals="${URLROOT}/corpora/${corpus}" + if git diff HEAD --quiet; then + local expid="${corpus}/${rev}/base" + else + local expid="${corpus}/${rev}/${DESC}" + fi + local output_dir="benchmark_results/${expid}" + local bucket="eval/${USER}/${expid}" + local indexhtml="index.$(echo ${codec} | tr ':' '_').html" + local url="${URLROOT}/${bucket}/${indexhtml}" + local use_decompressed="--save_decompressed --html_report_use_decompressed" + if [[ "${codec:0:4}" == "jpeg" ]]; then + use_decompressed="--nohtml_report_use_decompressed" + fi + ( + cd "${BUILD_DIR}" + tools/benchmark_xl \ + --output_dir "${output_dir}" \ + --input "${HOME}/corpora/${corpus}/*.??g" \ + --codec="${codec}" \ + --save_compressed \ + --write_html_report \ + "${use_decompressed}" \ + --originals_url="${originals}" \ + $@ + gsutil -m rsync "${output_dir}" "${GSROOT}/${bucket}" + echo "You can view evaluation results at:" + echo "${url}" + ) +} + +cmd_upload_corpus() { + local corpus="$1" + gsutil -m rsync "${HOME}/corpora/${corpus}" "${GSROOT}/corpora/${corpus}" +} + +cmd_report() { + local corpus="$1" + local codec="$2" + if [[ "${codec}" == *","* ]]; then + echo "Multiple codecs are not allowed in html report" + exit 1 + fi + download_corpus "${corpus}" + if [[ "${codec:0:4}" == "jpeg" ]]; then + build_mozjpeg + export LD_LIBRARY_PATH="${HOME}/mozjpeg/build:${LD_LIBRARY_PATH:-}" + fi + build_libjxl + create_report "$@" +} + +main() { + local cmd="${1:-}" + if [[ -z "${cmd}" ]]; then + cat >&2 <<EOF +Use: $0 CMD + +Where CMD is one of: + upload_corpus CORPUS + Upload the image corpus in $HOME/corpora/CORPUS to the cloud + report CORPUS CODEC + Build and run benchmark of codec CODEC on image corpus CORPUS and upload + the results to the cloud. If the codec is jpeg, the mozjpeg library will be + built and used through LD_LIBRARY_PATH +EOF + echo "Usage $0 CMD" + exit 1 + fi + cmd="cmd_${cmd}" + shift + set -x + "${cmd}" "$@" +} + +main "$@" diff --git a/tools/ossfuzz-build.sh b/tools/scripts/ossfuzz-build.sh index b5fbb45..7ab45b6 100755 --- a/tools/ossfuzz-build.sh +++ b/tools/scripts/ossfuzz-build.sh @@ -20,6 +20,7 @@ main() { build_args=( -G Ninja -DBUILD_TESTING=OFF + -DBUILD_SHARED_LIBS=OFF -DJPEGXL_ENABLE_BENCHMARK=OFF -DJPEGXL_ENABLE_DEVTOOLS=ON -DJPEGXL_ENABLE_EXAMPLES=OFF diff --git a/tools/progressive_saliency.conf b/tools/scripts/progressive_saliency.conf index 987651a..987651a 100644 --- a/tools/progressive_saliency.conf +++ b/tools/scripts/progressive_saliency.conf diff --git a/tools/progressive_sizes.sh b/tools/scripts/progressive_sizes.sh index a1e808d..08d3079 100755 --- a/tools/progressive_sizes.sh +++ b/tools/scripts/progressive_sizes.sh @@ -16,8 +16,8 @@ cleanup() { trap cleanup EXIT -CJXL=$(realpath $(dirname "$0"))/../build/tools/cjxl -DJXL=$(realpath $(dirname "$0"))/../build/tools/djxl +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]*') diff --git a/tools/reference_zip.sh b/tools/scripts/reference_zip.sh index 6a284b4..6a284b4 100755 --- a/tools/reference_zip.sh +++ b/tools/scripts/reference_zip.sh diff --git a/tools/roundtrip_test.sh b/tools/scripts/roundtrip_test.sh index 46b7756..b3bb300 100644 --- a/tools/roundtrip_test.sh +++ b/tools/scripts/roundtrip_test.sh @@ -7,12 +7,10 @@ # End-to-end roundtrip tests for cjxl and djxl tools. MYDIR=$(dirname $(realpath "$0")) -JPEGXL_TEST_DATA_PATH="${MYDIR}/../testdata" +JPEGXL_TEST_DATA_PATH="${MYDIR}/../../testdata" set -eux -EMULATOR=${EMULATOR:-} - # Temporary files cleanup hooks. CLEANUP_FILES=() cleanup() { @@ -22,14 +20,20 @@ cleanup() { } trap 'retcode=$?; { set +x; } 2>/dev/null; cleanup' INT TERM EXIT +roundtrip_lossless_pnm_test() { + local infn="${JPEGXL_TEST_DATA_PATH}/$1" + local jxlfn="$(mktemp -p "$tmpdir")" + local outfn="$(mktemp -p "$tmpdir").${infn: -3}" + + "${encoder}" "${infn}" "${jxlfn}" -d 0 -e 1 + "${decoder}" "${jxlfn}" "${outfn}" + diff "${infn}" "${outfn}" +} + roundtrip_test() { local infn="${JPEGXL_TEST_DATA_PATH}/$1" local encargs="$2" local maxdist="$3" - - local encoder="${EMULATOR} ${build_dir}/tools/cjxl" - local decoder="${EMULATOR} ${build_dir}/tools/djxl" - local comparator="${EMULATOR} ${build_dir}/tools/ssimulacra_main" local jxlfn="$(mktemp -p "$tmpdir")" "${encoder}" "${infn}" "${jxlfn}" $encargs @@ -66,6 +70,28 @@ roundtrip_test() { local dist="$("${comparator}" "${infn}" "${outfn}")" python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist})" + # Test decoding to 16 bit png. + "${decoder}" "${jxlfn}" "${outfn}" --bits_per_sample 16 + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist} + 0.0005)" + + # Test decoding to pfm. + local outfn="$(mktemp -p "$tmpdir").pfm" + "${decoder}" "${jxlfn}" "${outfn}" + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist})" + + # Test decoding to ppm. + local outfn="$(mktemp -p "$tmpdir").ppm" + "${decoder}" "${jxlfn}" "${outfn}" + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist})" + + # Test decoding to 16 bit ppm. + "${decoder}" "${jxlfn}" "${outfn}" --bits_per_sample 16 + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist} + 0.0005)" + # Test decoding to jpg. outfn="$(mktemp -p "$tmpdir").jpg" "${decoder}" "${jxlfn}" "${outfn}" --num_reps 2 @@ -83,9 +109,30 @@ main() { build_dir=$(realpath "${MYDIR}/../../build") fi - roundtrip_test "jxl/flower/flower.png" "-e 1" 0.02 - roundtrip_test "jxl/flower/flower.png" "-e 1 -d 0.0" 0.0 + local encoder="${build_dir}/tools/cjxl" + local decoder="${build_dir}/tools/djxl" + local comparator="${build_dir}/tools/ssimulacra_main" + + roundtrip_test "jxl/flower/flower_small.rgb.png" "-e 1" 0.02 + roundtrip_test "jxl/flower/flower_small.rgb.png" "-e 1 -d 0.0" 0.0 + roundtrip_test "jxl/flower/flower_small.rgb.depth8.ppm" \ + "-e 1 --streaming_input" 0.02 + roundtrip_test "jxl/flower/flower_small.rgb.depth8.ppm" \ + "-e 1 -d 0.0 --streaming_input" 0.0 + roundtrip_test "jxl/flower/flower_small.rgb.depth8.ppm" \ + "-e 1 --streaming_output" 0.02 + roundtrip_test "jxl/flower/flower_small.rgb.depth8.ppm" \ + "-e 1 -d 0.0 --streaming_input --streaming_output" 0.0 roundtrip_test "jxl/flower/flower_cropped.jpg" "-e 1" 0.0 + + roundtrip_lossless_pnm_test "jxl/flower/flower_small.rgb.depth1.ppm" + roundtrip_lossless_pnm_test "jxl/flower/flower_small.g.depth1.pgm" + for i in `seq 2 16`; do + roundtrip_lossless_pnm_test "jxl/flower/flower_small.rgb.depth$i.ppm" + roundtrip_lossless_pnm_test "jxl/flower/flower_small.g.depth$i.pgm" + roundtrip_lossless_pnm_test "jxl/flower/flower_small.ga.depth$i.pam" + roundtrip_lossless_pnm_test "jxl/flower/flower_small.rgba.depth$i.pam" + done } main "$@" diff --git a/tools/scripts/test_cost-arm64-lowprecision.zip b/tools/scripts/test_cost-arm64-lowprecision.zip Binary files differnew file mode 100644 index 0000000..92045e2 --- /dev/null +++ b/tools/scripts/test_cost-arm64-lowprecision.zip diff --git a/tools/scripts/test_cost-arm64.zip b/tools/scripts/test_cost-arm64.zip Binary files differnew file mode 100644 index 0000000..0d196ed --- /dev/null +++ b/tools/scripts/test_cost-arm64.zip diff --git a/tools/scripts/test_cost-armhf.zip b/tools/scripts/test_cost-armhf.zip Binary files differnew file mode 100644 index 0000000..988c96d --- /dev/null +++ b/tools/scripts/test_cost-armhf.zip diff --git a/tools/scripts/test_cost-i386.zip b/tools/scripts/test_cost-i386.zip Binary files differnew file mode 100644 index 0000000..718789f --- /dev/null +++ b/tools/scripts/test_cost-i386.zip diff --git a/tools/scripts/transform_sources_list.py b/tools/scripts/transform_sources_list.py new file mode 100644 index 0000000..1194fe0 --- /dev/null +++ b/tools/scripts/transform_sources_list.py @@ -0,0 +1,76 @@ +#!/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 sys + +def find_key(entries : list[str], key: str) -> int: + prefix = f"{key.lower()}: " + for i in range(len(entries)): + if entries[i].lower().startswith(prefix): + return i + return -1 + +def set_value(entries: list[str], key: str, value: str): + new_line = f'{key}: {value}' + # TODO(eustas): deal with repeated items + idx = find_key(entries, key) + if idx < 0: + entries.append(new_line) + else: + entries[idx] = new_line + +def transform_deb_822(archs): + sources_path = "/etc/apt/sources.list.d/debian.sources" + with open(sources_path) as f: + lines = [line.rstrip() for line in f] + lines.append('') + entries = [] + entry = [] + for line in lines: + if len(line) == 0: + if len(entry) > 0: + entries.append(entry) + entry = [] + else: + entry.append(line) + + new_entries = [] + for entry in entries: + types_key = find_key(entry, "Types") + if types_key < 0: + continue + if "types: deb" != entry[types_key].lower(): + continue + deb_entry = entry[:] + for arch in archs: + deb_entry.append(f"Architectures-Add: {arch}") + new_entries.append(deb_entry) + deb_src_entry = deb_entry[:] + set_value(deb_src_entry, "Types", "deb-src") + new_entries.append(deb_src_entry) + + new_lines = [] + for entry in new_entries: + if len(new_lines) > 0: + new_lines.append("") + new_lines.extend(entry) + + with open(sources_path, "w") as f: + f.write('\n'.join(new_lines)) + +def main(): + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[1]} ARCHS") + sys.exit(1) + archs_str = sys.argv[1] + archs = archs_str.split(',') + if True: + transform_deb_822(archs) + else: + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/tools/set_from_bytes_fuzzer.cc b/tools/set_from_bytes_fuzzer.cc index 5eb9f75..0b95072 100644 --- a/tools/set_from_bytes_fuzzer.cc +++ b/tools/set_from_bytes_fuzzer.cc @@ -7,27 +7,29 @@ #include <stdint.h> #include "lib/extras/codec.h" +#include "lib/extras/size_constraints.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" +#include "tools/thread_pool_internal.h" -namespace jxl { +namespace { 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); + jxl::CodecInOut io; + jxl::SizeConstraints constraints; + constraints.dec_max_xsize = 1u << 16; + constraints.dec_max_ysize = 1u << 16; + constraints.dec_max_pixels = 1u << 22; + jpegxl::tools::ThreadPoolInternal pool(0); - (void)SetFromBytes(Span<const uint8_t>(data, size), &io, &pool); + (void)jxl::SetFromBytes(jxl::Bytes(data, size), &io, &pool, &constraints); return 0; } -} // namespace jxl +} // namespace extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - return jxl::TestOneInput(data, size); + return TestOneInput(data, size); } diff --git a/tools/speed_stats.cc b/tools/speed_stats.cc index cdef814..d378d09 100644 --- a/tools/speed_stats.cc +++ b/tools/speed_stats.cc @@ -5,6 +5,10 @@ #include "tools/speed_stats.h" +#ifndef __STDC_FORMAT_MACROS +#define __STDC_FORMAT_MACROS +#endif + #include <inttypes.h> #include <math.h> #include <stddef.h> @@ -54,21 +58,19 @@ bool SpeedStats::GetSummary(SpeedStats::Summary* s) { s->central_tendency = pow(product, 1.0 / (elapsed_.size() - 1)); s->variability = 0.0; s->type = " geomean:"; - return true; + if (isnormal(s->central_tendency)) return true; } // Else: median std::sort(elapsed_.begin(), elapsed_.end()); s->central_tendency = elapsed_.data()[elapsed_.size() / 2]; - std::vector<double> deviations(elapsed_.size()); + double stdev = 0; for (size_t i = 0; i < elapsed_.size(); i++) { - deviations[i] = fabs(elapsed_[i] - s->central_tendency); + double diff = elapsed_[i] - s->central_tendency; + stdev += diff * diff; } - std::nth_element(deviations.begin(), - deviations.begin() + deviations.size() / 2, - deviations.end()); - s->variability = deviations[deviations.size() / 2]; - s->type = "median: "; + s->variability = sqrt(stdev); + s->type = " median:"; return true; } @@ -84,8 +86,14 @@ std::string SummaryStat(double value, const char* unit, const double value_min = value / s.max; const double value_max = value / s.min; - snprintf(stat_str, sizeof(stat_str), ",%s %.2f %s/s [%.2f, %.2f]", s.type, - value_tendency, unit, value_min, value_max); + char variability[20] = {'\0'}; + if (s.variability != 0.0) { + const double stdev = value / s.variability; + snprintf(variability, sizeof(variability), " (stdev %.3f)", stdev); + } + + snprintf(stat_str, sizeof(stat_str), ",%s %.3f %s/s [%.2f, %.2f]%s", s.type, + value_tendency, unit, value_min, value_max, variability); return stat_str; } @@ -99,16 +107,11 @@ bool SpeedStats::Print(size_t worker_threads) { 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 + "%" PRIu64 " x %" PRIu64 "%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, + mps_stats.c_str(), mbs_stats.c_str(), static_cast<uint64_t>(elapsed_.size()), static_cast<uint64_t>(worker_threads)); return true; diff --git a/tools/ssimulacra2.cc b/tools/ssimulacra2.cc new file mode 100644 index 0000000..5ddaab3 --- /dev/null +++ b/tools/ssimulacra2.cc @@ -0,0 +1,492 @@ +// 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 2 +Structural SIMilarity Unveiling Local And Compression Related Artifacts + +Perceptual metric developed by Jon Sneyers (Cloudinary) in July 2022, +updated in April 2023. +Design: +- XYB color space (rescaled to a 0..1 range and with B-Y) +- SSIM map (with correction: no double gamma correction) +- 'blockiness/ringing' map (distorted has edges where original is smooth) +- 'smoothing' map (distorted is smooth where original has edges) +- error maps are computed at 6 scales (1:1 to 1:32) for each component (X,Y,B) +- downscaling is done in linear RGB +- for all 6*3*3=54 maps, two norms are computed: 1-norm (mean) and 4-norm +- a weighted sum of these 54*2=108 norms leads to the final score +- weights were tuned based on a large set of subjective scores + (CID22, TID2013, Kadid10k, KonFiG-IQA). +*/ + +#include "tools/ssimulacra2.h" + +#include <jxl/cms.h> +#include <stdio.h> + +#include <cmath> + +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/gauss_blur.h" +#include "lib/jxl/image_ops.h" + +namespace { + +using jxl::Image3F; +using jxl::ImageF; + +static const float kC2 = 0.0009f; +static const int kNumScales = 6; + +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]; + } + } + } +} + +// 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_; +}; + +double tothe4th(double x) { + x *= x; + x *= x; + return x; +} +void SSIMMap(const Image3F& m1, const Image3F& m2, const Image3F& s11, + const Image3F& s22, const Image3F& s12, double* plane_averages) { + const double onePerPixels = 1.0 / (m1.ysize() * m1.xsize()); + for (size_t c = 0; c < 3; ++c) { + double sum1[2] = {0.0}; + for (size_t y = 0; y < m1.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); + for (size_t x = 0; x < m1.xsize(); ++x) { + float mu1 = row_m1[x]; + float mu2 = row_m2[x]; + float mu11 = mu1 * mu1; + float mu22 = mu2 * mu2; + float mu12 = mu1 * mu2; + /* Correction applied compared to the original SSIM formula, which has: + + luma_err = 2 * mu1 * mu2 / (mu1^2 + mu2^2) + = 1 - (mu1 - mu2)^2 / (mu1^2 + mu2^2) + + The denominator causes error in the darks (low mu1 and mu2) to weigh + more than error in the brights (high mu1 and mu2). This would make + sense if values correspond to linear luma. However, the actual values + are either gamma-compressed luma (which supposedly is already + perceptually uniform) or chroma (where weighing green more than red + or blue more than yellow does not make any sense at all). So it is + better to simply drop this denominator. + */ + float num_m = 1.0 - (mu1 - mu2) * (mu1 - mu2); + float num_s = 2 * (row_s12[x] - mu12) + kC2; + float denom_s = (row_s11[x] - mu11) + (row_s22[x] - mu22) + kC2; + + // Use 1 - SSIM' so it becomes an error score instead of a quality + // index. This makes it make sense to compute an L_4 norm. + double d = 1.0 - (num_m * num_s / denom_s); + d = std::max(d, 0.0); + sum1[0] += d; + sum1[1] += tothe4th(d); + } + } + plane_averages[c * 2] = onePerPixels * sum1[0]; + plane_averages[c * 2 + 1] = sqrt(sqrt(onePerPixels * sum1[1])); + } +} + +void EdgeDiffMap(const Image3F& img1, const Image3F& mu1, const Image3F& img2, + const Image3F& mu2, double* plane_averages) { + const double onePerPixels = 1.0 / (img1.ysize() * img1.xsize()); + for (size_t c = 0; c < 3; ++c) { + double sum1[4] = {0.0}; + 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); + for (size_t x = 0; x < img1.xsize(); ++x) { + double d1 = (1.0 + std::abs(row2[x] - rowm2[x])) / + (1.0 + std::abs(row1[x] - rowm1[x])) - + 1.0; + + // d1 > 0: distorted has an edge where original is smooth + // (indicating ringing, color banding, blockiness, etc) + double artifact = std::max(d1, 0.0); + sum1[0] += artifact; + sum1[1] += tothe4th(artifact); + + // d1 < 0: original has an edge where distorted is smooth + // (indicating smoothing, blurring, smearing, etc) + double detail_lost = std::max(-d1, 0.0); + sum1[2] += detail_lost; + sum1[3] += tothe4th(detail_lost); + } + } + plane_averages[c * 4] = onePerPixels * sum1[0]; + plane_averages[c * 4 + 1] = sqrt(sqrt(onePerPixels * sum1[1])); + plane_averages[c * 4 + 2] = onePerPixels * sum1[2]; + plane_averages[c * 4 + 3] = sqrt(sqrt(onePerPixels * sum1[3])); + } +} + +/* Get all components in more or less 0..1 range + Range of Rec2020 with these adjustments: + X: 0.017223..0.998838 + Y: 0.010000..0.855303 + B: 0.048759..0.989551 + Range of sRGB: + X: 0.204594..0.813402 + Y: 0.010000..0.855308 + B: 0.272295..0.938012 + The maximum pixel-wise difference has to be <= 1 for the ssim formula to make + sense. +*/ +void MakePositiveXYB(jxl::Image3F& img) { + for (size_t y = 0; y < img.ysize(); ++y) { + float* JXL_RESTRICT rowY = img.PlaneRow(1, y); + float* JXL_RESTRICT rowB = img.PlaneRow(2, y); + float* JXL_RESTRICT rowX = img.PlaneRow(0, y); + for (size_t x = 0; x < img.xsize(); ++x) { + rowB[x] = (rowB[x] - rowY[x]) + 0.55f; + rowX[x] = rowX[x] * 14.f + 0.42f; + rowY[x] += 0.01f; + } + } +} + +void AlphaBlend(jxl::ImageBundle& img, float bg) { + for (size_t y = 0; y < img.ysize(); ++y) { + float* JXL_RESTRICT r = img.color()->PlaneRow(0, y); + float* JXL_RESTRICT g = img.color()->PlaneRow(1, y); + float* JXL_RESTRICT b = img.color()->PlaneRow(2, y); + const float* JXL_RESTRICT a = img.alpha()->Row(y); + for (size_t x = 0; x < img.xsize(); ++x) { + r[x] = a[x] * r[x] + (1.f - a[x]) * bg; + g[x] = a[x] * g[x] + (1.f - a[x]) * bg; + b[x] = a[x] * b[x] + (1.f - a[x]) * bg; + } + } +} + +} // namespace + +/* +The final score is based on a weighted sum of 108 sub-scores: +- for 6 scales (1:1 to 1:32, downsampled in linear RGB) +- for 3 components (X, Y, B-Y, rescaled to 0..1 range) +- using 2 norms (the 1-norm and the 4-norm) +- over 3 error maps: + - SSIM' (SSIM without the spurious gamma correction term) + - "ringing" (distorted edges where there are no orig edges) + - "blurring" (orig edges where there are no distorted edges) + +The weights were obtained by running Nelder-Mead simplex search, +optimizing to minimize MSE for the CID22 training set and to +maximize Kendall rank correlation (and with a lower weight, +also Pearson correlation) with the CID22 training set and the +TID2013, Kadid10k and KonFiG-IQA datasets. +Validation was done on the CID22 validation set. + +Final results after tuning (Kendall | Spearman | Pearson): + CID22: 0.6903 | 0.8805 | 0.8583 + TID2013: 0.6590 | 0.8445 | 0.8471 + KADID-10k: 0.6175 | 0.8133 | 0.8030 + KonFiG(F): 0.7668 | 0.9194 | 0.9136 +*/ +double Msssim::Score() const { + double ssim = 0.0; + constexpr double weight[108] = {0.0, + 0.0007376606707406586, + 0.0, + 0.0, + 0.0007793481682867309, + 0.0, + 0.0, + 0.0004371155730107379, + 0.0, + 1.1041726426657346, + 0.00066284834129271, + 0.00015231632783718752, + 0.0, + 0.0016406437456599754, + 0.0, + 1.8422455520539298, + 11.441172603757666, + 0.0, + 0.0007989109436015163, + 0.000176816438078653, + 0.0, + 1.8787594979546387, + 10.94906990605142, + 0.0, + 0.0007289346991508072, + 0.9677937080626833, + 0.0, + 0.00014003424285435884, + 0.9981766977854967, + 0.00031949755934435053, + 0.0004550992113792063, + 0.0, + 0.0, + 0.0013648766163243398, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 7.466890328078848, + 0.0, + 17.445833984131262, + 0.0006235601634041466, + 0.0, + 0.0, + 6.683678146179332, + 0.00037724407979611296, + 1.027889937768264, + 225.20515300849274, + 0.0, + 0.0, + 19.213238186143016, + 0.0011401524586618361, + 0.001237755635509985, + 176.39317598450694, + 0.0, + 0.0, + 24.43300999870476, + 0.28520802612117757, + 0.0004485436923833408, + 0.0, + 0.0, + 0.0, + 34.77906344483772, + 44.835625328877896, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0008680556573291698, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0005313191874358747, + 0.0, + 0.00016533814161379112, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0004179171803251336, + 0.0017290828234722833, + 0.0, + 0.0020827005846636437, + 0.0, + 0.0, + 8.826982764996862, + 23.19243343998926, + 0.0, + 95.1080498811086, + 0.9863978034400682, + 0.9834382792465353, + 0.0012286405048278493, + 171.2667255897307, + 0.9807858872435379, + 0.0, + 0.0, + 0.0, + 0.0005130064588990679, + 0.0, + 0.00010854057858411537}; + + size_t i = 0; + char ch[] = "XYB"; + const bool verbose = false; + for (size_t c = 0; c < 3; ++c) { + for (size_t scale = 0; scale < scales.size(); ++scale) { + for (size_t n = 0; n < 2; n++) { +#ifdef SSIMULACRA2_OUTPUT_RAW_SCORES_FOR_WEIGHT_TUNING + printf("%.12f,%.12f,%.12f,", scales[scale].avg_ssim[c * 2 + n], + scales[scale].avg_edgediff[c * 4 + n], + scales[scale].avg_edgediff[c * 4 + 2 + n]); +#endif + if (verbose) { + printf("%f from channel %c ssim, scale 1:%i, %" PRIuS + "-norm (weight %f)\n", + weight[i] * std::abs(scales[scale].avg_ssim[c * 2 + n]), ch[c], + 1 << scale, n * 3 + 1, weight[i]); + } + ssim += weight[i++] * std::abs(scales[scale].avg_ssim[c * 2 + n]); + if (verbose) { + printf("%f from channel %c ringing, scale 1:%i, %" PRIuS + "-norm (weight %f)\n", + weight[i] * std::abs(scales[scale].avg_edgediff[c * 4 + n]), + ch[c], 1 << scale, n * 3 + 1, weight[i]); + } + ssim += weight[i++] * std::abs(scales[scale].avg_edgediff[c * 4 + n]); + if (verbose) { + printf( + "%f from channel %c blur, scale 1:%i, %" PRIuS + "-norm (weight %f)\n", + weight[i] * std::abs(scales[scale].avg_edgediff[c * 4 + n + 2]), + ch[c], 1 << scale, n * 3 + 1, weight[i]); + } + ssim += + weight[i++] * std::abs(scales[scale].avg_edgediff[c * 4 + n + 2]); + } + } + } + + ssim = ssim * 0.9562382616834844; + ssim = 2.326765642916932 * ssim - 0.020884521182843837 * ssim * ssim + + 6.248496625763138e-05 * ssim * ssim * ssim; + if (ssim > 0) { + ssim = 100.0 - 10.0 * pow(ssim, 0.6276336467831387); + } else { + ssim = 100.0; + } + return ssim; +} + +Msssim ComputeSSIMULACRA2(const jxl::ImageBundle& orig, + const jxl::ImageBundle& dist, float bg) { + Msssim msssim; + + jxl::Image3F img1(orig.xsize(), orig.ysize()); + jxl::Image3F img2(img1.xsize(), img1.ysize()); + + jxl::ImageBundle orig2 = orig.Copy(); + jxl::ImageBundle dist2 = dist.Copy(); + + if (orig.HasAlpha()) AlphaBlend(orig2, bg); + if (dist.HasAlpha()) AlphaBlend(dist2, bg); + orig2.ClearExtraChannels(); + dist2.ClearExtraChannels(); + + JXL_CHECK(orig2.TransformTo(jxl::ColorEncoding::LinearSRGB(orig2.IsGray()), + *JxlGetDefaultCms())); + JXL_CHECK(dist2.TransformTo(jxl::ColorEncoding::LinearSRGB(dist2.IsGray()), + *JxlGetDefaultCms())); + + jxl::ToXYB(orig2, nullptr, &img1, *JxlGetDefaultCms(), nullptr); + jxl::ToXYB(dist2, nullptr, &img2, *JxlGetDefaultCms(), nullptr); + MakePositiveXYB(img1); + MakePositiveXYB(img2); + + Image3F mul(img1.xsize(), img1.ysize()); + Blur blur(img1.xsize(), img1.ysize()); + + for (int scale = 0; scale < kNumScales; scale++) { + if (img1.xsize() < 8 || img1.ysize() < 8) { + break; + } + if (scale) { + orig2.SetFromImage(Downsample(*orig2.color(), 2, 2), + jxl::ColorEncoding::LinearSRGB(orig2.IsGray())); + dist2.SetFromImage(Downsample(*dist2.color(), 2, 2), + jxl::ColorEncoding::LinearSRGB(dist2.IsGray())); + img1.ShrinkTo(orig2.xsize(), orig2.ysize()); + img2.ShrinkTo(orig2.xsize(), orig2.ysize()); + jxl::ToXYB(orig2, nullptr, &img1, *JxlGetDefaultCms(), nullptr); + jxl::ToXYB(dist2, nullptr, &img2, *JxlGetDefaultCms(), nullptr); + MakePositiveXYB(img1); + MakePositiveXYB(img2); + } + mul.ShrinkTo(img1.xsize(), img1.ysize()); + blur.ShrinkTo(img1.xsize(), img1.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); + + MsssimScale sscale; + SSIMMap(mu1, mu2, sigma1_sq, sigma2_sq, sigma12, sscale.avg_ssim); + EdgeDiffMap(img1, mu1, img2, mu2, sscale.avg_edgediff); + msssim.scales.push_back(sscale); + } + return msssim; +} + +Msssim ComputeSSIMULACRA2(const jxl::ImageBundle& orig, + const jxl::ImageBundle& distorted) { + return ComputeSSIMULACRA2(orig, distorted, 0.5f); +} diff --git a/tools/ssimulacra2.h b/tools/ssimulacra2.h new file mode 100644 index 0000000..36d1193 --- /dev/null +++ b/tools/ssimulacra2.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_SSIMULACRA2_H_ +#define TOOLS_SSIMULACRA2_H_ + +#include <vector> + +#include "lib/jxl/image_bundle.h" + +struct MsssimScale { + double avg_ssim[3 * 2]; + double avg_edgediff[3 * 4]; +}; + +struct Msssim { + std::vector<MsssimScale> scales; + + double Score() const; +}; + +// Computes the SSIMULACRA 2 score between reference image 'orig' and +// distorted image 'distorted'. In case of alpha transparency, assume +// a gray background if intensity 'bg' (in range 0..1). +Msssim ComputeSSIMULACRA2(const jxl::ImageBundle &orig, + const jxl::ImageBundle &distorted, float bg); +Msssim ComputeSSIMULACRA2(const jxl::ImageBundle &orig, + const jxl::ImageBundle &distorted); + +#endif // TOOLS_SSIMULACRA2_H_ diff --git a/tools/ssimulacra2_main.cc b/tools/ssimulacra2_main.cc new file mode 100644 index 0000000..758f188 --- /dev/null +++ b/tools/ssimulacra2_main.cc @@ -0,0 +1,83 @@ +// 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" +// TODO(eustas): we should, but we can't? +// #include "lib/jxl/base/span.h" +#include "tools/file_io.h" +#include "tools/ssimulacra2.h" + +int PrintUsage(char** argv) { + fprintf(stderr, "Usage: %s orig.png distorted.png\n", argv[0]); + fprintf(stderr, + "Returns a score in range -inf..100, which correlates to subjective " + "visual quality:\n"); + fprintf(stderr, + " 30 = low quality (p10 worst output of mozjpeg -quality 30)\n"); + fprintf(stderr, + " 50 = medium quality (average output of cjxl -q 40 or mozjpeg " + "-quality 40,\n"); + fprintf(stderr, + " p10 output of cjxl -q 50 or mozjpeg " + "-quality 60)\n"); + fprintf(stderr, + " 70 = high quality (average output of cjxl -q 70 or mozjpeg " + "-quality 70,\n"); + fprintf(stderr, + " p10 output of cjxl -q 75 or mozjpeg " + "-quality 80)\n"); + fprintf(stderr, + " 90 = very high quality (impossible to distinguish from " + "original at 1:1,\n"); + fprintf(stderr, + " average output of cjxl -q 90 or " + "mozjpeg -quality 90)\n"); + return 1; +} + +int main(int argc, char** argv) { + if (argc != 3) return PrintUsage(argv); + + jxl::CodecInOut io[2]; + const char* purpose[] = {"original", "distorted"}; + for (size_t i = 0; i < 2; ++i) { + std::vector<uint8_t> encoded; + if (!jpegxl::tools::ReadFile(argv[1 + i], &encoded)) { + fprintf(stderr, "Could not load %s image: %s\n", purpose[i], argv[1 + i]); + return 1; + } + if (!jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(), + &io[i])) { + fprintf(stderr, "Could not decode %s image: %s\n", purpose[i], + argv[1 + i]); + return 1; + } + if (io[i].xsize() < 8 || io[i].ysize() < 8) { + fprintf(stderr, "Minimum image size is 8x8 pixels\n"); + return 1; + } + } + jxl::CodecInOut& io1 = io[0]; + jxl::CodecInOut& io2 = io[1]; + + if (io1.xsize() != io2.xsize() || io1.ysize() != io2.ysize()) { + fprintf(stderr, "Image size mismatch\n"); + return 1; + } + + if (!io1.Main().HasAlpha()) { + Msssim msssim = ComputeSSIMULACRA2(io1.Main(), io2.Main()); + printf("%.8f\n", msssim.Score()); + } else { + // in case of alpha transparency: blend against dark and bright backgrounds + // and return the worst of both scores + Msssim msssim0 = ComputeSSIMULACRA2(io1.Main(), io2.Main(), 0.1f); + Msssim msssim1 = ComputeSSIMULACRA2(io1.Main(), io2.Main(), 0.9f); + printf("%.8f\n", std::min(msssim0.Score(), msssim1.Score())); + } + return 0; +} diff --git a/tools/ssimulacra_main.cc b/tools/ssimulacra_main.cc index 5b48fe2..70538a5 100644 --- a/tools/ssimulacra_main.cc +++ b/tools/ssimulacra_main.cc @@ -6,8 +6,12 @@ #include <stdio.h> #include "lib/extras/codec.h" -#include "lib/jxl/color_management.h" -#include "lib/jxl/enc_color_management.h" +// TODO(eustas): we should, but we can't? +// #include "lib/jxl/base/span.h" +#include <jxl/cms.h> + +#include "lib/jxl/image_bundle.h" +#include "tools/file_io.h" #include "tools/ssimulacra.h" namespace ssimulacra { @@ -33,26 +37,31 @@ int Run(int argc, char** argv) { } 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()) { + jxl::CodecInOut io[2]; + for (size_t i = 0; i < 2; ++i) { + std::vector<uint8_t> encoded; + JXL_CHECK(jpegxl::tools::ReadFile(argv[input_arg + i], &encoded)); + JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(), + &io[i])); + } + jxl::ImageBundle& ib1 = io[0].Main(); + jxl::ImageBundle& ib2 = io[1].Main(); + JXL_CHECK(ib1.TransformTo(jxl::ColorEncoding::LinearSRGB(ib1.IsGray()), + *JxlGetDefaultCms(), nullptr)); + JXL_CHECK(ib2.TransformTo(jxl::ColorEncoding::LinearSRGB(ib2.IsGray()), + *JxlGetDefaultCms(), nullptr)); + jxl::Image3F& img1 = *ib1.color(); + jxl::Image3F& img2 = *ib2.color(); + if (img1.xsize() != img2.xsize() || img1.ysize() != img2.ysize()) { fprintf(stderr, "Image size mismatch\n"); return 1; } - if (io1.xsize() < 8 || io1.ysize() < 8) { + if (img1.xsize() < 8 || img1.ysize() < 8) { fprintf(stderr, "Minimum image size is 8x8 pixels\n"); return 1; } - Ssimulacra ssimulacra = - ComputeDiff(*io1.Main().color(), *io2.Main().color(), simple); + Ssimulacra ssimulacra = ComputeDiff(img1, img2, simple); if (verbose) { ssimulacra.PrintDetails(); diff --git a/tools/thread_pool_internal.h b/tools/thread_pool_internal.h new file mode 100644 index 0000000..92a1176 --- /dev/null +++ b/tools/thread_pool_internal.h @@ -0,0 +1,47 @@ +// 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_THREAD_POOL_INTERNAL_H_ +#define TOOLS_THREAD_POOL_INTERNAL_H_ + +#include <jxl/thread_parallel_runner_cxx.h> +#include <stddef.h> + +#include <cmath> +#include <thread> // NOLINT + +#include "lib/jxl/base/data_parallel.h" + +namespace jpegxl { +namespace tools { + +using ::jxl::ThreadPool; + +// Helper class to pass an internal ThreadPool-like object using threads. +class ThreadPoolInternal { + public: + // Starts the given number of worker threads and blocks until they are ready. + // "num_worker_threads" defaults to one per hyperthread. If zero, all tasks + // run on the main thread. + explicit ThreadPoolInternal( + size_t num_threads = std::thread::hardware_concurrency()) { + runner_ = + JxlThreadParallelRunnerMake(/* memory_manager */ nullptr, num_threads); + pool_.reset(new ThreadPool(JxlThreadParallelRunner, runner_.get())); + } + + ThreadPoolInternal(const ThreadPoolInternal&) = delete; + ThreadPoolInternal& operator&(const ThreadPoolInternal&) = delete; + ThreadPool* operator&() { return pool_.get(); } + + private: + JxlThreadParallelRunnerPtr runner_; + std::unique_ptr<ThreadPool> pool_; +}; + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_THREAD_POOL_INTERNAL_H_ diff --git a/tools/transforms_fuzzer.cc b/tools/transforms_fuzzer.cc index 1ef08b2..6d78a05 100644 --- a/tools/transforms_fuzzer.cc +++ b/tools/transforms_fuzzer.cc @@ -10,7 +10,21 @@ #include "lib/jxl/modular/encoding/encoding.h" #include "lib/jxl/modular/transform/transform.h" -namespace jxl { +namespace jpegxl { +namespace tools { + +using ::jxl::BitReader; +using ::jxl::BitReaderScopedCloser; +using ::jxl::Bytes; +using ::jxl::Channel; +using ::jxl::GroupHeader; +using ::jxl::Image; +using ::jxl::ModularOptions; +using ::jxl::pixel_type; +using ::jxl::Rng; +using ::jxl::Status; +using ::jxl::Transform; +using ::jxl::weighted::Header; namespace { void FillChannel(Channel& ch, Rng& rng) { @@ -32,7 +46,7 @@ void AssertEq(T a, T b) { int TestOneInput(const uint8_t* data, size_t size) { static Status nevermind = true; - BitReader reader(Span<const uint8_t>(data, size)); + BitReader reader(Bytes(data, size)); BitReaderScopedCloser reader_closer(&reader, &nevermind); Rng rng(reader.ReadFixedBits<56>()); @@ -50,8 +64,8 @@ int TestOneInput(const uint8_t* data, size_t size) { 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); + size_t w = jxl::DivCeil(w_orig, upsampling); + size_t h = jxl::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))) { @@ -80,21 +94,22 @@ int TestOneInput(const uint8_t* data, size_t size) { 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])); + ch.shrink(jxl::DivCeil(w, 1 << hshift[c]), jxl::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)); + int up_level = + jxl::CeilLog2Nonzero(ch_up) - jxl::CeilLog2Nonzero(upsampling); + ch.shrink(jxl::DivCeil(w_orig, ch_up), jxl::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; + if (!jxl::Bundle::Read(&reader, &header)) return 0; + Header w_header; + if (!jxl::Bundle::Read(&reader, &w_header)) return 0; // TODO(eustas): give it a try? if (!reader.AllReadsWithinBounds()) return 0; @@ -122,16 +137,17 @@ int TestOneInput(const uint8_t* data, size_t size) { 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])); + AssertEq(ch.w, jxl::DivCeil(w, 1 << hshift[c])); + AssertEq(ch.h, jxl::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)); + int up_level = + jxl::CeilLog2Nonzero(ch_up) - jxl::CeilLog2Nonzero(upsampling); + AssertEq(ch.w, jxl::DivCeil(w_orig, ch_up)); + AssertEq(ch.h, jxl::DivCeil(h_orig, ch_up)); AssertEq(ch.hshift, up_level); AssertEq(ch.vshift, up_level); } @@ -139,8 +155,9 @@ int TestOneInput(const uint8_t* data, size_t size) { return 0; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - return jxl::TestOneInput(data, size); + return jpegxl::tools::TestOneInput(data, size); } diff --git a/tools/upscaling_coefficients/upscaler_demo.py b/tools/upscaling_coefficients/upscaler_demo.py index 89f1320..e873bd1 100644 --- a/tools/upscaling_coefficients/upscaler_demo.py +++ b/tools/upscaling_coefficients/upscaler_demo.py @@ -37,14 +37,14 @@ def convolution(pixels, kernel): `kernel`. Args: - pixels: A [heigth, width]- or [height, width, num_channels]-array + pixels: A [height, 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 + A [upscaling_factor*height, upscaling_factor*width]- or [upscaling_factor*height, upscaling_factor*width, num_channels]-array representing the convoluted upscaled image. """ diff --git a/tools/viewer/CMakeLists.txt b/tools/viewer/CMakeLists.txt index 7dbe5e3..2b25e26 100644 --- a/tools/viewer/CMakeLists.txt +++ b/tools/viewer/CMakeLists.txt @@ -3,9 +3,9 @@ # 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.") +find_package(Qt6 QUIET COMPONENTS Widgets) +if (NOT Qt6_FOUND) + message(WARNING "Qt6 was not found. The directory viewer will not be built.") return() endif () @@ -31,7 +31,7 @@ target_include_directories(viewer PRIVATE "${PROJECT_SOURCE_DIR}" ) target_link_libraries(viewer - Qt5::Widgets + Qt6::Widgets icc_detect jxl jxl_threads diff --git a/tools/viewer/load_jxl.cc b/tools/viewer/load_jxl.cc index 7fd35d8..b97a906 100644 --- a/tools/viewer/load_jxl.cc +++ b/tools/viewer/load_jxl.cc @@ -5,18 +5,21 @@ #include "tools/viewer/load_jxl.h" +#include <jxl/decode.h> +#include <jxl/decode_cxx.h> +#include <jxl/thread_parallel_runner_cxx.h> +#include <jxl/types.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" +#define CMS_NO_REGISTER_KEYWORD 1 #include "lcms2.h" +#undef CMS_NO_REGISTER_KEYWORD -namespace jxl { +namespace jpegxl { +namespace tools { namespace { @@ -97,12 +100,11 @@ QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile, size_t icc_size; EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetICCProfileSize( - dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)); + dec.get(), 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())); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetColorAsICCProfile( + dec.get(), 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())); @@ -135,40 +137,19 @@ QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile, 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 - ); + : QImage::Format_RGBA64); 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 + data[4 * x + 3]); } } return result; } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/viewer/load_jxl.h b/tools/viewer/load_jxl.h index 594f646..85dc1a9 100644 --- a/tools/viewer/load_jxl.h +++ b/tools/viewer/load_jxl.h @@ -10,11 +10,13 @@ #include <QImage> #include <QString> -namespace jxl { +namespace jpegxl { +namespace tools { QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile, qint64* elapsed, bool* usedRequestedProfile = nullptr); -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_VIEWER_LOAD_JXL_H_ diff --git a/tools/viewer/main.cc b/tools/viewer/main.cc index d677888..1e80be3 100644 --- a/tools/viewer/main.cc +++ b/tools/viewer/main.cc @@ -12,7 +12,7 @@ int main(int argc, char** argv) { QStringList arguments = application.arguments(); arguments.removeFirst(); - jxl::ViewerWindow window; + jpegxl::tools::ViewerWindow window; window.show(); if (!arguments.empty()) { diff --git a/tools/viewer/viewer_window.cc b/tools/viewer/viewer_window.cc index 530c2f0..6b5f912 100644 --- a/tools/viewer/viewer_window.cc +++ b/tools/viewer/viewer_window.cc @@ -15,7 +15,8 @@ #include "tools/icc_detect/icc_detect.h" #include "tools/viewer/load_jxl.h" -namespace jxl { +namespace jpegxl { +namespace tools { namespace { @@ -50,7 +51,7 @@ void ViewerWindow::loadFilesAndDirectories(QStringList entries) { filenames_.clear(); QSet<QString> visited; for (const QString& entry : entries) { - recursivelyAddSubEntries(entry, &visited, &filenames_); + recursivelyAddSubEntries(QFileInfo(entry), &visited, &filenames_); } const bool several = filenames_.size() > 1; @@ -127,4 +128,5 @@ void ViewerWindow::refreshImage() { } } -} // namespace jxl +} // namespace tools +} // namespace jpegxl diff --git a/tools/viewer/viewer_window.h b/tools/viewer/viewer_window.h index 42de5bc..78aafb9 100644 --- a/tools/viewer/viewer_window.h +++ b/tools/viewer/viewer_window.h @@ -12,7 +12,8 @@ #include "tools/viewer/ui_viewer_window.h" -namespace jxl { +namespace jpegxl { +namespace tools { class ViewerWindow : public QMainWindow { Q_OBJECT @@ -36,6 +37,7 @@ class ViewerWindow : public QMainWindow { bool hasWarnedAboutMonitorProfile_ = false; }; -} // namespace jxl +} // namespace tools +} // namespace jpegxl #endif // TOOLS_VIEWER_VIEWER_WINDOW_H_ diff --git a/tools/wasm_demo/CMakeLists.txt b/tools/wasm_demo/CMakeLists.txt new file mode 100644 index 0000000..0549e76 --- /dev/null +++ b/tools/wasm_demo/CMakeLists.txt @@ -0,0 +1,64 @@ +if (NOT JPEGXL_ENABLE_TOOLS OR NOT EMSCRIPTEN) + return() +endif() + +# WASM API facade. +add_executable(jxl_decoder jxl_decoder.cc jxl_decompressor.cc no_png.cc) +add_executable(jxl_decoder_for_test jxl_decoder.cc jxl_decompressor.cc no_png.cc) +target_link_libraries(jxl_decoder jxl_extras-internal jxl_threads) +target_link_libraries(jxl_decoder_for_test jxl_extras-internal jxl_threads) + +set(JXL_C_SYMBOLS + _free + _malloc +) + +set(JXL_DECODER_SYMBOLS + _jxlCreateInstance + _jxlDestroyInstance + _jxlFlush + _jxlProcessInput +) + +set(JXL_DECOMPRESSOR_SYMBOLS + _jxlDecompress + _jxlCleanup +) + +set(JXL_MODULE_SYMBOLS ${JXL_C_SYMBOLS} ${JXL_DECODER_SYMBOLS} ${JXL_DECOMPRESSOR_SYMBOLS}) + +list(JOIN JXL_MODULE_SYMBOLS ", " JXL_MODULE_EXPORTS) + +set(JXL_WASM_SITE_LINK_FLAGS " -O3 -s FILESYSTEM=0 --closure 1 -mnontrapping-fptoint") +set(JXL_WASM_TEST_LINK_FLAGS " -O1 -s NODERAWFS=1 ") + +set(JXL_WASM_BASE_LINK_FLAGS "\ + -s ALLOW_MEMORY_GROWTH=1 \ + -s DISABLE_EXCEPTION_CATCHING=1 \ + -s MODULARIZE=1 \ + -s USE_PTHREADS=1 \ + -s PTHREAD_POOL_SIZE=4 \ +") + +# libpng is used only by "decompressor" +set(JXL_DECODER_LINK_FLAGS "${JXL_WASM_BASE_LINK_FLAGS} \ + -s EXPORT_NAME=\"JxlDecoderModule\" \ + -s \"EXPORTED_FUNCTIONS=[${JXL_MODULE_EXPORTS}]\" \ +") + +set_target_properties(jxl_decoder PROPERTIES LINK_FLAGS + "${JXL_DECODER_LINK_FLAGS} ${JXL_WASM_SITE_LINK_FLAGS}") + +set_target_properties(jxl_decoder_for_test PROPERTIES LINK_FLAGS + "${JXL_DECODER_LINK_FLAGS} ${JXL_WASM_TEST_LINK_FLAGS}") + +if (BUILD_TESTING) + add_test( + NAME test_wasm_jxl_decoder + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} + --no-experimental-fetch + ${CMAKE_CURRENT_SOURCE_DIR}/jxl_decoder_test.js + ) + set_tests_properties(test_wasm_jxl_decoder PROPERTIES + ENVIRONMENT NODE_PATH=$<TARGET_FILE_DIR:jxl_decoder_for_test>) +endif() # BUILD_TESTING diff --git a/tools/wasm_demo/README.md b/tools/wasm_demo/README.md new file mode 100644 index 0000000..804cd35 --- /dev/null +++ b/tools/wasm_demo/README.md @@ -0,0 +1,126 @@ +## WebAssembly demonstration + +This folder contains an example how to decode JPEG XL files on a web page using +WASM engine. + +### One line demo + +The simplest way to get support of JXL images on the client side is simply to +link one extra script (`<script src="service_worker.js">`) to the page. +This script installs a `ServiceWorker` that: + + - checks if the browser supports the JXL image format already + - if it is not, then advertise `image/jxl` as media format in image requests + - then, if the server responds with `image/jxl` content it gets decoded and + re-encoded to PNG on the fly + +Generally the message / data flow looks the following way: + + - `Fetch API` receives a resource request from client page (e.g. when the HTML + engine discovers an `img` tag) and asks the `ServiceWorker` how to proceed + - the `ServiceWorker` alters the request and uses the `Fetch API` + to obtain data + - when data arrives, the `ServiceWorker` forwards it to the "client" + (the page) that initiated the resource request + - the client forwards the data to a worker (see `client_worker.js`) to avoid + processing in the "main loop" thread + - a worker does the actual decoding; to make it faster several additional + workers are spawned (to enable multi-threading in WASM module); + the decoded image is wrapped in non-compressed PNG format and sent back + to client + - the client relays image data to `ServiceWorker` + - the `ServiceWorker` passes data to `Fetch API` as a response to initial + resource request + +Despite the additional "hop" (client) in the flow, data is not copied every +time but rather "transferred" between the participants. + +Demo page: `one_line_demo.html`. Extended demo, that also shows how long it +took do decode images: `one_line_demo_with_console.html`. + +Page that shows "manual" decoding (and has benchmarking capabilities): +`manual_decode_demo.html`. + +### Hosting + +To enable multi-threading some files should be served in a secure context (i.e. +transferred over HTTPS) and executed in a "site-isolation" mode (controlled by +COOP and COEP response headers). + +Unfortunately [GitHub Pages](https://pages.github.com/) does not allow setting +response headers. + +[Netlify](https://www.netlify.com/) provides free, easy to setup and deploy +platform for serving such demonstration sites. However, any other +service provider / software that allows changing response headers could be +employed as well. + +`netlify.toml` and `netlify/precompressed.ts` specify the serving rules. +Namely, some requests get "upgraded" responses: + + - if a request specifies that `brotli` compression is supported, + then precompressed entries are sent + - if a request specifies that `image/jxl` format is allowed, + then entries transcoded to JXL format are sent + +### How to build the demo + +`build_site.py` script takes care of JavaScript minification, template +substitution and resource compression. Its arguments are: + + - source path: site template directory (that contains this README file) + - binary path: build directory, that contains compiled WASM module + - output path + +To complete the site few more files are to be added to output directory: + + - `image00.jpg`, `image01.png` demo images; will be shown if `ServiceWorker` + is not yet operable (fallback); to see those one could initiate + "hard page reload" (press Shift-(Ctrl|Cmd)-R) + - `image00.jpg.jxl`, `image01.png.jxl` demo images in JXL format + - `imageNN.jxl` images for "manual" decoding demo; NN is a number starting + form `00` + - `favicon.ico` is an optional site icon + - `index.html` is an optional site "home" page + +In the source code (`service_worker.js`) there are two compile-time constants +that modify the behaviour of Service Worker: + + - `FORCE_COP` flag allows rewriting responses to add COOP / COEP headers; + this is useful when it is difficult / impossible to setup response headers + otherwise (e.g. GitHub Pages) + - `FORCE_DECODING` flag activate JXL decoding when image response type has + `Content-Encoding` header set to `application/octet-stream`; this happens + when server does not know the JXL MIME-type + +One dependency that `build_site.py` requires is [uglifyjs](https://github.com/mishoo/UglifyJS), which can be installed with +``` +npm install uglify-js -g +``` +If you followed the [wasm build instructions](../../docs/building_wasm.md), +assuming you are in the root level of the cloned libjxl repo a typical call to +build the site would be +```bash +python3 ./tools/wasm_demo/build_site.py ./tools/wasm_demo/ ./build-wasm32/tools/wasm_demo/ /path/to/demo-site +``` +Then you need to put your image files in the correct same place and are should be good to go. + + +To summarize, using the wasm decoder together with a service workder amounts to adding +```html +<script src="service_worker.js"></script> +``` +to your html and then putting the `service_worker.js` and `jxl_decoder.wasm` binary in directory where they can be read. + + +It is not guaranteed, but somewhat fresh demo is hosted on +`https://jxl-demo.netlify.app/`, e.g.: + + - [one line demo](https://jxl-demo.netlify.app/one_line_demo_with_console.html) + - [one line demo with console](https://jxl-demo.netlify.app/one_line_demo.html) + - [manual decode demo](https://jxl-demo.netlify.app/manual_decode_demo.html?img=1&colorSpace=rec2100-pq&runBenchmark=30&wantSdr=false&displayNits=1500); + URL contains query parameters that control rendering and benchmarking options; + please note, that HDR canvas is often not enabled by default, it could be + enabled in some browsers via `about://flags/#enable-experimental-web-platform-features` + - [`service_worker.js`](https://jxl-demo.netlify.app/service_worker.js) + - [`jxl_decoder.wasm`](https://jxl-demo.netlify.app/jxl_decoder.wasm) diff --git a/tools/wasm_demo/build_site.py b/tools/wasm_demo/build_site.py new file mode 100644 index 0000000..f47fa97 --- /dev/null +++ b/tools/wasm_demo/build_site.py @@ -0,0 +1,145 @@ +#!/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 shutil +import subprocess +import sys + +from pathlib import Path + +BROTLIFY = False +ZOPFLIFY = False +LEAN = True +NETLIFY = False + +REMOVE_SHEBANG = ['jxl_decoder.js'] +EMBED_BIN = [ + 'jxl_decoder.js', + 'jxl_decoder.worker.js' +] +EMBED_SRC = ['client_worker.js'] +TEMPLATES = ['service_worker.js'] +COPY_BIN = ['jxl_decoder.wasm'] + [] if LEAN else EMBED_BIN +COPY_SRC = [ + 'one_line_demo.html', + 'one_line_demo_with_console.html', + 'manual_decode_demo.html', +] + [] if not NETLIFY else [ + 'netlify.toml', + 'netlify' +] + [] if LEAN else EMBED_SRC + +COMPRESS = COPY_BIN + COPY_SRC + TEMPLATES +COMPRESSIBLE_EXT = ['.html', '.js', '.wasm'] + +def escape_js(js): + return js.replace('\\', '\\\\').replace('\'', '\\\'') + +def remove_shebang(txt): + lines = txt.splitlines(True) # Keep line-breaks + if len(lines) > 0: + if lines[0].startswith('#!'): + lines = lines[1:] + return ''.join(lines) + +def compress(path): + name = path.name + compressible = any([name.endswith(ext) for ext in COMPRESSIBLE_EXT]) + if not compressible: + print(f'Not compressing {name}') + return + print(f'Processing {name}') + orig_size = path.stat().st_size + if BROTLIFY: + cmd_brotli = ['brotli', '-Zfk', path.absolute()] + subprocess.run(cmd_brotli, check=True, stdout=sys.stdout, stderr=sys.stderr) + br_size = path.parent.joinpath(name + '.br').stat().st_size + print(f' Brotli: {orig_size} -> {br_size}') + if ZOPFLIFY: + cmd_zopfli = ['zopfli', path.absolute()] + subprocess.run(cmd_zopfli, check=True, stdout=sys.stdout, stderr=sys.stderr) + gz_size = path.parent.joinpath(name + '.gz').stat().st_size + print(f' Zopfli: {orig_size} -> {gz_size}') + +def check_util(name): + cmd = [name, '-h'] + try: + subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except: + print(f"NOTE: {name} not installed") + return False + return True + +def check_utils(): + global BROTLIFY + BROTLIFY = BROTLIFY and check_util('brotli') + global ZOPFLIFY + ZOPFLIFY = ZOPFLIFY and check_util('zopfli') + if not check_util('uglifyjs'): + print("FAIL: uglifyjs is required to build a site") + sys.exit() + +def uglify(text, name): + cmd = ['uglifyjs', '-m', '-c'] + ugly_result = subprocess.run( + cmd, capture_output=True, check=True, input=text, text=True) + ugly_text = ugly_result.stdout.strip() + print(f'Uglify {name}: {len(text)} -> {len(ugly_text)}') + return ugly_text + +if __name__ == "__main__": + if len(sys.argv) != 4: + print(f"Usage: python3 {sys.argv[0]} SRC_DIR BINARY_DIR OUTPUT_DIR") + exit(-1) + source_path = Path(sys.argv[1]) # CMake build dir + binary_path = Path(sys.argv[2]) # Site template dir + output_path = Path(sys.argv[3]) # Site output + + check_utils() + + for name in REMOVE_SHEBANG: + path = binary_path.joinpath(name) + text = path.read_text().strip() + path.write_text(remove_shebang(text)) + remove_shebang + + substitutes = {} + + for name in EMBED_BIN: + key = '$' + name + '$' + path = binary_path.joinpath(name) + value = escape_js(uglify(path.read_text().strip(), name)) + substitutes[key] = value + + for name in EMBED_SRC: + key = '$' + name + '$' + path = source_path.joinpath(name) + value = escape_js(uglify(path.read_text().strip(), name)) + substitutes[key] = value + + for name in TEMPLATES: + print(f'Processing template {name}') + path = source_path.joinpath(name) + text = path.read_text().strip() + for key, value in substitutes.items(): + text = text.replace(key, value) + #text = uglify(text, name) + output_path.joinpath(name).write_text(text) + + for name in COPY_SRC: + path = source_path.joinpath(name) + if path.is_dir(): + shutil.copytree(path, output_path.joinpath( + name).absolute(), dirs_exist_ok=True) + else: + shutil.copy(path, output_path.absolute()) + + # TODO(eustas): uglify + for name in COPY_BIN: + shutil.copy(binary_path.joinpath(name), output_path.absolute()) + + for name in COMPRESS: + compress(output_path.joinpath(name)) diff --git a/tools/wasm_demo/client_worker.js b/tools/wasm_demo/client_worker.js new file mode 100644 index 0000000..5751b38 --- /dev/null +++ b/tools/wasm_demo/client_worker.js @@ -0,0 +1,99 @@ +// 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. + +let decoder = null; + +// Serialize work; plus postpone processing until decoder is ready. +let jobs = []; + +const processJobs = () => { + // Decoder not yet loaded. + if (!decoder) { + return; + } + + while (true) { + let job = null; + // Currently we do not do progressive; process only "inputComplete" jobs. + for (let i = 0; i < jobs.length; ++i) { + if (!jobs[i].inputComplete) { + continue; + } + job = jobs[i]; + jobs[i] = jobs[jobs.length - 1]; + jobs.pop(); + break; + } + if (!job) { + return; + } + console.log('CW job: ' + job.uid); + const input = job.input; + let totalInputLength = 0; + for (let i = 0; i < input.length; i++) { + totalInputLength += input[i].length; + } + + // TODO(eustas): persist to reduce fragmentation? + const buffer = decoder._malloc(totalInputLength); + // TODO(eustas): check OOM + let offset = 0; + for (let i = 0; i < input.length; ++i) { + decoder.HEAP8.set(input[i], buffer + offset); + offset += input[i].length; + } + let t0 = Date.now(); + // TODO(eustas): check result + const result = decoder._jxlDecompress(buffer, totalInputLength); + let t1 = Date.now(); + const msg = 'Decoded ' + job.url + ' in ' + (t1 - t0) + 'ms'; + // console.log(msg); + decoder._free(buffer); + const outputLength = decoder.HEAP32[result >> 2]; + const outputAddr = decoder.HEAP32[(result + 4) >> 2]; + const output = new Uint8Array(outputLength); + const outputSrc = new Uint8Array(decoder.HEAP8.buffer); + output.set(outputSrc.slice(outputAddr, outputAddr + outputLength)); + decoder._jxlCleanup(result); + const response = {uid: job.uid, data: output, msg: msg}; + postMessage(response, [output.buffer]); + } +}; + +onmessage = function(event) { + const data = event.data; + console.log('CW received: ' + data.op); + if (data.op === 'decodeJxl') { + let job = null; + for (let i = 0; i < jobs.length; ++i) { + if (jobs[i].uid === data.uid) { + job = jobs[i]; + break; + } + } + if (!job) { + job = {uid: data.uid, input: [], inputComplete: false, url: data.url}; + jobs.push(job); + } + if (data.data) { + job.input.push(data.data); + } else { + job.inputComplete = true; + } + processJobs(); + } +}; + +const onLoadJxlModule = (instance) => { + decoder = instance; + processJobs(); +}; + +importScripts('jxl_decoder.js'); +const config = { + mainScriptUrlOrBlob: 'https://jxl-demo.netlify.app/jxl_decoder.js', + INITIAL_MEMORY: 16 * 1024 * 1024, +}; +JxlDecoderModule(config).then(onLoadJxlModule); diff --git a/tools/jxl_emcc.cc b/tools/wasm_demo/jxl_decoder.cc index c4c855a..633674c 100644 --- a/tools/jxl_emcc.cc +++ b/tools/wasm_demo/jxl_decoder.cc @@ -3,25 +3,25 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include "tools/wasm_demo/jxl_decoder.h" + +#include <jxl/decode.h> +#include <jxl/decode_cxx.h> +#include <jxl/thread_parallel_runner_cxx.h> + +#include <cstdio> #include <cstring> #include <memory> #include <vector> -#include "jxl/decode.h" -#include "jxl/decode_cxx.h" -#include "jxl/thread_parallel_runner_cxx.h" - -#if !defined(__wasm__) -#include "lib/jxl/base/file_io.h" -#endif +extern "C" { namespace { -struct DecoderInstance { - uint32_t width = 0; - uint32_t height = 0; - uint8_t* pixels = nullptr; - uint32_t color_space = 0; +struct DecoderInstancePrivate { + // Due to "Standard Layout" rules it is guaranteed that address of the entity + // and its first non-static member are the same. + DecoderInstance info; size_t pixels_size = 0; bool want_sdr; @@ -35,26 +35,29 @@ struct DecoderInstance { } // namespace -extern "C" { +DecoderInstance* jxlCreateInstance(bool want_sdr, uint32_t display_nits) { + DecoderInstancePrivate* self = new DecoderInstancePrivate(); -void* jxlCreateInstance(bool want_sdr, uint32_t display_nits) { - DecoderInstance* instance = new DecoderInstance(); - instance->want_sdr = want_sdr; - instance->display_nits = display_nits; + if (!self) { + return nullptr; + } + + self->want_sdr = want_sdr; + self->display_nits = display_nits; JxlDataType storageFormat = want_sdr ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16; - instance->format = {4, storageFormat, JXL_NATIVE_ENDIAN, 0}; - instance->decoder = JxlDecoderMake(nullptr); + self->format = {4, storageFormat, JXL_NATIVE_ENDIAN, 0}; + self->decoder = JxlDecoderMake(nullptr); - JxlDecoder* dec = instance->decoder.get(); + JxlDecoder* dec = self->decoder.get(); auto report_error = [&](uint32_t code, const char* text) { fprintf(stderr, "%s\n", text); - // instance->result = code; - return instance; + delete self; + return reinterpret_cast<DecoderInstance*>(code); }; - instance->thread_pool = JxlThreadParallelRunnerMake(nullptr, 4); - void* runner = instance->thread_pool.get(); + self->thread_pool = JxlThreadParallelRunnerMake(nullptr, 4); + void* runner = self->thread_pool.get(); auto status = JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner); @@ -74,33 +77,32 @@ void* jxlCreateInstance(bool want_sdr, uint32_t display_nits) { if (JXL_DEC_SUCCESS != status) { return report_error(3, "JxlDecoderSetProgressiveDetail failed"); } - return instance; + return &self->info; } -void jxlDestroyInstance(void* opaque_instance) { - if (opaque_instance == nullptr) return; - DecoderInstance* instance = - reinterpret_cast<DecoderInstance*>(opaque_instance); +void jxlDestroyInstance(DecoderInstance* instance) { + if (instance == nullptr) return; + DecoderInstancePrivate* self = + reinterpret_cast<DecoderInstancePrivate*>(instance); if (instance->pixels) { free(instance->pixels); } - delete instance; + delete self; } -uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input, +uint32_t jxlProcessInput(DecoderInstance* instance, const uint8_t* input, size_t input_size) { - if (opaque_instance == nullptr) return static_cast<uint32_t>(-1); - DecoderInstance* instance = - reinterpret_cast<DecoderInstance*>(opaque_instance); - JxlDecoder* dec = instance->decoder.get(); + if (instance == nullptr) return static_cast<uint32_t>(-1); + DecoderInstancePrivate* self = + reinterpret_cast<DecoderInstancePrivate*>(instance); + JxlDecoder* dec = self->decoder.get(); auto report_error = [&](int code, const char* text) { fprintf(stderr, "%s\n", text); - // instance->result = code; return static_cast<uint32_t>(code); }; - std::vector<uint8_t>& tail = instance->tail; + std::vector<uint8_t>& tail = self->tail; if (!tail.empty()) { tail.reserve(tail.size() + input_size); tail.insert(tail.end(), input, input + input_size); @@ -152,8 +154,8 @@ uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input, } instance->width = info.xsize; instance->height = info.ysize; - status = JxlDecoderImageOutBufferSize(dec, &instance->format, - &instance->pixels_size); + status = + JxlDecoderImageOutBufferSize(dec, &self->format, &self->pixels_size); if (status != JXL_DEC_SUCCESS) { release_input(); return report_error(-6, "JxlDecoderImageOutBufferSize failed"); @@ -162,15 +164,14 @@ uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input, release_input(); return report_error(-7, "Tried to realloc pixels"); } - instance->pixels = - reinterpret_cast<uint8_t*>(malloc(instance->pixels_size)); + instance->pixels = reinterpret_cast<uint8_t*>(malloc(self->pixels_size)); } else if (JXL_DEC_NEED_IMAGE_OUT_BUFFER == status) { - if (!instance->pixels) { + if (!self->info.pixels) { release_input(); return report_error(-8, "Out buffer not allocated"); } - status = JxlDecoderSetImageOutBuffer( - dec, &instance->format, instance->pixels, instance->pixels_size); + status = JxlDecoderSetImageOutBuffer(dec, &self->format, instance->pixels, + self->pixels_size); if (status != JXL_DEC_SUCCESS) { release_input(); return report_error(-9, "JxlDecoderSetImageOutBuffer failed"); @@ -180,8 +181,8 @@ uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input, color_encoding.color_space = JXL_COLOR_SPACE_RGB; color_encoding.white_point = JXL_WHITE_POINT_D65; color_encoding.primaries = - instance->want_sdr ? JXL_PRIMARIES_SRGB : JXL_PRIMARIES_2100; - color_encoding.transfer_function = instance->want_sdr + self->want_sdr ? JXL_PRIMARIES_SRGB : JXL_PRIMARIES_2100; + color_encoding.transfer_function = self->want_sdr ? JXL_TRANSFER_FUNCTION_SRGB : JXL_TRANSFER_FUNCTION_PQ; color_encoding.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL; @@ -200,15 +201,15 @@ uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input, return 0; } -uint32_t jxlFlush(void* opaque_instance) { - if (opaque_instance == nullptr) return static_cast<uint32_t>(-1); - DecoderInstance* instance = - reinterpret_cast<DecoderInstance*>(opaque_instance); - JxlDecoder* dec = instance->decoder.get(); +uint32_t jxlFlush(DecoderInstance* instance) { + if (instance == nullptr) return static_cast<uint32_t>(-1); + DecoderInstancePrivate* self = + reinterpret_cast<DecoderInstancePrivate*>(instance); + JxlDecoder* dec = self->decoder.get(); auto report_error = [&](int code, const char* text) { fprintf(stderr, "%s\n", text); - // instance->result = code; + // self->result = code; return static_cast<uint32_t>(code); }; @@ -224,20 +225,4 @@ uint32_t jxlFlush(void* opaque_instance) { return 0; } -#if !defined(__wasm__) -int main(int argc, char* argv[]) { - std::vector<uint8_t> data; - JXL_RETURN_IF_ERROR(jxl::ReadFile(argv[1], &data)); - fprintf(stderr, "File size: %d\n", (int)data.size()); - - void* instance = jxlCreateInstance(true, 100); - uint32_t status = jxlProcessInput(instance, data.data(), data.size()); - fprintf(stderr, "Process result: %d\n", status); - jxlFlush(instance); - status = jxlProcessInput(instance, nullptr, 0); - fprintf(stderr, "Process result: %d\n", status); - jxlDestroyInstance(instance); -} -#endif - } // extern "C" diff --git a/tools/wasm_demo/jxl_decoder.h b/tools/wasm_demo/jxl_decoder.h new file mode 100644 index 0000000..ad6d88e --- /dev/null +++ b/tools/wasm_demo/jxl_decoder.h @@ -0,0 +1,48 @@ +// 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_WASM_DEMO_JXL_DECODER_H_ +#define TOOLS_WASM_DEMO_JXL_DECODER_H_ + +#include <stddef.h> +#include <stdint.h> + +extern "C" { + +typedef struct DecoderInstance { + uint32_t width = 0; + uint32_t height = 0; + uint8_t* pixels = nullptr; + + // The rest is opaque. +} DecoderInstance; + +/* + Returns (as uint32_t): + 0 - OOM + 1 - JxlDecoderSetParallelRunner failed + 2 - JxlDecoderSubscribeEvents failed + 3 - JxlDecoderSetProgressiveDetail failed + >=4 - OK + */ +DecoderInstance* jxlCreateInstance(bool want_sdr, uint32_t display_nits); + +void jxlDestroyInstance(DecoderInstance* instance); + +/* + Returns (as uint32_t): + 0 - OK (pixels are ready) + 1 - ready to flush + 2 - needs more input + >=3 - error + */ +uint32_t jxlProcessInput(DecoderInstance* instance, const uint8_t* input, + size_t input_size); + +uint32_t jxlFlush(DecoderInstance* instance); + +} // extern "C" + +#endif // TOOLS_WASM_DEMO_JXL_DECODER_H_ diff --git a/tools/wasm_demo/jxl_decoder_test.js b/tools/wasm_demo/jxl_decoder_test.js new file mode 100644 index 0000000..22dfa07 --- /dev/null +++ b/tools/wasm_demo/jxl_decoder_test.js @@ -0,0 +1,140 @@ +// 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. + +function assertTrue(ok, msg) { + if (!ok) { + console.log('FAIL: ' + msg); + process.exit(1); + } +} + +function runTest(testFn) { + console.log('Running ' + testFn.name); + testFn(); + console.log('PASS'); +} + +let jxlModule; + +const isAddress = (v) => { + return (v >= 4) && ((v & (1 << 31)) === 0); +}; + +let splinesJxl = new Uint8Array([ + 0xff, 0x0a, 0xf8, 0x19, 0x10, 0x09, 0xd8, 0x63, 0x10, 0x00, 0xbc, 0x00, + 0xa6, 0x19, 0x4a, 0xa3, 0x56, 0x8c, 0x94, 0x62, 0x24, 0x7d, 0x12, 0x72, + 0x87, 0x00, 0x00, 0xda, 0xd4, 0xc9, 0xc1, 0xe2, 0x9e, 0x02, 0xb9, 0x37, + 0x00, 0xfe, 0x07, 0x9a, 0x91, 0x08, 0xcd, 0xbf, 0xa1, 0xdc, 0x71, 0x36, + 0x62, 0xc8, 0x97, 0x31, 0xc4, 0x3e, 0x58, 0x02, 0xc1, 0x01, 0x00 +]); + +let crossJxl = new Uint8Array([ + 0xff, 0x0a, 0x98, 0x10, 0x10, 0x50, 0x5c, 0x08, 0x08, 0x02, 0x01, + 0x00, 0x98, 0x00, 0x4b, 0x18, 0x8b, 0x15, 0x00, 0xd4, 0x92, 0x62, + 0xcc, 0x98, 0x91, 0x17, 0x08, 0x01, 0xe0, 0x92, 0xbc, 0x7e, 0xdf, + 0xbf, 0xff, 0x50, 0xc0, 0x64, 0x35, 0xb0, 0x40, 0x1e, 0x24, 0xa9, + 0xac, 0x38, 0xd9, 0x13, 0x1e, 0x85, 0x4a, 0x0d +]); + +function testSdr() { + let decoder = jxlModule._jxlCreateInstance( + /* wantSdr */ true, /* displayNits */ 100); + assertTrue(isAddress(decoder), 'create decoder instance'); + let encoded = splinesJxl; + let buffer = jxlModule._malloc(encoded.length); + jxlModule.HEAP8.set(encoded, buffer); + + let result = jxlModule._jxlProcessInput(decoder, buffer, encoded.length); + assertTrue(result === 0, 'process input'); + + let w = jxlModule.HEAP32[decoder >> 2]; + let h = jxlModule.HEAP32[(decoder + 4) >> 2]; + let pixelData = jxlModule.HEAP32[(decoder + 8) >> 2]; + + assertTrue(pixelData, 'output allocated'); + assertTrue(h === 320, 'output height'); + assertTrue(w === 320, 'output width '); + + jxlModule._jxlDestroyInstance(decoder); + jxlModule._free(buffer); +} + +function testRegular() { + let decoder = jxlModule._jxlCreateInstance( + /* wantSdr */ false, /* displayNits */ 100); + assertTrue(isAddress(decoder), 'create decoder instance'); + let encoded = splinesJxl; + let buffer = jxlModule._malloc(encoded.length); + jxlModule.HEAP8.set(encoded, buffer); + + let result = jxlModule._jxlProcessInput(decoder, buffer, encoded.length); + assertTrue(result === 0, 'process input'); + + let w = jxlModule.HEAP32[decoder >> 2]; + let h = jxlModule.HEAP32[(decoder + 4) >> 2]; + let pixelData = jxlModule.HEAP32[(decoder + 8) >> 2]; + + assertTrue(pixelData, 'output allocated'); + assertTrue(h === 320, 'output height'); + assertTrue(w === 320, 'output width '); + + jxlModule._jxlDestroyInstance(decoder); + jxlModule._free(buffer); +} + +function testChunks() { + let decoder = jxlModule._jxlCreateInstance( + /* wantSdr */ false, /* displayNits */ 100); + assertTrue(isAddress(decoder), 'create decoder instance'); + let encoded = splinesJxl; + let buffer = jxlModule._malloc(encoded.length); + jxlModule.HEAP8.set(encoded, buffer); + + let part1_length = encoded.length >> 1; + let part2_length = encoded.length - part1_length; + + let result = jxlModule._jxlProcessInput(decoder, buffer, part1_length); + assertTrue(result === 2, 'process first part'); + + result = + jxlModule._jxlProcessInput(decoder, buffer + part1_length, part2_length); + assertTrue(result === 0, 'process second part'); + + let w = jxlModule.HEAP32[decoder >> 2]; + let h = jxlModule.HEAP32[(decoder + 4) >> 2]; + let pixelData = jxlModule.HEAP32[(decoder + 8) >> 2]; + + assertTrue(pixelData, 'output allocated'); + assertTrue(h === 320, 'output height'); + assertTrue(w === 320, 'output width '); + + jxlModule._jxlDestroyInstance(decoder); + jxlModule._free(buffer); +} + +function testDecompress() { + let encoded = crossJxl; + let buffer = jxlModule._malloc(encoded.length); + jxlModule.HEAP8.set(encoded, buffer); + + let output = jxlModule._jxlDecompress(buffer, encoded.length); + assertTrue(isAddress(output), 'decompress'); + + jxlModule._free(buffer); + + let pngSize = jxlModule.HEAP32[output >> 2]; + let px = 20 * 20; + assertTrue(pngSize >= 6 * px, 'png size'); + assertTrue(pngSize <= 6 * px + 800, 'png size'); + + jxlModule._jxlCleanup(output); +} + +require('jxl_decoder_for_test.js')().then(module => { + jxlModule = module; + let tests = [testSdr, testRegular, testChunks, testDecompress]; + tests.forEach(runTest); + process.exit(0); +}); diff --git a/tools/wasm_demo/jxl_decompressor.cc b/tools/wasm_demo/jxl_decompressor.cc new file mode 100644 index 0000000..648e1ef --- /dev/null +++ b/tools/wasm_demo/jxl_decompressor.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/wasm_demo/jxl_decompressor.h" + +#include <jxl/thread_parallel_runner_cxx.h> + +#include <cstring> +#include <memory> + +#include "lib/extras/dec/jxl.h" +#include "tools/wasm_demo/no_png.h" + +extern "C" { + +namespace { + +struct DecompressorOutputPrivate { + // Due to "Standard Layout" rules it is guaranteed that address of the entity + // and its first non-static member are the same. + DecompressorOutput output; +}; + +void MaybeMakeCicp(const jxl::extras::PackedPixelFile& ppf, + std::vector<uint8_t>* cicp) { + cicp->clear(); + const JxlColorEncoding& clr = ppf.color_encoding; + uint8_t color_primaries = 0; + uint8_t transfer_function = static_cast<uint8_t>(clr.transfer_function); + + if (clr.color_space != JXL_COLOR_SPACE_RGB) { + return; + } + if (clr.primaries == JXL_PRIMARIES_P3) { + if (clr.white_point == JXL_WHITE_POINT_D65) { + color_primaries = 12; + } else if (clr.white_point == JXL_WHITE_POINT_DCI) { + color_primaries = 11; + } else { + return; + } + } else if (clr.primaries != JXL_PRIMARIES_CUSTOM && + clr.white_point == JXL_WHITE_POINT_D65) { + color_primaries = static_cast<uint8_t>(clr.primaries); + } else { + return; + } + if (clr.transfer_function == JXL_TRANSFER_FUNCTION_UNKNOWN || + clr.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) { + return; + } + + cicp->resize(4); + cicp->at(0) = color_primaries; // Colour Primaries + cicp->at(1) = transfer_function; // Transfer Function + cicp->at(2) = 0; // Matrix Coefficients + cicp->at(3) = 1; // Video Full Range Flag +} + +} // namespace + +DecompressorOutput* jxlDecompress(const uint8_t* input, size_t input_size) { + DecompressorOutputPrivate* self = new DecompressorOutputPrivate(); + + if (!self) { + return nullptr; + } + + auto report_error = [&](uint32_t code, const char* text) { + fprintf(stderr, "%s\n", text); + delete self; + return reinterpret_cast<DecompressorOutput*>(code); + }; + + auto thread_pool = JxlThreadParallelRunnerMake(nullptr, 4); + void* runner = thread_pool.get(); + + jxl::extras::JXLDecompressParams dparams; + JxlPixelFormat format = {/* num_channels */ 3, JXL_TYPE_UINT16, + JXL_BIG_ENDIAN, /* align */ 0}; + dparams.accepted_formats.push_back(format); + dparams.runner = JxlThreadParallelRunner; + dparams.runner_opaque = runner; + jxl::extras::PackedPixelFile ppf; + + if (!jxl::extras::DecodeImageJXL(input, input_size, dparams, nullptr, &ppf)) { + return report_error(1, "failed to decode jxl"); + } + + // Just 1-st frame. + const auto& image = ppf.frames[0].color; + std::vector<uint8_t> cicp; + MaybeMakeCicp(ppf, &cicp); + self->output.data = WrapPixelsToPng( + image.xsize, image.ysize, (format.data_type == JXL_TYPE_UINT16) ? 16 : 8, + /* has_alpha */ false, reinterpret_cast<const uint8_t*>(image.pixels()), + ppf.icc, cicp, &self->output.size); + if (!self->output.data) { + return report_error(2, "failed to encode png"); + } + + return &self->output; +} + +void jxlCleanup(DecompressorOutput* output) { + if (output == nullptr) return; + DecompressorOutputPrivate* self = + reinterpret_cast<DecompressorOutputPrivate*>(output); + if (self->output.data) { + free(self->output.data); + } + delete self; +} + +} // extern "C" diff --git a/tools/wasm_demo/jxl_decompressor.h b/tools/wasm_demo/jxl_decompressor.h new file mode 100644 index 0000000..2ba16a0 --- /dev/null +++ b/tools/wasm_demo/jxl_decompressor.h @@ -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. + +#ifndef TOOLS_WASM_DEMO_JXL_DECOMPRESSOR_H_ +#define TOOLS_WASM_DEMO_JXL_DECOMPRESSOR_H_ + +#include <stddef.h> +#include <stdint.h> + +extern "C" { + +typedef struct DecompressorOutput { + uint32_t size = 0; + uint8_t* data = nullptr; + + // The rest is opaque. +} DecompressorOutput; + +/* + Returns (as uint32_t): + 0 - OOM + 1 - decoding JXL failed + 2 - encoding PNG failed + >=4 - OK + */ +DecompressorOutput* jxlDecompress(const uint8_t* input, size_t input_size); + +void jxlCleanup(DecompressorOutput* output); + +} // extern "C" + +#endif // TOOLS_WASM_DEMO_JXL_DECOMPRESSOR_H_ diff --git a/tools/wasm_demo/manual_decode_demo.html b/tools/wasm_demo/manual_decode_demo.html new file mode 100644 index 0000000..e11aed6 --- /dev/null +++ b/tools/wasm_demo/manual_decode_demo.html @@ -0,0 +1,340 @@ +<html> +<head> + <link rel="icon" type="image/x-icon" href="favicon.ico"> + <style> +#log p { + margin: 0; +} + </style> +</head> +<body> +<div id="log" style="padding:2px; border: solid 1px #000; background-color: #ccc; margin:2px; height: 8em; font-family: monospace; overflow-y: auto; font-size: 8px;"></div> +<script> +// WASM module. +let jxlModule = null; +// Flag; if true, then HDR color space / 16 bit output is supported. +let hdrCanvas = false; + +// Add message to "console". +let addMessage = (text, color) => { + let log = document.getElementById('log'); + let message = document.createElement('p'); + message.style = 'color: ' + color + ';'; + message.textContent = text; + log.append(message); + log.scrollTop = log.scrollHeight; +} + +// Callback from WASM module when it becomes available. +let onLoadJxlModule = (module) => { + jxlModule = module; + addMessage('WASM module loaded', 'black'); + onJxlModuleReady(); +}; + +// Check if multi-threading is supported (i.e. SharedArrayBuffer is allowed). +let probeMutlithreading = () => { + try { + new SharedArrayBuffer(); + return true; + } catch (ex) { + addMessage('Installing Service Worker, please wait...', 'orange'); + return false; + } +}; + +// Check if HDR features are enabled. +let probeHdr = () => { + addMessage('Probing HDR features', 'black'); + try { + let tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = 1; + tmpCanvas.height = 1; + let ctx = tmpCanvas.getContext('2d', {colorSpace: 'rec2100-pq', pixelFormat: 'float16'}); + // make it fail on firefox... + ctx.getContextAttributes(); + addMessage('HDR canvas supported', 'green'); + return true; + } catch (ex) { + addMessage(ex, 'red'); + addMessage('Are Blink experiments enabled? about://flags/#enable-experimental-web-platform-features', 'blue'); + return false; + } +}; + +// "main" method executed after page is loaded; all scripts are "synchronous" elements, +// so it is guaranted that script elements are loaded and executed. +let onDomContentLoaded = () => { + if (!probeMutlithreading()) return; + hdrCanvas = probeHdr(); + JxlDecoderModule().then(onLoadJxlModule); +}; + +// Pass next chunk to decoder and interprets result. +let processInput = (img, chunkLen) => { + let response = { + wantFlush: false, + copyPixels: false, + error: false, + } + do { + let t0 = performance.now(); + let result = jxlModule._jxlProcessInput(img.decoder, img.buffer, chunkLen); + let t1 = performance.now(); + let tProcessing = t1 - t0; + // addMessage('Processed chunk in ' + tProcessing + 'ms', 'blue'); + img.totalProcessing += tProcessing; + // addMessage('Process result: ' + result, 'green'); + if (result === 2) { + addMessage('Needs more input', 'gray'); + } else if (result === 0) { + // addMessage('Image ready', 'gray'); + response.wantFlush = false; + response.copyPixels = true; + } else if (result === 1) { + if (img.wantProgressive) { + addMessage('DC ready', 'gray'); + response.wantFlush = true; + response.copyPixels = true; + } else { + // addMessage('Skipping DC flush', 'gray'); + chunkLen = 0; + continue; + } + } else { + addMessage('Processing error', 'red'); + img.broken = true; + response.error = true; + break; + } + break; + } while (true); + return response; +} + +// Decode chunk and present results (dump to canvas). +let processChunk = (img, chunkLen) => { + let result = processInput(img, chunkLen); + if (result.error) return; + + if (result.wantFlush) { + let t2 = performance.now(); + let flushResult = jxlModule._jxlFlush(img.decoder); + let t3 = performance.now(); + let tFlushing = t3 - t2; + addMessage('Flush result: ' + flushResult, 'gray'); + img.totalFlushing += tFlushing; + } + + if (!result.copyPixels) return; + + let w = jxlModule.HEAP32[img.decoder >> 2]; + let h = jxlModule.HEAP32[(img.decoder + 4) >> 2]; + let pixelData = jxlModule.HEAP32[(img.decoder + 8) >> 2]; + if (!img.canvas) { + img.canvas = document.createElement('canvas'); + img.canvas.width = w; + img.canvas.height = h; + img.canvas.style = 'width:100%'; + // TODO(eustas): postpone until really flushed + document.body.appendChild(img.canvas); + let ctxOptions = {colorSpace: img.colorSpace, pixelFormat: 'float16'}; + let pixelOptions = {colorSpace: img.colorSpace, storageFormat: 'uint16'}; + if (img.wantSdr) { + ctxOptions = null; + pixelOptions = null; + } + img.canvasCtx = img.canvas.getContext('2d', ctxOptions); + img.pixels = img.canvasCtx.getImageData(0, 0, w, h, pixelOptions); + } + + let src = null; + let start = pixelData; + if (img.wantSdr) { + src = new Uint8Array(jxlModule.HEAP8.buffer); + } else { + src = new Uint16Array(jxlModule.HEAP8.buffer); + start = start >> 1; + } + let end = start + w * h * 4; + img.pixels.data.set(src.slice(start, end)); + img.canvasCtx.putImageData(img.pixels, 0, 0); +}; + +const BUF_LEN = 150 * 1024; + +// Image data cache for benchmarking. +let fullImage = new Uint8Array(0); + +// Callback for fetch data. +let onChunk = (img, chunk) => { + if (chunk.done) { + addMessage('Read finished | total processing: ' + img.totalProcessing.toFixed(1) + 'ms | total flushing ' + img.totalFlushing.toFixed(1) + 'ms', 'black'); + cleanup(img); + img.onComplete(img); + return; + } + if (img.broken) return; + + if (!img.decoder) { + let decoder = jxlModule._jxlCreateInstance(img.wantSdr, img.displayNits); + if (decoder < 4) { + img.broken = true; + cleanup(img); + addMessage('Failed to create decoder instance', 'red'); + return; + } + img.decoder = decoder; + img.buffer = jxlModule._malloc(BUF_LEN); + } + + // addMessage('Received chunk: ' + chunk.value.length, 'gray'); + let newFullImage = new Uint8Array(fullImage.length + chunk.value.length); + newFullImage.set(fullImage); + newFullImage.set(chunk.value, fullImage.length); + fullImage = newFullImage; + + let offset = 0; + while (offset < chunk.value.length) { + let delta = chunk.value.length - offset; + if (delta > BUF_LEN) delta = BUF_LEN; + jxlModule.HEAP8.set(chunk.value.slice(offset, offset + delta), img.buffer); + offset += delta; + processChunk(img, delta); + if (img.broken) { + return; + } + } + + // Break the promise chain. + setTimeout(img.proceed, 0); +}; + +// Read next chunk; NB: used to break promise chain. +let proceed = (img) => { + img.reader.read().then(img.onChunk, img.onReadError); +}; + +// Release (in-module) memory resources. +let cleanup = (img) => { + if (img.decoder) { + jxlModule._jxlDestroyInstance(img.decoder); + img.decoder = 0; + } + if (img.buffer) { + jxlModule._free(img.buffer); + img.buffer = 0; + } +}; + +// Report error and cleanup. +let onReadError = (img, error) => { + img.broken = true; + cleanup(img); + addMessage('Read failed: ' + error, 'red'); +}; + +// On successful fetch start. +let onResponse = (img, response) => { + if (!response.ok) { + addMessage('Fetch failed: ' + response.status + ' (' + response.statusText + ')'); + return; + } + // Alas, not supported by fetch: + // let reader = response.body.getReader({mode: "byob"}); + img.reader = response.body.getReader(); + + img.proceed(); +}; + +// On image decoding completion. +let onComplete = (img) => { + if (!img.runBenchmark) return; + + let buffer = jxlModule._malloc(fullImage.length); + jxlModule.HEAP8.set(fullImage, buffer); + img.buffer = buffer; + let results = []; + + for (let i = 0; i < img.runBenchmark; ++i) { + img.totalProcessing = 0; + img.decoder = jxlModule._jxlCreateInstance(img.wantSdr, img.displayNits); + processChunk(img, fullImage.length); + jxlModule._jxlDestroyInstance(img.decoder); + results.push(img.totalProcessing); + //addMessage('Decoding time: ' + img.totalProcessing + 'ms', 'black'); + } + + results.sort(); + addMessage('Min decoding time: ' + results[0].toFixed(3) + 'ms', 'black'); + addMessage('Median decoding time: ' + results[results.length >> 1].toFixed(3) + 'ms', 'black'); + addMessage('Max decoding time: ' + results[results.length - 1].toFixed(3) + 'ms', 'black'); + + jxlModule._free(buffer); +}; + +// Fill cookie object template. +let makeImg = () => { + return { + name: '', + colorSpace: 'rec2100-pq', + wantSdr: false, + displayNits: 100, + broken: false, + decoder: 0, + canvas: null, + canvasCtx: null, + pixels: null, + buffer: 0, + wantProgressive: false, + onlyDecode: false, + totalProcessing: 0, + totalFlushing: 0, + runBenchmark: 0, + onChunk: () => {}, + onReadError: () => {}, + proceed: () => {}, + onComplete: () => {}, + }; +} + +// Parse URL query and run image decoding / benchmarking. +let onJxlModuleReady = () => { + let params = (new URL(document.location)).searchParams; + const images = ['image00.jxl', 'image01.jxl']; + let imgIdx = (params.get('img') | 0) % images.length; + let imgName = images[imgIdx]; + + let colorSpace = params.get('colorSpace') || 'srgb'; + let wantSdr = params.get('wantSdr') == 'true'; + let displayNits = parseInt(params.get('displayNits') || '0'); + let runBenchmark = parseInt(params.get('runBenchmark') || '0'); + + if (!hdrCanvas) { + colorSpace = 'srgb-linear'; + displayNits = displayNits || 100; + wantSdr = true; + } + + addMessage('Color-space: "' + colorSpace + '", tone-map to SDR: ' + wantSdr + ', displayNits: ' + (displayNits || 'n/a'), 'black'); + + let img = makeImg(); + img.name = imgName; + img.colorSpace = colorSpace; + img.wantSdr = wantSdr; + img.displayNits = displayNits; + img.onChunk = onChunk.bind(null, img); + img.onReadError = onReadError.bind(null, img); + img.proceed = proceed.bind(null, img); + img.onComplete = onComplete.bind(null, img); + img.runBenchmark = runBenchmark; + + fetch(new Request(imgName, {cache: "no-store"})).then(onResponse.bind(null, img)); +}; + +document.addEventListener('DOMContentLoaded', onDomContentLoaded); +</script> + +<script src="jxl_decoder.js"></script> +</body> +</html> diff --git a/tools/wasm_demo/netlify.toml b/tools/wasm_demo/netlify.toml new file mode 100644 index 0000000..44d9d56 --- /dev/null +++ b/tools/wasm_demo/netlify.toml @@ -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. + +# We use "edge functions" feature to substitute response with pre-compressed +# entries whenever those are available and browser supports Brotli or Gzip +# content-encoding. +[[edge_functions]] +path = "/*" +function = "precompressed" + +# Request browser "site-isolation" enabled. +# This allows using "SharedArrayBuffers" required for multi-threaded WASM. +[[headers]] +for = "/*" + [headers.values] + Cross-Origin-Opener-Policy = "same-origin" + Cross-Origin-Embedder-Policy = "require-corp" diff --git a/tools/wasm_demo/netlify/edge-functions/precompressed.ts b/tools/wasm_demo/netlify/edge-functions/precompressed.ts new file mode 100644 index 0000000..c169432 --- /dev/null +++ b/tools/wasm_demo/netlify/edge-functions/precompressed.ts @@ -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. + +import type {Context} from 'netlify:edge'; + +// This lambda is executed whenever request URL matches. +export default async (request: Request, context: Context) => { + // Measure time for debugging purpose. + let t0 = Date.now(); + // Get resource path (i.e. ignore query parameters). + let url = request.url.split('?')[0]; + // Pick request headers; fallback to empty string if header is not set. + let acceptEncodingHeader = request.headers.get('Accept-Encoding') || ''; + let acceptHeader = request.headers.get('Accept') || ''; + let etag = request.headers.get('If-None-Match') || ''; + // Roughly parse encodings list; this ignores "quality"; no modern browsers + // use it -> don't care. + let splitter = /[,;]/; + let supportedEncodings = + acceptEncodingHeader.split(splitter).map(v => v.trimStart()); + let supportsBr = supportedEncodings.includes('br'); + let supportedMedia = acceptHeader.split(splitter).map(v => v.trimStart()); + let supportsJxl = supportedMedia.includes('image/jxl'); + // Dump basic request info (we care about). + context.log( + 'URL: ' + url + '; acceptEncodingHeader: ' + acceptEncodingHeader + + '; supportsBr: ' + supportsBr + '; supportsJxl: ' + supportsJxl + + '; etag: ' + etag); + + // If browser does not support Brotli/Jxl - just process request normally. + + if (!supportsBr && !supportsJxl) { + return; + } + + // Jxl processing is higher priority, because images are (usually) transferred + // with 'identity' content encoding. + let isJxlWorkflow = supportsJxl; + let suffix = isJxlWorkflow ? '.jxl' : '.br'; + + // Request pre-compressed resource (with a suffix). + let response = await context.rewrite(url + suffix); + context.log('Response status: ' + response.status); + // First latency checkpoint (as we synchronously wait for resource fetch). + let t1 = Date.now(); + // If pre-compressed resource does not exist - pass. + if (response.status == 404) { + return; + } + // Get resource ETag. + let responseEtag = response.headers.get('ETag') || ''; + context.log('Response etag: ' + responseEtag); + // We rely on platform to check ETag; add debugging info just in case. + if (etag.length >= 4 && responseEtag === etag) { + console.log('Match; status: ' + response.status); + } + // Status 200 is regular "OK" - fetch resource; in such a case we need to + // craft response with the response contents. + // Status 3xx likely means "use cache"; pass response as is. + // Status 4xx is unlikely (404 has been already processed). + // Status 5xx is server error - nothing we could do around it. + if (response.status != 200) return response; + // Second time consuming operation - wait for resource contents. + let data = await response.arrayBuffer(); + let fixedHeaders = new Headers(response.headers); + + if (isJxlWorkflow) { + fixedHeaders.set('Content-Type', 'image/jxl'); + } else { // is Brotli workflow + // Set "Content-Type" based on resource suffix; + // otherwise browser will complain. + let contentEncoding = 'text/html; charset=UTF-8'; + if (url.endsWith('.js')) { + contentEncoding = 'application/javascript'; + } else if (url.endsWith('.wasm')) { + contentEncoding = 'application/wasm'; + } + fixedHeaders.set('Content-Type', contentEncoding); + // Inform browser that data stream is compressed. + fixedHeaders.set('Content-Encoding', 'br'); + } + let t2 = Date.now(); + console.log('Timing: ' + (t1 - t0) + ' ' + (t2 - t1)); + return new Response(data, {headers: fixedHeaders}); +}; diff --git a/tools/wasm_demo/no_png.cc b/tools/wasm_demo/no_png.cc new file mode 100644 index 0000000..01527d3 --- /dev/null +++ b/tools/wasm_demo/no_png.cc @@ -0,0 +1,220 @@ +// 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/wasm_demo/no_png.h" + +#include <array> +#include <memory> + +extern "C" { + +namespace { + +static std::array<uint32_t, 256> makeCrc32Lut() { + std::array<uint32_t, 256> result; + for (uint32_t i = 0; i < 256; ++i) { + constexpr uint32_t poly = 0xEDB88320; + uint32_t v = i; + for (size_t i = 0; i < 8; ++i) { + uint32_t mask = ~((v & 1) - 1); + v = (v >> 1) ^ (poly & mask); + } + result[i] = v; + } + return result; +} + +const std::array<uint32_t, 256> kCrc32Lut = makeCrc32Lut(); + +const std::array<uint32_t, 8> kPngMagic = {137, 80, 78, 71, 13, 10, 26, 10}; + +// No need to SIMDify it, only small blocks are actually checksummed. +uint32_t CalculateCrc32(const uint8_t* start, const uint8_t* end) { + uint32_t result = ~0; + for (const uint8_t* data = start; data < end; ++data) { + result ^= *data; + result = (result >> 8) ^ kCrc32Lut[result & 0xFF]; + } + return ~result; +} + +void AdlerCopy(const uint8_t* src, uint8_t* dst, size_t length, uint32_t* s1, + uint32_t* s2) { + // TODO(eustas): SIMD-ify and use multithreading. + + // Precondition: s1, s2 normalized; length <= 65535 + uint32_t a = *s1; + uint32_t b = *s2; + + for (size_t i = 0; i < length; ++i) { + const uint8_t v = src[i]; + a += v; + b += a; + dst[i] = v; + } + + // Postcondition: s1, s2 normalized. + *s1 = a % 65521; + *s2 = b % 65521; +} + +constexpr size_t kMaxDeflateBlock = 65535; +constexpr uint32_t kIhdrSize = 13; +constexpr uint32_t kCicpSize = 4; + +void WriteU8(uint8_t*& dst, uint8_t value) { *(dst++) = value; } + +void WriteU16(uint8_t*& dst, uint16_t value) { + memcpy(dst, &value, 2); + dst += 2; +} + +void WriteU32(uint8_t*& dst, uint32_t value) { + memcpy(dst, &value, 4); + dst += 4; +} + +void WriteU32BE(uint8_t*& dst, uint32_t value) { + WriteU32(dst, __builtin_bswap32(value)); +} + +} // namespace + +uint8_t* WrapPixelsToPng(size_t width, size_t height, size_t bit_depth, + bool has_alpha, const uint8_t* input, + const std::vector<uint8_t>& icc, + const std::vector<uint8_t>& cicp, + uint32_t* output_size) { + size_t row_size = width * (bit_depth / 8) * (3 + has_alpha); + size_t data_size = height * (row_size + 1); + size_t num_deflate_blocks = + (data_size + kMaxDeflateBlock - 1) / kMaxDeflateBlock; + size_t idat_size = data_size + num_deflate_blocks * 5 + 6; + // 64k is enough for everyone + bool has_iccp = !icc.empty() && (icc.size() <= kMaxDeflateBlock); + size_t iccp_size = 3 + icc.size() + 5 + 6; // name + data + deflate-wrapping + bool has_cicp = (cicp.size() == kCicpSize); + size_t total_size = 0; + total_size += kPngMagic.size(); + total_size += 12 + kIhdrSize; + total_size += has_cicp ? (kCicpSize + 12) : 0; + total_size += has_iccp ? (iccp_size + 12) : 0; + total_size += 12 + idat_size; + total_size += 12; // IEND + + uint8_t* output = static_cast<uint8_t*>(malloc(total_size)); + if (!output) { + return nullptr; + } + uint8_t* dst = output; + *output_size = total_size; + + for (size_t i = 0; i < kPngMagic.size(); ++i) { + *(dst++) = kPngMagic[i]; + } + + // IHDR + WriteU32BE(dst, kIhdrSize); + uint8_t* chunk_start = dst; + WriteU32(dst, 0x52444849); + WriteU32BE(dst, width); + WriteU32BE(dst, height); + WriteU8(dst, bit_depth); + WriteU8(dst, has_alpha ? 6 : 2); + WriteU8(dst, 0); // compression: deflate + WriteU8(dst, 0); // filters: standard + WriteU8(dst, 0); // interlace: no + uint32_t crc32 = CalculateCrc32(chunk_start, dst); + WriteU32BE(dst, crc32); + + if (has_cicp) { + // cICP + WriteU32BE(dst, kCicpSize); + uint8_t* chunk_start = dst; + WriteU32(dst, 0x50434963); + for (size_t i = 0; i < kCicpSize; ++i) { + WriteU8(dst, cicp[i]); + } + uint32_t crc32 = CalculateCrc32(chunk_start, dst); + WriteU32BE(dst, crc32); + } + + if (has_iccp) { + // iCCP + WriteU32BE(dst, iccp_size); + uint8_t* chunk_start = dst; + WriteU32(dst, 0x50434369); + WriteU8(dst, '1'); // Profile name + WriteU8(dst, 0); // NUL terminator + WriteU8(dst, 0); // Compression method: deflate + WriteU8(dst, 0x08); // CM = 8 (deflate), CINFO = 0 (window size = 2**(0+8)) + WriteU8(dst, 29); // FCHECK; (FCHECK + 256* CMF) % 31 = 0 + uint32_t adler_s1 = 1; + uint32_t adler_s2 = 0; + WriteU8(dst, 1); // btype = 00 (uncompressed), last + uint16_t block_size = static_cast<uint16_t>(icc.size()); + WriteU16(dst, block_size); + WriteU16(dst, ~block_size); + AdlerCopy(icc.data(), dst, block_size, &adler_s1, &adler_s2); + dst += block_size; + uint32_t adler = (adler_s2 << 8) | adler_s1; + WriteU32BE(dst, adler); + uint32_t crc32 = CalculateCrc32(chunk_start, dst); + WriteU32BE(dst, crc32); + } + + // IDAT + WriteU32BE(dst, idat_size); + WriteU32(dst, 0x54414449); + size_t offset = 0; + size_t bytes_to_next_row = 0; + uint32_t adler_s1 = 1; + uint32_t adler_s2 = 0; + WriteU8(dst, 0x08); // CM = 8 (deflate), CINFO = 0 (window size = 2**(0+8)) + WriteU8(dst, 29); // FCHECK; (FCHECK + 256* CMF) % 31 = 0 + for (size_t i = 0; i < num_deflate_blocks; ++i) { + size_t block_size = data_size - offset; + if (block_size > kMaxDeflateBlock) { + block_size = kMaxDeflateBlock; + } + bool is_last = ((i + 1) == num_deflate_blocks); + WriteU8(dst, is_last); // btype = 00 (uncompressed) + offset += block_size; + + WriteU16(dst, block_size); + WriteU16(dst, ~block_size); + while (block_size > 0) { + if (bytes_to_next_row == 0) { + WriteU8(dst, 0); // filter: raw + adler_s2 += adler_s1; + bytes_to_next_row = row_size; + block_size--; + continue; + } + size_t bytes_to_copy = std::min(block_size, bytes_to_next_row); + AdlerCopy(input, dst, bytes_to_copy, &adler_s1, &adler_s2); + dst += bytes_to_copy; + input += bytes_to_copy; + block_size -= bytes_to_copy; + bytes_to_next_row -= bytes_to_copy; + } + } + // Fake Adler works well in Chrome; so let's not waste CPU cycles. + uint32_t adler = 0; // (adler_s2 << 8) | adler_s1; + WriteU32BE(dst, adler); + WriteU32BE(dst, 0); // Fake CRC32 + + // IEND + WriteU32BE(dst, 0); + chunk_start = dst; + WriteU32(dst, 0x444E4549); + // TODO(eustas): this is fixed value; precalculate? + crc32 = CalculateCrc32(chunk_start, dst); + WriteU32BE(dst, crc32); + + return output; +} + +} // extern "C" diff --git a/tools/wasm_demo/no_png.h b/tools/wasm_demo/no_png.h new file mode 100644 index 0000000..1486c47 --- /dev/null +++ b/tools/wasm_demo/no_png.h @@ -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. + +#ifndef TOOLS_WASM_DEMO_NO_PNG_H_ +#define TOOLS_WASM_DEMO_NO_PNG_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <vector> + +extern "C" { + +uint8_t* WrapPixelsToPng(size_t width, size_t height, size_t bit_depth, + bool has_alpha, const uint8_t* input, + const std::vector<uint8_t>& icc, + const std::vector<uint8_t>& cicp, + uint32_t* output_size); + +} // extern "C" + +#endif // TOOLS_WASM_DEMO_NO_PNG_H_ diff --git a/tools/wasm_demo/one_line_demo.html b/tools/wasm_demo/one_line_demo.html new file mode 100644 index 0000000..a2966ac --- /dev/null +++ b/tools/wasm_demo/one_line_demo.html @@ -0,0 +1,20 @@ +<html> + +<head> + <link rel="icon" type="image/x-icon" href="favicon.ico" /> + <script src="service_worker.js"> +/* + * Just load this script, et voila! It will install ServiceWorker to + * advertise image/jxl media type and decode responses. + * NB: if "addMessage" function is defined it will be used to report + * decoding times / problems. + */ + </script> +</head> + +<body> + <img src="image00.jxl" style="width:100%" /> + <img src="image01.jxl" style="width:100%" /> +</body> + +</html> diff --git a/tools/wasm_demo/one_line_demo_with_console.html b/tools/wasm_demo/one_line_demo_with_console.html new file mode 100644 index 0000000..e2c52ae --- /dev/null +++ b/tools/wasm_demo/one_line_demo_with_console.html @@ -0,0 +1,34 @@ +<html> + +<head> + <link rel="icon" type="image/x-icon" href="favicon.ico"> + <script src="service_worker.js"></script> + <style> + #log p { + margin: 0; + } + </style> +</head> + +<body> + <div id="log" style="padding:2px; border: solid 1px #000; background-color: #ccc; margin:2px; height: 8em; font-family: monospace; overflow-y: auto; font-size: 8px;"></div> + <script> + let addMessage = (text, color) => { + let log = document.getElementById('log'); + let message = document.createElement('p'); + message.style = 'color: ' + color + ';'; + message.textContent = text; + log.append(message); + log.scrollTop = log.scrollHeight; + } + </script> + +<!-- Use those with capable server + <img src="image00.jpg" style="width:100%" /> + <img src="image01.png" style="width:100%" /> +--> + <img src="image00.jxl" style="width:100%" /> + <img src="image01.jxl" style="width:100%" /> +</body> + +</html> diff --git a/tools/wasm_demo/service_worker.js b/tools/wasm_demo/service_worker.js new file mode 100644 index 0000000..531e5c2 --- /dev/null +++ b/tools/wasm_demo/service_worker.js @@ -0,0 +1,317 @@ +// 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. + +/* + * ServiceWorker script. + * + * Multi-threading in WASM is currently implemented by the means of + * SharedArrayBuffer. Due to infamous vulnerabilities this feature is disabled + * unless site is running in "cross-origin isolated" mode. + * If there is not enough control over the server (e.g. when pages are hosted as + * "github pages") ServiceWorker is used to upgrade responses with corresponding + * headers. + * + * This script could be executed in 2 environments: HTML page or ServiceWorker. + * The environment is detected by the type of "window" reference. + * + * When this script is executed from HTML page then ServiceWorker is registered. + * Page reload might be necessary in some situations. By default it is done via + * `window.location.reload()`. However this can be altered by setting a + * configuration object `window.serviceWorkerConfig`. It's `doReload` property + * should be a replacement callable. + * + * When this script is executed from ServiceWorker then standard lifecycle + * event dispatchers are setup along with `fetch` interceptor. + */ + +(() => { + // Set COOP/COEP headers for document/script responses; use when this can not + // be done on server side (e.g. GitHub Pages). + const FORCE_COP = true; + // Interpret 'content-type: application/octet-stream' as JXL; use when server + // does not set appropriate content type (e.g. GitHub Pages). + const FORCE_DECODING = true; + // Embedded (baked-in) responses for faster turn-around. + const EMBEDDED = { + 'client_worker.js': '$client_worker.js$', + 'jxl_decoder.js': '$jxl_decoder.js$', + 'jxl_decoder.worker.js': '$jxl_decoder.worker.js$', + }; + + // Enable SharedArrayBuffer. + const setCopHeaders = (headers) => { + headers.set('Cross-Origin-Embedder-Policy', 'require-corp'); + headers.set('Cross-Origin-Opener-Policy', 'same-origin'); + }; + + // Inflight object: {clientId, uid, timestamp, controller} + const inflight = []; + + // Generate (very likely) unique string. + const makeUid = () => { + return Math.random().toString(36).substring(2) + + Math.random().toString(36).substring(2); + }; + + // Make list (non-recursively) of transferable entities. + const gatherTransferrables = (...args) => { + const result = []; + for (let i = 0; i < args.length; ++i) { + if (args[i] && args[i].buffer) { + result.push(args[i].buffer); + } + } + return result; + }; + + // Serve items that are embedded in this service worker. + const maybeProcessEmbeddedResources = (event) => { + const url = event.request.url; + // Shortcut for baked-in scripts. + for (const [key, value] of Object.entries(EMBEDDED)) { + if (url.endsWith(key)) { + const headers = new Headers(); + headers.set('Content-Type', 'application/javascript'); + setCopHeaders(headers); + + event.respondWith(new Response(value, { + status: 200, + statusText: 'OK', + headers: headers, + })); + return true; + } + } + return false; + }; + + // Decode JXL image response and serve it as a PNG image. + const wrapImageResponse = async (clientId, originalResponse) => { + // TODO(eustas): cache? + const client = await clients.get(clientId); + // Client is gone? Not our problem then. + if (!client) { + return originalResponse; + } + + const inputStream = await originalResponse.body; + // Can't use "BYOB" for regular responses. + const reader = inputStream.getReader(); + + const inflightEntry = { + clientId: clientId, + uid: makeUid(), + timestamp: Date.now(), + inputStreamReader: reader, + outputStreamController: null + }; + inflight.push(inflightEntry); + + const outputStream = new ReadableStream({ + start: (controller) => { + inflightEntry.outputStreamController = controller; + } + }); + + const onRead = (chunk) => { + const msg = { + op: 'decodeJxl', + uid: inflightEntry.uid, + url: originalResponse.url, + data: chunk.value || null + }; + client.postMessage(msg, gatherTransferrables(msg.data)); + if (!chunk.done) { + reader.read().then(onRead); + } + }; + // const view = new SharedArrayBuffer(65536); + const view = new Uint8Array(65536); + reader.read(view).then(onRead); + + let modifiedResponseHeaders = new Headers(originalResponse.headers); + modifiedResponseHeaders.delete('Content-Length'); + modifiedResponseHeaders.set('Content-Type', 'image/png'); + modifiedResponseHeaders.set('Server', 'ServiceWorker'); + return new Response(outputStream, {headers: modifiedResponseHeaders}); + }; + + // Check if response needs decoding; if so - do it. + const wrapImageRequest = async (clientId, request) => { + let modifiedRequestHeaders = new Headers(request.headers); + modifiedRequestHeaders.append('Accept', 'image/jxl'); + let modifiedRequest = + new Request(request, {headers: modifiedRequestHeaders}); + let originalResponse = await fetch(modifiedRequest); + let contentType = originalResponse.headers.get('Content-Type'); + + let isJxlResponse = (contentType === 'image/jxl'); + if (FORCE_DECODING && contentType === 'application/octet-stream') { + isJxlResponse = true; + } + if (isJxlResponse) { + return wrapImageResponse(clientId, originalResponse); + } + + return originalResponse; + }; + + const reportError = (err) => { + // console.error(err); + }; + + const upgradeResponse = (response) => { + if (response.status === 0) { + return response; + } + + const newHeaders = new Headers(response.headers); + setCopHeaders(newHeaders); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, + }); + }; + + // Process fetch request; either bypass, or serve embedded resource, + // or upgrade. + const onFetch = async (event) => { + const clientId = event.clientId; + const request = event.request; + + // Pass direct cached resource requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return; + } + + // Serve backed resources. + if (maybeProcessEmbeddedResources(event)) { + return; + } + + // Notify server we are JXL-capable. + if (request.destination === 'image') { + let accept = request.headers.get('Accept'); + // Only if browser does not support JXL. + if (accept.indexOf('image/jxl') === -1) { + event.respondWith(wrapImageRequest(clientId, request)); + } + return; + } + + if (FORCE_COP) { + event.respondWith( + fetch(event.request).then(upgradeResponse).catch(reportError)); + } + }; + + // Serve decoded bytes. + const onMessage = (event) => { + const data = event.data; + const uid = data.uid; + let inflightEntry = null; + for (let i = 0; i < inflight.length; ++i) { + if (inflight[i].uid === uid) { + inflightEntry = inflight[i]; + break; + } + } + if (!inflightEntry) { + console.log('Ooops, not found: ' + uid); + return; + } + inflightEntry.outputStreamController.enqueue(data.data); + inflightEntry.outputStreamController.close(); + }; + + // This method is "main" for service worker. + const serviceWorkerMain = () => { + // https://v8.dev/blog/wasm-code-caching + // > Every web site must perform at least one full compilation of a + // > WebAssembly module — use workers to hide that from your users. + // TODO(eustas): not 100% reliable, investigate why + self['JxlDecoderLeak'] = + WebAssembly.compileStreaming(fetch('jxl_decoder.wasm')); + + // ServiceWorker lifecycle. + self.addEventListener('install', () => { + return self.skipWaiting(); + }); + self.addEventListener( + 'activate', (event) => event.waitUntil(self.clients.claim())); + self.addEventListener('message', onMessage); + // Intercept some requests. + self.addEventListener('fetch', onFetch); + }; + + // Service workers does not support multi-threading; that is why decoding is + // relayed back to "client" (document / window). + const prepareClient = () => { + const clientWorker = new Worker('client_worker.js'); + clientWorker.onmessage = (event) => { + const data = event.data; + if (typeof addMessage !== 'undefined') { + if (data.msg) { + addMessage(data.msg, 'blue'); + } + } + navigator.serviceWorker.controller.postMessage( + data, gatherTransferrables(data.data)); + }; + + // Forward ServiceWorker requests to "Client" worker. + navigator.serviceWorker.addEventListener('message', (event) => { + clientWorker.postMessage( + event.data, gatherTransferrables(event.data.data)); + }); + }; + + // Executed in HTML page environment. + const maybeRegisterServiceWorker = () => { + const config = { + log: console.log, + error: console.error, + requestReload: (msg) => window.location.reload(), + ...window.serviceWorkerConfig // add overrides + } + + if (!window.isSecureContext) { + config.log('Secure context is required for this ServiceWorker.'); + return; + } + + const nav = navigator; // Explicitly capture navigator object. + const onServiceWorkerRegistrationSuccess = (registration) => { + config.log('Service Worker registered', registration.scope); + if (!registration.active || !nav.serviceWorker.controller) { + config.requestReload( + 'Reload to allow Service Worker process all requests'); + } + }; + + const onServiceWorkerRegistrationFailure = (err) => { + config.error('Service Worker failed to register:', err); + }; + + navigator.serviceWorker.register(window.document.currentScript.src) + .then( + onServiceWorkerRegistrationSuccess, + onServiceWorkerRegistrationFailure); + }; + + const pageMain = () => { + maybeRegisterServiceWorker(); + prepareClient(); + }; + + // Detect environment and run corresponding "main" method. + if (typeof window === 'undefined') { + serviceWorkerMain(); + } else { + pageMain(); + } +})(); diff --git a/tools/xyb_range.cc b/tools/xyb_range.cc index 1ce4882..e2afd56 100644 --- a/tools/xyb_range.cc +++ b/tools/xyb_range.cc @@ -3,6 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <jxl/cms.h> #include <stdio.h> #include <utility> @@ -12,15 +13,20 @@ #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 jpegxl { +namespace tools { namespace { +using ::jxl::CodecInOut; +using ::jxl::ColorEncoding; +using ::jxl::Image3F; +using ::jxl::ImageBundle; +using ::jxl::ThreadPool; + void PrintXybRange() { Image3F linear(1u << 16, 257); for (int b = 0; b < 256; ++b) { @@ -43,7 +49,7 @@ void PrintXybRange() { const ImageBundle& ib = io.Main(); ThreadPool* null_pool = nullptr; Image3F opsin(ib.xsize(), ib.ysize()); - (void)ToXYB(ib, null_pool, &opsin, GetJxlCms()); + (void)jxl::ToXYB(ib, null_pool, &opsin, *JxlGetDefaultCms()); for (size_t c = 0; c < 3; ++c) { float minval = 1e10f; float maxval = -1e10f; @@ -75,6 +81,7 @@ void PrintXybRange() { } } // namespace -} // namespace jxl +} // namespace tools +} // namespace jpegxl -int main() { jxl::PrintXybRange(); } +int main() { jpegxl::tools::PrintXybRange(); } |