summaryrefslogtreecommitdiff
path: root/media/libjxl/src/tools
diff options
context:
space:
mode:
authorJob Bautista <jobbautista9@protonmail.com>2022-06-19 15:34:18 +0800
committerJob Bautista <jobbautista9@protonmail.com>2022-06-29 13:53:09 +0800
commit73e06b6df9e396ef080fefcaa3196265bff723c4 (patch)
tree47caa76143ab4bdf70c2a0eb5f4657d2fe0ee22b /media/libjxl/src/tools
parent8c1064bc1cb494e445a6447587afb8942a6b783b (diff)
downloaduxp-73e06b6df9e396ef080fefcaa3196265bff723c4.tar.gz
Issue #1769 - Part 1: Add vendored libjxl and highway sources.
Used old-configure to add the build option for enabling JPEG-XL support. Highway version: 0.17.0 libjxl version: tree of commit 318c592d98b97d103941b90d47107f06a10c71da
Diffstat (limited to 'media/libjxl/src/tools')
-rw-r--r--media/libjxl/src/tools/CMakeLists.txt483
-rw-r--r--media/libjxl/src/tools/README.cjpeg_hdr.md73
-rw-r--r--media/libjxl/src/tools/args.h163
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_args.cc281
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_args.h174
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec.cc192
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec.h102
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc358
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_avif.h20
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc163
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_custom.h46
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc125
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.h20
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc405
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_jxl.h23
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_png.cc75
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_png.h26
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc280
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_codec_webp.h23
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_file_io.cc232
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_file_io.h53
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_stats.cc369
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_stats.h81
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_utils.cc90
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_utils.h35
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_xl.cc1084
-rw-r--r--media/libjxl/src/tools/benchmark/hm/README.md12
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/hm/decode.sh98
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/hm/encode.sh97
-rw-r--r--media/libjxl/src/tools/benchmark/metrics/compute-hdrvdp.m17
-rw-r--r--media/libjxl/src/tools/benchmark/metrics/compute-pumetrics.m26
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/compute_octave_metric.sh41
l---------media/libjxl/src/tools/benchmark/metrics/dists-rgb.sh1
l---------media/libjxl/src/tools/benchmark/metrics/fsim-rgb.sh1
l---------media/libjxl/src/tools/benchmark/metrics/fsim-y.sh1
l---------media/libjxl/src/tools/benchmark/metrics/gmsd-rgb.sh1
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/hdr_plots.sh10
-rw-r--r--media/libjxl/src/tools/benchmark/metrics/hdrvdp-fixes.patch110
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/hdrvdp.sh9
-rw-r--r--media/libjxl/src/tools/benchmark/metrics/iqa.py90
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/iqa_wrapper.sh7
l---------media/libjxl/src/tools/benchmark/metrics/lpips-rgb.sh1
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/mrse.sh36
l---------media/libjxl/src/tools/benchmark/metrics/msssim-rgb.sh1
l---------media/libjxl/src/tools/benchmark/metrics/msssim-y.sh1
l---------media/libjxl/src/tools/benchmark/metrics/nlpd-y.sh1
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/plots.py259
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/prepare_metrics.sh64
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/pupsnr.sh9
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/pussim.sh9
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/run_all_hdr_metrics.sh30
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/run_all_sdr_metrics.sh41
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/sdr_plots.sh10
l---------media/libjxl/src/tools/benchmark/metrics/ssim-rgb.sh1
l---------media/libjxl/src/tools/benchmark/metrics/ssim-y.sh1
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/ssimulacra.sh7
l---------media/libjxl/src/tools/benchmark/metrics/vif-rgb.sh1
-rwxr-xr-xmedia/libjxl/src/tools/benchmark/metrics/vmaf.sh52
l---------media/libjxl/src/tools/benchmark/metrics/vsi-rgb.sh1
-rwxr-xr-xmedia/libjxl/src/tools/bisector281
-rw-r--r--media/libjxl/src/tools/box/CMakeLists.txt28
-rw-r--r--media/libjxl/src/tools/box/box.cc334
-rw-r--r--media/libjxl/src/tools/box/box.h120
-rw-r--r--media/libjxl/src/tools/box/box_list_main.cc90
-rw-r--r--media/libjxl/src/tools/box/box_test.cc76
-rwxr-xr-xmedia/libjxl/src/tools/build_cleaner.py321
-rwxr-xr-xmedia/libjxl/src/tools/build_stats.py412
-rw-r--r--media/libjxl/src/tools/butteraugli_main.cc142
-rwxr-xr-xmedia/libjxl/src/tools/check_author.py102
-rw-r--r--media/libjxl/src/tools/cjpeg_hdr.cc306
-rw-r--r--media/libjxl/src/tools/cjxl.cc769
-rw-r--r--media/libjxl/src/tools/cjxl.h109
-rwxr-xr-xmedia/libjxl/src/tools/cjxl_bisect_bpp40
-rwxr-xr-xmedia/libjxl/src/tools/cjxl_bisect_size36
-rw-r--r--media/libjxl/src/tools/cjxl_main.cc151
-rw-r--r--media/libjxl/src/tools/cjxl_ng_main.cc922
-rw-r--r--media/libjxl/src/tools/cmdline.cc95
-rw-r--r--media/libjxl/src/tools/cmdline.h322
-rw-r--r--media/libjxl/src/tools/codec_config.cc57
-rw-r--r--media/libjxl/src/tools/codec_config.h22
-rw-r--r--media/libjxl/src/tools/color_encoding_fuzzer.cc24
-rw-r--r--media/libjxl/src/tools/comparison_viewer/CMakeLists.txt74
-rw-r--r--media/libjxl/src/tools/comparison_viewer/codec_comparison_window.cc316
-rw-r--r--media/libjxl/src/tools/comparison_viewer/codec_comparison_window.h77
-rw-r--r--media/libjxl/src/tools/comparison_viewer/codec_comparison_window.ui170
-rw-r--r--media/libjxl/src/tools/comparison_viewer/compare_codecs.cc75
-rw-r--r--media/libjxl/src/tools/comparison_viewer/compare_images.cc128
-rw-r--r--media/libjxl/src/tools/comparison_viewer/image_loading.cc111
-rw-r--r--media/libjxl/src/tools/comparison_viewer/image_loading.h29
-rw-r--r--media/libjxl/src/tools/comparison_viewer/settings.cc51
-rw-r--r--media/libjxl/src/tools/comparison_viewer/settings.h40
-rw-r--r--media/libjxl/src/tools/comparison_viewer/settings.ui120
-rw-r--r--media/libjxl/src/tools/comparison_viewer/split_image_renderer.cc239
-rw-r--r--media/libjxl/src/tools/comparison_viewer/split_image_renderer.h90
-rw-r--r--media/libjxl/src/tools/comparison_viewer/split_image_view.cc71
-rw-r--r--media/libjxl/src/tools/comparison_viewer/split_image_view.h40
-rw-r--r--media/libjxl/src/tools/comparison_viewer/split_image_view.ui141
-rw-r--r--media/libjxl/src/tools/conformance/CMakeLists.txt21
-rwxr-xr-xmedia/libjxl/src/tools/conformance/conformance.py219
-rw-r--r--media/libjxl/src/tools/conformance/djxl_conformance.cc669
-rwxr-xr-xmedia/libjxl/src/tools/conformance/generator.py129
-rw-r--r--media/libjxl/src/tools/conformance/lcms2.py117
-rwxr-xr-xmedia/libjxl/src/tools/conformance/tooling_test.sh60
-rw-r--r--media/libjxl/src/tools/decode_and_encode.cc50
-rw-r--r--media/libjxl/src/tools/decode_basic_info_fuzzer.cc58
-rwxr-xr-xmedia/libjxl/src/tools/demo_progressive_saliency_encoding.py182
-rwxr-xr-xmedia/libjxl/src/tools/demo_vardct_select.sh93
-rw-r--r--media/libjxl/src/tools/djxl.cc329
-rw-r--r--media/libjxl/src/tools/djxl.h91
-rw-r--r--media/libjxl/src/tools/djxl_fuzzer.cc570
-rw-r--r--media/libjxl/src/tools/djxl_main.cc195
-rw-r--r--media/libjxl/src/tools/djxl_ng_main.cc476
-rw-r--r--media/libjxl/src/tools/example_tree.txt50
-rw-r--r--media/libjxl/src/tools/fields_fuzzer.cc85
-rw-r--r--media/libjxl/src/tools/flicker_test/CMakeLists.txt38
-rw-r--r--media/libjxl/src/tools/flicker_test/main.cc22
-rw-r--r--media/libjxl/src/tools/flicker_test/parameters.cc87
-rw-r--r--media/libjxl/src/tools/flicker_test/parameters.h32
-rw-r--r--media/libjxl/src/tools/flicker_test/setup.cc151
-rw-r--r--media/libjxl/src/tools/flicker_test/setup.h44
-rw-r--r--media/libjxl/src/tools/flicker_test/setup.ui422
-rw-r--r--media/libjxl/src/tools/flicker_test/split_view.cc167
-rw-r--r--media/libjxl/src/tools/flicker_test/split_view.h84
-rw-r--r--media/libjxl/src/tools/flicker_test/test_window.cc184
-rw-r--r--media/libjxl/src/tools/flicker_test/test_window.h50
-rw-r--r--media/libjxl/src/tools/flicker_test/test_window.ui115
-rw-r--r--media/libjxl/src/tools/fuzzer_corpus.cc473
-rw-r--r--media/libjxl/src/tools/fuzzer_stub.cc45
-rw-r--r--media/libjxl/src/tools/git_version.cmake34
-rw-r--r--media/libjxl/src/tools/hdr/README.md137
-rw-r--r--media/libjxl/src/tools/hdr/display_to_hlg.cc85
-rw-r--r--media/libjxl/src/tools/hdr/generate_lut_template.cc59
-rw-r--r--media/libjxl/src/tools/hdr/pq_to_hlg.cc80
-rw-r--r--media/libjxl/src/tools/hdr/render_hlg.cc94
-rw-r--r--media/libjxl/src/tools/hdr/texture_to_cube.cc71
-rw-r--r--media/libjxl/src/tools/hdr/tone_map.cc89
-rw-r--r--media/libjxl/src/tools/icc_codec_fuzzer.cc68
-rw-r--r--media/libjxl/src/tools/icc_detect/icc_detect.h19
-rw-r--r--media/libjxl/src/tools/icc_detect/icc_detect_empty.cc14
-rw-r--r--media/libjxl/src/tools/icc_detect/icc_detect_win32.cc64
-rw-r--r--media/libjxl/src/tools/icc_detect/icc_detect_x11.cc77
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java39
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java73
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java127
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java25
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java13
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Status.java17
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java18
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc276
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h43
-rw-r--r--media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc52
-rw-r--r--media/libjxl/src/tools/jxl_emcc.cc66
-rw-r--r--media/libjxl/src/tools/jxl_from_tree.cc506
-rw-r--r--media/libjxl/src/tools/jxlinfo.c461
-rw-r--r--media/libjxl/src/tools/libjxl_test.c17
-rwxr-xr-xmedia/libjxl/src/tools/optimizer/simplex_fork.py255
-rwxr-xr-xmedia/libjxl/src/tools/ossfuzz-build.sh71
-rw-r--r--media/libjxl/src/tools/progressive_saliency.conf32
-rwxr-xr-xmedia/libjxl/src/tools/progressive_sizes.sh27
-rw-r--r--media/libjxl/src/tools/rans_fuzzer.cc46
-rwxr-xr-xmedia/libjxl/src/tools/reference_zip.sh73
-rw-r--r--media/libjxl/src/tools/set_from_bytes_fuzzer.cc33
-rw-r--r--media/libjxl/src/tools/speed_stats.cc117
-rw-r--r--media/libjxl/src/tools/speed_stats.h63
-rw-r--r--media/libjxl/src/tools/ssimulacra.cc331
-rw-r--r--media/libjxl/src/tools/ssimulacra.h36
-rw-r--r--media/libjxl/src/tools/ssimulacra.txt382
-rw-r--r--media/libjxl/src/tools/ssimulacra_main.cc67
-rw-r--r--media/libjxl/src/tools/tool_version.cc18
-rw-r--r--media/libjxl/src/tools/tool_version.h22
-rw-r--r--media/libjxl/src/tools/transforms_fuzzer.cc146
-rwxr-xr-xmedia/libjxl/src/tools/upscaling_coefficients/generate_upscaling_coefficients.py242
-rw-r--r--media/libjxl/src/tools/upscaling_coefficients/upscaler_demo.py814
-rw-r--r--media/libjxl/src/tools/viewer/CMakeLists.txt39
-rw-r--r--media/libjxl/src/tools/viewer/load_jxl.cc174
-rw-r--r--media/libjxl/src/tools/viewer/load_jxl.h20
-rw-r--r--media/libjxl/src/tools/viewer/main.cc23
-rw-r--r--media/libjxl/src/tools/viewer/viewer_window.cc130
-rw-r--r--media/libjxl/src/tools/viewer/viewer_window.h41
-rw-r--r--media/libjxl/src/tools/viewer/viewer_window.ui125
-rw-r--r--media/libjxl/src/tools/xyb_range.cc80
181 files changed, 23892 insertions, 0 deletions
diff --git a/media/libjxl/src/tools/CMakeLists.txt b/media/libjxl/src/tools/CMakeLists.txt
new file mode 100644
index 0000000000..9ee2e53fcb
--- /dev/null
+++ b/media/libjxl/src/tools/CMakeLists.txt
@@ -0,0 +1,483 @@
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# ICC detection library used by the comparison and viewer tools.
+if(JPEGXL_ENABLE_VIEWERS)
+if(WIN32)
+ find_package(Qt5 QUIET COMPONENTS Widgets)
+ if (NOT Qt5_FOUND)
+ message(WARNING "Qt5 was not found.")
+ else()
+ add_library(icc_detect STATIC EXCLUDE_FROM_ALL
+ icc_detect/icc_detect_win32.cc
+ icc_detect/icc_detect.h
+ )
+ target_include_directories(icc_detect PRIVATE "${PROJECT_SOURCE_DIR}")
+ target_link_libraries(icc_detect PUBLIC Qt5::Widgets)
+ if(JPEGXL_DEP_LICENSE_DIR)
+ configure_file("${JPEGXL_DEP_LICENSE_DIR}/libqt5widgets5/copyright"
+ ${PROJECT_BINARY_DIR}/LICENSE.libqt5widgets5 COPYONLY)
+ endif() # JPEGXL_DEP_LICENSE_DIR
+ endif()
+elseif(APPLE)
+ find_package(Qt5 QUIET COMPONENTS Widgets)
+ if (Qt5_FOUND)
+ add_library(icc_detect STATIC EXCLUDE_FROM_ALL
+ icc_detect/icc_detect_empty.cc
+ icc_detect/icc_detect.h
+ )
+ target_include_directories(icc_detect PRIVATE "${PROJECT_SOURCE_DIR}")
+ target_link_libraries(icc_detect PUBLIC Qt5::Widgets)
+ else()
+ message(WARNING "APPLE: Qt5 was not found.")
+ endif()
+else()
+ find_package(Qt5 QUIET COMPONENTS Widgets X11Extras)
+ find_package(ECM QUIET NO_MODULE)
+ if (NOT Qt5_FOUND OR NOT ECM_FOUND)
+ if (NOT Qt5_FOUND)
+ message(WARNING "Qt5 was not found.")
+ else()
+ message(WARNING "extra-cmake-modules were not found.")
+ endif()
+ else()
+ set(CMAKE_MODULE_PATH ${ECM_FIND_MODULE_DIR})
+ find_package(XCB COMPONENTS XCB)
+ if (XCB_FOUND)
+ add_library(icc_detect STATIC EXCLUDE_FROM_ALL
+ icc_detect/icc_detect_x11.cc
+ icc_detect/icc_detect.h
+ )
+ target_link_libraries(icc_detect PUBLIC jxl-static Qt5::Widgets Qt5::X11Extras XCB::XCB)
+ endif ()
+ endif()
+endif()
+endif() # JPEGXL_ENABLE_VIEWERS
+
+# Tools are added conditionally below.
+set(TOOL_BINARIES)
+
+add_library(jxl_tool STATIC EXCLUDE_FROM_ALL
+ cmdline.cc
+ codec_config.cc
+ tool_version.cc
+)
+target_compile_options(jxl_tool PUBLIC "${JPEGXL_INTERNAL_FLAGS}")
+target_link_libraries(jxl_tool jxl-static)
+
+target_include_directories(jxl_tool
+ PUBLIC "${PROJECT_SOURCE_DIR}")
+
+# The JPEGXL_VERSION is set from the builders.
+if(NOT DEFINED JPEGXL_VERSION OR JPEGXL_VERSION STREQUAL "")
+ find_package(Git QUIET)
+ execute_process(
+ COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD
+ OUTPUT_VARIABLE GIT_REV
+ WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+ ERROR_QUIET)
+ string(STRIP "${GIT_REV}" GIT_REV)
+ if(GIT_REV STREQUAL "")
+ set(JPEGXL_VERSION "(unknown)")
+ endif()
+endif()
+
+if(NOT DEFINED JPEGXL_VERSION OR JPEGXL_VERSION STREQUAL "")
+ # We are building from a git environment and the user didn't set
+ # JPEGXL_VERSION. Make a target that computes the GIT_REV at build-time always
+ # but only updates the file if it changed. This allows rebuilds without
+ # modifying cmake files to update the JPEGXL_VERSION.
+ message(STATUS "Building with JPEGXL_VERSION=${GIT_REV} (auto-updated)")
+ add_custom_target(
+ tool_version_git
+ ${CMAKE_COMMAND}
+ -D JPEGXL_ROOT_DIR=${CMAKE_SOURCE_DIR}
+ -D DST=${CMAKE_CURRENT_BINARY_DIR}/tool_version_git.h
+ -P ${CMAKE_CURRENT_SOURCE_DIR}/git_version.cmake
+ BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/tool_version_git.h"
+ )
+ add_dependencies(jxl_tool tool_version_git)
+
+ set_source_files_properties(tool_version.cc PROPERTIES
+ COMPILE_DEFINITIONS JPEGXL_VERSION_FROM_GIT=1)
+ target_include_directories(jxl_tool PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
+ # Note: Ninja looks for dependencies on the jxl_tool target before running
+ # the tool_version_git targets, so when updating the tool_version_git.h the
+ # jxl_tool target is not rebuilt. This forces to generate it at configure time
+ # if needed.
+ execute_process(
+ COMMAND ${CMAKE_COMMAND}
+ -D JPEGXL_ROOT_DIR=${CMAKE_SOURCE_DIR}
+ -D DST=${CMAKE_CURRENT_BINARY_DIR}/tool_version_git.h
+ -P ${CMAKE_CURRENT_SOURCE_DIR}/git_version.cmake)
+else()
+ message(STATUS "Building with JPEGXL_VERSION=${JPEGXL_VERSION}")
+ set_source_files_properties(tool_version.cc PROPERTIES
+ COMPILE_DEFINITIONS JPEGXL_VERSION=\"${JPEGXL_VERSION}\")
+endif()
+
+if(JPEGXL_ENABLE_TOOLS)
+ list(APPEND TOOL_BINARIES
+ cjxl
+ cjxl_ng
+ djxl
+ djxl_ng
+ cjpeg_hdr
+ jxlinfo
+ )
+
+ # Main compressor.
+ add_executable(cjxl
+ cjxl.cc
+ speed_stats.cc
+ cjxl_main.cc
+ )
+ target_link_libraries(cjxl
+ box
+ jxl-static
+ jxl_extras-static
+ jxl_threads-static
+ )
+
+
+ add_executable(cjxl_ng
+ cjxl_ng_main.cc
+ )
+ target_link_libraries(cjxl_ng
+ jxl
+ jxl_extras_dec-static
+ jxl_threads
+ hwy
+ jxl_gflags
+ )
+ target_include_directories(cjxl_ng PRIVATE "${PROJECT_SOURCE_DIR}")
+ if(JPEGXL_EMSCRIPTEN)
+ set_target_properties(cjxl_ng PROPERTIES LINK_FLAGS "-s USE_LIBPNG=1")
+ endif()
+
+ add_executable(djxl_ng
+ djxl_ng_main.cc
+ )
+ target_link_libraries(djxl_ng
+ jxl
+ jxl_gflags
+ )
+
+ add_executable(cjpeg_hdr
+ cjpeg_hdr.cc
+ )
+ target_link_libraries(cjpeg_hdr
+ box
+ jxl-static
+ jxl_extras-static
+ jxl_threads-static
+ )
+
+ # Main decompressor.
+ add_library(djxltool STATIC
+ djxl.cc
+ speed_stats.cc
+ )
+ target_link_libraries(djxltool
+ box
+ jxl-static
+ jxl_extras-static
+ jxl_threads-static
+ )
+
+ add_executable(djxl
+ djxl_main.cc
+ )
+ target_link_libraries(djxl djxltool)
+
+ add_executable(jxlinfo
+ jxlinfo.c
+ )
+ target_link_libraries(jxlinfo jxl)
+
+ if(NOT ${SANITIZER} STREQUAL "none")
+ # Linking a C test binary with the C++ JPEG XL implementation when using
+ # address sanitizer is not well supported by clang 9, so force using clang++
+ # for linking this test if a sanitizer is used.
+ set_target_properties(jxlinfo PROPERTIES LINKER_LANGUAGE CXX)
+ endif() # SANITIZER != "none"
+
+endif() # JPEGXL_ENABLE_TOOLS
+
+# Other developer tools.
+if(${JPEGXL_ENABLE_DEVTOOLS})
+ list(APPEND TOOL_BINARIES
+ fuzzer_corpus
+ butteraugli_main
+ decode_and_encode
+ display_to_hlg
+ pq_to_hlg
+ render_hlg
+ tone_map
+ texture_to_cube
+ generate_lut_template
+ ssimulacra_main
+ xyb_range
+ jxl_from_tree
+ )
+
+ add_executable(fuzzer_corpus fuzzer_corpus.cc)
+
+ add_executable(ssimulacra_main ssimulacra_main.cc ssimulacra.cc)
+ add_executable(butteraugli_main butteraugli_main.cc)
+ add_executable(decode_and_encode decode_and_encode.cc)
+ add_executable(display_to_hlg hdr/display_to_hlg.cc)
+ add_executable(pq_to_hlg hdr/pq_to_hlg.cc)
+ add_executable(render_hlg hdr/render_hlg.cc)
+ add_executable(tone_map hdr/tone_map.cc)
+ add_executable(texture_to_cube hdr/texture_to_cube.cc)
+ add_executable(generate_lut_template hdr/generate_lut_template.cc)
+ add_executable(xyb_range xyb_range.cc)
+ add_executable(jxl_from_tree jxl_from_tree.cc)
+endif() # JPEGXL_ENABLE_DEVTOOLS
+
+# Benchmark tools.
+if(${JPEGXL_ENABLE_BENCHMARK})
+ list(APPEND TOOL_BINARIES
+ benchmark_xl
+ )
+
+ add_executable(benchmark_xl
+ benchmark/benchmark_xl.cc
+ benchmark/benchmark_args.cc
+ benchmark/benchmark_codec.cc
+ benchmark/benchmark_file_io.cc
+ benchmark/benchmark_stats.cc
+ benchmark/benchmark_utils.cc
+ benchmark/benchmark_utils.h
+ benchmark/benchmark_codec_custom.cc
+ benchmark/benchmark_codec_custom.h
+ benchmark/benchmark_codec_jxl.cc
+ benchmark/benchmark_codec_jxl.h
+ speed_stats.cc
+ speed_stats.h
+ ../third_party/dirent.cc
+ )
+ target_link_libraries(benchmark_xl Threads::Threads)
+ if(MINGW)
+ # MINGW doesn't support glob.h.
+ target_compile_definitions(benchmark_xl PRIVATE "-DHAS_GLOB=0")
+ endif() # MINGW
+
+ find_package(JPEG)
+ if(JPEG_FOUND)
+ target_sources(benchmark_xl PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_jpeg.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_jpeg.h"
+ )
+ endif ()
+
+ if(NOT JPEGXL_BUNDLE_LIBPNG)
+ find_package(PNG)
+ endif()
+ if(PNG_FOUND)
+ target_sources(benchmark_xl PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_png.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_png.h"
+ )
+ endif()
+
+ find_package(PkgConfig)
+ pkg_check_modules(WebP IMPORTED_TARGET libwebp)
+ if(WebP_FOUND)
+ target_sources(benchmark_xl PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_webp.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_webp.h"
+ )
+ target_compile_definitions(benchmark_xl PRIVATE -DBENCHMARK_WEBP)
+
+ # Use the static version of webp if available.
+ find_library(WebP_STATIC_LINK_LIBRARY NAMES libwebp.a
+ PATHS "${WebP_LIBDIR}")
+ if("${WebP_STATIC_LINK_LIBRARY}" STREQUAL "WebP_STATIC_LINK_LIBRARY-NOTFOUND")
+ message(WARNING "Using dynamic libwebp")
+ target_link_libraries(benchmark_xl PkgConfig::WebP)
+ else()
+ target_link_libraries(benchmark_xl "${WebP_STATIC_LINK_LIBRARY}")
+ target_include_directories(benchmark_xl
+ PRIVATE ${WebP_STATIC_INCLUDE_DIRS})
+ target_compile_options(benchmark_xl PRIVATE ${WebP_STATIC_CFLAGS_OTHER})
+ endif()
+ endif()
+
+ pkg_check_modules(AVIF IMPORTED_TARGET libavif)
+ if(AVIF_FOUND)
+ target_sources(benchmark_xl PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_avif.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_avif.h"
+ )
+ target_compile_definitions(benchmark_xl PRIVATE -DBENCHMARK_AVIF)
+ target_link_libraries(benchmark_xl PkgConfig::AVIF)
+ endif()
+endif() # JPEGXL_ENABLE_BENCHMARK
+
+# All tool binaries depend on "jxl" library and the tool helpers.
+foreach(BINARY IN LISTS TOOL_BINARIES)
+ target_include_directories("${BINARY}" PRIVATE "${PROJECT_SOURCE_DIR}")
+ target_link_libraries("${BINARY}" box jxl-static jxl_extras-static jxl_threads-static jxl_tool)
+ if(JPEGXL_EMSCRIPTEN)
+ set_target_properties(${BINARY} PROPERTIES LINK_FLAGS "-s USE_LIBPNG=1")
+ endif()
+
+endforeach()
+install(TARGETS ${TOOL_BINARIES} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
+message(STATUS "Building tools: ${TOOL_BINARIES}")
+
+set(FUZZER_BINARIES
+ color_encoding_fuzzer
+ decode_basic_info_fuzzer
+ djxl_fuzzer
+ icc_codec_fuzzer
+ fields_fuzzer
+ rans_fuzzer
+ set_from_bytes_fuzzer
+ transforms_fuzzer
+)
+
+# Fuzzers.
+foreach(FUZZER IN LISTS FUZZER_BINARIES)
+ if(${JPEGXL_ENABLE_FUZZERS})
+ set(BINARY "${FUZZER}")
+ add_executable("${BINARY}" "${BINARY}.cc")
+ target_link_libraries("${BINARY}" ${JPEGXL_FUZZER_LINK_FLAGS})
+ else()
+ # When not enabled we want a lightweight alternative for regular fuzzers
+ # that just run the target.
+ set(BINARY "${FUZZER}_runner")
+ add_executable("${BINARY}" EXCLUDE_FROM_ALL
+ "fuzzer_stub.cc" "${FUZZER}.cc")
+ endif() # JPEGXL_ENABLE_FUZZERS
+ target_include_directories("${BINARY}" PRIVATE "${CMAKE_SOURCE_DIR}")
+ if(${FUZZER} STREQUAL djxl_fuzzer)
+ target_link_libraries("${BINARY}"
+ jxl_dec-static
+ jxl_threads-static
+ )
+ else()
+ target_link_libraries("${BINARY}"
+ jxl-static
+ jxl_extras-static
+ jxl_threads-static
+ jxl_tool
+ )
+ endif()
+endforeach()
+
+# EMSCRIPTEN doesn't support dynamic libraries so testing for linkage there
+# doesn't make much sense.
+if(BUILD_TESTING AND TARGET jxl AND NOT JPEGXL_EMSCRIPTEN)
+# Library API test. This test is only to check that we can link against the
+# shared library from C99 file and don't need to use internal symbols.
+file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tests)
+add_executable(libjxl_test libjxl_test.c)
+set_property(TARGET libjxl_test PROPERTY C_STANDARD 99)
+if(NOT ${SANITIZER} STREQUAL "none")
+ # Linking a C test binary with the C++ JPEG XL implementation when using
+ # address sanitizer is not well supported by clang 9, so force using clang++
+ # for linking this test if a sanitizer is used.
+ set_target_properties(libjxl_test PROPERTIES LINKER_LANGUAGE CXX)
+endif() # SANITIZER != "none"
+set_target_properties(libjxl_test PROPERTIES PREFIX "tests/")
+target_link_libraries(libjxl_test jxl)
+if (NOT MSVC)
+target_compile_options(libjxl_test PRIVATE -Wall -Wextra -Werror)
+if(NOT WIN32)
+ target_compile_options(libjxl_test PRIVATE -pedantic)
+endif() # NOT WIN32
+endif() # NOT MSVC
+
+add_test(NAME LibraryCLinkageTest COMMAND libjxl_test)
+
+endif() # BUILD_TESTING AND TARGET jxl AND NOT JPEGXL_EMSCRIPTEN
+
+# Tools defined in subdirectories.
+if(${JPEGXL_ENABLE_VIEWERS})
+add_subdirectory(viewer)
+add_subdirectory(comparison_viewer)
+add_subdirectory(flicker_test)
+endif()
+
+add_subdirectory(box)
+add_subdirectory(conformance)
+
+
+if ("${JPEGXL_EMSCRIPTEN}")
+# WASM API facade.
+add_executable(jxl_emcc jxl_emcc.cc)
+target_link_libraries(jxl_emcc jxl-static jxl_extras-static)
+set_target_properties(jxl_emcc PROPERTIES LINK_FLAGS "\
+ -O3\
+ --closure 1 \
+ -s ALLOW_MEMORY_GROWTH=1 \
+ -s USE_LIBPNG=1 \
+ -s DISABLE_EXCEPTION_CATCHING=1 \
+ -s MODULARIZE=1 \
+ -s FILESYSTEM=0 \
+ -s EXPORT_NAME=\"JxlCodecModule\"\
+ -s \"EXPORTED_FUNCTIONS=[\
+ _jxlCompress,\
+ _jxlDecompress,\
+ _free,\
+ _malloc\
+ ]\"\
+")
+endif () # JPEGXL_EMSCRIPTEN
+
+if(JPEGXL_ENABLE_JNI)
+find_package(JNI QUIET)
+find_package(Java QUIET)
+
+if ("${JNI_FOUND}" AND "${Java_FOUND}")
+ include(UseJava)
+
+ # decoder_jni_onload.cc might be necessary for Android; not used yet.
+ add_library(jxl_jni SHARED jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc)
+ target_include_directories(jxl_jni PRIVATE "${JNI_INCLUDE_DIRS}" "${PROJECT_SOURCE_DIR}")
+ target_link_libraries(jxl_jni PUBLIC jxl_dec-static jxl_threads-static)
+ if(NOT DEFINED JPEGXL_INSTALL_JNIDIR)
+ set(JPEGXL_INSTALL_JNIDIR ${CMAKE_INSTALL_LIBDIR})
+ endif()
+ install(TARGETS jxl_jni DESTINATION ${JPEGXL_INSTALL_JNIDIR})
+
+ add_jar(jxl_jni_wrapper SOURCES
+ jni/org/jpeg/jpegxl/wrapper/Decoder.java
+ jni/org/jpeg/jpegxl/wrapper/DecoderJni.java
+ jni/org/jpeg/jpegxl/wrapper/ImageData.java
+ jni/org/jpeg/jpegxl/wrapper/PixelFormat.java
+ jni/org/jpeg/jpegxl/wrapper/Status.java
+ jni/org/jpeg/jpegxl/wrapper/StreamInfo.java
+ OUTPUT_NAME org.jpeg.jpegxl
+ )
+ get_target_property(JXL_JNI_WRAPPER_JAR jxl_jni_wrapper JAR_FILE)
+ if(NOT DEFINED JPEGXL_INSTALL_JARDIR)
+ set(JPEGXL_INSTALL_JARDIR ${CMAKE_INSTALL_LIBDIR})
+ endif()
+ install_jar(jxl_jni_wrapper DESTINATION ${JPEGXL_INSTALL_JARDIR})
+
+ add_jar(jxl_jni_wrapper_test
+ SOURCES jni/org/jpeg/jpegxl/wrapper/DecoderTest.java
+ INCLUDE_JARS jxl_jni_wrapper
+ )
+ get_target_property(JXL_JNI_WRAPPER_TEST_JAR jxl_jni_wrapper_test JAR_FILE)
+
+ if(NOT "${SANITIZER}" MATCHES ".san")
+ # NB: Vanilla OpenJDK 8 / 11 are known to work well (i.e. either
+ # "which java" or JAVA_HOME environment variable point to the path like
+ # "/usr/lib/jvm/java-xx-openjdk-yyy" on Debian Linux).
+ add_test(
+ NAME test_jxl_jni_wrapper
+ COMMAND ${Java_JAVA_EXECUTABLE}
+ -cp "${JXL_JNI_WRAPPER_JAR}:${JXL_JNI_WRAPPER_TEST_JAR}"
+ -Dorg.jpeg.jpegxl.wrapper.lib=$<TARGET_FILE:jxl_jni>
+ org.jpeg.jpegxl.wrapper.DecoderTest
+ )
+ endif() # JPEGXL_ENABLE_FUZZERS
+endif() # JNI_FOUND & Java_FOUND
+endif() # JPEGXL_ENABLE_JNI
diff --git a/media/libjxl/src/tools/README.cjpeg_hdr.md b/media/libjxl/src/tools/README.cjpeg_hdr.md
new file mode 100644
index 0000000000..bd7c793bdb
--- /dev/null
+++ b/media/libjxl/src/tools/README.cjpeg_hdr.md
@@ -0,0 +1,73 @@
+# High bit depth JPEG encoder
+`cjpeg_hdr` is an (experimental) JPEG encoder that can preserve a higher bit
+depth than a traditional JPEG encoder. In particular, it may be used to produce
+HDR JPEGs that do not show obvious signs of banding.
+
+Note that at this point in time `cjpeg_hdr` does not attempt to actually
+*compress* the image - it behaves in the same way as a "quality 100" JPEG
+encoder would normally do, i.e. no quantization, to achieve the maximum
+possible visual quality. Moreover, no Huffman optimization is performed.
+
+## Generating HBD JPEGs
+Note: this and the following sections assume that `libjxl` has been built in
+the `build/` directory, either by using CMake or by running `./ci.sh opt`.
+
+It should be sufficient to run `build/tools/cjpeg_hdr input_image output.jpg`.
+Various input formats are supported, including NetBPM and (8- or 16-bit) PNG.
+
+If the PNG image includes a colour profile, it will be copied in the resulting
+JPEG image. If this colour profile approximates the PQ or HLG transfer curves,
+some applications will consider the resulting image to be HDR.
+
+To attach a PQ profile to an image without a colour profile (or with a
+different colour profile), the following command can be used:
+
+```
+ build/tools/decode_and_encode input RGB_D65_202_Rel_PeQ output_with_pq.png 16
+```
+
+Similarly, to attach an HLG profile, the following command can be used
+
+```
+ build/tools/decode_and_encode input RGB_D65_202_Rel_HLG output_with_pq.png 16
+```
+
+## Decoding HBD JPEGs
+HBD JPEGs are fully retrocompatible with libjpeg, and any JPEG viewer ought to
+be able to visualize them. Nonetheless, to achieve the best visual quality, a
+high bit depth decoder should be used.
+
+Such a decoder does not exist today. As a workaround, it is possible to do a
+lossless conversion to JPEG XL and then view the resulting image:
+
+```
+ build/tools/cjxl --jpeg_transcode_disable_cfl hbd.jpeg hbd.jxl
+```
+
+The resulting JPEG XL file can be visualized, for example, in a browser,
+assuming that the corresponding flag is enabled in the settings.
+
+In particular, if the HBD JPEG has a PQ or HLG profile attached and the current
+display is an HDR display, Chrome ought to visualize the image as HDR content.
+
+It is also possible to convert the JPEG XL file back to a 16-bit PNG:
+
+```
+ build/tools/djxl hbd.jxl --bits_per_sample=16 output.png
+```
+
+Note however that as of today (2 Nov 2021) Chrome does not interpret such a PNG
+as an HDR image, even if a PQ or HLG profile is attached. Thus, to display the
+HDR image correctly it is recommended to either display the JPEG XL image
+directly or to convert the PNG to a format that Chrome interprets as HDR, such
+as AVIF. This can be done with the following command for a PQ image:
+
+```
+ avifenc -l -y 444 --depth 10 --cicp 9/16/9 image.png output.avif
+```
+
+and the following one for an HLG image:
+
+```
+ avifenc -l -y 444 --depth 10 --cicp 9/18/9 image.png output.avif
+```
diff --git a/media/libjxl/src/tools/args.h b/media/libjxl/src/tools/args.h
new file mode 100644
index 0000000000..cc26a35dc7
--- /dev/null
+++ b/media/libjxl/src/tools/args.h
@@ -0,0 +1,163 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_ARGS_H_
+#define TOOLS_ARGS_H_
+
+// Helpers for parsing command line arguments. No include guard needed.
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/codec_in_out.h" // DecoderHints
+#include "lib/jxl/gaborish.h"
+#include "lib/jxl/modular/options.h"
+
+namespace jpegxl {
+namespace tools {
+
+static inline bool ParseOverride(const char* arg, jxl::Override* out) {
+ const std::string s_arg(arg);
+ if (s_arg == "1") {
+ *out = jxl::Override::kOn;
+ return true;
+ }
+ if (s_arg == "0") {
+ *out = jxl::Override::kOff;
+ return true;
+ }
+ fprintf(stderr, "Invalid flag, %s must be 0 or 1\n", arg);
+ return JXL_FAILURE("Args");
+}
+
+static inline bool ParseUnsigned(const char* arg, size_t* out) {
+ char* end;
+ *out = static_cast<size_t>(strtoull(arg, &end, 0));
+ if (end[0] != '\0') {
+ fprintf(stderr, "Unable to interpret as unsigned integer: %s.\n", arg);
+ return JXL_FAILURE("Args");
+ }
+ return true;
+}
+
+static inline bool ParseUint32(const char* arg, uint32_t* out) {
+ size_t value = 0;
+ bool ret = ParseUnsigned(arg, &value);
+ if (ret) *out = value;
+ return ret;
+}
+
+static inline bool ParseSigned(const char* arg, int* out) {
+ char* end;
+ *out = static_cast<int>(strtol(arg, &end, 0));
+ if (end[0] != '\0') {
+ fprintf(stderr, "Unable to interpret as signed integer: %s.\n", arg);
+ return JXL_FAILURE("Args");
+ }
+ return true;
+}
+
+static inline bool ParseFloat(const char* arg, float* out) {
+ char* end;
+ *out = static_cast<float>(strtod(arg, &end));
+ if (end[0] != '\0') {
+ fprintf(stderr, "Unable to interpret as float: %s.\n", arg);
+ return JXL_FAILURE("Args");
+ }
+ return true;
+}
+
+static inline bool ParseFloatPair(const char* arg,
+ std::pair<float, float>* out) {
+ int parsed = sscanf(arg, "%f,%f", &out->first, &out->second);
+ if (parsed == 1) {
+ out->second = out->first;
+ } else if (parsed != 2) {
+ fprintf(stderr,
+ "Unable to interpret as float pair separated by a comma: %s.\n",
+ arg);
+ return JXL_FAILURE("Args");
+ }
+ return true;
+}
+
+static inline bool ParseDouble(const char* arg, double* out) {
+ char* end;
+ *out = static_cast<double>(strtod(arg, &end));
+ if (end[0] != '\0') {
+ fprintf(stderr, "Unable to interpret as double: %s.\n", arg);
+ return JXL_FAILURE("Args");
+ }
+ return true;
+}
+
+static inline bool ParseAndAppendKeyValue(const char* arg,
+ jxl::extras::ColorHints* out) {
+ const char* eq = strchr(arg, '=');
+ if (!eq) {
+ fprintf(stderr, "Expected argument as 'key=value' but received '%s'\n",
+ arg);
+ return false;
+ }
+ std::string key(arg, eq);
+ out->Add(key, std::string(eq + 1));
+ return true;
+}
+
+static inline bool ParsePredictor(const char* arg, jxl::Predictor* out) {
+ char* end;
+ uint64_t p = static_cast<uint64_t>(strtoull(arg, &end, 0));
+ if (end[0] != '\0') {
+ fprintf(stderr, "Invalid predictor: %s.\n", arg);
+ return JXL_FAILURE("Args");
+ }
+ if (p >= jxl::kNumModularEncoderPredictors) {
+ fprintf(stderr,
+ "Invalid predictor value %" PRIu64 ", must be less than %" PRIu64
+ ".\n",
+ p, static_cast<uint64_t>(jxl::kNumModularEncoderPredictors));
+ return JXL_FAILURE("Args");
+ }
+ *out = static_cast<jxl::Predictor>(p);
+ return true;
+}
+
+static inline bool ParseString(const char* arg, std::string* out) {
+ out->assign(arg);
+ return true;
+}
+
+static inline bool ParseCString(const char* arg, const char** out) {
+ *out = arg;
+ return true;
+}
+
+static inline bool SetBooleanTrue(bool* out) {
+ *out = true;
+ return true;
+}
+
+static inline bool SetBooleanFalse(bool* out) {
+ *out = false;
+ return true;
+}
+
+static inline bool IncrementUnsigned(size_t* out) {
+ (*out)++;
+ return true;
+}
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_ARGS_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_args.cc b/media/libjxl/src/tools/benchmark/benchmark_args.cc
new file mode 100644
index 0000000000..2bd3eb8932
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_args.cc
@@ -0,0 +1,281 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/benchmark/benchmark_args.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/color_description.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/color_management.h"
+#include "tools/benchmark/benchmark_codec_jpeg.h" // for AddCommand..
+#include "tools/benchmark/benchmark_codec_jxl.h"
+#if JPEGXL_ENABLE_APNG
+#include "tools/benchmark/benchmark_codec_png.h"
+#endif
+
+#ifdef BENCHMARK_WEBP
+#include "tools/benchmark/benchmark_codec_webp.h"
+#endif // BENCHMARK_WEBP
+
+#ifdef BENCHMARK_AVIF
+#include "tools/benchmark/benchmark_codec_avif.h"
+#endif // BENCHMARK_AVIF
+
+namespace jxl {
+
+std::vector<std::string> SplitString(const std::string& s, char c) {
+ std::vector<std::string> result;
+ size_t pos = 0;
+ for (size_t i = 0; i <= s.size(); i++) {
+ if (i == s.size() || s[i] == c) {
+ result.push_back(s.substr(pos, i - pos));
+ pos = i + 1;
+ }
+ }
+ return result;
+}
+
+int ParseIntParam(const std::string& param, int lower_bound, int upper_bound) {
+ int val = strtol(param.substr(1).c_str(), nullptr, 10);
+ JXL_CHECK(val >= lower_bound && val <= upper_bound);
+ return val;
+}
+
+BenchmarkArgs* Args() {
+ static BenchmarkArgs args;
+ return &args;
+}
+
+Status BenchmarkArgs::AddCommandLineOptions() {
+ AddString(&input, "input", "File or file pattern matching input files.");
+ AddString(&codec, "codec",
+ "Comma separated list of image codec descriptions to benchmark.",
+ "jxl");
+ AddFlag(&print_details, "print_details",
+ "Prints size and distortion for each image. Not safe for "
+ "concurrent benchmark runs.",
+ false);
+ AddFlag(&print_details_csv, "print_details_csv",
+ "When print_details is used, print as CSV.", false);
+ AddString(&extra_metrics, "extra_metrics",
+ "Extra metrics to be computed. Only displayed with --print_details "
+ "or --print_details_csv. Comma-separated list of NAME:COMMAND "
+ "pairs; COMMAND is invoked with the original image as the first "
+ "argument, the decompressed image as a second argument, and the "
+ "name of the file where to write the metric value (as a single "
+ "floating point number) as the third argument.",
+ "");
+ AddFlag(
+ &print_more_stats, "print_more_stats",
+ "Prints codec-specific stats. Not safe for concurrent benchmark runs.",
+ false);
+ AddFlag(&print_distance_percentiles, "print_distance_percentiles",
+ "Prints distance percentiles for the corpus. Not safe for "
+ "concurrent benchmark runs.",
+ false);
+ AddFlag(&silent_errors, "silent_errors",
+ "If true, doesn't print error messages on compression or"
+ " decompression errors. Errors counts are still visible in the"
+ " 'Errors' column of the result table. Please note that depending"
+ " depending on the JXL build settings, error messages and asserts"
+ " from within the codec may be printed irrespective of this flag"
+ " anyway, use release build to ensure no messages.",
+ false);
+ AddFlag(&save_compressed, "save_compressed",
+ "Saves the compressed files for each input image and each codec.",
+ false);
+ AddFlag(&save_decompressed, "save_decompressed",
+ "Saves the decompressed files as PNG for each input image "
+ "and each codec.",
+ false);
+ AddString(&output_extension, "output_extension",
+ "Extension (starting with dot) to use for saving output images.",
+ ".png");
+ AddString(&output_description, "output_description",
+ "Color encoding (see ParseDescription; e.g. RGB_D65_SRG_Rel_709) "
+ "for saving output images, "
+ " defaults to sRGB.");
+
+ AddFloat(&intensity_target, "intensity_target",
+ "Intended viewing intensity target in nits. Defaults to 255 for "
+ "SDR images, 4000 for HDR images (when the input image uses PQ or "
+ "HLG transfer function)",
+ 0);
+
+ AddString(&color_hints_string, "dec-hints",
+ "Color encoding hints for the input images to encoder. Comma "
+ "separated key=value pairs. The key color_space indicates "
+ "ColorEncoding (see ParseDescription; e.g. RGB_D65_SRG_Rel_709) "
+ "for input images without color encoding (such as PNM)");
+
+ AddUnsigned(
+ &override_bitdepth, "override_bitdepth",
+ "If nonzero, store the given bit depth in the JPEG XL file metadata"
+ " (1-32), instead of using the bit depth from the original input"
+ " image.",
+ 0);
+
+ AddDouble(&mul_output, "mul_output",
+ "If nonzero, multiplies linear sRGB by this and clamps to 255",
+ 0.0);
+ AddDouble(&heatmap_good, "heatmap_good",
+ "If greater than zero, use this as the good "
+ "threshold for creating heatmap images.",
+ 0.0);
+ AddDouble(&heatmap_bad, "heatmap_bad",
+ "If greater than zero, use this as the bad "
+ "threshold for creating heatmap images.",
+ 0.0);
+
+ AddFlag(&write_html_report, "write_html_report",
+ "Creates an html report with original and compressed images.", false);
+ AddFlag(&html_report_self_contained, "html_report_self_contained",
+ "Base64-encode the images in the HTML report rather than use "
+ "external file names. May cause very large HTML data size.",
+ false);
+
+ AddFlag(
+ &markdown, "markdown",
+ "Adds formatting around ASCII table to render correctly in Markdown based"
+ " interfaces",
+ true);
+
+ AddFlag(&more_columns, "more_columns", "Print extra columns in the table",
+ false);
+
+ AddString(&originals_url, "originals_url",
+ "Url prefix to serve original images from in the html report.");
+ AddString(&output_dir, "output_dir",
+ "If not empty, save compressed and decompressed "
+ "images here.");
+
+ AddSigned(&num_threads, "num_threads",
+ "The number of threads for concurrent benchmarking. Defaults to "
+ "1 thread per CPU core (if negative).",
+ -1);
+ AddSigned(&inner_threads, "inner_threads",
+ "The number of extra threads per task. "
+ "Defaults to occupy cores (if negative).",
+ -1);
+ AddUnsigned(&encode_reps, "encode_reps",
+ "How many times to encode (>1 for more precise measurements). "
+ "Defaults to 1.",
+ 1);
+ AddUnsigned(&decode_reps, "decode_reps",
+ "How many times to decode (>1 for more precise measurements). "
+ "Defaults to 1.",
+ 1);
+
+ AddString(&sample_tmp_dir, "sample_tmp_dir",
+ "Directory to put samples from input images.");
+
+ AddSigned(&num_samples, "num_samples", "How many sample areas to take.", 0);
+ AddSigned(&sample_dimensions, "sample_dimensions",
+ "How big areas to sample from the input.", 64);
+
+ AddDouble(&error_pnorm, "error_pnorm",
+ "smallest p norm for pooling butteraugli values", 3.0);
+
+ AddFloat(&ba_params.hf_asymmetry, "hf_asymmetry",
+ "Multiplier for weighting HF artefacts more than features "
+ "being smoothed out. 1.0 means no HF asymmetry. 0.3 is "
+ "a good value to start exploring for asymmetry.",
+ 0.8f);
+ AddFlag(&profiler, "profiler", "If true, print profiler results.", false);
+
+ AddFlag(&show_progress, "show_progress",
+ "Show activity dots per completed file during benchmark.", false);
+
+ AddFlag(&skip_butteraugli, "skip_butteraugli",
+ "If true, doesn't compute distance metrics, only compression and"
+ " decompression speed and size. Distance numbers shown in the"
+ " table are invalid.",
+ false);
+
+ AddFlag(
+ &decode_only, "decode_only",
+ "If true, only decodes, and the input files must be compressed with a "
+ "compatible format for the given codec(s). Only measures decompression "
+ "speed and sizes, and can only use a single set of compatible decoders. "
+ "Distance numbers and compression speeds shown in the table are invalid.",
+ false);
+
+ if (!AddCommandLineOptionsJxlCodec(this)) return false;
+#ifdef BENCHMARK_JPEG
+ if (!AddCommandLineOptionsJPEGCodec(this)) return false;
+#endif // BENCHMARK_JPEG
+#if JPEGXL_ENABLE_APNG
+ if (!AddCommandLineOptionsPNGCodec(this)) return false;
+#endif
+#ifdef BENCHMARK_WEBP
+ if (!AddCommandLineOptionsWebPCodec(this)) return false;
+#endif // BENCHMARK_WEBP
+#ifdef BENCHMARK_AVIF
+ if (!AddCommandLineOptionsAvifCodec(this)) return false;
+#endif // BENCHMARK_AVIF
+
+ return true;
+}
+
+Status BenchmarkArgs::ValidateArgs() {
+ size_t bits_per_sample = 0; // unused
+ if (input.empty()) {
+ fprintf(stderr, "Missing --input filename(s).\n");
+ return false;
+ }
+ if (extras::CodecFromExtension(output_extension, &bits_per_sample) ==
+ extras::Codec::kUnknown) {
+ JXL_WARNING("Unrecognized output_extension %s, try .png",
+ output_extension.c_str());
+ return false; // already warned
+ }
+
+ // If empty, don't do anything; callers must only use output_encoding if
+ // output_description is not empty.
+ if (!output_description.empty()) {
+ // Validate, but also create the profile (only needs to happen once).
+ JxlColorEncoding output_encoding_external;
+ if (!ParseDescription(output_description, &output_encoding_external)) {
+ JXL_WARNING("Unrecognized output_description %s, try RGB_D65_SRG_Rel_Lin",
+ output_description.c_str());
+ return false; // already warned
+ }
+ JXL_RETURN_IF_ERROR(jxl::ConvertExternalToInternalColorEncoding(
+ output_encoding_external, &output_encoding));
+ JXL_RETURN_IF_ERROR(output_encoding.CreateICC());
+ }
+
+ JXL_RETURN_IF_ERROR(ValidateArgsJxlCodec(this));
+
+ if (print_details_csv) print_details = true;
+
+ if (override_bitdepth > 32) {
+ return JXL_FAILURE("override_bitdepth must be <= 32");
+ }
+
+ if (!color_hints_string.empty()) {
+ std::vector<std::string> hints = SplitString(color_hints_string, ',');
+ for (const auto& hint : hints) {
+ std::vector<std::string> kv = SplitString(hint, '=');
+ if (kv.size() != 2) {
+ return JXL_FAILURE(
+ "dec-hints key value pairs must have the form 'key=value'");
+ }
+ color_hints.Add(kv[0], kv[1]);
+ }
+ }
+
+ return true;
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/benchmark/benchmark_args.h b/media/libjxl/src/tools/benchmark/benchmark_args.h
new file mode 100644
index 0000000000..bebc0ac49d
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_args.h
@@ -0,0 +1,174 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_ARGS_H_
+#define TOOLS_BENCHMARK_BENCHMARK_ARGS_H_
+
+// Command line parsing and arguments for benchmark_xl
+
+#include <stddef.h>
+
+#include <algorithm>
+#include <deque>
+#include <string>
+#include <vector>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/butteraugli/butteraugli.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+
+namespace jxl {
+
+std::vector<std::string> SplitString(const std::string& s, char c);
+
+int ParseIntParam(const std::string& param, int lower_bound, int upper_bound);
+
+struct BenchmarkArgs {
+ using OptionId = jpegxl::tools::CommandLineParser::OptionId;
+
+ void AddFlag(bool* field, const char* longName, const char* help,
+ bool defaultValue) {
+ const char* noName = RememberString_(std::string("no") + longName);
+ cmdline.AddOptionFlag('\0', longName, nullptr, field,
+ &jpegxl::tools::SetBooleanTrue);
+ cmdline.AddOptionFlag('\0', noName, help, field,
+ &jpegxl::tools::SetBooleanFalse);
+ *field = defaultValue;
+ }
+
+ OptionId AddOverride(Override* field, const char* longName,
+ const char* help) {
+ OptionId result = cmdline.AddOptionValue('\0', longName, "0|1", help, field,
+ &jpegxl::tools::ParseOverride);
+ *field = Override::kDefault;
+ return result;
+ }
+
+ OptionId AddString(std::string* field, const char* longName, const char* help,
+ const std::string& defaultValue = "") {
+ OptionId result = cmdline.AddOptionValue(
+ '\0', longName, "<string>", help, field, &jpegxl::tools::ParseString);
+ *field = defaultValue;
+ return result;
+ }
+
+ OptionId AddFloat(float* field, const char* longName, const char* help,
+ float defaultValue) {
+ OptionId result = cmdline.AddOptionValue('\0', longName, "<scalar>", help,
+ field, &jpegxl::tools::ParseFloat);
+ *field = defaultValue;
+ return result;
+ }
+
+ OptionId AddDouble(double* field, const char* longName, const char* help,
+ double defaultValue) {
+ OptionId result = cmdline.AddOptionValue(
+ '\0', longName, "<scalar>", help, field, &jpegxl::tools::ParseDouble);
+ *field = defaultValue;
+ return result;
+ }
+
+ OptionId AddSigned(int* field, const char* longName, const char* help,
+ int defaultValue) {
+ OptionId result = cmdline.AddOptionValue(
+ '\0', longName, "<integer>", help, field, &jpegxl::tools::ParseSigned);
+ *field = defaultValue;
+ return result;
+ }
+
+ OptionId AddUnsigned(size_t* field, const char* longName, const char* help,
+ size_t defaultValue) {
+ OptionId result =
+ cmdline.AddOptionValue('\0', longName, "<unsigned>", help, field,
+ &jpegxl::tools::ParseUnsigned);
+ *field = defaultValue;
+ return result;
+ }
+
+ Status AddCommandLineOptions();
+
+ Status ValidateArgs();
+
+ bool Parse(int argc, const char** argv) { return cmdline.Parse(argc, argv); }
+
+ void PrintHelp() const { cmdline.PrintHelp(); }
+
+ std::string input;
+ std::string codec;
+ bool print_details;
+ bool print_details_csv;
+ bool print_more_stats;
+ bool print_distance_percentiles;
+ bool silent_errors;
+ bool save_compressed;
+ bool save_decompressed;
+ std::string output_extension; // see CodecFromExtension
+ std::string output_description; // see ParseDescription
+ ColorEncoding output_encoding; // determined by output_description
+
+ bool decode_only;
+ bool skip_butteraugli;
+
+ float intensity_target;
+
+ std::string color_hints_string;
+ jxl::extras::ColorHints color_hints;
+
+ size_t override_bitdepth;
+
+ double mul_output;
+ double heatmap_good;
+ double heatmap_bad;
+
+ bool write_html_report;
+ bool html_report_self_contained;
+ bool markdown;
+ bool more_columns;
+
+ std::string originals_url;
+ std::string output_dir;
+
+ int num_threads;
+ int inner_threads;
+ size_t decode_reps;
+ size_t encode_reps;
+
+ std::string sample_tmp_dir;
+
+ int num_samples;
+ int sample_dimensions;
+ ButteraugliParams ba_params;
+
+ bool profiler;
+ double error_pnorm;
+ bool show_progress;
+
+ std::string extra_metrics;
+
+ jpegxl::tools::CommandLineParser cmdline;
+
+ private:
+ const char* RememberString_(const std::string& text) {
+ const char* data = text.c_str();
+ std::vector<char> copy(data, data + text.size() + 1);
+ string_pool_.push_back(copy);
+ return string_pool_.back().data();
+ }
+
+ // A memory pool with stable addresses for strings to provide stable
+ // const char pointers to cmdline.h for dynamic help/name strings.
+ std::deque<std::vector<char>> string_pool_;
+};
+
+// Returns singleton
+BenchmarkArgs* Args();
+
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_ARGS_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec.cc b/media/libjxl/src/tools/benchmark/benchmark_codec.cc
new file mode 100644
index 0000000000..2deb7409dd
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec.cc
@@ -0,0 +1,192 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/benchmark/benchmark_codec.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "lib/extras/time.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/profiler.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_ops.h"
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_codec_custom.h"
+#ifdef JPEGXL_ENABLE_JPEG
+#include "tools/benchmark/benchmark_codec_jpeg.h"
+#endif // JPEG_ENABLE_JPEG
+#include "tools/benchmark/benchmark_codec_jxl.h"
+#include "tools/benchmark/benchmark_codec_png.h"
+#include "tools/benchmark/benchmark_stats.h"
+
+#ifdef BENCHMARK_WEBP
+#include "tools/benchmark/benchmark_codec_webp.h"
+#endif // BENCHMARK_WEBP
+
+#ifdef BENCHMARK_AVIF
+#include "tools/benchmark/benchmark_codec_avif.h"
+#endif // BENCHMARK_AVIF
+
+namespace jxl {
+
+void ImageCodec::ParseParameters(const std::string& parameters) {
+ params_ = parameters;
+ std::vector<std::string> parts = SplitString(parameters, ':');
+ for (size_t i = 0; i < parts.size(); ++i) {
+ if (!ParseParam(parts[i])) {
+ JXL_ABORT("Invalid parameter %s", parts[i].c_str());
+ }
+ }
+}
+
+Status ImageCodec::ParseParam(const std::string& param) {
+ if (param[0] == 'q') { // libjpeg-style quality, [0,100]
+ const std::string quality_param = param.substr(1);
+ char* end;
+ const float q_target = strtof(quality_param.c_str(), &end);
+ if (end == quality_param.c_str() ||
+ end != quality_param.c_str() + quality_param.size()) {
+ return false;
+ }
+ q_target_ = q_target;
+ return true;
+ }
+ if (param[0] == 'd') { // butteraugli distance
+ const std::string distance_param = param.substr(1);
+ char* end;
+ const float butteraugli_target = strtof(distance_param.c_str(), &end);
+ if (end == distance_param.c_str() ||
+ end != distance_param.c_str() + distance_param.size()) {
+ return false;
+ }
+ butteraugli_target_ = butteraugli_target;
+
+ // full hf asymmetry at high distance
+ static const double kHighDistance = 2.5;
+
+ // no hf asymmetry at low distance
+ static const double kLowDistance = 0.6;
+
+ if (butteraugli_target_ >= kHighDistance) {
+ ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry;
+ } else if (butteraugli_target_ >= kLowDistance) {
+ float w =
+ (butteraugli_target_ - kLowDistance) / (kHighDistance - kLowDistance);
+ ba_params_.hf_asymmetry =
+ args_.ba_params.hf_asymmetry * w + 1.0f * (1.0f - w);
+ } else {
+ ba_params_.hf_asymmetry = 1.0f;
+ }
+ return true;
+ } else if (param[0] == 'r') {
+ butteraugli_target_ = -1.0;
+ ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry;
+ bitrate_target_ = strtof(param.substr(1).c_str(), nullptr);
+ return true;
+ }
+ return false;
+}
+
+// Low-overhead "codec" for measuring benchmark overhead.
+class NoneCodec : public ImageCodec {
+ public:
+ explicit NoneCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
+ Status ParseParam(const std::string& param) override { return true; }
+
+ Status Compress(const std::string& filename, const CodecInOut* io,
+ ThreadPoolInternal* pool, PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ PROFILER_ZONE("NoneCompress");
+ const double start = Now();
+ // Encode image size so we "decompress" something of the same size, as
+ // required by butteraugli.
+ const uint32_t xsize = io->xsize();
+ const uint32_t ysize = io->ysize();
+ compressed->resize(8);
+ memcpy(compressed->data(), &xsize, 4);
+ memcpy(compressed->data() + 4, &ysize, 4);
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ return true;
+ }
+
+ Status Decompress(const std::string& filename,
+ const Span<const uint8_t> compressed,
+ ThreadPoolInternal* pool, CodecInOut* io,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ PROFILER_ZONE("NoneDecompress");
+ const double start = Now();
+ JXL_ASSERT(compressed.size() == 8);
+ uint32_t xsize, ysize;
+ memcpy(&xsize, compressed.data(), 4);
+ memcpy(&ysize, compressed.data() + 4, 4);
+ Image3F image(xsize, ysize);
+ ZeroFillImage(&image);
+ io->metadata.m.SetFloat32Samples();
+ io->metadata.m.color_encoding = ColorEncoding::SRGB();
+ io->SetFromImage(std::move(image), io->metadata.m.color_encoding);
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ return true;
+ }
+
+ void GetMoreStats(BenchmarkStats* stats) override {}
+};
+
+ImageCodecPtr CreateImageCodec(const std::string& description) {
+ std::string name = description;
+ std::string parameters = "";
+ size_t colon = description.find(':');
+ if (colon < description.size()) {
+ name = description.substr(0, colon);
+ parameters = description.substr(colon + 1);
+ }
+ ImageCodecPtr result;
+ if (name == "jxl") {
+ result.reset(CreateNewJxlCodec(*Args()));
+#if !defined(__wasm__)
+ } else if (name == "custom") {
+ result.reset(CreateNewCustomCodec(*Args()));
+#endif
+#ifdef JPEGXL_ENABLE_JPEG
+ } else if (name == "jpeg") {
+ result.reset(CreateNewJPEGCodec(*Args()));
+#endif // BENCHMARK_JPEG
+#if JPEGXL_ENABLE_APNG
+ } else if (name == "png") {
+ result.reset(CreateNewPNGCodec(*Args()));
+#endif
+ } else if (name == "none") {
+ result.reset(new NoneCodec(*Args()));
+#ifdef BENCHMARK_WEBP
+ } else if (name == "webp") {
+ result.reset(CreateNewWebPCodec(*Args()));
+#endif // BENCHMARK_WEBP
+#ifdef BENCHMARK_AVIF
+ } else if (name == "avif") {
+ result.reset(CreateNewAvifCodec(*Args()));
+#endif // BENCHMARK_AVIF
+ } else {
+ JXL_ABORT("Unknown image codec: %s", name.c_str());
+ }
+ result->set_description(description);
+ if (!parameters.empty()) result->ParseParameters(parameters);
+ return result;
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec.h b/media/libjxl/src/tools/benchmark/benchmark_codec.h
new file mode 100644
index 0000000000..abc5c1a618
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec.h
@@ -0,0 +1,102 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_H_
+#define TOOLS_BENCHMARK_BENCHMARK_CODEC_H_
+
+#include <stdint.h>
+
+#include <deque>
+#include <string>
+#include <vector>
+
+#include "lib/jxl/aux_out.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/butteraugli/butteraugli.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/image.h"
+#include "tools/args.h"
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_stats.h"
+#include "tools/cmdline.h"
+#include "tools/speed_stats.h"
+
+namespace jxl {
+
+// Thread-compatible.
+class ImageCodec {
+ public:
+ explicit ImageCodec(const BenchmarkArgs& args)
+ : args_(args),
+ butteraugli_target_(1.0f),
+ q_target_(100.0f),
+ bitrate_target_(0.0f) {}
+
+ virtual ~ImageCodec() = default;
+
+ void set_description(const std::string& desc) { description_ = desc; }
+ const std::string& description() const { return description_; }
+
+ const ButteraugliParams& BaParams() const { return ba_params_; }
+
+ virtual void ParseParameters(const std::string& parameters);
+
+ virtual Status ParseParam(const std::string& param);
+
+ // Returns true iff the codec instance (including parameters) can tolerate
+ // ImageBundle c_current() != metadata()->color_encoding, and the possibility
+ // of negative (out of gamut) pixel values.
+ virtual bool IsColorAware() const { return false; }
+
+ // Returns true iff the codec instance (including parameters) will operate
+ // only with quantized DCT (JPEG) coefficients in input.
+ virtual bool IsJpegTranscoder() const { return false; }
+
+ virtual Status Compress(const std::string& filename, const CodecInOut* io,
+ ThreadPoolInternal* pool, PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) = 0;
+
+ virtual Status Decompress(const std::string& filename,
+ const Span<const uint8_t> compressed,
+ ThreadPoolInternal* pool, CodecInOut* io,
+ jpegxl::tools::SpeedStats* speed_stats) = 0;
+
+ virtual void GetMoreStats(BenchmarkStats* stats) {}
+
+ virtual Status CanRecompressJpeg() const { return false; }
+ virtual Status RecompressJpeg(const std::string& filename,
+ const std::string& data,
+ PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) {
+ return false;
+ }
+
+ virtual std::string GetErrorMessage() const { return error_message_; }
+
+ protected:
+ const BenchmarkArgs& args_;
+ std::string params_;
+ std::string description_;
+ float butteraugli_target_;
+ float q_target_;
+ float bitrate_target_;
+ ButteraugliParams ba_params_;
+ std::string error_message_;
+};
+
+using ImageCodecPtr = std::unique_ptr<ImageCodec>;
+
+// Creates an image codec by name, e.g. "jxl" to get a new instance of the
+// jxl codec. Optionally, behind a colon, parameters can be specified,
+// then ParseParameters of the codec gets called with the part behind the colon.
+ImageCodecPtr CreateImageCodec(const std::string& description);
+
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc
new file mode 100644
index 0000000000..68f47e69a4
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc
@@ -0,0 +1,358 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+#include "tools/benchmark/benchmark_codec_avif.h"
+
+#include <avif/avif.h>
+
+#include "lib/extras/time.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/dec_external_image.h"
+#include "lib/jxl/enc_external_image.h"
+#include "tools/cmdline.h"
+
+#define JXL_RETURN_IF_AVIF_ERROR(result) \
+ do { \
+ avifResult jxl_return_if_avif_error_result = (result); \
+ if (jxl_return_if_avif_error_result != AVIF_RESULT_OK) { \
+ return JXL_FAILURE("libavif error: %s", \
+ avifResultToString(jxl_return_if_avif_error_result)); \
+ } \
+ } while (false)
+
+namespace jxl {
+
+namespace {
+
+struct AvifArgs {
+ avifPixelFormat chroma_subsampling = AVIF_PIXEL_FORMAT_YUV444;
+};
+
+AvifArgs* const avifargs = new AvifArgs;
+
+bool ParseChromaSubsampling(const char* arg, avifPixelFormat* subsampling) {
+ if (strcmp(arg, "444") == 0) {
+ *subsampling = AVIF_PIXEL_FORMAT_YUV444;
+ return true;
+ }
+ if (strcmp(arg, "422") == 0) {
+ *subsampling = AVIF_PIXEL_FORMAT_YUV422;
+ return true;
+ }
+ if (strcmp(arg, "420") == 0) {
+ *subsampling = AVIF_PIXEL_FORMAT_YUV420;
+ return true;
+ }
+ if (strcmp(arg, "400") == 0) {
+ *subsampling = AVIF_PIXEL_FORMAT_YUV400;
+ return true;
+ }
+ return false;
+}
+
+void SetUpAvifColor(const ColorEncoding& color, avifImage* const image) {
+ bool need_icc = color.white_point != WhitePoint::kD65;
+
+ image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT709;
+ if (!color.HasPrimaries()) {
+ need_icc = true;
+ } else {
+ switch (color.primaries) {
+ case Primaries::kSRGB:
+ image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709;
+ break;
+ case Primaries::k2100:
+ image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT2020;
+ image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT2020_NCL;
+ break;
+ default:
+ need_icc = true;
+ image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNKNOWN;
+ break;
+ }
+ }
+
+ switch (color.tf.GetTransferFunction()) {
+ case TransferFunction::kSRGB:
+ image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
+ break;
+ case TransferFunction::kLinear:
+ image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_LINEAR;
+ break;
+ case TransferFunction::kPQ:
+ image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084;
+ break;
+ case TransferFunction::kHLG:
+ image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_HLG;
+ break;
+ default:
+ need_icc = true;
+ image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNKNOWN;
+ break;
+ }
+
+ if (need_icc) {
+ avifImageSetProfileICC(image, color.ICC().data(), color.ICC().size());
+ }
+}
+
+Status ReadAvifColor(const avifImage* const image, ColorEncoding* const color) {
+ if (image->icc.size != 0) {
+ PaddedBytes icc;
+ icc.assign(image->icc.data, image->icc.data + image->icc.size);
+ return color->SetICC(std::move(icc));
+ }
+
+ color->white_point = WhitePoint::kD65;
+ switch (image->colorPrimaries) {
+ case AVIF_COLOR_PRIMARIES_BT709:
+ color->primaries = Primaries::kSRGB;
+ break;
+ case AVIF_COLOR_PRIMARIES_BT2020:
+ color->primaries = Primaries::k2100;
+ break;
+ default:
+ return JXL_FAILURE("unsupported avif primaries");
+ }
+ switch (image->transferCharacteristics) {
+ case AVIF_TRANSFER_CHARACTERISTICS_BT470M:
+ JXL_RETURN_IF_ERROR(color->tf.SetGamma(2.2));
+ break;
+ case AVIF_TRANSFER_CHARACTERISTICS_BT470BG:
+ JXL_RETURN_IF_ERROR(color->tf.SetGamma(2.8));
+ break;
+ case AVIF_TRANSFER_CHARACTERISTICS_LINEAR:
+ color->tf.SetTransferFunction(TransferFunction::kLinear);
+ break;
+ case AVIF_TRANSFER_CHARACTERISTICS_SRGB:
+ color->tf.SetTransferFunction(TransferFunction::kSRGB);
+ break;
+ case AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084:
+ color->tf.SetTransferFunction(TransferFunction::kPQ);
+ break;
+ case AVIF_TRANSFER_CHARACTERISTICS_HLG:
+ color->tf.SetTransferFunction(TransferFunction::kHLG);
+ break;
+ default:
+ return JXL_FAILURE("unsupported avif TRC");
+ }
+ return color->CreateICC();
+}
+
+} // namespace
+
+Status AddCommandLineOptionsAvifCodec(BenchmarkArgs* args) {
+ args->cmdline.AddOptionValue(
+ '\0', "avif_chroma_subsampling", "444/422/420/400",
+ "default AVIF chroma subsampling (default: 444).",
+ &avifargs->chroma_subsampling, &ParseChromaSubsampling);
+ return true;
+}
+
+class AvifCodec : public ImageCodec {
+ public:
+ explicit AvifCodec(const BenchmarkArgs& args) : ImageCodec(args) {
+ chroma_subsampling_ = avifargs->chroma_subsampling;
+ }
+
+ Status ParseParam(const std::string& param) override {
+ if (param.compare(0, 3, "yuv") == 0) {
+ if (param.size() != 6) return false;
+ return ParseChromaSubsampling(param.c_str() + 3, &chroma_subsampling_);
+ }
+ if (param.compare(0, 10, "log2_cols=") == 0) {
+ log2_cols = strtol(param.c_str() + 10, nullptr, 10);
+ return true;
+ }
+ if (param.compare(0, 10, "log2_rows=") == 0) {
+ log2_rows = strtol(param.c_str() + 10, nullptr, 10);
+ return true;
+ }
+ if (param[0] == 's') {
+ speed_ = strtol(param.c_str() + 1, nullptr, 10);
+ return true;
+ }
+ if (param == "aomenc") {
+ encoder_ = AVIF_CODEC_CHOICE_AOM;
+ return true;
+ }
+ if (param == "aomdec") {
+ decoder_ = AVIF_CODEC_CHOICE_AOM;
+ return true;
+ }
+ if (param == "aom") {
+ encoder_ = AVIF_CODEC_CHOICE_AOM;
+ decoder_ = AVIF_CODEC_CHOICE_AOM;
+ return true;
+ }
+ if (param == "rav1e") {
+ encoder_ = AVIF_CODEC_CHOICE_RAV1E;
+ return true;
+ }
+ if (param == "dav1d") {
+ decoder_ = AVIF_CODEC_CHOICE_DAV1D;
+ return true;
+ }
+ if (param.compare(0, 2, "a=") == 0) {
+ std::string subparam = param.substr(2);
+ size_t pos = subparam.find('=');
+ if (pos == std::string::npos) {
+ codec_specific_options_.emplace_back(subparam, "");
+ } else {
+ std::string key = subparam.substr(0, pos);
+ std::string value = subparam.substr(pos + 1);
+ codec_specific_options_.emplace_back(key, value);
+ }
+ return true;
+ }
+ return ImageCodec::ParseParam(param);
+ }
+
+ Status Compress(const std::string& filename, const CodecInOut* io,
+ ThreadPoolInternal* pool, PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ double elapsed_convert_image = 0;
+ const double start = Now();
+ {
+ const auto depth =
+ std::min<int>(16, io->metadata.m.bit_depth.bits_per_sample);
+ std::unique_ptr<avifEncoder, void (*)(avifEncoder*)> encoder(
+ avifEncoderCreate(), &avifEncoderDestroy);
+ encoder->codecChoice = encoder_;
+ // TODO(sboukortt): configure this separately.
+ encoder->minQuantizer = 0;
+ encoder->maxQuantizer = 63;
+ encoder->tileColsLog2 = log2_cols;
+ encoder->tileRowsLog2 = log2_rows;
+ encoder->speed = speed_;
+ encoder->maxThreads = pool->NumThreads();
+ for (const auto& opts : codec_specific_options_) {
+ avifEncoderSetCodecSpecificOption(encoder.get(), opts.first.c_str(),
+ opts.second.c_str());
+ }
+ avifAddImageFlags add_image_flags = AVIF_ADD_IMAGE_FLAG_SINGLE;
+ if (io->metadata.m.have_animation) {
+ encoder->timescale = std::lround(
+ static_cast<float>(io->metadata.m.animation.tps_numerator) /
+ io->metadata.m.animation.tps_denominator);
+ add_image_flags = AVIF_ADD_IMAGE_FLAG_NONE;
+ }
+ for (const ImageBundle& ib : io->frames) {
+ std::unique_ptr<avifImage, void (*)(avifImage*)> image(
+ avifImageCreate(ib.xsize(), ib.ysize(), depth, chroma_subsampling_),
+ &avifImageDestroy);
+ image->width = ib.xsize();
+ image->height = ib.ysize();
+ image->depth = depth;
+ SetUpAvifColor(ib.c_current(), image.get());
+ std::unique_ptr<avifRWData, void (*)(avifRWData*)> icc_freer(
+ &image->icc, &avifRWDataFree);
+ avifRGBImage rgb_image;
+ avifRGBImageSetDefaults(&rgb_image, image.get());
+ rgb_image.format =
+ ib.HasAlpha() ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB;
+ avifRGBImageAllocatePixels(&rgb_image);
+ std::unique_ptr<avifRGBImage, void (*)(avifRGBImage*)> pixels_freer(
+ &rgb_image, &avifRGBImageFreePixels);
+ const double start_convert_image = Now();
+ JXL_RETURN_IF_ERROR(ConvertToExternal(
+ ib, depth, /*float_out=*/false,
+ /*num_channels=*/ib.HasAlpha() ? 4 : 3, JXL_NATIVE_ENDIAN,
+ /*stride=*/rgb_image.rowBytes, pool, rgb_image.pixels,
+ rgb_image.rowBytes * rgb_image.height,
+ /*out_callback=*/{}, jxl::Orientation::kIdentity));
+ const double end_convert_image = Now();
+ elapsed_convert_image += end_convert_image - start_convert_image;
+ JXL_RETURN_IF_AVIF_ERROR(avifImageRGBToYUV(image.get(), &rgb_image));
+ JXL_RETURN_IF_AVIF_ERROR(avifEncoderAddImage(
+ encoder.get(), image.get(), ib.duration, add_image_flags));
+ }
+ avifRWData buffer = AVIF_DATA_EMPTY;
+ JXL_RETURN_IF_AVIF_ERROR(avifEncoderFinish(encoder.get(), &buffer));
+ compressed->assign(buffer.data, buffer.data + buffer.size);
+ avifRWDataFree(&buffer);
+ }
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start - elapsed_convert_image);
+ return true;
+ }
+
+ Status Decompress(const std::string& filename,
+ const Span<const uint8_t> compressed,
+ ThreadPoolInternal* pool, CodecInOut* io,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ io->frames.clear();
+ io->dec_pixels = 0;
+ double elapsed_convert_image = 0;
+ const double start = Now();
+ {
+ std::unique_ptr<avifDecoder, void (*)(avifDecoder*)> decoder(
+ avifDecoderCreate(), &avifDecoderDestroy);
+ decoder->codecChoice = decoder_;
+ decoder->maxThreads = pool->NumThreads();
+ JXL_RETURN_IF_AVIF_ERROR(avifDecoderSetIOMemory(
+ decoder.get(), compressed.data(), compressed.size()));
+ JXL_RETURN_IF_AVIF_ERROR(avifDecoderParse(decoder.get()));
+ const bool has_alpha = decoder->alphaPresent;
+ io->metadata.m.have_animation = decoder->imageCount > 1;
+ io->metadata.m.animation.tps_numerator = decoder->timescale;
+ io->metadata.m.animation.tps_denominator = 1;
+ io->metadata.m.SetUintSamples(decoder->image->depth);
+ io->SetSize(decoder->image->width, decoder->image->height);
+ avifResult next_image;
+ while ((next_image = avifDecoderNextImage(decoder.get())) ==
+ AVIF_RESULT_OK) {
+ ColorEncoding color;
+ JXL_RETURN_IF_ERROR(ReadAvifColor(decoder->image, &color));
+ avifRGBImage rgb_image;
+ avifRGBImageSetDefaults(&rgb_image, decoder->image);
+ rgb_image.format =
+ has_alpha ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB;
+ avifRGBImageAllocatePixels(&rgb_image);
+ std::unique_ptr<avifRGBImage, void (*)(avifRGBImage*)> pixels_freer(
+ &rgb_image, &avifRGBImageFreePixels);
+ JXL_RETURN_IF_AVIF_ERROR(avifImageYUVToRGB(decoder->image, &rgb_image));
+ const double start_convert_image = Now();
+ {
+ ImageBundle ib(&io->metadata.m);
+ JXL_RETURN_IF_ERROR(ConvertFromExternal(
+ Span<const uint8_t>(rgb_image.pixels,
+ rgb_image.height * rgb_image.rowBytes),
+ rgb_image.width, rgb_image.height, color, (has_alpha ? 4 : 3),
+ /*alpha_is_premultiplied=*/false, rgb_image.depth,
+ JXL_NATIVE_ENDIAN, /*flipped_y=*/false, pool, &ib,
+ /*float_in=*/false, /*align=*/0));
+ io->frames.push_back(std::move(ib));
+ io->dec_pixels += rgb_image.width * rgb_image.height;
+ }
+ const double end_convert_image = Now();
+ elapsed_convert_image += end_convert_image - start_convert_image;
+ }
+ if (next_image != AVIF_RESULT_NO_IMAGES_REMAINING) {
+ JXL_RETURN_IF_AVIF_ERROR(next_image);
+ }
+ }
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start - elapsed_convert_image);
+ return true;
+ }
+
+ protected:
+ avifPixelFormat chroma_subsampling_;
+ avifCodecChoice encoder_ = AVIF_CODEC_CHOICE_AUTO;
+ avifCodecChoice decoder_ = AVIF_CODEC_CHOICE_AUTO;
+ int speed_ = AVIF_SPEED_DEFAULT;
+ int log2_cols = 0;
+ int log2_rows = 0;
+ std::vector<std::pair<std::string, std::string>> codec_specific_options_;
+};
+
+ImageCodec* CreateNewAvifCodec(const BenchmarkArgs& args) {
+ return new AvifCodec(args);
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_avif.h b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.h
new file mode 100644
index 0000000000..b3dc38e97e
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.h
@@ -0,0 +1,20 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_AVIF_H_
+#define TOOLS_BENCHMARK_BENCHMARK_CODEC_AVIF_H_
+
+#include "lib/jxl/base/status.h"
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_codec.h"
+
+namespace jxl {
+ImageCodec* CreateNewAvifCodec(const BenchmarkArgs& args);
+
+// Registers the avif-specific command line options.
+Status AddCommandLineOptionsAvifCodec(BenchmarkArgs* args);
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_AVIF_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc
new file mode 100644
index 0000000000..a93fd50193
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc
@@ -0,0 +1,163 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/benchmark/benchmark_codec_custom.h"
+
+// Not supported on Windows due to Linux-specific functions.
+#ifndef _WIN32
+
+#include <libgen.h>
+
+#include <fstream>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/enc/apng.h"
+#include "lib/extras/time.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/image_bundle.h"
+#include "tools/benchmark/benchmark_utils.h"
+
+namespace jxl {
+namespace {
+
+std::string GetBaseName(std::string filename) {
+ std::string result = std::move(filename);
+ result = basename(&result[0]);
+ const size_t dot = result.rfind('.');
+ if (dot != std::string::npos) {
+ result.resize(dot);
+ }
+ return result;
+}
+
+// This uses `output_filename` to determine the name of the corresponding
+// `.time` file.
+template <typename F>
+Status ReportCodecRunningTime(F&& function, std::string output_filename,
+ jpegxl::tools::SpeedStats* const speed_stats) {
+ const double start = Now();
+ JXL_RETURN_IF_ERROR(function());
+ const double end = Now();
+ const std::string time_filename =
+ GetBaseName(std::move(output_filename)) + ".time";
+ std::ifstream time_stream(time_filename);
+ double time;
+ if (time_stream >> time) {
+ // Report the time measured by the external codec itself.
+ speed_stats->NotifyElapsed(time);
+ } else {
+ // Fall back to the less accurate time that we measured.
+ speed_stats->NotifyElapsed(end - start);
+ }
+ if (time_stream.is_open()) {
+ remove(time_filename.c_str());
+ }
+ return true;
+}
+
+class CustomCodec : public ImageCodec {
+ public:
+ explicit CustomCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
+
+ Status ParseParam(const std::string& param) override {
+ switch (param_index_) {
+ case 0:
+ extension_ = param;
+ break;
+
+ case 1:
+ compress_command_ = param;
+ break;
+
+ case 2:
+ decompress_command_ = param;
+ break;
+
+ default:
+ compress_args_.push_back(param);
+ break;
+ }
+ ++param_index_;
+ return true;
+ }
+
+ Status Compress(const std::string& filename, const CodecInOut* io,
+ ThreadPoolInternal* pool, PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ JXL_RETURN_IF_ERROR(param_index_ > 2);
+
+ const std::string basename = GetBaseName(filename);
+ TemporaryFile png_file(basename, "png"), encoded_file(basename, extension_);
+ std::string png_filename, encoded_filename;
+ JXL_RETURN_IF_ERROR(png_file.GetFileName(&png_filename));
+ JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename));
+ saved_intensity_target_ = io->metadata.m.IntensityTarget();
+
+ const size_t bits = io->metadata.m.bit_depth.bits_per_sample;
+ PaddedBytes png;
+ JXL_RETURN_IF_ERROR(
+ extras::EncodeImageAPNG(io, io->Main().c_current(), bits, pool, &png));
+ JXL_RETURN_IF_ERROR(WriteFile(png, png_filename));
+ std::vector<std::string> arguments = compress_args_;
+ arguments.push_back(png_filename);
+ arguments.push_back(encoded_filename);
+ JXL_RETURN_IF_ERROR(ReportCodecRunningTime(
+ [&, this] { return RunCommand(compress_command_, arguments); },
+ encoded_filename, speed_stats));
+ return ReadFile(encoded_filename, compressed);
+ }
+
+ Status Decompress(const std::string& filename,
+ const Span<const uint8_t> compressed,
+ ThreadPoolInternal* pool, CodecInOut* io,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ const std::string basename = GetBaseName(filename);
+ TemporaryFile encoded_file(basename, extension_), png_file(basename, "png");
+ std::string encoded_filename, png_filename;
+ JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename));
+ JXL_RETURN_IF_ERROR(png_file.GetFileName(&png_filename));
+
+ JXL_RETURN_IF_ERROR(WriteFile(compressed, encoded_filename));
+ JXL_RETURN_IF_ERROR(ReportCodecRunningTime(
+ [&, this] {
+ return RunCommand(
+ decompress_command_,
+ std::vector<std::string>{encoded_filename, png_filename});
+ },
+ png_filename, speed_stats));
+ JXL_RETURN_IF_ERROR(
+ SetFromFile(png_filename, extras::ColorHints(), io, pool));
+ io->metadata.m.SetIntensityTarget(saved_intensity_target_);
+ return true;
+ }
+
+ private:
+ std::string extension_;
+ std::string compress_command_;
+ std::string decompress_command_;
+ std::vector<std::string> compress_args_;
+ int param_index_ = 0;
+ int saved_intensity_target_ = 255;
+};
+
+} // namespace
+
+ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args) {
+ return new CustomCodec(args);
+}
+
+} // namespace jxl
+
+#else
+
+namespace jxl {
+
+ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args) { return nullptr; }
+
+} // namespace jxl
+
+#endif // _MSC_VER
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_custom.h b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.h
new file mode 100644
index 0000000000..b2711cd5cc
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.h
@@ -0,0 +1,46 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_CUSTOM_H_
+#define TOOLS_BENCHMARK_BENCHMARK_CODEC_CUSTOM_H_
+
+// This is a benchmark codec that can be used with any command-line
+// encoder/decoder that satisfies the following conditions:
+//
+// - the encoder can read from a PNG file `$input.png` and write the encoded
+// image to `$encoded.$ext` if it is called as:
+//
+// $encoder [OPTIONS] $input.png $encoded.$ext
+//
+// - the decoder can read from an encoded file `$encoded.$ext` and write to a
+// PNG file `$decoded.png` if it is called as:
+//
+// $decoder $encoded.$ext $decoded.png
+//
+// On the benchmark command line, the codec must be specified as:
+//
+// custom:$ext:$encoder:$decoder:$options
+//
+// Where the options are also separated by colons.
+//
+// An example with JPEG XL itself would be:
+//
+// custom:jxl:cjxl:djxl:--distance:3
+//
+// Optionally, to have encoding and decoding speed reported, the codec may write
+// the number of seconds (as a floating point number) elapsed during actual
+// encoding/decoding to $encoded.time and $decoded.time, respectively (replacing
+// the .$ext and .png extensions).
+
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_codec.h"
+
+namespace jxl {
+
+ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args);
+
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_CUSTOM_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc
new file mode 100644
index 0000000000..78b304e832
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc
@@ -0,0 +1,125 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+#include "tools/benchmark/benchmark_codec_jpeg.h"
+
+#include <stddef.h>
+#include <stdio.h>
+// After stddef/stdio
+#include <stdint.h>
+#include <string.h>
+
+#include <numeric> // partial_sum
+#include <string>
+
+#include "lib/extras/dec/jpg.h"
+#include "lib/extras/enc/jpg.h"
+#include "lib/extras/packed_image.h"
+#include "lib/extras/packed_image_convert.h"
+#include "lib/extras/time.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "tools/cmdline.h"
+
+using jxl::extras::JpegEncoder;
+
+namespace jxl {
+
+namespace {
+
+struct JPEGArgs {
+ JpegEncoder encoder = JpegEncoder::kLibJpeg;
+ YCbCrChromaSubsampling chroma_subsampling;
+};
+
+JPEGArgs* const jpegargs = new JPEGArgs;
+
+bool ParseChromaSubsampling(const char* param,
+ YCbCrChromaSubsampling* subsampling) {
+ std::vector<std::pair<
+ std::string, std::pair<std::array<uint8_t, 3>, std::array<uint8_t, 3>>>>
+ options = {{"444", {{{1, 1, 1}}, {{1, 1, 1}}}},
+ {"420", {{{2, 1, 1}}, {{2, 1, 1}}}},
+ {"422", {{{2, 1, 1}}, {{1, 1, 1}}}},
+ {"440", {{{1, 1, 1}}, {{2, 1, 1}}}}};
+ for (const auto& option : options) {
+ if (param == option.first) {
+ JXL_CHECK(subsampling->Set(option.second.first.data(),
+ option.second.second.data()));
+ return true;
+ }
+ }
+ return false;
+}
+
+} // namespace
+
+Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args) {
+ args->cmdline.AddOptionValue(
+ '\0', "chroma_subsampling", "444/422/420/411",
+ "default JPEG chroma subsampling (default: 444).",
+ &jpegargs->chroma_subsampling, &ParseChromaSubsampling);
+ return true;
+}
+
+class JPEGCodec : public ImageCodec {
+ public:
+ explicit JPEGCodec(const BenchmarkArgs& args) : ImageCodec(args) {
+ encoder_ = jpegargs->encoder;
+ chroma_subsampling_ = jpegargs->chroma_subsampling;
+ }
+
+ Status ParseParam(const std::string& param) override {
+ if (ImageCodec::ParseParam(param)) {
+ return true;
+ }
+ if (param == "sjpeg") {
+ encoder_ = JpegEncoder::kSJpeg;
+ return true;
+ }
+ if (param.compare(0, 3, "yuv") == 0) {
+ if (param.size() != 6) return false;
+ return ParseChromaSubsampling(param.c_str() + 3, &chroma_subsampling_);
+ }
+ return false;
+ }
+
+ Status Compress(const std::string& filename, const CodecInOut* io,
+ ThreadPoolInternal* pool, PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ const double start = Now();
+ JXL_RETURN_IF_ERROR(EncodeImageJPG(io, encoder_,
+ static_cast<int>(std::round(q_target_)),
+ chroma_subsampling_, pool, compressed));
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ return true;
+ }
+
+ Status Decompress(const std::string& filename,
+ const Span<const uint8_t> compressed,
+ ThreadPoolInternal* pool, CodecInOut* io,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ extras::PackedPixelFile ppf;
+ const double start = Now();
+ JXL_RETURN_IF_ERROR(DecodeImageJPG(compressed, extras::ColorHints(),
+ SizeConstraints(), &ppf));
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
+ return true;
+ }
+
+ protected:
+ JpegEncoder encoder_;
+ YCbCrChromaSubsampling chroma_subsampling_;
+};
+
+ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args) {
+ return new JPEGCodec(args);
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.h b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.h
new file mode 100644
index 0000000000..cd4b009a78
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.h
@@ -0,0 +1,20 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_JPEG_H_
+#define TOOLS_BENCHMARK_BENCHMARK_CODEC_JPEG_H_
+
+#include "lib/jxl/base/status.h"
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_codec.h"
+
+namespace jxl {
+ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args);
+
+// Registers the jpeg-specific command line options.
+Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args);
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_JPEG_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc
new file mode 100644
index 0000000000..6dd1ad016e
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc
@@ -0,0 +1,405 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+#include "tools/benchmark/benchmark_codec_jxl.h"
+
+#include <cstdint>
+#include <cstdlib>
+#include <functional>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "jxl/decode_cxx.h"
+#include "jxl/thread_parallel_runner_cxx.h"
+#include "lib/extras/codec.h"
+#include "lib/extras/time.h"
+#include "lib/jxl/aux_out.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/dec_file.h"
+#include "lib/jxl/dec_params.h"
+#include "lib/jxl/enc_cache.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_external_image.h"
+#include "lib/jxl/enc_file.h"
+#include "lib/jxl/enc_params.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_metadata.h"
+#include "lib/jxl/modular/encoding/encoding.h"
+#include "tools/benchmark/benchmark_file_io.h"
+#include "tools/benchmark/benchmark_stats.h"
+#include "tools/cmdline.h"
+
+namespace jxl {
+
+// Output function for EncodeBrunsli.
+size_t OutputToBytes(void* data, const uint8_t* buf, size_t count) {
+ PaddedBytes* output = reinterpret_cast<PaddedBytes*>(data);
+ output->append(buf, buf + count);
+ return count;
+}
+
+struct JxlArgs {
+ double xmul;
+ double quant_bias;
+
+ bool use_ac_strategy;
+ bool qprogressive; // progressive with shift-quantization.
+ bool progressive;
+ int progressive_dc;
+
+ Override noise;
+ Override dots;
+ Override patches;
+
+ std::string debug_image_dir;
+};
+
+static JxlArgs* const jxlargs = new JxlArgs;
+
+Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args) {
+ args->AddDouble(&jxlargs->xmul, "xmul",
+ "Multiplier for the difference in X channel in Butteraugli.",
+ 1.0);
+ args->AddDouble(&jxlargs->quant_bias, "quant_bias",
+ "Bias border pixels during quantization by this ratio.", 0.0);
+ args->AddFlag(&jxlargs->use_ac_strategy, "use_ac_strategy",
+ "If true, AC strategy will be used.", false);
+ args->AddFlag(&jxlargs->qprogressive, "qprogressive",
+ "Enable quantized progressive mode for AC.", false);
+ args->AddFlag(&jxlargs->progressive, "progressive",
+ "Enable progressive mode for AC.", false);
+ args->AddSigned(&jxlargs->progressive_dc, "progressive_dc",
+ "Enable progressive mode for DC.", -1);
+
+ args->AddOverride(&jxlargs->noise, "noise",
+ "Enable(1)/disable(0) noise generation.");
+ args->AddOverride(&jxlargs->dots, "dots",
+ "Enable(1)/disable(0) dots generation.");
+ args->AddOverride(&jxlargs->patches, "patches",
+ "Enable(1)/disable(0) patch dictionary.");
+
+ args->AddString(
+ &jxlargs->debug_image_dir, "debug_image_dir",
+ "If not empty, saves debug images for each "
+ "input image and each codec that provides it to this directory.");
+
+ return true;
+}
+
+Status ValidateArgsJxlCodec(BenchmarkArgs* args) { return true; }
+
+class JxlCodec : public ImageCodec {
+ public:
+ explicit JxlCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
+
+ Status ParseParam(const std::string& param) override {
+ const std::string kMaxPassesPrefix = "max_passes=";
+ const std::string kDownsamplingPrefix = "downsampling=";
+ const std::string kResamplingPrefix = "resampling=";
+ const std::string kEcResamplingPrefix = "ec_resampling=";
+
+ if (param.substr(0, kResamplingPrefix.size()) == kResamplingPrefix) {
+ std::istringstream parser(param.substr(kResamplingPrefix.size()));
+ parser >> cparams_.resampling;
+ } else if (param.substr(0, kEcResamplingPrefix.size()) ==
+ kEcResamplingPrefix) {
+ std::istringstream parser(param.substr(kEcResamplingPrefix.size()));
+ parser >> cparams_.ec_resampling;
+ } else if (ImageCodec::ParseParam(param)) {
+ // Nothing to do.
+ } else if (param == "uint8") {
+ uint8_ = true;
+ } else if (param[0] == 'u') {
+ char* end;
+ cparams_.uniform_quant = strtof(param.c_str() + 1, &end);
+ if (end == param.c_str() + 1 || *end != '\0') {
+ return JXL_FAILURE("failed to parse uniform quant parameter %s",
+ param.c_str());
+ }
+ ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry;
+ } else if (param.substr(0, kMaxPassesPrefix.size()) == kMaxPassesPrefix) {
+ std::istringstream parser(param.substr(kMaxPassesPrefix.size()));
+ parser >> dparams_.max_passes;
+ } else if (param.substr(0, kDownsamplingPrefix.size()) ==
+ kDownsamplingPrefix) {
+ std::istringstream parser(param.substr(kDownsamplingPrefix.size()));
+ parser >> dparams_.max_downsampling;
+ } else if (ParseSpeedTier(param, &cparams_.speed_tier)) {
+ // Nothing to do.
+ } else if (param[0] == 'X') {
+ cparams_.channel_colors_pre_transform_percent =
+ strtol(param.substr(1).c_str(), nullptr, 10);
+ } else if (param[0] == 'Y') {
+ cparams_.channel_colors_percent =
+ strtol(param.substr(1).c_str(), nullptr, 10);
+ } else if (param[0] == 'p') {
+ cparams_.palette_colors = strtol(param.substr(1).c_str(), nullptr, 10);
+ } else if (param == "lp") {
+ cparams_.lossy_palette = true;
+ } else if (param[0] == 'C') {
+ cparams_.colorspace = strtol(param.substr(1).c_str(), nullptr, 10);
+ } else if (param[0] == 'c') {
+ cparams_.color_transform =
+ (jxl::ColorTransform)strtol(param.substr(1).c_str(), nullptr, 10);
+ has_ctransform_ = true;
+ } else if (param[0] == 'I') {
+ cparams_.options.nb_repeats = strtof(param.substr(1).c_str(), nullptr);
+ } else if (param[0] == 'E') {
+ cparams_.options.max_properties =
+ strtof(param.substr(1).c_str(), nullptr);
+ } else if (param[0] == 'P') {
+ cparams_.options.predictor =
+ static_cast<Predictor>(strtof(param.substr(1).c_str(), nullptr));
+ } else if (param == "slow") {
+ cparams_.options.nb_repeats = 2;
+ } else if (param == "R") {
+ cparams_.responsive = 1;
+ } else if (param[0] == 'R') {
+ cparams_.responsive = strtol(param.substr(1).c_str(), nullptr, 10);
+ } else if (param == "m") {
+ cparams_.modular_mode = true;
+ cparams_.color_transform = jxl::ColorTransform::kNone;
+ } else if (param.substr(0, 3) == "gab") {
+ long gab = strtol(param.substr(3).c_str(), nullptr, 10);
+ if (gab != 0 && gab != 1) {
+ return JXL_FAILURE("Invalid gab value");
+ }
+ cparams_.gaborish = static_cast<Override>(gab);
+ } else if (param[0] == 'g') {
+ long gsize = strtol(param.substr(1).c_str(), nullptr, 10);
+ if (gsize < 0 || gsize > 3) {
+ return JXL_FAILURE("Invalid group size shift value");
+ }
+ cparams_.modular_group_size_shift = gsize;
+ } else if (param == "new_heuristics") {
+ cparams_.use_new_heuristics = true;
+ } else if (param == "plt") {
+ cparams_.options.max_properties = 0;
+ cparams_.options.nb_repeats = 0;
+ cparams_.options.predictor = Predictor::Zero;
+ cparams_.responsive = 0;
+ cparams_.colorspace = 0;
+ cparams_.channel_colors_pre_transform_percent = 0;
+ cparams_.channel_colors_percent = 0;
+ } else if (param.substr(0, 3) == "epf") {
+ cparams_.epf = strtol(param.substr(3).c_str(), nullptr, 10);
+ if (cparams_.epf > 3) {
+ return JXL_FAILURE("Invalid epf value");
+ }
+ } else if (param.substr(0, 16) == "faster_decoding=") {
+ cparams_.decoding_speed_tier =
+ strtol(param.substr(16).c_str(), nullptr, 10);
+ } else {
+ return JXL_FAILURE("Unrecognized param");
+ }
+ return true;
+ }
+
+ bool IsColorAware() const override {
+ // Can't deal with negative values from color space conversion.
+ if (cparams_.modular_mode) return false;
+ // Otherwise, input may be in any color space.
+ return true;
+ }
+
+ bool IsJpegTranscoder() const override {
+ // TODO(veluca): figure out when to turn this on.
+ return false;
+ }
+
+ Status Compress(const std::string& filename, const CodecInOut* io,
+ ThreadPoolInternal* pool, PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ if (!jxlargs->debug_image_dir.empty()) {
+ cinfo_.dump_image = [](const CodecInOut& io, const std::string& path) {
+ return EncodeToFile(io, path);
+ };
+ cinfo_.debug_prefix =
+ JoinPath(jxlargs->debug_image_dir, FileBaseName(filename)) +
+ ".jxl:" + params_ + ".dbg/";
+ JXL_RETURN_IF_ERROR(MakeDir(cinfo_.debug_prefix));
+ }
+ cparams_.butteraugli_distance = butteraugli_target_;
+ cparams_.target_bitrate = bitrate_target_;
+
+ cparams_.dots = jxlargs->dots;
+ cparams_.patches = jxlargs->patches;
+
+ cparams_.progressive_mode = jxlargs->progressive;
+ cparams_.qprogressive_mode = jxlargs->qprogressive;
+ cparams_.progressive_dc = jxlargs->progressive_dc;
+
+ cparams_.noise = jxlargs->noise;
+
+ cparams_.quant_border_bias = static_cast<float>(jxlargs->quant_bias);
+ cparams_.ba_params.hf_asymmetry = ba_params_.hf_asymmetry;
+ cparams_.ba_params.xmul = static_cast<float>(jxlargs->xmul);
+
+ if (cparams_.butteraugli_distance > 0.f &&
+ cparams_.color_transform == ColorTransform::kNone &&
+ cparams_.modular_mode && !has_ctransform_) {
+ cparams_.color_transform = ColorTransform::kXYB;
+ }
+
+ const double start = Now();
+ PassesEncoderState passes_encoder_state;
+ if (cparams_.use_new_heuristics) {
+ passes_encoder_state.heuristics =
+ jxl::make_unique<jxl::FastEncoderHeuristics>();
+ }
+ JXL_RETURN_IF_ERROR(EncodeFile(cparams_, io, &passes_encoder_state,
+ compressed, GetJxlCms(), &cinfo_, pool));
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ return true;
+ }
+
+ Status Decompress(const std::string& filename,
+ const Span<const uint8_t> compressed,
+ ThreadPoolInternal* pool, CodecInOut* io,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ io->frames.clear();
+ if (dparams_.max_passes != DecompressParams().max_passes ||
+ dparams_.max_downsampling != DecompressParams().max_downsampling) {
+ // Must use the C++ API to honor non-default dparams.
+ if (uint8_) {
+ return JXL_FAILURE(
+ "trying to use decompress params that are not all available in "
+ "either decoding API");
+ }
+ const double start = Now();
+ JXL_RETURN_IF_ERROR(DecodeFile(dparams_, compressed, io, pool));
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ return true;
+ }
+
+ double elapsed_convert_image = 0;
+ const double start = Now();
+ {
+ std::vector<uint8_t> pixel_data;
+ PaddedBytes icc_profile;
+ auto runner = JxlThreadParallelRunnerMake(nullptr, pool->NumThreads());
+ auto dec = JxlDecoderMake(nullptr);
+ // By default, the decoder will undo exif orientation, giving an image
+ // with identity exif rotation as result. However, the benchmark does
+ // not undo exif orientation of the originals, and compares against the
+ // originals, so we must set the option to keep the original orientation
+ // instead.
+ JxlDecoderSetKeepOrientation(dec.get(), JXL_TRUE);
+ JXL_RETURN_IF_ERROR(
+ JXL_DEC_SUCCESS ==
+ JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO |
+ JXL_DEC_COLOR_ENCODING |
+ JXL_DEC_FULL_IMAGE));
+ JxlBasicInfo info{};
+ JxlPixelFormat format = {/*num_channels=*/3,
+ /*data_type=*/JXL_TYPE_FLOAT,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0};
+ if (uint8_) {
+ format.data_type = JXL_TYPE_UINT8;
+ }
+ JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size());
+ JxlDecoderStatus status;
+ while ((status = JxlDecoderProcessInput(dec.get())) != JXL_DEC_SUCCESS) {
+ switch (status) {
+ case JXL_DEC_ERROR:
+ return JXL_FAILURE("decoder error");
+ case JXL_DEC_NEED_MORE_INPUT:
+ return JXL_FAILURE("decoder requests more input");
+ case JXL_DEC_BASIC_INFO:
+ JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS ==
+ JxlDecoderGetBasicInfo(dec.get(), &info));
+ format.num_channels = info.num_color_channels;
+ if (info.alpha_bits != 0) {
+ ++format.num_channels;
+ io->metadata.m.extra_channel_info.resize(1);
+ io->metadata.m.extra_channel_info[0].type =
+ jxl::ExtraChannel::kAlpha;
+ }
+ break;
+ case JXL_DEC_COLOR_ENCODING: {
+ size_t icc_size;
+ JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS ==
+ JxlDecoderGetICCProfileSize(
+ dec.get(), &format,
+ JXL_COLOR_PROFILE_TARGET_DATA, &icc_size));
+ icc_profile.resize(icc_size);
+ JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS ==
+ JxlDecoderGetColorAsICCProfile(
+ dec.get(), &format,
+ JXL_COLOR_PROFILE_TARGET_DATA,
+ icc_profile.data(), icc_profile.size()));
+ break;
+ }
+ case JXL_DEC_NEED_IMAGE_OUT_BUFFER: {
+ size_t buffer_size;
+ JXL_RETURN_IF_ERROR(
+ JXL_DEC_SUCCESS ==
+ JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size));
+ JXL_RETURN_IF_ERROR(buffer_size ==
+ info.xsize * info.ysize * format.num_channels *
+ (uint8_ ? sizeof(uint8_t) : sizeof(float)));
+ pixel_data.resize(buffer_size);
+ JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS ==
+ JxlDecoderSetImageOutBuffer(dec.get(), &format,
+ pixel_data.data(),
+ buffer_size));
+ break;
+ }
+ case JXL_DEC_FULL_IMAGE: {
+ const double start_convert_image = Now();
+ {
+ ColorEncoding color_encoding;
+ JXL_RETURN_IF_ERROR(
+ color_encoding.SetICC(PaddedBytes(icc_profile)));
+ ImageBundle frame(&io->metadata.m);
+ JXL_RETURN_IF_ERROR(BufferToImageBundle(
+ format, info.xsize, info.ysize, pixel_data.data(),
+ pixel_data.size(), pool, color_encoding, &frame));
+ io->frames.push_back(std::move(frame));
+ io->dec_pixels += info.xsize * info.ysize;
+ }
+ const double end_convert_image = Now();
+ elapsed_convert_image += end_convert_image - start_convert_image;
+ break;
+ }
+ default:
+ return JXL_FAILURE("unrecognized status %d",
+ static_cast<int>(status));
+ }
+ }
+ }
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start - elapsed_convert_image);
+ return true;
+ }
+
+ void GetMoreStats(BenchmarkStats* stats) override {
+ JxlStats jxl_stats;
+ jxl_stats.num_inputs = 1;
+ jxl_stats.aux_out = cinfo_;
+ stats->jxl_stats.Assimilate(jxl_stats);
+ }
+
+ protected:
+ AuxOut cinfo_;
+ CompressParams cparams_;
+ bool has_ctransform_ = false;
+ DecompressParams dparams_;
+ bool uint8_ = false;
+};
+
+ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args) {
+ return new JxlCodec(args);
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.h b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.h
new file mode 100644
index 0000000000..12e9fef79a
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.h
@@ -0,0 +1,23 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_JXL_H_
+#define TOOLS_BENCHMARK_BENCHMARK_CODEC_JXL_H_
+
+#include <string>
+
+#include "lib/jxl/base/status.h"
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_codec.h"
+
+namespace jxl {
+ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args);
+
+// Registers the jxl-specific command line options.
+Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args);
+Status ValidateArgsJxlCodec(BenchmarkArgs* args);
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_JXL_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc
new file mode 100644
index 0000000000..e479d7ad24
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc
@@ -0,0 +1,75 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#if JPEGXL_ENABLE_APNG
+
+#include "tools/benchmark/benchmark_codec_png.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+
+#include "lib/extras/dec/apng.h"
+#include "lib/extras/enc/apng.h"
+#include "lib/extras/packed_image.h"
+#include "lib/extras/packed_image_convert.h"
+#include "lib/extras/time.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+
+namespace jxl {
+
+struct PNGArgs {
+ // Empty, no PNG-specific args currently.
+};
+
+static PNGArgs* const pngargs = new PNGArgs;
+
+Status AddCommandLineOptionsPNGCodec(BenchmarkArgs* args) { return true; }
+
+// Lossless.
+class PNGCodec : public ImageCodec {
+ public:
+ explicit PNGCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
+
+ Status ParseParam(const std::string& param) override { return true; }
+
+ Status Compress(const std::string& filename, const CodecInOut* io,
+ ThreadPoolInternal* pool, PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ const size_t bits = io->metadata.m.bit_depth.bits_per_sample;
+ const double start = Now();
+ JXL_RETURN_IF_ERROR(extras::EncodeImageAPNG(io, io->Main().c_current(),
+ bits, pool, compressed));
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ return true;
+ }
+
+ Status Decompress(const std::string& /*filename*/,
+ const Span<const uint8_t> compressed,
+ ThreadPoolInternal* pool, CodecInOut* io,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ extras::PackedPixelFile ppf;
+ const double start = Now();
+ JXL_RETURN_IF_ERROR(extras::DecodeImageAPNG(
+ compressed, extras::ColorHints(), SizeConstraints(), &ppf));
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
+ return true;
+ }
+};
+
+ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args) {
+ return new PNGCodec(args);
+}
+
+} // namespace jxl
+
+#endif \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_png.h b/media/libjxl/src/tools/benchmark/benchmark_codec_png.h
new file mode 100644
index 0000000000..23d982e17b
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_png.h
@@ -0,0 +1,26 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_
+#define TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_
+
+#if JPEGXL_ENABLE_APNG
+
+#include <string>
+
+#include "lib/jxl/base/status.h"
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_codec.h"
+
+namespace jxl {
+ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args);
+
+// Registers the png-specific command line options.
+Status AddCommandLineOptionsPNGCodec(BenchmarkArgs* args);
+} // namespace jxl
+
+#endif
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc
new file mode 100644
index 0000000000..376018c364
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc
@@ -0,0 +1,280 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+#include "tools/benchmark/benchmark_codec_webp.h"
+
+#include <stdint.h>
+#include <string.h>
+#include <webp/decode.h>
+#include <webp/encode.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/time.h"
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/dec_external_image.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_external_image.h"
+#include "lib/jxl/enc_image_bundle.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/sanitizers.h"
+
+namespace jxl {
+
+// Sets image data from 8-bit sRGB pixel array in bytes.
+// Amount of input bytes per pixel must be:
+// (is_gray ? 1 : 3) + (has_alpha ? 1 : 0)
+Status FromSRGB(const size_t xsize, const size_t ysize, const bool is_gray,
+ const bool has_alpha, const bool alpha_is_premultiplied,
+ const bool is_16bit, const JxlEndianness endianness,
+ const uint8_t* pixels, const uint8_t* end, ThreadPool* pool,
+ ImageBundle* ib) {
+ const ColorEncoding& c = ColorEncoding::SRGB(is_gray);
+ const size_t bits_per_sample = (is_16bit ? 2 : 1) * kBitsPerByte;
+ const Span<const uint8_t> span(pixels, end - pixels);
+ return ConvertFromExternal(span, xsize, ysize, c,
+ (is_gray ? 1 : 3) + (has_alpha ? 1 : 0),
+ alpha_is_premultiplied, bits_per_sample,
+ endianness, /*flipped_y=*/false, pool, ib,
+ /*float_in=*/false, /*align=*/0);
+}
+
+struct WebPArgs {
+ // Empty, no WebP-specific args currently.
+};
+
+static WebPArgs* const webpargs = new WebPArgs;
+
+Status AddCommandLineOptionsWebPCodec(BenchmarkArgs* args) { return true; }
+
+class WebPCodec : public ImageCodec {
+ public:
+ explicit WebPCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
+
+ Status ParseParam(const std::string& param) override {
+ // Ensure that the 'q' parameter is not used up by ImageCodec.
+ if (param[0] == 'q') {
+ if (near_lossless_) {
+ near_lossless_quality_ = ParseIntParam(param, 0, 99);
+ } else {
+ quality_ = ParseIntParam(param, 1, 100);
+ }
+ return true;
+ } else if (ImageCodec::ParseParam(param)) {
+ return true;
+ } else if (param == "ll") {
+ lossless_ = true;
+ JXL_CHECK(!near_lossless_);
+ return true;
+ } else if (param == "nl") {
+ near_lossless_ = true;
+ JXL_CHECK(!lossless_);
+ return true;
+ } else if (param[0] == 'm') {
+ method_ = ParseIntParam(param, 1, 6);
+ return true;
+ }
+ return false;
+ }
+
+ Status Compress(const std::string& filename, const CodecInOut* io,
+ ThreadPoolInternal* pool, PaddedBytes* compressed,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ const double start = Now();
+ const ImageBundle& ib = io->Main();
+
+ if (ib.HasAlpha() && ib.metadata()->GetAlphaBits() > 8) {
+ return JXL_FAILURE("WebP alpha must be 8-bit");
+ }
+
+ size_t num_chans = (ib.HasAlpha() ? 4 : 3);
+ ImageMetadata metadata = io->metadata.m;
+ ImageBundle store(&metadata);
+ const ImageBundle* transformed;
+ const ColorEncoding& c_desired = ColorEncoding::SRGB(false);
+ JXL_RETURN_IF_ERROR(TransformIfNeeded(ib, c_desired, GetJxlCms(), pool,
+ &store, &transformed));
+ size_t xsize = ib.oriented_xsize();
+ size_t ysize = ib.oriented_ysize();
+ size_t stride = xsize * num_chans;
+ PaddedBytes srgb(stride * ysize);
+ JXL_RETURN_IF_ERROR(ConvertToExternal(
+ *transformed, 8, /*float_out=*/false, num_chans, JXL_BIG_ENDIAN, stride,
+ pool, srgb.data(), srgb.size(),
+ /*out_callback=*/{}, metadata.GetOrientation()));
+
+ if (lossless_ || near_lossless_) {
+ // The lossless codec does not support 16-bit channels.
+ // Color models are currently not supported here and the sRGB 8-bit
+ // conversion causes loss due to clipping.
+ if (!ib.IsSRGB() || ib.metadata()->bit_depth.bits_per_sample > 8 ||
+ ib.metadata()->bit_depth.exponent_bits_per_sample > 0) {
+ return JXL_FAILURE("%s: webp:ll/nl requires 8-bit sRGB",
+ filename.c_str());
+ }
+ JXL_RETURN_IF_ERROR(
+ CompressInternal(srgb, xsize, ysize, num_chans, 100, compressed));
+ } else if (bitrate_target_ > 0.0) {
+ int quality_bad = 100;
+ int quality_good = 92;
+ size_t target_size = xsize * ysize * bitrate_target_ / 8.0;
+ while (quality_good > 0 &&
+ CompressInternal(srgb, xsize, ysize, num_chans, quality_good,
+ compressed) &&
+ compressed->size() > target_size) {
+ quality_bad = quality_good;
+ quality_good -= 8;
+ }
+ if (quality_good <= 0) quality_good = 1;
+ while (quality_good + 1 < quality_bad) {
+ int quality = (quality_bad + quality_good) / 2;
+ if (!CompressInternal(srgb, xsize, ysize, num_chans, quality,
+ compressed)) {
+ break;
+ }
+ if (compressed->size() <= target_size) {
+ quality_good = quality;
+ } else {
+ quality_bad = quality;
+ }
+ }
+ JXL_RETURN_IF_ERROR(CompressInternal(srgb, xsize, ysize, num_chans,
+ quality_good, compressed));
+ } else if (quality_ > 0) {
+ JXL_RETURN_IF_ERROR(CompressInternal(srgb, xsize, ysize, num_chans,
+ quality_, compressed));
+ } else {
+ return false;
+ }
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ return true;
+ }
+
+ Status Decompress(const std::string& filename,
+ const Span<const uint8_t> compressed,
+ ThreadPoolInternal* pool, CodecInOut* io,
+ jpegxl::tools::SpeedStats* speed_stats) override {
+ WebPDecoderConfig config;
+#ifdef MEMORY_SANITIZER
+ // config is initialized by libwebp, which we are not instrumenting with
+ // msan, therefore we need to initialize it here.
+ memset(&config, 0, sizeof(config));
+#endif
+ JXL_RETURN_IF_ERROR(WebPInitDecoderConfig(&config) == 1);
+ config.options.use_threads = 0;
+ config.options.dithering_strength = 0;
+ config.options.bypass_filtering = 0;
+ config.options.no_fancy_upsampling = 0;
+ WebPDecBuffer* const buf = &config.output;
+ buf->colorspace = MODE_RGBA;
+ const uint8_t* webp_data = compressed.data();
+ const int webp_size = compressed.size();
+ const double start = Now();
+ if (WebPDecode(webp_data, webp_size, &config) != VP8_STATUS_OK) {
+ return JXL_FAILURE("WebPDecode failed");
+ }
+ const double end = Now();
+ speed_stats->NotifyElapsed(end - start);
+ JXL_CHECK(buf->u.RGBA.stride == buf->width * 4);
+
+ const bool is_gray = false;
+ const bool has_alpha = true;
+ const uint8_t* data_begin = &buf->u.RGBA.rgba[0];
+ const uint8_t* data_end = data_begin + buf->width * buf->height * 4;
+ // The image data is initialized by libwebp, which we are not instrumenting
+ // with msan.
+ msan::UnpoisonMemory(data_begin, data_end - data_begin);
+ if (io->metadata.m.color_encoding.IsGray() != is_gray) {
+ // TODO(lode): either ensure is_gray matches what the color profile says,
+ // or set a correct color profile, e.g.
+ // io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
+ // Return a standard failure because SetFromSRGB triggers a fatal assert
+ // for this instead.
+ return JXL_FAILURE("Color profile is-gray mismatch");
+ }
+ io->metadata.m.SetAlphaBits(8);
+ const Status ok =
+ FromSRGB(buf->width, buf->height, is_gray, has_alpha,
+ /*alpha_is_premultiplied=*/false, /*is_16bit=*/false,
+ JXL_LITTLE_ENDIAN, data_begin, data_end, pool, &io->Main());
+ WebPFreeDecBuffer(buf);
+ JXL_RETURN_IF_ERROR(ok);
+ io->dec_pixels = buf->width * buf->height;
+ return true;
+ }
+
+ private:
+ static int WebPStringWrite(const uint8_t* data, size_t data_size,
+ const WebPPicture* const picture) {
+ if (data_size) {
+ PaddedBytes* const out = static_cast<PaddedBytes*>(picture->custom_ptr);
+ const size_t pos = out->size();
+ out->resize(pos + data_size);
+ memcpy(out->data() + pos, data, data_size);
+ }
+ return 1;
+ }
+ Status CompressInternal(const PaddedBytes& srgb, size_t xsize, size_t ysize,
+ size_t num_chans, int quality,
+ PaddedBytes* compressed) {
+ *compressed = PaddedBytes();
+ WebPConfig config;
+ WebPConfigInit(&config);
+ JXL_ASSERT(!lossless_ || !near_lossless_); // can't have both
+ config.lossless = lossless_;
+ config.quality = quality;
+ config.method = method_;
+#if WEBP_ENCODER_ABI_VERSION >= 0x020a
+ config.near_lossless = near_lossless_ ? near_lossless_quality_ : 100;
+#else
+ if (near_lossless_) {
+ JXL_WARNING("Near lossless not supported by this WebP version");
+ }
+#endif
+ JXL_CHECK(WebPValidateConfig(&config));
+
+ WebPPicture pic;
+ WebPPictureInit(&pic);
+ pic.width = static_cast<int>(xsize);
+ pic.height = static_cast<int>(ysize);
+ pic.writer = &WebPStringWrite;
+ if (lossless_ || near_lossless_) pic.use_argb = 1;
+ pic.custom_ptr = compressed;
+
+ if (num_chans == 3) {
+ WebPPictureImportRGB(&pic, srgb.data(), 3 * xsize);
+ } else {
+ WebPPictureImportRGBA(&pic, srgb.data(), 4 * xsize);
+ }
+
+ // WebP encoding may fail, for example, if the image is more than 16384
+ // pixels high or wide.
+ bool ok = WebPEncode(&config, &pic);
+ WebPPictureFree(&pic);
+ // Compressed image data is initialized by libwebp, which we are not
+ // instrumenting with msan.
+ msan::UnpoisonMemory(compressed->data(), compressed->size());
+ return ok;
+ }
+
+ int quality_ = 90;
+ bool lossless_ = false;
+ bool near_lossless_ = false;
+ bool near_lossless_quality_ = 40; // only used if near_lossless_
+ int method_ = 6; // smallest, some speed cost
+};
+
+ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args) {
+ return new WebPCodec(args);
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_webp.h b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.h
new file mode 100644
index 0000000000..cd4c60fb5e
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.h
@@ -0,0 +1,23 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_WEBP_H_
+#define TOOLS_BENCHMARK_BENCHMARK_CODEC_WEBP_H_
+
+// To support webp, install libwebp-dev and rerun cmake.
+
+#include <string>
+
+#include "lib/jxl/base/status.h"
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_codec.h"
+
+namespace jxl {
+ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args);
+
+// Registers the webp-specific command line options.
+Status AddCommandLineOptionsWebPCodec(BenchmarkArgs* args);
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_WEBP_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_file_io.cc b/media/libjxl/src/tools/benchmark/benchmark_file_io.cc
new file mode 100644
index 0000000000..c5db02b8f1
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_file_io.cc
@@ -0,0 +1,232 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+#include "tools/benchmark/benchmark_file_io.h"
+
+#include <errno.h>
+#include <sys/stat.h>
+
+#include <cstdio>
+
+#if defined(_WIN32) || defined(_WIN64)
+#include "third_party/dirent.h"
+#else
+#include <dirent.h>
+#include <unistd.h>
+#endif
+
+#ifndef HAS_GLOB
+#define HAS_GLOB 0
+#if defined __has_include
+// <glob.h> is included in previous APIs but glob() function is not defined
+// until API 28.
+#if __has_include(<glob.h>) && \
+ (!defined(__ANDROID_API__) || __ANDROID_API__ >= 28)
+#undef HAS_GLOB
+#define HAS_GLOB 1
+#endif // __has_include(<glob.h>)
+#endif // __has_include
+#endif // HAS_GLOB
+
+#if HAS_GLOB
+#include <glob.h>
+#endif // HAS_GLOB
+
+// There is no "user" in embedded filesystems.
+#ifndef GLOB_TILDE
+#define GLOB_TILDE 0
+#endif
+
+namespace jxl {
+
+const char kPathSeparator = '/';
+
+// RAII, ensures dir is closed even when returning early.
+class DirWrapper {
+ public:
+ DirWrapper(const DirWrapper& other) = delete;
+ DirWrapper& operator=(const DirWrapper& other) = delete;
+
+ explicit DirWrapper(const std::string& pathname)
+ : dir_(opendir(pathname.c_str())) {}
+
+ ~DirWrapper() {
+ if (dir_ != nullptr) {
+ const int err = closedir(dir_);
+ JXL_CHECK(err == 0);
+ }
+ }
+
+ // NOLINTNEXTLINE(google-explicit-constructor)
+ operator DIR*() const { return dir_; }
+
+ private:
+ DIR* const dir_;
+};
+
+// Checks if the file exists, either as file or as directory
+bool PathExists(const std::string& fname) {
+ struct stat s;
+ if (stat(fname.c_str(), &s) != 0) return false;
+ return true;
+}
+
+// Checks if the file exists and is a regular file.
+bool IsRegularFile(const std::string& fname) {
+ struct stat s;
+ if (stat(fname.c_str(), &s) != 0) return false;
+ return S_ISREG(s.st_mode);
+}
+
+// Checks if the file exists and is a directory.
+bool IsDirectory(const std::string& fname) {
+ struct stat s;
+ if (stat(fname.c_str(), &s) != 0) return false;
+ return S_ISDIR(s.st_mode);
+}
+
+// Recursively makes dir, or successfully does nothing if it already exists.
+Status MakeDir(const std::string& dirname) {
+ size_t pos = 0;
+ for (pos = dirname.size(); pos > 0; pos--) {
+ if (pos == dirname.size() || dirname[pos] == kPathSeparator) {
+ // Found existing dir or regular file, break and then start creating
+ // from here (in the latter case we'll get error below).
+ if (PathExists(dirname.substr(0, pos + 1))) {
+ pos += 1; // Skip past this existing path
+ break;
+ }
+ }
+ }
+ for (; pos <= dirname.size(); pos++) {
+ if (pos == dirname.size() || dirname[pos] == kPathSeparator) {
+ std::string subdir = dirname.substr(0, pos + 1);
+ if (mkdir(subdir.c_str(), 0777) && errno != EEXIST) {
+ return JXL_FAILURE("Failed to create directory");
+ }
+ }
+ }
+ if (!IsDirectory(dirname)) return JXL_FAILURE("Failed to create directory");
+ return true; // success
+}
+
+Status DeleteFile(const std::string& fname) {
+ if (!IsRegularFile(fname)) {
+ return JXL_FAILURE("Trying to delete non-regular file");
+ }
+ if (std::remove(fname.c_str())) return JXL_FAILURE("Failed to delete file");
+ return true;
+}
+
+std::string FileBaseName(const std::string& fname) {
+ size_t pos = fname.rfind('/');
+ if (pos == std::string::npos) return fname;
+ return fname.substr(pos + 1);
+}
+
+std::string FileDirName(const std::string& fname) {
+ size_t pos = fname.rfind('/');
+ if (pos == std::string::npos) return "";
+ return fname.substr(0, pos);
+}
+
+std::string FileExtension(const std::string& fname) {
+ size_t pos = fname.rfind('.');
+ if (pos == std::string::npos) return "";
+ return fname.substr(pos);
+}
+
+std::string JoinPath(const std::string& first, const std::string& second) {
+ JXL_CHECK(second.empty() || second[0] != kPathSeparator);
+ return (!first.empty() && first.back() == kPathSeparator)
+ ? (first + second)
+ : (first + kPathSeparator + second);
+}
+
+// Can match a single file, or multiple files in a directory (non-recursive).
+// With POSIX, supports glob(), otherwise supports a subset.
+Status MatchFiles(const std::string& pattern, std::vector<std::string>* list) {
+#if HAS_GLOB
+ glob_t g;
+ memset(&g, 0, sizeof(g));
+ int error = glob(pattern.c_str(), GLOB_TILDE, NULL, &g);
+ if (!error) {
+ for (size_t i = 0; i < g.gl_pathc; ++i) {
+ list->push_back(g.gl_pathv[i]);
+ }
+ }
+ globfree(&g);
+ if (error) return JXL_FAILURE("glob failed for %s", pattern.c_str());
+ return true;
+#else
+ std::string dirname = FileDirName(pattern);
+ std::string basename = FileBaseName(pattern);
+ size_t pos0 = basename.find('*');
+ size_t pos1 = pos0 == std::string::npos ? pos0 : basename.find('*', pos0 + 1);
+ std::string prefix, middle, suffix;
+ if (pos0 != std::string::npos) {
+ prefix = basename.substr(0, pos0);
+ if (pos1 != std::string::npos) {
+ middle = basename.substr(pos0 + 1, pos1 - pos0 - 1);
+ suffix = basename.substr(pos1 + 1);
+ } else {
+ suffix = basename.substr(pos0 + 1);
+ }
+ }
+
+ if (prefix.find_first_of("*?[") != std::string::npos ||
+ middle.find_first_of("*?[") != std::string::npos ||
+ suffix.find_first_of("*?[") != std::string::npos ||
+ dirname.find_first_of("*?[") != std::string::npos) {
+ return JXL_FAILURE(
+ "Only glob patterns with max two '*' in the basename"
+ " are supported, e.g. directory/path/*.png or"
+ " /directory/path/*heatmap*");
+ }
+
+ if (pos0 != std::string::npos) {
+ DirWrapper dir(dirname);
+ if (!dir) return JXL_FAILURE("directory %s doesn't exist", dirname.c_str());
+ for (;;) {
+ dirent* ent = readdir(dir);
+ if (!ent) break;
+ std::string name = ent->d_name;
+ // If there was a suffix, only add if it matches (e.g. ".png")
+ bool matches =
+ name.size() >= (prefix.size() + middle.size() + suffix.size());
+ if (matches) {
+ if (!prefix.empty() && name.substr(0, prefix.size()) != prefix) {
+ matches = false;
+ }
+ if (!middle.empty()) {
+ size_t pos = name.find(middle, prefix.size());
+ if (pos == std::string::npos ||
+ pos + middle.size() > name.size() - suffix.size()) {
+ matches = false;
+ }
+ }
+ if (!suffix.empty() &&
+ name.substr(name.size() - suffix.size()) != suffix) {
+ matches = false;
+ }
+ }
+ if (matches) {
+ std::string path = JoinPath(dirname, name);
+
+ if (IsRegularFile(path)) {
+ list->push_back(path);
+ }
+ }
+ }
+ return true;
+ }
+ // No *, so a single regular file is intended
+ if (IsRegularFile(pattern)) {
+ list->push_back(pattern);
+ }
+ return true;
+#endif // HAS_GLOB
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/benchmark/benchmark_file_io.h b/media/libjxl/src/tools/benchmark/benchmark_file_io.h
new file mode 100644
index 0000000000..ecb83590d6
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_file_io.h
@@ -0,0 +1,53 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// File utilities for benchmarking and testing, but which are not needed for
+// main jxl itself.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_FILE_IO_H_
+#define TOOLS_BENCHMARK_BENCHMARK_FILE_IO_H_
+
+#include <string>
+#include <vector>
+
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+// Checks if the file exists, either as file or as directory
+bool PathExists(const std::string& fname);
+
+// Checks if the file exists and is a regular file.
+bool IsRegularFile(const std::string& fname);
+
+// Checks if the file exists and is a directory.
+bool IsDirectory(const std::string& fname);
+
+// Recursively makes dir, or successfully does nothing if it already exists.
+Status MakeDir(const std::string& dirname);
+
+// Deletes a single regular file.
+Status DeleteFile(const std::string& fname);
+
+// Returns value similar to unix basename, except it returns empty string if
+// fname ends in '/'.
+std::string FileBaseName(const std::string& fname);
+// Returns value similar to unix dirname, except returns up to before the last
+// slash if fname ends in '/'.
+std::string FileDirName(const std::string& fname);
+
+// Returns the part of the filename starting from the last dot, or empty
+// string if there is no dot.
+std::string FileExtension(const std::string& fname);
+
+// Matches one or more files given glob pattern.
+Status MatchFiles(const std::string& pattern, std::vector<std::string>* list);
+
+std::string JoinPath(const std::string& first, const std::string& second);
+
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_FILE_IO_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_stats.cc b/media/libjxl/src/tools/benchmark/benchmark_stats.cc
new file mode 100644
index 0000000000..ef5932aa9d
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_stats.cc
@@ -0,0 +1,369 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/benchmark/benchmark_stats.h"
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <algorithm>
+#include <cmath>
+
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/status.h"
+#include "tools/benchmark/benchmark_args.h"
+
+namespace jxl {
+namespace {
+
+// Computes longest codec name from Args()->codec, for table alignment.
+uint32_t ComputeLargestCodecName() {
+ std::vector<std::string> methods = SplitString(Args()->codec, ',');
+ size_t max = strlen("Aggregate:"); // Include final row's name
+ for (const auto& method : methods) {
+ max = std::max(max, method.size());
+ }
+ return max;
+}
+
+// The benchmark result is a table of heterogeneous data, the column type
+// specifies its data type. The type affects how it is printed as well as how
+// aggregate values are computed.
+enum ColumnType {
+ // Formatted string
+ TYPE_STRING,
+ // Positive size, prints 0 as "---"
+ TYPE_SIZE,
+ // Floating point value (double precision) which is interpreted as
+ // "not applicable" if <= 0, must be strictly positive to be valid but can be
+ // set to 0 or negative to be printed as "---", for example for a speed that
+ // is not measured.
+ TYPE_POSITIVE_FLOAT,
+ // Counts of some event
+ TYPE_COUNT,
+};
+
+struct ColumnDescriptor {
+ // Column name
+ std::string label;
+ // Total width to render the values of this column. If t his is a floating
+ // point value, make sure this is large enough to contain a space and the
+ // point, plus precision digits after the point, plus the max amount of
+ // integer digits you expect in front of the point.
+ uint32_t width;
+ // Amount of digits after the point, or 0 if not a floating point value.
+ uint32_t precision;
+ ColumnType type;
+ bool more; // Whether to print only if more_columns is enabled
+};
+
+static const ColumnDescriptor ExtraMetricDescriptor() {
+ ColumnDescriptor d{{"DO NOT USE"}, 12, 4, TYPE_POSITIVE_FLOAT, false};
+ return d;
+}
+
+// To add or change a column to the benchmark ASCII table output, add/change
+// an entry here with table header line 1, table header line 2, width of the
+// column, precision after the point in case of floating point, and the
+// data type. Then add/change the corresponding formula or formatting in
+// the function ComputeColumns.
+std::vector<ColumnDescriptor> GetColumnDescriptors(size_t num_extra_metrics) {
+ // clang-format off
+ std::vector<ColumnDescriptor> result = {
+ {{"Encoding"}, ComputeLargestCodecName() + 1, 0, TYPE_STRING, false},
+ {{"kPixels"}, 10, 0, TYPE_SIZE, false},
+ {{"Bytes"}, 9, 0, TYPE_SIZE, false},
+ {{"BPP"}, 13, 7, TYPE_POSITIVE_FLOAT, false},
+ {{"E MP/s"}, 8, 3, TYPE_POSITIVE_FLOAT, false},
+ {{"D MP/s"}, 8, 3, TYPE_POSITIVE_FLOAT, false},
+ {{"Max norm"}, 13, 8, TYPE_POSITIVE_FLOAT, false},
+ {{"pnorm"}, 13, 8, TYPE_POSITIVE_FLOAT, false},
+ {{"PSNR"}, 7, 2, TYPE_POSITIVE_FLOAT, true},
+ {{"QABPP"}, 8, 3, TYPE_POSITIVE_FLOAT, true},
+ {{"SmallB"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"DCT4x8"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"AFV"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"DCT8x8"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"8x16"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"8x32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"16"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"16x32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"32x64"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"64"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"BPP*pnorm"}, 16, 12, TYPE_POSITIVE_FLOAT, false},
+ {{"Bugs"}, 7, 5, TYPE_COUNT, false},
+ };
+ // clang-format on
+
+ for (size_t i = 0; i < num_extra_metrics; i++) {
+ result.push_back(ExtraMetricDescriptor());
+ }
+
+ return result;
+}
+
+// Computes throughput [megapixels/s] as reported in the report table
+static double ComputeSpeed(size_t pixels, double time_s) {
+ if (time_s == 0.0) return 0;
+ return pixels * 1E-6 / time_s;
+}
+
+static std::string FormatFloat(const ColumnDescriptor& label, double value) {
+ std::string result =
+ StringPrintf("%*.*f", label.width - 1, label.precision, value);
+
+ // Reduce precision if the value is too wide for the column. However, keep
+ // at least one digit to the right of the point, and especially the integer
+ // digits.
+ if (result.size() >= label.width) {
+ size_t point = result.rfind('.');
+ if (point != std::string::npos) {
+ int end = std::max<int>(point + 2, label.width - 1);
+ result = result.substr(0, end);
+ }
+ }
+ return result;
+}
+
+} // namespace
+
+std::string StringPrintf(const char* format, ...) {
+ char buf[2000];
+ va_list args;
+ va_start(args, format);
+ vsnprintf(buf, sizeof(buf), format, args);
+ va_end(args);
+ return std::string(buf);
+}
+
+void BenchmarkStats::Assimilate(const BenchmarkStats& victim) {
+ total_input_files += victim.total_input_files;
+ total_input_pixels += victim.total_input_pixels;
+ total_compressed_size += victim.total_compressed_size;
+ total_adj_compressed_size += victim.total_adj_compressed_size;
+ total_time_encode += victim.total_time_encode;
+ total_time_decode += victim.total_time_decode;
+ max_distance = std::max(max_distance, victim.max_distance);
+ distance_p_norm += victim.distance_p_norm;
+ distance_2 += victim.distance_2;
+ distances.insert(distances.end(), victim.distances.begin(),
+ victim.distances.end());
+ total_errors += victim.total_errors;
+ jxl_stats.Assimilate(victim.jxl_stats);
+ if (extra_metrics.size() < victim.extra_metrics.size()) {
+ extra_metrics.resize(victim.extra_metrics.size());
+ }
+ for (size_t i = 0; i < victim.extra_metrics.size(); i++) {
+ extra_metrics[i] += victim.extra_metrics[i];
+ }
+}
+
+void BenchmarkStats::PrintMoreStats() const {
+ if (Args()->print_more_stats) {
+ jxl_stats.Print();
+ }
+ if (Args()->print_distance_percentiles) {
+ std::vector<float> sorted = distances;
+ std::sort(sorted.begin(), sorted.end());
+ int p50idx = 0.5 * distances.size();
+ int p90idx = 0.9 * distances.size();
+ printf("50th/90th percentile distance: %.8f %.8f\n", sorted[p50idx],
+ sorted[p90idx]);
+ }
+}
+
+std::vector<ColumnValue> BenchmarkStats::ComputeColumns(
+ const std::string& codec_desc, size_t corpus_size) const {
+ JXL_CHECK(total_input_files == corpus_size);
+ const double comp_bpp = total_compressed_size * 8.0 / total_input_pixels;
+ const double adj_comp_bpp =
+ total_adj_compressed_size * 8.0 / total_input_pixels;
+ // Note: this is not affected by alpha nor bit depth.
+ const double compression_speed =
+ ComputeSpeed(total_input_pixels, total_time_encode);
+ const double decompression_speed =
+ ComputeSpeed(total_input_pixels, total_time_decode);
+ // Already weighted, no need to divide by #channels.
+ const double rmse = std::sqrt(distance_2 / total_input_pixels);
+ const double psnr = total_compressed_size == 0 ? 0.0
+ : (distance_2 == 0) ? 99.99
+ : (20 * std::log10(1 / rmse));
+ const double p_norm = distance_p_norm / total_input_pixels;
+ const double bpp_p_norm = p_norm * comp_bpp;
+
+ std::vector<ColumnValue> values(
+ GetColumnDescriptors(extra_metrics.size()).size());
+
+ values[0].s = codec_desc;
+ values[1].i = total_input_pixels / 1000;
+ values[2].i = total_compressed_size;
+ values[3].f = comp_bpp;
+ values[4].f = compression_speed;
+ values[5].f = decompression_speed;
+ values[6].f = static_cast<double>(max_distance);
+ values[7].f = p_norm;
+ values[8].f = psnr;
+ values[9].f = adj_comp_bpp;
+ // The DCT2, DCT4, AFV and DCT4X8 are applied to an 8x8 block by having 4x4
+ // DCT2X2s, 2x2 DCT4x4s/AFVs, or 2x1 DCT4X8s, filling the whole 8x8 blocks.
+ // Thus we need to multiply the block count by 8.0 * 8.0 pixels for these
+ // transforms.
+ values[10].f = 100.f * jxl_stats.aux_out.num_small_blocks * 8.0 * 8.0 /
+ total_input_pixels;
+ values[11].f = 100.f * jxl_stats.aux_out.num_dct4x8_blocks * 8.0 * 8.0 /
+ total_input_pixels;
+ values[12].f =
+ 100.f * jxl_stats.aux_out.num_afv_blocks * 8.0 * 8.0 / total_input_pixels;
+ values[13].f = 100.f * jxl_stats.aux_out.num_dct8_blocks * 8.0 * 8.0 /
+ total_input_pixels;
+ values[14].f = 100.f * jxl_stats.aux_out.num_dct8x16_blocks * 8.0 * 16.0 /
+ total_input_pixels;
+ values[15].f = 100.f * jxl_stats.aux_out.num_dct8x32_blocks * 8.0 * 32.0 /
+ total_input_pixels;
+ values[16].f = 100.f * jxl_stats.aux_out.num_dct16_blocks * 16.0 * 16.0 /
+ total_input_pixels;
+ values[17].f = 100.f * jxl_stats.aux_out.num_dct16x32_blocks * 16.0 * 32.0 /
+ total_input_pixels;
+ values[18].f = 100.f * jxl_stats.aux_out.num_dct32_blocks * 32.0 * 32.0 /
+ total_input_pixels;
+ values[19].f = 100.f * jxl_stats.aux_out.num_dct32x64_blocks * 32.0 * 64.0 /
+ total_input_pixels;
+ values[20].f = 100.f * jxl_stats.aux_out.num_dct64_blocks * 64.0 * 64.0 /
+ total_input_pixels;
+ values[21].f = bpp_p_norm;
+ values[22].i = total_errors;
+ for (size_t i = 0; i < extra_metrics.size(); i++) {
+ values[23 + i].f = extra_metrics[i] / total_input_files;
+ }
+ return values;
+}
+
+static std::string PrintFormattedEntries(
+ size_t num_extra_metrics, const std::vector<ColumnValue>& values) {
+ const auto& descriptors = GetColumnDescriptors(num_extra_metrics);
+
+ std::string out;
+ for (size_t i = 0; i < descriptors.size(); i++) {
+ if (!Args()->more_columns && descriptors[i].more) continue;
+ std::string value;
+ if (descriptors[i].type == TYPE_STRING) {
+ value = values[i].s;
+ } else if (descriptors[i].type == TYPE_SIZE) {
+ value = values[i].i ? StringPrintf("%" PRIdS, values[i].i) : "---";
+ } else if (descriptors[i].type == TYPE_POSITIVE_FLOAT) {
+ value = FormatFloat(descriptors[i], values[i].f);
+ value = FormatFloat(descriptors[i], values[i].f);
+ } else if (descriptors[i].type == TYPE_COUNT) {
+ value = StringPrintf("%" PRIdS, values[i].i);
+ }
+
+ int numspaces = descriptors[i].width - value.size();
+ if (numspaces < 1) {
+ numspaces = 1;
+ }
+ // All except the first one are right-aligned, the first one is the name,
+ // others are numbers with digits matching from the right.
+ if (i == 0) out += value.c_str();
+ out += std::string(numspaces, ' ');
+ if (i != 0) out += value.c_str();
+ }
+ return out + "\n";
+}
+
+std::string BenchmarkStats::PrintLine(const std::string& codec_desc,
+ size_t corpus_size) const {
+ std::vector<ColumnValue> values = ComputeColumns(codec_desc, corpus_size);
+ return PrintFormattedEntries(extra_metrics.size(), values);
+}
+
+std::string PrintHeader(const std::vector<std::string>& extra_metrics_names) {
+ std::string out;
+ // Extra metrics are handled separately.
+ const auto& descriptors = GetColumnDescriptors(0);
+ for (size_t i = 0; i < descriptors.size(); i++) {
+ if (!Args()->more_columns && descriptors[i].more) continue;
+ const std::string& label = descriptors[i].label;
+ int numspaces = descriptors[i].width - label.size();
+ // All except the first one are right-aligned.
+ if (i == 0) out += label.c_str();
+ out += std::string(numspaces, ' ');
+ if (i != 0) out += label.c_str();
+ }
+ for (const std::string& em : extra_metrics_names) {
+ int numspaces = ExtraMetricDescriptor().width - em.size();
+ JXL_CHECK(numspaces >= 1);
+ out += std::string(numspaces, ' ');
+ out += em;
+ }
+ out += '\n';
+ for (const auto& descriptor : descriptors) {
+ if (!Args()->more_columns && descriptor.more) continue;
+ out += std::string(descriptor.width, '-');
+ }
+ out += std::string(ExtraMetricDescriptor().width * extra_metrics_names.size(),
+ '-');
+ return out + "\n";
+}
+
+std::string PrintAggregate(
+ size_t num_extra_metrics,
+ const std::vector<std::vector<ColumnValue>>& aggregate) {
+ const auto& descriptors = GetColumnDescriptors(num_extra_metrics);
+
+ for (size_t i = 0; i < aggregate.size(); i++) {
+ // Check when statistics has wrong amount of column entries
+ JXL_CHECK(aggregate[i].size() == descriptors.size());
+ }
+
+ std::vector<ColumnValue> result(descriptors.size());
+
+ // Statistics for the aggregate row are combined together with different
+ // formulas than Assimilate uses for combining the statistics of files.
+ for (size_t i = 0; i < descriptors.size(); i++) {
+ if (descriptors[i].type == TYPE_STRING) {
+ // "---" for the Iters column since this does not have meaning for
+ // the aggregate stats.
+ result[i].s = i == 0 ? "Aggregate:" : "---";
+ continue;
+ }
+ if (descriptors[i].type == TYPE_COUNT) {
+ size_t sum = 0;
+ for (size_t j = 0; j < aggregate.size(); j++) {
+ sum += aggregate[j][i].i;
+ }
+ result[i].i = sum;
+ continue;
+ }
+
+ ColumnType type = descriptors[i].type;
+
+ double logsum = 0;
+ size_t numvalid = 0;
+ for (size_t j = 0; j < aggregate.size(); j++) {
+ double value =
+ (type == TYPE_SIZE) ? aggregate[j][i].i : aggregate[j][i].f;
+ if (value > 0) {
+ numvalid++;
+ logsum += std::log2(value);
+ }
+ }
+ double geomean = numvalid ? std::exp2(logsum / numvalid) : 0.0;
+
+ if (type == TYPE_SIZE || type == TYPE_COUNT) {
+ result[i].i = static_cast<size_t>(geomean + 0.5);
+ } else if (type == TYPE_POSITIVE_FLOAT) {
+ result[i].f = geomean;
+ } else {
+ JXL_ABORT("unknown entry type");
+ }
+ }
+
+ return PrintFormattedEntries(num_extra_metrics, result);
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/benchmark/benchmark_stats.h b/media/libjxl/src/tools/benchmark/benchmark_stats.h
new file mode 100644
index 0000000000..a23c4a1aeb
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_stats.h
@@ -0,0 +1,81 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_STATS_H_
+#define TOOLS_BENCHMARK_BENCHMARK_STATS_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/jxl/aux_out.h"
+
+namespace jxl {
+
+std::string StringPrintf(const char* format, ...);
+
+struct JxlStats {
+ JxlStats() {
+ num_inputs = 0;
+ aux_out = AuxOut();
+ }
+ void Assimilate(const JxlStats& victim) {
+ num_inputs += victim.num_inputs;
+ aux_out.Assimilate(victim.aux_out);
+ }
+ void Print() const { aux_out.Print(num_inputs); }
+
+ size_t num_inputs;
+ AuxOut aux_out;
+};
+
+// The value of an entry in the table. Depending on the ColumnType, the string,
+// size_t or double should be used.
+struct ColumnValue {
+ std::string s; // for TYPE_STRING
+ size_t i; // for TYPE_SIZE and TYPE_COUNT
+ double f; // for TYPE_POSITIVE_FLOAT
+};
+
+struct BenchmarkStats {
+ void Assimilate(const BenchmarkStats& victim);
+
+ std::vector<ColumnValue> ComputeColumns(const std::string& codec_desc,
+ size_t corpus_size) const;
+
+ std::string PrintLine(const std::string& codec_desc,
+ size_t corpus_size) const;
+
+ void PrintMoreStats() const;
+
+ size_t total_input_files = 0;
+ size_t total_input_pixels = 0;
+ size_t total_compressed_size = 0;
+ size_t total_adj_compressed_size = 0;
+ double total_time_encode = 0.0;
+ double total_time_decode = 0.0;
+ float max_distance = -1.0; // Max butteraugli score
+ // sum of 8th powers of butteraugli distmap pixels.
+ double distance_p_norm = 0.0;
+ // sum of 2nd powers of differences between R, G, B.
+ double distance_2 = 0.0;
+ std::vector<float> distances;
+ size_t total_errors = 0;
+ JxlStats jxl_stats;
+ std::vector<float> extra_metrics;
+};
+
+std::string PrintHeader(const std::vector<std::string>& extra_metrics_names);
+
+// Given the rows of all printed statistics, print an aggregate row.
+std::string PrintAggregate(
+ size_t num_extra_metrics,
+ const std::vector<std::vector<ColumnValue>>& aggregate);
+
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_STATS_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_utils.cc b/media/libjxl/src/tools/benchmark/benchmark_utils.cc
new file mode 100644
index 0000000000..4b531317e6
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_utils.cc
@@ -0,0 +1,90 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#define _DEFAULT_SOURCE // for mkstemps().
+
+#include "tools/benchmark/benchmark_utils.h"
+
+// Not supported on Windows due to Linux-specific functions.
+// Not supported in Android NDK before API 28.
+#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && \
+ (!defined(__ANDROID_API__) || __ANDROID_API__ >= 28)
+
+#include <libgen.h>
+#include <spawn.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <fstream>
+
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/image_bundle.h"
+
+extern char** environ;
+
+namespace jxl {
+TemporaryFile::TemporaryFile(std::string basename, std::string extension) {
+ const auto extension_size = 1 + extension.size();
+ temp_filename_ = std::move(basename) + "_XXXXXX." + std::move(extension);
+ const int fd = mkstemps(&temp_filename_[0], extension_size);
+ if (fd == -1) {
+ ok_ = false;
+ return;
+ }
+ close(fd);
+}
+TemporaryFile::~TemporaryFile() {
+ if (ok_) {
+ unlink(temp_filename_.c_str());
+ }
+}
+
+Status TemporaryFile::GetFileName(std::string* const output) const {
+ JXL_RETURN_IF_ERROR(ok_);
+ *output = temp_filename_;
+ return true;
+}
+
+Status RunCommand(const std::string& command,
+ const std::vector<std::string>& arguments) {
+ std::vector<char*> args;
+ args.reserve(arguments.size() + 2);
+ args.push_back(const_cast<char*>(command.c_str()));
+ for (const std::string& argument : arguments) {
+ args.push_back(const_cast<char*>(argument.c_str()));
+ }
+ args.push_back(nullptr);
+ pid_t pid;
+ JXL_RETURN_IF_ERROR(posix_spawnp(&pid, command.c_str(), nullptr, nullptr,
+ args.data(), environ) == 0);
+ int wstatus;
+ waitpid(pid, &wstatus, 0);
+ return WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == EXIT_SUCCESS;
+}
+
+} // namespace jxl
+
+#else
+
+namespace jxl {
+
+TemporaryFile::TemporaryFile(std::string basename, std::string extension) {}
+TemporaryFile::~TemporaryFile() {}
+Status TemporaryFile::GetFileName(std::string* const output) const {
+ (void)ok_;
+ return JXL_FAILURE("Not supported on this build");
+}
+
+Status RunCommand(const std::string& command,
+ const std::vector<std::string>& arguments) {
+ return JXL_FAILURE("Not supported on this build");
+}
+
+} // namespace jxl
+
+#endif // _MSC_VER
diff --git a/media/libjxl/src/tools/benchmark/benchmark_utils.h b/media/libjxl/src/tools/benchmark/benchmark_utils.h
new file mode 100644
index 0000000000..027fa0868f
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_utils.h
@@ -0,0 +1,35 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_BENCHMARK_BENCHMARK_UTILS_H_
+#define TOOLS_BENCHMARK_BENCHMARK_UTILS_H_
+
+#include <string>
+#include <vector>
+
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+class TemporaryFile final {
+ public:
+ explicit TemporaryFile(std::string basename, std::string extension);
+ TemporaryFile(const TemporaryFile&) = delete;
+ TemporaryFile& operator=(const TemporaryFile&) = delete;
+ ~TemporaryFile();
+ Status GetFileName(std::string* output) const;
+
+ private:
+ bool ok_ = true;
+
+ std::string temp_filename_;
+};
+
+Status RunCommand(const std::string& command,
+ const std::vector<std::string>& arguments);
+
+} // namespace jxl
+
+#endif // TOOLS_BENCHMARK_BENCHMARK_UTILS_H_
diff --git a/media/libjxl/src/tools/benchmark/benchmark_xl.cc b/media/libjxl/src/tools/benchmark/benchmark_xl.cc
new file mode 100644
index 0000000000..e91fbb8c5a
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_xl.cc
@@ -0,0 +1,1084 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "jxl/decode.h"
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/time.h"
+#include "lib/jxl/alpha.h"
+#include "lib/jxl/base/cache_aligned.h"
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/profiler.h"
+#include "lib/jxl/base/random.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/enc_butteraugli_comparator.h"
+#include "lib/jxl/enc_butteraugli_pnorm.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_ops.h"
+#include "lib/jxl/jpeg/enc_jpeg_data.h"
+#include "tools/benchmark/benchmark_args.h"
+#include "tools/benchmark/benchmark_codec.h"
+#include "tools/benchmark/benchmark_file_io.h"
+#include "tools/benchmark/benchmark_stats.h"
+#include "tools/benchmark/benchmark_utils.h"
+#include "tools/codec_config.h"
+#include "tools/speed_stats.h"
+
+namespace jxl {
+namespace {
+
+Status WriteImage(Image3F&& image, ThreadPool* pool,
+ const std::string& filename) {
+ CodecInOut io;
+ io.metadata.m.SetUintSamples(8);
+ io.metadata.m.color_encoding = ColorEncoding::SRGB();
+ io.SetFromImage(std::move(image), io.metadata.m.color_encoding);
+ return EncodeToFile(io, filename, pool);
+}
+
+Status ReadPNG(const std::string& filename, Image3F* image) {
+ CodecInOut io;
+ JXL_CHECK(SetFromFile(filename, extras::ColorHints(), &io));
+ *image = CopyImage(*io.Main().color());
+ return true;
+}
+
+void DoCompress(const std::string& filename, const CodecInOut& io,
+ const std::vector<std::string>& extra_metrics_commands,
+ ImageCodec* codec, ThreadPoolInternal* inner_pool,
+ PaddedBytes* compressed, BenchmarkStats* s) {
+ PROFILER_FUNC;
+ ++s->total_input_files;
+
+ if (io.frames.size() != 1) {
+ // Multiple frames not supported (io.xsize() will checkfail)
+ s->total_errors++;
+ if (!Args()->silent_errors) {
+ JXL_WARNING("multiframe input image not supported %s", filename.c_str());
+ }
+ return;
+ }
+ const size_t xsize = io.xsize();
+ const size_t ysize = io.ysize();
+ const size_t input_pixels = xsize * ysize;
+
+ jpegxl::tools::SpeedStats speed_stats;
+ jpegxl::tools::SpeedStats::Summary summary;
+
+ bool valid = true; // false if roundtrip, encoding or decoding errors occur.
+
+ if (!Args()->decode_only && (io.xsize() == 0 || io.ysize() == 0)) {
+ // This means the benchmark couldn't load the image, e.g. due to invalid
+ // ICC profile. Warning message about that was already printed. Continue
+ // this function to indicate it as error in the stats.
+ valid = false;
+ }
+
+ std::string ext = FileExtension(filename);
+ if (valid && !Args()->decode_only) {
+ for (size_t i = 0; i < Args()->encode_reps; ++i) {
+ if (codec->CanRecompressJpeg() && (ext == ".jpg" || ext == ".jpeg")) {
+ std::string data_in;
+ JXL_CHECK(ReadFile(filename, &data_in));
+ JXL_CHECK(
+ codec->RecompressJpeg(filename, data_in, compressed, &speed_stats));
+ } else {
+ Status status = codec->Compress(filename, &io, inner_pool, compressed,
+ &speed_stats);
+ if (!status) {
+ valid = false;
+ if (!Args()->silent_errors) {
+ std::string message = codec->GetErrorMessage();
+ if (!message.empty()) {
+ fprintf(stderr, "Error in %s codec: %s\n",
+ codec->description().c_str(), message.c_str());
+ } else {
+ fprintf(stderr, "Error in %s codec\n",
+ codec->description().c_str());
+ }
+ }
+ }
+ }
+ }
+ JXL_CHECK(speed_stats.GetSummary(&summary));
+ s->total_time_encode += summary.central_tendency;
+ }
+
+ if (valid && Args()->decode_only) {
+ std::string data_in;
+ JXL_CHECK(ReadFile(filename, &data_in));
+ compressed->append((uint8_t*)data_in.data(),
+ (uint8_t*)data_in.data() + data_in.size());
+ }
+
+ // Decompress
+ CodecInOut io2;
+ io2.metadata.m = io.metadata.m;
+ if (valid) {
+ speed_stats = jpegxl::tools::SpeedStats();
+ for (size_t i = 0; i < Args()->decode_reps; ++i) {
+ if (!codec->Decompress(filename, Span<const uint8_t>(*compressed),
+ inner_pool, &io2, &speed_stats)) {
+ if (!Args()->silent_errors) {
+ fprintf(stderr,
+ "%s failed to decompress encoded image. Original source:"
+ " %s\n",
+ codec->description().c_str(), filename.c_str());
+ }
+ valid = false;
+ }
+
+ // io2.dec_pixels increases each time, but the total should be independent
+ // of decode_reps, so only take the value from the first iteration.
+ if (i == 0) s->total_input_pixels += io2.dec_pixels;
+ }
+ JXL_CHECK(speed_stats.GetSummary(&summary));
+ s->total_time_decode += summary.central_tendency;
+ }
+
+ std::string name = FileBaseName(filename);
+ std::string codec_name = codec->description();
+
+ if (!valid) {
+ s->total_errors++;
+ }
+
+ if (io.frames.size() != io2.frames.size()) {
+ if (!Args()->silent_errors) {
+ // Animated gifs not supported yet?
+ fprintf(stderr,
+ "Frame sizes not equal, is this an animated gif? %s %s %" PRIuS
+ " %" PRIuS "\n",
+ codec_name.c_str(), name.c_str(), io.frames.size(),
+ io2.frames.size());
+ }
+ valid = false;
+ }
+
+ bool lossless = codec->IsJpegTranscoder();
+ bool skip_butteraugli =
+ Args()->skip_butteraugli || Args()->decode_only || lossless;
+ ImageF distmap;
+ float max_distance = 1.0f;
+
+ if (valid && !skip_butteraugli) {
+ JXL_ASSERT(io.frames.size() == io2.frames.size());
+ for (size_t i = 0; i < io.frames.size(); i++) {
+ const ImageBundle& ib1 = io.frames[i];
+ ImageBundle& ib2 = io2.frames[i];
+
+ // Verify output
+ PROFILER_ZONE("Benchmark stats");
+ float distance;
+ if (SameSize(ib1, ib2)) {
+ ButteraugliParams params = codec->BaParams();
+ if (ib1.metadata()->IntensityTarget() !=
+ ib2.metadata()->IntensityTarget()) {
+ fprintf(stderr,
+ "WARNING: input and output images have different intensity "
+ "targets");
+ }
+ params.intensity_target = ib1.metadata()->IntensityTarget();
+ // Hack the default intensity target value to be 80.0, the intensity
+ // target of sRGB images and a more reasonable viewing default than
+ // JPEG XL file format's default.
+ if (fabs(params.intensity_target - 255.0f) < 1e-3) {
+ params.intensity_target = 80.0;
+ }
+ distance = ButteraugliDistance(ib1, ib2, params, GetJxlCms(), &distmap,
+ inner_pool);
+ // Ensure pixels in range 0-1
+ s->distance_2 += ComputeDistance2(ib1, ib2, GetJxlCms());
+ } else {
+ // TODO(veluca): re-upsample and compute proper distance.
+ distance = 1e+4f;
+ distmap = ImageF(1, 1);
+ distmap.Row(0)[0] = distance;
+ s->distance_2 += distance;
+ }
+ // Update stats
+ s->distance_p_norm +=
+ ComputeDistanceP(distmap, Args()->ba_params, Args()->error_pnorm) *
+ input_pixels;
+ s->max_distance = std::max(s->max_distance, distance);
+ s->distances.push_back(distance);
+ max_distance = std::max(max_distance, distance);
+ }
+ }
+
+ s->total_compressed_size += compressed->size();
+ s->total_adj_compressed_size += compressed->size() * max_distance;
+ codec->GetMoreStats(s);
+
+ if (io2.frames.size() == 1 &&
+ (Args()->save_compressed || Args()->save_decompressed)) {
+ JXL_ASSERT(io2.frames.size() == 1);
+ ImageBundle& ib2 = io2.Main();
+
+ // By default the benchmark will save the image after roundtrip with the
+ // same color encoding as the image before roundtrip. Not all codecs
+ // necessarily preserve the amount of channels (1 for gray, 3 for RGB)
+ // though, since not all image formats necessarily allow a way to remember
+ // what amount of channels you happened to give the benchmark codec
+ // input (say, an RGB-only format) and that is fine since in the end what
+ // matters is that the pixels look the same on a 3-channel RGB monitor
+ // while using grayscale encoding is an internal compression optimization.
+ // If that is the case, output with the current color model instead,
+ // because CodecInOut does not automatically convert between 1 or 3
+ // channels, and giving a ColorEncoding with a different amount of
+ // channels is not allowed.
+ const ColorEncoding* c_desired =
+ (ib2.metadata()->color_encoding.Channels() ==
+ ib2.c_current().Channels())
+ ? &ib2.metadata()->color_encoding
+ : &ib2.c_current();
+ // Allow overriding via --output_encoding.
+ if (!Args()->output_description.empty()) {
+ c_desired = &Args()->output_encoding;
+ }
+
+ std::string dir = FileDirName(filename);
+ std::string outdir =
+ Args()->output_dir.empty() ? dir + "/out" : Args()->output_dir;
+ // Make compatible for filename
+ std::replace(codec_name.begin(), codec_name.end(), ':', '_');
+ std::string compressed_fn = outdir + "/" + name + "." + codec_name;
+ std::string decompressed_fn = compressed_fn + Args()->output_extension;
+#if JPEGXL_ENABLE_APNG
+ std::string heatmap_fn = compressed_fn + ".heatmap.png";
+#else
+ std::string heatmap_fn = compressed_fn + ".heatmap.ppm";
+#endif
+ JXL_CHECK(MakeDir(outdir));
+ if (Args()->save_compressed) {
+ std::string compressed_str(
+ reinterpret_cast<const char*>(compressed->data()),
+ compressed->size());
+ JXL_CHECK(WriteFile(compressed_str, compressed_fn));
+ }
+ if (Args()->save_decompressed && valid) {
+ // For verifying HDR: scale output.
+ if (Args()->mul_output != 0.0) {
+ fprintf(stderr, "WARNING: scaling outputs by %f\n", Args()->mul_output);
+ JXL_CHECK(ib2.TransformTo(ColorEncoding::LinearSRGB(ib2.IsGray()),
+ GetJxlCms(), inner_pool));
+ ScaleImage(static_cast<float>(Args()->mul_output), ib2.color());
+ }
+
+ JXL_CHECK(EncodeToFile(io2, *c_desired,
+ ib2.metadata()->bit_depth.bits_per_sample,
+ decompressed_fn));
+ if (!skip_butteraugli) {
+ float good = Args()->heatmap_good > 0.0f ? Args()->heatmap_good
+ : ButteraugliFuzzyInverse(1.5);
+ float bad = Args()->heatmap_bad > 0.0f ? Args()->heatmap_bad
+ : ButteraugliFuzzyInverse(0.5);
+ JXL_CHECK(WriteImage(CreateHeatMapImage(distmap, good, bad), inner_pool,
+ heatmap_fn));
+ }
+ }
+ }
+ if (!extra_metrics_commands.empty()) {
+ CodecInOut in_copy;
+ in_copy.SetFromImage(std::move(*io.Main().Copy().color()),
+ io.Main().c_current());
+ TemporaryFile tmp_in("original", "pfm");
+ TemporaryFile tmp_out("decoded", "pfm");
+ TemporaryFile tmp_res("result", "txt");
+ std::string tmp_in_fn, tmp_out_fn, tmp_res_fn;
+ JXL_CHECK(tmp_in.GetFileName(&tmp_in_fn));
+ JXL_CHECK(tmp_out.GetFileName(&tmp_out_fn));
+ JXL_CHECK(tmp_res.GetFileName(&tmp_res_fn));
+
+ // Convert everything to non-linear SRGB - this is what most metrics expect.
+ const ColorEncoding& c_desired = ColorEncoding::SRGB(io.Main().IsGray());
+ JXL_CHECK(EncodeToFile(io, c_desired,
+ io.metadata.m.bit_depth.bits_per_sample, tmp_in_fn));
+ JXL_CHECK(EncodeToFile(
+ io2, c_desired, io.metadata.m.bit_depth.bits_per_sample, tmp_out_fn));
+ if (io.metadata.m.IntensityTarget() != io2.metadata.m.IntensityTarget()) {
+ fprintf(stderr,
+ "WARNING: original and decoded have different intensity targets "
+ "(%f vs. %f).\n",
+ io.metadata.m.IntensityTarget(),
+ io2.metadata.m.IntensityTarget());
+ }
+ std::string intensity_target;
+ {
+ std::ostringstream intensity_target_oss;
+ intensity_target_oss << io.metadata.m.IntensityTarget();
+ intensity_target = intensity_target_oss.str();
+ }
+ for (size_t i = 0; i < extra_metrics_commands.size(); i++) {
+ float res = nanf("");
+ bool error = false;
+ if (RunCommand(extra_metrics_commands[i],
+ {tmp_in_fn, tmp_out_fn, tmp_res_fn, intensity_target})) {
+ FILE* f = fopen(tmp_res_fn.c_str(), "r");
+ if (fscanf(f, "%f", &res) != 1) {
+ error = true;
+ }
+ fclose(f);
+ } else {
+ error = true;
+ }
+ if (error) {
+ fprintf(stderr,
+ "WARNING: Computation of metric with command %s failed\n",
+ extra_metrics_commands[i].c_str());
+ }
+ s->extra_metrics.push_back(res);
+ }
+ }
+
+ if (Args()->show_progress) {
+ fprintf(stderr, ".");
+ fflush(stderr);
+ }
+}
+
+// Makes a base64 data URI for embedded image in HTML
+std::string Base64Image(const std::string& filename) {
+ PaddedBytes bytes;
+ if (!ReadFile(filename, &bytes)) {
+ return "";
+ }
+ static const char* symbols =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ std::string result;
+ for (size_t i = 0; i < bytes.size(); i += 3) {
+ uint8_t o0 = bytes[i + 0];
+ uint8_t o1 = (i + 1 < bytes.size()) ? bytes[i + 1] : 0;
+ uint8_t o2 = (i + 2 < bytes.size()) ? bytes[i + 2] : 0;
+ uint32_t value = (o0 << 16) | (o1 << 8) | o2;
+ for (size_t j = 0; j < 4; j++) {
+ result += (i + j <= bytes.size()) ? symbols[(value >> (6 * (3 - j))) & 63]
+ : '=';
+ }
+ }
+ // NOTE: Chrome supports max 2MB of data this way for URLs, but appears to
+ // support larger images anyway as long as it's embedded in the HTML file
+ // itself. If more data is needed, use createObjectURL.
+ return "data:image;base64," + result;
+}
+
+struct Task {
+ ImageCodecPtr codec;
+ size_t idx_image;
+ size_t idx_method;
+ const CodecInOut* image;
+ BenchmarkStats stats;
+};
+
+void WriteHtmlReport(const std::string& codec_desc,
+ const std::vector<std::string>& fnames,
+ const std::vector<const Task*>& tasks,
+ const std::vector<const CodecInOut*>& images,
+ bool self_contained) {
+ std::string toggle_js =
+ "<script type=\"text/javascript\">\n"
+ " var codecname = '" +
+ codec_desc + "';\n";
+ toggle_js += R"(
+ var maintitle = codecname + ' - click images to toggle, press space to' +
+ ' toggle all, h to toggle all heatmaps. Zoom in with CTRL+wheel or' +
+ ' CTRL+plus.';
+ document.title = maintitle;
+ var counter = [];
+ function setState(i, s) {
+ var preview = document.getElementById("preview" + i);
+ var orig = document.getElementById("orig" + i);
+ var hm = document.getElementById("hm" + i);
+ if (s == 0) {
+ preview.style.display = 'none';
+ orig.style.display = 'block';
+ hm.style.display = 'none';
+ } else if (s == 1) {
+ preview.style.display = 'block';
+ orig.style.display = 'none';
+ hm.style.display = 'none';
+ } else if (s == 2) {
+ preview.style.display = 'none';
+ orig.style.display = 'none';
+ hm.style.display = 'block';
+ }
+ }
+ function toggle3(i) {
+ for (index = counter.length; index <= i; index++) {
+ counter.push(1);
+ }
+ setState(i, counter[i]);
+ counter[i] = (counter[i] + 1) % 3;
+ document.title = maintitle;
+ }
+ var toggleall_state = 1;
+ document.body.onkeydown = function(e) {
+ // space (32) to toggle orig/compr, 'h' (72) to toggle heatmap/compr
+ if (e.keyCode == 32 || e.keyCode == 72) {
+ var divs = document.getElementsByTagName('div');
+ var key_state = (e.keyCode == 32) ? 0 : 2;
+ toggleall_state = (toggleall_state == key_state) ? 1 : key_state;
+ document.title = codecname + ' - ' + (toggleall_state == 0 ?
+ 'originals' : (toggleall_state == 1 ? 'compressed' : 'heatmaps'));
+ for (var i = 0; i < divs.length; i++) {
+ setState(i, toggleall_state);
+ }
+ return false;
+ }
+ };
+</script>
+)";
+ std::string out_html;
+ std::string outdir;
+ out_html += "<body bgcolor=\"#000\">\n";
+ out_html += "<style>img { image-rendering: pixelated; }</style>\n";
+ std::string codec_name = codec_desc;
+ // Make compatible for filename
+ std::replace(codec_name.begin(), codec_name.end(), ':', '_');
+ for (size_t i = 0; i < fnames.size(); ++i) {
+ std::string name = FileBaseName(fnames[i]);
+ std::string dir = FileDirName(fnames[i]);
+ outdir = Args()->output_dir.empty() ? dir + "/out" : Args()->output_dir;
+ std::string name_out = name + "." + codec_name + Args()->output_extension;
+ std::string heatmap_out = name + "." + codec_name + ".heatmap.png";
+
+ std::string fname_orig = fnames[i];
+ std::string fname_out = outdir + "/" + name_out;
+ std::string fname_heatmap = outdir + "/" + heatmap_out;
+ std::string url_orig = Args()->originals_url.empty()
+ ? ("file://" + fnames[i])
+ : (Args()->originals_url + "/" + name);
+ std::string url_out = name_out;
+ std::string url_heatmap = heatmap_out;
+ if (self_contained) {
+ url_orig = Base64Image(fname_orig);
+ url_out = Base64Image(fname_out);
+ url_heatmap = Base64Image(fname_heatmap);
+ }
+ std::string number = StringPrintf("%" PRIuS, i);
+ const CodecInOut& image = *images[i];
+ size_t xsize = image.frames.size() == 1 ? image.xsize() : 0;
+ size_t ysize = image.frames.size() == 1 ? image.ysize() : 0;
+ std::string html_width = StringPrintf("%" PRIuS "px", xsize);
+ std::string html_height = StringPrintf("%" PRIuS "px", ysize);
+ double bpp = tasks[i]->stats.total_compressed_size * 8.0 /
+ tasks[i]->stats.total_input_pixels;
+ double pnorm =
+ tasks[i]->stats.distance_p_norm / tasks[i]->stats.total_input_pixels;
+ double max_dist = tasks[i]->stats.max_distance;
+ std::string compressed_title = StringPrintf(
+ "compressed. bpp: %f, pnorm: %f, max dist: %f", bpp, pnorm, max_dist);
+ out_html += "<div onclick=\"toggle3(" + number +
+ ");\" style=\"display:inline-block;width:" + html_width +
+ ";height:" + html_height +
+ ";\">\n"
+ " <img title=\"" +
+ compressed_title + "\" id=\"preview" + number + "\" src=";
+ out_html += "\"" + url_out + "\"";
+ out_html +=
+ " style=\"display:block;\"/>\n"
+ " <img title=\"original\" id=\"orig" +
+ number + "\" src=";
+ out_html += "\"" + url_orig + "\"";
+ out_html +=
+ " style=\"display:none;\"/>\n"
+ " <img title=\"heatmap\" id=\"hm" +
+ number + "\" src=";
+ out_html += "\"" + url_heatmap + "\"";
+ out_html += " style=\"display:none;\"/>\n</div>\n";
+ }
+ out_html += "</body>\n";
+ out_html += toggle_js;
+ JXL_CHECK(WriteFile(out_html, outdir + "/index." + codec_name + ".html"));
+}
+
+// Prints the detailed and aggregate statistics, in the correct order but as
+// soon as possible when multithreaded tasks are done.
+struct StatPrinter {
+ StatPrinter(const std::vector<std::string>& methods,
+ const std::vector<std::string>& extra_metrics_names,
+ const std::vector<std::string>& fnames,
+ const std::vector<Task>& tasks)
+ : methods_(&methods),
+ extra_metrics_names_(&extra_metrics_names),
+ fnames_(&fnames),
+ tasks_(&tasks),
+ tasks_done_(0),
+ stats_printed_(0),
+ details_printed_(0) {
+ stats_done_.resize(methods.size(), 0);
+ details_done_.resize(tasks.size(), 0);
+ max_fname_width_ = 0;
+ for (const auto& fname : fnames) {
+ max_fname_width_ = std::max(max_fname_width_, FileBaseName(fname).size());
+ }
+ max_method_width_ = 0;
+ for (const auto& method : methods) {
+ max_method_width_ =
+ std::max(max_method_width_, FileBaseName(method).size());
+ }
+ }
+
+ void TaskDone(size_t task_index, const Task& t) {
+ PROFILER_FUNC;
+ std::lock_guard<std::mutex> guard(mutex);
+ tasks_done_++;
+ if (Args()->print_details || Args()->show_progress) {
+ if (Args()->print_details) {
+ // Render individual results as soon as they are ready and all previous
+ // ones in task order are ready.
+ details_done_[task_index] = 1;
+ if (task_index == details_printed_) {
+ while (details_printed_ < tasks_->size() &&
+ details_done_[details_printed_]) {
+ PrintDetails((*tasks_)[details_printed_]);
+ details_printed_++;
+ }
+ }
+ }
+ // When using "show_progress" or "print_details", the table must be
+ // rendered at the very end, else the details or progress would be
+ // rendered in-between the table rows.
+ if (tasks_done_ == tasks_->size()) {
+ PrintStatsHeader();
+ for (size_t i = 0; i < methods_->size(); i++) {
+ PrintStats((*methods_)[i], i);
+ }
+ PrintStatsFooter();
+ }
+ } else {
+ if (tasks_done_ == 1) {
+ PrintStatsHeader();
+ }
+ // Render lines of the table as soon as it is ready and all previous
+ // lines have been printed.
+ stats_done_[t.idx_method]++;
+ if (stats_done_[t.idx_method] == fnames_->size() &&
+ t.idx_method == stats_printed_) {
+ while (stats_printed_ < stats_done_.size() &&
+ stats_done_[stats_printed_] == fnames_->size()) {
+ PrintStats((*methods_)[stats_printed_], stats_printed_);
+ stats_printed_++;
+ }
+ }
+ if (tasks_done_ == tasks_->size()) {
+ PrintStatsFooter();
+ }
+ }
+ }
+
+ void PrintDetails(const Task& t) {
+ double comp_bpp =
+ t.stats.total_compressed_size * 8.0 / t.stats.total_input_pixels;
+ double p_norm = t.stats.distance_p_norm / t.stats.total_input_pixels;
+ double bpp_p_norm = p_norm * comp_bpp;
+
+ const double adj_comp_bpp =
+ t.stats.total_adj_compressed_size * 8.0 / t.stats.total_input_pixels;
+
+ const double rmse =
+ std::sqrt(t.stats.distance_2 / t.stats.total_input_pixels);
+ const double psnr = t.stats.total_compressed_size == 0 ? 0.0
+ : (t.stats.distance_2 == 0)
+ ? 99.99
+ : (20 * std::log10(1 / rmse));
+ size_t pixels = t.stats.total_input_pixels;
+
+ const double enc_mps =
+ t.stats.total_input_pixels / (1000000.0 * t.stats.total_time_encode);
+ const double dec_mps =
+ t.stats.total_input_pixels / (1000000.0 * t.stats.total_time_decode);
+ if (Args()->print_details_csv) {
+ printf("%s,%s,%" PRIdS ",%" PRIdS ",%" PRIdS
+ ",%.8f,%.8f,%.8f,%.8f,%.8f,%.8f,%.8f,%.8f",
+ (*methods_)[t.idx_method].c_str(),
+ FileBaseName((*fnames_)[t.idx_image]).c_str(),
+ t.stats.total_errors, t.stats.total_compressed_size, pixels,
+ enc_mps, dec_mps, comp_bpp, t.stats.max_distance, psnr, p_norm,
+ bpp_p_norm, adj_comp_bpp);
+ for (float m : t.stats.extra_metrics) {
+ printf(",%.8f", m);
+ }
+ printf("\n");
+ } else {
+ printf("%s", (*methods_)[t.idx_method].c_str());
+ for (size_t i = (*methods_)[t.idx_method].size(); i <= max_method_width_;
+ i++) {
+ printf(" ");
+ }
+ printf("%s", FileBaseName((*fnames_)[t.idx_image]).c_str());
+ for (size_t i = FileBaseName((*fnames_)[t.idx_image]).size();
+ i <= max_fname_width_; i++) {
+ printf(" ");
+ }
+ printf(
+ "error:%" PRIdS " size:%8" PRIdS " pixels:%9" PRIdS
+ " enc_speed:%8.8f dec_speed:%8.8f bpp:%10.8f dist:%10.8f"
+ " psnr:%10.8f p:%10.8f bppp:%10.8f qabpp:%10.8f ",
+ t.stats.total_errors, t.stats.total_compressed_size, pixels, enc_mps,
+ dec_mps, comp_bpp, t.stats.max_distance, psnr, p_norm, bpp_p_norm,
+ adj_comp_bpp);
+ for (size_t i = 0; i < t.stats.extra_metrics.size(); i++) {
+ printf(" %s:%.8f", (*extra_metrics_names_)[i].c_str(),
+ t.stats.extra_metrics[i]);
+ }
+ printf("\n");
+ }
+ fflush(stdout);
+ }
+
+ void PrintStats(const std::string& method, size_t idx_method) {
+ PROFILER_FUNC;
+ // Assimilate all tasks with the same idx_method.
+ BenchmarkStats method_stats;
+ std::vector<const CodecInOut*> images;
+ std::vector<const Task*> tasks;
+ for (const Task& t : *tasks_) {
+ if (t.idx_method == idx_method) {
+ method_stats.Assimilate(t.stats);
+ images.push_back(t.image);
+ tasks.push_back(&t);
+ }
+ }
+
+ std::string out;
+
+ method_stats.PrintMoreStats(); // not concurrent
+ out += method_stats.PrintLine(method, fnames_->size());
+
+ if (Args()->write_html_report) {
+ WriteHtmlReport(method, *fnames_, tasks, images,
+ Args()->html_report_self_contained);
+ }
+
+ stats_aggregate_.push_back(
+ method_stats.ComputeColumns(method, fnames_->size()));
+
+ printf("%s", out.c_str());
+ fflush(stdout);
+ }
+
+ void PrintStatsHeader() {
+ if (Args()->markdown) {
+ if (Args()->show_progress) {
+ fprintf(stderr, "\n");
+ fflush(stderr);
+ }
+ printf("```\n");
+ }
+ if (fnames_->size() == 1) printf("%s\n", (*fnames_)[0].c_str());
+ printf("%s", PrintHeader(*extra_metrics_names_).c_str());
+ fflush(stdout);
+ }
+
+ void PrintStatsFooter() {
+ printf(
+ "%s",
+ PrintAggregate(extra_metrics_names_->size(), stats_aggregate_).c_str());
+ if (Args()->markdown) printf("```\n");
+ printf("\n");
+ fflush(stdout);
+ }
+
+ const std::vector<std::string>* methods_;
+ const std::vector<std::string>* extra_metrics_names_;
+ const std::vector<std::string>* fnames_;
+ const std::vector<Task>* tasks_;
+
+ size_t tasks_done_;
+
+ size_t stats_printed_;
+ std::vector<size_t> stats_done_;
+
+ size_t details_printed_;
+ std::vector<size_t> details_done_;
+
+ size_t max_fname_width_;
+ size_t max_method_width_;
+
+ std::vector<std::vector<ColumnValue>> stats_aggregate_;
+
+ std::mutex mutex;
+};
+
+class Benchmark {
+ using StringVec = std::vector<std::string>;
+
+ public:
+ // Return the exit code of the program.
+ static int Run() {
+ int ret = EXIT_SUCCESS;
+ {
+ PROFILER_FUNC;
+
+ const StringVec methods = GetMethods();
+ const StringVec extra_metrics_names = GetExtraMetricsNames();
+ const StringVec extra_metrics_commands = GetExtraMetricsCommands();
+ const StringVec fnames = GetFilenames();
+ bool all_color_aware;
+ bool jpeg_transcoding_requested;
+ // (non-const because Task.stats are updated)
+ std::vector<Task> tasks = CreateTasks(methods, fnames, &all_color_aware,
+ &jpeg_transcoding_requested);
+
+ std::unique_ptr<ThreadPoolInternal> pool;
+ std::vector<std::unique_ptr<ThreadPoolInternal>> inner_pools;
+ InitThreads(static_cast<int>(tasks.size()), &pool, &inner_pools);
+
+ const std::vector<CodecInOut> loaded_images = LoadImages(
+ fnames, all_color_aware, jpeg_transcoding_requested, pool.get());
+
+ if (RunTasks(methods, extra_metrics_names, extra_metrics_commands, fnames,
+ loaded_images, pool.get(), inner_pools, &tasks) != 0) {
+ ret = EXIT_FAILURE;
+ if (!Args()->silent_errors) {
+ fprintf(stderr, "There were error(s) in the benchmark.\n");
+ }
+ }
+ }
+
+ // Must have exited profiler zone above before calling.
+ if (Args()->profiler) {
+ PROFILER_PRINT_RESULTS();
+ }
+ CacheAligned::PrintStats();
+ return ret;
+ }
+
+ private:
+ static int NumOuterThreads(const int num_hw_threads, const int num_tasks) {
+ int num_threads = Args()->num_threads;
+ // Default to #cores
+ if (num_threads < 0) num_threads = num_hw_threads;
+
+ // As a safety precaution, limit the number of threads to 4x the number of
+ // available CPUs.
+ num_threads =
+ std::min<int>(num_threads, 4 * std::thread::hardware_concurrency());
+
+ // Don't create more threads than there are tasks (pointless/wasteful).
+ num_threads = std::min(num_threads, num_tasks);
+
+ // Just one thread is counterproductive.
+ if (num_threads == 1) num_threads = 0;
+
+ return num_threads;
+ }
+
+ static int NumInnerThreads(const int num_hw_threads, const int num_threads) {
+ int num_inner = Args()->inner_threads;
+
+ // Default: distribute remaining cores among tasks.
+ if (num_inner < 0) {
+ const int cores_for_outer = num_hw_threads - num_threads;
+ num_inner =
+ num_threads == 0 ? num_hw_threads : cores_for_outer / num_threads;
+ }
+
+ // Just one thread is counterproductive.
+ if (num_inner == 1) num_inner = 0;
+
+ return num_inner;
+ }
+
+ static void InitThreads(
+ const int num_tasks, std::unique_ptr<ThreadPoolInternal>* pool,
+ std::vector<std::unique_ptr<ThreadPoolInternal>>* inner_pools) {
+ const int num_hw_threads = std::thread::hardware_concurrency();
+ const int num_threads = NumOuterThreads(num_hw_threads, num_tasks);
+ const int num_inner = NumInnerThreads(num_hw_threads, num_threads);
+
+ fprintf(stderr,
+ "%d total threads, %d tasks, %d threads, %d inner threads\n",
+ num_hw_threads, num_tasks, num_threads, num_inner);
+
+ pool->reset(new ThreadPoolInternal(num_threads));
+ // Main thread OR worker threads in pool each get a possibly empty nested
+ // pool (helps use all available cores when #tasks < #threads)
+ for (size_t i = 0; i < (*pool)->NumThreads(); ++i) {
+ inner_pools->emplace_back(new ThreadPoolInternal(num_inner));
+ }
+ }
+
+ static StringVec GetMethods() {
+ StringVec methods = SplitString(Args()->codec, ',');
+ for (auto it = methods.begin(); it != methods.end();) {
+ if (it->empty()) {
+ it = methods.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ return methods;
+ }
+
+ static StringVec GetExtraMetricsNames() {
+ StringVec metrics = SplitString(Args()->extra_metrics, ',');
+ for (auto it = metrics.begin(); it != metrics.end();) {
+ if (it->empty()) {
+ it = metrics.erase(it);
+ } else {
+ *it = SplitString(*it, ':')[0];
+ ++it;
+ }
+ }
+ return metrics;
+ }
+
+ static StringVec GetExtraMetricsCommands() {
+ StringVec metrics = SplitString(Args()->extra_metrics, ',');
+ for (auto it = metrics.begin(); it != metrics.end();) {
+ if (it->empty()) {
+ it = metrics.erase(it);
+ } else {
+ auto s = SplitString(*it, ':');
+ JXL_CHECK(s.size() == 2);
+ *it = s[1];
+ ++it;
+ }
+ }
+ return metrics;
+ }
+
+ static StringVec SampleFromInput(const StringVec& fnames,
+ const std::string& sample_tmp_dir,
+ int num_samples, size_t size) {
+ JXL_CHECK(!sample_tmp_dir.empty());
+ fprintf(stderr, "Creating samples of %" PRIuS "x%" PRIuS " tiles...\n",
+ size, size);
+ StringVec fnames_out;
+ std::vector<Image3F> images;
+ std::vector<size_t> offsets;
+ size_t total_num_tiles = 0;
+ for (const auto& fname : fnames) {
+ Image3F img;
+ JXL_CHECK(ReadPNG(fname, &img));
+ JXL_CHECK(img.xsize() >= size);
+ JXL_CHECK(img.ysize() >= size);
+ total_num_tiles += (img.xsize() - size + 1) * (img.ysize() - size + 1);
+ offsets.push_back(total_num_tiles);
+ images.emplace_back(std::move(img));
+ }
+ JXL_CHECK(MakeDir(sample_tmp_dir));
+ Rng rng(0);
+ for (int i = 0; i < num_samples; ++i) {
+ int val = rng.UniformI(0, offsets.back());
+ size_t idx = (std::lower_bound(offsets.begin(), offsets.end(), val) -
+ offsets.begin());
+ JXL_CHECK(idx < images.size());
+ const Image3F& img = images[idx];
+ int x0 = rng.UniformI(0, img.xsize() - size);
+ int y0 = rng.UniformI(0, img.ysize() - size);
+ Image3F sample(size, size);
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < size; ++y) {
+ const float* JXL_RESTRICT row_in = img.PlaneRow(c, y0 + y);
+ float* JXL_RESTRICT row_out = sample.PlaneRow(c, y);
+ memcpy(row_out, &row_in[x0], size * sizeof(row_out[0]));
+ }
+ }
+ std::string fn_output =
+ StringPrintf("%s/%s.crop_%dx%d+%d+%d.png", sample_tmp_dir.c_str(),
+ FileBaseName(fnames[idx]).c_str(), size, size, x0, y0);
+ ThreadPool* null_pool = nullptr;
+ JXL_CHECK(WriteImage(std::move(sample), null_pool, fn_output));
+ fnames_out.push_back(fn_output);
+ }
+ fprintf(stderr, "Created %d sample tiles\n", num_samples);
+ return fnames_out;
+ }
+
+ static StringVec GetFilenames() {
+ StringVec fnames;
+ JXL_CHECK(MatchFiles(Args()->input, &fnames));
+ if (fnames.empty()) {
+ JXL_ABORT("No input file matches pattern: '%s'", Args()->input.c_str());
+ }
+ if (Args()->print_details) {
+ std::sort(fnames.begin(), fnames.end());
+ }
+
+ if (Args()->num_samples > 0) {
+ fnames = SampleFromInput(fnames, Args()->sample_tmp_dir,
+ Args()->num_samples, Args()->sample_dimensions);
+ }
+ return fnames;
+ }
+
+ // (Load only once, not for every codec)
+ static std::vector<CodecInOut> LoadImages(
+ const StringVec& fnames, const bool all_color_aware,
+ const bool jpeg_transcoding_requested, ThreadPool* pool) {
+ PROFILER_FUNC;
+ std::vector<CodecInOut> loaded_images;
+ loaded_images.resize(fnames.size());
+ JXL_CHECK(RunOnPool(
+ pool, 0, static_cast<uint32_t>(fnames.size()), ThreadPool::NoInit,
+ [&](const uint32_t task, size_t /*thread*/) {
+ const size_t i = static_cast<size_t>(task);
+ Status ok = true;
+
+ if (!Args()->decode_only) {
+ PaddedBytes encoded;
+ ok = ReadFile(fnames[i], &encoded) &&
+ (jpeg_transcoding_requested
+ ? jpeg::DecodeImageJPG(Span<const uint8_t>(encoded),
+ &loaded_images[i])
+ : SetFromBytes(Span<const uint8_t>(encoded),
+ Args()->color_hints, &loaded_images[i]));
+ if (ok && Args()->intensity_target != 0) {
+ loaded_images[i].metadata.m.SetIntensityTarget(
+ Args()->intensity_target);
+ }
+ }
+ if (!ok) {
+ if (!Args()->silent_errors) {
+ fprintf(stderr, "Failed to load image %s\n", fnames[i].c_str());
+ }
+ return;
+ }
+
+ if (!Args()->decode_only && all_color_aware) {
+ const bool is_gray = loaded_images[i].Main().IsGray();
+ const ColorEncoding& c_desired = ColorEncoding::LinearSRGB(is_gray);
+ if (!loaded_images[i].TransformTo(c_desired, GetJxlCms(),
+ /*pool=*/nullptr)) {
+ JXL_ABORT("Failed to transform to lin. sRGB %s",
+ fnames[i].c_str());
+ }
+ }
+
+ if (!Args()->decode_only && Args()->override_bitdepth != 0) {
+ if (Args()->override_bitdepth == 32) {
+ loaded_images[i].metadata.m.SetFloat32Samples();
+ } else {
+ loaded_images[i].metadata.m.SetUintSamples(
+ Args()->override_bitdepth);
+ }
+ }
+ },
+ "Load images"));
+ return loaded_images;
+ }
+
+ static std::vector<Task> CreateTasks(const StringVec& methods,
+ const StringVec& fnames,
+ bool* all_color_aware,
+ bool* jpeg_transcoding_requested) {
+ std::vector<Task> tasks;
+ tasks.reserve(methods.size() * fnames.size());
+ *all_color_aware = true;
+ *jpeg_transcoding_requested = false;
+ for (size_t idx_image = 0; idx_image < fnames.size(); ++idx_image) {
+ for (size_t idx_method = 0; idx_method < methods.size(); ++idx_method) {
+ tasks.emplace_back();
+ Task& t = tasks.back();
+ t.codec = CreateImageCodec(methods[idx_method]);
+ *all_color_aware &= t.codec->IsColorAware();
+ *jpeg_transcoding_requested |= t.codec->IsJpegTranscoder();
+ t.idx_image = idx_image;
+ t.idx_method = idx_method;
+ // t.stats is default-initialized.
+ }
+ }
+ JXL_ASSERT(tasks.size() == tasks.capacity());
+ return tasks;
+ }
+
+ // Return the total number of errors.
+ static size_t RunTasks(
+ const StringVec& methods, const StringVec& extra_metrics_names,
+ const StringVec& extra_metrics_commands, const StringVec& fnames,
+ const std::vector<CodecInOut>& loaded_images, ThreadPoolInternal* pool,
+ const std::vector<std::unique_ptr<ThreadPoolInternal>>& inner_pools,
+ std::vector<Task>* tasks) {
+ PROFILER_FUNC;
+ StatPrinter printer(methods, extra_metrics_names, fnames, *tasks);
+ if (Args()->print_details_csv) {
+ // Print CSV header
+ printf(
+ "method,image,error,size,pixels,enc_speed,dec_speed,"
+ "bpp,dist,psnr,p,bppp,qabpp");
+ for (const std::string& s : extra_metrics_names) {
+ printf(",%s", s.c_str());
+ }
+ printf("\n");
+ }
+
+ std::vector<uint64_t> errors_thread;
+ JXL_CHECK(RunOnPool(
+ pool, 0, tasks->size(),
+ [&](const size_t num_threads) {
+ // Reduce false sharing by only writing every 8th slot (64 bytes).
+ errors_thread.resize(8 * num_threads);
+ return true;
+ },
+ [&](const uint32_t i, const size_t thread) {
+ Task& t = (*tasks)[i];
+ const CodecInOut& image = loaded_images[t.idx_image];
+ t.image = &image;
+ PaddedBytes compressed;
+ DoCompress(fnames[t.idx_image], image, extra_metrics_commands,
+ t.codec.get(), inner_pools[thread].get(), &compressed,
+ &t.stats);
+ printer.TaskDone(i, t);
+ errors_thread[8 * thread] += t.stats.total_errors;
+ },
+ "Benchmark tasks"));
+ if (Args()->show_progress) fprintf(stderr, "\n");
+ return std::accumulate(errors_thread.begin(), errors_thread.end(), 0);
+ }
+};
+
+int BenchmarkMain(int argc, const char** argv) {
+ fprintf(stderr, "benchmark_xl %s\n",
+ jpegxl::tools::CodecConfigString(JxlDecoderVersion()).c_str());
+
+ JXL_CHECK(Args()->AddCommandLineOptions());
+
+ if (!Args()->Parse(argc, argv)) {
+ fprintf(stderr, "Use '%s -h' for more information\n", argv[0]);
+ return 1;
+ }
+
+ if (Args()->cmdline.HelpFlagPassed()) {
+ Args()->PrintHelp();
+ return 0;
+ }
+ if (!Args()->ValidateArgs()) {
+ fprintf(stderr, "Use '%s -h' for more information\n", argv[0]);
+ return 1;
+ }
+ return Benchmark::Run();
+}
+
+} // namespace
+} // namespace jxl
+
+int main(int argc, const char** argv) { return jxl::BenchmarkMain(argc, argv); }
diff --git a/media/libjxl/src/tools/benchmark/hm/README.md b/media/libjxl/src/tools/benchmark/hm/README.md
new file mode 100644
index 0000000000..e54904eff9
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/hm/README.md
@@ -0,0 +1,12 @@
+This directory contains encoding and decoding scripts for HEVC, for use with
+the benchmark custom codec. They use the HEVC reference encoder at https://hevc.hhi.fraunhofer.de/svn/svn_HEVCSoftware/
+and require the `TAppEncoderHighBitDepthStatic` and
+`TAppDecoderHighBitDepthStatic` binaries to be placed in this directory.
+
+Example usage, for encoding at QP = 30:
+
+```
+tools/benchmark_xl --input=image.png --codec='custom:bin:.../tools/benchmark/hm/encode.sh:.../tools/benchmark/hm/decode.sh:-q:30'
+```
+
+The paths to the encode and decode scripts should be adjusted as necessary.
diff --git a/media/libjxl/src/tools/benchmark/hm/decode.sh b/media/libjxl/src/tools/benchmark/hm/decode.sh
new file mode 100755
index 0000000000..624c8ba729
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/hm/decode.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -euo pipefail
+
+decoder="$(dirname "$0")"/TAppDecoderHighBitDepthStatic
+
+usage() {
+ echo "$0 [-v] <input.bin> <output.png>" >&2
+ exit 1
+}
+
+verbose=0
+
+while getopts ':hv' arg; do
+ case "$arg" in
+ h)
+ usage
+ ;;
+
+ v)
+ verbose=1
+ ;;
+
+ \?)
+ echo "Unrecognized option -$OPTARG" >&2
+ exit 1
+ ;;
+ esac
+done
+shift $((OPTIND-1))
+
+if [ $# -lt 2 ]; then
+ usage
+fi
+
+run() {
+ if [ "$verbose" -eq 1 ]; then
+ "$@"
+ else
+ "$@" > /dev/null 2>&1
+ fi
+}
+
+input="$1"
+output="$2"
+
+bin="$(mktemp)"
+yuv="$(mktemp)"
+width_file="$(mktemp)"
+height_file="$(mktemp)"
+icc_file="$(mktemp --suffix=.icc)"
+
+cleanup() {
+ rm -- "$bin" "$yuv" "$width_file" "$height_file" "$icc_file"
+}
+trap cleanup EXIT
+
+unpack_program="$(cat <<'END'
+ use File::Copy;
+ my ($input, $bin, $width_file, $height_file, $icc_file) = @ARGV;
+ open my $input_fh, '<:raw', $input;
+ sysread($input_fh, my $size, 8) == 8 or die;
+ my ($width, $height) = unpack 'NN', $size;
+ open my $width_fh, '>', $width_file;
+ print {$width_fh} "$width\n";
+ open my $height_fh, '>', $height_file;
+ print {$height_fh} "$height\n";
+ sysread($input_fh, my $icc_size, 4) == 4 or die;
+ $icc_size = unpack 'N', $icc_size;
+ sysread($input_fh, my $icc_data, $icc_size) == $icc_size or die;
+ open my $icc_fh, '>', $icc_file;
+ print {$icc_fh} $icc_data;
+ copy $input_fh, $bin;
+END
+)"
+run perl -Mstrict -Mwarnings -Mautodie -e "$unpack_program" -- "$input" "$bin" "$width_file" "$height_file" "$icc_file"
+
+width="$(cat "$width_file")"
+height="$(cat "$height_file")"
+
+start="$EPOCHREALTIME"
+run "$decoder" --OutputBitDepth=10 -b "$bin" -o "$yuv"
+end="$EPOCHREALTIME"
+
+elapsed="$(echo "$end - $start" | bc)"
+run echo "Completed in $elapsed seconds"
+
+echo "$elapsed" > "${output%.png}".time
+
+run ffmpeg -hide_banner -f rawvideo -vcodec rawvideo -s "${width}x$height" -r 25 -pix_fmt yuv444p10le -i "$yuv" -pix_fmt rgb24 -vf scale=in_color_matrix=bt709 -y "$output"
+if [ -s "$icc_file" ]; then
+ mogrify -profile "$icc_file" "$output"
+fi
diff --git a/media/libjxl/src/tools/benchmark/hm/encode.sh b/media/libjxl/src/tools/benchmark/hm/encode.sh
new file mode 100755
index 0000000000..319ba6953c
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/hm/encode.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -euo pipefail
+
+encoder="$(dirname "$0")"/TAppEncoderHighBitDepthStatic
+cfg_dir="$(dirname "$0")"/../../../third_party/HEVCSoftware/cfg
+
+usage() {
+ echo "$0 [-v] [-q <N>] <input.png> <output.bin>" >&2
+ exit 1
+}
+
+q=27
+verbose=0
+
+while getopts ':hq:v' arg; do
+ case "$arg" in
+ h)
+ usage
+ ;;
+
+ q)
+ q="$OPTARG"
+ ;;
+
+ v)
+ verbose=1
+ ;;
+
+ \?)
+ echo "Unrecognized option -$OPTARG" >&2
+ exit 1
+ ;;
+ esac
+done
+shift $((OPTIND-1))
+
+if [ $# -lt 2 ]; then
+ usage
+fi
+
+run() {
+ if [ "$verbose" -eq 1 ]; then
+ "$@"
+ else
+ "$@" > /dev/null 2>&1
+ fi
+}
+
+input="$1"
+output="$2"
+
+yuv="$(mktemp)"
+bin="$(mktemp)"
+
+to_clean=("$yuv" "$bin")
+cleanup() {
+ rm -- "${to_clean[@]}"
+}
+trap cleanup EXIT
+
+run ffmpeg -hide_banner -i "$input" -pix_fmt yuv444p10le -vf scale=out_color_matrix=bt709 -color_primaries bt709 -color_trc bt709 -colorspace bt709 -f rawvideo -y "$yuv"
+
+width="$(identify -format '%w' "$input")"
+height="$(identify -format '%h' "$input")"
+
+start="$EPOCHREALTIME"
+run "$encoder" -c "$cfg_dir"/encoder_intra_main_scc_10.cfg -f 1 -fr 1 -wdt "$width" -hgt "$height" --InputChromaFormat=444 --InputBitDepth=10 --ConformanceWindowMode=1 -i "$yuv" -b "$bin" -q "$q"
+end="$EPOCHREALTIME"
+
+elapsed="$(echo "$end - $start" | bc)"
+run echo "Completed in $elapsed seconds"
+
+echo "$elapsed" > "${output%.bin}".time
+
+icc="${output%.*}.icc"
+if run convert "$input" "$icc"; then
+ to_clean+=("$icc")
+fi
+
+pack_program="$(cat <<'END'
+ use File::Copy;
+ use IO::Handle;
+ my ($width, $height, $bin, $icc, $output) = @ARGV;
+ open my $output_fh, '>:raw', $output;
+ syswrite $output_fh, pack 'NN', $width, $height;
+ syswrite $output_fh, pack 'N', -s $icc;
+ copy $icc, $output_fh;
+ copy $bin, $output_fh;
+END
+)"
+run perl -Mstrict -Mwarnings -Mautodie -e "$pack_program" -- "$width" "$height" "$bin" "$icc" "$output"
diff --git a/media/libjxl/src/tools/benchmark/metrics/compute-hdrvdp.m b/media/libjxl/src/tools/benchmark/metrics/compute-hdrvdp.m
new file mode 100644
index 0000000000..60e40bf32f
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/compute-hdrvdp.m
@@ -0,0 +1,17 @@
+% Copyright (c) the JPEG XL Project Authors. All rights reserved.
+%
+% Use of this source code is governed by a BSD-style
+% license that can be found in the LICENSE file.
+
+pkg load image;
+
+args = argv();
+
+original_filename = args{1};
+decoded_filename = args{2};
+
+original = pfs_read_luminance(original_filename);
+decoded = pfs_read_luminance(decoded_filename);
+
+res = hdrvdp(decoded, original, 'luminance', 30, {});
+printf("%f\n", res.Q);
diff --git a/media/libjxl/src/tools/benchmark/metrics/compute-pumetrics.m b/media/libjxl/src/tools/benchmark/metrics/compute-pumetrics.m
new file mode 100644
index 0000000000..df0fe4bd0e
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/compute-pumetrics.m
@@ -0,0 +1,26 @@
+% Copyright (c) the JPEG XL Project Authors. All rights reserved.
+%
+% Use of this source code is governed by a BSD-style
+% license that can be found in the LICENSE file.
+
+pkg load image;
+
+args = argv();
+
+metric = args{1};
+original_filename = args{2};
+decoded_filename = args{3};
+
+original = pfs_read_luminance(original_filename);
+decoded = pfs_read_luminance(decoded_filename);
+
+switch (metric)
+ case "psnr"
+ res = qm_pu2_psnr(original, decoded);
+ case "ssim"
+ res = qm_pu2_ssim(original, decoded);
+ otherwise
+ error(sprintf("unrecognized metric %s", metric));
+end
+
+printf("%f\n", res);
diff --git a/media/libjxl/src/tools/benchmark/metrics/compute_octave_metric.sh b/media/libjxl/src/tools/benchmark/metrics/compute_octave_metric.sh
new file mode 100755
index 0000000000..a31c266592
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/compute_octave_metric.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# Usage: ./compute-octave-metric.sh <original> <decoded> <output> <intensity_target> [octave args...]
+# Where octave args do not need to contain -qf or the path to the original and decoded images.
+
+set -euo pipefail
+
+original="$1"
+decoded="$2"
+output="$3"
+intensity_target="$4"
+shift 4
+
+tmpdir="$(mktemp --directory)"
+
+linearized_original="$(mktemp --tmpdir="$tmpdir" --suffix='.pfm')"
+linearized_decoded="$(mktemp --tmpdir="$tmpdir" --suffix='.pfm')"
+
+cleanup() {
+ rm -- "$linearized_original" "$linearized_decoded"
+ rmdir --ignore-fail-on-non-empty -- "$tmpdir"
+}
+trap cleanup EXIT
+
+linearize() {
+ local input="$1"
+ local output="$2"
+ convert "$input" -set colorspace sRGB -colorspace RGB -evaluate multiply "$intensity_target" "$output"
+}
+
+linearize "$original" "$linearized_original"
+linearize "$decoded" "$linearized_decoded"
+
+octave -qf "$@" \
+ "$linearized_original" "$linearized_decoded" \
+ 2> /dev/null \
+ > "$output"
diff --git a/media/libjxl/src/tools/benchmark/metrics/dists-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/dists-rgb.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/dists-rgb.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/fsim-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/fsim-rgb.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/fsim-rgb.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/fsim-y.sh b/media/libjxl/src/tools/benchmark/metrics/fsim-y.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/fsim-y.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/gmsd-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/gmsd-rgb.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/gmsd-rgb.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/hdr_plots.sh b/media/libjxl/src/tools/benchmark/metrics/hdr_plots.sh
new file mode 100755
index 0000000000..4ce5d9fc4b
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/hdr_plots.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"$(dirname "$0")/run_all_hdr_metrics.sh" "$@" | sed -n '/```/q;p' > hdr_results.csv
+mkdir -p hdr_plots/
+rm -rf hdr_plots/*
+python3 "$(dirname "$0")/plots.py" hdr_results.csv hdr_plots
diff --git a/media/libjxl/src/tools/benchmark/metrics/hdrvdp-fixes.patch b/media/libjxl/src/tools/benchmark/metrics/hdrvdp-fixes.patch
new file mode 100644
index 0000000000..23f3f17b6d
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/hdrvdp-fixes.patch
@@ -0,0 +1,110 @@
+From 44a21be2c4de409f80d90cbcc2c20cb3f42e859e Mon Sep 17 00:00:00 2001
+From: Sami Boukortt <sboukortt@google.com>
+Date: Fri, 16 Oct 2020 20:01:02 +0200
+Subject: [PATCH] Fixes for Octave
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Copyright (c) the JPEG XL Project Authors. All rights reserved.
+
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file.
+
+----
+
+ifft2: https://savannah.gnu.org/bugs/?43742
+
+Removing #include <matrix.h>: https://octave.org/doc/v5.2.0/Getting-Started-with-Mex_002dFiles.html
+“One important difference between Octave and MATLAB is that the header
+"matrix.h" is implicitly included through the inclusion of "mex.h".”
+
+Length checks: it appears that functions(…).file for MEX files in Octave
+is empty.
+---
+ fast_conv_fft.m | 2 +-
+ matlabPyrTools_1.4_fixed/MEX/corrDn.c | 1 -
+ matlabPyrTools_1.4_fixed/MEX/pointOp.c | 1 -
+ matlabPyrTools_1.4_fixed/MEX/upConv.c | 1 -
+ matlabPyrTools_1.4_fixed/reconSpyr.m | 2 +-
+ matlabPyrTools_1.4_fixed/reconSpyrLevs.m | 2 +-
+ 6 files changed, 3 insertions(+), 6 deletions(-)
+
+diff --git a/fast_conv_fft.m b/fast_conv_fft.m
+index 65ceef8..b89e54b 100644
+--- a/fast_conv_fft.m
++++ b/fast_conv_fft.m
+@@ -16,7 +16,7 @@ pad_size = (size(fH)-size(X));
+
+ fX = fft2( padarray( X, pad_size, pad_value, 'post' ) );
+
+-Yl = real(ifft2( fX.*fH, size(fX,1), size(fX,2), 'symmetric' ));
++Yl = real(ifft2( fX.*fH, size(fX,1), size(fX,2)));
+
+ Y = Yl(1:size(X,1),1:size(X,2));
+
+diff --git a/matlabPyrTools_1.4_fixed/MEX/corrDn.c b/matlabPyrTools_1.4_fixed/MEX/corrDn.c
+index d02e272..17e739e 100755
+--- a/matlabPyrTools_1.4_fixed/MEX/corrDn.c
++++ b/matlabPyrTools_1.4_fixed/MEX/corrDn.c
+@@ -6,7 +6,6 @@ RES = corrDn(IM, FILT, EDGES, STEP, START, STOP);
+ */
+
+ #define V4_COMPAT
+-#include <matrix.h> /* Matlab matrices */
+ #include <mex.h>
+
+ #include "convolve.h"
+diff --git a/matlabPyrTools_1.4_fixed/MEX/pointOp.c b/matlabPyrTools_1.4_fixed/MEX/pointOp.c
+index 3623a02..e553adf 100755
+--- a/matlabPyrTools_1.4_fixed/MEX/pointOp.c
++++ b/matlabPyrTools_1.4_fixed/MEX/pointOp.c
+@@ -5,7 +5,6 @@ RES = pointOp(IM, LUT, ORIGIN, INCREMENT, WARNINGS)
+ */
+
+ #define V4_COMPAT
+-#include <matrix.h> /* Matlab matrices */
+ #include <mex.h>
+
+ #include <stddef.h> /* NULL */
+diff --git a/matlabPyrTools_1.4_fixed/MEX/upConv.c b/matlabPyrTools_1.4_fixed/MEX/upConv.c
+index 98a2bec..08fdf75 100755
+--- a/matlabPyrTools_1.4_fixed/MEX/upConv.c
++++ b/matlabPyrTools_1.4_fixed/MEX/upConv.c
+@@ -6,7 +6,6 @@ RES = upConv(IM, FILT, EDGES, STEP, START, STOP, RES);
+ */
+
+ #define V4_COMPAT
+-#include <matrix.h> /* Matlab matrices */
+ #include <mex.h>
+
+ #include "convolve.h"
+diff --git a/matlabPyrTools_1.4_fixed/reconSpyr.m b/matlabPyrTools_1.4_fixed/reconSpyr.m
+index 05eeafb..1440d8a 100644
+--- a/matlabPyrTools_1.4_fixed/reconSpyr.m
++++ b/matlabPyrTools_1.4_fixed/reconSpyr.m
+@@ -31,7 +31,7 @@ function res = reconSpyr(pyr, pind, filtfile, edges, levs, bands)
+ % Deterimine whether a MEX version of upConv is available
+ is_mex = true;
+ finfo = functions( @upConv );
+-if( strcmp( finfo.file((end-2):end), '.m') )
++if( length(finfo.file) > 2 && strcmp( finfo.file((end-2):end), '.m') )
+ is_mex = false;
+ end
+
+diff --git a/matlabPyrTools_1.4_fixed/reconSpyrLevs.m b/matlabPyrTools_1.4_fixed/reconSpyrLevs.m
+index ac5e2b1..d3b91d5 100644
+--- a/matlabPyrTools_1.4_fixed/reconSpyrLevs.m
++++ b/matlabPyrTools_1.4_fixed/reconSpyrLevs.m
+@@ -11,7 +11,7 @@ function res = reconSpyrLevs(pyr,pind,lofilt,bfilts,edges,levs,bands)
+ % Deterimine whether MEX version of upConv is available
+ is_mex = true;
+ finfo = functions( @upConv );
+-if( strcmp( finfo.file((end-2):end), '.m') )
++if( length(finfo.file) > 2 && strcmp( finfo.file((end-2):end), '.m') )
+ is_mex = false;
+ end
+
+--
+2.28.0
+
diff --git a/media/libjxl/src/tools/benchmark/metrics/hdrvdp.sh b/media/libjxl/src/tools/benchmark/metrics/hdrvdp.sh
new file mode 100755
index 0000000000..659ab85308
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/hdrvdp.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"$(dirname "$0")"/compute_octave_metric.sh "$@" \
+ --path "$(dirname "$0")"/../../../third_party/hdrvdp-2.2.2/ \
+ "$(dirname "$0")"/compute-hdrvdp.m
diff --git a/media/libjxl/src/tools/benchmark/metrics/iqa.py b/media/libjxl/src/tools/benchmark/metrics/iqa.py
new file mode 100644
index 0000000000..1be9699926
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/iqa.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+import os
+import sys
+import pathlib
+import torch
+from torchvision import transforms
+import numpy as np
+
+path = pathlib.Path(__file__).parent.absolute(
+) / '..' / '..' / '..' / 'third_party' / 'IQA-optimization'
+sys.path.append(str(path))
+
+from IQA_pytorch import SSIM, MS_SSIM, CW_SSIM, GMSD, LPIPSvgg, DISTS, NLPD, FSIM, VSI, VIFs, VIF, MAD
+
+
+# only really works with the output from JXL, but we don't need more than that.
+def read_pfm(fname):
+ with open(fname, 'rb') as f:
+ header_width_height = []
+ while len(header_width_height) < 3:
+ header_width_height += f.readline().rstrip().split()
+ header, width, height = header_width_height
+ assert header == b'PF' or header == b'Pf'
+ width, height = int(width), int(height)
+ scale = float(f.readline().rstrip())
+ fmt = '<f' if scale < 0 else '>f'
+ data = np.fromfile(f, fmt)
+ if header == b'PF':
+ out = np.reshape(data, (height, width, 3))[::-1, :, :]
+ else:
+ out = np.reshape(data, (height, width))[::-1, :]
+ return out.astype(np.float)
+
+
+D_dict = {
+ 'cwssim': CW_SSIM,
+ 'dists': DISTS,
+ 'fsim': FSIM,
+ 'gmsd': GMSD,
+ 'lpips': LPIPSvgg,
+ 'mad': MAD,
+ 'msssim': MS_SSIM,
+ 'nlpd': NLPD,
+ 'ssim': SSIM,
+ 'vif': VIF,
+ 'vsi': VSI,
+}
+
+algo = os.path.basename(sys.argv[1]).split('.')[0]
+algo, color = algo.split('-')
+
+channels = 3
+
+if color == 'y':
+ channels = 1
+
+
+def Load(path):
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
+ transform = transforms.Compose([
+ transforms.ToTensor(),
+ ])
+ img = read_pfm(path)
+ if len(img.shape) == 3 and channels == 1: # rgb -> Y
+ assert img.shape[2] == 3
+ tmp = np.zeros((img.shape[0], img.shape[1], 1), dtype=float)
+ tmp[:, :, 0] = (0.2126 * img[:, :, 0] + 0.7152 * img[:, :, 1] +
+ 0.0722 * img[:, :, 2])
+ img = tmp
+ if len(img.shape) == 2 and channels == 3: # Y -> rgb
+ gray = img
+ img = np.zeros((img.shape[0], img.shape[1], 3), dtype=float)
+ img[:, :, 0] = img[:, :, 1] = img[:, :, 2] = gray
+ if len(img.shape) == 3:
+ img = np.transpose(img, axes=(2, 0, 1)).copy()
+ return torch.FloatTensor(img).unsqueeze(0).to(device)
+
+
+ref_img = Load(sys.argv[2])
+enc_img = Load(sys.argv[3])
+D = D_dict[algo](channels=channels)
+score = D(ref_img, enc_img, as_loss=False)
+
+with open(sys.argv[4], 'w') as f:
+ print(score.item(), file=f)
diff --git a/media/libjxl/src/tools/benchmark/metrics/iqa_wrapper.sh b/media/libjxl/src/tools/benchmark/metrics/iqa_wrapper.sh
new file mode 100755
index 0000000000..1d179fdedc
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/iqa_wrapper.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+python3 "$(dirname "$0")/iqa.py" "$0" "$@"
diff --git a/media/libjxl/src/tools/benchmark/metrics/lpips-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/lpips-rgb.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/lpips-rgb.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/mrse.sh b/media/libjxl/src/tools/benchmark/metrics/mrse.sh
new file mode 100755
index 0000000000..54d18d6fe0
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/mrse.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -euo pipefail
+
+original="$1"
+decoded="$2"
+output="$3"
+intensity_target="$4"
+
+tmpdir="$(mktemp --directory)"
+
+linearized_original="$(mktemp --tmpdir="$tmpdir" --suffix='.pfm')"
+linearized_decoded="$(mktemp --tmpdir="$tmpdir" --suffix='.pfm')"
+
+cleanup() {
+ rm -- "$linearized_original" "$linearized_decoded"
+ rmdir --ignore-fail-on-non-empty -- "$tmpdir"
+}
+trap cleanup EXIT
+
+linearize() {
+ local input="$1"
+ local output="$2"
+ convert "$input" -set colorspace sRGB -colorspace RGB -evaluate multiply "$intensity_target" "$output"
+}
+
+linearize "$original" "$linearized_original"
+linearize "$decoded" "$linearized_decoded"
+
+"$(dirname "$0")"/../../../third_party/difftest_ng/difftest_ng --mrse "$linearized_original" "$linearized_decoded" \
+ | sed -e 's/^MRSE:\s*//' \
+ > "$output"
diff --git a/media/libjxl/src/tools/benchmark/metrics/msssim-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/msssim-rgb.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/msssim-rgb.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/msssim-y.sh b/media/libjxl/src/tools/benchmark/metrics/msssim-y.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/msssim-y.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/nlpd-y.sh b/media/libjxl/src/tools/benchmark/metrics/nlpd-y.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/nlpd-y.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/plots.py b/media/libjxl/src/tools/benchmark/metrics/plots.py
new file mode 100755
index 0000000000..04b2bb24e5
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/plots.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+import csv
+import sys
+import math
+import plotly.graph_objects as go
+
+_, results, output_dir, *rest = sys.argv
+OUTPUT = rest[0] if rest else 'svg'
+# valid values: html, svg, png, webp, jpeg, pdf
+
+with open(results, 'r') as f:
+ reader = csv.DictReader(f)
+ all_results = list(reader)
+
+nonmetric_columns = set([
+ "method", "image", "error", "size", "pixels", "enc_speed", "dec_speed",
+ "bpp", "bppp", "qabpp"
+])
+
+metrics = set(all_results[0].keys()) - nonmetric_columns
+
+
+def codec(method):
+ sm = method.split(':')
+ ssm = set(sm)
+ speeds = set([
+ 'kitten', 'falcon', 'wombat', 'cheetah', 'tortoise', 'squirrel',
+ 'hare', 'fast'
+ ])
+ s = speeds.intersection(ssm)
+ if sm[0] == 'custom':
+ return sm[1]
+ if sm[0] == 'jxl' and s:
+ return 'jxl-' + list(s)[0]
+ return sm[0]
+
+
+data = {(m, img): {c: []
+ for c in {codec(x['method'])
+ for x in all_results}}
+ for m in metrics for img in {x['image']
+ for x in all_results}}
+
+for r in all_results:
+ c = codec(r['method'])
+ img = r['image']
+ bpp = r['bpp']
+ for m in metrics:
+ data[(m, img)][c].append((float(bpp), float(r[m])))
+
+
+def pos(codec):
+ if 'jxl-dis' in codec:
+ return 6, codec
+ elif 'jxl' in codec:
+ return 7, codec
+ elif 'avif' in codec:
+ return 5, codec
+ elif 'kdu' in codec:
+ return 4, codec
+ elif 'heif' in codec:
+ return 3, codec
+ elif 'fuif' in codec or 'pik' in codec:
+ return 2, codec
+ elif 'jpg' in codec or 'jpeg' in codec or 'web' in codec:
+ return 1, codec
+ else:
+ return 0, codec
+
+
+def style(codec):
+ configs = {
+ 'jxl-cheetah': {
+ 'color': '#e41a1c',
+ 'dash': '1px, 1px',
+ 'width': 2
+ },
+ 'jxl-wombat': {
+ 'color': '#e41a1c',
+ 'dash': '2px, 2px',
+ 'width': 2
+ },
+ 'jxl-squirrel': {
+ 'color': '#e41a1c',
+ 'dash': '5px, 5px',
+ 'width': 2
+ },
+ 'jxl-kitten': {
+ 'color': '#e41a1c',
+ 'width': 2
+ },
+ 'jxl-dis-cheetah': {
+ 'color': '#377eb8',
+ 'dash': '1px, 1px',
+ 'width': 2
+ },
+ 'jxl-dis-wombat': {
+ 'color': '#377eb8',
+ 'dash': '2px, 2px',
+ 'width': 2
+ },
+ 'jxl-dis-squirrel': {
+ 'color': '#377eb8',
+ 'dash': '5px, 5px',
+ 'width': 2
+ },
+ 'jxl-dis-kitten': {
+ 'color': '#377eb8',
+ 'width': 2
+ },
+ 'rav1e.avif': {
+ 'color': '#4daf4a',
+ 'dash': '3px, 3px',
+ 'width': 2
+ },
+ '420.rav1e.avif': {
+ 'color': '#4daf4a',
+ 'dash': '1px, 1px',
+ 'width': 2
+ },
+ '444.rav1e.avif': {
+ 'color': '#4daf4a',
+ 'dash': '3px, 3px',
+ 'width': 2
+ },
+ 'psnr.420.aom.avif': {
+ 'color': '#4daf4a',
+ 'dash': '5px, 5px',
+ 'width': 2
+ },
+ 'psnr.444.aom.avif': {
+ 'color': '#4daf4a',
+ 'dash': '7px, 7px',
+ 'width': 2
+ },
+ 'ssim.420.aom.avif': {
+ 'color': '#4daf4a',
+ 'dash': '9px, 9px',
+ 'width': 2
+ },
+ 'ssim.444.aom.avif': {
+ 'color': '#4daf4a',
+ 'width': 2
+ },
+ 'heif': {
+ 'color': '#984ea3',
+ 'width': 2
+ },
+ 'fuif': {
+ 'color': '#ff7f00',
+ 'dash': '2px, 2px',
+ 'width': 2
+ },
+ 'pik-cfp': {
+ 'color': '#ff7f00',
+ 'width': 2
+ },
+ 'pik-cfp-fast': {
+ 'color': '#ff7f00',
+ 'dash': '4px, 4px',
+ 'width': 2
+ },
+ 'webp': {
+ 'color': '#000000',
+ 'width': 2
+ },
+ 'jpeg': {
+ 'color': '#a65628',
+ 'width': 2
+ },
+ 'xt.jpg': {
+ 'color': '#a65628',
+ 'width': 2
+ },
+ 'perc1.kdu.j2k': {
+ 'color': '#f781bf',
+ 'dash': '1px, 1px',
+ 'width': 2
+ },
+ 'perc2.kdu.j2k': {
+ 'color': '#f781bf',
+ 'dash': '3px, 3px',
+ 'width': 2
+ },
+ 'perc3.kdu.j2k': {
+ 'color': '#f781bf',
+ 'dash': '5px, 5px',
+ 'width': 2
+ },
+ 'perc4.kdu.j2k': {
+ 'color': '#f781bf',
+ 'dash': '7px, 7px',
+ 'width': 2
+ },
+ 'default.kdu.j2k': {
+ 'color': '#f781bf',
+ 'width': 2
+ },
+ }
+ return configs.get(codec, dict())
+
+
+visible_by_default = set([
+ 'jxl-kitten', 'ssim.444.aom.avif', 'heif', 'webp', 'jpeg', 'xt.jpg',
+ 'default.kdu.j2k'
+])
+
+column_remap = {
+ 'p': '6-Butteraugli',
+ 'dist': 'Max-Butteraugli',
+ 'psnr': "PSNR-YUV 6/8 Y",
+ 'MS-SSIM-Y': '-log10(1 - MS-SSIM-Y)',
+ 'puSSIM': '-log10(1 - puSSIM)',
+ 'FSIM-Y': '-log10(1 - FSIM-Y)',
+ 'FSIM-RGB': '-log10(1 - FSIM-RGB)',
+ 'VMAF': '-log10(1 - VMAF / 100)',
+}
+
+
+def remap(metric):
+ funs = {
+ 'MS-SSIM-Y': lambda x: -math.log10(1 - x),
+ 'puSSIM': lambda x: -math.log10(1 - x),
+ 'FSIM-Y': lambda x: -math.log10(1 - x),
+ 'FSIM-RGB': lambda x: -math.log10(1 - x),
+ 'VMAF': lambda x: -math.log10(1 + 1e-8 - x / 100),
+ }
+ return funs.get(metric, lambda x: x)
+
+
+for (m, img) in data:
+ fname = "%s/%s_%s" % (output_dir, m, img)
+ fig = go.Figure()
+ for method in sorted(data[(m, img)].keys(), key=pos):
+ vals = data[(m, img)][method]
+ zvals = list(zip(*sorted(vals)))
+ if not zvals:
+ continue
+ fig.add_trace(
+ go.Scatter(x=zvals[0],
+ y=[remap(m)(x) for x in zvals[1]],
+ mode='lines',
+ name=method,
+ line=style(method),
+ visible=True
+ if method in visible_by_default else 'legendonly'))
+ fig.update_layout(title=img,
+ xaxis_title='bpp',
+ yaxis_title=column_remap.get(m, m))
+ fig.update_xaxes(type='log')
+ if OUTPUT == 'html':
+ fig.write_html(fname + '.html', include_plotlyjs='directory')
+ else:
+ fig.write_image(fname + '.' + OUTPUT, scale=4)
diff --git a/media/libjxl/src/tools/benchmark/metrics/prepare_metrics.sh b/media/libjxl/src/tools/benchmark/metrics/prepare_metrics.sh
new file mode 100755
index 0000000000..7ecfaaf194
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/prepare_metrics.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -eu
+
+MYDIR=$(dirname $(realpath "$0"))
+
+
+main() {
+ cd "${MYDIR}/../../../third_party"
+ local zipurl
+ local repourl
+ for repourl in \
+ 'https://github.com/veluca93/IQA-optimization.git' \
+ 'https://github.com/Netflix/vmaf.git' \
+ 'https://github.com/thorfdbg/difftest_ng.git'
+ do
+ local reponame=$(basename "${repourl%.git}")
+ local dirname=$(basename "${reponame}")
+ if [[ ! -e "${dirname}" ]]; then
+ git clone "${repourl}"
+ fi
+ done
+ for zipurl in \
+ 'https://sourceforge.net/projects/hdrvdp/files/hdrvdp/2.2.2/hdrvdp-2.2.2.zip' \
+ 'https://sourceforge.net/projects/hdrvdp/files/simple_metrics/1.0/hdr_metrics.zip'
+ do
+ local zipfile="$(basename "${zipurl}")"
+ local dirname="$(basename "${zipfile}" '.zip')"
+ rm -fr "${dirname}"
+ if [[ ! -e "${zipfile}" ]]; then
+ wget -O "${zipfile}.tmp" "${zipurl}"
+ mv "${zipfile}.tmp" "${zipfile}"
+ fi
+ unzip "${zipfile}" "${dirname}"/'*'
+ done
+
+ pushd hdrvdp-2.2.2
+ patch -p1 < ../../tools/benchmark/metrics/hdrvdp-fixes.patch
+ pushd matlabPyrTools_1.4_fixed
+ mkoctfile --mex MEX/corrDn.c MEX/convolve.c MEX/wrap.c MEX/edges.c
+ mkoctfile --mex MEX/pointOp.c
+ mkoctfile --mex MEX/upConv.c
+ popd
+ popd
+
+
+ pushd difftest_ng
+ ./configure
+ make
+ popd
+
+
+ pushd vmaf/libvmaf
+ rm -rf build
+ meson build --buildtype release
+ ninja -vC build
+ popd
+}
+main "$@"
+
diff --git a/media/libjxl/src/tools/benchmark/metrics/pupsnr.sh b/media/libjxl/src/tools/benchmark/metrics/pupsnr.sh
new file mode 100755
index 0000000000..869fc36173
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/pupsnr.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+./compute_octave_metric.sh "$@" \
+ --path "$(dirname "$0")"/../../../third_party/hdr_metrics/ \
+ "$(dirname "$0")"/compute-pumetrics.m 'psnr'
diff --git a/media/libjxl/src/tools/benchmark/metrics/pussim.sh b/media/libjxl/src/tools/benchmark/metrics/pussim.sh
new file mode 100755
index 0000000000..957cfa1dc1
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/pussim.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+./compute_octave_metric.sh "$@" \
+ --path "$(dirname "$0")"/../../../third_party/hdr_metrics/ \
+ "$(dirname "$0")"/compute-pumetrics.m 'ssim'
diff --git a/media/libjxl/src/tools/benchmark/metrics/run_all_hdr_metrics.sh b/media/libjxl/src/tools/benchmark/metrics/run_all_hdr_metrics.sh
new file mode 100755
index 0000000000..5fb769d667
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/run_all_hdr_metrics.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -eu
+dir="$(dirname "$0")"
+
+main() {
+ local metrics=(
+ HDR-VDP:"${dir}"/hdrvdp.sh
+ MRSE:"${dir}"/mrse.sh
+ puPSNR:"${dir}"/pupsnr.sh
+ puSSIM:"${dir}"/pussim.sh
+ )
+
+ local metrics_args=$(printf '%s' "${metrics[@]/#/,}")
+ metrics_args=${metrics_args:1}
+
+
+ "${dir}/../../../build/tools/benchmark_xl" \
+ --print_details_csv \
+ --num_threads=32 \
+ --error_pnorm=6 \
+ --extra_metrics ${metrics_args} \
+ "$@"
+}
+
+main "$@"
diff --git a/media/libjxl/src/tools/benchmark/metrics/run_all_sdr_metrics.sh b/media/libjxl/src/tools/benchmark/metrics/run_all_sdr_metrics.sh
new file mode 100755
index 0000000000..def887b09e
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/run_all_sdr_metrics.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -eu
+dir="$(dirname "$0")"
+
+main() {
+ local metrics=(
+ FSIM-Y:"${dir}"/fsim-y.sh
+ FSIM-RGB:"${dir}"/fsim-rgb.sh
+ LPIPS:"${dir}"/lpips-rgb.sh
+ MS-SSIM-Y:"${dir}"/msssim-y.sh
+ NLPD:"${dir}"/nlpd-y.sh
+ SSIMULACRA:"${dir}"/ssimulacra.sh
+ VIF:"${dir}"/vif-rgb.sh
+ VMAF:"${dir}"/vmaf.sh
+ )
+ # other metrics, not in core experiments:
+# VSI:"${dir}"/vsi-rgb.sh
+# SSIM-RGB:"${dir}"/ssim-rgb.sh
+# SSIM-Y:"${dir}"/ssim-y.sh
+# GMSD:"${dir}"/gmsd.sh
+# DISTS:"${dir}"/dists-rgb.sh
+# MS-SSIM-RGB:"${dir}"/msssim-rgb.sh
+
+ local metrics_args=$(printf '%s' "${metrics[@]/#/,}")
+ metrics_args=${metrics_args:1}
+
+
+ "${dir}/../../../build/tools/benchmark_xl" \
+ --print_details_csv \
+ --num_threads=1 \
+ --error_pnorm=6 \
+ --extra_metrics ${metrics_args} \
+ "$@"
+}
+
+main "$@"
diff --git a/media/libjxl/src/tools/benchmark/metrics/sdr_plots.sh b/media/libjxl/src/tools/benchmark/metrics/sdr_plots.sh
new file mode 100755
index 0000000000..d97648e8f8
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/sdr_plots.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"$(dirname "$0")/run_all_sdr_metrics.sh" "$@" | sed -n '/```/q;p' > sdr_results.csv
+mkdir -p sdr_plots/
+rm -rf sdr_plots/*
+python3 "$(dirname "$0")/plots.py" sdr_results.csv sdr_plots
diff --git a/media/libjxl/src/tools/benchmark/metrics/ssim-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/ssim-rgb.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/ssim-rgb.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/ssim-y.sh b/media/libjxl/src/tools/benchmark/metrics/ssim-y.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/ssim-y.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/ssimulacra.sh b/media/libjxl/src/tools/benchmark/metrics/ssimulacra.sh
new file mode 100755
index 0000000000..65617d1c08
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/ssimulacra.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"$(dirname "$0")"/../../../build/tools/ssimulacra_main "$1" "$2" > "$3" 2>/dev/null
diff --git a/media/libjxl/src/tools/benchmark/metrics/vif-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/vif-rgb.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/vif-rgb.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/benchmark/metrics/vmaf.sh b/media/libjxl/src/tools/benchmark/metrics/vmaf.sh
new file mode 100755
index 0000000000..ab406d011c
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/vmaf.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -euo pipefail
+
+original="$1"
+decoded="$2"
+output="$3"
+
+tmpdir="$(mktemp --directory)"
+
+exr_original="$(mktemp --tmpdir="$tmpdir" --suffix='.exr')"
+exr_decoded="$(mktemp --tmpdir="$tmpdir" --suffix='.exr')"
+
+yuv_original="$(mktemp --tmpdir="$tmpdir" --suffix='.yuv')"
+yuv_decoded="$(mktemp --tmpdir="$tmpdir" --suffix='.yuv')"
+
+vmaf_csv="$(mktemp --tmpdir="$tmpdir" --suffix='.csv')"
+
+cleanup() {
+ rm -- "$exr_original" "$exr_decoded" "$yuv_original" "$yuv_decoded" "$vmaf_csv"
+ rmdir --ignore-fail-on-non-empty -- "$tmpdir"
+}
+trap cleanup EXIT
+
+convert "$original" "$exr_original"
+convert "$decoded" "$exr_decoded"
+
+srgb=(-colorspace bt709 -color_primaries bt709 -color_trc iec61966-2-1)
+ffmpeg "${srgb[@]}" -i "$exr_original" -pix_fmt yuv444p10le "${srgb[@]}" -y "$yuv_original" &>/dev/null
+ffmpeg "${srgb[@]}" -i "$exr_decoded" -pix_fmt yuv444p10le "${srgb[@]}" -y "$yuv_decoded" &>/dev/null
+
+"$(dirname "$0")"/../../../third_party/vmaf/libvmaf/build/tools/vmafossexec \
+ yuv444p10le \
+ "$(identify -format '%w' "$original")" "$(identify -format '%h' "$original")" \
+ "$yuv_original" "$yuv_decoded" \
+ "$(dirname "$0")/../../../third_party/vmaf/model/vmaf_v0.6.1.pkl" \
+ --log-fmt csv --log "$vmaf_csv" &>/dev/null
+
+read_csv="$(cat <<'END'
+import csv
+import sys
+reader = csv.DictReader(sys.stdin)
+for row in reader:
+ print(row['vmaf'])
+END
+)"
+
+python -c "$read_csv" < "$vmaf_csv" > "$output"
diff --git a/media/libjxl/src/tools/benchmark/metrics/vsi-rgb.sh b/media/libjxl/src/tools/benchmark/metrics/vsi-rgb.sh
new file mode 120000
index 0000000000..9e57c8f660
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/metrics/vsi-rgb.sh
@@ -0,0 +1 @@
+iqa_wrapper.sh \ No newline at end of file
diff --git a/media/libjxl/src/tools/bisector b/media/libjxl/src/tools/bisector
new file mode 100755
index 0000000000..2552045dfd
--- /dev/null
+++ b/media/libjxl/src/tools/bisector
@@ -0,0 +1,281 @@
+#!/usr/bin/env python
+r"""General-purpose bisector
+
+Prints a space-separated list of values to stdout:
+1_if_success_0_otherwise left_x left_f(x) right_x right_f(x)
+
+Usage examples:
+
+# Finding the square root of 200 via bisection:
+bisector --var=BB --range=0.0,100.0 --target=200 --maxiter=100 \
+ --atol_val=1e-12 --rtol_val=0 --cmd='echo "$BB * $BB" | bc'
+# => 1 14.142135623730923 199.99999999999923 14.142135623731633 200.0000000000193
+
+# Finding an integer approximation to sqrt(200) via bisection:
+bisector --var=BB --range=0,100 --target=200 --maxiter=100 \
+ --atol_arg=1 --cmd='echo "$BB * $BB" | bc'
+# => 1 14 196.0 15 225.0
+
+# Finding a change-id that broke something via bisection:
+bisector --var=BB --range=0,1000000 --target=0.5 --maxiter=100 \
+ --atol_arg=1 \
+ --cmd='test $BB -gt 123456 && echo 1 || echo 0' --verbosity=3
+# => 1 123456 0.0 123457 1.0
+
+# Finding settings that compress /usr/share/dict/words to a given target size:
+bisector --var=BB --range=1,9 --target=250000 --atol_arg=1 \
+ --cmd='gzip -$BB </usr/share/dict/words >/tmp/w_$BB.gz; wc -c /tmp/w_$BB.gz' \
+ --final='mv /tmp/w_$BB.gz /tmp/words.gz; rm /tmp/w_*.gz' \
+ --verbosity=1
+# => 1 3 263170.0 4 240043.0
+
+# JXL-encoding with bisection-for-size (tolerance 0.5%):
+bisector --var=BB --range=0.1,3.0 --target=3500 --rtol_val=0.005 \
+ --cmd='(build/tools/cjxl --distance=$BB /tmp/baseball.png /tmp/baseball_$BB.jxl && wc -c /tmp/baseball_$BB.jxl)' \
+ --final='mv /tmp/baseball_$BB.jxl /tmp/baseball.jxl; rm -f /tmp/baseball_*.jxl' \
+ --verbosity=1
+# => 1 1.1875 3573.0 1.278125 3481.0
+
+# JXL-encoding with bisection-for-bits-per-pixel (tolerance 0.5%), using helper:
+bisector --var=BB --range=0.1,3.0 --target=1.2 --rtol_val=0.005 \
+ --cmd='(build/tools/cjxl --distance=$BB /tmp/baseball.png /tmp/baseball_$BB.jxl && get_bpp /tmp/baseball_$BB.jxl)' \
+ --final='mv /tmp/baseball_$BB.jxl /tmp/baseball.jxl; rm -f /tmp/baseball_*.jxl' \
+ --verbosity=1
+# => ...
+"""
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+
+
+def _expandvars(vardef, env,
+ max_recursion=100,
+ max_length=10**6,
+ verbosity=0):
+ """os.path.expandvars() variant using parameter env rather than os.environ."""
+ current_expanded = vardef
+ for num_recursions in range(max_recursion):
+ if verbosity >= 3:
+ print(f'_expandvars(): num_recursions={num_recursions}, '
+ f'len={len(current_expanded)}' +
+ (', current: ' + current_expanded if verbosity >= 4 else ''))
+ if len > max_length:
+ break
+ current_expanded, num_replacements = re.subn(
+ r'$\{(\w+)\}|$(\w+)',
+ lambda m: env.get(m[1] if m[1] is not None else m[2], ''),
+ current_expanded)
+ if num_replacements == 0:
+ break
+ return current_expanded
+
+
+def _strtod(string):
+ """Extracts leftmost float from string (like strtod(3))."""
+ match = re.match(r'[+-]?\d*[.]?\d*(?:[eE][+-]?\d+)?', string)
+ return float(match[0]) if match[0] else None
+
+
+def run_shell_command(shell_command,
+ bisect_var, bisect_val,
+ extra_env_defs,
+ verbosity=0):
+ """Runs a shell command with env modifications, fetching return value."""
+ shell_env = dict(os.environ)
+ shell_env[bisect_var] = str(bisect_val)
+ for env_def in extra_env_defs:
+ varname, vardef = env_def.split('=', 1)
+ shell_env[varname] = _expandvars(vardev, shell_env,
+ verbosity=verbosity)
+ shell_ret = subprocess.run(shell_command,
+ # We explicitly want subshell semantics!
+ shell=True,
+ capture_output=True,
+ env=shell_env)
+ stdout = shell_ret.stdout.decode('utf-8')
+ score = _strtod(stdout)
+ if verbosity >= 2:
+ print(f'{bisect_var}={bisect_val} {shell_command} => '
+ f'{shell_ret.returncode} # {stdout.strip()}')
+ return (shell_ret.returncode == 0, # Command was successful?
+ score)
+
+
+def _bisect(*,
+ shell_command,
+ final_shell_command,
+ target,
+ int_args,
+ bisect_var, bisect_left, bisect_right,
+ rtol_val, atol_val, rtol_arg, atol_arg,
+ maxiter,
+ extra_env_defs,
+ verbosity=0
+ ):
+ """Performs bisection."""
+ def _get_val(x):
+ success, val = run_shell_command(shell_command,
+ bisect_var, x,
+ extra_env_defs,
+ verbosity=verbosity)
+ if not success:
+ raise RuntimeError(f'Bisection failed for: {bisect_var}={x}: '
+ f'success={success}, val={val}, '
+ f'cmd={shell_command}, var={bisect_var}')
+ return val
+ #
+ bisect_mid, value_mid = None, None
+ try:
+ value_left = _get_val(bisect_left)
+ value_right = _get_val(bisect_right)
+ if (value_left < target) != (target <= value_right):
+ raise RuntimeError(
+ f'Cannot bisect: target={target}, value_left={value_left}, '
+ f'value_right={value_right}')
+ for num_iter in range(maxiter):
+ bisect_mid_f = 0.5 * (bisect_left + bisect_right)
+ bisect_mid = round(bisect_mid_f) if int_args else bisect_mid_f
+ value_mid = _get_val(bisect_mid)
+ if (value_left < target) == (value_mid < target):
+ # Relative to target, `value_mid` is on the same side
+ # as `value_left`.
+ bisect_left = bisect_mid
+ value_left = value_mid
+ else:
+ # Otherwise, this situation must hold for value_right
+ # ("tertium non datur").
+ bisect_right = bisect_mid
+ value_right = value_mid
+ if verbosity >= 1:
+ print(f'bisect target={target}, '
+ f'left: {value_left} at {bisect_left}, '
+ f'right: {value_right} at {bisect_right}, '
+ f'mid: {value_mid} at {bisect_mid}')
+ delta_val = target - value_mid
+ if abs(delta_val) <= atol_val + rtol_val * abs(target):
+ return 1, bisect_left, value_left, bisect_right, value_right
+ delta_arg = bisect_right - bisect_left
+ # Also check whether the argument is "within tolerance".
+ # Here, we have to be careful if bisect_left and bisect_right
+ # have different signs: Then, their absolute magnitude
+ # "sets the relevant scale".
+ if abs(delta_arg) <= atol_arg + (
+ rtol_arg * 0.5 * (abs(bisect_left) + abs(bisect_right))):
+ return 1, bisect_left, value_left, bisect_right, value_right
+ return 0, bisect_left, value_left, bisect_right, value_right
+ finally:
+ # If cleanup is specified, always run it
+ if final_shell_command:
+ run_shell_command(
+ final_shell_command,
+ bisect_var,
+ bisect_mid if bisect_mid is not None else bisect_left,
+ extra_env_defs, verbosity=verbosity)
+
+
+def main(args):
+ """Main entry point."""
+ parser = argparse.ArgumentParser(description='mhtml_walk args')
+ parser.add_argument(
+ '--var',
+ help='The variable to use for bisection.',
+ default='BISECT')
+ parser.add_argument(
+ '--range',
+ help=('The argument range for bisecting, as {low},{high}. '
+ 'If no argument has a decimal dot, assume integer parameters.'),
+ default='0.0,1.0')
+ parser.add_argument(
+ '--max',
+ help='The maximal value for bisecting.',
+ type=float,
+ default=0.0)
+ parser.add_argument(
+ '--target',
+ help='The target value to aim for.',
+ type=float,
+ default=1.0)
+ parser.add_argument(
+ '--maxiter',
+ help='The maximal number of iterations to perform.',
+ type=int,
+ default=40)
+ parser.add_argument(
+ '--rtol_val',
+ help='Relative tolerance to accept for deviations from target value.',
+ type=float,
+ default=0.0)
+ parser.add_argument(
+ '--atol_val',
+ help='Absolute tolerance to accept for deviations from target value.',
+ type=float,
+ default=0.0)
+ parser.add_argument(
+ '--rtol_arg',
+ help='Relative tolerance to accept for the argument.',
+ type=float,
+ default=0.0)
+ parser.add_argument(
+ '--atol_arg',
+ help=('Absolute tolerance to accept for the argument '
+ '(e.g. for bisecting change-IDs).'),
+ type=float,
+ default=0.0)
+ parser.add_argument(
+ '--verbosity',
+ help='The verbosity level.',
+ type=int,
+ default=1)
+ parser.add_argument(
+ '--env',
+ help=('Comma-separated list of extra environment variables '
+ 'to incrementally add before executing the shell-command.'),
+ default='')
+ parser.add_argument(
+ '--cmd',
+ help=('The shell command to execute. Must print a numerical result '
+ 'to stdout.'))
+ parser.add_argument(
+ '--final',
+ help='The cleanup shell command to execute.')
+ #
+ parsed = parser.parse_args(args)
+ extra_env_defs = tuple(filter(None, parsed.env.split(',')))
+ try:
+ low_high = parsed.range.split(',')
+ if len(low_high) != 2:
+ raise ValueError('--range must be {low},{high}')
+ int_args = False
+ low_val, high_val = map(float, low_high)
+ low_val_int = round(low_val)
+ high_val_int = round(high_val)
+ if low_high == [str(low_val_int), str(high_val_int)]:
+ int_args = True
+ low_val = low_val_int
+ high_val = high_val_int
+ ret = _bisect(
+ shell_command=parsed.cmd,
+ final_shell_command=parsed.final,
+ target=parsed.target,
+ int_args=int_args,
+ bisect_var=parsed.var,
+ bisect_left=low_val,
+ bisect_right=high_val,
+ rtol_val=parsed.rtol_val,
+ atol_val=parsed.atol_val,
+ rtol_arg=parsed.rtol_arg,
+ atol_arg=parsed.atol_arg,
+ maxiter=parsed.maxiter,
+ extra_env_defs=extra_env_defs,
+ verbosity=parsed.verbosity,
+ )
+ print(' '.join(map(str, ret)))
+ except Exception as exn:
+ sys.exit(f'Problem: {exn}')
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/media/libjxl/src/tools/box/CMakeLists.txt b/media/libjxl/src/tools/box/CMakeLists.txt
new file mode 100644
index 0000000000..3072cfef0b
--- /dev/null
+++ b/media/libjxl/src/tools/box/CMakeLists.txt
@@ -0,0 +1,28 @@
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+add_library(box STATIC EXCLUDE_FROM_ALL
+ box.cc
+ box.h
+)
+# This library can be included into position independent binaries.
+set_target_properties(box PROPERTIES POSITION_INDEPENDENT_CODE TRUE)
+target_link_libraries(box
+ jxl-static
+ jxl_threads-static
+)
+target_include_directories(box
+ PRIVATE
+ "${PROJECT_SOURCE_DIR}"
+)
+
+if(${JPEGXL_ENABLE_DEVTOOLS})
+add_executable(box_list
+ box_list_main.cc
+)
+target_link_libraries(box_list
+ box
+)
+endif() # JPEGXL_ENABLE_DEVTOOLS
diff --git a/media/libjxl/src/tools/box/box.cc b/media/libjxl/src/tools/box/box.cc
new file mode 100644
index 0000000000..a60af5b14b
--- /dev/null
+++ b/media/libjxl/src/tools/box/box.cc
@@ -0,0 +1,334 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/box/box.h"
+
+#include "lib/jxl/base/byte_order.h" // for GetMaximumBrunsliEncodedSize
+#include "lib/jxl/jpeg/dec_jpeg_data.h"
+#include "lib/jxl/jpeg/jpeg_data.h"
+
+namespace jpegxl {
+namespace tools {
+
+namespace {
+// Checks if a + b > size, taking possible integer overflow into account.
+bool OutOfBounds(size_t a, size_t b, size_t size) {
+ size_t pos = a + b;
+ if (pos > size) return true;
+ if (pos < a) return true; // overflow happened
+ return false;
+}
+} // namespace
+
+// Parses the header of a BMFF box. Returns the result in a Box struct.
+// Sets the position to the end of the box header after parsing. The data size
+// is output if known, or must be handled by the caller and runs until the end
+// of the container file if not known.
+jxl::Status ParseBoxHeader(const uint8_t** next_in, size_t* available_in,
+ Box* box) {
+ size_t pos = 0;
+ size_t size = *available_in;
+ const uint8_t* in = *next_in;
+
+ if (OutOfBounds(pos, 8, size)) return JXL_FAILURE("out of bounds");
+
+ const size_t initial_pos = pos;
+
+ // Total box_size including this header itself.
+ uint64_t box_size = LoadBE32(in + pos);
+ memcpy(box->type, in + pos + 4, 4);
+
+ pos += 8;
+
+ if (box_size == 1) {
+ // If the size is 1, it indicates extended size read from 64-bit integer.
+ if (OutOfBounds(pos, 8, size)) return JXL_FAILURE("out of bounds");
+ box_size = LoadBE64(in + pos);
+ pos += 8;
+ }
+
+ if (!memcmp("uuid", box->type, 4)) {
+ if (OutOfBounds(pos, 16, size)) return JXL_FAILURE("out of bounds");
+ memcpy(box->extended_type, in + pos, 16);
+ pos += 16;
+ }
+
+ // This is the end of the box header, the box data begins here. Handle
+ // the data size now.
+ const size_t data_pos = pos;
+ const size_t header_size = data_pos - initial_pos;
+
+ if (box_size != 0) {
+ if (box_size < header_size) {
+ return JXL_FAILURE("invalid box size");
+ }
+ box->data_size_given = true;
+ box->data_size = box_size - header_size;
+ } else {
+ // The size extends to the end of the file. We don't necessarily know the
+ // end of the file here, since the input size may be only part of the full
+ // container file. Indicate the size is not given, the caller must handle
+ // this.
+ box->data_size_given = false;
+ box->data_size = 0;
+ }
+
+ // The remaining bytes are the data. If the box is a full box, the first
+ // bytes of the data have a certain structure but this is to be handled by
+ // the caller for the appropriate box type.
+ *next_in += pos;
+ *available_in -= pos;
+
+ return true;
+}
+
+jxl::Status AppendBoxHeader(const Box& box, jxl::PaddedBytes* out) {
+ bool use_extended = !memcmp("uuid", box.type, 4);
+
+ uint64_t box_size = 0;
+ bool large_size = false;
+ if (box.data_size_given) {
+ box_size = box.data_size + 8 + (use_extended ? 16 : 0);
+ if (box_size >= 0x100000000ull) {
+ large_size = true;
+ }
+ }
+
+ out->resize(out->size() + 4);
+ StoreBE32(large_size ? 1 : box_size, &out->back() - 4 + 1);
+
+ out->resize(out->size() + 4);
+ memcpy(&out->back() - 4 + 1, box.type, 4);
+
+ if (large_size) {
+ out->resize(out->size() + 8);
+ StoreBE64(box_size, &out->back() - 8 + 1);
+ }
+
+ if (use_extended) {
+ out->resize(out->size() + 16);
+ memcpy(&out->back() - 16 + 1, box.extended_type, 16);
+ }
+
+ return true;
+}
+
+bool IsContainerHeader(const uint8_t* data, size_t size) {
+ const uint8_t box_header[] = {0, 0, 0, 0xc, 'J', 'X',
+ 'L', ' ', 0xd, 0xa, 0x87, 0xa};
+ if (size < sizeof(box_header)) return false;
+ return memcmp(box_header, data, sizeof(box_header)) == 0;
+}
+
+jxl::Status DecodeJpegXlContainerOneShot(const uint8_t* data, size_t size,
+ JpegXlContainer* container) {
+ const uint8_t* in = data;
+ size_t available_in = size;
+
+ container->exif = nullptr;
+ container->exif_size = 0;
+ container->exfc = nullptr;
+ container->exfc_size = 0;
+ container->xml.clear();
+ container->xmlc.clear();
+ container->jumb = nullptr;
+ container->jumb_size = 0;
+ container->codestream.clear();
+ container->jpeg_reconstruction = nullptr;
+ container->jpeg_reconstruction_size = 0;
+
+ size_t box_index = 0;
+
+ while (available_in != 0) {
+ Box box;
+ if (!ParseBoxHeader(&in, &available_in, &box)) {
+ return JXL_FAILURE("Invalid box header");
+ }
+
+ size_t data_size = box.data_size_given ? box.data_size : available_in;
+
+ if (box.data_size > available_in) {
+ return JXL_FAILURE("Unexpected end of file");
+ }
+
+ if (box_index == 0) {
+ // TODO(lode): leave out magic signature box?
+ // Must be magic signature box.
+ if (memcmp("JXL ", box.type, 4) != 0) {
+ return JXL_FAILURE("Invalid magic signature");
+ }
+ if (box.data_size != 4) return JXL_FAILURE("Invalid magic signature");
+ if (in[0] != 0xd || in[1] != 0xa || in[2] != 0x87 || in[3] != 0xa) {
+ return JXL_FAILURE("Invalid magic signature");
+ }
+ } else if (box_index == 1) {
+ // Must be ftyp box.
+ if (memcmp("ftyp", box.type, 4) != 0) {
+ return JXL_FAILURE("Invalid ftyp");
+ }
+ if (box.data_size != 12) return JXL_FAILURE("Invalid ftyp");
+ const char* expected = "jxl \0\0\0\0jxl ";
+ if (memcmp(expected, in, 12) != 0) return JXL_FAILURE("Invalid ftyp");
+ } else if (!memcmp("jxli", box.type, 4)) {
+ // TODO(lode): parse JXL frame index box
+ if (!container->codestream.empty()) {
+ return JXL_FAILURE("frame index must come before codestream");
+ }
+ } else if (!memcmp("jxlc", box.type, 4)) {
+ container->codestream.append(in, in + data_size);
+ } else if (!memcmp("jxlp", box.type, 4)) {
+ if (data_size < 4) return JXL_FAILURE("Invalid jxlp");
+ // TODO(jon): don't just ignore the counter
+ container->codestream.append(in + 4, in + data_size);
+ } else if (!memcmp("Exif", box.type, 4)) {
+ if (data_size < 4) return JXL_FAILURE("Invalid Exif");
+ uint32_t tiff_header_offset = LoadBE32(in);
+ if (tiff_header_offset > data_size - 4)
+ return JXL_FAILURE("Invalid Exif tiff header offset");
+ container->exif = in + 4 + tiff_header_offset;
+ container->exif_size = data_size - 4 - tiff_header_offset;
+ } else if (!memcmp("Exfc", box.type, 4)) {
+ container->exfc = in;
+ container->exfc_size = data_size;
+ } else if (!memcmp("xml ", box.type, 4)) {
+ container->xml.emplace_back(in, data_size);
+ } else if (!memcmp("xmlc", box.type, 4)) {
+ container->xmlc.emplace_back(in, data_size);
+ } else if (!memcmp("jumb", box.type, 4)) {
+ container->jumb = in;
+ container->jumb_size = data_size;
+ } else if (!memcmp("jbrd", box.type, 4)) {
+ container->jpeg_reconstruction = in;
+ container->jpeg_reconstruction_size = data_size;
+ } else {
+ // Do nothing: box not recognized here but may be recognizable by
+ // other software.
+ }
+
+ in += data_size;
+ available_in -= data_size;
+ box_index++;
+ }
+
+ return true;
+}
+
+static jxl::Status AppendBoxAndData(const char type[4], const uint8_t* data,
+ size_t data_size, jxl::PaddedBytes* out,
+ bool exif = false) {
+ Box box;
+ memcpy(box.type, type, 4);
+ box.data_size = data_size + (exif ? 4 : 0);
+ box.data_size_given = true;
+ JXL_RETURN_IF_ERROR(AppendBoxHeader(box, out));
+ // for Exif: always use tiff header offset 0
+ if (exif)
+ for (int i = 0; i < 4; i++) out->push_back(0);
+ out->append(data, data + data_size);
+ return true;
+}
+
+jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container,
+ jxl::PaddedBytes* out) {
+ const unsigned char header[] = {0, 0, 0, 0xc, 'J', 'X', 'L', ' ',
+ 0xd, 0xa, 0x87, 0xa, 0, 0, 0, 0x14,
+ 'f', 't', 'y', 'p', 'j', 'x', 'l', ' ',
+ 0, 0, 0, 0, 'j', 'x', 'l', ' '};
+ size_t header_size = sizeof(header);
+ out->append(header, header + header_size);
+
+ if (container.exif) {
+ JXL_RETURN_IF_ERROR(AppendBoxAndData("Exif", container.exif,
+ container.exif_size, out, true));
+ }
+
+ if (container.exfc) {
+ JXL_RETURN_IF_ERROR(
+ AppendBoxAndData("Exfc", container.exfc, container.exfc_size, out));
+ }
+
+ for (size_t i = 0; i < container.xml.size(); i++) {
+ JXL_RETURN_IF_ERROR(AppendBoxAndData("xml ", container.xml[i].first,
+ container.xml[i].second, out));
+ }
+
+ for (size_t i = 0; i < container.xmlc.size(); i++) {
+ JXL_RETURN_IF_ERROR(AppendBoxAndData("xmlc", container.xmlc[i].first,
+ container.xmlc[i].second, out));
+ }
+
+ if (container.jpeg_reconstruction) {
+ JXL_RETURN_IF_ERROR(AppendBoxAndData("jbrd", container.jpeg_reconstruction,
+ container.jpeg_reconstruction_size,
+ out));
+ }
+
+ if (!container.codestream.empty()) {
+ JXL_RETURN_IF_ERROR(AppendBoxAndData("jxlc", container.codestream.data(),
+ container.codestream.size(), out));
+ } else {
+ return JXL_FAILURE("must have primary image frame");
+ }
+
+ if (container.jumb) {
+ JXL_RETURN_IF_ERROR(
+ AppendBoxAndData("jumb", container.jumb, container.jumb_size, out));
+ }
+
+ return true;
+}
+
+// TODO(veluca): the format defined here encode some things multiple times. Fix
+// that.
+jxl::Status DecodeJpegXlToJpeg(jxl::DecompressParams params,
+ const JpegXlContainer& container,
+ jxl::CodecInOut* io, jxl::ThreadPool* pool) {
+ params.keep_dct = true;
+ if (container.jpeg_reconstruction == nullptr) {
+ return JXL_FAILURE(
+ "Cannot decode to JPEG without a JPEG reconstruction box");
+ }
+
+ io->Main().jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>();
+
+ JXL_RETURN_IF_ERROR(DecodeJPEGData(
+ jxl::Span<const uint8_t>(container.jpeg_reconstruction,
+ container.jpeg_reconstruction_size),
+ io->Main().jpeg_data.get()));
+
+ auto& jpeg_data = io->Main().jpeg_data;
+ bool have_exif = false, have_xmp = false;
+ for (size_t i = 0; i < jpeg_data->app_data.size(); i++) {
+ if (jpeg_data->app_marker_type[i] == jxl::jpeg::AppMarkerType::kExif) {
+ if (have_exif)
+ return JXL_FAILURE("Unexpected: more than one Exif box required?");
+ if (jpeg_data->app_data[i].size() != container.exif_size + 9) {
+ return JXL_FAILURE(
+ "Exif box size does not match JPEG reconstruction data");
+ }
+ have_exif = true;
+ memcpy(&jpeg_data->app_data[i][3 + 6], container.exif,
+ container.exif_size);
+ }
+ if (jpeg_data->app_marker_type[i] == jxl::jpeg::AppMarkerType::kXMP) {
+ if (have_xmp)
+ return JXL_FAILURE("Unexpected: more than one XMP box required?");
+ if (jpeg_data->app_data[i].size() != container.xml[0].second + 32) {
+ return JXL_FAILURE(
+ "XMP box size does not match JPEG reconstruction data");
+ }
+ have_xmp = true;
+ memcpy(&jpeg_data->app_data[i][3 + 29], container.xml[0].first,
+ container.xml[0].second);
+ }
+ }
+
+ JXL_RETURN_IF_ERROR(DecodeFile(
+ params, jxl::Span<const uint8_t>(container.codestream), io, pool));
+ return true;
+}
+
+} // namespace tools
+} // namespace jpegxl
diff --git a/media/libjxl/src/tools/box/box.h b/media/libjxl/src/tools/box/box.h
new file mode 100644
index 0000000000..d6fd34fb67
--- /dev/null
+++ b/media/libjxl/src/tools/box/box.h
@@ -0,0 +1,120 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Tools for reading from / writing to ISOBMFF format for JPEG XL.
+
+#ifndef TOOLS_BOX_BOX_H_
+#define TOOLS_BOX_BOX_H_
+
+#include <string>
+#include <vector>
+
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/dec_file.h"
+#include "lib/jxl/enc_file.h"
+
+namespace jpegxl {
+namespace tools {
+
+// A top-level box in the box format.
+struct Box {
+ // The type of the box.
+ // If "uuid", use extended_type instead
+ char type[4];
+
+ // The extended_type is only used when type == "uuid".
+ // Extended types are not used in JXL. However, the box format itself
+ // supports this so they are handled correctly.
+ char extended_type[16];
+
+ // Size of the data, excluding box header. The box ends, and next box
+ // begins, at data + size. May not be used if data_size_given is false.
+ uint64_t data_size;
+
+ // If the size is not given, the datasize extends to the end of the file.
+ // If this field is false, the size field may not be used.
+ bool data_size_given;
+};
+
+// Parses the header of a BMFF box. Returns the result in a Box struct.
+// Updates next_in and available_in to point at the data in the box, directly
+// after the header.
+// Sets the data_size if known, or must be handled by the caller and runs until
+// the end of the container file if not known.
+// NOTE: available_in should be at least 8 up to 32 bytes to parse the
+// header without error.
+jxl::Status ParseBoxHeader(const uint8_t** next_in, size_t* available_in,
+ Box* box);
+
+// TODO(lode): streaming C API
+jxl::Status AppendBoxHeader(const Box& box, jxl::PaddedBytes* out);
+
+// NOTE: after DecodeJpegXlContainerOneShot, the exif etc. pointers point to
+// regions within the input data passed to that function.
+struct JpegXlContainer {
+ // Exif metadata, or null if not present in the container.
+ // The exif data has the format of 'Exif block' as defined in
+ // ISO/IEC23008-12:2017 Clause A.2.1
+ // Here we assume the tiff header offset is 0 and store only the
+ // actual Exif data (starting with the tiff header MM or II)
+ // TODO(lode): support the theoretical case of multiple exif boxes
+ const uint8_t* exif = nullptr; // Not owned
+ size_t exif_size = 0;
+
+ // Brotli-compressed exif metadata, if present. The data points to the brotli
+ // compressed stream, it is not decompressed here.
+ const uint8_t* exfc = nullptr; // Not owned
+ size_t exfc_size = 0;
+
+ // XML boxes for XMP. There may be multiple XML boxes.
+ // Each entry points to XML location and provides size.
+ // The memory is not owned.
+ // TODO(lode): for C API, cannot use std::vector.
+ std::vector<std::pair<const uint8_t*, size_t>> xml;
+
+ // Brotli-compressed xml boxes. The bytes are given in brotli-compressed form
+ // and are not decompressed here.
+ std::vector<std::pair<const uint8_t*, size_t>> xmlc;
+
+ // JUMBF superbox data, or null if not present in the container.
+ // The parsing of the nested boxes inside is not handled here.
+ const uint8_t* jumb = nullptr; // Not owned
+ size_t jumb_size = 0;
+
+ // TODO(lode): add frame index data
+
+ // JPEG reconstruction data, or null if not present in the container.
+ const uint8_t* jpeg_reconstruction = nullptr;
+ size_t jpeg_reconstruction_size = 0;
+
+ // The main JPEG XL codestream, of which there must be 1 in the container.
+ jxl::PaddedBytes codestream;
+};
+
+// Returns whether `data` starts with a container header; definitely returns
+// false if `size` is less than 12 bytes.
+bool IsContainerHeader(const uint8_t* data, size_t size);
+
+// NOTE: the input data must remain valid as long as `container` is used,
+// because its exif etc. pointers point to that data.
+jxl::Status DecodeJpegXlContainerOneShot(const uint8_t* data, size_t size,
+ JpegXlContainer* container);
+
+// TODO(lode): streaming C API
+jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container,
+ jxl::PaddedBytes* out);
+
+// TODO(veluca): this doesn't really belong here.
+jxl::Status DecodeJpegXlToJpeg(jxl::DecompressParams params,
+ const JpegXlContainer& container,
+ jxl::CodecInOut* io,
+ jxl::ThreadPool* pool = nullptr);
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_BOX_BOX_H_
diff --git a/media/libjxl/src/tools/box/box_list_main.cc b/media/libjxl/src/tools/box/box_list_main.cc
new file mode 100644
index 0000000000..40ca910e5e
--- /dev/null
+++ b/media/libjxl/src/tools/box/box_list_main.cc
@@ -0,0 +1,90 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This binary tool lists the boxes of any box-based format (JPEG XL,
+// JPEG 2000, MP4, ...).
+// This exists as a test for manual verification, rather than an actual tool.
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/status.h"
+#include "tools/box/box.h"
+
+namespace jpegxl {
+namespace tools {
+
+int RunMain(int argc, const char* argv[]) {
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s <filename>", argv[0]);
+ return 1;
+ }
+
+ jxl::PaddedBytes compressed;
+ if (!jxl::ReadFile(argv[1], &compressed)) return 1;
+ fprintf(stderr, "Read %" PRIuS " compressed bytes\n", compressed.size());
+
+ const uint8_t* in = compressed.data();
+ size_t available_in = compressed.size();
+
+ fprintf(stderr, "File size: %" PRIuS "\n", compressed.size());
+
+ while (available_in != 0) {
+ const uint8_t* start = in;
+ Box box;
+ if (!ParseBoxHeader(&in, &available_in, &box)) {
+ fprintf(stderr, "Failed at %" PRIuS "\n",
+ compressed.size() - available_in);
+ break;
+ }
+
+ size_t data_size = box.data_size_given ? box.data_size : available_in;
+ size_t header_size = in - start;
+ size_t box_size = header_size + data_size;
+
+ for (size_t i = 0; i < sizeof(box.type); i++) {
+ char c = box.type[i];
+ if (c < 32 || c > 127) {
+ printf("Unprintable character in box type, likely not a box file.\n");
+ return 0;
+ }
+ }
+
+ printf("box: \"%.4s\" box_size:%" PRIuS " data_size:%" PRIuS, box.type,
+ box_size, data_size);
+ if (!memcmp("uuid", box.type, 4)) {
+ printf(" -- extended type:\"%.16s\"", box.extended_type);
+ }
+ if (!memcmp("ftyp", box.type, 4) && data_size > 4) {
+ std::string ftype(in, in + 4);
+ printf(" -- ftype:\"%s\"", ftype.c_str());
+ }
+ printf("\n");
+
+ if (data_size > available_in) {
+ fprintf(
+ stderr, "Unexpected end of file %" PRIuS " %" PRIuS " %" PRIuS "\n",
+ static_cast<size_t>(box.data_size), available_in, compressed.size());
+ break;
+ }
+
+ in += data_size;
+ available_in -= data_size;
+ }
+
+ return 0;
+}
+
+} // namespace tools
+} // namespace jpegxl
+
+int main(int argc, const char* argv[]) {
+ return jpegxl::tools::RunMain(argc, argv);
+}
diff --git a/media/libjxl/src/tools/box/box_test.cc b/media/libjxl/src/tools/box/box_test.cc
new file mode 100644
index 0000000000..3146bcfa6f
--- /dev/null
+++ b/media/libjxl/src/tools/box/box_test.cc
@@ -0,0 +1,76 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/box/box.h"
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "gtest/gtest.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/status.h"
+
+TEST(BoxTest, BoxTest) {
+ size_t test_size = 256;
+ jxl::PaddedBytes exif(test_size);
+ jxl::PaddedBytes xml0(test_size);
+ jxl::PaddedBytes xml1(test_size);
+ jxl::PaddedBytes jumb(test_size);
+ jxl::PaddedBytes codestream(test_size);
+ // Generate arbitrary data for the codestreams: the test is not testing
+ // the contents of them but whether they are preserved in the container.
+ uint8_t v = 0;
+ for (size_t i = 0; i < test_size; ++i) {
+ exif[i] = v++;
+ xml0[i] = v++;
+ xml1[i] = v++;
+ jumb[i] = v++;
+ codestream[i] = v++;
+ }
+
+ jpegxl::tools::JpegXlContainer container;
+ container.exif = exif.data();
+ container.exif_size = exif.size();
+ container.xml.emplace_back(xml0.data(), xml0.size());
+ container.xml.emplace_back(xml1.data(), xml1.size());
+ container.xmlc.emplace_back(xml1.data(), xml1.size());
+ container.jumb = jumb.data();
+ container.jumb_size = jumb.size();
+ container.codestream = std::move(codestream);
+
+ jxl::PaddedBytes file;
+ EXPECT_EQ(true,
+ jpegxl::tools::EncodeJpegXlContainerOneShot(container, &file));
+
+ jpegxl::tools::JpegXlContainer container2;
+ EXPECT_EQ(true, jpegxl::tools::DecodeJpegXlContainerOneShot(
+ file.data(), file.size(), &container2));
+
+ EXPECT_EQ(exif.size(), container2.exif_size);
+ EXPECT_EQ(0, memcmp(exif.data(), container2.exif, container2.exif_size));
+ EXPECT_EQ(2u, container2.xml.size());
+ if (container2.xml.size() == 2) {
+ EXPECT_EQ(xml0.size(), container2.xml[0].second);
+ EXPECT_EQ(0, memcmp(xml0.data(), container2.xml[0].first,
+ container2.xml[0].second));
+ EXPECT_EQ(xml1.size(), container2.xml[1].second);
+ EXPECT_EQ(0, memcmp(xml1.data(), container2.xml[1].first,
+ container2.xml[1].second));
+ }
+ EXPECT_EQ(1u, container2.xmlc.size());
+ if (container2.xmlc.size() == 1) {
+ EXPECT_EQ(xml1.size(), container2.xmlc[0].second);
+ EXPECT_EQ(0, memcmp(xml1.data(), container2.xmlc[0].first,
+ container2.xmlc[0].second));
+ }
+ EXPECT_EQ(jumb.size(), container2.jumb_size);
+ EXPECT_EQ(0, memcmp(jumb.data(), container2.jumb, container2.jumb_size));
+ EXPECT_EQ(container.codestream.size(), container2.codestream.size());
+ EXPECT_EQ(0, memcmp(container.codestream.data(), container2.codestream.data(),
+ container2.codestream.size()));
+}
diff --git a/media/libjxl/src/tools/build_cleaner.py b/media/libjxl/src/tools/build_cleaner.py
new file mode 100755
index 0000000000..0a0df75635
--- /dev/null
+++ b/media/libjxl/src/tools/build_cleaner.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+
+"""build_cleaner.py: Update build files.
+
+This tool keeps certain parts of the build files up to date.
+"""
+
+import argparse
+import collections
+import locale
+import os
+import re
+import subprocess
+import sys
+import tempfile
+
+
+def RepoFiles(src_dir):
+ """Return the list of files from the source git repository"""
+ git_bin = os.environ.get('GIT_BIN', 'git')
+ files = subprocess.check_output([git_bin, '-C', src_dir, 'ls-files'])
+ ret = files.decode(locale.getpreferredencoding()).splitlines()
+ ret.sort()
+ return ret
+
+def GetPrefixLibFiles(repo_files, prefix, suffixes=('.h', '.cc', '.ui')):
+ """Gets the library files that start with the prefix and end with source
+ code suffix."""
+ prefix_files = [
+ fn for fn in repo_files
+ if fn.startswith(prefix) and any(fn.endswith(suf) for suf in suffixes)]
+ return prefix_files
+
+# Type holding the different types of sources in libjxl:
+# * decoder and common sources,
+# * encoder-only sources,
+# * tests-only sources,
+# * google benchmark sources,
+# * threads library sources,
+# * extras library sources,
+# * libjxl (encoder+decoder) public include/ headers and
+# * threads public include/ headers.
+JxlSources = collections.namedtuple(
+ 'JxlSources', ['dec', 'enc', 'test', 'gbench', 'threads',
+ 'extras', 'jxl_public_hdrs', 'threads_public_hdrs'])
+
+def SplitLibFiles(repo_files):
+ """Splits the library files into the different groups.
+
+ """
+ testonly = (
+ 'testdata.h', 'test_utils.h', '_test.h', '_test.cc',
+ # _testonly.* files are library code used in tests only.
+ '_testonly.h', '_testonly.cc'
+ )
+ main_srcs = GetPrefixLibFiles(repo_files, 'lib/jxl/')
+ extras_srcs = GetPrefixLibFiles(repo_files, 'lib/extras/')
+ test_srcs = [fn for fn in main_srcs
+ if any(patt in fn for patt in testonly)]
+ lib_srcs = [fn for fn in main_srcs
+ if not any(patt in fn for patt in testonly)]
+
+ # Google benchmark sources.
+ gbench_srcs = sorted(fn for fn in lib_srcs + extras_srcs
+ if fn.endswith('_gbench.cc'))
+ lib_srcs = [fn for fn in lib_srcs if fn not in gbench_srcs]
+ # Exclude optional codecs from extras.
+ exclude_extras = [
+ '/dec/gif',
+ '/dec/apng', '/enc/apng',
+ '/dec/exr', '/enc/exr',
+ '/dec/jpg', '/enc/jpg',
+ ]
+ extras_srcs = [fn for fn in extras_srcs if fn not in gbench_srcs and
+ not any(patt in fn for patt in testonly) and
+ not any(patt in fn for patt in exclude_extras)]
+
+
+ enc_srcs = [fn for fn in lib_srcs
+ if os.path.basename(fn).startswith('enc_') or
+ os.path.basename(fn).startswith('butteraugli')]
+ enc_srcs.extend([
+ "lib/jxl/encode.cc",
+ "lib/jxl/encode_internal.h",
+ "lib/jxl/gaborish.cc",
+ "lib/jxl/gaborish.h",
+ "lib/jxl/huffman_tree.cc",
+ "lib/jxl/huffman_tree.h",
+ # Only the inlines in linalg.h header are used in the decoder.
+ # TODO(deymo): split out encoder only linalg.h functions.
+ "lib/jxl/linalg.cc",
+ "lib/jxl/optimize.cc",
+ "lib/jxl/optimize.h",
+ "lib/jxl/progressive_split.cc",
+ "lib/jxl/progressive_split.h",
+ # TODO(deymo): Add luminance.cc and luminance.h here too. Currently used
+ # by aux_out.h.
+ # dec_file is not intended to be part of the decoder library, so move it
+ # to the encoder source set
+ "lib/jxl/dec_file.cc",
+ "lib/jxl/dec_file.h",
+ ])
+ # Temporarily remove enc_bit_writer from the encoder sources: a lot of
+ # decoder source code still needs to be split up into encoder and decoder.
+ # Including the enc_bit_writer in the decoder allows to build a working
+ # libjxl_dec library.
+ # TODO(lode): remove the dependencies of the decoder on enc_bit_writer and
+ # remove enc_bit_writer from the dec_srcs again.
+ enc_srcs.remove("lib/jxl/enc_bit_writer.cc")
+ enc_srcs.remove("lib/jxl/enc_bit_writer.h")
+ enc_srcs.sort()
+
+ enc_srcs_set = set(enc_srcs)
+ lib_srcs = [fn for fn in lib_srcs if fn not in enc_srcs_set]
+
+ # The remaining of the files are in the dec_library.
+ dec_srcs = lib_srcs
+
+ thread_srcs = GetPrefixLibFiles(repo_files, 'lib/threads/')
+ thread_srcs = [fn for fn in thread_srcs
+ if not any(patt in fn for patt in testonly)]
+ public_hdrs = GetPrefixLibFiles(repo_files, 'lib/include/jxl/')
+
+ threads_public_hdrs = [fn for fn in public_hdrs if '_parallel_runner' in fn]
+ jxl_public_hdrs = list(sorted(set(public_hdrs) - set(threads_public_hdrs)))
+ return JxlSources(dec_srcs, enc_srcs, test_srcs, gbench_srcs, thread_srcs,
+ extras_srcs, jxl_public_hdrs, threads_public_hdrs)
+
+
+def CleanFile(args, filename, pattern_data_list):
+ """Replace a pattern match with new data in the passed file.
+
+ Given a regular expression pattern with a single () match, it runs the regex
+ over the passed filename and replaces the match () with the new data. If
+ args.update is set, it will update the file with the new contents, otherwise
+ it will return True when no changes were needed.
+
+ Multiple pairs of (regular expression, new data) can be passed to the
+ pattern_data_list parameter and will be applied in order.
+
+ The regular expression must match at least once in the file.
+ """
+ filepath = os.path.join(args.src_dir, filename)
+ with open(filepath, 'r') as f:
+ src_text = f.read()
+
+ if not pattern_data_list:
+ return True
+
+ new_text = src_text
+
+ for pattern, data in pattern_data_list:
+ offset = 0
+ chunks = []
+ for match in re.finditer(pattern, new_text):
+ chunks.append(new_text[offset:match.start(1)])
+ offset = match.end(1)
+ chunks.append(data)
+ if not chunks:
+ raise Exception('Pattern not found for %s: %r' % (filename, pattern))
+ chunks.append(new_text[offset:])
+ new_text = ''.join(chunks)
+
+ if new_text == src_text:
+ return True
+
+ if args.update:
+ print('Updating %s' % filename)
+ with open(filepath, 'w') as f:
+ f.write(new_text)
+ return True
+ else:
+ with tempfile.NamedTemporaryFile(
+ mode='w', prefix=os.path.basename(filename)) as new_file:
+ new_file.write(new_text)
+ new_file.flush()
+ subprocess.call(
+ ['diff', '-u', filepath, '--label', 'a/' + filename, new_file.name,
+ '--label', 'b/' + filename])
+ return False
+
+
+def BuildCleaner(args):
+ repo_files = RepoFiles(args.src_dir)
+ ok = True
+
+ # jxl version
+ with open(os.path.join(args.src_dir, 'lib/CMakeLists.txt'), 'r') as f:
+ cmake_text = f.read()
+
+ gni_patterns = []
+ for varname in ('JPEGXL_MAJOR_VERSION', 'JPEGXL_MINOR_VERSION',
+ 'JPEGXL_PATCH_VERSION'):
+ # Defined in CMakeLists.txt as "set(varname 1234)"
+ match = re.search(r'set\(' + varname + r' ([0-9]+)\)', cmake_text)
+ version_value = match.group(1)
+ gni_patterns.append((r'"' + varname + r'=([0-9]+)"', version_value))
+
+ jxl_src = SplitLibFiles(repo_files)
+
+ # libjxl
+ jxl_cmake_patterns = []
+ jxl_cmake_patterns.append(
+ (r'set\(JPEGXL_INTERNAL_SOURCES_DEC\n([^\)]+)\)',
+ ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.dec)))
+ jxl_cmake_patterns.append(
+ (r'set\(JPEGXL_INTERNAL_SOURCES_ENC\n([^\)]+)\)',
+ ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.enc)))
+ ok = CleanFile(
+ args, 'lib/jxl.cmake',
+ jxl_cmake_patterns) and ok
+
+ ok = CleanFile(
+ args, 'lib/jxl_benchmark.cmake',
+ [(r'set\(JPEGXL_INTERNAL_SOURCES_GBENCH\n([^\)]+)\)',
+ ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.gbench))]) and ok
+
+ gni_patterns.append((
+ r'libjxl_dec_sources = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.dec)))
+ gni_patterns.append((
+ r'libjxl_enc_sources = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.enc)))
+ gni_patterns.append((
+ r'libjxl_gbench_sources = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.gbench)))
+
+
+ tests = [fn[len('lib/'):] for fn in jxl_src.test if fn.endswith('_test.cc')]
+ testlib = [fn[len('lib/'):] for fn in jxl_src.test
+ if not fn.endswith('_test.cc')]
+ gni_patterns.append((
+ r'libjxl_tests_sources = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn for fn in tests)))
+ gni_patterns.append((
+ r'libjxl_testlib_sources = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn for fn in testlib)))
+
+ # libjxl_threads
+ ok = CleanFile(
+ args, 'lib/jxl_threads.cmake',
+ [(r'set\(JPEGXL_THREADS_SOURCES\n([^\)]+)\)',
+ ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.threads))]) and ok
+
+ gni_patterns.append((
+ r'libjxl_threads_sources = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.threads)))
+
+ # libjxl_extras
+ ok = CleanFile(
+ args, 'lib/jxl_extras.cmake',
+ [(r'set\(JPEGXL_EXTRAS_SOURCES\n([^\)]+)\)',
+ ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.extras))]) and ok
+
+ gni_patterns.append((
+ r'libjxl_extras_sources = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.extras)))
+
+ # libjxl_profiler
+ profiler_srcs = [fn[len('lib/'):] for fn in repo_files
+ if fn.startswith('lib/profiler')]
+ ok = CleanFile(
+ args, 'lib/jxl_profiler.cmake',
+ [(r'set\(JPEGXL_PROFILER_SOURCES\n([^\)]+)\)',
+ ''.join(' %s\n' % fn for fn in profiler_srcs))]) and ok
+
+ gni_patterns.append((
+ r'libjxl_profiler_sources = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn for fn in profiler_srcs)))
+
+ # Public headers.
+ gni_patterns.append((
+ r'libjxl_public_headers = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn[len('lib/'):]
+ for fn in jxl_src.jxl_public_hdrs)))
+ gni_patterns.append((
+ r'libjxl_threads_public_headers = \[\n([^\]]+)\]',
+ ''.join(' "%s",\n' % fn[len('lib/'):]
+ for fn in jxl_src.threads_public_hdrs)))
+
+
+ # Update the list of tests. CMake version include test files in other libs,
+ # not just in libjxl.
+ tests = [fn[len('lib/'):] for fn in repo_files
+ if fn.endswith('_test.cc') and fn.startswith('lib/')]
+ ok = CleanFile(
+ args, 'lib/jxl_tests.cmake',
+ [(r'set\(TEST_FILES\n([^\)]+) ### Files before this line',
+ ''.join(' %s\n' % fn for fn in tests))]) and ok
+ ok = CleanFile(
+ args, 'lib/jxl_tests.cmake',
+ [(r'set\(TESTLIB_FILES\n([^\)]+)\)',
+ ''.join(' %s\n' % fn for fn in testlib))]) and ok
+
+ # Update lib.gni
+ ok = CleanFile(args, 'lib/lib.gni', gni_patterns) and ok
+
+ return ok
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--src-dir',
+ default=os.path.realpath(os.path.join(
+ os.path.dirname(__file__), '..')),
+ help='path to the build directory')
+ parser.add_argument('--update', default=False, action='store_true',
+ help='update the build files instead of only checking')
+ args = parser.parse_args()
+ if not BuildCleaner(args):
+ print('Build files need update.')
+ sys.exit(2)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/media/libjxl/src/tools/build_stats.py b/media/libjxl/src/tools/build_stats.py
new file mode 100755
index 0000000000..b1dc1ea393
--- /dev/null
+++ b/media/libjxl/src/tools/build_stats.py
@@ -0,0 +1,412 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+
+"""build_stats.py: Gather statistics about sizes of dependencies.
+
+This tools computes a realistic estimate of the size contribution to a binary
+from a statically linked library. Statically linked libraries compiled with
+-ffunction-sections and linked -gc-sections mean that we could drop part of the
+library at the final binary linking time. This tool takes that into account the
+symbols that end up in the final binary and not just all the symbols of the
+components.
+"""
+
+import argparse
+import collections
+import itertools
+import json
+import os
+import re
+import struct
+import subprocess
+import sys
+import tempfile
+
+# Ignore functions with stack size smaller than this value.
+MIN_STACK_SIZE = 32
+
+
+Symbol = collections.namedtuple('Symbol', ['address', 'size', 'typ', 'name'])
+
+# Represents the stack size information of a function (defined by its address).
+SymbolStack = collections.namedtuple('SymbolStack',
+ ['address', 'stack_size'])
+
+ObjectStats = collections.namedtuple('ObjectStats',
+ ['name', 'in_partition', 'size_map'])
+
+# An object target file in the build system.
+Target = collections.namedtuple('Target',
+ ['name', 'deps', 'filename'])
+
+# Sections that end up in the binary file.
+# t - text (code), d - global non-const data, n/r - read-only data,
+# w - weak symbols (likely inline code not inlined),
+# v - weak symbols (vtable / typeinfo)
+# u - unique symbols
+BIN_SIZE = 'tdnrwvu'
+
+# Sections that end up in static RAM.
+RAM_SIZE = 'dbs'
+
+# u - symbols imported from some other library
+# a - absolute address symbols
+IGNORE_SYMBOLS = 'ua'
+
+SIMD_NAMESPACES = [
+ 'N_SCALAR', 'N_WASM', 'N_NEON', 'N_PPC8', 'N_SSE4', 'N_AVX2', 'N_AVX3']
+
+
+def LoadSymbols(filename):
+ ret = []
+ nmout = subprocess.check_output(['nm', '--format=posix', filename])
+ for line in nmout.decode('utf-8').splitlines():
+ if line.rstrip().endswith(':'):
+ # Ignore object names.
+ continue
+ # symbol_name, symbol_type, (optional) address, (optional) size
+ symlist = line.rstrip().split(' ')
+ assert 2 <= len(symlist) <= 4
+ ret.append(Symbol(
+ int(symlist[2], 16) if len(symlist) > 2 else None,
+ int(symlist[3], 16) if len(symlist) > 3 else None,
+ symlist[1],
+ symlist[0]))
+ return ret
+
+def LoadTargetCommand(target, build_dir):
+ stdout = subprocess.check_output(
+ ['ninja', '-C', build_dir, '-t', 'commands', target])
+ # The last command is always the command to build (link) the requested
+ # target.
+ command = stdout.splitlines()[-1]
+ return command.decode('utf-8')
+
+
+def LoadTarget(target, build_dir):
+ """Loads a build system target and its dependencies into a Target object"""
+ if target.endswith('.o'):
+ # Speed up this case.
+ return Target(target, [], target)
+
+ link_params = LoadTargetCommand(target, build_dir).split()
+ if 'cmake_symlink_library' in link_params:
+ # The target is a library symlinked, use the target of the symlink
+ # instead.
+ target = link_params[link_params.index('cmake_symlink_library') + 1]
+ link_params = LoadTargetCommand(target, build_dir).split()
+
+ # The target name is not always the same as the filename of the output, for
+ # example, "djxl" target generates "tools/djxl" file.
+ if '-o' in link_params:
+ target_filename = link_params[link_params.index('-o') + 1]
+ elif target.endswith('.a'):
+ # Command is '/path/to/ar', 'qc', 'target.a', ...
+ target_filename = link_params[link_params.index('qc') + 1]
+ else:
+ raise Exception('Unknown "%s" output filename in command: %r' %
+ (target, link_params))
+
+ tgt_libs = []
+ for entry in link_params:
+ if not entry or not (entry.endswith('.o') or entry.endswith('.a')):
+ continue
+ if entry == target_filename:
+ continue
+ fn = os.path.join(build_dir, entry)
+ if not os.path.exists(fn):
+ continue
+ if entry in tgt_libs:
+ continue
+ tgt_libs.append(entry)
+
+ return Target(target, tgt_libs, target_filename)
+
+
+def TargetTransitiveDeps(all_tgts, target):
+ """Returns the list of all transitive dependencies of target"""
+ ret = all_tgts[target].deps
+ # There can't be loop dependencies in the targets.
+ i = 0
+ while i < len(ret):
+ ret.extend(all_tgts[ret[i]].deps)
+ i += 1
+ return ret
+
+
+def LoadStackSizes(filename, binutils=''):
+ """Loads the stack size used by functions from the ELF.
+
+ This function loads the stack size the compiler stored in the .stack_sizes
+ section, which can be done by compiling with -fstack-size-section in clang.
+ """
+ with tempfile.NamedTemporaryFile() as stack_sizes_sec:
+ subprocess.check_call(
+ [binutils + 'objcopy', '-O', 'binary', '--only-section=.stack_sizes',
+ '--set-section-flags', '.stack_sizes=alloc', filename,
+ stack_sizes_sec.name])
+ stack_sizes = stack_sizes_sec.read()
+ # From the documentation:
+ # The section will contain an array of pairs of function symbol values
+ # (pointer size) and stack sizes (unsigned LEB128). The stack size values
+ # only include the space allocated in the function prologue. Functions with
+ # dynamic stack allocations are not included.
+
+ # Get the pointer format based on the ELF file.
+ output = subprocess.check_output(
+ [binutils + 'objdump', '-a', filename]).decode('utf-8')
+ elf_format = re.search('file format (.*)$', output, re.MULTILINE).group(1)
+ if elf_format.startswith('elf64-little') or elf_format == 'elf64-x86-64':
+ pointer_fmt = '<Q'
+ elif elf_format.startswith('elf32-little') or elf_format == 'elf32-i386':
+ pointer_fmt = '<I'
+ else:
+ raise Exception('Unknown ELF format: %s' % elf_format)
+ pointer_size = struct.calcsize(pointer_fmt)
+
+ ret = []
+ i = 0
+ while i < len(stack_sizes):
+ assert len(stack_sizes) >= i + pointer_size
+ addr, = struct.unpack_from(pointer_fmt, stack_sizes, i)
+ i += pointer_size
+ # Parse LEB128
+ size = 0
+ for j in range(10):
+ b = stack_sizes[i]
+ i += 1
+ size += (b & 0x7f) << (7 * j)
+ if (b & 0x80) == 0:
+ break
+ if size >= MIN_STACK_SIZE:
+ ret.append(SymbolStack(addr, size))
+ return ret
+
+
+def TargetSize(symbols, symbol_filter=None):
+ ret = {}
+ for sym in symbols:
+ if not sym.size or (symbol_filter is not None and
+ sym.name not in symbol_filter):
+ continue
+ t = sym.typ.lower()
+ # We can remove symbols if they appear in multiple objects since they will
+ # be merged by the linker.
+ if symbol_filter is not None and (t == sym.typ or t in 'wv'):
+ symbol_filter.remove(sym.name)
+ ret.setdefault(t, 0)
+ ret[t] += sym.size
+ return ret
+
+
+def PrintStats(stats):
+ """Print a table with the size stats for a target"""
+ table = []
+ sum_bin_size = 0
+ sum_ram_size = 0
+
+ for objstat in stats:
+ bin_size = 0
+ ram_size = 0
+ for typ, size in objstat.size_map.items():
+ if typ in BIN_SIZE:
+ bin_size += size
+ if typ in RAM_SIZE:
+ ram_size += size
+ if typ not in BIN_SIZE + RAM_SIZE:
+ raise Exception('Unknown type "%s"' % typ)
+ if objstat.in_partition:
+ sum_bin_size += bin_size
+ sum_ram_size += ram_size
+
+ table.append((objstat.name, bin_size, ram_size))
+ mx_bin_size = max(row[1] for row in table)
+ mx_ram_size = max(row[2] for row in table)
+
+ table.append(('-- unknown --', mx_bin_size - sum_bin_size,
+ mx_ram_size - sum_ram_size))
+
+ # Print the table
+ print('%-32s %17s %17s' % ('Object name', 'Binary size', 'Static RAM size'))
+ for name, bin_size, ram_size in table:
+ print('%-32s %8d (%5.1f%%) %8d (%5.1f%%)' % (
+ name, bin_size, 100. * bin_size / mx_bin_size,
+ ram_size, (100. * ram_size / mx_ram_size) if mx_ram_size else 0))
+ print()
+
+
+def PrintStackStats(tgt_stack_sizes, top_entries=20):
+ if not tgt_stack_sizes:
+ return
+ print(' Stack Symbol name')
+ for i, (name, size) in zip(itertools.count(), tgt_stack_sizes.items()):
+ if top_entries > 0 and i >= top_entries:
+ break
+ print('%8d %s' % (size, name))
+ print()
+
+
+def PrintTopSymbols(tgt_top_symbols):
+ if not tgt_top_symbols:
+ return
+ print(' Size T Symbol name')
+ for size, typ, name in tgt_top_symbols:
+ print('%9d %s %s' % (size, typ, name))
+ print()
+
+
+def SizeStats(args):
+ """Main entry point of the program after parsing parameters.
+
+ Computes the size statistics of the given targets and their components."""
+ # The dictionary with the stats that we store on disk as a json. This includes
+ # one entry per passed args.target.
+ stats = {}
+
+ # Cache of Target object of a target.
+ tgts = {}
+
+ # Load all the targets.
+ pending = set(args.target)
+ while pending:
+ target = pending.pop()
+ tgt = LoadTarget(target, args.build_dir)
+ tgts[target] = tgt
+ if args.recursive:
+ for dep in tgt.deps:
+ if dep not in tgts:
+ pending.add(dep)
+
+ # Cache of symbols of a target.
+ syms = {}
+ # Load the symbols from the all targets and its deps.
+ all_deps = set(tgts.keys()).union(*[set(tgt.deps) for tgt in tgts.values()])
+ for entry in all_deps:
+ fn = os.path.join(args.build_dir,
+ tgts[entry].filename if entry in tgts else entry)
+ syms[entry] = LoadSymbols(fn)
+
+ for target in args.target:
+ tgt_stats = []
+ tgt = tgts[target]
+
+ tgt_syms = syms[target]
+ used_syms = set()
+ for sym in tgt_syms:
+ if sym.typ.lower() in BIN_SIZE + RAM_SIZE:
+ used_syms.add(sym.name)
+ elif sym.typ.lower() in IGNORE_SYMBOLS:
+ continue
+ else:
+ print('Unknown: %s %s' % (sym.typ, sym.name))
+
+ target_path = os.path.join(args.build_dir, tgt.filename)
+ sym_stacks = []
+ if not target_path.endswith('.a'):
+ sym_stacks = LoadStackSizes(target_path, args.binutils)
+ symbols_by_addr = {sym.address: sym for sym in tgt_syms
+ if sym.typ.lower() in 'tw'}
+ tgt_stack_sizes = collections.OrderedDict()
+ for sym_stack in sorted(sym_stacks, key=lambda s: -s.stack_size):
+ tgt_stack_sizes[
+ symbols_by_addr[sym_stack.address].name] = sym_stack.stack_size
+
+ tgt_top_symbols = []
+ if args.top_symbols:
+ tgt_top_symbols = [(sym.size, sym.typ, sym.name) for sym in tgt_syms
+ if sym.name in used_syms and sym.size]
+ tgt_top_symbols.sort(key=lambda t: (-t[0], t[2]))
+ tgt_top_symbols = tgt_top_symbols[:args.top_symbols]
+
+ tgt_size = TargetSize(tgt_syms)
+ tgt_stats.append(ObjectStats(target, False, tgt_size))
+
+ # Split out by SIMD.
+ for namespace in SIMD_NAMESPACES:
+ mangled = str(len(namespace)) + namespace
+ if not any(mangled in sym.name for sym in tgt_syms):
+ continue
+ ret = {}
+ for sym in tgt_syms:
+ if not sym.size or mangled not in sym.name:
+ continue
+ t = sym.typ.lower()
+ ret.setdefault(t, 0)
+ ret[t] += sym.size
+ # SIMD namespaces are not part of the partition, they are already included
+ # in the jpegxl-static normally.
+ if not ret:
+ continue
+ tgt_stats.append(ObjectStats('\\--> ' + namespace, False, ret))
+
+ for obj in tgt.deps:
+ dep_used_syms = used_syms.copy()
+ obj_size = TargetSize(syms[obj], used_syms)
+ if not obj_size:
+ continue
+ tgt_stats.append(ObjectStats(os.path.basename(obj), True, obj_size))
+ if args.recursive:
+ # Not really recursive, but it shows all the remaining deps at a second
+ # level.
+ for obj_dep in sorted(TargetTransitiveDeps(tgts, obj),
+ key=os.path.basename):
+ obj_dep_size = TargetSize(syms[obj_dep], dep_used_syms)
+ if not obj_dep_size:
+ continue
+ tgt_stats.append(ObjectStats(
+ ' '+ os.path.basename(obj_dep), False, obj_dep_size))
+
+ PrintStats(tgt_stats)
+ PrintStackStats(tgt_stack_sizes)
+ PrintTopSymbols(tgt_top_symbols)
+ stats[target] = {
+ 'build': tgt_stats,
+ 'stack': tgt_stack_sizes,
+ 'top': tgt_top_symbols,
+ }
+
+ if args.save:
+ with open(args.save, 'w') as f:
+ json.dump(stats, f)
+
+ # Check the maximum stack size.
+ exit_code = 0
+ if args.max_stack:
+ for name, size in tgt_stack_sizes.items():
+ if size > args.max_stack:
+ print('Error: %s exceeds stack limit: %d vs %d' % (
+ name, size, args.max_stack),
+ file=sys.stderr)
+ exit_code = 1
+
+ return exit_code
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('target', type=str, nargs='+',
+ help='target(s) to analyze')
+ parser.add_argument('--build-dir', default='build',
+ help='path to the build directory')
+ parser.add_argument('--save', default=None,
+ help='path to save the stats as JSON file')
+ parser.add_argument('-r', '--recursive', default=False, action='store_true',
+ help='Print recursive entries.')
+ parser.add_argument('--top-symbols', default=0, type=int,
+ help='Number of largest symbols to print')
+ parser.add_argument('--binutils', default='',
+ help='prefix path to binutils tools, such as '
+ 'aarch64-linux-gnu-')
+ parser.add_argument('--max-stack', default=None, type=int,
+ help=('Maximum static stack size of a function. If a '
+ 'static stack is larger it will exit with an error '
+ 'code.'))
+ args = parser.parse_args()
+ sys.exit(SizeStats(args))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/media/libjxl/src/tools/butteraugli_main.cc b/media/libjxl/src/tools/butteraugli_main.cc
new file mode 100644
index 0000000000..f212aadd7c
--- /dev/null
+++ b/media/libjxl/src/tools/butteraugli_main.cc
@@ -0,0 +1,142 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+#include <stdio.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/color_hints.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/butteraugli/butteraugli.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/enc_butteraugli_comparator.h"
+#include "lib/jxl/enc_butteraugli_pnorm.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_ops.h"
+
+namespace jxl {
+namespace {
+
+Status WriteImage(Image3F&& image, const std::string& filename) {
+ ThreadPoolInternal pool(4);
+ CodecInOut io;
+ io.metadata.m.SetUintSamples(8);
+ io.metadata.m.color_encoding = ColorEncoding::SRGB();
+ io.SetFromImage(std::move(image), io.metadata.m.color_encoding);
+ return EncodeToFile(io, filename, &pool);
+}
+
+Status RunButteraugli(const char* pathname1, const char* pathname2,
+ const std::string& distmap_filename,
+ const std::string& colorspace_hint, double p,
+ float intensity_target) {
+ extras::ColorHints color_hints;
+ if (!colorspace_hint.empty()) {
+ color_hints.Add("color_space", colorspace_hint);
+ }
+
+ CodecInOut io1;
+ ThreadPoolInternal pool(4);
+ if (!SetFromFile(pathname1, color_hints, &io1, &pool)) {
+ fprintf(stderr, "Failed to read image from %s\n", pathname1);
+ return false;
+ }
+
+ CodecInOut io2;
+ if (!SetFromFile(pathname2, color_hints, &io2, &pool)) {
+ fprintf(stderr, "Failed to read image from %s\n", pathname2);
+ return false;
+ }
+
+ if (io1.xsize() != io2.xsize()) {
+ fprintf(stderr, "Width mismatch: %" PRIuS " %" PRIuS "\n", io1.xsize(),
+ io2.xsize());
+ return false;
+ }
+ if (io1.ysize() != io2.ysize()) {
+ fprintf(stderr, "Height mismatch: %" PRIuS " %" PRIuS "\n", io1.ysize(),
+ io2.ysize());
+ return false;
+ }
+
+ ImageF distmap;
+ ButteraugliParams ba_params;
+ ba_params.hf_asymmetry = 0.8f;
+ ba_params.xmul = 1.0f;
+ ba_params.intensity_target = intensity_target;
+ const float distance = ButteraugliDistance(io1.Main(), io2.Main(), ba_params,
+ GetJxlCms(), &distmap, &pool);
+ printf("%.10f\n", distance);
+
+ double pnorm = ComputeDistanceP(distmap, ba_params, p);
+ printf("%g-norm: %f\n", p, pnorm);
+
+ if (!distmap_filename.empty()) {
+ float good = ButteraugliFuzzyInverse(1.5);
+ float bad = ButteraugliFuzzyInverse(0.5);
+ JXL_CHECK(
+ WriteImage(CreateHeatMapImage(distmap, good, bad), distmap_filename));
+ }
+ return true;
+}
+
+} // namespace
+} // namespace jxl
+
+int main(int argc, char** argv) {
+ if (argc < 3) {
+ fprintf(stderr,
+ "Usage: %s <reference> <distorted> [--distmap <distmap>] "
+ "[--intensity_target <intensity_target>]\n"
+ "[--colorspace <colorspace_hint>]\n"
+ "NOTE: images get converted to linear sRGB for butteraugli. Images"
+ " without attached profiles (such as ppm or pfm) are interpreted"
+ " as nonlinear sRGB. The hint format is RGB_D65_SRG_Rel_Lin for"
+ " linear sRGB. Intensity target is viewing conditions screen nits"
+ ", defaults to 80.\n",
+ argv[0]);
+ return 1;
+ }
+ std::string distmap;
+ std::string colorspace;
+ double p = 3;
+ float intensity_target = 80.0; // sRGB intensity target.
+ for (int i = 3; i < argc; i++) {
+ if (std::string(argv[i]) == "--distmap" && i + 1 < argc) {
+ distmap = argv[++i];
+ } else if (std::string(argv[i]) == "--colorspace" && i + 1 < argc) {
+ colorspace = argv[++i];
+ } else if (std::string(argv[i]) == "--intensity_target" && i + 1 < argc) {
+ intensity_target = std::stof(std::string(argv[i + 1]));
+ } else if (std::string(argv[i]) == "--pnorm" && i + 1 < argc) {
+ char* end;
+ p = strtod(argv[++i], &end);
+ if (end == argv[i]) {
+ fprintf(stderr, "Failed to parse pnorm \"%s\".\n", argv[i]);
+ return 1;
+ }
+ } else {
+ fprintf(stderr, "Unrecognized flag \"%s\".\n", argv[i]);
+ return 1;
+ }
+ }
+
+ return jxl::RunButteraugli(argv[1], argv[2], distmap, colorspace, p,
+ intensity_target)
+ ? 0
+ : 1;
+}
diff --git a/media/libjxl/src/tools/check_author.py b/media/libjxl/src/tools/check_author.py
new file mode 100755
index 0000000000..ae1c2798fb
--- /dev/null
+++ b/media/libjxl/src/tools/check_author.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+
+"""check_author.py: Check that a given author is listed in the AUTHORS file."""
+
+import argparse
+import fnmatch
+import os
+import re
+import sys
+
+
+def IsAuthorInFile(email, name, filename):
+ """Return whether we find the name/email in the authors filename"""
+ # Organization emails have emails listed as <*@domain.com>. This matches those
+ # patterns.
+ email_pattern_regex = re.compile(r'.*<([^>]+)>')
+
+ with open(filename, 'r') as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith('#') or not line:
+ continue
+ # Exact match for a line without an email is OK.
+ if line == name:
+ return True
+ # Exact email address match is OK, even if the name is different.
+ if fnmatch.fnmatch(line, '* <%s>' % email):
+ print(
+ "User %s <%s> matched with different name %s" % (name, email, line),
+ file=sys.stderr)
+ return True
+ # Organizations often have *@domain.com email patterns which don't match
+ # the name.
+ if '*' in line:
+ m = email_pattern_regex.match(line)
+ if m and fnmatch.fnmatch(email, m.group(1)):
+ print("User %s <%s> matched pattern %s" % (name, email, line),
+ file=sys.stderr)
+ return True
+ return False
+
+def IndividualsInAlphabeticOrder(filename):
+ """Checks if the names are in alphabetic order"""
+ with open(filename, 'r') as f:
+ lines = f.readlines()
+ individual_header = '# Individuals:\n'
+ if individual_header in lines:
+ individual_authors = lines[lines.index(individual_header) + 1:]
+ sorted_authors = sorted(individual_authors, key=str.casefold)
+ if sorted_authors == individual_authors:
+ print("Individual authors are sorted alphabetically.")
+ return True
+ else:
+ print("Individual authors are not sorted alphabetically."
+ " The expected order is:")
+ print(''.join(sorted_authors))
+ return False
+ else:
+ print("Cannot find line '# Individuals:' in file.")
+ return False
+
+
+def CheckAuthor(args):
+ authors_path = os.path.join(args.source_dir, 'AUTHORS')
+ author_in_file = IsAuthorInFile(
+ args.email, args.name, authors_path)
+ if not author_in_file:
+ print("User %s <%s> not found, please add yourself to the AUTHORS file" % (
+ args.name, args.email),
+ file=sys.stderr)
+
+ sorted_alphabetically = IndividualsInAlphabeticOrder(authors_path)
+ if not sorted_alphabetically:
+ print("Authors not in alphabetical order, please sort them.", file=sys.stderr)
+ if not author_in_file or not sorted_alphabetically:
+ if not args.dry_run:
+ sys.exit(1)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('email', type=str,
+ help='email of the commit author to check')
+ parser.add_argument('name', type=str,
+ help='name of the commit author to check')
+ parser.add_argument(
+ '--source-dir',
+ default=os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
+ help='path to the source directory where the AUTHORS file is located')
+ parser.add_argument('--dry-run', default=False, action='store_true',
+ help='Don\'t return an exit code in case of failure')
+ args = parser.parse_args()
+ CheckAuthor(args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/media/libjxl/src/tools/cjpeg_hdr.cc b/media/libjxl/src/tools/cjpeg_hdr.cc
new file mode 100644
index 0000000000..cfe272ee25
--- /dev/null
+++ b/media/libjxl/src/tools/cjpeg_hdr.cc
@@ -0,0 +1,306 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <tuple>
+
+#undef HWY_TARGET_INCLUDE
+#define HWY_TARGET_INCLUDE "tools/cjpeg_hdr.cc"
+#include <hwy/foreach_target.h>
+#include <hwy/highway.h>
+
+#include "lib/extras/codec.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/common.h"
+#include "lib/jxl/enc_adaptive_quantization.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_transforms.h"
+#include "lib/jxl/enc_xyb.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_metadata.h"
+#include "lib/jxl/image_ops.h"
+#include "lib/jxl/jpeg/dec_jpeg_data_writer.h"
+#include "lib/jxl/quant_weights.h"
+
+HWY_BEFORE_NAMESPACE();
+namespace jpegxl {
+namespace tools {
+namespace HWY_NAMESPACE {
+void FillJPEGData(const jxl::Image3F& ycbcr, const jxl::PaddedBytes& icc,
+ const jxl::ImageF& quant_field,
+ const jxl::FrameDimensions& frame_dim,
+ jxl::jpeg::JPEGData* out) {
+ // JFIF
+ out->marker_order.push_back(0xE0);
+ out->app_data.emplace_back(std::vector<uint8_t>{
+ 0xe0, // Marker
+ 0, 16, // Length
+ 'J', 'F', 'I', 'F', '\0', // ID
+ 1, 1, // Version (1.1)
+ 0, // No density units
+ 0, 1, 0, 1, // Pixel density 1
+ 0, 0 // No thumbnail
+ });
+ // ICC
+ if (!icc.empty()) {
+ out->marker_order.push_back(0xE2);
+ std::vector<uint8_t> icc_marker(17 + icc.size());
+ icc_marker[0] = 0xe2;
+ icc_marker[1] = (icc_marker.size() - 1) >> 8;
+ icc_marker[2] = (icc_marker.size() - 1) & 0xFF;
+ memcpy(&icc_marker[3], "ICC_PROFILE", 12);
+ icc_marker[15] = 1;
+ icc_marker[16] = 1;
+ memcpy(&icc_marker[17], icc.data(), icc.size());
+ out->app_data.push_back(std::move(icc_marker));
+ }
+
+ // DQT
+ out->marker_order.emplace_back(0xdb);
+ out->quant.resize(2);
+ out->quant[0].is_last = false;
+ out->quant[0].index = 0;
+ out->quant[1].is_last = true;
+ out->quant[1].index = 1;
+ jxl::DequantMatrices dequant;
+
+ // mozjpeg q99
+ int qluma[64] = {
+ 1, 1, 1, 1, 1, 1, 1, 2, //
+ 1, 1, 1, 1, 1, 1, 1, 2, //
+ 1, 1, 1, 1, 1, 1, 2, 3, //
+ 1, 1, 1, 1, 1, 1, 2, 3, //
+ 1, 1, 1, 1, 1, 2, 3, 4, //
+ 1, 1, 1, 1, 2, 2, 3, 5, //
+ 1, 1, 2, 2, 3, 3, 5, 6, //
+ 2, 2, 3, 3, 4, 5, 6, 8, //
+ };
+ // mozjpeg q95
+ int qchroma[64] = {
+ 2, 2, 2, 2, 3, 4, 6, 9, //
+ 2, 2, 2, 3, 3, 4, 5, 8, //
+ 2, 2, 2, 3, 4, 6, 9, 14, //
+ 2, 3, 3, 4, 5, 7, 11, 16, //
+ 3, 3, 4, 5, 7, 9, 13, 19, //
+ 4, 4, 6, 7, 9, 12, 17, 24, //
+ 6, 5, 9, 11, 13, 17, 23, 31, //
+ 9, 8, 14, 16, 19, 24, 31, 42, //
+ };
+ // Disable quantization for now.
+ std::fill(std::begin(qluma), std::end(qluma), 1);
+ std::fill(std::begin(qchroma), std::end(qchroma), 1);
+
+ memcpy(out->quant[0].values.data(), qluma, sizeof(qluma));
+ memcpy(out->quant[1].values.data(), qchroma, sizeof(qchroma));
+
+ // SOF
+ out->marker_order.emplace_back(0xc2);
+ out->components.resize(3);
+ out->height = frame_dim.ysize;
+ out->width = frame_dim.xsize_padded;
+ out->components[0].id = 1;
+ out->components[1].id = 2;
+ out->components[2].id = 3;
+ out->components[0].h_samp_factor = out->components[1].h_samp_factor =
+ out->components[2].h_samp_factor = out->components[0].v_samp_factor =
+ out->components[1].v_samp_factor = out->components[2].v_samp_factor =
+ 1;
+ out->components[0].width_in_blocks = out->components[1].width_in_blocks =
+ out->components[2].width_in_blocks = frame_dim.xsize_blocks;
+ out->components[0].quant_idx = 0;
+ out->components[1].quant_idx = 1;
+ out->components[2].quant_idx = 1;
+ out->components[0].coeffs.resize(frame_dim.xsize_blocks *
+ frame_dim.ysize_blocks * 64);
+ out->components[1].coeffs.resize(frame_dim.xsize_blocks *
+ frame_dim.ysize_blocks * 64);
+ out->components[2].coeffs.resize(frame_dim.xsize_blocks *
+ frame_dim.ysize_blocks * 64);
+
+ HWY_ALIGN float scratch_space[2 * 64];
+
+ for (size_t c = 0; c < 3; c++) {
+ int* qt = c == 0 ? qluma : qchroma;
+ for (size_t by = 0; by < frame_dim.ysize_blocks; by++) {
+ for (size_t bx = 0; bx < frame_dim.xsize_blocks; bx++) {
+ float deadzone = 0.5f / quant_field.Row(by)[bx];
+ // Disable quantization for now.
+ deadzone = 0;
+ auto q = [&](float coeff, size_t x, size_t y) -> int {
+ size_t pos = x * 8 + y;
+ float scoeff = coeff / qt[pos];
+ if (pos == 0) {
+ return std::round(scoeff);
+ }
+ if (std::abs(scoeff) < deadzone) return 0;
+ if (std::abs(scoeff) < 2 * deadzone && x + y >= 7) return 0;
+ return std::round(scoeff);
+ };
+ HWY_ALIGN float dct[64];
+ TransformFromPixels(jxl::AcStrategy::Type::DCT,
+ ycbcr.PlaneRow(c, 8 * by) + 8 * bx,
+ ycbcr.PixelsPerRow(), dct, scratch_space);
+ for (size_t iy = 0; iy < 8; iy++) {
+ for (size_t ix = 0; ix < 8; ix++) {
+ float coeff = dct[iy * 8 + ix] * 2040; // not a typo
+ out->components[c]
+ .coeffs[(frame_dim.xsize_blocks * by + bx) * 64 + ix * 8 + iy] =
+ q(coeff, ix, iy);
+ }
+ }
+ }
+ }
+ }
+
+ // DHT
+ // TODO: optimize
+ out->marker_order.emplace_back(0xC4);
+ out->huffman_code.resize(2);
+ out->huffman_code[0].slot_id = 0x00; // DC
+ out->huffman_code[0].counts = {{0, 0, 0, 0, 13}};
+ std::iota(out->huffman_code[0].values.begin(),
+ out->huffman_code[0].values.end(), 0);
+ out->huffman_code[0].is_last = false;
+
+ out->huffman_code[1].slot_id = 0x10; // AC
+ out->huffman_code[1].counts = {{0, 0, 0, 0, 0, 0, 0, 0, 255}};
+ std::iota(out->huffman_code[1].values.begin(),
+ out->huffman_code[1].values.end(), 0);
+ out->huffman_code[1].is_last = true;
+
+ // SOS
+ for (size_t _ = 0; _ < 7; _++) {
+ out->marker_order.emplace_back(0xDA);
+ }
+ out->scan_info.resize(7);
+ // DC
+ // comp id, DC tbl, AC tbl
+ out->scan_info[0].num_components = 3;
+ out->scan_info[0].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0},
+ jxl::jpeg::JPEGComponentScanInfo{1, 0, 0},
+ jxl::jpeg::JPEGComponentScanInfo{2, 0, 0}}};
+ out->scan_info[0].Ss = 0;
+ out->scan_info[0].Se = 0;
+ out->scan_info[0].Ah = out->scan_info[0].Al = 0;
+ // AC 1 - highest bits
+ out->scan_info[1].num_components = 1;
+ out->scan_info[1].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0}}};
+ out->scan_info[1].Ss = 1;
+ out->scan_info[1].Se = 63;
+ out->scan_info[1].Ah = 0;
+ out->scan_info[1].Al = 1;
+
+ // Copy for X / B-Y
+ out->scan_info[2] = out->scan_info[1];
+ out->scan_info[2].components[0].comp_idx = 1;
+ out->scan_info[3] = out->scan_info[1];
+ out->scan_info[3].components[0].comp_idx = 2;
+
+ // AC 2 - lowest bit
+ out->scan_info[4].num_components = 1;
+ out->scan_info[4].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0}}};
+ out->scan_info[4].Ss = 1;
+ out->scan_info[4].Se = 63;
+ out->scan_info[4].Ah = 1;
+ out->scan_info[4].Al = 0;
+
+ // Copy for X / B-Y
+ out->scan_info[5] = out->scan_info[4];
+ out->scan_info[5].components[0].comp_idx = 1;
+ out->scan_info[6] = out->scan_info[4];
+ out->scan_info[6].components[0].comp_idx = 2;
+
+ // EOI
+ out->marker_order.push_back(0xd9);
+}
+} // namespace HWY_NAMESPACE
+} // namespace tools
+} // namespace jpegxl
+HWY_AFTER_NAMESPACE();
+
+#if HWY_ONCE
+
+namespace jpegxl {
+namespace tools {
+
+HWY_EXPORT(FillJPEGData);
+
+int HBDJPEGMain(int argc, const char* argv[]) {
+ if (argc < 3) {
+ fprintf(stderr, "Usage: %s input output.jpg\n", argv[0]);
+ return 1;
+ }
+ fprintf(stderr, "Compressing %s to %s\n", argv[1], argv[2]);
+ jxl::CodecInOut io;
+ if (!jxl::SetFromFile(argv[1], jxl::extras::ColorHints{}, &io)) {
+ fprintf(stderr, "Failed to read image %s.\n", argv[1]);
+ return 1;
+ }
+ jxl::Image3F ycbcr(jxl::RoundUpToBlockDim(io.xsize()),
+ jxl::RoundUpToBlockDim(io.ysize()));
+ ycbcr.ShrinkTo(io.xsize(), io.ysize());
+ jxl::FrameDimensions frame_dim;
+ frame_dim.Set(io.xsize(), io.ysize(), 0, 0, 0, false, 1);
+ for (size_t y = 0; y < ycbcr.ysize(); y++) {
+ for (size_t x = 0; x < ycbcr.xsize(); x++) {
+ float r = io.Main().color()->PlaneRow(0, y)[x];
+ float g = io.Main().color()->PlaneRow(1, y)[x];
+ float b = io.Main().color()->PlaneRow(2, y)[x];
+ ycbcr.PlaneRow(0, y)[x] =
+ 0.299 * r + 0.587 * g + 0.114 * b - (128. / 255.);
+ ycbcr.PlaneRow(1, y)[x] = -0.168736 * r - 0.331264 * g + 0.5 * b;
+ ycbcr.PlaneRow(2, y)[x] = 0.5 * r - 0.418688 * g - 0.081312 * b;
+ }
+ }
+ jxl::Image3F rgb2(ycbcr.xsize(), ycbcr.ysize());
+ jxl::Image3F ycbcr2(ycbcr.xsize(), ycbcr.ysize());
+ for (size_t y = 0; y < ycbcr.ysize(); y++) {
+ for (size_t x = 0; x < ycbcr.xsize(); x++) {
+ ycbcr2.PlaneRow(0, y)[x] = ycbcr.PlaneRow(1, y)[x];
+ ycbcr2.PlaneRow(1, y)[x] = ycbcr.PlaneRow(0, y)[x];
+ ycbcr2.PlaneRow(2, y)[x] = ycbcr.PlaneRow(2, y)[x];
+ }
+ }
+ jxl::YcbcrToRgb(ycbcr2, &rgb2, jxl::Rect(ycbcr));
+
+ PadImageToBlockMultipleInPlace(&ycbcr);
+
+ jxl::Image3F opsin(jxl::RoundUpToBlockDim(io.xsize()),
+ jxl::RoundUpToBlockDim(io.ysize()));
+ opsin.ShrinkTo(io.xsize(), io.ysize());
+ jxl::ToXYB(io.Main(), nullptr, &opsin, jxl::GetJxlCms());
+ PadImageToBlockMultipleInPlace(&opsin);
+ jxl::ImageF mask;
+ jxl::ImageF qf =
+ InitialQuantField(1.0, opsin, frame_dim, nullptr, 1.0, &mask);
+
+ jxl::CodecInOut out;
+ out.Main().jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>();
+ HWY_DYNAMIC_DISPATCH(FillJPEGData)
+ (ycbcr, io.metadata.m.color_encoding.ICC(), qf, frame_dim,
+ out.Main().jpeg_data.get());
+ jxl::PaddedBytes output;
+ if (!jxl::jpeg::EncodeImageJPGCoefficients(&out, &output)) {
+ return 1;
+ }
+ if (!jxl::WriteFile(output, argv[2])) {
+ fprintf(stderr, "Failed to write to \"%s\"\n", argv[2]);
+ return 1;
+ }
+ return 0;
+}
+
+} // namespace tools
+} // namespace jpegxl
+
+int main(int argc, const char** argv) {
+ return jpegxl::tools::HBDJPEGMain(argc, argv);
+}
+#endif
diff --git a/media/libjxl/src/tools/cjxl.cc b/media/libjxl/src/tools/cjxl.cc
new file mode 100644
index 0000000000..7d61feba64
--- /dev/null
+++ b/media/libjxl/src/tools/cjxl.cc
@@ -0,0 +1,769 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/cjxl.h"
+
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include <algorithm>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/time.h"
+#include "lib/jxl/aux_out.h"
+#include "lib/jxl/base/cache_aligned.h"
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/profiler.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/common.h"
+#include "lib/jxl/enc_cache.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_file.h"
+#include "lib/jxl/enc_params.h"
+#include "lib/jxl/frame_header.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/jpeg/enc_jpeg_data.h"
+#include "lib/jxl/modular/encoding/encoding.h"
+#include "tools/args.h"
+#include "tools/box/box.h"
+#include "tools/speed_stats.h"
+
+namespace jpegxl {
+namespace tools {
+namespace {
+
+static inline bool ParseSpeedTier(const char* arg, jxl::SpeedTier* out) {
+ return jxl::ParseSpeedTier(arg, out);
+}
+static inline bool ParseColorTransform(const char* arg,
+ jxl::ColorTransform* out) {
+ size_t value = 0;
+ bool ret = ParseUnsigned(arg, &value);
+ if (ret && value > 2) ret = false;
+ if (ret) *out = jxl::ColorTransform(value);
+ return ret;
+}
+static inline bool ParseIntensityTarget(const char* arg, float* out) {
+ return ParseFloat(arg, out) && *out > 0;
+}
+static inline bool ParsePhotonNoiseParameter(const char* arg, float* out) {
+ return strncmp(arg, "ISO", 3) == 0 && ParseFloat(arg + 3, out) && *out > 0;
+}
+
+// Proposes a distance to try for a given bpp target. This could depend
+// on the entropy in the image, too, but let's start with something.
+static double ApproximateDistanceForBPP(double bpp) {
+ return 1.704 * pow(bpp, -0.804);
+}
+
+jxl::Status LoadSaliencyMap(const std::string& filename_heatmap,
+ jxl::ThreadPool* pool, jxl::ImageF* out_map) {
+ jxl::CodecInOut io_heatmap;
+ if (!SetFromFile(filename_heatmap, jxl::extras::ColorHints(), &io_heatmap,
+ pool)) {
+ return JXL_FAILURE("Could not load heatmap.");
+ }
+ *out_map = std::move(io_heatmap.Main().color()->Plane(0));
+ return true;
+}
+
+void SetParametersForSizeOrBitrate(jxl::ThreadPoolInternal* pool,
+ const size_t pixels, CompressArgs* args) {
+ CompressArgs s = *args; // Args for search.
+
+ // If fixed size, convert to bitrate.
+ if (s.params.target_size > 0) {
+ s.params.target_bitrate = s.params.target_size * 8.0 / pixels;
+ s.params.target_size = 0;
+ }
+ const double target_size = s.params.target_bitrate * (1 / 8.) * pixels;
+
+ double dist = ApproximateDistanceForBPP(s.params.target_bitrate);
+ s.params.target_bitrate = 0;
+ double best_dist = 1.0;
+ double best_loss = 1e99;
+
+ jxl::CodecInOut io;
+ double decode_mps = 0;
+ if (!LoadAll(*args, pool, &io, &decode_mps)) {
+ s.params.butteraugli_distance = static_cast<float>(dist);
+ printf("couldn't load image\n");
+ return;
+ }
+
+ for (int i = 0; i < 7; ++i) {
+ s.params.butteraugli_distance = static_cast<float>(dist);
+ jxl::PaddedBytes candidate;
+ bool ok =
+ CompressJxl(io, decode_mps, pool, s, &candidate, /*print_stats=*/false);
+ if (!ok) {
+ printf(
+ "Compression error occurred during the search for best size. "
+ "Trying with butteraugli distance %.15g\n",
+ best_dist);
+ break;
+ }
+ printf("Butteraugli distance %.3f yields %6" PRIuS " bytes, %.3f bpp.\n",
+ dist, candidate.size(), candidate.size() * 8.0 / pixels);
+ const double ratio = static_cast<double>(candidate.size()) / target_size;
+ const double loss = std::max(ratio, 1.0 / std::max(ratio, 1e-30));
+ if (best_loss > loss) {
+ best_dist = dist;
+ best_loss = loss;
+ }
+ dist *= ratio;
+ if (dist < 0.01) {
+ dist = 0.01;
+ }
+ if (dist >= 16.0) {
+ dist = 16.0;
+ }
+ }
+ args->params.butteraugli_distance = static_cast<float>(best_dist);
+ args->params.target_bitrate = 0;
+ args->params.target_size = 0;
+}
+
+const char* ModeFromArgs(const CompressArgs& args) {
+ if (args.jpeg_transcode) return "JPEG";
+ if (args.params.modular_mode) return "Modular";
+ return "VarDCT";
+}
+
+std::string QualityFromArgs(const CompressArgs& args) {
+ char buf[100];
+ if (args.jpeg_transcode) {
+ snprintf(buf, sizeof(buf), "lossless transcode");
+ } else if (args.params.IsLossless()) {
+ snprintf(buf, sizeof(buf), "lossless");
+ } else {
+ snprintf(buf, sizeof(buf), "d%.3f", args.params.butteraugli_distance);
+ }
+ return buf;
+}
+
+void PrintMode(jxl::ThreadPoolInternal* pool, const jxl::CodecInOut& io,
+ const double decode_mps, const CompressArgs& args) {
+ const char* mode = ModeFromArgs(args);
+ const char* speed = SpeedTierName(args.params.speed_tier);
+ const std::string quality = QualityFromArgs(args);
+ fprintf(stderr,
+ "Read %" PRIuS "x%" PRIuS
+ " image, %.1f MP/s\n"
+ "Encoding [%s%s, %s, %s",
+ io.xsize(), io.ysize(), decode_mps,
+ (args.use_container ? "Container | " : ""), mode, quality.c_str(),
+ speed);
+ if (args.use_container) {
+ if (args.jpeg_transcode && args.store_jpeg_metadata)
+ fprintf(stderr, " | JPEG reconstruction data");
+ if (!io.blobs.exif.empty())
+ fprintf(stderr, " | %" PRIuS "-byte Exif", io.blobs.exif.size());
+ if (!io.blobs.xmp.empty())
+ fprintf(stderr, " | %" PRIuS "-byte XMP", io.blobs.xmp.size());
+ if (!io.blobs.jumbf.empty())
+ fprintf(stderr, " | %" PRIuS "-byte JUMBF", io.blobs.jumbf.size());
+ }
+ fprintf(stderr, "], %" PRIuS " threads.\n", pool->NumWorkerThreads());
+}
+
+} // namespace
+
+void CompressArgs::AddCommandLineOptions(CommandLineParser* cmdline) {
+ // Positional arguments.
+ cmdline->AddPositionalOption("INPUT", /* required = */ true,
+ "the input can be "
+#if JPEGXL_ENABLE_APNG
+ "PNG, APNG, "
+#endif
+#if JPEGXL_ENABLE_GIF
+ "GIF, "
+#endif
+#if JPEGXL_ENABLE_JPEG
+ "JPEG, "
+#else
+ "JPEG (lossless recompression only), "
+#endif
+#if JPEGXL_ENABLE_EXR
+ "EXR, "
+#endif
+ "PPM, PFM, or PGX",
+ &file_in);
+ cmdline->AddPositionalOption(
+ "OUTPUT", /* required = */ true,
+ "the compressed JXL output file (can be omitted for benchmarking)",
+ &file_out);
+
+ // Flags.
+ // TODO(lode): also add options to add exif/xmp/other metadata in the
+ // container.
+ // TODO(lode): decide on good name for this flag: box, container, bmff, ...
+ cmdline->AddOptionFlag(
+ '\0', "container",
+ "Always encode using container format (default: only if needed)",
+ &use_container, &SetBooleanTrue, 1);
+
+ cmdline->AddOptionFlag('\0', "strip",
+ "Do not encode using container format (strips "
+ "Exif/XMP/JPEG bitstream reconstruction data)",
+ &no_container, &SetBooleanTrue, 2);
+
+ cmdline->AddOptionFlag('\0', "strip_jpeg_metadata",
+ "Do not encode JPEG bitstream reconstruction data",
+ &store_jpeg_metadata, &SetBooleanFalse, 2);
+
+ // Target distance/size/bpp
+ opt_distance_id = cmdline->AddOptionValue(
+ 'd', "distance", "maxError",
+ ("Max. butteraugli distance, lower = higher quality. Range: 0 .. 25.\n"
+ " 0.0 = mathematically lossless. Default for already-lossy input "
+ "(JPEG/GIF).\n"
+ " 1.0 = visually lossless. Default for other input.\n"
+ " Recommended range: 0.5 .. 3.0."),
+ &params.butteraugli_distance, &ParseFloat);
+ opt_target_size_id = cmdline->AddOptionValue(
+ '\0', "target_size", "N",
+ ("Aim at file size of N bytes.\n"
+ " Compresses to 1 % of the target size in ideal conditions.\n"
+ " Runs the same algorithm as --target_bpp"),
+ &params.target_size, &ParseUnsigned, 1);
+ opt_target_bpp_id = cmdline->AddOptionValue(
+ '\0', "target_bpp", "BPP",
+ ("Aim at file size that has N bits per pixel.\n"
+ " Compresses to 1 % of the target BPP in ideal conditions."),
+ &params.target_bitrate, &ParseFloat, 1);
+
+ // High-level options
+ opt_quality_id = cmdline->AddOptionValue(
+ 'q', "quality", "QUALITY",
+ "Quality setting (is remapped to --distance). Range: -inf .. 100.\n"
+ " 100 = mathematically lossless. Default for already-lossy input "
+ "(JPEG/GIF).\n Positive quality values roughly match libjpeg quality.",
+ &quality, &ParseFloat);
+
+ cmdline->AddOptionValue(
+ 'e', "effort", "EFFORT",
+ "Encoder effort setting. Range: 1 .. 9.\n"
+ " Default: 7. Higher number is more effort (slower).",
+ &params.speed_tier, &ParseSpeedTier);
+
+ cmdline->AddOptionValue('\0', "brotli_effort", "B_EFFORT",
+ "Brotli effort setting. Range: 0 .. 11.\n"
+ " Default: -1 (based on EFFORT). Higher number is "
+ "more effort (slower).",
+ &params.brotli_effort, &ParseSigned, -1);
+
+ cmdline->AddOptionValue(
+ 's', "speed", "ANIMAL",
+ "Deprecated synonym for --effort. Valid values are:\n"
+ " lightning (1), thunder, falcon, cheetah, hare, wombat, squirrel, "
+ "kitten, tortoise (9)\n"
+ " Default: squirrel. Values are in order from faster to slower.\n",
+ &params.speed_tier, &ParseSpeedTier, 2);
+
+ cmdline->AddOptionValue('\0', "faster_decoding", "AMOUNT",
+ "Favour higher decoding speed. 0 = default, higher "
+ "values give higher speed at the expense of quality",
+ &params.decoding_speed_tier, &ParseUnsigned, 2);
+
+ cmdline->AddOptionFlag('p', "progressive",
+ "Enable progressive/responsive decoding.",
+ &progressive, &SetBooleanTrue);
+
+ cmdline->AddOptionFlag('\0', "premultiply",
+ "Force premultiplied (associated) alpha.",
+ &force_premultiplied, &SetBooleanTrue, 1);
+ cmdline->AddOptionValue('\0', "keep_invisible", "0|1",
+ "force disable/enable preserving color of invisible "
+ "pixels (default: 1 if lossless, 0 if lossy).",
+ &params.keep_invisible, &ParseOverride, 1);
+
+ cmdline->AddOptionFlag('\0', "centerfirst",
+ "Put center groups first in the compressed file.",
+ &params.centerfirst, &SetBooleanTrue, 1);
+
+ cmdline->AddOptionValue('\0', "center_x", "0..XSIZE",
+ "Put center groups first in the compressed file.",
+ &params.center_x, &ParseUnsigned, 1);
+ cmdline->AddOptionValue('\0', "center_y", "0..YSIZE",
+ "Put center groups first in the compressed file.",
+ &params.center_y, &ParseUnsigned, 1);
+
+ // Flags.
+ cmdline->AddOptionFlag('\0', "progressive_ac",
+ "Use the progressive mode for AC.",
+ &params.progressive_mode, &SetBooleanTrue, 1);
+ cmdline->AddOptionFlag('\0', "qprogressive_ac",
+ "Use the progressive mode for AC.",
+ &params.qprogressive_mode, &SetBooleanTrue, 1);
+ cmdline->AddOptionValue('\0', "progressive_dc", "num_dc_frames",
+ "Use progressive mode for DC.",
+ &params.progressive_dc, &ParseSigned, 1);
+ cmdline->AddOptionFlag('m', "modular",
+ "Use the modular mode (lossy / lossless).",
+ &params.modular_mode, &SetBooleanTrue, 1);
+ cmdline->AddOptionFlag('\0', "use_new_heuristics",
+ "use new and not yet ready encoder heuristics",
+ &params.use_new_heuristics, &SetBooleanTrue, 2);
+
+ // JPEG modes: parallel Brunsli, pixels to JPEG, or JPEG to Brunsli
+ cmdline->AddOptionFlag('j', "jpeg_transcode",
+ "Do lossy transcode of input JPEG file (decode to "
+ "pixels instead of doing lossless transcode).",
+ &jpeg_transcode, &SetBooleanFalse, 1);
+ cmdline->AddOptionFlag('\0', "jpeg_transcode_disable_cfl",
+ "Disable CFL for lossless JPEG recompression",
+ &params.force_cfl_jpeg_recompression, &SetBooleanFalse,
+ 2);
+
+ cmdline->AddOptionValue('\0', "num_threads", "N",
+ "number of worker threads (zero = none).",
+ &num_threads, &ParseUnsigned, 1);
+ cmdline->AddOptionValue('\0', "num_reps", "N", "how many times to compress.",
+ &num_reps, &ParseUnsigned, 1);
+
+ cmdline->AddOptionValue('\0', "noise", "0|1",
+ "force disable/enable noise generation.",
+ &params.noise, &ParseOverride, 1);
+ cmdline->AddOptionValue(
+ '\0', "photon_noise", "ISO3200",
+ "Set the noise to approximately what it would be at a given nominal "
+ "exposure on a 35mm camera. For formats other than 35mm, or when the "
+ "whole sensor was not used, you can multiply the ISO value by the "
+ "equivalence ratio squared, for example by 2.25 for an APS-C camera.",
+ &params.photon_noise_iso, &ParsePhotonNoiseParameter, 1);
+ cmdline->AddOptionValue('\0', "dots", "0|1",
+ "force disable/enable dots generation.", &params.dots,
+ &ParseOverride, 1);
+ cmdline->AddOptionValue('\0', "patches", "0|1",
+ "force disable/enable patches generation.",
+ &params.patches, &ParseOverride, 1);
+ cmdline->AddOptionValue(
+ '\0', "resampling", "-1|1|2|4|8",
+ "Subsample all color channels by this factor, or use -1 to choose the "
+ "resampling factor based on distance.",
+ &params.resampling, &ParseSigned, 0);
+ cmdline->AddOptionValue(
+ '\0', "ec_resampling", "1|2|4|8",
+ "Subsample all extra channels by this factor. If this value is smaller "
+ "than the resampling of color channels, it will be increased to match.",
+ &params.ec_resampling, &ParseSigned, 2);
+ cmdline->AddOptionFlag('\0', "already_downsampled",
+ "Do not downsample the given input before encoding, "
+ "but still signal that the decoder should upsample.",
+ &params.already_downsampled, &SetBooleanTrue, 2);
+
+ cmdline->AddOptionValue(
+ '\0', "epf", "-1..3",
+ "Edge preserving filter level (-1 = choose based on quality, default)",
+ &params.epf, &ParseSigned, 1);
+
+ cmdline->AddOptionValue('\0', "gaborish", "0|1",
+ "force disable/enable gaborish.", &params.gaborish,
+ &ParseOverride, 1);
+
+ opt_intensity_target_id = cmdline->AddOptionValue(
+ '\0', "intensity_target", "N",
+ ("Intensity target of monitor in nits, higher\n"
+ " results in higher quality image. Must be strictly positive.\n"
+ " Default is 255 for standard images, 4000 for input images known to\n"
+ " to have PQ or HLG transfer function."),
+ &intensity_target, &ParseIntensityTarget, 1);
+
+ cmdline->AddOptionValue('\0', "saliency_num_progressive_steps", "N", nullptr,
+ &params.saliency_num_progressive_steps,
+ &ParseUnsigned, 2);
+ cmdline->AddOptionValue('\0', "saliency_map_filename", "STRING", nullptr,
+ &saliency_map_filename, &ParseString, 2);
+ cmdline->AddOptionValue('\0', "saliency_threshold", "0..1", nullptr,
+ &params.saliency_threshold, &ParseFloat, 2);
+
+ cmdline->AddOptionValue(
+ 'x', "dec-hints", "key=value",
+ "color_space indicates the ColorEncoding, see Description();\n"
+ "icc_pathname refers to a binary file containing an ICC profile.",
+ &color_hints, &ParseAndAppendKeyValue, 1);
+
+ cmdline->AddOptionValue(
+ '\0', "override_bitdepth", "0=use from image, 1-32=override",
+ "If nonzero, store the given bit depth in the JPEG XL file metadata"
+ " (1-32), instead of using the bit depth from the original input"
+ " image.",
+ &override_bitdepth, &ParseUnsigned, 2);
+
+ opt_color_id = cmdline->AddOptionValue(
+ 'c', "colortransform", "0..2", "0=XYB, 1=None, 2=YCbCr",
+ &params.color_transform, &ParseColorTransform, 2);
+
+ // modular mode options
+ cmdline->AddOptionValue(
+ 'I', "iterations", "F",
+ "[modular encoding] fraction of pixels used to learn MA trees "
+ "(default=0.5, try 0 for no MA and fast decode)",
+ &params.options.nb_repeats, &ParseFloat, 2);
+
+ cmdline->AddOptionValue(
+ 'C', "colorspace", "K",
+ ("[modular encoding] color transform: 0=RGB, 1=YCoCg, "
+ "2-37=RCT (default: try several, depending on speed)"),
+ &params.colorspace, &ParseSigned, 1);
+
+ opt_m_group_size_id = cmdline->AddOptionValue(
+ 'g', "group-size", "K",
+ ("[modular encoding] set group size to 128 << K "
+ "(default: 1 or 2)"),
+ &params.modular_group_size_shift, &ParseUnsigned, 1);
+
+ cmdline->AddOptionValue(
+ 'P', "predictor", "K",
+ "[modular encoding] predictor(s) to use: 0=zero, "
+ "1=left, 2=top, 3=avg0, 4=select, 5=gradient, 6=weighted, "
+ "7=topright, 8=topleft, 9=leftleft, 10=avg1, 11=avg2, 12=avg3, "
+ "13=toptop predictive average "
+ "14=mix 5 and 6, 15=mix everything. Default 14, at slowest speed "
+ "default 15",
+ &params.options.predictor, &ParsePredictor, 1);
+
+ cmdline->AddOptionValue(
+ 'E', "extra-properties", "K",
+ "[modular encoding] number of extra MA tree properties to use",
+ &params.options.max_properties, &ParseSigned, 2);
+
+ cmdline->AddOptionValue('\0', "palette", "K",
+ "[modular encoding] use a palette if image has at "
+ "most K colors (default: 1024)",
+ &params.palette_colors, &ParseSigned, 1);
+
+ cmdline->AddOptionFlag(
+ '\0', "lossy-palette",
+ "[modular encoding] quantize to a palette that has fewer entries than "
+ "would be necessary for perfect preservation; for the time being, it is "
+ "recommended to set --palette=0 with this option to use the default "
+ "palette only",
+ &params.lossy_palette, &SetBooleanTrue, 1);
+
+ cmdline->AddOptionValue(
+ 'X', "pre-compact", "PERCENT",
+ ("[modular encoding] compact channels (globally) if ratio "
+ "used/range is below this (default: 80%)"),
+ &params.channel_colors_pre_transform_percent, &ParseFloat, 2);
+
+ cmdline->AddOptionValue(
+ 'Y', "post-compact", "PERCENT",
+ ("[modular encoding] compact channels (per-group) if ratio "
+ "used/range is below this (default: 80%)"),
+ &params.channel_colors_percent, &ParseFloat, 2);
+
+ cmdline->AddOptionValue('R', "responsive", "K",
+ "[modular encoding] do Squeeze transform, 0=false, "
+ "1=true (default: true if lossy, false if lossless)",
+ &params.responsive, &ParseSigned, 1);
+
+ cmdline->AddOptionFlag('V', "version", "Print version number and exit",
+ &version, &SetBooleanTrue, 1);
+ cmdline->AddOptionFlag('\0', "quiet", "Be more silent", &quiet,
+ &SetBooleanTrue, 1);
+ cmdline->AddOptionValue('\0', "print_profile", "0|1",
+ "Print timing information before exiting",
+ &print_profile, &ParseOverride, 1);
+
+ cmdline->AddOptionFlag(
+ 'v', "verbose",
+ "Verbose output; can be repeated, also applies to help (!).",
+ &params.verbose, &SetBooleanTrue);
+}
+
+jxl::Status CompressArgs::ValidateArgs(const CommandLineParser& cmdline) {
+ params.file_in = file_in;
+ params.file_out = file_out;
+
+ if (file_in == nullptr) {
+ fprintf(stderr, "Missing INPUT filename.\n");
+ return false;
+ }
+
+ bool got_distance = cmdline.GetOption(opt_distance_id)->matched();
+ bool got_target_size = cmdline.GetOption(opt_target_size_id)->matched();
+ bool got_target_bpp = cmdline.GetOption(opt_target_bpp_id)->matched();
+ bool got_quality = cmdline.GetOption(opt_quality_id)->matched();
+ bool got_intensity_target =
+ cmdline.GetOption(opt_intensity_target_id)->matched();
+
+ if (got_quality) {
+ default_settings = false;
+ if (quality < 100) jpeg_transcode = false;
+ if (quality < 7 || quality == 100 || params.modular_mode) {
+ if (jpeg_transcode == false) params.modular_mode = true;
+ }
+ // Quality settings roughly match libjpeg qualities.
+ if (quality >= 100) {
+ params.butteraugli_distance = 0;
+ } else if (quality >= 30) {
+ params.butteraugli_distance = 0.1 + (100 - quality) * 0.09;
+ } else {
+ params.butteraugli_distance =
+ 6.4 + pow(2.5, (30 - quality) / 5.0f) / 6.25f;
+ }
+ }
+
+ if (params.resampling > 1 && !params.already_downsampled)
+ jpeg_transcode = false;
+
+ if (progressive) {
+ params.qprogressive_mode = true;
+ params.responsive = 1;
+ default_settings = false;
+ }
+ if (got_target_size || got_target_bpp || got_intensity_target) {
+ default_settings = false;
+ }
+
+ if (params.progressive_dc < -1 || params.progressive_dc > 2) {
+ fprintf(stderr, "Invalid/out of range progressive_dc (%d), try -1 to 2.\n",
+ params.progressive_dc);
+ return false;
+ }
+
+ if (got_distance) {
+ constexpr float butteraugli_min_dist = 0.1f;
+ constexpr float butteraugli_max_dist = 25.0f;
+ if (!(0 <= params.butteraugli_distance &&
+ params.butteraugli_distance <= butteraugli_max_dist)) {
+ fprintf(stderr, "Invalid/out of range distance, try 0 to %g.\n",
+ butteraugli_max_dist);
+ return false;
+ }
+ if (params.butteraugli_distance > 0) jpeg_transcode = false;
+ if (params.butteraugli_distance == 0) {
+ // Use modular for lossless.
+ if (jpeg_transcode == false) params.modular_mode = true;
+ } else if (params.butteraugli_distance < butteraugli_min_dist) {
+ params.butteraugli_distance = butteraugli_min_dist;
+ }
+ default_settings = false;
+ }
+
+ if (got_target_bpp || got_target_size) {
+ jpeg_transcode = false;
+ }
+ if (params.brotli_effort > 11) {
+ fprintf(stderr, "Invalid --brotli_effort value\n");
+ return false;
+ }
+ if (got_target_bpp + got_target_size + got_distance + got_quality > 1) {
+ fprintf(stderr,
+ "You can specify only one of '--distance', '-q', "
+ "'--target_bpp' and '--target_size'. They are all different ways"
+ " to specify the image quality. When in doubt, use --distance."
+ " It gives the most visually consistent results.\n");
+ return false;
+ }
+
+ if (!saliency_map_filename.empty()) {
+ if (!params.progressive_mode) {
+ saliency_map_filename.clear();
+ fprintf(stderr,
+ "Warning: Specifying --saliency_map_filename only makes sense "
+ "for --progressive_ac mode.\n");
+ }
+ }
+
+ if (!params.file_in) {
+ fprintf(stderr, "Missing input filename.\n");
+ return false;
+ }
+
+ if (!cmdline.GetOption(opt_color_id)->matched()) {
+ // default to RGB for lossless modular
+ if (params.modular_mode) {
+ if (params.butteraugli_distance > 0.f) {
+ params.color_transform = jxl::ColorTransform::kXYB;
+ } else {
+ params.color_transform = jxl::ColorTransform::kNone;
+ }
+ }
+ }
+
+ if (override_bitdepth > 32) {
+ fprintf(stderr, "override_bitdepth must be <= 32\n");
+ return false;
+ }
+
+ if (params.epf > 3) {
+ fprintf(stderr, "--epf must be in the 0..3 range\n");
+ return false;
+ }
+
+ return true;
+}
+
+jxl::Status CompressArgs::ValidateArgsAfterLoad(
+ const CommandLineParser& cmdline, const jxl::CodecInOut& io) {
+ if (!ValidateArgs(cmdline)) return false;
+ bool got_m_group_size = cmdline.GetOption(opt_m_group_size_id)->matched();
+ if (params.modular_mode && !got_m_group_size) {
+ // Default modular group size: set to 512 if 256 would be silly
+ const size_t kThinImageThr = 256 + 64;
+ const size_t kSmallImageThr = 256 + 128;
+ if (io.xsize() < kThinImageThr || io.ysize() < kThinImageThr ||
+ (io.xsize() < kSmallImageThr && io.ysize() < kSmallImageThr)) {
+ params.modular_group_size_shift = 2;
+ }
+ }
+ if (!io.blobs.exif.empty() || !io.blobs.xmp.empty() ||
+ !io.blobs.jumbf.empty() || !io.blobs.iptc.empty() ||
+ (jpeg_transcode && store_jpeg_metadata)) {
+ use_container = true;
+ }
+ if (no_container) use_container = false;
+ if (jpeg_transcode && params.modular_mode) {
+ fprintf(stderr,
+ "Error: cannot do lossless JPEG transcode in modular mode.\n");
+ return false;
+ }
+ if (jpeg_transcode) {
+ if (params.progressive_mode || params.qprogressive_mode ||
+ params.progressive_dc > 0) {
+ fprintf(stderr,
+ "Error: progressive lossless JPEG transcode is not yet "
+ "implemented.\n");
+ return false;
+ }
+ }
+ return true;
+}
+
+jxl::Status LoadAll(CompressArgs& args, jxl::ThreadPoolInternal* pool,
+ jxl::CodecInOut* io, double* decode_mps) {
+ const double t0 = jxl::Now();
+
+ jxl::PaddedBytes encoded;
+ JXL_RETURN_IF_ERROR(jxl::ReadFile(args.params.file_in, &encoded));
+ jxl::extras::Codec input_codec;
+ bool ok;
+ if (args.jpeg_transcode && encoded.size() >= 2 && encoded[0] == 0xFF &&
+ encoded[1] == 0xD8) {
+ input_codec = jxl::extras::Codec::kJPG;
+ ok = jxl::jpeg::DecodeImageJPG(jxl::Span<const uint8_t>(encoded), io);
+ } else {
+ ok = jxl::SetFromBytes(jxl::Span<const uint8_t>(encoded), args.color_hints,
+ io, nullptr, &input_codec);
+ }
+ if (!ok) {
+ fprintf(stderr, "Failed to read image %s.\n", args.params.file_in);
+ return false;
+ }
+ if (args.intensity_target != 0) {
+ io->metadata.m.SetIntensityTarget(args.intensity_target);
+ }
+ if (input_codec != jxl::extras::Codec::kJPG) args.jpeg_transcode = false;
+ if (args.jpeg_transcode) args.params.butteraugli_distance = 0;
+
+ if (input_codec == jxl::extras::Codec::kGIF && args.default_settings) {
+ args.params.modular_mode = true;
+ args.params.butteraugli_distance = 0;
+ }
+ if (args.override_bitdepth != 0) {
+ if (args.override_bitdepth == 32) {
+ io->metadata.m.SetFloat32Samples();
+ } else {
+ io->metadata.m.SetUintSamples(args.override_bitdepth);
+ }
+ }
+ if (args.force_premultiplied) {
+ io->PremultiplyAlpha();
+ }
+
+ jxl::ImageF saliency_map;
+ if (!args.saliency_map_filename.empty()) {
+ if (!LoadSaliencyMap(args.saliency_map_filename, pool, &saliency_map)) {
+ fprintf(stderr, "Failed to read saliency map %s.\n",
+ args.saliency_map_filename.c_str());
+ return false;
+ }
+ args.params.saliency_map = &saliency_map;
+ }
+
+ const double t1 = jxl::Now();
+ const size_t pixels = io->xsize() * io->ysize();
+ *decode_mps = pixels * io->frames.size() * 1E-6 / (t1 - t0);
+
+ return true;
+}
+
+jxl::Status CompressJxl(jxl::CodecInOut& io, double decode_mps,
+ jxl::ThreadPoolInternal* pool, CompressArgs& args,
+ jxl::PaddedBytes* compressed, bool print_stats) {
+ JXL_CHECK(pool);
+
+ const size_t pixels = io.xsize() * io.ysize();
+
+ if (args.params.target_size > 0 || args.params.target_bitrate > 0) {
+ // Slow iterative search for parameters that reach target bpp / size.
+ SetParametersForSizeOrBitrate(pool, pixels, &args);
+ }
+
+ if (print_stats) PrintMode(pool, io, decode_mps, args);
+
+ // Final/actual compression run (possibly repeated for benchmarking).
+ jxl::AuxOut aux_out;
+ if (args.inspector_image3f) {
+ aux_out.SetInspectorImage3F(args.inspector_image3f);
+ }
+ SpeedStats stats;
+ jxl::PassesEncoderState passes_encoder_state;
+ if (args.params.use_new_heuristics) {
+ passes_encoder_state.heuristics =
+ jxl::make_unique<jxl::FastEncoderHeuristics>();
+ }
+ for (size_t i = 0; i < args.num_reps; ++i) {
+ const double t0 = jxl::Now();
+ jxl::Status ok = false;
+ if (io.Main().IsJPEG()) {
+ // TODO(lode): automate this in the encoder. The encoder must in the
+ // beginning choose to either do all in xyb, or all in non-xyb, write
+ // that in the xyb_encoded header flag, and persistently keep that state
+ // to check if every frame uses an allowed color transform.
+ args.params.color_transform = io.Main().color_transform;
+ }
+ ok = EncodeFile(args.params, &io, &passes_encoder_state, compressed,
+ jxl::GetJxlCms(), &aux_out, pool);
+ if (!ok) {
+ fprintf(stderr, "Failed to compress to %s.\n", ModeFromArgs(args));
+ return false;
+ }
+ const double t1 = jxl::Now();
+ stats.NotifyElapsed(t1 - t0);
+ stats.SetImageSize(io.xsize(), io.ysize());
+ }
+
+ if (print_stats) {
+ const double bpp =
+ static_cast<double>(compressed->size() * jxl::kBitsPerByte) / pixels;
+ fprintf(stderr, "Compressed to %" PRIuS " bytes (%.3f bpp%s).\n",
+ compressed->size(), bpp / io.frames.size(),
+ io.frames.size() == 1 ? "" : "/frame");
+ JXL_CHECK(stats.Print(args.num_threads));
+ if (args.params.verbose) {
+ aux_out.Print(1);
+ }
+ }
+
+ return true;
+}
+
+} // namespace tools
+} // namespace jpegxl
diff --git a/media/libjxl/src/tools/cjxl.h b/media/libjxl/src/tools/cjxl.h
new file mode 100644
index 0000000000..1be3500dfd
--- /dev/null
+++ b/media/libjxl/src/tools/cjxl.h
@@ -0,0 +1,109 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_CJXL_H_
+#define TOOLS_CJXL_H_
+
+#include <stddef.h>
+
+#include <string>
+#include <thread>
+#include <utility>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/enc_params.h"
+#include "lib/jxl/jxl_inspection.h"
+#include "tools/cmdline.h"
+
+namespace jpegxl {
+namespace tools {
+
+struct CompressArgs {
+ void SetInspectorImage3F(const jxl::InspectorImage3F& inspector) {
+ inspector_image3f = inspector;
+ }
+
+ // Add all the command line options to the CommandLineParser. Note that the
+ // options are tied to the instance that this was called on.
+ void AddCommandLineOptions(CommandLineParser* cmdline);
+
+ // Post-processes and validates the passed arguments, checking whether all
+ // passed options are compatible. Returns whether the validation was
+ // successful.
+ jxl::Status ValidateArgs(const CommandLineParser& cmdline);
+
+ // Validates the arguments again, having loaded the input so sensible defaults
+ // can be chosen based on e.g. dimensions.
+ jxl::Status ValidateArgsAfterLoad(const CommandLineParser& cmdline,
+ const jxl::CodecInOut& io);
+
+ // Common flags.
+ bool version = false;
+ bool use_container = false;
+ bool no_container = false;
+ bool quiet = false;
+
+ const char* file_in = nullptr;
+ const char* file_out = nullptr;
+ jxl::Override print_profile = jxl::Override::kDefault;
+
+ // Decoding source image flags
+ jxl::extras::ColorHints color_hints;
+
+ // JXL flags
+ size_t override_bitdepth = 0;
+ jxl::CompressParams params;
+ size_t num_threads = std::thread::hardware_concurrency();
+ size_t num_reps = 1;
+ float intensity_target = 0;
+
+ // Filename for the user provided saliency-map.
+ std::string saliency_map_filename;
+
+ // Whether to perform lossless transcoding with kVarDCT or kJPEG encoding.
+ // If true, attempts to load JPEG coefficients instead of pixels.
+ // Reset to false if input image is not a JPEG.
+ bool jpeg_transcode = true;
+
+ bool store_jpeg_metadata = true;
+
+ float quality = -1001.f; // Default to lossless if input is already lossy,
+ // or to VarDCT otherwise.
+ bool progressive = false;
+ bool default_settings = true;
+ bool force_premultiplied = false;
+
+ // Will get passed on to AuxOut.
+ jxl::InspectorImage3F inspector_image3f;
+
+ // References (ids) of specific options to check if they were matched.
+ CommandLineParser::OptionId opt_distance_id = -1;
+ CommandLineParser::OptionId opt_target_size_id = -1;
+ CommandLineParser::OptionId opt_target_bpp_id = -1;
+ CommandLineParser::OptionId opt_quality_id = -1;
+ CommandLineParser::OptionId opt_near_lossless_id = -1;
+ CommandLineParser::OptionId opt_intensity_target_id = -1;
+ CommandLineParser::OptionId opt_color_id = -1;
+ CommandLineParser::OptionId opt_m_group_size_id = -1;
+};
+
+jxl::Status LoadAll(CompressArgs& args, jxl::ThreadPoolInternal* pool,
+ jxl::CodecInOut* io, double* decode_mps);
+
+// The input image must already have been loaded into io using LoadAll.
+jxl::Status CompressJxl(jxl::CodecInOut& io, double decode_mps,
+ jxl::ThreadPoolInternal* pool, CompressArgs& args,
+ jxl::PaddedBytes* compressed, bool print_stats = true);
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_CJXL_H_
diff --git a/media/libjxl/src/tools/cjxl_bisect_bpp b/media/libjxl/src/tools/cjxl_bisect_bpp
new file mode 100755
index 0000000000..d7a1066e1c
--- /dev/null
+++ b/media/libjxl/src/tools/cjxl_bisect_bpp
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# Bisects JPEG XL encoding quality parameter to reach a given
+# target bits-per-pixel value.
+# (To be used directly, or as a template for tailored processing.)
+#
+# Usage: cjxl_bisect_size {input_filename} {output_filename} {target_bpp}
+
+#
+# We take the `bisector` tool from $PATH, or, if not available,
+# try to locate it in the same directory as the current script.
+# The `get_bpp` helper is taken from the same directory as the current script.
+#
+
+input_filename=$1
+output_filename=$2
+target_size=$3
+
+script_dir=$(dirname $(readlink -f $0))
+bisect_tool=$(which bisector)
+if [ -z $bisect_tool ] ; then
+ bisect_tool="${script_dir}/bisector"
+fi
+jxl_get_bpp_helper="${script_dir}/jxl_get_bpp_helper"
+# If $CJXL_BIN is set, we use this instead of looking for `cjxl` on $PATH.
+
+cjxl_bin=${CJXL_BIN}
+if [ -z $cjxl_bin ] ; then
+ cjxl_bin="cjxl"
+fi
+
+# Using `identify` from ImageMagick here.
+num_pixels=$(identify -format "%w*%h\n" /tmp/baseball.png|bc)
+
+# Allow 0.5% tolerance in size (--rtol=0.005).
+exec $bisect_tool --var=BISECT --range=0.01,15.0 --target=$target_size \
+ --rtol_val=0.005 \
+ --cmd="$cjxl_bin --distance=\$BISECT ${input_filename} ${output_filename}_bisect_\$BISECT.jxl ; (find ${output_filename}_bisect_\$BISECT.jxl -printf \"scale=10;%s/$num_pixels\n\" | bc -l)" \
+ --final="mv ${output_filename}_bisect_\$BISECT.jxl ${output_filename}; rm -f ${output_filename}_bisect_*.jxl" \
+ --verbosity=1
diff --git a/media/libjxl/src/tools/cjxl_bisect_size b/media/libjxl/src/tools/cjxl_bisect_size
new file mode 100755
index 0000000000..9cd88ea529
--- /dev/null
+++ b/media/libjxl/src/tools/cjxl_bisect_size
@@ -0,0 +1,36 @@
+#!/bin/sh
+#
+# Bisects JPEG XL encoding quality parameter to reach a given
+# target byte-size.
+# (To be used directly, or as a template for tailored processing.)
+#
+# Usage: cjxl_bisect_size {input_filename} {output_filename} {target_size}
+
+#
+# We take the `bisector` tool from $PATH, or, if not available,
+# try to locate it in the same directory as the current script.
+#
+
+input_filename=$1
+output_filename=$2
+target_size=$3
+
+script_dir=$(dirname $(readlink -f $0))
+bisect_tool=$(which bisector)
+if [ -z $bisect_tool ] ; then
+ bisect_tool="${script_dir}/bisector"
+fi
+
+# If $CJXL_BIN is set, we use this instead of looking for `cjxl` on $PATH.
+
+cjxl_bin=${CJXL_BIN}
+if [-z $cjxl_bin ] ; then
+ cjxl_bin="cjxl"
+fi
+
+# Allow 0.5% tolerance in size (--rtol=0.005).
+exec $bisect_tool --var=BISECT --range=0.01,10.0 --target=$target_size \
+ --rtol_val=0.005 \
+ --cmd="$cjxl_bin --distance=\$BISECT ${input_filename} ${output_filename}_bisect_\$BISECT.jxl && wc -c ${output_filename}_bisect_\$BISECT.jxl" \
+ --final="mv ${output_filename}_bisect_\$BISECT.jxl ${output_filename}; rm -f ${output_filename}_bisect_*.jxl" \
+ --verbosity=1
diff --git a/media/libjxl/src/tools/cjxl_main.cc b/media/libjxl/src/tools/cjxl_main.cc
new file mode 100644
index 0000000000..157400a13d
--- /dev/null
+++ b/media/libjxl/src/tools/cjxl_main.cc
@@ -0,0 +1,151 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+
+#include "jxl/encode.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/profiler.h"
+#include "lib/jxl/jpeg/enc_jpeg_data.h"
+#include "tools/box/box.h"
+#include "tools/cjxl.h"
+#include "tools/codec_config.h"
+
+namespace jpegxl {
+namespace tools {
+
+enum CjxlRetCode : int {
+ OK = 0,
+ ERR_PARSE,
+ ERR_INVALID_ARG,
+ ERR_LOAD_INPUT,
+ ERR_INVALID_INPUT,
+ ERR_ENCODING,
+ ERR_CONTAINER,
+ ERR_WRITE,
+ DROPPED_JBRD,
+};
+
+int CompressJpegXlMain(int argc, const char* argv[]) {
+ CommandLineParser cmdline;
+ CompressArgs args;
+ args.AddCommandLineOptions(&cmdline);
+
+ if (!cmdline.Parse(argc, argv)) {
+ // Parse already printed the actual error cause.
+ fprintf(stderr, "Use '%s -h' for more information\n", argv[0]);
+ return CjxlRetCode::ERR_PARSE;
+ }
+
+ if (args.version) {
+ fprintf(stdout, "cjxl %s\n",
+ CodecConfigString(JxlEncoderVersion()).c_str());
+ fprintf(stdout, "Copyright (c) the JPEG XL Project\n");
+ return CjxlRetCode::OK;
+ }
+
+ if (!args.quiet) {
+ fprintf(stderr, "JPEG XL encoder %s\n",
+ CodecConfigString(JxlEncoderVersion()).c_str());
+ }
+
+ if (cmdline.HelpFlagPassed()) {
+ cmdline.PrintHelp();
+ return CjxlRetCode::OK;
+ }
+
+ if (!args.ValidateArgs(cmdline)) {
+ // ValidateArgs already printed the actual error cause.
+ fprintf(stderr, "Use '%s -h' for more information\n", argv[0]);
+ return CjxlRetCode::ERR_INVALID_ARG;
+ }
+
+ jxl::PaddedBytes compressed;
+
+ jxl::ThreadPoolInternal pool(args.num_threads);
+ jxl::CodecInOut io;
+ double decode_mps = 0;
+ if (!LoadAll(args, &pool, &io, &decode_mps)) {
+ return CjxlRetCode::ERR_LOAD_INPUT;
+ }
+
+ // need to validate again because now we know the input
+ if (!args.ValidateArgsAfterLoad(cmdline, io)) {
+ fprintf(stderr, "Use '%s -h' for more information\n", argv[0]);
+ return CjxlRetCode::ERR_INVALID_INPUT;
+ }
+ if (!args.file_out && !args.quiet) {
+ fprintf(stderr,
+ "No output file specified.\n"
+ "Encoding will be performed, but the result will be discarded.\n");
+ }
+ if (!CompressJxl(io, decode_mps, &pool, args, &compressed, !args.quiet)) {
+ return CjxlRetCode::ERR_ENCODING;
+ }
+
+ int ret = CjxlRetCode::OK;
+ if (args.use_container) {
+ JpegXlContainer container;
+ container.codestream = std::move(compressed);
+ if (!io.blobs.exif.empty()) {
+ container.exif = io.blobs.exif.data();
+ container.exif_size = io.blobs.exif.size();
+ }
+ auto append_xml = [&container](const std::vector<uint8_t>& bytes) {
+ if (bytes.empty()) return;
+ container.xml.emplace_back(bytes.data(), bytes.size());
+ };
+ append_xml(io.blobs.xmp);
+ if (!io.blobs.jumbf.empty()) {
+ container.jumb = io.blobs.jumbf.data();
+ container.jumb_size = io.blobs.jumbf.size();
+ }
+ jxl::PaddedBytes jpeg_data;
+ if (args.store_jpeg_metadata && io.Main().IsJPEG()) {
+ jxl::jpeg::JPEGData data_in = *io.Main().jpeg_data;
+ if (EncodeJPEGData(data_in, &jpeg_data, args.params)) {
+ container.jpeg_reconstruction = jpeg_data.data();
+ container.jpeg_reconstruction_size = jpeg_data.size();
+ } else {
+ fprintf(stderr, "Warning: failed to create JPEG reconstruction data\n");
+ ret = CjxlRetCode::DROPPED_JBRD;
+ }
+ }
+ compressed = {};
+ if (!EncodeJpegXlContainerOneShot(container, &compressed)) {
+ fprintf(stderr, "Failed to encode container format\n");
+ return CjxlRetCode::ERR_CONTAINER;
+ }
+ if (!args.quiet) {
+ const size_t pixels = io.xsize() * io.ysize();
+ const double bpp =
+ static_cast<double>(compressed.size() * jxl::kBitsPerByte) / pixels;
+ fprintf(stderr, "Including container: %llu bytes (%.3f bpp%s).\n",
+ static_cast<long long unsigned>(compressed.size()),
+ bpp / io.frames.size(), io.frames.size() == 1 ? "" : "/frame");
+ }
+ }
+ if (args.file_out) {
+ if (!jxl::WriteFile(compressed, args.file_out)) {
+ fprintf(stderr, "Failed to write to \"%s\"\n", args.file_out);
+ return CjxlRetCode::ERR_WRITE;
+ }
+ }
+
+ if (args.print_profile == jxl::Override::kOn) {
+ PROFILER_PRINT_RESULTS();
+ }
+ if (!args.quiet && cmdline.verbosity > 0) {
+ jxl::CacheAligned::PrintStats();
+ }
+ return ret;
+}
+
+} // namespace tools
+} // namespace jpegxl
+
+int main(int argc, const char** argv) {
+ return jpegxl::tools::CompressJpegXlMain(argc, argv);
+}
diff --git a/media/libjxl/src/tools/cjxl_ng_main.cc b/media/libjxl/src/tools/cjxl_ng_main.cc
new file mode 100644
index 0000000000..403ee82cfd
--- /dev/null
+++ b/media/libjxl/src/tools/cjxl_ng_main.cc
@@ -0,0 +1,922 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Note: This encoder binary does extensive flag-validity checking (in
+// order to produce meaningful error messages), and on top of that
+// checks all libjxl C API call return values. The downside of this
+// vs. libjxl providing meaningful error messages is that a change to
+// the accepted range of a flag-specified parameter in libjxl will
+// also require a change to the range-check here. The advantage is
+// that this minimizes the size of libjxl.
+
+#include <stdint.h>
+
+#include <cmath>
+#include <cstdlib>
+#include <functional>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "gflags/gflags.h"
+#include "jxl/codestream_header.h"
+#include "jxl/encode.h"
+#include "jxl/encode_cxx.h"
+#include "jxl/thread_parallel_runner.h"
+#include "jxl/thread_parallel_runner_cxx.h"
+#include "jxl/types.h"
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/apng.h"
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/dec/gif.h"
+#include "lib/extras/dec/jpg.h"
+#include "lib/extras/dec/pgx.h"
+#include "lib/extras/dec/pnm.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/size_constraints.h"
+
+DECLARE_bool(help);
+DECLARE_bool(helpshort);
+// The flag --version is owned by gflags itself.
+DEFINE_bool(encoder_version, false,
+ "Print encoder library version number and exit.");
+
+DEFINE_bool(lossless_jpeg, true,
+ "If the input is JPEG, use JxlEncoderAddJPEGFrame "
+ "to add a JPEG frame (i.e. losslessly transcoding JPEG), "
+ "rather than using JxlEncoderAddImageFrame to reencode pixels.");
+
+DEFINE_bool(jpeg_store_metadata, true,
+ "If --lossless_jpeg is set, store JPEG reconstruction "
+ "metadata in the JPEG XL container "
+ "(for lossless reconstruction of the JPEG codestream).");
+
+DEFINE_bool(jpeg_reconstruction_cfl, true,
+ "Enable/disable chroma-from-luma (CFL) for lossless "
+ "JPEG reconstruction.");
+
+DEFINE_bool(container, false,
+ "Force using container format (default: use only if needed).");
+
+DEFINE_bool(strip, false,
+ "Do not encode using container format (strips "
+ "Exif/XMP/JPEG bitstream reconstruction data).");
+
+DEFINE_bool(responsive, false, "[modular encoding] Do Squeeze transform");
+
+DEFINE_bool(progressive, false, "Enable progressive/responsive decoding.");
+
+DEFINE_bool(progressive_ac, false, "Use progressive mode for AC.");
+
+DEFINE_bool(qprogressive_ac, false, "Use progressive mode for AC.");
+
+DEFINE_bool(modular_lossy_palette, false, "Use delta-palette.");
+
+DEFINE_int32(premultiply, -1,
+ "Force premultiplied (associated) alpha. "
+ "-1 = Do what the input does, 0 = Do not premultiply, "
+ "1 = force premultiply.");
+
+DEFINE_bool(already_downsampled, false,
+ "Do not downsample the given input before encoding, "
+ "but still signal that the decoder should upsample.");
+
+DEFINE_bool(
+ modular, false,
+ "Use modular mode (not provided = encoder chooses, 0 = enforce VarDCT, "
+ "1 = enforce modular mode).");
+
+DEFINE_bool(keep_invisible, false,
+ "Force disable/enable preserving color of invisible "
+ "pixels. (not provided = default, 0 = disable, 1 = enable).");
+
+DEFINE_bool(dots, false,
+ "Force disable/enable dots generation. "
+ "(not provided = default, 0 = disable, 1 = enable).");
+
+DEFINE_bool(patches, false,
+ "Force disable/enable patches generation. "
+ "(not provided = default, 0 = disable, 1 = enable).");
+
+DEFINE_bool(gaborish, false,
+ "Force disable/enable the gaborish filter. "
+ "(not provided = default, 0 = disable, 1 = enable).");
+
+DEFINE_bool(
+ group_order, false,
+ "Order in which 256x256 regions are stored "
+ "in the codestream for progressive rendering. "
+ "Value not provided means 'encoder default', 0 means 'scanline order', "
+ "1 means 'center-first order'.");
+
+DEFINE_double(
+ intensity_target, 0.0,
+ "Upper bound on the intensity level present in the image in nits. "
+ "Leaving this set to its default of 0 lets libjxl choose a sensible "
+ "default "
+ "value based on the color encoding.");
+
+// TODO(tfish):
+// --dec-hints, -- NEED (passed to image decoders, via extras, tweaks decoding)
+// --override_bitdepth, -- NEED
+
+DEFINE_int32(progressive_dc, -1,
+ "Progressive-DC setting. Valid values are: -1, 0, 1, 2.");
+
+DEFINE_int32(faster_decoding, 0,
+ "Favour higher decoding speed. 0 = default, higher "
+ "values give higher speed at the expense of quality");
+
+DEFINE_int32(
+ resampling, -1,
+ "Resampling. Default of -1 applies resampling only for low quality. "
+ "Value 1 does no downsampling (1x1), 2 does 2x2 downsampling, "
+ "4 is for 4x4 downsampling, and 8 for 8x8 downsampling.");
+
+DEFINE_int32(
+ ec_resampling, -1,
+ "Resampling for extra channels. Default of -1 applies resampling only "
+ "for low quality. Value 1 does no downsampling (1x1), 2 does 2x2 "
+ "downsampling, 4 is for 4x4 downsampling, and 8 for 8x8 downsampling.");
+
+DEFINE_int32(
+ epf, -1,
+ "Edge preserving filter level, -1 to 3. "
+ "Value -1 means: default (encoder chooses), 0 to 3 set a strength.");
+
+DEFINE_int64(
+ center_x, -1,
+ "Determines the horizontal position of center for the center-first "
+ "group order. The value -1 means 'use the middle of the image', "
+ "other values [0..xsize) set this to a particular coordinate.");
+
+DEFINE_int64(center_y, -1,
+ "Determines the vertical position of center for the center-first "
+ "group order. The value -1 means 'use the middle of the image', "
+ "other values [0..ysize) set this to a particular coordinate.");
+
+DEFINE_int64(num_threads, -1,
+ "Number of worker threads (-1 == use machine default, "
+ "0 == do not use multithreading).");
+
+DEFINE_int64(num_reps, 1, "How many times to compress. (For benchmarking).");
+
+DEFINE_int32(modular_group_size, -1,
+ "[modular encoding] group size: -1 == default. 0 => 128, "
+ "1 => 256, 2 => 512, 3 => 1024");
+
+DEFINE_int32(modular_predictor, -1,
+ "[modular encoding] predictor(s) to use: 0=zero, "
+ "1=left, 2=top, 3=avg0, 4=select, 5=gradient, 6=weighted, "
+ "7=topright, 8=topleft, 9=leftleft, 10=avg1, 11=avg2, 12=avg3, "
+ "13=toptop predictive average "
+ "14=mix 5 and 6, 15=mix everything. If unset, uses default 14, "
+ "at slowest speed default 15.");
+
+DEFINE_int32(modular_colorspace, -1,
+ "[modular encoding] color transform: 0=RGB, 1=YCoCg, "
+ "2-37=RCT (default: try several, depending on speed)");
+
+DEFINE_int32(
+ modular_channel_colors_global_percent, -1,
+ "[modular encoding] Use Global channel palette if the number of "
+ "colors is smaller than this percentage of range. "
+ "Use 0-100 to set an explicit percentage, -1 to use the encoder default.");
+
+DEFINE_int32(
+ modular_channel_colors_group_percent, -1,
+ "[modular encoding] Use Local (per-group) channel palette if the number "
+ "of colors is smaller than this percentage of range. Use 0-100 to set "
+ "an explicit percentage, -1 to use the encoder default.");
+
+DEFINE_int32(
+ modular_palette_colors, -1,
+ "[modular encoding] Use color palette if number of colors is smaller "
+ "than or equal to this, or -1 to use the encoder default.");
+
+DEFINE_int32(modular_nb_prev_channels, -1,
+ "[modular encoding] number of extra MA tree properties to use");
+
+DEFINE_int32(modular_ma_tree_learning_percent, -1,
+ "[modular encoding] Fraction of pixels used to learn MA trees as "
+ "a percentage. -1 = default, 0 = no MA and fast decode, 50 = "
+ "default value, 100 = all, values above 100 are also permitted. "
+ "Higher values use more encoder memory.");
+
+DEFINE_int32(photon_noise_iso, 0,
+ "Adds noise to the image emulating photographic film noise. "
+ "The higher the given number, the grainier the image will be. "
+ "As an example, a value of 100 gives low noise whereas a value "
+ "of 3200 gives a lot of noise. The default value is 0.");
+
+DEFINE_int32(codestream_level, 5, "The codestream level. Either `5` or `10`.");
+
+DEFINE_double(
+ distance, 1.0,
+ "Max. butteraugli distance, lower = higher quality.\n"
+ " 0.0 = mathematically lossless. Default for already-lossy input "
+ "(JPEG/GIF).\n"
+ " 1.0 = visually lossless. Default for other input.\n"
+ " Recommended range: 0.5 .. 3.0. Mutually exclusive with --quality.");
+
+DEFINE_double(
+ quality, 100.0,
+ "Quality setting (is remapped to --distance). Range: -inf .. 100.\n"
+ " 100 = mathematically lossless. Default for already-lossy input "
+ "(JPEG/GIF).\n"
+ " Other input gets encoded as per --distance default.\n"
+ " Positive quality values roughly match libjpeg quality.\n"
+ " Mutually exclusive with --distance.");
+
+DEFINE_int64(effort, 3,
+ "Encoder effort setting. Range: 1 .. 9.\n"
+ " Higher number is more effort (slower).");
+
+DEFINE_int32(brotli_effort, 9,
+ "Brotli effort setting. Range: 0 .. 11.\n"
+ " Default: 9. Higher number is more effort (slower).");
+
+DEFINE_string(frame_indexing, "",
+ // TODO(tfish): Add a more convenient vanilla alternative.
+ "If non-empty, a string matching '^[01]*$'. If this string has a "
+ "'1' in i-th position, then the i-th frame will be indexed in "
+ "the frame index box.");
+
+namespace {
+/**
+ * Writes bytes to file.
+ */
+bool WriteFile(const std::vector<uint8_t>& bytes, const char* filename) {
+ FILE* file = fopen(filename, "wb");
+ if (!file) {
+ std::cerr << "Could not open file: " << filename << " for writing"
+ << std::endl
+ << "Error: " << strerror(errno) << std::endl;
+ return false;
+ }
+ if (fwrite(bytes.data(), sizeof(uint8_t), bytes.size(), file) !=
+ bytes.size()) {
+ std::cerr << "Could not write bytes to file: " << filename << std::endl
+ << "Error: " << strerror(errno) << std::endl;
+ return false;
+ }
+ if (fclose(file) != 0) {
+ std::cerr << "Could not close file: " << filename << std::endl
+ << "Error: " << strerror(errno) << std::endl;
+ return false;
+ }
+ return true;
+}
+
+void SetFlagFrameOptionOrDie(const char* flag_name, int32_t flag_value,
+ JxlEncoderFrameSettings* frame_settings,
+ JxlEncoderFrameSettingId encoder_option) {
+ if (JXL_ENC_SUCCESS != JxlEncoderFrameSettingsSetOption(
+ frame_settings, encoder_option, flag_value)) {
+ std::cerr << "Setting encoder option from flag -- " << flag_name
+ << "failed." << std::endl;
+ exit(EXIT_FAILURE);
+ }
+}
+
+void SetDistanceFromFlags(JxlEncoderFrameSettings* jxl_encoder_frame_settings,
+ const jxl::extras::Codec& codec) {
+ bool distance_set =
+ !gflags::GetCommandLineFlagInfoOrDie("distance").is_default;
+ bool quality_set = !gflags::GetCommandLineFlagInfoOrDie("quality").is_default;
+
+ if (distance_set && quality_set) {
+ std::cerr << "Must not set both --distance and --quality." << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ if (distance_set) {
+ if (JXL_ENC_SUCCESS != JxlEncoderSetFrameDistance(
+ jxl_encoder_frame_settings, FLAGS_distance)) {
+ std::cerr << "Setting --distance parameter failed." << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ return;
+ }
+ if (quality_set) {
+ double distance = FLAGS_quality >= 100 ? 0.0
+ : FLAGS_quality >= 30
+ ? 0.1 + (100 - FLAGS_quality) * 0.09
+ : 6.4 + pow(2.5, (30 - FLAGS_quality) / 5.0) / 6.25;
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, distance)) {
+ std::cerr << "Setting --quality parameter failed." << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ return;
+ }
+ // No flag set, but input is JPG or GIF: Use distance 0 default.
+ if (codec == jxl::extras::Codec::kJPG || codec == jxl::extras::Codec::kGIF) {
+ if (JXL_ENC_SUCCESS ==
+ JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, 0.0)) {
+ std::cerr << "Setting 'lossless' default for GIF or JPEG input."
+ << std::endl;
+ }
+ }
+}
+
+typedef std::function<std::string(int32_t)> flag_check_fn;
+
+bool IsJPG(const jxl::PaddedBytes& image_data) {
+ return (image_data.size() >= 2 && image_data[0] == 0xFF &&
+ image_data[1] == 0xD8);
+}
+
+void SetCodestreamLevel(JxlEncoder* jxl_encoder, bool for_lossless_jpeg) {
+ bool flag_set =
+ !gflags::GetCommandLineFlagInfoOrDie("codestream_level").is_default;
+ int32_t codestream_level = FLAGS_codestream_level;
+ auto set_codestream_level = [&jxl_encoder, &codestream_level]() {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetCodestreamLevel(jxl_encoder, codestream_level)) {
+ std::cerr << "Setting --codestream_level failed." << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ };
+ if (for_lossless_jpeg) {
+ if (!flag_set) {
+ set_codestream_level();
+ }
+ } else {
+ if (!flag_set) {
+ codestream_level = static_cast<int32_t>(
+ JxlEncoderGetRequiredCodestreamLevel(jxl_encoder));
+ if (codestream_level == -1) {
+ std::cerr << "No codestream_level supports the given image parameters."
+ << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ }
+ set_codestream_level();
+ }
+}
+
+// TODO(tfish): Replace with non-C-API library function.
+// Implementation is in extras/.
+jxl::Status GetPixeldata(const jxl::PaddedBytes& image_data,
+ jxl::extras::PackedPixelFile& ppf,
+ jxl::extras::Codec& codec) {
+ // Any valid encoding is larger (ensures codecs can read the first few bytes).
+ constexpr size_t kMinBytes = 9;
+
+ if (image_data.size() < kMinBytes) return JXL_FAILURE("Input too small.");
+ jxl::Span<const uint8_t> encoded(image_data);
+
+ ppf.info.orientation = JXL_ORIENT_IDENTITY;
+ jxl::extras::ColorHints color_hints;
+ jxl::SizeConstraints size_constraints;
+
+#if JPEGXL_ENABLE_APNG
+ if (jxl::extras::DecodeImageAPNG(encoded, color_hints, size_constraints,
+ &ppf)) {
+ codec = jxl::extras::Codec::kPNG;
+ } else
+#endif
+ if (jxl::extras::DecodeImagePGX(encoded, color_hints, size_constraints,
+ &ppf)) {
+ codec = jxl::extras::Codec::kPGX;
+ } else if (jxl::extras::DecodeImagePNM(encoded, color_hints, size_constraints,
+ &ppf)) {
+ codec = jxl::extras::Codec::kPNM;
+ }
+#if JPEGXL_ENABLE_GIF
+ else if (jxl::extras::DecodeImageGIF(encoded, color_hints, size_constraints,
+ &ppf)) {
+ codec = jxl::extras::Codec::kGIF;
+ }
+#endif
+#if JPEGXL_ENABLE_JPEG
+ else if (jxl::extras::DecodeImageJPG(encoded, color_hints, size_constraints,
+ &ppf)) {
+ codec = jxl::extras::Codec::kJPG;
+ }
+#endif
+ else { // TODO(tfish): Bring back EXR and PSD.
+ return JXL_FAILURE("Codecs failed to decode input.");
+ }
+ // TODO(tfish): Migrate this:
+ // if (!skip_ppf_conversion) {
+ // JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
+ // }
+ return true;
+}
+
+void set_usage_message_and_version(const char* argv0) {
+ gflags::SetUsageMessage(
+ "JPEG XL-encodes an image.\n"
+ " Input format can be one of: "
+#if JPEGXL_ENABLE_APNG
+ "PNG, APNG, "
+#endif
+#if JPEGXL_ENABLE_GIF
+ "GIF, "
+#endif
+#if JPEGXL_ENABLE_JPEG
+ "JPEG, "
+#endif
+ "PPM, PFM, PGX.\n Sample usage:\n" +
+ std::string(argv0) + " <source_image_filename> <target_image_filename>");
+ uint32_t version = JxlEncoderVersion();
+
+ gflags::SetVersionString(std::to_string(version / 1000000) + "." +
+ std::to_string((version / 1000) % 1000) + "." +
+ std::to_string(version % 1000));
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+ std::cerr << "Warning: This is work in progress, consider using cjxl instead!"
+ << std::endl;
+ set_usage_message_and_version(argv[0]);
+ // TODO(firsching): rethink --help handling
+ gflags::ParseCommandLineNonHelpFlags(&argc, &argv, /*remove_flags=*/true);
+ if (FLAGS_help) {
+ FLAGS_help = false;
+ FLAGS_helpshort = true;
+ }
+ gflags::HandleCommandLineHelpFlags();
+
+ if (argc != 3) {
+ FLAGS_help = false;
+ FLAGS_helpshort = true;
+ gflags::HandleCommandLineHelpFlags();
+ return EXIT_FAILURE;
+ }
+ const char* filename_in = argv[1];
+ const char* filename_out = argv[2];
+
+ // Loading the input.
+ // Depending on flags-settings, we want to either load a JPEG and
+ // faithfully convert it to JPEG XL, or load (JPEG or non-JPEG)
+ // pixel data. For benchmarking, we want to be able to do
+ // N repetitions of image-compression, but the input should
+ // not get reloaded as part of that.
+ // Since we do not want to load the input before we decided that
+ // flag-settings are valid, we need a mechanism to lazy-load the image.
+ bool input_image_loaded = false;
+ jxl::PaddedBytes image_data;
+ jxl::extras::PackedPixelFile ppf;
+ jxl::extras::Codec codec = jxl::extras::Codec::kUnknown;
+ auto ensure_image_loaded = [&filename_in, &input_image_loaded, &image_data,
+ &ppf, &codec]() {
+ if (input_image_loaded) return;
+ if (!ReadFile(filename_in, &image_data)) {
+ std::cerr << "Reading image data failed." << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ if (!(FLAGS_lossless_jpeg && IsJPG(image_data))) {
+ jxl::Status status = GetPixeldata(image_data, ppf, codec);
+ if (!status) {
+ std::cerr << "Getting pixel data." << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ if (ppf.frames.size() < 1) {
+ std::cerr << "No frames on input file." << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ }
+ input_image_loaded = true;
+ };
+
+ JxlEncoderPtr enc = JxlEncoderMake(/*memory_manager=*/nullptr);
+ JxlEncoder* jxl_encoder = enc.get();
+ JxlThreadParallelRunnerPtr runner;
+ for (int num_rep = 0; num_rep < FLAGS_num_reps; ++num_rep) {
+ JxlEncoderReset(jxl_encoder);
+ if (FLAGS_num_threads != 0) {
+ size_t num_worker_threads =
+ JxlThreadParallelRunnerDefaultNumWorkerThreads();
+ {
+ int64_t flag_num_worker_threads = FLAGS_num_threads;
+ if (flag_num_worker_threads != -1) {
+ num_worker_threads = flag_num_worker_threads;
+ }
+ }
+ if (runner == nullptr) {
+ runner = JxlThreadParallelRunnerMake(
+ /*memory_manager=*/nullptr, num_worker_threads);
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetParallelRunner(jxl_encoder, JxlThreadParallelRunner,
+ runner.get())) {
+ std::cerr << "JxlEncoderSetParallelRunner failed." << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+
+ JxlEncoderFrameSettings* jxl_encoder_frame_settings =
+ JxlEncoderFrameSettingsCreate(jxl_encoder, nullptr);
+
+ auto process_flag = [&jxl_encoder_frame_settings](
+ const char* flag_name, int32_t flag_value,
+ JxlEncoderFrameSettingId encoder_option,
+ flag_check_fn flag_check) {
+ gflags::CommandLineFlagInfo flag_info =
+ gflags::GetCommandLineFlagInfoOrDie(flag_name);
+ if (!flag_info.is_default) {
+ std::string error = flag_check(flag_value);
+ if (!error.empty()) {
+ std::cerr << "Invalid flag value for --" << flag_name << ": " << error
+ << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ SetFlagFrameOptionOrDie(flag_name, flag_value,
+ jxl_encoder_frame_settings, encoder_option);
+ }
+ };
+
+ auto process_bool_flag = [&process_flag](
+ const char* flag_name, int32_t flag_value,
+ JxlEncoderFrameSettingId encoder_option) {
+ process_flag(flag_name, static_cast<int32_t>(flag_value), encoder_option,
+ [](int32_t x) { return ""; });
+ };
+
+ { // Processing tuning flags.
+ bool use_container = FLAGS_container;
+ // TODO(tfish): Set use_container according to need of encoded data.
+ // This will likely require moving this piece out of flags-processing.
+ if (FLAGS_strip) {
+ use_container = false;
+ }
+ JxlEncoderUseContainer(jxl_encoder, use_container);
+
+ process_bool_flag("modular", FLAGS_modular,
+ JXL_ENC_FRAME_SETTING_MODULAR);
+ process_bool_flag("keep_invisible", FLAGS_keep_invisible,
+ JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE);
+ process_bool_flag("dots", FLAGS_dots, JXL_ENC_FRAME_SETTING_DOTS);
+ process_bool_flag("patches", FLAGS_patches,
+ JXL_ENC_FRAME_SETTING_PATCHES);
+ process_bool_flag("gaborish", FLAGS_gaborish,
+ JXL_ENC_FRAME_SETTING_GABORISH);
+ process_bool_flag("group_order", FLAGS_group_order,
+ JXL_ENC_FRAME_SETTING_GROUP_ORDER);
+
+ if (!FLAGS_frame_indexing.empty()) {
+ bool must_be_all_zeros = FLAGS_frame_indexing[0] != '1';
+ for (char c : FLAGS_frame_indexing) {
+ if (c == '1') {
+ if (must_be_all_zeros) {
+ std::cerr
+ << "Invalid --frame_indexing. If the first character is "
+ "'0', all must be '0'."
+ << std::endl;
+ return EXIT_FAILURE;
+ }
+ } else if (c != '0') {
+ std::cerr << "Invalid --frame_indexing. Must match the pattern "
+ "'^(0*|1[01]*)$'."
+ << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+ }
+
+ process_flag(
+ "effort", FLAGS_effort, JXL_ENC_FRAME_SETTING_EFFORT,
+ [](int32_t x) -> std::string {
+ return (1 <= x && x <= 9) ? "" : "Valid range is {1, 2, ..., 9}.";
+ });
+ process_flag(
+ "brotli_effort", FLAGS_brotli_effort,
+ JXL_ENC_FRAME_SETTING_BROTLI_EFFORT, [](int32_t x) -> std::string {
+ return (-1 <= x && x <= 11) ? ""
+ : "Valid range is {-1, 0, 1, ..., 11}.";
+ });
+ process_flag("epf", FLAGS_epf, JXL_ENC_FRAME_SETTING_EPF,
+ [](int32_t x) -> std::string {
+ return (-1 <= x && x <= 3)
+ ? ""
+ : "Valid range is {-1, 0, 1, 2, 3}.\n";
+ });
+ process_flag(
+ "faster_decoding", FLAGS_faster_decoding,
+ JXL_ENC_FRAME_SETTING_DECODING_SPEED, [](int32_t x) -> std::string {
+ return (0 <= x && x <= 4) ? ""
+ : "Valid range is {0, 1, 2, 3, 4}.\n";
+ });
+ process_flag("resampling", FLAGS_resampling,
+ JXL_ENC_FRAME_SETTING_RESAMPLING,
+ [](int32_t x) -> std::string {
+ return (x == -1 || x == 1 || x == 4 || x == 8)
+ ? ""
+ : "Valid values are {-1, 1, 2, 4, 8}.\n";
+ });
+ process_flag("ec_resampling", FLAGS_ec_resampling,
+ JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING,
+ [](int32_t x) -> std::string {
+ return (x == -1 || x == 1 || x == 4 || x == 8)
+ ? ""
+ : "Valid values are {-1, 1, 2, 4, 8}.\n";
+ });
+ process_flag("photon_noise_iso", FLAGS_photon_noise_iso,
+ JXL_ENC_FRAME_SETTING_PHOTON_NOISE,
+ [](int32_t x) -> std::string {
+ return x >= 0 ? "" : "Must be >= 0.";
+ });
+ process_bool_flag("already_downsampled", FLAGS_already_downsampled,
+ JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED);
+ SetDistanceFromFlags(jxl_encoder_frame_settings, codec);
+
+ if (!FLAGS_group_order &&
+ (FLAGS_center_x != -1 || FLAGS_center_y != -1)) {
+ std::cerr
+ << "Invalid flag combination. Setting --center_x or --center_y "
+ << "requires setting --group_order=1" << std::endl;
+ return EXIT_FAILURE;
+ }
+ process_flag("center_x", FLAGS_center_x,
+ JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X,
+ [](int32_t x) -> std::string {
+ if (x < -1) {
+ return "Valid values are: -1 or [0 .. xsize).";
+ }
+ return "";
+ });
+ process_flag("center_y", FLAGS_center_y,
+ JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y,
+ [](int32_t x) -> std::string {
+ if (x < -1) {
+ return "Valid values are: -1 or [0 .. ysize).";
+ }
+ return "";
+ });
+ }
+ { // Progressive/responsive mode settings.
+ bool qprogressive_ac_set =
+ !gflags::GetCommandLineFlagInfoOrDie("qprogressive_ac").is_default;
+ int32_t qprogressive_ac = FLAGS_qprogressive_ac ? 1 : 0;
+ bool responsive_set =
+ !gflags::GetCommandLineFlagInfoOrDie("responsive").is_default;
+ int32_t responsive = FLAGS_responsive ? 1 : 0;
+
+ process_flag(
+ "progressive_dc", FLAGS_progressive_dc,
+ JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, [](int32_t x) -> std::string {
+ return (-1 <= x && x <= 2) ? "" : "Valid range is {-1, 0, 1, 2}.\n";
+ });
+ process_bool_flag("progressive_ac", FLAGS_progressive_ac,
+ JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC);
+
+ if (FLAGS_progressive) {
+ qprogressive_ac = 1;
+ qprogressive_ac_set = true;
+ responsive = 1;
+ responsive_set = true;
+ }
+ if (responsive_set) {
+ SetFlagFrameOptionOrDie("responsive", responsive,
+ jxl_encoder_frame_settings,
+ JXL_ENC_FRAME_SETTING_RESPONSIVE);
+ }
+ if (qprogressive_ac_set) {
+ SetFlagFrameOptionOrDie("qprogressive_ac", qprogressive_ac,
+ jxl_encoder_frame_settings,
+ JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC);
+ }
+ }
+ { // Modular mode related.
+ process_flag("modular_group_size", FLAGS_modular_group_size,
+ JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE,
+ [](int32_t x) -> std::string {
+ return (-1 <= x && x <= 3)
+ ? ""
+ : "Invalid --modular_group_size. Valid "
+ "range is {-1, 0, 1, 2, 3}.\n";
+ });
+ process_flag("modular_predictor", FLAGS_modular_predictor,
+ JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR,
+ [](int32_t x) -> std::string {
+ return (0 <= x && x <= 15)
+ ? ""
+ : "Invalid --modular_predictor. Valid "
+ "range is {-1, 0, 1, ..., 15}.\n";
+ });
+ process_flag(
+ "modular_colorspace", FLAGS_modular_colorspace,
+ JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE,
+ [](int32_t x) -> std::string {
+ return (0 <= x && x <= 15)
+ ? ""
+ : "Invalid --modular_colorspace. Valid range is "
+ "{-1, 0, 1, ..., 37}.\n";
+ });
+ process_flag("modular_ma_tree_learning_percent",
+ FLAGS_modular_ma_tree_learning_percent,
+ JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT,
+ [](int32_t x) -> std::string {
+ return (-1 <= x && x <= 100)
+ ? ""
+ : "Invalid --modular_ma_tree_learning_percent. "
+ "Valid range is {-1, 0, 1, ..., 100}.\n";
+ });
+ process_flag("modular_nb_prev_channels", FLAGS_modular_nb_prev_channels,
+ JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS,
+ [](int32_t x) -> std::string {
+ return (-1 <= x && x <= 11)
+ ? ""
+ : "Invalid --modular_nb_prev_channels. Valid "
+ "range is {-1, 0, 1, ..., 11}.\n";
+ });
+ process_bool_flag("modular_lossy_palette", FLAGS_modular_lossy_palette,
+ JXL_ENC_FRAME_SETTING_LOSSY_PALETTE);
+ process_flag("modular_palette_colors", FLAGS_modular_palette_colors,
+ JXL_ENC_FRAME_SETTING_PALETTE_COLORS,
+ [](int32_t x) -> std::string { return ""; });
+ process_flag(
+ "modular_channel_colors_global_percent",
+ FLAGS_modular_channel_colors_global_percent,
+ JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT,
+ [](int32_t x) -> std::string {
+ return (-1 <= x && x <= 100)
+ ? ""
+ : "Invalid --modular_channel_colors_global_percent. "
+ "Valid "
+ "range is {-1, 0, 1, ..., 100}.\n";
+ });
+ process_flag(
+ "modular_channel_colors_group_percent",
+ FLAGS_modular_channel_colors_group_percent,
+ JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT,
+ [](int32_t x) -> std::string {
+ return (-1 <= x && x <= 100)
+ ? ""
+ : "Invalid --modular_channel_colors_group_percent. "
+ "Valid "
+ "range is {-1, 0, 1, ..., 100}.\n";
+ });
+ }
+ ensure_image_loaded();
+ if (FLAGS_lossless_jpeg && IsJPG(image_data)) {
+ if (gflags::GetCommandLineFlagInfoOrDie("lossless_jpeg").is_default) {
+ std::cerr << "Note: Implicit-default for JPEG is lossless-transcoding. "
+ << "To silence this message, set --lossless_jpeg=(1|0)."
+ << std::endl;
+ }
+ if (FLAGS_jpeg_store_metadata) {
+ if (JXL_ENC_SUCCESS != JxlEncoderStoreJPEGMetadata(jxl_encoder, true)) {
+ std::cerr << "Storing JPEG metadata failed. " << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+ process_bool_flag("jpeg_reconstruction_cfl",
+ FLAGS_jpeg_reconstruction_cfl,
+ JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL);
+ SetCodestreamLevel(jxl_encoder, /*for_lossless_jpeg=*/true);
+ if (JXL_ENC_SUCCESS != JxlEncoderAddJPEGFrame(jxl_encoder_frame_settings,
+ image_data.data(),
+ image_data.size())) {
+ std::cerr << "JxlEncoderAddJPEGFrame() failed." << std::endl;
+ return EXIT_FAILURE;
+ }
+ } else { // Do JxlEncoderAddImageFrame().
+ size_t num_alpha_channels = 0; // Adjusted below.
+ {
+ JxlBasicInfo basic_info = ppf.info;
+ if (basic_info.alpha_bits > 0) num_alpha_channels = 1;
+ basic_info.intensity_target =
+ static_cast<float>(FLAGS_intensity_target);
+ basic_info.num_extra_channels = num_alpha_channels;
+ basic_info.num_color_channels = ppf.info.num_color_channels;
+ basic_info.uses_original_profile = JXL_FALSE;
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetBasicInfo(jxl_encoder, &basic_info)) {
+ std::cerr << "JxlEncoderSetBasicInfo() failed." << std::endl;
+ return EXIT_FAILURE;
+ }
+ SetCodestreamLevel(jxl_encoder, /*for_lossless_jpeg=*/false);
+ }
+
+ if (!ppf.icc.empty()) {
+ JxlEncoderSetICCProfile(jxl_encoder, ppf.icc.data(), ppf.icc.size());
+ if (JXL_ENC_SUCCESS != JxlEncoderSetICCProfile(jxl_encoder,
+ ppf.icc.data(),
+ ppf.icc.size())) {
+ std::cerr << "JxlEncoderSetICCProfile() failed." << std::endl;
+ return EXIT_FAILURE;
+ }
+ } else {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetColorEncoding(jxl_encoder, &ppf.color_encoding)) {
+ std::cerr << "JxlEncoderSetColorEncoding() failed." << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+
+ for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) {
+ const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame];
+ const jxl::extras::PackedImage& pimage = pframe.color;
+ JxlPixelFormat ppixelformat = pimage.format;
+ {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetFrameHeader(jxl_encoder_frame_settings,
+ &pframe.frame_info)) {
+ std::cerr << "JxlEncoderSetFrameHeader() failed." << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+ if (num_frame < FLAGS_frame_indexing.size() &&
+ FLAGS_frame_indexing[num_frame] == '1') {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderFrameSettingsSetOption(jxl_encoder_frame_settings,
+ JXL_ENC_FRAME_INDEX_BOX, 1)) {
+ std::cerr << "Setting option JXL_ENC_FRAME_INDEX_BOX failed."
+ << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+ jxl::Status enc_status(true);
+ {
+ if (num_alpha_channels > 0) {
+ JxlExtraChannelInfo extra_channel_info;
+ JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA,
+ &extra_channel_info);
+ enc_status = JxlEncoderSetExtraChannelInfo(jxl_encoder, 0,
+ &extra_channel_info);
+ if (JXL_ENC_SUCCESS != enc_status) {
+ std::cerr << "JxlEncoderSetExtraChannelInfo() failed."
+ << std::endl;
+ return EXIT_FAILURE;
+ }
+ if (FLAGS_premultiply != -1) {
+ if (!(FLAGS_premultiply == 0 || FLAGS_premultiply == 1)) {
+ std::cerr << "Flag --premultiply must be one of: -1, 0, 1."
+ << std::endl;
+ return EXIT_FAILURE;
+ }
+ extra_channel_info.alpha_premultiplied = FLAGS_premultiply;
+ }
+ // We take the extra channel blend info frame_info, but don't do
+ // clamping.
+ JxlBlendInfo extra_channel_blend_info =
+ pframe.frame_info.layer_info.blend_info;
+ extra_channel_blend_info.clamp = JXL_FALSE;
+ JxlEncoderSetExtraChannelBlendInfo(jxl_encoder_frame_settings, 0,
+ &extra_channel_blend_info);
+ }
+ enc_status =
+ JxlEncoderAddImageFrame(jxl_encoder_frame_settings, &ppixelformat,
+ pimage.pixels(), pimage.pixels_size);
+ if (JXL_ENC_SUCCESS != enc_status) {
+ std::cerr << "JxlEncoderAddImageFrame() failed." << std::endl;
+ return EXIT_FAILURE;
+ }
+ // Only set extra channel buffer if is is provided non-interleaved.
+ if (!pframe.extra_channels.empty()) {
+ enc_status = JxlEncoderSetExtraChannelBuffer(
+ jxl_encoder_frame_settings, &ppixelformat,
+ pframe.extra_channels[0].pixels(),
+ pframe.extra_channels[0].stride *
+ pframe.extra_channels[0].ysize,
+ 0);
+ if (JXL_ENC_SUCCESS != enc_status) {
+ std::cerr << "JxlEncoderSetExtraChannelBuffer() failed."
+ << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+ }
+ }
+ }
+ JxlEncoderCloseInput(jxl_encoder);
+ }
+ // Reading compressed output
+ std::vector<uint8_t> compressed;
+ compressed.resize(4096);
+ uint8_t* next_out = compressed.data();
+ size_t avail_out = compressed.size() - (next_out - compressed.data());
+ JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT;
+ while (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
+ process_result =
+ JxlEncoderProcessOutput(jxl_encoder, &next_out, &avail_out);
+ if (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
+ size_t offset = next_out - compressed.data();
+ compressed.resize(compressed.size() * 2);
+ next_out = compressed.data() + offset;
+ avail_out = compressed.size() - offset;
+ }
+ }
+ compressed.resize(next_out - compressed.data());
+ if (JXL_ENC_SUCCESS != process_result) {
+ std::cerr << "JxlEncoderProcessOutput failed." << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ // TODO(firsching): print info about compressed size and other image stats
+ // here and in the beginning, like is done in current cjxl.
+ if (!WriteFile(compressed, filename_out)) {
+ std::cerr << "Could not write jxl file." << std::endl;
+ return EXIT_FAILURE;
+ }
+ return EXIT_SUCCESS;
+}
diff --git a/media/libjxl/src/tools/cmdline.cc b/media/libjxl/src/tools/cmdline.cc
new file mode 100644
index 0000000000..f777c9469c
--- /dev/null
+++ b/media/libjxl/src/tools/cmdline.cc
@@ -0,0 +1,95 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/cmdline.h"
+
+#include <memory>
+#include <string>
+
+namespace jpegxl {
+namespace tools {
+
+void CommandLineParser::PrintHelp() const {
+ // Use stdout, not stderr, so help can easily be grepped.
+ FILE* out = stdout;
+ fprintf(out, "Usage: %s", program_name_ ? program_name_ : "command");
+
+ for (const auto& option : options_) {
+ if (option->positional()) {
+ if (option->verbosity_level() > verbosity) continue;
+ if (option->required()) {
+ fprintf(out, " %s", option->help_flags().c_str());
+ } else {
+ fprintf(out, " [%s]", option->help_flags().c_str());
+ }
+ }
+ }
+ fprintf(out, " [OPTIONS...]\n");
+
+ bool showed_all = true;
+ for (const auto& option : options_) {
+ if (option->verbosity_level() > verbosity) {
+ showed_all = false;
+ continue;
+ }
+ fprintf(out, " %s\n", option->help_flags().c_str());
+ const char* help_text = option->help_text();
+ if (help_text) {
+ fprintf(out, " %s\n", help_text);
+ }
+ }
+ fprintf(out, " -h, --help\n Prints this help message%s.\n",
+ (showed_all ? "" : " (use -v to see more options)"));
+}
+
+bool CommandLineParser::Parse(int argc, const char* argv[]) {
+ if (argc) program_name_ = argv[0];
+ int i = 1; // argv[0] is the program name.
+ // if false, stop matching options and take only positional arguments
+ bool parse_options = true;
+ while (i < argc) {
+ if (!strcmp("-h", argv[i]) || !strcmp("--help", argv[i])) {
+ help_ = true;
+ i++;
+ continue;
+ }
+ if (!strcmp("-v", argv[i]) || !strcmp("--verbose", argv[i])) {
+ verbosity++;
+ }
+ // after "--", filenames starting with "-" can be used
+ if (!strcmp("--", argv[i])) {
+ parse_options = false;
+ i++;
+ continue;
+ }
+ // special case: "-" is a filename denoting stdin or stdout
+ bool parse_this_option = true;
+ if (!strcmp("-", argv[i])) {
+ parse_this_option = false;
+ }
+ bool found = false;
+ for (const auto& option : options_) {
+ if (option->Match(argv[i], parse_options && parse_this_option)) {
+ // Parsing advances the value i on success.
+ const char* arg = argv[i];
+ if (!option->Parse(argc, argv, &i)) {
+ fprintf(stderr, "Error parsing flag %s\n", arg);
+ return false;
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // No option matched argv[i].
+ fprintf(stderr, "Unknown argument: %s\n", argv[i]);
+ return false;
+ }
+ }
+ return true;
+}
+
+} // namespace tools
+} // namespace jpegxl
diff --git a/media/libjxl/src/tools/cmdline.h b/media/libjxl/src/tools/cmdline.h
new file mode 100644
index 0000000000..ed5d847050
--- /dev/null
+++ b/media/libjxl/src/tools/cmdline.h
@@ -0,0 +1,322 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_CMDLINE_H_
+#define TOOLS_CMDLINE_H_
+
+#include <stdio.h>
+#include <string.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "lib/jxl/base/status.h"
+
+namespace jpegxl {
+namespace tools {
+
+class CommandLineParser {
+ public:
+ typedef size_t OptionId;
+
+ // An abstract class for defining command line options.
+ class CmdOptionInterface {
+ public:
+ CmdOptionInterface() = default;
+ virtual ~CmdOptionInterface() = default;
+
+ // Return a string with the option name or available flags.
+ virtual std::string help_flags() const = 0;
+
+ // Return the help string if any, or nullptr if no help string.
+ virtual const char* help_text() const = 0;
+
+ // Return the verbosity level for this option
+ virtual int verbosity_level() const = 0;
+
+ // Return whether the option was passed.
+ virtual bool matched() const = 0;
+
+ // Returns whether this option matches the passed command line argument.
+ virtual bool Match(const char* arg, bool parse_options) const = 0;
+
+ // Parses the option. The passed i points to the argument with the flag
+ // that matches either the short or the long name.
+ virtual bool Parse(int argc, const char* argv[], int* i) = 0;
+
+ // Returns whether the option is positional, and therefore will be shown
+ // in the first command line representation of the help output.
+ virtual bool positional() const = 0;
+
+ // Returns whether the option should be displayed as required in the help
+ // output. No effect on validation.
+ virtual bool required() const = 0;
+ };
+
+ // Add a positional argument. Returns the id of the added option or
+ // kOptionError on error.
+ // The "required" flag indicates whether the parameter is mandatory or
+ // optional, but is only used for how it is displayed in the command line
+ // help.
+ OptionId AddPositionalOption(const char* name, bool required,
+ const char* help_text, const char** storage,
+ int verbosity_level = 0) {
+ options_.emplace_back(new CmdOptionPositional(name, help_text, storage,
+ verbosity_level, required));
+ return options_.size() - 1;
+ }
+
+ // Add an option with a value of type T. The option can be passed as
+ // '-s <value>' or '--long value' or '--long=value'. The CommandLineParser
+ // parser will call the function parser with the string pointing to '<value>'
+ // in either case. Returns the id of the added option or kOptionError on
+ // error.
+ template <typename T>
+ OptionId AddOptionValue(char short_name, const char* long_name,
+ const char* metavar, const char* help_text,
+ T* storage, bool(parser)(const char*, T*),
+ int verbosity_level = 0) {
+ options_.emplace_back(new CmdOptionFlag<T>(short_name, long_name, metavar,
+ help_text, storage, parser,
+ verbosity_level));
+ return options_.size() - 1;
+ }
+
+ // Add a flag without a value. Returns the id of the added option or
+ // kOptionError on error.
+ template <typename T>
+ OptionId AddOptionFlag(char short_name, const char* long_name,
+ const char* help_text, T* storage, bool(parser)(T*),
+ int verbosity_level = 0) {
+ options_.emplace_back(new CmdOptionFlag<T>(
+ short_name, long_name, help_text, storage, parser, verbosity_level));
+ return options_.size() - 1;
+ }
+
+ const CmdOptionInterface* GetOption(OptionId id) const {
+ JXL_ASSERT(id < options_.size());
+ return options_[id].get();
+ }
+
+ // Print the help message to stdout.
+ void PrintHelp() const;
+
+ // Whether a help flag was specified
+ bool HelpFlagPassed() const { return help_; }
+
+ int verbosity = 0;
+
+ // Parse the command line.
+ bool Parse(int argc, const char* argv[]);
+
+ // Return the remaining positional args
+ std::vector<const char*> PositionalArgs() const;
+
+ private:
+ // A positional argument.
+ class CmdOptionPositional : public CmdOptionInterface {
+ public:
+ CmdOptionPositional(const char* name, const char* help_text,
+ const char** storage, int verbosity_level,
+ bool required)
+ : name_(name),
+ help_text_(help_text),
+ storage_(storage),
+ verbosity_level_(verbosity_level),
+ required_(required) {}
+
+ std::string help_flags() const override { return name_; }
+ const char* help_text() const override { return help_text_; }
+ int verbosity_level() const override { return verbosity_level_; }
+ bool matched() const override { return matched_; }
+
+ // Only match non-flag values. This means that you can't pass '-foo' as a
+ // positional argument, but it helps with detecting when passed a flag with
+ // a typo. After '--', option matching is disabled so positional arguments
+ // starting with '-' can be used.
+ bool Match(const char* arg, bool parse_options) const override {
+ return !matched_ && (!parse_options || arg[0] != '-');
+ }
+
+ bool Parse(const int argc, const char* argv[], int* i) override {
+ *storage_ = argv[*i];
+ (*i)++;
+ matched_ = true;
+ return true;
+ }
+
+ bool positional() const override { return true; }
+
+ bool required() const override { return required_; }
+
+ private:
+ const char* name_;
+ const char* help_text_;
+ const char** storage_;
+ const int verbosity_level_;
+ const bool required_;
+
+ bool matched_{false};
+ };
+
+ // A class for handling an option flag like '-v' or '--foo=bar'.
+ template <typename T>
+ class CmdOptionFlag : public CmdOptionInterface {
+ public:
+ // Construct a flag that doesn't take any value, for example '-v' or
+ // '--long'. Passing a value to it raises an error.
+ CmdOptionFlag(char short_name, const char* long_name, const char* help_text,
+ T* storage, bool(parser)(T*), int verbosity_level)
+ : short_name_(short_name),
+ long_name_(long_name),
+ long_name_len_(long_name ? strlen(long_name) : 0),
+ metavar_(nullptr),
+ help_text_(help_text),
+ storage_(storage),
+ verbosity_level_(verbosity_level) {
+ parser_.parser_no_value_ = parser;
+ }
+
+ // Construct a flag that expects a value to be passed.
+ CmdOptionFlag(char short_name, const char* long_name, const char* metavar,
+ const char* help_text, T* storage,
+ bool(parser)(const char* arg, T*), int verbosity_level)
+ : short_name_(short_name),
+ long_name_(long_name),
+ long_name_len_(long_name ? strlen(long_name) : 0),
+ metavar_(metavar ? metavar : ""),
+ help_text_(help_text),
+ storage_(storage),
+ verbosity_level_(verbosity_level) {
+ parser_.parser_with_arg_ = parser;
+ }
+
+ std::string help_flags() const override {
+ std::string ret;
+ if (short_name_) {
+ ret += std::string("-") + short_name_;
+ if (metavar_) ret += std::string(" ") + metavar_;
+ if (long_name_) ret += ", ";
+ }
+ if (long_name_) {
+ ret += std::string("--") + long_name_;
+ if (metavar_) ret += std::string("=") + metavar_;
+ }
+ return ret;
+ }
+ const char* help_text() const override { return help_text_; }
+ int verbosity_level() const override { return verbosity_level_; }
+ bool matched() const override { return matched_; }
+
+ bool Match(const char* arg, bool parse_options) const override {
+ return parse_options && (MatchShort(arg) || MatchLong(arg));
+ }
+
+ bool Parse(const int argc, const char* argv[], int* i) override {
+ matched_ = true;
+ if (MatchLong(argv[*i])) {
+ const char* arg = argv[*i] + 2 + long_name_len_;
+ if (arg[0] == '=') {
+ if (metavar_) {
+ // Passed '--long_name=...'.
+ (*i)++;
+ // Skip over the '=' on the LongMatch.
+ arg += 1;
+ return (*parser_.parser_with_arg_)(arg, storage_);
+ } else {
+ fprintf(stderr, "--%s didn't expect any argument passed to it.\n",
+ argv[*i]);
+ return false;
+ }
+ }
+ }
+ // In any other case, it passed a -s or --long_name
+ (*i)++;
+ if (metavar_) {
+ if (argc <= *i) {
+ fprintf(stderr, "--%s expected an argument but none passed.\n",
+ argv[*i - 1]);
+ return false;
+ }
+ return (*parser_.parser_with_arg_)(argv[(*i)++], storage_);
+ } else {
+ return (*parser_.parser_no_value_)(storage_);
+ }
+ }
+
+ bool positional() const override { return false; }
+
+ bool required() const override {
+ // Only used for help display of positional arguments.
+ return false;
+ }
+
+ private:
+ // Returns whether arg matches the short_name flag of this option.
+ bool MatchShort(const char* arg) const {
+ if (!short_name_ || arg[0] != '-') return false;
+ return arg[1] == short_name_ && arg[2] == 0;
+ }
+
+ // Returns whether arg matches the long_name flag of this option,
+ // potentially with an argument passed to it.
+ bool MatchLong(const char* arg) const {
+ if (!long_name_ || arg[0] != '-' || arg[1] != '-') return false;
+ arg += 2; // Skips the '--'
+ if (strncmp(long_name_, arg, long_name_len_) != 0) return false;
+ arg += long_name_len_;
+ // Allow "--long_name=foo" and "--long_name" as long matches.
+ return arg[0] == 0 || arg[0] == '=';
+ }
+
+ // A short option passed as '-X' where X is the char. A value of 0 means
+ // no short option.
+ const char short_name_;
+
+ // A long option name passed as '--long' where 'long' is the name of the
+ // option.
+ const char* long_name_;
+ size_t long_name_len_;
+
+ // The text to display when referring to the value passed to this flag, for
+ // example "N" in the flag '--value N'. If null, this flag accepts no value
+ // and therefore no value must be passed.
+ const char* metavar_;
+
+ // The help string for this flag.
+ const char* help_text_;
+
+ // The pointer to the storage of this flag used when parsing.
+ T* storage_;
+
+ // At which verbosity level do we show this option?
+ int verbosity_level_;
+
+ // The function to use to parse the value when matched. The function used is
+ // parser_with_arg_ when metavar_ is not null (and the value string will be
+ // used) or parser_no_value_ when metavar_ is null.
+ union {
+ bool (*parser_with_arg_)(const char*, T*);
+ bool (*parser_no_value_)(T*);
+ } parser_;
+
+ // Whether this flag was matched.
+ bool matched_{false};
+ };
+
+ const char* program_name_{nullptr};
+
+ std::vector<std::unique_ptr<CmdOptionInterface>> options_;
+
+ // If true, help argument was given, so print help to stdout rather than
+ // stderr.
+ bool help_ = false;
+};
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_CMDLINE_H_
diff --git a/media/libjxl/src/tools/codec_config.cc b/media/libjxl/src/tools/codec_config.cc
new file mode 100644
index 0000000000..9ed64a23c8
--- /dev/null
+++ b/media/libjxl/src/tools/codec_config.cc
@@ -0,0 +1,57 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/codec_config.h"
+
+#include <hwy/targets.h>
+
+#include "lib/jxl/base/status.h"
+#include "tools/tool_version.h"
+
+namespace jpegxl {
+namespace tools {
+
+std::string CodecConfigString(uint32_t lib_version) {
+ std::string config;
+
+ if (lib_version != 0) {
+ char version_str[20];
+ snprintf(version_str, sizeof(version_str), "v%d.%d.%d ",
+ lib_version / 1000000, (lib_version / 1000) % 1000,
+ lib_version % 1000);
+ config += version_str;
+ }
+
+ std::string version = kJpegxlVersion;
+ if (version != "(unknown)") {
+ config += version + ' ';
+ }
+
+#if defined(ADDRESS_SANITIZER)
+ config += " asan ";
+#elif defined(MEMORY_SANITIZER)
+ config += " msan ";
+#elif defined(THREAD_SANITIZER)
+ config += " tsan ";
+#else
+#endif
+
+ bool saw_target = false;
+ config += "[";
+ for (const uint32_t target : hwy::SupportedAndGeneratedTargets()) {
+ config += hwy::TargetName(target);
+ config += ',';
+ saw_target = true;
+ }
+ JXL_ASSERT(saw_target);
+ (void)saw_target;
+ config.resize(config.size() - 1); // remove trailing comma
+ config += "]";
+
+ return config;
+}
+
+} // namespace tools
+} // namespace jpegxl
diff --git a/media/libjxl/src/tools/codec_config.h b/media/libjxl/src/tools/codec_config.h
new file mode 100644
index 0000000000..729c96d4a8
--- /dev/null
+++ b/media/libjxl/src/tools/codec_config.h
@@ -0,0 +1,22 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_CODEC_CONFIG_H_
+#define TOOLS_CODEC_CONFIG_H_
+
+#include <string>
+
+namespace jpegxl {
+namespace tools {
+
+// Returns a short string describing the codec version (if known) and build
+// settings such as sanitizers and SIMD targets. Used in the benchmark and
+// command-line tools.
+std::string CodecConfigString(uint32_t lib_version);
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_CODEC_CONFIG_H_
diff --git a/media/libjxl/src/tools/color_encoding_fuzzer.cc b/media/libjxl/src/tools/color_encoding_fuzzer.cc
new file mode 100644
index 0000000000..087bd8ba1e
--- /dev/null
+++ b/media/libjxl/src/tools/color_encoding_fuzzer.cc
@@ -0,0 +1,24 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <string>
+
+#include "lib/extras/dec/color_description.h"
+
+namespace jxl {
+
+int TestOneInput(const uint8_t* data, size_t size) {
+ std::string description(reinterpret_cast<const char*>(data), size);
+ JxlColorEncoding c;
+ (void)ParseDescription(description, &c);
+
+ return 0;
+}
+
+} // namespace jxl
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return jxl::TestOneInput(data, size);
+}
diff --git a/media/libjxl/src/tools/comparison_viewer/CMakeLists.txt b/media/libjxl/src/tools/comparison_viewer/CMakeLists.txt
new file mode 100644
index 0000000000..b5b5fa742e
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/CMakeLists.txt
@@ -0,0 +1,74 @@
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+find_package(Qt5 QUIET COMPONENTS Concurrent Widgets)
+if (NOT Qt5_FOUND)
+ message(WARNING "Qt5 was not found. The comparison tool will not be built.")
+ return()
+endif ()
+
+if (NOT TARGET icc_detect)
+ message(WARNING "icc_detect not built. The comparison tool will not be built.")
+ return ()
+endif ()
+
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOUIC ON)
+
+add_library(image_loading STATIC
+ ../viewer/load_jxl.cc
+ ../viewer/load_jxl.h
+ image_loading.cc
+ image_loading.h
+)
+target_include_directories(image_loading PRIVATE
+ $<TARGET_PROPERTY:lcms2,INCLUDE_DIRECTORIES>
+)
+target_link_libraries(image_loading PUBLIC
+ Qt5::Widgets
+ jxl-static
+ jxl_threads-static
+ jxl_extras-static
+ lcms2
+)
+
+add_executable(compare_codecs WIN32
+ codec_comparison_window.cc
+ codec_comparison_window.h
+ codec_comparison_window.ui
+ compare_codecs.cc
+ settings.cc
+ settings.h
+ settings.ui
+ split_image_renderer.cc
+ split_image_renderer.h
+ split_image_view.cc
+ split_image_view.h
+ split_image_view.ui
+)
+target_link_libraries(compare_codecs
+ image_loading
+ Qt5::Concurrent
+ Qt5::Widgets
+ icc_detect
+)
+
+add_executable(compare_images WIN32
+ compare_images.cc
+ settings.cc
+ settings.h
+ settings.ui
+ split_image_renderer.cc
+ split_image_renderer.h
+ split_image_view.cc
+ split_image_view.h
+ split_image_view.ui
+)
+target_link_libraries(compare_images
+ image_loading
+ Qt5::Widgets
+ icc_detect
+)
diff --git a/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.cc b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.cc
new file mode 100644
index 0000000000..9bf6253ba6
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.cc
@@ -0,0 +1,316 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/comparison_viewer/codec_comparison_window.h"
+
+#include <stdlib.h>
+
+#include <QCollator>
+#include <QComboBox>
+#include <QDir>
+#include <QFileInfo>
+#include <QFlags>
+#include <QIcon>
+#include <QImage>
+#include <QImageReader>
+#include <QLabel>
+#include <QList>
+#include <QMap>
+#include <QString>
+#include <QStringList>
+#include <QtConcurrent>
+#include <algorithm>
+#include <climits>
+#include <functional>
+#include <utility>
+
+#include "lib/extras/codec.h"
+#include "tools/comparison_viewer/image_loading.h"
+#include "tools/comparison_viewer/split_image_view.h"
+#include "tools/icc_detect/icc_detect.h"
+
+namespace jxl {
+
+static constexpr char kPngSuffix[] = "png";
+
+namespace {
+
+QVector<QPair<QComboBox*, QString>> currentCodecSelection(
+ const Ui::CodecComparisonWindow& ui) {
+ QVector<QPair<QComboBox*, QString>> result;
+ for (QComboBox* const comboBox :
+ {ui.codec1ComboBox, ui.codec2ComboBox, ui.compressionLevel1ComboBox,
+ ui.compressionLevel2ComboBox}) {
+ result << qMakePair(comboBox, comboBox->currentText());
+ }
+ return result;
+}
+
+void restoreCodecSelection(
+ const QVector<QPair<QComboBox*, QString>>& selection) {
+ for (const auto& comboBox : selection) {
+ const int index = comboBox.first->findText(comboBox.second);
+ if (index != -1) {
+ comboBox.first->setCurrentIndex(index);
+ }
+ }
+}
+
+} // namespace
+
+CodecComparisonWindow::CodecComparisonWindow(const QString& directory,
+ const float intensityTarget,
+ QWidget* const parent)
+ : QMainWindow(parent),
+ intensityTarget_(intensityTarget),
+ monitorIccProfile_(GetMonitorIccProfile(this)) {
+ ui_.setupUi(this);
+
+ connect(ui_.imageSetComboBox, &QComboBox::currentTextChanged, this,
+ &CodecComparisonWindow::handleImageSetSelection);
+ connect(ui_.imageComboBox, &QComboBox::currentTextChanged, this,
+ &CodecComparisonWindow::handleImageSelection);
+
+ connect(ui_.codec1ComboBox, &QComboBox::currentTextChanged,
+ [this]() { handleCodecChange(Side::LEFT); });
+ connect(ui_.codec2ComboBox, &QComboBox::currentTextChanged,
+ [this]() { handleCodecChange(Side::RIGHT); });
+
+ connect(ui_.compressionLevel1ComboBox, &QComboBox::currentTextChanged,
+ [this]() { updateSideImage(Side::LEFT); });
+ connect(ui_.compressionLevel2ComboBox, &QComboBox::currentTextChanged,
+ [this]() { updateSideImage(Side::RIGHT); });
+
+ connect(ui_.match1Label, &QLabel::linkActivated,
+ [this]() { matchSize(Side::LEFT); });
+ connect(ui_.match2Label, &QLabel::linkActivated,
+ [this]() { matchSize(Side::RIGHT); });
+
+ connect(
+ ui_.splitImageView, &SplitImageView::renderingModeChanged,
+ [this](const SplitImageRenderer::RenderingMode newMode) {
+ switch (newMode) {
+ case SplitImageRenderer::RenderingMode::LEFT:
+ case SplitImageRenderer::RenderingMode::RIGHT: {
+ QString codec, compressionLevel;
+ if (newMode == SplitImageRenderer::RenderingMode::LEFT) {
+ codec = ui_.codec1ComboBox->currentText();
+ compressionLevel = ui_.compressionLevel1ComboBox->currentText();
+ } else {
+ codec = ui_.codec2ComboBox->currentText();
+ compressionLevel = ui_.compressionLevel2ComboBox->currentText();
+ }
+ ui_.renderingModeLabel->setText(tr("Currently displaying: %1 @ %2")
+ .arg(codec)
+ .arg(compressionLevel));
+ break;
+ }
+
+ case SplitImageRenderer::RenderingMode::MIDDLE:
+ ui_.renderingModeLabel->setText(
+ tr("Currently displaying the original image."));
+ break;
+
+ default:
+ ui_.renderingModeLabel->clear();
+ break;
+ }
+ });
+
+ loadDirectory(directory);
+}
+
+void CodecComparisonWindow::handleImageSetSelection(
+ const QString& imageSetName) {
+ const auto selection = currentCodecSelection(ui_);
+ {
+ const QSignalBlocker blocker(ui_.imageComboBox);
+ ui_.imageComboBox->clear();
+ }
+ const QStringList imageNames = imageSets_.value(imageSetName).keys();
+ const std::function<QIcon(const QString&)> loadIcon =
+ [this, &imageSetName](const QString& imageName) {
+ return QIcon(pathToOriginalImage(imageSetName, imageName));
+ };
+ const QFuture<QIcon> thumbnails = QtConcurrent::mapped(imageNames, loadIcon);
+ int i = 0;
+ for (const QString& imageName : imageNames) {
+ ui_.imageComboBox->addItem(thumbnails.resultAt(i), imageName);
+ ++i;
+ }
+ restoreCodecSelection(selection);
+}
+
+void CodecComparisonWindow::handleImageSelection(const QString& imageName) {
+ const QString imageSetName = ui_.imageSetComboBox->currentText();
+ ui_.splitImageView->setMiddleImage(
+ loadImage(pathToOriginalImage(imageSetName, imageName),
+ monitorIccProfile_, intensityTarget_));
+
+ const auto selection = currentCodecSelection(ui_);
+ QStringList codecs = imageSets_.value(imageSetName).value(imageName).keys();
+ for (QComboBox* const codecComboBox :
+ {ui_.codec1ComboBox, ui_.codec2ComboBox}) {
+ {
+ const QSignalBlocker blocker(codecComboBox);
+ codecComboBox->clear();
+ }
+ codecComboBox->addItems(codecs);
+ }
+ restoreCodecSelection(selection);
+}
+
+void CodecComparisonWindow::handleCodecChange(const Side side) {
+ const QComboBox* const codecComboBox =
+ side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox;
+ QComboBox* const compressionLevelComboBox =
+ side == Side::LEFT ? ui_.compressionLevel1ComboBox
+ : ui_.compressionLevel2ComboBox;
+
+ QStringList compressionLevels =
+ imageSets_.value(ui_.imageSetComboBox->currentText())
+ .value(ui_.imageComboBox->currentText())
+ .value(codecComboBox->currentText())
+ .keys();
+ QCollator collator;
+ collator.setNumericMode(true);
+ std::sort(compressionLevels.begin(), compressionLevels.end(), collator);
+
+ {
+ const QSignalBlocker blocker(compressionLevelComboBox);
+ compressionLevelComboBox->clear();
+ }
+ compressionLevelComboBox->addItems(compressionLevels);
+ matchSize(side);
+}
+
+void CodecComparisonWindow::updateSideImage(const Side side) {
+ const ComparableImage& imageInfo = currentlySelectedImage(side);
+ if (imageInfo.decodedImagePath.isEmpty()) return;
+ QImage image = loadImage(imageInfo.decodedImagePath, monitorIccProfile_,
+ intensityTarget_);
+ const int pixels = image.width() * image.height();
+ QLabel* const sizeInfoLabel =
+ side == Side::LEFT ? ui_.size1Label : ui_.size2Label;
+ if (pixels == 0) {
+ sizeInfoLabel->setText(tr("Empty image."));
+ } else {
+ const double bpp =
+ CHAR_BIT * static_cast<double>(imageInfo.byteSize) / pixels;
+ sizeInfoLabel->setText(tr("%L1bpp").arg(bpp));
+ }
+
+ if (side == Side::LEFT) {
+ ui_.splitImageView->setLeftImage(std::move(image));
+ } else {
+ ui_.splitImageView->setRightImage(std::move(image));
+ }
+}
+
+QString CodecComparisonWindow::pathToOriginalImage(
+ const QString& imageSetName, const QString& imageName) const {
+ return baseDirectory_.absolutePath() + "/" + imageSetName + "/" + imageName +
+ "/original.png";
+}
+
+CodecComparisonWindow::ComparableImage
+CodecComparisonWindow::currentlySelectedImage(const Side side) const {
+ const QComboBox* const codecComboBox =
+ side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox;
+ QComboBox* const compressionLevelComboBox =
+ side == Side::LEFT ? ui_.compressionLevel1ComboBox
+ : ui_.compressionLevel2ComboBox;
+
+ return imageSets_.value(ui_.imageSetComboBox->currentText())
+ .value(ui_.imageComboBox->currentText())
+ .value(codecComboBox->currentText())
+ .value(compressionLevelComboBox->currentText());
+}
+
+void CodecComparisonWindow::matchSize(const Side side) {
+ const Side otherSide = (side == Side::LEFT ? Side::RIGHT : Side::LEFT);
+ const qint64 otherSideSize = currentlySelectedImage(otherSide).byteSize;
+ if (otherSideSize == 0) return;
+
+ const QComboBox* const codecComboBox =
+ side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox;
+ QComboBox* const compressionLevelComboBox =
+ side == Side::LEFT ? ui_.compressionLevel1ComboBox
+ : ui_.compressionLevel2ComboBox;
+ const Codec codec = imageSets_.value(ui_.imageSetComboBox->currentText())
+ .value(ui_.imageComboBox->currentText())
+ .value(codecComboBox->currentText());
+ if (codec.empty()) return;
+ Codec::ConstIterator bestMatch = codec.begin();
+ for (auto it = codec.begin(); it != codec.end(); ++it) {
+ if (std::abs(it->byteSize - otherSideSize) <
+ std::abs(bestMatch->byteSize - otherSideSize)) {
+ bestMatch = it;
+ }
+ }
+ compressionLevelComboBox->setCurrentText(bestMatch.key());
+}
+
+void CodecComparisonWindow::loadDirectory(const QString& directory) {
+ baseDirectory_.setPath(directory);
+ baseDirectory_.makeAbsolute();
+ imageSets_.clear();
+ visited_.clear();
+
+ browseDirectory(directory);
+
+ {
+ const QSignalBlocker blocker(ui_.imageSetComboBox);
+ ui_.imageSetComboBox->clear();
+ }
+ ui_.imageSetComboBox->addItems(imageSets_.keys());
+}
+
+void CodecComparisonWindow::browseDirectory(const QDir& directory, int depth) {
+ for (const QFileInfo& subdirectory : directory.entryInfoList(
+ QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks)) {
+ if (visited_.contains(subdirectory.absoluteFilePath())) continue;
+ visited_.insert(subdirectory.absoluteFilePath());
+ browseDirectory(subdirectory.absoluteFilePath(), depth + 1);
+ }
+
+ // Need at least image_name/codec_name/file.
+ if (depth < 2) return;
+
+ for (const QFileInfo& file : directory.entryInfoList(QDir::Files)) {
+ if (file.suffix() == kPngSuffix) continue;
+ QString decodedImage;
+ if (canLoadImageWithExtension(file.suffix())) {
+ decodedImage = file.absoluteFilePath();
+ } else {
+ QFileInfo png(file.absolutePath() + "/" + file.completeBaseName() + "." +
+ kPngSuffix);
+ if (png.exists()) {
+ decodedImage = png.absoluteFilePath();
+ }
+ }
+
+ if (decodedImage.isEmpty()) continue;
+
+ const QString codec = file.absoluteDir().dirName();
+ QDir imageDirectory = file.absoluteDir();
+ if (!imageDirectory.cdUp()) return;
+ const QString imageName = imageDirectory.dirName();
+ QDir imageSetDirectory = imageDirectory;
+ if (!imageSetDirectory.cdUp()) return;
+ QString imageSetPath =
+ baseDirectory_.relativeFilePath(imageSetDirectory.absolutePath());
+ if (imageSetPath.isEmpty()) {
+ imageSetPath = ".";
+ }
+
+ ComparableImage& image =
+ imageSets_[imageSetPath][imageName][codec][file.completeBaseName()];
+ image.decodedImagePath = decodedImage;
+ image.byteSize = file.size();
+ }
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.h b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.h
new file mode 100644
index 0000000000..b157a5a9ef
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.h
@@ -0,0 +1,77 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_COMPARISON_VIEWER_CODEC_COMPARISON_WINDOW_H_
+#define TOOLS_COMPARISON_VIEWER_CODEC_COMPARISON_WINDOW_H_
+
+#include <QDir>
+#include <QMainWindow>
+#include <QMap>
+#include <QSet>
+#include <QString>
+
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/common.h"
+#include "tools/comparison_viewer/ui_codec_comparison_window.h"
+
+namespace jxl {
+
+class CodecComparisonWindow : public QMainWindow {
+ Q_OBJECT
+
+ public:
+ explicit CodecComparisonWindow(
+ const QString& directory, float intensityTarget = kDefaultIntensityTarget,
+ QWidget* parent = nullptr);
+ ~CodecComparisonWindow() override = default;
+
+ private slots:
+ void handleImageSetSelection(const QString& imageSetName);
+ void handleImageSelection(const QString& imageName);
+
+ private:
+ struct ComparableImage {
+ // Absolute path to the decoded PNG (or an image that Qt can read).
+ QString decodedImagePath;
+ // Size of the encoded image (*not* the PNG).
+ qint64 byteSize = 0;
+ };
+ // Keys are compression levels.
+ using Codec = QMap<QString, ComparableImage>;
+ // Keys are codec names.
+ using Codecs = QMap<QString, Codec>;
+ // Keys are image names (relative to the image set directory).
+ using ImageSet = QMap<QString, Codecs>;
+ // Keys are paths to image sets (relative to the base directory chosen by the
+ // user).
+ using ImageSets = QMap<QString, ImageSet>;
+
+ enum class Side { LEFT, RIGHT };
+
+ QString pathToOriginalImage(const QString& imageSet,
+ const QString& imageName) const;
+ ComparableImage currentlySelectedImage(Side side) const;
+
+ void handleCodecChange(Side side);
+ void updateSideImage(Side side);
+ void matchSize(Side side);
+
+ void loadDirectory(const QString& directory);
+ // Recursive, called by loadDirectory.
+ void browseDirectory(const QDir& directory, int depth = 0);
+
+ Ui::CodecComparisonWindow ui_;
+
+ QDir baseDirectory_;
+ ImageSets imageSets_;
+ QSet<QString> visited_;
+
+ const float intensityTarget_;
+ const QByteArray monitorIccProfile_;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_COMPARISON_VIEWER_CODEC_COMPARISON_WINDOW_H_
diff --git a/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.ui b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.ui
new file mode 100644
index 0000000000..1fbda6a1ca
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/codec_comparison_window.ui
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <comment>
+ Copyright (c) the JPEG XL Project Authors. All rights reserved.
+
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+ </comment>
+ <class>CodecComparisonWindow</class>
+ <widget class="QMainWindow" name="CodecComparisonWindow">
+ <property name="windowTitle">
+ <string>Codec Comparison Tool</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,1">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0,1">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="1">
+ <widget class="QComboBox" name="imageSetComboBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="imageSetLabel">
+ <property name="text">
+ <string>Image set:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="imageLabel">
+ <property name="text">
+ <string>Image:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="imageComboBox"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0">
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="1">
+ <widget class="QComboBox" name="compressionLevel1ComboBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="codec1ComboBox"/>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="match1Label">
+ <property name="text">
+ <string>&lt;a href=&quot;#match1&quot;&gt;Match →&lt;/a&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="0">
+ <widget class="QLabel" name="match2Label">
+ <property name="text">
+ <string>&lt;a href=&quot;#match2&quot;&gt;Match ←&lt;/a&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="compressionLevel2ComboBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="codec2ComboBox"/>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1,0,1,0">
+ <item>
+ <widget class="QLabel" name="size1Label">
+ <property name="text">
+ <string>No image loaded.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="renderingModeLabel">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="size2Label">
+ <property name="text">
+ <string>No image loaded.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="jxl::SplitImageView" name="splitImageView" native="true"/>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>jxl::SplitImageView</class>
+ <extends>QWidget</extends>
+ <header>split_image_view.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/media/libjxl/src/tools/comparison_viewer/compare_codecs.cc b/media/libjxl/src/tools/comparison_viewer/compare_codecs.cc
new file mode 100644
index 0000000000..932765e479
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/compare_codecs.cc
@@ -0,0 +1,75 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdlib.h>
+
+#include <QApplication>
+#include <QCommandLineParser>
+#include <QMessageBox>
+#include <QString>
+#include <QStringList>
+
+#include "tools/comparison_viewer/codec_comparison_window.h"
+
+int main(int argc, char** argv) {
+ QApplication application(argc, argv);
+
+ QCommandLineParser parser;
+ parser.setApplicationDescription(
+ QCoreApplication::translate("compare_codecs", "Codec comparison tool"));
+ parser.addHelpOption();
+
+ QCommandLineOption intensityTargetOption(
+ {"intensity-target", "intensity_target", "i"},
+ QCoreApplication::translate("compare_codecs",
+ "The peak luminance of the display."),
+ QCoreApplication::translate("compare_codecs", "nits"),
+ QString::number(jxl::kDefaultIntensityTarget));
+ parser.addOption(intensityTargetOption);
+
+ parser.addPositionalArgument(
+ "folders", QCoreApplication::translate("compare_codecs", "Image folders"),
+ "<folders>...");
+
+ parser.process(application);
+
+ bool ok;
+ const float intensityTarget =
+ parser.value(intensityTargetOption).toFloat(&ok);
+ if (!ok) {
+ parser.showHelp(EXIT_FAILURE);
+ }
+
+ QStringList folders = parser.positionalArguments();
+
+ if (folders.empty()) {
+ QMessageBox message;
+ message.setIcon(QMessageBox::Information);
+ message.setWindowTitle(
+ QCoreApplication::translate("CodecComparisonWindow", "Usage"));
+ message.setText(QCoreApplication::translate(
+ "CodecComparisonWindow", "Please specify a directory to use."));
+ message.setDetailedText(QCoreApplication::translate(
+ "CodecComparisonWindow",
+ "That directory should contain images in the following layout:\n"
+ "- .../<image name>/original.png (optional)\n"
+ "- .../<image_name>/<codec_name>/<compression_level>.<ext>\n"
+ "- .../<image_name>/<codec_name>/<compression_level>.png (optional for "
+ "formats that Qt can load)\n"
+ "With arbitrary nesting allowed before that. (The \"...\" part is "
+ "referred to as an \"image set\" by the tool."));
+ message.exec();
+ return EXIT_FAILURE;
+ }
+
+ for (const QString& folder : folders) {
+ auto* const window =
+ new jxl::CodecComparisonWindow(folder, intensityTarget);
+ window->setAttribute(Qt::WA_DeleteOnClose);
+ window->show();
+ }
+
+ return application.exec();
+}
diff --git a/media/libjxl/src/tools/comparison_viewer/compare_images.cc b/media/libjxl/src/tools/comparison_viewer/compare_images.cc
new file mode 100644
index 0000000000..cf39f88128
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/compare_images.cc
@@ -0,0 +1,128 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdlib.h>
+
+#include <QApplication>
+#include <QCommandLineOption>
+#include <QCommandLineParser>
+#include <QFlags>
+#include <QImage>
+#include <QMessageBox>
+#include <QStringList>
+
+#include "tools/comparison_viewer/image_loading.h"
+#include "tools/comparison_viewer/split_image_view.h"
+#include "tools/icc_detect/icc_detect.h"
+
+namespace {
+
+void displayLoadingError(const QString& path) {
+ QMessageBox message;
+ message.setIcon(QMessageBox::Critical);
+ message.setWindowTitle(
+ QCoreApplication::translate("SplitImageView", "Error"));
+ message.setText(QCoreApplication::translate("SplitImageView",
+ "Could not load image \"%1\".")
+ .arg(path));
+ message.exec();
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+ QApplication application(argc, argv);
+
+ QCommandLineParser parser;
+ parser.setApplicationDescription(
+ QCoreApplication::translate("compare_images", "Image comparison tool"));
+ parser.addHelpOption();
+ parser.addPositionalArgument(
+ "left-image",
+ QCoreApplication::translate("compare_images",
+ "The image to display on the left."),
+ "<left-image>");
+ parser.addPositionalArgument(
+ "right-image",
+ QCoreApplication::translate("compare_images",
+ "The image to display on the right."),
+ "<right-image>");
+ parser.addPositionalArgument(
+ "middle-image",
+ QCoreApplication::translate(
+ "compare_images", "The image to display in the middle (optional)."),
+ "[<middle-image>]");
+
+ QCommandLineOption colorSpaceOption(
+ {"color-space", "color_space", "c"},
+ QCoreApplication::translate(
+ "compare_images",
+ "The color space to use for untagged images (typically PNM)."),
+ QCoreApplication::translate("compare_images", "color-space"));
+ parser.addOption(colorSpaceOption);
+
+ QCommandLineOption intensityTargetOption(
+ {"intensity-target", "intensity_target", "i"},
+ QCoreApplication::translate("compare_images",
+ "The peak luminance of the display."),
+ QCoreApplication::translate("compare_images", "nits"),
+ QString::number(jxl::kDefaultIntensityTarget));
+ parser.addOption(intensityTargetOption);
+
+ parser.process(application);
+
+ const QString colorSpaceHint = parser.value(colorSpaceOption);
+
+ QStringList arguments = parser.positionalArguments();
+ if (arguments.size() < 2 || arguments.size() > 3) {
+ parser.showHelp(EXIT_FAILURE);
+ }
+
+ bool ok;
+ const float intensityTarget =
+ parser.value(intensityTargetOption).toFloat(&ok);
+ if (!ok) {
+ parser.showHelp(EXIT_FAILURE);
+ }
+
+ jxl::SplitImageView view;
+
+ const QByteArray monitorIccProfile = jxl::GetMonitorIccProfile(&view);
+
+ const QString leftImagePath = arguments.takeFirst();
+ QImage leftImage = jxl::loadImage(leftImagePath, monitorIccProfile,
+ intensityTarget, colorSpaceHint);
+ if (leftImage.isNull()) {
+ displayLoadingError(leftImagePath);
+ return EXIT_FAILURE;
+ }
+ view.setLeftImage(std::move(leftImage));
+
+ const QString rightImagePath = arguments.takeFirst();
+ QImage rightImage = jxl::loadImage(rightImagePath, monitorIccProfile,
+ intensityTarget, colorSpaceHint);
+ if (rightImage.isNull()) {
+ displayLoadingError(rightImagePath);
+ return EXIT_FAILURE;
+ }
+ view.setRightImage(std::move(rightImage));
+
+ if (!arguments.empty()) {
+ const QString middleImagePath = arguments.takeFirst();
+ QImage middleImage = jxl::loadImage(middleImagePath, monitorIccProfile,
+ intensityTarget, colorSpaceHint);
+ if (middleImage.isNull()) {
+ displayLoadingError(middleImagePath);
+ return EXIT_FAILURE;
+ }
+ view.setMiddleImage(std::move(middleImage));
+ }
+
+ view.setWindowFlags(view.windowFlags() | Qt::Window);
+ view.setWindowState(Qt::WindowMaximized);
+ view.show();
+
+ return application.exec();
+}
diff --git a/media/libjxl/src/tools/comparison_viewer/image_loading.cc b/media/libjxl/src/tools/comparison_viewer/image_loading.cc
new file mode 100644
index 0000000000..55bebb8a1a
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/image_loading.cc
@@ -0,0 +1,111 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/comparison_viewer/image_loading.h"
+
+#include <QRgb>
+#include <QThread>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/color_hints.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/enc_color_management.h"
+#include "tools/viewer/load_jxl.h"
+
+namespace jxl {
+
+namespace {
+
+Status loadFromFile(const QString& filename,
+ const extras::ColorHints& color_hints,
+ CodecInOut* const decoded, ThreadPool* const pool) {
+ PaddedBytes compressed;
+ JXL_RETURN_IF_ERROR(ReadFile(filename.toStdString(), &compressed));
+ const Span<const uint8_t> compressed_span(compressed);
+ return SetFromBytes(compressed_span, color_hints, decoded, pool, nullptr);
+}
+
+} // namespace
+
+bool canLoadImageWithExtension(QString extension) {
+ extension = extension.toLower();
+ size_t bitsPerSampleUnused;
+ return extension == "jxl" || extension == "j" || extension == "brn" ||
+ extras::CodecFromExtension("." + extension.toStdString(),
+ &bitsPerSampleUnused) !=
+ jxl::extras::Codec::kUnknown;
+}
+
+QImage loadImage(const QString& filename, const QByteArray& targetIccProfile,
+ const float intensityTarget,
+ const QString& sourceColorSpaceHint) {
+ qint64 elapsed;
+ QImage img = loadJxlImage(filename, targetIccProfile, &elapsed);
+ if (img.width() != 0 && img.height() != 0) {
+ return img;
+ }
+ static ThreadPoolInternal pool(QThread::idealThreadCount());
+
+ CodecInOut decoded;
+ extras::ColorHints color_hints;
+ if (!sourceColorSpaceHint.isEmpty()) {
+ color_hints.Add("color_space", sourceColorSpaceHint.toStdString());
+ }
+ if (!loadFromFile(filename, color_hints, &decoded, &pool)) {
+ return QImage();
+ }
+ decoded.metadata.m.SetIntensityTarget(intensityTarget);
+ const ImageBundle& ib = decoded.Main();
+
+ ColorEncoding targetColorSpace;
+ PaddedBytes icc;
+ icc.assign(reinterpret_cast<const uint8_t*>(targetIccProfile.data()),
+ reinterpret_cast<const uint8_t*>(targetIccProfile.data() +
+ targetIccProfile.size()));
+ if (!targetColorSpace.SetICC(std::move(icc))) {
+ targetColorSpace = ColorEncoding::SRGB(ib.IsGray());
+ }
+ Image3F converted;
+ if (!ib.CopyTo(Rect(ib), targetColorSpace, GetJxlCms(), &converted, &pool)) {
+ return QImage();
+ }
+
+ QImage image(converted.xsize(), converted.ysize(), QImage::Format_ARGB32);
+
+ const auto ScaleAndClamp = [](const float x) {
+ return Clamp1(x * 255 + .5f, 0.f, 255.f);
+ };
+
+ if (ib.HasAlpha()) {
+ for (int y = 0; y < image.height(); ++y) {
+ QRgb* const row = reinterpret_cast<QRgb*>(image.scanLine(y));
+ const float* const alphaRow = ib.alpha().ConstRow(y);
+ const float* const redRow = converted.ConstPlaneRow(0, y);
+ const float* const greenRow = converted.ConstPlaneRow(1, y);
+ const float* const blueRow = converted.ConstPlaneRow(2, y);
+ for (int x = 0; x < image.width(); ++x) {
+ row[x] = qRgba(ScaleAndClamp(redRow[x]), ScaleAndClamp(greenRow[x]),
+ ScaleAndClamp(blueRow[x]), ScaleAndClamp(alphaRow[x]));
+ }
+ }
+ } else {
+ for (int y = 0; y < image.height(); ++y) {
+ QRgb* const row = reinterpret_cast<QRgb*>(image.scanLine(y));
+ const float* const redRow = converted.ConstPlaneRow(0, y);
+ const float* const greenRow = converted.ConstPlaneRow(1, y);
+ const float* const blueRow = converted.ConstPlaneRow(2, y);
+ for (int x = 0; x < image.width(); ++x) {
+ row[x] = qRgb(ScaleAndClamp(redRow[x]), ScaleAndClamp(greenRow[x]),
+ ScaleAndClamp(blueRow[x]));
+ }
+ }
+ }
+
+ return image;
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/comparison_viewer/image_loading.h b/media/libjxl/src/tools/comparison_viewer/image_loading.h
new file mode 100644
index 0000000000..89b37d13b9
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/image_loading.h
@@ -0,0 +1,29 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_COMPARISON_VIEWER_IMAGE_LOADING_H_
+#define TOOLS_COMPARISON_VIEWER_IMAGE_LOADING_H_
+
+#include <QByteArray>
+#include <QImage>
+#include <QString>
+
+#include "lib/jxl/common.h"
+
+namespace jxl {
+
+// `extension` should not include the dot.
+bool canLoadImageWithExtension(QString extension);
+
+// Converts the loaded image to the given display profile, or sRGB if not
+// specified. Thread-hostile.
+QImage loadImage(const QString& filename,
+ const QByteArray& targetIccProfile = QByteArray(),
+ float intensityTarget = kDefaultIntensityTarget,
+ const QString& sourceColorSpaceHint = QString());
+
+} // namespace jxl
+
+#endif // TOOLS_COMPARISON_VIEWER_IMAGE_LOADING_H_
diff --git a/media/libjxl/src/tools/comparison_viewer/settings.cc b/media/libjxl/src/tools/comparison_viewer/settings.cc
new file mode 100644
index 0000000000..9ef117b0a7
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/settings.cc
@@ -0,0 +1,51 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/comparison_viewer/settings.h"
+
+namespace jxl {
+
+SettingsDialog::SettingsDialog(QWidget* const parent)
+ : QDialog(parent), settings_("JPEG XL project", "Comparison tool") {
+ ui_.setupUi(this);
+
+ settings_.beginGroup("rendering");
+ renderingSettings_.fadingMSecs = settings_.value("fadingMSecs", 300).toInt();
+ settings_.beginGroup("gray");
+ renderingSettings_.gray = settings_.value("enabled", false).toBool();
+ renderingSettings_.grayMSecs = settings_.value("delayMSecs", 300).toInt();
+ settings_.endGroup();
+ settings_.endGroup();
+
+ settingsToUi();
+}
+
+SplitImageRenderingSettings SettingsDialog::renderingSettings() const {
+ return renderingSettings_;
+}
+
+void SettingsDialog::on_SettingsDialog_accepted() {
+ renderingSettings_.fadingMSecs = ui_.fadingTime->value();
+ renderingSettings_.gray = ui_.grayGroup->isChecked();
+ renderingSettings_.grayMSecs = ui_.grayTime->value();
+
+ settings_.beginGroup("rendering");
+ settings_.setValue("fadingMSecs", renderingSettings_.fadingMSecs);
+ settings_.beginGroup("gray");
+ settings_.setValue("enabled", renderingSettings_.gray);
+ settings_.setValue("delayMSecs", renderingSettings_.grayMSecs);
+ settings_.endGroup();
+ settings_.endGroup();
+}
+
+void SettingsDialog::on_SettingsDialog_rejected() { settingsToUi(); }
+
+void SettingsDialog::settingsToUi() {
+ ui_.fadingTime->setValue(renderingSettings_.fadingMSecs);
+ ui_.grayGroup->setChecked(renderingSettings_.gray);
+ ui_.grayTime->setValue(renderingSettings_.grayMSecs);
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/comparison_viewer/settings.h b/media/libjxl/src/tools/comparison_viewer/settings.h
new file mode 100644
index 0000000000..bd91f710aa
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/settings.h
@@ -0,0 +1,40 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_COMPARISON_VIEWER_SETTINGS_H_
+#define TOOLS_COMPARISON_VIEWER_SETTINGS_H_
+
+#include <QDialog>
+#include <QSettings>
+
+#include "tools/comparison_viewer/split_image_renderer.h"
+#include "tools/comparison_viewer/ui_settings.h"
+
+namespace jxl {
+
+class SettingsDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ explicit SettingsDialog(QWidget* parent = nullptr);
+ ~SettingsDialog() override = default;
+
+ SplitImageRenderingSettings renderingSettings() const;
+
+ private slots:
+ void on_SettingsDialog_accepted();
+ void on_SettingsDialog_rejected();
+
+ private:
+ void settingsToUi();
+
+ Ui::SettingsDialog ui_;
+ QSettings settings_;
+ SplitImageRenderingSettings renderingSettings_;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_COMPARISON_VIEWER_SETTINGS_H_
diff --git a/media/libjxl/src/tools/comparison_viewer/settings.ui b/media/libjxl/src/tools/comparison_viewer/settings.ui
new file mode 100644
index 0000000000..ca81a33aec
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/settings.ui
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <comment>
+ Copyright (c) the JPEG XL Project Authors. All rights reserved.
+
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+ </comment>
+ <class>SettingsDialog</class>
+ <widget class="QDialog" name="SettingsDialog">
+ <property name="windowTitle">
+ <string>Comparison tool settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetFixedSize</enum>
+ </property>
+ <item>
+ <layout class="QFormLayout" name="settingsLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="fadingTimePromptLabel">
+ <property name="text">
+ <string>Fading time:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="fadingTime">
+ <property name="suffix">
+ <string> ms</string>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="singleStep">
+ <number>50</number>
+ </property>
+ <property name="value">
+ <number>300</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="grayGroup">
+ <property name="title">
+ <string>Gray in between</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="grayTime">
+ <property name="suffix">
+ <string> ms</string>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="singleStep">
+ <number>50</number>
+ </property>
+ <property name="value">
+ <number>300</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="grayTimePromptLabel">
+ <property name="text">
+ <string>Time on gray:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>SettingsDialog</receiver>
+ <slot>accept()</slot>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>SettingsDialog</receiver>
+ <slot>reject()</slot>
+ </connection>
+ </connections>
+</ui>
diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_renderer.cc b/media/libjxl/src/tools/comparison_viewer/split_image_renderer.cc
new file mode 100644
index 0000000000..acade64d34
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/split_image_renderer.cc
@@ -0,0 +1,239 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/comparison_viewer/split_image_renderer.h"
+
+#include <algorithm>
+#include <cmath>
+#include <utility>
+
+#include <QEvent>
+#include <QGuiApplication>
+#include <QPainter>
+#include <QPalette>
+#include <QPen>
+#include <QPoint>
+#include <QRect>
+
+namespace jxl {
+
+SplitImageRenderer::SplitImageRenderer(QWidget* const parent)
+ : QWidget(parent) {
+ setAttribute(Qt::WA_OpaquePaintEvent);
+ setMouseTracking(true);
+ setFocusPolicy(Qt::WheelFocus);
+ grabKeyboard();
+
+ connect(&fadingPoint_, &QVariantAnimation::valueChanged,
+ [this] { update(); });
+}
+
+void SplitImageRenderer::setLeftImage(QImage image) {
+ leftImage_ = QPixmap::fromImage(std::move(image));
+ updateMinimumSize();
+ update();
+}
+void SplitImageRenderer::setRightImage(QImage image) {
+ rightImage_ = QPixmap::fromImage(std::move(image));
+ updateMinimumSize();
+ update();
+}
+void SplitImageRenderer::setMiddleImage(QImage image) {
+ middleImage_ = QPixmap::fromImage(std::move(image));
+ updateMinimumSize();
+ update();
+}
+
+void SplitImageRenderer::setRenderingSettings(
+ const SplitImageRenderingSettings& settings) {
+ renderingSettings_ = settings;
+}
+
+void SplitImageRenderer::setMiddleWidthPercent(const int percent) {
+ middleWidthPercent_ = percent;
+ update();
+}
+
+void SplitImageRenderer::setZoomLevel(double scale) {
+ scale_ = scale;
+ updateMinimumSize();
+ update();
+}
+
+void SplitImageRenderer::keyPressEvent(QKeyEvent* const event) {
+ switch (event->key()) {
+ case Qt::Key_Left:
+ setRenderingMode(RenderingMode::LEFT);
+ break;
+
+ case Qt::Key_Right:
+ setRenderingMode(RenderingMode::RIGHT);
+ break;
+
+ case Qt::Key_Up:
+ case Qt::Key_Down:
+ setRenderingMode(RenderingMode::MIDDLE);
+ break;
+
+ case Qt::Key_Escape:
+ QCoreApplication::quit();
+ break;
+
+ case Qt::Key_ZoomIn:
+ emit zoomLevelIncreaseRequested();
+ break;
+ case Qt::Key_ZoomOut:
+ emit zoomLevelDecreaseRequested();
+ break;
+
+ default:
+ QWidget::keyPressEvent(event);
+ break;
+ }
+ update();
+}
+
+void SplitImageRenderer::mouseMoveEvent(QMouseEvent* const event) {
+ setRenderingMode(RenderingMode::SPLIT);
+ middleX_ = event->pos().x();
+ update();
+}
+
+void SplitImageRenderer::wheelEvent(QWheelEvent* event) {
+ if (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) {
+ if (event->angleDelta().y() > 0) {
+ emit zoomLevelIncreaseRequested();
+ return;
+ } else if (event->angleDelta().y() < 0) {
+ emit zoomLevelDecreaseRequested();
+ return;
+ }
+ }
+
+ event->ignore();
+}
+
+void SplitImageRenderer::paintEvent(QPaintEvent* const event) {
+ QRectF drawingArea(0., 0., minimumWidth(), minimumHeight());
+
+ QPainter painter(this);
+ painter.fillRect(rect(), QColor(119, 119, 119));
+ painter.translate(QRectF(rect()).center() - drawingArea.center());
+ painter.scale(scale_, scale_);
+ if (scale_ < 1.) {
+ painter.setRenderHint(QPainter::SmoothPixmapTransform);
+ }
+
+ const auto drawSingleImage = [&](const RenderingMode mode) {
+ const QPixmap* image = nullptr;
+ switch (mode) {
+ case RenderingMode::LEFT:
+ image = &leftImage_;
+ break;
+ case RenderingMode::RIGHT:
+ image = &rightImage_;
+ break;
+ case RenderingMode::MIDDLE:
+ image = &middleImage_;
+ break;
+
+ default:
+ return;
+ }
+ painter.drawPixmap(QPointF(0., 0.), *image);
+ };
+
+ if (mode_ != RenderingMode::SPLIT) {
+ if (fadingPoint_.state() != QAbstractAnimation::Running) {
+ drawSingleImage(mode_);
+ return;
+ }
+
+ const float fadingPoint = fadingPoint_.currentValue().toFloat();
+ if (renderingSettings_.gray) {
+ if (fadingPoint < renderingSettings_.fadingMSecs) {
+ painter.setOpacity((renderingSettings_.fadingMSecs - fadingPoint) /
+ renderingSettings_.fadingMSecs);
+ drawSingleImage(previousMode_);
+ } else if (fadingPoint > renderingSettings_.fadingMSecs +
+ renderingSettings_.grayMSecs) {
+ painter.setOpacity((fadingPoint - renderingSettings_.fadingMSecs -
+ renderingSettings_.grayMSecs) /
+ renderingSettings_.fadingMSecs);
+ drawSingleImage(mode_);
+ }
+ } else {
+ drawSingleImage(previousMode_);
+ painter.setOpacity(fadingPoint / renderingSettings_.fadingMSecs);
+ drawSingleImage(mode_);
+ }
+
+ return;
+ }
+
+ const qreal middleWidth =
+ std::min<qreal>((minimumWidth() / scale_) * middleWidthPercent_ / 100.,
+ middleImage_.width());
+
+ const double transformedMiddleX =
+ painter.transform().inverted().map(QPointF(middleX_, 0.)).x();
+ QRectF middleRect = middleImage_.rect();
+ middleRect.setWidth(middleWidth);
+ middleRect.moveCenter(QPointF(transformedMiddleX, middleRect.center().y()));
+ middleRect.setLeft(std::round(middleRect.left()));
+ middleRect.setRight(std::round(middleRect.right()));
+
+ QRectF leftRect = leftImage_.rect();
+ leftRect.setRight(middleRect.left());
+
+ QRectF rightRect = rightImage_.rect();
+ rightRect.setLeft(middleRect.right());
+
+ painter.drawPixmap(leftRect, leftImage_, leftRect);
+ painter.drawPixmap(rightRect, rightImage_, rightRect);
+ painter.drawPixmap(middleRect, middleImage_, middleRect);
+
+ QPen middlePen;
+ middlePen.setStyle(Qt::DotLine);
+ painter.setPen(middlePen);
+ painter.drawLine(leftRect.topRight(), leftRect.bottomRight());
+ painter.drawLine(rightRect.topLeft(), rightRect.bottomLeft());
+}
+
+void SplitImageRenderer::updateMinimumSize() {
+ const int imagesWidth = std::max(
+ std::max(leftImage_.width(), rightImage_.width()), middleImage_.width());
+ const int imagesHeight =
+ std::max(std::max(leftImage_.height(), rightImage_.height()),
+ middleImage_.height());
+ setMinimumSize(scale_ * QSize(imagesWidth, imagesHeight));
+}
+
+void SplitImageRenderer::setRenderingMode(const RenderingMode newMode) {
+ if (newMode == mode_) return;
+ previousMode_ = mode_;
+ mode_ = newMode;
+ if (previousMode_ == RenderingMode::SPLIT || mode_ == RenderingMode::SPLIT) {
+ fadingPoint_.stop();
+ } else {
+ const int msecs =
+ renderingSettings_.gray
+ ? 2 * renderingSettings_.fadingMSecs + renderingSettings_.grayMSecs
+ : renderingSettings_.fadingMSecs;
+ const float startValue = fadingPoint_.state() == QAbstractAnimation::Running
+ ? fadingPoint_.endValue().toFloat() -
+ fadingPoint_.currentValue().toFloat()
+ : 0.f;
+ fadingPoint_.stop();
+ fadingPoint_.setStartValue(startValue);
+ fadingPoint_.setEndValue(static_cast<float>(msecs));
+ fadingPoint_.setDuration(fadingPoint_.endValue().toFloat() -
+ fadingPoint_.startValue().toFloat());
+ fadingPoint_.start();
+ }
+ emit renderingModeChanged(mode_);
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_renderer.h b/media/libjxl/src/tools/comparison_viewer/split_image_renderer.h
new file mode 100644
index 0000000000..decb407ff3
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/split_image_renderer.h
@@ -0,0 +1,90 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_RENDERER_H_
+#define TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_RENDERER_H_
+
+#include <QImage>
+#include <QKeyEvent>
+#include <QMouseEvent>
+#include <QPaintEvent>
+#include <QPixmap>
+#include <QVariantAnimation>
+#include <QWheelEvent>
+#include <QWidget>
+
+namespace jxl {
+
+struct SplitImageRenderingSettings {
+ int fadingMSecs;
+ bool gray;
+ int grayMSecs;
+};
+
+class SplitImageRenderer : public QWidget {
+ Q_OBJECT
+
+ public:
+ enum class RenderingMode {
+ // The default mode when using the mouse: one (partial) image is shown on
+ // each side of the cursor, with a vertical band of the middle image if
+ // applicable.
+ SPLIT,
+ // Only show the left image (accessed by pressing the left arrow key when
+ // the renderer has focus).
+ LEFT,
+ // Only show the right image (accessed by pressing the right arrow key).
+ RIGHT,
+ // Only show the middle image (accessed by pressing the up or down arrow
+ // key).
+ MIDDLE,
+ };
+ Q_ENUM(RenderingMode)
+
+ explicit SplitImageRenderer(QWidget* parent = nullptr);
+ ~SplitImageRenderer() override = default;
+
+ QSize sizeHint() const override { return minimumSize(); }
+
+ void setLeftImage(QImage image);
+ void setRightImage(QImage image);
+ void setMiddleImage(QImage image);
+
+ void setRenderingSettings(const SplitImageRenderingSettings& settings);
+
+ public slots:
+ void setMiddleWidthPercent(int percent);
+ void setZoomLevel(double scale);
+
+ signals:
+ void zoomLevelIncreaseRequested();
+ void zoomLevelDecreaseRequested();
+
+ void renderingModeChanged(RenderingMode newMode);
+
+ protected:
+ void keyPressEvent(QKeyEvent* event) override;
+ void mouseMoveEvent(QMouseEvent* event) override;
+ void wheelEvent(QWheelEvent* event) override;
+ void paintEvent(QPaintEvent* event) override;
+
+ private:
+ void updateMinimumSize();
+ void setRenderingMode(RenderingMode newMode);
+
+ QPixmap leftImage_, rightImage_, middleImage_;
+ RenderingMode mode_ = RenderingMode::SPLIT;
+ RenderingMode previousMode_ = RenderingMode::SPLIT;
+ SplitImageRenderingSettings renderingSettings_;
+ // Goes from 0 to the animation duration in milliseconds, as a float.
+ QVariantAnimation fadingPoint_;
+ int middleX_ = 0;
+ int middleWidthPercent_ = 10;
+ double scale_ = 1.;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_RENDERER_H_
diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_view.cc b/media/libjxl/src/tools/comparison_viewer/split_image_view.cc
new file mode 100644
index 0000000000..76c8edca7c
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/split_image_view.cc
@@ -0,0 +1,71 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/comparison_viewer/split_image_view.h"
+
+#include <utility>
+
+#include <QLabel>
+
+#include "tools/comparison_viewer/split_image_renderer.h"
+
+namespace jxl {
+
+SplitImageView::SplitImageView(QWidget* const parent) : QWidget(parent) {
+ ui_.setupUi(this);
+
+ ui_.splitImageRenderer->setRenderingSettings(settings_.renderingSettings());
+
+ connect(ui_.middleWidthSlider, &QSlider::valueChanged,
+ [this](const int value) {
+ ui_.middleWidthDisplayLabel->setText(tr("%L1%").arg(value));
+ });
+ connect(ui_.middleWidthSlider, &QSlider::valueChanged, ui_.splitImageRenderer,
+ &SplitImageRenderer::setMiddleWidthPercent);
+
+ connect(ui_.zoomLevelSlider, &QSlider::valueChanged, [this](const int value) {
+ if (value >= 0) {
+ ui_.zoomLevelDisplayLabel->setText(tr("&times;%L1").arg(1 << value));
+ ui_.splitImageRenderer->setZoomLevel(1 << value);
+ } else {
+ ui_.zoomLevelDisplayLabel->setText(tr("&times;1/%L1").arg(1 << -value));
+ ui_.splitImageRenderer->setZoomLevel(1. / (1 << -value));
+ }
+ });
+
+ connect(ui_.splitImageRenderer,
+ &SplitImageRenderer::zoomLevelIncreaseRequested, [this]() {
+ ui_.zoomLevelSlider->triggerAction(
+ QAbstractSlider::SliderSingleStepAdd);
+ });
+ connect(ui_.splitImageRenderer,
+ &SplitImageRenderer::zoomLevelDecreaseRequested, [this]() {
+ ui_.zoomLevelSlider->triggerAction(
+ QAbstractSlider::SliderSingleStepSub);
+ });
+
+ connect(ui_.splitImageRenderer, &SplitImageRenderer::renderingModeChanged,
+ this, &SplitImageView::renderingModeChanged);
+}
+
+void SplitImageView::setLeftImage(QImage image) {
+ ui_.splitImageRenderer->setLeftImage(std::move(image));
+}
+
+void SplitImageView::setRightImage(QImage image) {
+ ui_.splitImageRenderer->setRightImage(std::move(image));
+}
+
+void SplitImageView::setMiddleImage(QImage image) {
+ ui_.splitImageRenderer->setMiddleImage(std::move(image));
+}
+
+void SplitImageView::on_settingsButton_clicked() {
+ if (settings_.exec()) {
+ ui_.splitImageRenderer->setRenderingSettings(settings_.renderingSettings());
+ }
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_view.h b/media/libjxl/src/tools/comparison_viewer/split_image_view.h
new file mode 100644
index 0000000000..4978750d1e
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/split_image_view.h
@@ -0,0 +1,40 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_VIEW_H_
+#define TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_VIEW_H_
+
+#include <QWidget>
+
+#include "tools/comparison_viewer/settings.h"
+#include "tools/comparison_viewer/ui_split_image_view.h"
+
+namespace jxl {
+
+class SplitImageView : public QWidget {
+ Q_OBJECT
+
+ public:
+ explicit SplitImageView(QWidget* parent = nullptr);
+ ~SplitImageView() override = default;
+
+ void setLeftImage(QImage image);
+ void setRightImage(QImage image);
+ void setMiddleImage(QImage image);
+
+ signals:
+ void renderingModeChanged(SplitImageRenderer::RenderingMode newMode);
+
+ private slots:
+ void on_settingsButton_clicked();
+
+ private:
+ Ui::SplitImageView ui_;
+ SettingsDialog settings_;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_VIEW_H_
diff --git a/media/libjxl/src/tools/comparison_viewer/split_image_view.ui b/media/libjxl/src/tools/comparison_viewer/split_image_view.ui
new file mode 100644
index 0000000000..0755a58d18
--- /dev/null
+++ b/media/libjxl/src/tools/comparison_viewer/split_image_view.ui
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <comment>
+ Copyright (c) the JPEG XL Project Authors. All rights reserved.
+
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+ </comment>
+ <class>SplitImageView</class>
+ <widget class="QWidget" name="SplitImageView">
+ <property name="windowTitle">
+ <string>Image Comparison Tool</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="jxl::SplitImageRenderer" name="splitImageRenderer"/>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0,0">
+ <item>
+ <layout class="QFormLayout" name="zoomLevelFormLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="zoomLevelPromptLabel">
+ <property name="text">
+ <string>Zoom level:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QSlider" name="zoomLevelSlider">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimum">
+ <number>-3</number>
+ </property>
+ <property name="maximum">
+ <number>3</number>
+ </property>
+ <property name="pageStep">
+ <number>2</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="zoomLevelDisplayLabel">
+ <property name="text">
+ <string>×1</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QFormLayout" name="middleWidthFormLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="middleWidthPromptLabel">
+ <property name="text">
+ <string>Width of the central band:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QSlider" name="middleWidthSlider">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>10</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="middleWidthDisplayLabel">
+ <property name="text">
+ <string>10%</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QToolButton" name="settingsButton">
+ <property name="text">
+ <string>Settings</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>jxl::SplitImageRenderer</class>
+ <extends>QWidget</extends>
+ <header>split_image_renderer.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/media/libjxl/src/tools/conformance/CMakeLists.txt b/media/libjxl/src/tools/conformance/CMakeLists.txt
new file mode 100644
index 0000000000..9bc7e317f7
--- /dev/null
+++ b/media/libjxl/src/tools/conformance/CMakeLists.txt
@@ -0,0 +1,21 @@
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+add_executable(djxl_conformance djxl_conformance.cc)
+target_link_libraries(djxl_conformance jxl_dec)
+
+if(BUILD_TESTING AND CMAKE_EXECUTABLE_SUFFIX STREQUAL "")
+# Script to validate the tooling.
+find_program (BASH_PROGRAM bash)
+if(BASH_PROGRAM)
+ add_test(
+ NAME conformance_tooling_test
+ COMMAND
+ ${BASH_PROGRAM} ${CMAKE_CURRENT_SOURCE_DIR}/tooling_test.sh
+ ${CMAKE_BINARY_DIR} ${JPEGXL_TEST_DATA_PATH})
+ # Skip the test if dependencies are not available.
+ set_tests_properties(conformance_tooling_test PROPERTIES SKIP_RETURN_CODE 254)
+endif()
+endif() # BUILD_TESTING
diff --git a/media/libjxl/src/tools/conformance/conformance.py b/media/libjxl/src/tools/conformance/conformance.py
new file mode 100755
index 0000000000..b012716d2e
--- /dev/null
+++ b/media/libjxl/src/tools/conformance/conformance.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+"""JPEG XL conformance test runner.
+
+Tool to perform a conformance test for a decoder.
+"""
+
+import argparse
+import json
+import numpy
+import os
+import subprocess
+import sys
+import tempfile
+
+import lcms2
+
+
+class ConformanceTestError(Exception):
+ """General conformance test error."""
+
+
+def CompareNPY(ref, ref_icc, dec, dec_icc, frame_idx, rmse, peak_error):
+ """Compare a decoded numpy against the reference one."""
+ if ref.shape != dec.shape:
+ raise ConformanceTestError(
+ f'Expected shape {ref.shape} but found {dec.shape}')
+ ref_frame = ref[frame_idx]
+ dec_frame = dec[frame_idx]
+ num_channels = ref_frame.shape[2]
+
+ if ref_icc != dec_icc:
+ # Transform colors before comparison.
+ if num_channels < 3:
+ raise ConformanceTestError(f"Only RGB images are supported")
+ ref_clr = ref_frame[:, :, 0:3]
+ dec_clr = dec_frame[:, :, 0:3]
+ dec_frame[:, :, 0:3] = lcms2.convert_pixels(dec_icc, ref_icc, dec_clr)
+
+ error = numpy.abs(ref_frame - dec_frame)
+ for ch in range(num_channels):
+ error_ch = error[:, :, ch]
+ actual_rmse = numpy.sqrt(numpy.mean(error_ch * error_ch))
+ if actual_rmse > rmse:
+ raise ConformanceTestError(
+ f"RMSE too large: {actual_rmse} > {rmse}")
+
+ actual_peak_error = error.max()
+ if actual_peak_error > peak_error:
+ raise ConformanceTestError(
+ f"Peak error too large: {actual_peak_error} > {peak_error}")
+
+
+def CompareBinaries(ref_bin, dec_bin):
+ """Compare a decoded binary file against the reference for exact contents."""
+ with open(ref_bin, 'rb') as reff:
+ ref_data = reff.read()
+
+ with open(dec_bin, 'rb') as decf:
+ dec_data = decf.read()
+
+ if ref_data != dec_data:
+ raise ConformanceTestError(
+ f'Binary files mismatch: {ref_bin} {dec_bin}')
+
+
+TEST_KEYS = set(
+ ['reconstructed_jpeg', 'original_icc', 'rms_error', 'peak_error'])
+
+
+def CheckMeta(dec, ref):
+ if isinstance(ref, dict):
+ if not isinstance(dec, dict):
+ raise ConformanceTestError("Malformed metadata file")
+ for k, v in ref.items():
+ if k in TEST_KEYS:
+ continue
+ if k not in dec:
+ raise ConformanceTestError(
+ f"Malformed metadata file: key {k} not found")
+ vv = dec[k]
+ CheckMeta(vv, v)
+ elif isinstance(ref, list):
+ if not isinstance(dec, list) or len(dec) != len(ref):
+ raise ConformanceTestError("Malformed metadata file")
+ for vv, v in zip(dec, ref):
+ CheckMeta(vv, v)
+ elif isinstance(ref, float):
+ if not isinstance(dec, float):
+ raise ConformanceTestError("Malformed metadata file")
+ if abs(dec - ref) > 0.0001:
+ raise ConformanceTestError(
+ f"Metadata: Expected {ref}, found {dec}")
+ elif dec != ref:
+ raise ConformanceTestError(f"Metadata: Expected {ref}, found {dec}")
+
+
+def ConformanceTestRunner(args):
+ # We can pass either the .txt file or the directory which defaults to the
+ # full corpus. This is useful to run a subset of the corpus in other .txt
+ # files.
+ if os.path.isdir(args.corpus):
+ corpus_dir = args.corpus
+ corpus_txt = os.path.join(args.corpus, 'corpus.txt')
+ else:
+ corpus_dir = os.path.dirname(args.corpus)
+ corpus_txt = args.corpus
+
+ with open(corpus_txt, 'r') as f:
+ for test_id in f:
+ test_id = test_id.rstrip('\n')
+ print('Testing %s' % test_id)
+ test_dir = os.path.join(corpus_dir, test_id)
+
+ with open(os.path.join(test_dir, 'test.json'), 'r') as f:
+ descriptor = json.load(f)
+ if 'sha256sums' in descriptor:
+ del descriptor['sha256sums']
+
+ exact_tests = []
+
+ with tempfile.TemporaryDirectory(prefix=test_id) as work_dir:
+ cmd = [args.decoder, os.path.join(test_dir, 'input.jxl')]
+ # Select the parameters to run.
+ pixel_prefix = os.path.join(work_dir, 'decoded')
+ cmd.extend(['-p', pixel_prefix])
+ if 'reconstructed_jpeg' in descriptor:
+ jpeg_filename = os.path.join(work_dir, 'reconstructed.jpg')
+ cmd.extend(['-j', jpeg_filename])
+ exact_tests.append(('reconstructed.jpg', jpeg_filename))
+ if 'original_icc' in descriptor:
+ decoded_original_icc = os.path.join(
+ work_dir, 'decoded_org.icc')
+ cmd.extend(['-i', decoded_original_icc])
+ exact_tests.append(('original.icc', decoded_original_icc))
+ meta_filename = os.path.join(work_dir, 'meta.json')
+ cmd.extend(['-m', meta_filename])
+
+ if subprocess.call(cmd) != 0:
+ raise ConformanceTestError(
+ 'Running the decoder (%s) returned error' %
+ ' '.join(cmd))
+
+ # Run validation of exact files.
+ for reference_basename, decoded_filename in exact_tests:
+ reference_filename = os.path.join(test_dir,
+ reference_basename)
+ CompareBinaries(reference_filename, decoded_filename)
+
+ # Validate metadata.
+ with open(meta_filename, 'r') as f:
+ meta = json.load(f)
+
+ CheckMeta(meta, descriptor)
+
+ # Pixel data.
+ decoded_icc = pixel_prefix + '.icc'
+ with open(decoded_icc, 'rb') as f:
+ decoded_icc = f.read()
+ reference_icc = os.path.join(test_dir, "reference.icc")
+ with open(reference_icc, 'rb') as f:
+ reference_icc = f.read()
+
+ reference_npy = os.path.join(test_dir, 'reference_image.npy')
+ decoded_npy = os.path.join(work_dir, 'decoded_image.npy')
+
+ if not os.path.exists(decoded_npy):
+ raise ConformanceTestError(
+ 'File not decoded: decoded_image.npy')
+
+ reference_npy = numpy.load(reference_npy)
+ decoded_npy = numpy.load(decoded_npy)
+
+ for i, fd in enumerate(descriptor['frames']):
+ CompareNPY(reference_npy, reference_icc, decoded_npy,
+ decoded_icc, i, fd['rms_error'],
+ fd['peak_error'])
+
+ if 'preview' in descriptor:
+ reference_npy = os.path.join(test_dir,
+ 'reference_preview.npy')
+ decoded_npy = os.path.join(work_dir, 'decoded_preview.npy')
+
+ if not os.path.exists(decoded_npy):
+ raise ConformanceTestError(
+ 'File not decoded: decoded_preview.npy')
+
+ reference_npy = numpy.load(reference_npy)
+ decoded_npy = numpy.load(decoded_npy)
+ CompareNPY(reference_npy, reference_icc, decoded_npy,
+ decoded_icc, 0,
+ descriptor['preview']['rms_error'],
+ descriptor['preview']['peak_error'])
+
+ return True
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--decoder',
+ metavar='DECODER',
+ required=True,
+ help='path to the decoder binary under test.')
+ parser.add_argument(
+ '--corpus',
+ metavar='CORPUS',
+ required=True,
+ help=('path to the corpus directory or corpus descriptor'
+ ' text file.'))
+ args = parser.parse_args()
+ if not ConformanceTestRunner(args):
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/media/libjxl/src/tools/conformance/djxl_conformance.cc b/media/libjxl/src/tools/conformance/djxl_conformance.cc
new file mode 100644
index 0000000000..77d0c68722
--- /dev/null
+++ b/media/libjxl/src/tools/conformance/djxl_conformance.cc
@@ -0,0 +1,669 @@
+
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <algorithm>
+#include <fstream>
+#include <iostream>
+#include <iterator>
+#include <memory>
+#include <sstream>
+#include <vector>
+
+#include "jxl/decode.h"
+#include "jxl/decode_cxx.h"
+#include "jxl/thread_parallel_runner.h"
+#include "jxl/thread_parallel_runner_cxx.h"
+
+namespace {
+
+struct DecodeOptions {
+ // Path to the input .jxl file.
+ const char* input = nullptr;
+
+ // Prefix of the output path where to generate the pixel data or nullptr if
+ // no pixel data should be save to disk.
+ const char* pixel_prefix = nullptr;
+
+ // Path to the original ICC profile to be generated, if requested.
+ const char* icc_path = nullptr;
+
+ // Path to JPEG reconstruction file to be generated, if requested.
+ const char* jpeg_path = nullptr;
+
+ // Path to the metadata text file to be generated, if requested.
+ const char* metadata_path = nullptr;
+};
+
+bool LoadFile(const char* filename, std::vector<uint8_t>* data) {
+ std::ifstream ifs(filename, std::ios::binary);
+ std::vector<uint8_t> contents((std::istreambuf_iterator<char>(ifs)),
+ std::istreambuf_iterator<char>());
+ ifs.close();
+ *data = std::move(contents);
+ return ifs.good();
+}
+
+bool SaveFile(const char* filename, std::vector<uint8_t> data) {
+ std::ofstream ofs(filename, std::ios::binary);
+ ofs.write(reinterpret_cast<const char*>(data.data()), data.size());
+ ofs.close();
+ return ofs.good();
+}
+
+struct ImageArray {
+ uint32_t xsize, ysize;
+ // amount of color channels: 1 for grayscale, 3 for RGB
+ uint32_t num_color_channels;
+ // amount of extra channels, including alpha channels, spot colors, ...
+ uint32_t num_extra_channels;
+
+ // Both frames and ec_frames are filled in by the JXL decoder, and will be
+ // converted into a numpy array of the form (frame, ysize, xsize, channel)
+
+ // Array of the color channels of the frames. This is an array of frames,
+ // where each frame is an array of pixels. The pixels in a frame are laid
+ // out per scanline, then per channel, and finally individual pixels as
+ // little endian 32-bit floating point.
+ std::vector<std::vector<uint8_t>> frames;
+
+ // Array of the extra channels of the frames. This is an array of frames,
+ // where each frame is an array of extra channels. The pixels in an extra
+ // channel are laid out per scanline, then individual pixels as
+ // little endian 32-bit floating point.
+ std::vector<std::vector<std::vector<uint8_t>>> ec_frames;
+};
+
+// Saves an ImageArray as a numpy 4D ndarray in binary format.
+bool SaveNPYArray(const char* filename, const ImageArray& arr) {
+ size_t image_size =
+ sizeof(float) * arr.xsize * arr.ysize * arr.num_color_channels;
+ size_t ec_size = sizeof(float) * arr.xsize * arr.ysize;
+ for (const auto& frame : arr.frames) {
+ if (frame.size() != image_size) {
+ fprintf(stderr, "Invalid frame size\n");
+ return false;
+ }
+ }
+ for (const auto& frame : arr.ec_frames) {
+ if (frame.size() != arr.num_extra_channels) {
+ fprintf(stderr, "Invalid extra channel count\n");
+ return false;
+ }
+ for (const auto& ch : frame) {
+ if (ch.size() != ec_size) {
+ fprintf(stderr, "Invalid extra channel size\n");
+ return false;
+ }
+ }
+ }
+
+ FILE* file = fopen(filename, "wb");
+ if (!file) {
+ fprintf(stderr, "Could not open %s for writing", filename);
+ return false;
+ }
+#define WRITE_TO_FILE(ptr, len) \
+ do { \
+ if (fwrite((ptr), (len), 1, file) != 1) { \
+ fprintf(stderr, "Error writing " #ptr " to file %s\n", filename); \
+ fclose(file); \
+ return false; \
+ } \
+ } while (0)
+
+ const uint8_t header[] = "\x93NUMPY\x01\x00";
+ WRITE_TO_FILE(header, 8);
+
+ {
+ uint32_t num_channels = arr.num_color_channels + arr.num_extra_channels;
+ std::stringstream ss;
+ ss << "{'descr': '<f4', 'fortran_order': False, 'shape': ("
+ << arr.frames.size() << ", " << arr.ysize << ", " << arr.xsize << ", "
+ << num_channels << "), }\n";
+ // 16-bit little endian header length.
+ uint8_t header_len[2] = {static_cast<uint8_t>(ss.str().size() % 256),
+ static_cast<uint8_t>(ss.str().size() / 256)};
+ WRITE_TO_FILE(header_len, 2);
+ WRITE_TO_FILE(ss.str().data(), ss.str().size());
+ }
+
+ // interleave the samples from color and extra channels
+ for (size_t f = 0; f < arr.frames.size(); ++f) {
+ size_t pos = 0;
+ for (size_t y = 0; y < arr.ysize; ++y) {
+ for (size_t x = 0; x < arr.xsize; ++x, pos += sizeof(float)) {
+ WRITE_TO_FILE(arr.frames[f].data() + pos * arr.num_color_channels,
+ arr.num_color_channels * sizeof(float));
+ for (size_t i = 0; i < arr.num_extra_channels; i++) {
+ WRITE_TO_FILE(arr.ec_frames[f][i].data() + pos, sizeof(float));
+ }
+ }
+ }
+ }
+
+ return fclose(file) == 0;
+#undef WRITE_TO_FILE
+}
+
+// JSON value writing
+
+class JSONField {
+ public:
+ virtual ~JSONField() = default;
+ virtual void Write(std::ostream& o, uint32_t indent) const = 0;
+
+ protected:
+ JSONField() = default;
+};
+
+class JSONValue : public JSONField {
+ public:
+ template <typename T>
+ explicit JSONValue(const T& value) : value_(std::to_string(value)) {}
+
+ explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {}
+
+ explicit JSONValue(bool value) : value_(value ? "true" : "false") {}
+
+ void Write(std::ostream& o, uint32_t indent) const override { o << value_; }
+
+ private:
+ std::string value_;
+};
+
+class JSONDict : public JSONField {
+ public:
+ JSONDict() = default;
+
+ template <typename T>
+ T* AddEmpty(const std::string& key) {
+ static_assert(std::is_convertible<T*, JSONField*>::value,
+ "T must be a JSONField");
+ T* ret = new T();
+ values_.emplace_back(
+ key, std::unique_ptr<JSONField>(static_cast<JSONField*>(ret)));
+ return ret;
+ }
+
+ template <typename T>
+ void Add(const std::string& key, const T& value) {
+ values_.emplace_back(key, std::unique_ptr<JSONField>(new JSONValue(value)));
+ }
+
+ void Write(std::ostream& o, uint32_t indent) const override {
+ std::string indent_str(indent, ' ');
+ o << "{";
+ bool is_first = true;
+ for (const auto& key_value : values_) {
+ if (!is_first) {
+ o << ",";
+ }
+ is_first = false;
+ o << std::endl << indent_str << " \"" << key_value.first << "\": ";
+ key_value.second->Write(o, indent + 2);
+ }
+ if (!values_.empty()) {
+ o << std::endl << indent_str;
+ }
+ o << "}";
+ }
+
+ private:
+ // Dictionary with order.
+ std::vector<std::pair<std::string, std::unique_ptr<JSONField>>> values_;
+};
+
+class JSONArray : public JSONField {
+ public:
+ JSONArray() = default;
+
+ template <typename T>
+ T* AddEmpty() {
+ static_assert(std::is_convertible<T*, JSONField*>::value,
+ "T must be a JSONField");
+ T* ret = new T();
+ values_.emplace_back(ret);
+ return ret;
+ }
+
+ template <typename T>
+ void Add(const T& value) {
+ values_.emplace_back(new JSONValue(value));
+ }
+
+ void Write(std::ostream& o, uint32_t indent) const override {
+ std::string indent_str(indent, ' ');
+ o << "[";
+ bool is_first = true;
+ for (const auto& value : values_) {
+ if (!is_first) {
+ o << ",";
+ }
+ is_first = false;
+ o << std::endl << indent_str << " ";
+ value->Write(o, indent + 2);
+ }
+ if (!values_.empty()) {
+ o << std::endl << indent_str;
+ }
+ o << "]";
+ }
+
+ private:
+ std::vector<std::unique_ptr<JSONField>> values_;
+};
+
+#define EXPECT_TRUE(X) \
+ do { \
+ if (!(X)) { \
+ fprintf(stderr, "Failed: %s\n", #X); \
+ return false; \
+ } \
+ } while (false)
+
+// Helper macro for decoder error checking.
+#define EXPECT_SUCCESS(X) EXPECT_TRUE((X) == JXL_DEC_SUCCESS)
+
+// TODO(veluca): merge this back in DecodeJXL once/if the API supports decoding
+// to JPEG and to pixels at the same time.
+bool DecodeJXLToJpeg(const char* input_path, const char* output_path) {
+ // JPEG output buffer when reconstructing a JPEG file.
+ std::vector<uint8_t> jpeg_data;
+ std::vector<uint8_t> jpeg_data_chunk(16 * 1024);
+ auto dec = JxlDecoderMake(nullptr);
+
+ uint32_t events = JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE;
+ EXPECT_SUCCESS(JxlDecoderSubscribeEvents(dec.get(), events));
+
+ // TODO(deymo): Consider using a multi-threading decoder for conformance
+ // testing as well.
+
+ // Load and set input all at oncee.
+ std::vector<uint8_t> jxl_input;
+ EXPECT_TRUE(LoadFile(input_path, &jxl_input));
+ EXPECT_SUCCESS(
+ JxlDecoderSetInput(dec.get(), jxl_input.data(), jxl_input.size()));
+
+ bool has_jpeg_reconstruction = false;
+
+ while (true) {
+ JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
+ if (status == JXL_DEC_ERROR) {
+ fprintf(stderr, "Error decoding.\n");
+ return false;
+ } else if (status == JXL_DEC_NEED_MORE_INPUT) {
+ fprintf(stderr, "Error decoding: expected more input.\n");
+ return false;
+ } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) {
+ has_jpeg_reconstruction = true;
+ // Decoding to JPEG.
+ EXPECT_SUCCESS(JxlDecoderSetJPEGBuffer(dec.get(), jpeg_data_chunk.data(),
+ jpeg_data_chunk.size()));
+ } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) {
+ // Decoded a chunk to JPEG.
+ size_t used_jpeg_output =
+ jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get());
+ jpeg_data.insert(jpeg_data.end(), jpeg_data_chunk.data(),
+ jpeg_data_chunk.data() + used_jpeg_output);
+ if (used_jpeg_output == 0) {
+ // Chunk is too small.
+ jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2);
+ }
+ EXPECT_SUCCESS(JxlDecoderSetJPEGBuffer(dec.get(), jpeg_data_chunk.data(),
+ jpeg_data_chunk.size()));
+ } else if (status == JXL_DEC_SUCCESS) {
+ break;
+ } else if (status == JXL_DEC_FULL_IMAGE) {
+ } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ return true;
+ } else {
+ fprintf(stderr, "Error: unexpected status: %d\n",
+ static_cast<int>(status));
+ return false;
+ }
+ }
+
+ if (has_jpeg_reconstruction) {
+ size_t used_jpeg_output =
+ jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get());
+ jpeg_data.insert(jpeg_data.end(), jpeg_data_chunk.data(),
+ jpeg_data_chunk.data() + used_jpeg_output);
+ EXPECT_TRUE(SaveFile(output_path, jpeg_data));
+ }
+ return true;
+}
+
+bool DecodeJXL(const DecodeOptions& opts) {
+ auto dec = JxlDecoderMake(nullptr);
+
+ uint32_t events =
+ JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_PREVIEW_IMAGE;
+ if (opts.pixel_prefix) events |= JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE;
+ // We need to output the frame header info in the metadata.
+ if (opts.metadata_path) events |= JXL_DEC_FRAME;
+
+ if (opts.jpeg_path) {
+ EXPECT_TRUE(DecodeJXLToJpeg(opts.input, opts.jpeg_path));
+ }
+
+ EXPECT_SUCCESS(JxlDecoderSubscribeEvents(dec.get(), events));
+ EXPECT_SUCCESS(JxlDecoderSetRenderSpotcolors(dec.get(), JXL_FALSE));
+
+ // TODO(deymo): Consider using a multi-threading decoder for conformance
+ // testing as well.
+
+ // Load and set input all at oncee.
+ std::vector<uint8_t> jxl_input;
+ EXPECT_TRUE(LoadFile(opts.input, &jxl_input));
+ EXPECT_SUCCESS(
+ JxlDecoderSetInput(dec.get(), jxl_input.data(), jxl_input.size()));
+
+ JxlBasicInfo info{};
+
+ // Pixel data when decoding a frame or a preview frame.
+ std::vector<uint8_t> pixels;
+ std::vector<uint8_t> preview_pixels;
+
+ std::vector<JxlExtraChannelInfo> extra_channels;
+ std::vector<std::vector<uint8_t>> extra_channel_pixels;
+
+ std::vector<JxlFrameHeader> frame_headers;
+ std::vector<std::string> frame_names;
+
+ JxlPixelFormat format;
+
+ ImageArray image, preview;
+
+ while (true) {
+ JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
+ if (status == JXL_DEC_ERROR) {
+ fprintf(stderr, "Error decoding.\n");
+ return false;
+ } else if (status == JXL_DEC_NEED_MORE_INPUT) {
+ fprintf(stderr, "Error decoding: expected more input.\n");
+ return false;
+ } else if (status == JXL_DEC_BASIC_INFO) {
+ // Basic info.
+ EXPECT_SUCCESS(JxlDecoderGetBasicInfo(dec.get(), &info));
+ extra_channels.resize(info.num_extra_channels);
+ for (uint32_t i = 0; i < info.num_extra_channels; ++i) {
+ EXPECT_SUCCESS(
+ JxlDecoderGetExtraChannelInfo(dec.get(), i, &extra_channels[i]));
+ std::vector<char> name(extra_channels[i].name_length + 1);
+ EXPECT_SUCCESS(JxlDecoderGetExtraChannelName(dec.get(), i, name.data(),
+ name.size()));
+ }
+
+ // Select the output pixel format based on the basic info.
+ format = JxlPixelFormat{info.num_color_channels, JXL_TYPE_FLOAT,
+ JXL_LITTLE_ENDIAN, 0};
+ image.num_color_channels = info.num_color_channels;
+ image.num_extra_channels = info.num_extra_channels;
+ image.xsize = info.xsize;
+ image.ysize = info.ysize;
+
+ if (info.have_preview) {
+ preview.num_color_channels = info.num_color_channels;
+ preview.num_extra_channels = info.num_extra_channels;
+ preview.xsize = info.preview.xsize;
+ preview.ysize = info.preview.ysize;
+ }
+ } else if (status == JXL_DEC_COLOR_ENCODING) {
+ // ICC profiles.
+ if (opts.icc_path) {
+ // Store the original ICC if requested.
+ size_t icc_size;
+ EXPECT_SUCCESS(JxlDecoderGetICCProfileSize(
+ dec.get(), nullptr, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &icc_size));
+ std::vector<uint8_t> icc_original(icc_size);
+ EXPECT_SUCCESS(JxlDecoderGetColorAsICCProfile(
+ dec.get(), nullptr, JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ icc_original.data(), icc_original.size()));
+ EXPECT_TRUE(SaveFile(opts.icc_path, icc_original));
+ }
+
+ if (opts.pixel_prefix) {
+ // Get the ICC color profile of the pixel data and store it.
+ size_t icc_size;
+ EXPECT_SUCCESS(JxlDecoderGetICCProfileSize(
+ dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size));
+ std::vector<uint8_t> icc_data(icc_size);
+ EXPECT_SUCCESS(JxlDecoderGetColorAsICCProfile(
+ dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, icc_data.data(),
+ icc_data.size()));
+ std::string icc_data_filename = std::string(opts.pixel_prefix) + ".icc";
+ EXPECT_TRUE(SaveFile(icc_data_filename.c_str(), icc_data));
+ }
+ } else if (status == JXL_DEC_FRAME) {
+ // Capture the frame header information.
+ JxlFrameHeader frame_header;
+ EXPECT_SUCCESS(JxlDecoderGetFrameHeader(dec.get(), &frame_header));
+ std::vector<char> frame_name(frame_header.name_length + 1);
+ EXPECT_SUCCESS(JxlDecoderGetFrameName(dec.get(), frame_name.data(),
+ frame_name.size()));
+ EXPECT_TRUE(frame_name[frame_name.size() - 1] == '\0');
+ frame_headers.emplace_back(frame_header);
+ frame_names.emplace_back(frame_name.begin(), frame_name.end() - 1);
+ } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ // Set pixel output buffer.
+ size_t buffer_size;
+ EXPECT_SUCCESS(
+ JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size));
+ pixels.resize(buffer_size);
+ memset(pixels.data(), 0, pixels.size());
+ EXPECT_SUCCESS(JxlDecoderSetImageOutBuffer(dec.get(), &format,
+ pixels.data(), pixels.size()));
+ extra_channel_pixels.resize(info.num_extra_channels);
+ for (uint32_t i = 0; i < info.num_extra_channels; ++i) {
+ EXPECT_SUCCESS(JxlDecoderExtraChannelBufferSize(dec.get(), &format,
+ &buffer_size, i));
+ extra_channel_pixels[i].resize(buffer_size);
+ memset(extra_channel_pixels[i].data(), 0,
+ extra_channel_pixels[i].size());
+ EXPECT_SUCCESS(JxlDecoderSetExtraChannelBuffer(
+ dec.get(), &format, extra_channel_pixels[i].data(),
+ extra_channel_pixels[i].size(), i));
+ }
+ } else if (status == JXL_DEC_NEED_PREVIEW_OUT_BUFFER) {
+ // Set preview pixel output buffer.
+ size_t buffer_size;
+ EXPECT_SUCCESS(
+ JxlDecoderPreviewOutBufferSize(dec.get(), &format, &buffer_size));
+ preview_pixels.resize(buffer_size);
+ memset(preview_pixels.data(), 0, preview_pixels.size());
+ EXPECT_SUCCESS(JxlDecoderSetPreviewOutBuffer(
+ dec.get(), &format, preview_pixels.data(), preview_pixels.size()));
+ } else if (status == JXL_DEC_FULL_IMAGE) {
+ // Pixel output buffer is set.
+ if (opts.pixel_prefix) {
+ image.frames.emplace_back();
+ swap(image.frames.back(), pixels);
+ image.ec_frames.emplace_back();
+ for (uint32_t i = 0; i < info.num_extra_channels; ++i) {
+ image.ec_frames.back().emplace_back();
+ swap(image.ec_frames.back().back(), extra_channel_pixels[i]);
+ }
+ }
+
+ // TODO(deymo): Get the extra channel pixel data an store it.
+ } else if (status == JXL_DEC_PREVIEW_IMAGE) {
+ // Preview pixel output buffer is set.
+ if (opts.pixel_prefix && info.have_preview) {
+ preview.frames.emplace_back();
+ swap(preview.frames.back(), preview_pixels);
+ }
+ } else if (status == JXL_DEC_SUCCESS) {
+ break;
+ } else {
+ fprintf(stderr, "Error: unexpected status: %d\n",
+ static_cast<int>(status));
+ return false;
+ }
+ }
+
+ if (opts.pixel_prefix) {
+ std::string name = std::string(opts.pixel_prefix) + "_image.npy";
+ EXPECT_TRUE(SaveNPYArray(name.c_str(), image));
+ }
+
+ if (opts.pixel_prefix && info.have_preview) {
+ std::string name = std::string(opts.pixel_prefix) + "_preview.npy";
+ EXPECT_TRUE(SaveNPYArray(name.c_str(), preview));
+ }
+
+ if (opts.metadata_path) {
+ JSONDict meta;
+ // Same order as in 18181-3 CD.
+
+ // Frames.
+ auto* meta_frames = meta.AddEmpty<JSONArray>("frames");
+ for (size_t i = 0; i < frame_headers.size(); i++) {
+ auto* frame_i = meta_frames->AddEmpty<JSONDict>();
+ if (info.have_animation) {
+ frame_i->Add("duration", JSONValue(frame_headers[i].duration * 1.0f *
+ info.animation.tps_denominator /
+ info.animation.tps_numerator));
+ }
+
+ frame_i->Add("name", JSONValue(frame_names[i]));
+
+ if (info.animation.have_timecodes) {
+ frame_i->Add("timecode", JSONValue(frame_headers[i].timecode));
+ }
+ }
+
+#define METADATA(FIELD) meta.Add(#FIELD, info.FIELD)
+
+ METADATA(intensity_target);
+ METADATA(min_nits);
+ METADATA(relative_to_max_display);
+ METADATA(linear_below);
+
+ if (info.have_preview) {
+ meta.AddEmpty<JSONDict>("preview");
+ // TODO(veluca): can we have duration/name/timecode here?
+ }
+
+ {
+ auto ectype = meta.AddEmpty<JSONArray>("extra_channel_type");
+ auto bps = meta.AddEmpty<JSONArray>("bits_per_sample");
+ auto ebps = meta.AddEmpty<JSONArray>("exp_bits_per_sample");
+ bps->Add(info.bits_per_sample);
+ ebps->Add(info.exponent_bits_per_sample);
+ for (size_t i = 0; i < extra_channels.size(); i++) {
+ switch (extra_channels[i].type) {
+ case JXL_CHANNEL_ALPHA: {
+ ectype->Add(std::string("Alpha"));
+ break;
+ }
+ case JXL_CHANNEL_DEPTH: {
+ ectype->Add(std::string("Depth"));
+ break;
+ }
+ case JXL_CHANNEL_SPOT_COLOR: {
+ ectype->Add(std::string("SpotColor"));
+ break;
+ }
+ case JXL_CHANNEL_SELECTION_MASK: {
+ ectype->Add(std::string("SelectionMask"));
+ break;
+ }
+ case JXL_CHANNEL_BLACK: {
+ ectype->Add(std::string("Black"));
+ break;
+ }
+ case JXL_CHANNEL_CFA: {
+ ectype->Add(std::string("CFA"));
+ break;
+ }
+ case JXL_CHANNEL_THERMAL: {
+ ectype->Add(std::string("Thermal"));
+ break;
+ }
+ default: {
+ ectype->Add(std::string("UNKNOWN"));
+ break;
+ }
+ }
+ bps->Add(extra_channels[i].bits_per_sample);
+ ebps->Add(extra_channels[i].exponent_bits_per_sample);
+ }
+ }
+
+ std::ofstream ofs(opts.metadata_path);
+ meta.Write(ofs, 0);
+ ofs << std::endl;
+ ofs.close();
+ EXPECT_TRUE(ofs.good());
+ }
+ return true;
+}
+
+int Usage(const char* program) {
+ fprintf(
+ stderr,
+ "Usage: %s INPUT_JXL [-i ORG_ICC] [-p PREFIX] [-m METADATA]\n"
+ "\n"
+ " INPUT_JXL: Path to the input .jxl file.\n"
+ " -i ORG_ICC: Path to the output \"original\" ICC profile.\n"
+ " -p PREFIX: Prefix path to generate the pixel numpy data image (with\n"
+ " suffix \".npy\") and ICC profile (with suffix \".icc\"). The \n"
+ " image data will be a 4D numpy array with dimensions (number of \n"
+ " frames, height, width, number of channels).\n"
+ " -j JPEG: Path to the output reconstructed JPEG file.\n"
+ " -m METADATA: Path to the output JSON text metadata file.\n",
+ program);
+ return 1;
+}
+
+} // namespace
+
+// Helper macro to check that an extra argument was passed to ARG.
+#define EXPECT_ARG(ARG) \
+ if (optind >= argc) { \
+ fprintf(stderr, "%s needs an argument value.\n", ARG); \
+ return Usage(argv[0]); \
+ }
+
+int main(int argc, char* argv[]) {
+ DecodeOptions opts;
+
+ for (int optind = 1; optind < argc;) {
+ if (!strcmp(argv[optind], "-i")) {
+ optind++;
+ EXPECT_ARG("-i");
+ opts.icc_path = argv[optind++];
+ } else if (!strcmp(argv[optind], "-p")) {
+ optind++;
+ EXPECT_ARG("-p");
+ opts.pixel_prefix = argv[optind++];
+ } else if (!strcmp(argv[optind], "-j")) {
+ optind++;
+ EXPECT_ARG("-j");
+ opts.jpeg_path = argv[optind++];
+ } else if (!strcmp(argv[optind], "-m")) {
+ optind++;
+ EXPECT_ARG("-m");
+ opts.metadata_path = argv[optind++];
+ } else if (opts.input == nullptr) {
+ opts.input = argv[optind++];
+ } else {
+ fprintf(stderr, "Unknown parameter: \"%s\".\n", argv[optind]);
+ return Usage(argv[0]);
+ }
+ }
+ if (!opts.input) {
+ fprintf(stderr, "JXL decoder for conformance testing.\n");
+ return Usage(argv[0]);
+ }
+
+ return DecodeJXL(opts) ? 0 : 1;
+}
diff --git a/media/libjxl/src/tools/conformance/generator.py b/media/libjxl/src/tools/conformance/generator.py
new file mode 100755
index 0000000000..e230d48b3c
--- /dev/null
+++ b/media/libjxl/src/tools/conformance/generator.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+"""Tool for generating a conformance testing corpus from a set of .jxl files.
+
+This is not the JPEG XL conformance test runner. This is a tool to generate a
+conformance testing corpus from a set of .jxl files.
+"""
+
+import argparse
+import itertools
+import json
+import os
+import shutil
+import subprocess
+import sys
+
+
+def GenerateConformanceCorpus(args):
+ """Generate the conformance test corpus for the given arguments."""
+ files = []
+ for jxl in args.inputs:
+ if os.path.isdir(jxl):
+ # Add all the .jxl files recursively.
+ for root, _, dir_files in os.walk(jxl):
+ files.extend(
+ os.path.join(root, filename) for filename in dir_files
+ if filename.lower().endswith('.jxl'))
+ else:
+ files.append(jxl)
+
+ os.makedirs(args.output, 0o755, exist_ok=True)
+
+ test_ids = []
+ for jxl in files:
+ # Generate a unique test_id for this file based on the filename.
+ test_id = os.path.basename(jxl).lower()
+ if test_id.endswith('.jxl'):
+ test_id = test_id[:-4]
+ if test_id in test_ids:
+ for i in itertools.count(2):
+ candidate = test_id + '%02d' % i
+ if candidate not in test_ids:
+ test_id = candidate
+ break
+ test_ids.append(test_id)
+
+ test_dir = os.path.join(args.output, test_id)
+ os.makedirs(test_dir, 0o755, exist_ok=True)
+ print('Generating %s' % (test_id, ))
+ input_file = os.path.join(test_dir, 'input.jxl')
+ shutil.copy(jxl, input_file)
+
+ # The test descriptor file.
+ descriptor = {}
+ descriptor['jxl'] = 'input.jxl'
+
+ cmd = [args.decoder, input_file]
+ original_icc_filename = os.path.join(test_dir, 'original.icc')
+ reconstructed_filename = os.path.join(test_dir, 'reconstructed.jpg')
+ pixel_prefix = os.path.join(test_dir, 'reference')
+ cmd.extend(['-p', pixel_prefix])
+ cmd.extend(['-i', original_icc_filename])
+ cmd.extend(['-j', reconstructed_filename])
+ metadata_filename = os.path.join(test_dir, 'test.json')
+ cmd.extend(['-m', metadata_filename])
+
+ # Decode and generate the reference files.
+ subprocess.check_call(cmd)
+
+ with open(metadata_filename, 'r') as f:
+ metadata = json.load(f)
+
+ if os.path.exists(original_icc_filename):
+ metadata['original_icc'] = "original.icc"
+
+ if os.path.exists(reconstructed_filename):
+ metadata['reconstructed_jpeg'] = "reconstructed.jpg"
+
+ for frame in metadata['frames']:
+ frame['rms_error'] = args.rmse
+ frame['peak_error'] = args.peak_error
+
+ if 'preview' in metadata:
+ metadata['preview']['rms_error'] = args.rmse
+ metadata['preview']['peak_error'] = args.peak_error
+
+ # Create the test descriptor file.
+ with open(metadata_filename, 'w') as f:
+ json.dump(metadata, f, indent=2)
+
+ # Generate a corpus descriptor with the list of the all the test_id names,
+ # one per line.
+ with open(os.path.join(args.output, 'corpus.txt'), 'w') as f:
+ f.write(''.join(line + '\n' for line in test_ids))
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--decoder',
+ metavar='DECODER',
+ required=True,
+ help='path to the decoder binary under test.')
+ parser.add_argument('--output',
+ metavar='DIR',
+ required=True,
+ help='path to the output directory')
+ parser.add_argument('--peak_error',
+ metavar='PEAK_ERROR',
+ type=float,
+ required=True,
+ help='peak error for each testcase')
+ parser.add_argument('--rmse',
+ metavar='RMSE',
+ type=float,
+ required=True,
+ help='max RMSE for each testcase')
+ parser.add_argument('inputs',
+ metavar='JXL',
+ nargs='+',
+ help='path to input .jxl file(s)')
+ args = parser.parse_args()
+ GenerateConformanceCorpus(args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/media/libjxl/src/tools/conformance/lcms2.py b/media/libjxl/src/tools/conformance/lcms2.py
new file mode 100644
index 0000000000..f8313cd6b4
--- /dev/null
+++ b/media/libjxl/src/tools/conformance/lcms2.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+import ctypes
+from numpy.ctypeslib import ndpointer
+import numpy
+import os
+
+lcms2_lib_path = os.getenv("LCMS2_LIB_PATH", "liblcms2.so.2")
+lcms2_lib = ctypes.cdll.LoadLibrary(lcms2_lib_path)
+
+native_open_profile = lcms2_lib.cmsOpenProfileFromMem
+native_open_profile.restype = ctypes.c_void_p
+native_open_profile.argtypes = [
+ ctypes.c_char_p, # MemPtr
+ ctypes.c_size_t # dwSize
+]
+
+native_close_profile = lcms2_lib.cmsCloseProfile
+native_close_profile.restype = ctypes.c_int
+native_close_profile.argtypes = [
+ ctypes.c_void_p # hProfile
+]
+
+native_create_transform = lcms2_lib.cmsCreateTransform
+native_create_transform.restype = ctypes.c_void_p
+native_create_transform.argtypes = [
+ ctypes.c_void_p, # Input
+ ctypes.c_uint32, # InputFormat
+ ctypes.c_void_p, # Output
+ ctypes.c_uint32, # OutputFormat
+ ctypes.c_uint32, # Intent
+ ctypes.c_uint32 # dwFlags
+]
+
+native_delete_transform = lcms2_lib.cmsDeleteTransform
+native_delete_transform.restype = None
+native_delete_transform.argtypes = [
+ ctypes.c_void_p # hTransform
+]
+
+native_do_transform = lcms2_lib.cmsDoTransform
+native_do_transform.restype = None
+native_do_transform.argtypes = [
+ ctypes.c_void_p, # Transform
+ ndpointer(ctypes.c_double, flags="C_CONTIGUOUS"), # InputBuffer
+ ndpointer(ctypes.c_double, flags="C_CONTIGUOUS"), # OutputBuffer
+ ctypes.c_uint32 # Size
+]
+
+
+def make_format(
+ bytes_per_sample=4, # float32
+ num_channels=3, # RGB or XYZ
+ extra_channels=0,
+ swap_channels=0,
+ swap_endiannes=0,
+ planar=0,
+ flavor=0,
+ swap_first=0,
+ unused=0,
+ pixel_type=4, # RGB
+ optimized=0,
+ floating_point=1):
+ values = [bytes_per_sample, num_channels, extra_channels, swap_channels,
+ swap_endiannes, planar, flavor, swap_first, unused, pixel_type,
+ optimized, floating_point]
+ bit_width = [3, 4, 3, 1, 1, 1, 1, 1, 1, 5, 1, 1]
+ result = 0
+ shift = 0
+ for i in range(len(bit_width)):
+ result += values[i] << shift
+ shift += bit_width[i]
+ return result
+
+
+def convert_pixels(from_icc, to_icc, from_pixels):
+ from_icc = bytearray(from_icc)
+ to_icc = bytearray(to_icc)
+
+ if len(from_pixels.shape) != 3 or from_pixels.shape[2] != 3:
+ raise ValueError("Only WxHx3 shapes are supported")
+ from_pixels_plain = from_pixels.ravel().astype(numpy.float64)
+ num_pixels = len(from_pixels_plain) // 3
+ to_pixels_plain = numpy.empty(num_pixels * 3, dtype=numpy.float64)
+
+ from_icc = (ctypes.c_char * len(from_icc)).from_buffer(from_icc)
+ from_profile = native_open_profile(
+ ctypes.cast(ctypes.pointer(from_icc), ctypes.c_char_p), len(from_icc))
+
+ to_icc = (ctypes.c_char * len(to_icc)).from_buffer(to_icc)
+ to_profile = native_open_profile(
+ ctypes.cast(ctypes.pointer(to_icc), ctypes.c_char_p), len(to_icc))
+
+ # bytes_per_sample=0 actually means 8 bytes (but there are just 3 bits to
+ # encode the length of sample)
+ format_rgb_f64 = make_format(bytes_per_sample=0)
+ intent = 0 # INTENT_PERCEPTUAL
+ flags = 0 # default; no "no-optimization"
+ transform = native_create_transform(
+ from_profile, format_rgb_f64, to_profile, format_rgb_f64, intent, flags)
+
+ native_do_transform(
+ transform, from_pixels_plain, to_pixels_plain, num_pixels)
+
+ native_delete_transform(transform)
+ native_close_profile(to_profile)
+ native_close_profile(from_profile)
+
+ # Return same shape and size as input
+ return to_pixels_plain.reshape(from_pixels.shape).astype(from_pixels.dtype)
+
+if __name__ == '__main__':
+ raise Exception("Not an executable")
diff --git a/media/libjxl/src/tools/conformance/tooling_test.sh b/media/libjxl/src/tools/conformance/tooling_test.sh
new file mode 100755
index 0000000000..4366599838
--- /dev/null
+++ b/media/libjxl/src/tools/conformance/tooling_test.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# Conformance test tooling test. This is not the JPEG XL conformance test
+# runner. This test that the tooling to generate the conformance test and the
+# conformance test runner work together.
+
+MYDIR=$(dirname $(realpath "$0"))
+
+if [[ $# -eq 2 ]]; then
+ JPEGXL_TEST_DATA_PATH="$2"
+else
+ JPEGXL_TEST_DATA_PATH="${MYDIR}/../../third_party/testdata"
+fi
+
+set -eux
+
+# Temporary files cleanup hooks.
+CLEANUP_FILES=()
+cleanup() {
+ if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then
+ rm -rf "${CLEANUP_FILES[@]}"
+ fi
+}
+trap 'retcode=$?; { set +x; } 2>/dev/null; cleanup' INT TERM EXIT
+
+main() {
+ local tmpdir=$(mktemp -d)
+ CLEANUP_FILES+=("${tmpdir}")
+
+ if ! python3 -c 'import numpy'; then
+ echo "Missing numpy, skipping test." >&2
+ exit 254 # Signals ctest that we should mark this test as skipped.
+ fi
+
+ local build_dir="${1:-}"
+ if [[ -z "${build_dir}" ]]; then
+ build_dir=$(realpath "${MYDIR}/../../build")
+ fi
+
+ local decoder="${build_dir}/tools/conformance/djxl_conformance"
+ "${MYDIR}/generator.py" \
+ --decoder="${decoder}" \
+ --output="${tmpdir}" \
+ --peak_error=0.001 \
+ --rmse=0.001 \
+ "${JPEGXL_TEST_DATA_PATH}/jxl/blending/cropped_traffic_light.jxl"
+
+ # List the contents of the corpus dir.
+ tree "${tmpdir}" || true
+
+ "${MYDIR}/conformance.py" \
+ --decoder="${decoder}" \
+ --corpus="${tmpdir}"
+}
+
+main "$@"
diff --git a/media/libjxl/src/tools/decode_and_encode.cc b/media/libjxl/src/tools/decode_and_encode.cc
new file mode 100644
index 0000000000..59b1d6d3af
--- /dev/null
+++ b/media/libjxl/src/tools/decode_and_encode.cc
@@ -0,0 +1,50 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+
+#include <string>
+
+#include "lib/extras/codec.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+
+namespace jxl {
+namespace {
+
+// Reads an input file (typically PNM) with color_space hint and writes to an
+// output file (typically PNG) which supports all required metadata.
+int Convert(int argc, char** argv) {
+ if (argc != 4 && argc != 5) {
+ fprintf(stderr, "Args: in colorspace_description out [bits]\n");
+ return 1;
+ }
+ const std::string& pathname_in = argv[1];
+ const std::string& desc = argv[2];
+ const std::string& pathname_out = argv[3];
+
+ CodecInOut io;
+ extras::ColorHints color_hints;
+ ThreadPoolInternal pool(4);
+ color_hints.Add("color_space", desc);
+ if (!SetFromFile(pathname_in, color_hints, &io, &pool)) {
+ fprintf(stderr, "Failed to read %s\n", pathname_in.c_str());
+ return 1;
+ }
+
+ if (!EncodeToFile(io, pathname_out, &pool)) {
+ fprintf(stderr, "Failed to write %s\n", pathname_out.c_str());
+ return 1;
+ }
+
+ return 0;
+}
+
+} // namespace
+} // namespace jxl
+
+int main(int argc, char** argv) { return jxl::Convert(argc, argv); }
diff --git a/media/libjxl/src/tools/decode_basic_info_fuzzer.cc b/media/libjxl/src/tools/decode_basic_info_fuzzer.cc
new file mode 100644
index 0000000000..59f7089f65
--- /dev/null
+++ b/media/libjxl/src/tools/decode_basic_info_fuzzer.cc
@@ -0,0 +1,58 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+
+#include "jxl/decode.h"
+
+namespace jxl {
+
+int TestOneInput(const uint8_t* data, size_t size) {
+ JxlDecoderStatus status;
+ JxlDecoder* dec = JxlDecoderCreate(nullptr);
+ JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING);
+ JxlDecoderSetInput(dec, data, size);
+
+ status = JxlDecoderProcessInput(dec);
+
+ if (status != JXL_DEC_BASIC_INFO) {
+ JxlDecoderDestroy(dec);
+ return 0;
+ }
+
+ JxlBasicInfo info;
+ bool have_basic_info = !JxlDecoderGetBasicInfo(dec, &info);
+
+ if (have_basic_info) {
+ if (info.alpha_bits != 0) {
+ for (int i = 0; i < info.num_extra_channels; ++i) {
+ JxlExtraChannelInfo extra;
+ JxlDecoderGetExtraChannelInfo(dec, 0, &extra);
+ }
+ }
+ }
+ status = JxlDecoderProcessInput(dec);
+
+ if (status != JXL_DEC_COLOR_ENCODING) {
+ JxlDecoderDestroy(dec);
+ return 0;
+ }
+
+ JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0};
+ JxlDecoderGetColorAsEncodedProfile(
+ dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, nullptr);
+ size_t dec_profile_size;
+ JxlDecoderGetICCProfileSize(dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ &dec_profile_size);
+
+ JxlDecoderDestroy(dec);
+ return 0;
+}
+
+} // namespace jxl
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return jxl::TestOneInput(data, size);
+}
diff --git a/media/libjxl/src/tools/demo_progressive_saliency_encoding.py b/media/libjxl/src/tools/demo_progressive_saliency_encoding.py
new file mode 100755
index 0000000000..6eb5cadd54
--- /dev/null
+++ b/media/libjxl/src/tools/demo_progressive_saliency_encoding.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"""Produces demos for how progressive-saliency encoding would look like.
+
+As long as we do not have a progressive decoder that allows showing images
+generated from partially-available data, we can resort to building
+animated gifs that show how progressive loading would look like.
+
+Method:
+
+1. JPEG-XL encode the image, but stop at the pre-final (2nd) step.
+2. Use separate tool to compute a heatmap which shows where differences between
+ the pre-final and final image are expected to be perceptually worst.
+3. Use this heatmap to JPEG-XL encode the image with the final step split into
+ 'salient parts only' and 'non-salient parts'. Generate a sequence of images
+ that stop decoding after the 1st, 2nd, 3rd, 4th step. JPEG-XL decode these
+ truncated images back to PNG.
+4. Measure byte sizes of the truncated-encoded images.
+5. Build an animated GIF with variable delays by calling ImageMagick's
+ `convert` command.
+
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from six.moves import zip
+import ast # For ast.literal_eval() only.
+import os
+import re
+import shlex
+import subprocess
+import sys
+
+_BLOCKSIZE = 8
+
+_CONF_PARSERS = dict(
+ keep_tempfiles=lambda s: bool(ast.literal_eval(s)),
+ heatmap_command=shlex.split,
+ simulated_progressive_loading_time_sec=float,
+ simulated_progressive_loading_delay_until_looparound_sec=float,
+ jpegxl_encoder=shlex.split,
+ jpegxl_decoder=shlex.split,
+ blurring=lambda s: s.split(),
+)
+
+
+def parse_config(config_filename):
+ """Parses the configuration file."""
+ conf = {}
+ re_comment = re.compile(r'^\s*(?:#.*)?$')
+ re_param = re.compile(r'^(?P<option>\w+)\s*:\s*(?P<value>.*?)\s*$')
+ try:
+ with open(config_filename) as h:
+ for line in h:
+ if re_comment.match(line):
+ continue
+ m = re_param.match(line)
+ if not m:
+ raise ValueError('Syntax error')
+ conf[m.group('option')] = (
+ _CONF_PARSERS[m.group('option')](m.group('value')))
+ except Exception as exn:
+ raise ValueError('Bad Configuration line ({}): {}'.format(exn, line))
+ missing_options = set(_CONF_PARSERS) - set(conf)
+ if missing_options:
+ raise ValueError('Missing configuration options: ' + ', '.join(
+ sorted(missing_options)))
+ return conf
+
+
+def generate_demo_image(config, input_filename, output_filename):
+ tempfiles = []
+ #
+ def encode_img(input_filename, output_filename, num_steps,
+ heatmap_filename=None):
+ replacements = {
+ '${INPUT}': input_filename,
+ '${OUTPUT}': output_filename,
+ '${STEPS}': str(num_steps),
+ # Heatmap argument will be provided in --param=value form.
+ '${HEATMAP_ARG}': ('--saliency_map_filename=' + heatmap_filename
+ if heatmap_filename is not None else '')
+ }
+ # Remove empty args. This removes the heatmap-argument if no heatmap
+ # is provided..
+ cmd = [
+ _f for _f in
+ [replacements.get(arg, arg) for arg in config['jpegxl_encoder']] if _f
+ ]
+ tempfiles.append(output_filename)
+ subprocess.call(cmd)
+ #
+ def decode_img(input_filename, output_filename):
+ replacements = {'${INPUT}': input_filename, '${OUTPUT}': output_filename}
+ cmd = [replacements.get(arg, arg) for arg in config['jpegxl_decoder']]
+ tempfiles.append(output_filename)
+ subprocess.call(cmd)
+ #
+ def generate_heatmap(orig_image_filename, coarse_grained_filename,
+ heatmap_filename):
+ cmd = config['heatmap_command'] + [
+ str(_BLOCKSIZE), orig_image_filename, coarse_grained_filename,
+ heatmap_filename]
+ tempfiles.append(heatmap_filename)
+ subprocess.call(cmd)
+ #
+ try:
+ encode_img(input_filename, output_filename + '._step1.pik', 1)
+ decode_img(output_filename + '._step1.pik', output_filename + '._step1.png')
+ encode_img(input_filename, output_filename + '._step2.pik', 2)
+ decode_img(output_filename + '._step2.pik', output_filename + '._step2.png')
+ generate_heatmap(input_filename, output_filename + '._step2.png',
+ output_filename + '._heatmap.png')
+ encode_img(input_filename,
+ output_filename + '._step3.pik', 3,
+ output_filename + '._heatmap.png')
+ encode_img(input_filename,
+ output_filename + '._step4.pik', 4,
+ output_filename + '._heatmap.png')
+ decode_img(output_filename + '._step3.pik', output_filename + '._step3.png')
+ decode_img(output_filename + '._step4.pik', output_filename + '._step4.png')
+ data_sizes = [
+ os.stat('{}._step{}.pik'.format(output_filename, num_step)).st_size
+ for num_step in (1, 2, 3, 4)]
+ time_offsets = [0] + [
+ # Imagemagick's `convert` accepts delays in units of 1/100 sec.
+ round(100 * config['simulated_progressive_loading_time_sec'] * size /
+ data_sizes[-1]) for size in data_sizes]
+ time_delays = [t_next - t_prev
+ for t_next, t_prev in zip(time_offsets[1:], time_offsets)]
+ # Add a fake white initial image. As long as no usable image data is
+ # available, the user will see a white background.
+ subprocess.call(['convert',
+ output_filename + '._step1.png',
+ '-fill', 'white', '-colorize', '100%',
+ output_filename + '._step0.png'])
+ tempfiles.append(output_filename + '._step0.png')
+ subprocess.call(
+ ['convert', '-loop', '0', output_filename + '._step0.png'] +
+ [arg for args in [
+ ['-delay', str(time_delays[n - 1]),
+ '-blur', config['blurring'][n - 1],
+ '{}._step{}.png'.format(output_filename, n)]
+ for n in (1, 2, 3, 4)] for arg in args] +
+ ['-delay', str(round(100 * config[
+ 'simulated_progressive_loading_delay_until_looparound_sec'])),
+ output_filename + '._step4.png',
+ output_filename])
+ finally:
+ if not config['keep_tempfiles']:
+ for filename in tempfiles:
+ try:
+ os.unlink(filename)
+ except OSError:
+ pass # May already have been deleted otherwise.
+
+
+def main():
+ if sys.version.startswith('2.'):
+ sys.exit('This is a python3-only script.')
+ if (len(sys.argv) != 4 or not sys.argv[-1].endswith('.gif')
+ or not sys.argv[-2].endswith('.png')):
+ sys.exit(
+ 'Usage: {} [config_options_file] [input.png] [output.gif]'.format(
+ sys.argv[0]))
+ try:
+ _, config_filename, input_filename, output_filename = sys.argv
+ config = parse_config(config_filename)
+ generate_demo_image(config, input_filename, output_filename)
+ except ValueError as exn:
+ sys.exit(exn)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/media/libjxl/src/tools/demo_vardct_select.sh b/media/libjxl/src/tools/demo_vardct_select.sh
new file mode 100755
index 0000000000..414eacbbd2
--- /dev/null
+++ b/media/libjxl/src/tools/demo_vardct_select.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# Produces a demo video showing VarDCT block type selection
+# from very high quality to very low quality.
+
+# Assumes ImageMagick convert, ffmpeg, bc are available.
+
+set -eu
+
+MYDIR=$(dirname $(realpath "$0"))
+
+CLEANUP_FILES=()
+cleanup() {
+ if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then
+ rm -fr "${CLEANUP_FILES[@]}"
+ fi
+}
+trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT
+
+
+
+main() {
+ local infile="${1:-}"
+ if [[ -z "${infile}" ]]; then
+ cat >&2 <<EOF
+Use: $0 IMAGE [OUT.apng]
+
+Where IMAGE is an input image and OUT.apng is the output
+EOF
+ exit 1
+ fi
+
+ shift
+
+ local outfile="$@"
+ if [[ -z "${outfile}" ]]; then
+ # default output filename
+ outfile=vardct-select-demo.apng
+ fi
+
+ if ! command -v benchmark_xl &>/dev/null 2>&1; then
+ PATH=$PATH:$MYDIR/../build/tools
+ if ! command -v benchmark_xl &>/dev/null 2>&1; then
+ echo "Could not find benchmark_xl, try building first"
+ exit
+ fi
+ fi
+ local b=benchmark_xl
+
+ if ! command -v ffmpeg &>/dev/null 2>&1; then
+ echo "Could not find ffmpeg"
+ exit
+ fi
+
+ if ! command -v convert &>/dev/null 2>&1; then
+ echo "Could not find ImageMagick (convert)"
+ exit
+ fi
+
+ local tmp=$(mktemp -d --suffix=vardctdemo)
+ CLEANUP_FILES+=("${tmp}")
+
+ cp $infile $tmp/orig
+
+ local n=0
+ local pixels="$(identify -format "(%w * %h)" $tmp/orig)"
+ for i in $(seq 0.2 0.2 2) $(seq 2.5 0.5 5) $(seq 6 1 10) $(seq 12 2 40); do
+ $b --input=$tmp/orig --codec=jxl:d$i --save_decompressed --save_compressed \
+ --debug_image_dir=$tmp --output_dir=$tmp
+ convert $tmp/orig \( $tmp/orig.jxl:d$i.dbg/ac_strategy.png \
+ -alpha set -channel A -evaluate set 66% \) \
+ -composite $tmp/t.ppm
+ bytes=$(stat -c "%s" $tmp/orig.jxl_d$i)
+ bpp=$( echo "$bytes * 8 / $pixels " | bc -l | cut -b 1-6 )
+ label="cjxl -d $i ($((bytes / 1000)) kb, bpp: $bpp)"
+ convert +append $tmp/t.ppm $tmp/orig.jxl_d$i.png $tmp/t2.ppm
+ convert $tmp/t2.ppm \
+ -gravity north \
+ -pointsize 32 \
+ -stroke '#000C' -strokewidth 5 -annotate +0+12 "$label" \
+ -stroke none -fill white -annotate +0+12 "$label" $tmp/frame-$n.png
+
+ n=$((n+1))
+ done
+
+ ffmpeg -framerate 1 -i $tmp/frame-%d.png $outfile
+}
+
+main "$@"
diff --git a/media/libjxl/src/tools/djxl.cc b/media/libjxl/src/tools/djxl.cc
new file mode 100644
index 0000000000..8487fcdf09
--- /dev/null
+++ b/media/libjxl/src/tools/djxl.cc
@@ -0,0 +1,329 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/djxl.h"
+
+#include <stdio.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/color_description.h"
+#include "lib/extras/time.h"
+#include "lib/extras/tone_mapping.h"
+#include "lib/jxl/alpha.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/dec_file.h"
+#include "lib/jxl/frame_header.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_ops.h"
+#include "lib/jxl/jpeg/dec_jpeg_data_writer.h"
+#include "tools/args.h"
+#include "tools/box/box.h"
+
+namespace jpegxl {
+namespace tools {
+
+static inline bool ParseLuminanceRange(const char* arg,
+ std::pair<float, float>* out) {
+ char* end;
+ out->first = static_cast<float>(strtod(arg, &end));
+ if (*end == '\0') {
+ // That was actually the upper bound.
+ out->second = out->first;
+ out->first = 0;
+ return true;
+ }
+ if (*end != '-') {
+ fprintf(stderr, "Unable to interpret as luminance range: %s.\n", arg);
+ return JXL_FAILURE("Args");
+ }
+ const char* second = end + 1;
+ out->second = static_cast<float>(strtod(second, &end));
+ if (*end != '\0') {
+ fprintf(stderr, "Unable to interpret as luminance range: %s.\n", arg);
+ return JXL_FAILURE("Args");
+ }
+ return true;
+}
+
+void DecompressArgs::AddCommandLineOptions(CommandLineParser* cmdline) {
+ // Positional arguments.
+ cmdline->AddPositionalOption("INPUT", /* required = */ true,
+ "the compressed input file", &file_in);
+
+ cmdline->AddPositionalOption(
+ "OUTPUT", /* required = */ true,
+ "the output can be PNG with ICC, JPG, or PPM/PFM.", &file_out);
+
+ cmdline->AddOptionFlag('V', "version", "print version number and exit",
+ &version, &SetBooleanTrue);
+
+ cmdline->AddOptionValue('\0', "num_reps", "N", nullptr, &num_reps,
+ &ParseUnsigned);
+
+ cmdline->AddOptionValue('\0', "num_threads", "N",
+ "The number of threads to use", &num_threads,
+ &ParseUnsigned);
+
+ cmdline->AddOptionValue('\0', "print_profile", "0|1",
+ "print timing information before exiting",
+ &print_profile, &ParseOverride);
+
+ cmdline->AddOptionValue('\0', "bits_per_sample", "N",
+ "defaults to original (input) bit depth",
+ &bits_per_sample, &ParseUnsigned);
+
+ cmdline->AddOptionFlag(
+ '\0', "tone_map",
+ "tone map the image to the luminance range indicated by --display_nits "
+ "instead of performing a naive 0-1 -> 0-1 conversion",
+ &tone_map, &SetBooleanTrue);
+
+ cmdline->AddOptionValue('\0', "display_nits", "0.3-250",
+ "luminance range of the display to which to "
+ "tone-map; the lower bound can be omitted",
+ &display_nits, &ParseLuminanceRange);
+ cmdline->AddOptionValue(
+ '\0', "preserve_saturation", "0..1",
+ "with --tone_map, how much to favor saturation over luminance",
+ &preserve_saturation, &ParseFloat);
+
+ cmdline->AddOptionValue('\0', "color_space", "RGB_D65_SRG_Rel_Lin",
+ "defaults to original (input) color space",
+ &color_space, &ParseString);
+
+ cmdline->AddOptionValue('s', "downsampling", "1,2,4,8,16",
+ "maximum permissible downsampling factor (values "
+ "greater than 16 will return the LQIP if available)",
+ &params.max_downsampling, &ParseUnsigned);
+
+ cmdline->AddOptionFlag('\0', "allow_partial_files",
+ "allow decoding of truncated files",
+ &params.allow_partial_files, &SetBooleanTrue);
+
+ cmdline->AddOptionFlag('\0', "allow_more_progressive_steps",
+ "allow decoding more progressive steps in truncated "
+ "files. No effect without --allow_partial_files",
+ &params.allow_more_progressive_steps, &SetBooleanTrue);
+
+#if JPEGXL_ENABLE_JPEG
+ cmdline->AddOptionFlag(
+ 'j', "pixels_to_jpeg",
+ "By default, if the input JPEG XL contains a recompressed JPEG file, "
+ "djxl "
+ "reconstructs the exact original JPEG file. This flag causes the decoder "
+ "to instead decode the image to pixels and encode a new (lossy) JPEG. "
+ "The output file if provided must be a .jpg or .jpeg file.",
+ &decode_to_pixels, &SetBooleanTrue);
+
+ opt_jpeg_quality_id =
+ cmdline->AddOptionValue('q', "jpeg_quality", "N",
+ "JPEG output quality. Setting an output quality "
+ "implies --pixels_to_jpeg.",
+ &jpeg_quality, &ParseUnsigned);
+#endif
+
+#if JPEGXL_ENABLE_SJPEG
+ cmdline->AddOptionFlag('\0', "use_sjpeg",
+ "use sjpeg instead of libjpeg for JPEG output",
+ &use_sjpeg, &SetBooleanTrue);
+#endif
+
+ cmdline->AddOptionFlag('\0', "print_read_bytes",
+ "print total number of decoded bytes",
+ &print_read_bytes, &SetBooleanTrue);
+
+ cmdline->AddOptionFlag('\0', "quiet", "silence output (except for errors)",
+ &quiet, &SetBooleanTrue);
+}
+
+jxl::Status DecompressArgs::ValidateArgs(const CommandLineParser& cmdline) {
+ if (file_in == nullptr) {
+ fprintf(stderr, "Missing INPUT filename.\n");
+ return false;
+ }
+
+#if JPEGXL_ENABLE_JPEG
+ if (cmdline.GetOption(opt_jpeg_quality_id)->matched()) {
+ decode_to_pixels = true;
+ }
+#endif
+ if (file_out) {
+ const std::string extension = jxl::Extension(file_out);
+ const jxl::extras::Codec codec =
+ jxl::extras::CodecFromExtension(extension, &bits_per_sample);
+ if (codec != jxl::extras::Codec::kJPG) {
+ // when decoding to anything-but-JPEG, we'll need pixels
+ decode_to_pixels = true;
+ }
+ } else {
+ decode_to_pixels = true;
+ }
+ return true;
+}
+
+jxl::Status DecompressJxlToPixels(const jxl::Span<const uint8_t> compressed,
+ const jxl::DecompressParams& params,
+ jxl::ThreadPool* pool,
+ jxl::CodecInOut* JXL_RESTRICT io,
+ SpeedStats* JXL_RESTRICT stats) {
+ const double t0 = jxl::Now();
+ if (!jxl::DecodeFile(params, compressed, io, pool)) {
+ fprintf(stderr, "Failed to decompress to pixels.\n");
+ return false;
+ }
+ const double t1 = jxl::Now();
+ stats->NotifyElapsed(t1 - t0);
+ stats->SetImageSize(io->xsize(), io->ysize());
+ return true;
+}
+
+jxl::Status DecompressJxlToJPEG(const JpegXlContainer& container,
+ const DecompressArgs& args,
+ jxl::ThreadPool* pool, jxl::PaddedBytes* output,
+ SpeedStats* JXL_RESTRICT stats) {
+ output->clear();
+ const double t0 = jxl::Now();
+
+ jxl::Span<const uint8_t> compressed(container.codestream);
+
+ JXL_RETURN_IF_ERROR(compressed.size() >= 2);
+
+ // JXL case
+ // Decode to DCT when possible and generate a JPG file.
+ jxl::CodecInOut io;
+ // Set JPEG quality.
+ // TODO(deymo): We should probably fail to give a JPEG file if the
+ // original image can't be transcoded to a JPEG file without passing
+ // through pixels, or at least signal this to the user.
+ io.use_sjpeg = args.use_sjpeg;
+ io.jpeg_quality = args.jpeg_quality;
+
+ if (!DecodeJpegXlToJpeg(args.params, container, &io, pool)) {
+ return JXL_FAILURE("Failed to decode JXL to JPEG");
+ }
+ if (!jxl::jpeg::EncodeImageJPGCoefficients(&io, output)) {
+ return JXL_FAILURE("Failed to generate JPEG");
+ }
+ stats->SetImageSize(io.xsize(), io.ysize());
+
+ const double t1 = jxl::Now();
+ stats->NotifyElapsed(t1 - t0);
+ stats->SetFileSize(output->size());
+ return true;
+}
+
+jxl::Status WriteJxlOutput(const DecompressArgs& args, const char* file_out,
+ jxl::CodecInOut& io, jxl::ThreadPool* pool) {
+ // Can only write if we decoded and have an output filename.
+ // (Writing large PNGs is slow, so allow skipping it for benchmarks.)
+ if (file_out == nullptr) return true;
+
+ // Stay in original color space unless something else is needed.
+ jxl::ColorEncoding c_out = io.metadata.m.color_encoding;
+ // Override original color space with sRGB if input is CMYK.
+ if (io.Main().HasBlack()) c_out = jxl::ColorEncoding::SRGB(false);
+ // Override original color space with arg if specified.
+ if (!args.color_space.empty()) {
+ bool color_space_applied = false;
+ JxlColorEncoding c_out_external;
+ if (jxl::ParseDescription(args.color_space, &c_out_external) &&
+ ConvertExternalToInternalColorEncoding(c_out_external, &c_out) &&
+ c_out.CreateICC()) {
+ color_space_applied = true;
+ } else {
+ jxl::PaddedBytes icc;
+ if (jxl::ReadFile(args.color_space, &icc) &&
+ c_out.SetICC(std::move(icc))) {
+ color_space_applied = true;
+ }
+ }
+
+ if (!color_space_applied) {
+ fprintf(stderr, "Failed to apply color_space.\n");
+ return false;
+ }
+ }
+
+ // Override original #bits with arg if specified.
+ size_t bits_per_sample = io.metadata.m.bit_depth.bits_per_sample;
+ if (args.bits_per_sample != 0) bits_per_sample = args.bits_per_sample;
+
+ if (args.tone_map) {
+ jxl::Status status = jxl::ToneMapTo(args.display_nits, &io, pool);
+ if (!status) fprintf(stderr, "Failed to map tones.\n");
+ JXL_RETURN_IF_ERROR(status);
+ status = jxl::GamutMap(&io, args.preserve_saturation, pool);
+ if (!status) fprintf(stderr, "Failed to map gamut.\n");
+ JXL_RETURN_IF_ERROR(status);
+ if (c_out.tf.IsPQ() && args.color_space.empty()) {
+ // Prevent writing the tone-mapped image to PQ output unless explicitly
+ // requested. The result would look even dimmer than it would have without
+ // tone mapping.
+ c_out.tf.SetTransferFunction(jxl::TransferFunction::k709);
+ status = c_out.CreateICC();
+ if (!status) fprintf(stderr, "Failed to create ICC\n");
+ JXL_RETURN_IF_ERROR(c_out.CreateICC());
+ }
+ }
+
+ const char* extension = strrchr(file_out, '.');
+ std::string base = extension == nullptr
+ ? std::string(file_out)
+ : std::string(file_out, extension - file_out);
+ if (extension == nullptr) extension = "";
+ const jxl::extras::Codec codec = jxl::extras::CodecFromExtension(extension);
+ if (!io.metadata.m.have_animation || codec == jxl::extras::Codec::kPNG) {
+ bool ok;
+ if (io.Main().IsJPEG() && codec == jxl::extras::Codec::kJPG) {
+ jxl::PaddedBytes encoded;
+ ok = jxl::jpeg::EncodeImageJPGCoefficients(&io, &encoded) &&
+ jxl::WriteFile(encoded, file_out);
+ } else {
+ ok = jxl::EncodeToFile(io, c_out, bits_per_sample, file_out, pool);
+ }
+ if (!ok) {
+ fprintf(stderr, "Failed to write decoded image.\n");
+ return false;
+ }
+ } else {
+ const int digits = 1 + static_cast<int>(std::log10(std::max(
+ 1, static_cast<int>(io.frames.size() - 1))));
+ std::vector<char> output_filename;
+ output_filename.resize(base.size() + 1 + digits + strlen(extension) + 1);
+
+ for (size_t i = 0; i < io.frames.size(); ++i) {
+ jxl::CodecInOut frame_io;
+ frame_io.SetFromImage(jxl::CopyImage(*io.frames[i].color()),
+ io.frames[i].c_current());
+ frame_io.metadata.m = *io.frames[i].metadata();
+ frame_io.jpeg_quality = io.jpeg_quality;
+ frame_io.use_sjpeg = io.use_sjpeg;
+ if (io.frames[i].HasAlpha()) {
+ frame_io.Main().SetAlpha(
+ jxl::CopyImage(*io.frames[i].alpha()),
+ /*alpha_is_premultiplied=*/io.frames[i].AlphaIsPremultiplied());
+ }
+ snprintf(output_filename.data(), output_filename.size(), "%s-%0*zu%s",
+ base.c_str(), digits, i, extension);
+ if (!EncodeToFile(frame_io, c_out, bits_per_sample,
+ output_filename.data(), pool)) {
+ fprintf(stderr,
+ "Failed to write decoded image for frame %" PRIuS "/%" PRIuS
+ ".\n",
+ i + 1, io.frames.size());
+ }
+ }
+ }
+ return true;
+}
+
+} // namespace tools
+} // namespace jpegxl
diff --git a/media/libjxl/src/tools/djxl.h b/media/libjxl/src/tools/djxl.h
new file mode 100644
index 0000000000..d091ed7a3f
--- /dev/null
+++ b/media/libjxl/src/tools/djxl.h
@@ -0,0 +1,91 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_DJXL_H_
+#define TOOLS_DJXL_H_
+
+#include <stddef.h>
+
+#include <thread>
+
+#include "jxl/decode.h"
+#include "lib/jxl/aux_out.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/dec_params.h"
+#include "tools/args.h"
+#include "tools/box/box.h"
+#include "tools/cmdline.h"
+#include "tools/speed_stats.h"
+
+namespace jpegxl {
+namespace tools {
+
+// Common JPEG XL decompress arguments.
+struct DecompressArgs {
+ // Initialize non-static default options.
+ DecompressArgs() = default;
+
+ // Add all the command line options to the CommandLineParser. Note that the
+ // options are tied to the instance that this was called on.
+ void AddCommandLineOptions(CommandLineParser* cmdline);
+
+ // Validate the passed arguments, checking whether all passed options are
+ // compatible. Returns whether the validation was successful.
+ jxl::Status ValidateArgs(const CommandLineParser& cmdline);
+
+ // Common djxl parameters.
+ const char* file_in = nullptr;
+ const char* file_out = nullptr;
+ size_t num_threads = std::thread::hardware_concurrency();
+ bool use_sjpeg = false;
+ size_t jpeg_quality = 95;
+ bool decode_to_pixels = false;
+ bool version = false;
+ jxl::Override print_profile = jxl::Override::kDefault;
+
+ size_t num_reps = 1;
+
+ // Format parameters:
+
+ size_t bits_per_sample = 0;
+ bool tone_map = false;
+ std::pair<float, float> display_nits = {0.f, jxl::kDefaultIntensityTarget};
+ float preserve_saturation = .1f;
+ std::string color_space; // description or path to ICC profile
+
+ jxl::DecompressParams params;
+
+ // If true, print the effective amount of bytes read from the bitstream.
+ bool print_read_bytes = false;
+ bool quiet = false;
+
+ // References (ids) of specific options to check if they were matched.
+ CommandLineParser::OptionId opt_jpeg_quality_id = -1;
+};
+
+// Decompresses and notifies SpeedStats of elapsed time.
+jxl::Status DecompressJxlToPixels(const jxl::Span<const uint8_t> compressed,
+ const jxl::DecompressParams& params,
+ jxl::ThreadPool* pool,
+ jxl::CodecInOut* JXL_RESTRICT io,
+ SpeedStats* JXL_RESTRICT stats);
+
+jxl::Status DecompressJxlToJPEG(const JpegXlContainer& container,
+ const DecompressArgs& args,
+ jxl::ThreadPool* pool, jxl::PaddedBytes* output,
+ SpeedStats* JXL_RESTRICT stats);
+
+jxl::Status WriteJxlOutput(const DecompressArgs& args, const char* file_out,
+ jxl::CodecInOut& io,
+ jxl::ThreadPool* pool = nullptr);
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_DJXL_H_
diff --git a/media/libjxl/src/tools/djxl_fuzzer.cc b/media/libjxl/src/tools/djxl_fuzzer.cc
new file mode 100644
index 0000000000..a03472a58a
--- /dev/null
+++ b/media/libjxl/src/tools/djxl_fuzzer.cc
@@ -0,0 +1,570 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <algorithm>
+#include <map>
+#include <mutex>
+#include <random>
+#include <vector>
+
+#include "hwy/targets.h"
+#include "jxl/decode.h"
+#include "jxl/decode_cxx.h"
+#include "jxl/thread_parallel_runner.h"
+#include "jxl/thread_parallel_runner_cxx.h"
+
+namespace {
+
+// Externally visible value to ensure pixels are used in the fuzzer.
+int external_code = 0;
+
+constexpr const size_t kStreamingTargetNumberOfChunks = 128;
+
+// Options for the fuzzing
+struct FuzzSpec {
+ JxlDataType output_type;
+ JxlEndianness output_endianness;
+ size_t output_align;
+ bool get_alpha;
+ bool get_grayscale;
+ bool use_streaming;
+ bool jpeg_to_pixels; // decode to pixels even if it is JPEG-reconstructible
+ // Whether to use the callback mechanism for the output image or not.
+ bool use_callback;
+ bool keep_orientation;
+ bool decode_boxes;
+ bool coalescing;
+ // Used for random variation of chunk sizes, extra channels, ... to get
+ uint32_t random_seed;
+};
+
+template <typename It>
+void Consume(const It& begin, const It& end) {
+ for (auto it = begin; it < end; ++it) {
+ if (*it == 0) {
+ external_code ^= ~0;
+ } else {
+ external_code ^= *it;
+ }
+ }
+}
+
+template <typename T>
+void Consume(const T& entry) {
+ const uint8_t* begin = reinterpret_cast<const uint8_t*>(&entry);
+ Consume(begin, begin + sizeof(T));
+}
+
+// use_streaming: if true, decodes the data in small chunks, if false, decodes
+// it in one shot.
+bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels,
+ const FuzzSpec& spec, std::vector<uint8_t>* pixels,
+ std::vector<uint8_t>* jpeg, size_t* xsize, size_t* ysize,
+ std::vector<uint8_t>* icc_profile) {
+ // Multi-threaded parallel runner. Limit to max 2 threads since the fuzzer
+ // itself is already multithreaded.
+ size_t num_threads =
+ std::min<size_t>(2, JxlThreadParallelRunnerDefaultNumWorkerThreads());
+ auto runner = JxlThreadParallelRunnerMake(nullptr, num_threads);
+
+ std::mt19937 mt(spec.random_seed);
+ std::exponential_distribution<> dis_streaming(kStreamingTargetNumberOfChunks);
+
+ auto dec = JxlDecoderMake(nullptr);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSubscribeEvents(
+ dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_EXTENSIONS |
+ JXL_DEC_COLOR_ENCODING | JXL_DEC_PREVIEW_IMAGE |
+ JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE |
+ JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_BOX)) {
+ return false;
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(),
+ JxlThreadParallelRunner,
+ runner.get())) {
+ return false;
+ }
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetKeepOrientation(dec.get(), spec.keep_orientation)) {
+ abort();
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderSetCoalescing(dec.get(), spec.coalescing)) {
+ abort();
+ }
+ JxlBasicInfo info;
+ uint32_t channels = (spec.get_grayscale ? 1 : 3) + (spec.get_alpha ? 1 : 0);
+ JxlPixelFormat format = {channels, spec.output_type, spec.output_endianness,
+ spec.output_align};
+
+ if (!spec.use_streaming) {
+ // Set all input at once
+ JxlDecoderSetInput(dec.get(), jxl, size);
+ JxlDecoderCloseInput(dec.get());
+ }
+
+ bool seen_basic_info = false;
+ bool seen_extensions = false;
+ bool seen_color_encoding = false;
+ bool seen_preview = false;
+ bool seen_need_image_out = false;
+ bool seen_full_image = false;
+ bool seen_frame = false;
+ uint32_t num_frames = 0;
+ bool seen_jpeg_reconstruction = false;
+ bool seen_jpeg_need_more_output = false;
+ // If streaming and seen around half the input, test flushing
+ bool tested_flush = false;
+
+ // Size made available for the streaming input, emulating a subset of the
+ // full input size.
+ size_t streaming_size = 0;
+ size_t leftover = size;
+ size_t preview_xsize = 0;
+ size_t preview_ysize = 0;
+ bool want_preview = false;
+ std::vector<uint8_t> preview_pixels;
+
+ std::vector<uint8_t> extra_channel_pixels;
+
+ // Callback function used when decoding with use_callback.
+ struct DecodeCallbackData {
+ JxlBasicInfo info;
+ size_t xsize = 0;
+ size_t ysize = 0;
+ std::mutex called_rows_mutex;
+ // For each row stores the segments of the row being called. For each row
+ // the sum of all the int values in the map up to [i] (inclusive) tell how
+ // many times a callback included the pixel i of that row.
+ std::vector<std::map<uint32_t, int>> called_rows;
+
+ // Use the pixel values.
+ uint32_t value = 0;
+ };
+ DecodeCallbackData decode_callback_data;
+ auto decode_callback = +[](void* opaque, size_t x, size_t y,
+ size_t num_pixels, const void* pixels) {
+ DecodeCallbackData* data = static_cast<DecodeCallbackData*>(opaque);
+ if (num_pixels > data->xsize) abort();
+ if (x + num_pixels > data->xsize) abort();
+ if (y >= data->ysize) abort();
+ if (num_pixels && !pixels) abort();
+ // Keep track of the segments being called by the callback.
+ {
+ const std::lock_guard<std::mutex> lock(data->called_rows_mutex);
+ data->called_rows[y][x]++;
+ data->called_rows[y][x + num_pixels]--;
+ data->value += *static_cast<const uint8_t*>(pixels);
+ }
+ };
+
+ JxlExtraChannelInfo extra_channel_info;
+
+ std::vector<uint8_t> box_buffer;
+
+ if (spec.decode_boxes &&
+ JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(dec.get(), JXL_TRUE)) {
+ // error ignored, can still fuzz if it doesn't brotli-decompress brob boxes.
+ }
+
+ for (;;) {
+ JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
+ if (status == JXL_DEC_ERROR) {
+ return false;
+ } else if (status == JXL_DEC_NEED_MORE_INPUT) {
+ if (spec.use_streaming) {
+ size_t remaining = JxlDecoderReleaseInput(dec.get());
+ // move any remaining bytes to the front if necessary
+ size_t used = streaming_size - remaining;
+ jxl += used;
+ leftover -= used;
+ streaming_size -= used;
+ size_t chunk_size = std::max<size_t>(
+ 1, size * std::min<double>(1.0, dis_streaming(mt)));
+ size_t add_size =
+ std::min<size_t>(chunk_size, leftover - streaming_size);
+ if (add_size == 0) {
+ // End of the streaming data reached
+ return false;
+ }
+ streaming_size += add_size;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetInput(dec.get(), jxl, streaming_size)) {
+ return false;
+ }
+ if (leftover == streaming_size) {
+ // All possible input bytes given
+ JxlDecoderCloseInput(dec.get());
+ }
+
+ if (!tested_flush && seen_frame) {
+ // Test flush max once to avoid too slow fuzzer run
+ tested_flush = true;
+ JxlDecoderFlushImage(dec.get());
+ }
+ } else {
+ return false;
+ }
+ } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) {
+ if (spec.jpeg_to_pixels) abort();
+ if (!seen_jpeg_reconstruction) abort();
+ seen_jpeg_need_more_output = true;
+ size_t used_jpeg_output =
+ jpeg->size() - JxlDecoderReleaseJPEGBuffer(dec.get());
+ jpeg->resize(std::max<size_t>(4096, jpeg->size() * 2));
+ uint8_t* jpeg_buffer = jpeg->data() + used_jpeg_output;
+ size_t jpeg_buffer_size = jpeg->size() - used_jpeg_output;
+
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetJPEGBuffer(dec.get(), jpeg_buffer, jpeg_buffer_size)) {
+ return false;
+ }
+ } else if (status == JXL_DEC_BASIC_INFO) {
+ if (seen_basic_info) abort(); // already seen basic info
+ seen_basic_info = true;
+
+ memset(&info, 0, sizeof(info));
+ if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &info)) {
+ return false;
+ }
+ Consume(info);
+
+ *xsize = info.xsize;
+ *ysize = info.ysize;
+ decode_callback_data.info = info;
+ size_t num_pixels = *xsize * *ysize;
+ // num_pixels overflow
+ if (*xsize != 0 && num_pixels / *xsize != *ysize) return false;
+ // limit max memory of this fuzzer test
+ if (num_pixels > max_pixels) return false;
+
+ if (info.have_preview) {
+ want_preview = true;
+ preview_xsize = info.preview.xsize;
+ preview_ysize = info.preview.ysize;
+ size_t preview_num_pixels = preview_xsize * preview_ysize;
+ // num_pixels overflow
+ if (preview_xsize != 0 &&
+ preview_num_pixels / preview_xsize != preview_ysize) {
+ return false;
+ }
+ // limit max memory of this fuzzer test
+ if (preview_num_pixels > max_pixels) return false;
+ }
+
+ for (size_t ec = 0; ec < info.num_extra_channels; ++ec) {
+ memset(&extra_channel_info, 0, sizeof(extra_channel_info));
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetExtraChannelInfo(dec.get(), ec, &extra_channel_info)) {
+ abort();
+ }
+ Consume(extra_channel_info);
+ std::vector<char> ec_name(extra_channel_info.name_length + 1);
+ if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelName(dec.get(), ec,
+ ec_name.data(),
+ ec_name.size())) {
+ abort();
+ }
+ Consume(ec_name.cbegin(), ec_name.cend());
+ }
+ } else if (status == JXL_DEC_EXTENSIONS) {
+ if (!seen_basic_info) abort(); // expected basic info first
+ if (seen_color_encoding) abort(); // should happen after this
+ if (seen_extensions) abort(); // already seen extensions
+ seen_extensions = true;
+ // TODO(eustas): get extensions?
+ } else if (status == JXL_DEC_COLOR_ENCODING) {
+ if (!seen_basic_info) abort(); // expected basic info first
+ if (seen_color_encoding) abort(); // already seen color encoding
+ seen_color_encoding = true;
+
+ // Get the ICC color profile of the pixel data
+ size_t icc_size;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetICCProfileSize(
+ dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) {
+ return false;
+ }
+ icc_profile->resize(icc_size);
+ if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile(
+ dec.get(), &format,
+ JXL_COLOR_PROFILE_TARGET_DATA,
+ icc_profile->data(), icc_profile->size())) {
+ return false;
+ }
+ if (want_preview) {
+ size_t preview_size;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderPreviewOutBufferSize(dec.get(), &format, &preview_size)) {
+ return false;
+ }
+ preview_pixels.resize(preview_size);
+ if (JXL_DEC_SUCCESS != JxlDecoderSetPreviewOutBuffer(
+ dec.get(), &format, preview_pixels.data(),
+ preview_pixels.size())) {
+ abort();
+ }
+ }
+ } else if (status == JXL_DEC_PREVIEW_IMAGE) {
+ if (seen_preview) abort();
+ if (!want_preview) abort();
+ if (!seen_color_encoding) abort();
+ want_preview = false;
+ seen_preview = true;
+ Consume(preview_pixels.cbegin(), preview_pixels.cend());
+ } else if (status == JXL_DEC_FRAME ||
+ status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ if (want_preview) abort(); // expected preview before frame
+ if (!seen_color_encoding) abort(); // expected color encoding first
+ if (status == JXL_DEC_FRAME) {
+ if (seen_frame) abort(); // already seen JXL_DEC_FRAME
+ seen_frame = true;
+ JxlFrameHeader frame_header;
+ memset(&frame_header, 0, sizeof(frame_header));
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetFrameHeader(dec.get(), &frame_header)) {
+ abort();
+ }
+ decode_callback_data.xsize = frame_header.layer_info.xsize;
+ decode_callback_data.ysize = frame_header.layer_info.ysize;
+ if (!spec.coalescing) {
+ decode_callback_data.called_rows.clear();
+ }
+ decode_callback_data.called_rows.resize(decode_callback_data.ysize);
+ Consume(frame_header);
+ std::vector<char> frame_name(frame_header.name_length + 1);
+ if (JXL_DEC_SUCCESS != JxlDecoderGetFrameName(dec.get(),
+ frame_name.data(),
+ frame_name.size())) {
+ abort();
+ }
+ Consume(frame_name.cbegin(), frame_name.cend());
+ // When not testing streaming, test that JXL_DEC_NEED_IMAGE_OUT_BUFFER
+ // occurs instead, so do not set buffer now.
+ if (!spec.use_streaming) continue;
+ }
+ if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ // expected JXL_DEC_FRAME instead
+ if (!seen_frame) abort();
+ // already should have set buffer if streaming
+ if (spec.use_streaming) abort();
+ // already seen need image out
+ if (seen_need_image_out) abort();
+ seen_need_image_out = true;
+ }
+
+ if (info.num_extra_channels > 0) {
+ std::uniform_int_distribution<> dis(0, info.num_extra_channels);
+ size_t ec_index = dis(mt);
+ // There is also a probability no extra channel is chosen
+ if (ec_index < info.num_extra_channels) {
+ size_t ec_index = info.num_extra_channels - 1;
+ size_t ec_size;
+ if (JXL_DEC_SUCCESS != JxlDecoderExtraChannelBufferSize(
+ dec.get(), &format, &ec_size, ec_index)) {
+ return false;
+ }
+ extra_channel_pixels.resize(ec_size);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetExtraChannelBuffer(dec.get(), &format,
+ extra_channel_pixels.data(),
+ ec_size, ec_index)) {
+ return false;
+ }
+ }
+ }
+
+ if (spec.use_callback) {
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetImageOutCallback(dec.get(), &format, decode_callback,
+ &decode_callback_data)) {
+ return false;
+ }
+ } else {
+ // Use the pixels output buffer.
+ size_t buffer_size;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) {
+ return false;
+ }
+ pixels->resize(buffer_size);
+ void* pixels_buffer = (void*)pixels->data();
+ size_t pixels_buffer_size = pixels->size();
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetImageOutBuffer(dec.get(), &format, pixels_buffer,
+ pixels_buffer_size)) {
+ return false;
+ }
+ }
+ } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) {
+ if (want_preview) abort(); // expected preview before frame
+ if (seen_jpeg_reconstruction) abort();
+ seen_jpeg_reconstruction = true;
+ if (!spec.jpeg_to_pixels) {
+ // Make sure buffer is allocated, but current size is too small to
+ // contain valid JPEG.
+ jpeg->resize(1);
+ uint8_t* jpeg_buffer = jpeg->data();
+ size_t jpeg_buffer_size = jpeg->size();
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetJPEGBuffer(dec.get(), jpeg_buffer, jpeg_buffer_size)) {
+ return false;
+ }
+ }
+ } else if (status == JXL_DEC_FULL_IMAGE) {
+ if (want_preview) abort(); // expected preview before frame
+ if (!spec.jpeg_to_pixels && seen_jpeg_reconstruction) {
+ if (!seen_jpeg_need_more_output) abort();
+ jpeg->resize(jpeg->size() - JxlDecoderReleaseJPEGBuffer(dec.get()));
+ } else {
+ // expected need image out or frame first
+ if (!seen_need_image_out && !seen_frame) abort();
+ }
+
+ seen_full_image = true; // there may be multiple if animated
+
+ // There may be a next animation frame so expect those again:
+ seen_need_image_out = false;
+ seen_frame = false;
+ num_frames++;
+
+ // "Use" all the pixels; MSAN needs a conditional to count as usage.
+ Consume(pixels->cbegin(), pixels->cend());
+ Consume(jpeg->cbegin(), jpeg->cend());
+
+ // When not coalescing, check that the whole (possibly cropped) frame was
+ // sent
+ if (seen_need_image_out && spec.use_callback && spec.coalescing) {
+ // Check that the callback sent all the pixels
+ for (uint32_t y = 0; y < decode_callback_data.ysize; y++) {
+ // Check that each row was at least called once.
+ if (decode_callback_data.called_rows[y].empty()) abort();
+ uint32_t last_idx = 0;
+ int calls = 0;
+ for (auto it : decode_callback_data.called_rows[y]) {
+ if (it.first > last_idx) {
+ if (static_cast<uint32_t>(calls) != 1) abort();
+ }
+ calls += it.second;
+ last_idx = it.first;
+ }
+ }
+ }
+ // Nothing to do. Do not yet return. If the image is an animation, more
+ // full frames may be decoded. This example only keeps the last one.
+ } else if (status == JXL_DEC_SUCCESS) {
+ if (!seen_full_image) abort(); // expected full image before finishing
+
+ // When decoding we may not get seen_need_image_out unless we were
+ // decoding the image to pixels.
+ if (seen_need_image_out && spec.use_callback && spec.coalescing) {
+ // Check that the callback sent all the pixels
+ for (uint32_t y = 0; y < decode_callback_data.ysize; y++) {
+ // Check that each row was at least called once.
+ if (decode_callback_data.called_rows[y].empty()) abort();
+ uint32_t last_idx = 0;
+ int calls = 0;
+ for (auto it : decode_callback_data.called_rows[y]) {
+ if (it.first > last_idx) {
+ if (static_cast<uint32_t>(calls) != num_frames) abort();
+ }
+ calls += it.second;
+ last_idx = it.first;
+ }
+ }
+ }
+
+ // All decoding successfully finished.
+ // It's not required to call JxlDecoderReleaseInput(dec.get()) here since
+ // the decoder will be destroyed.
+ return true;
+ } else if (status == JXL_DEC_BOX) {
+ if (spec.decode_boxes) {
+ if (!box_buffer.empty()) {
+ size_t remaining = JxlDecoderReleaseBoxBuffer(dec.get());
+ size_t box_size = box_buffer.size() - remaining;
+ if (box_size != 0) {
+ Consume(box_buffer.begin(), box_buffer.begin() + box_size);
+ box_buffer.clear();
+ }
+ }
+ box_buffer.resize(64);
+ JxlDecoderSetBoxBuffer(dec.get(), box_buffer.data(), box_buffer.size());
+ }
+ } else if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
+ if (!spec.decode_boxes) {
+ abort(); // Not expected when not setting output buffer
+ }
+ size_t remaining = JxlDecoderReleaseBoxBuffer(dec.get());
+ size_t box_size = box_buffer.size() - remaining;
+ box_buffer.resize(box_buffer.size() * 2);
+ JxlDecoderSetBoxBuffer(dec.get(), box_buffer.data() + box_size,
+ box_buffer.size() - box_size);
+ } else {
+ return false;
+ }
+ }
+}
+
+int TestOneInput(const uint8_t* data, size_t size) {
+ if (size < 4) return 0;
+ uint32_t flags = 0;
+ size_t used_flag_bits = 0;
+ memcpy(&flags, data + size - 4, 4);
+ size -= 4;
+
+ const auto getFlag = [&flags, &used_flag_bits](size_t max_value) {
+ size_t limit = 1;
+ while (limit <= max_value) {
+ limit <<= 1;
+ used_flag_bits++;
+ if (used_flag_bits > 32) abort();
+ }
+ uint32_t result = flags % limit;
+ flags /= limit;
+ return result % (max_value + 1);
+ };
+
+ FuzzSpec spec;
+ // Allows some different possible variations in the chunk sizes of the
+ // streaming case
+ spec.random_seed = flags ^ size;
+ spec.get_alpha = !!getFlag(1);
+ spec.get_grayscale = !!getFlag(1);
+ spec.use_streaming = !!getFlag(1);
+ spec.jpeg_to_pixels = !!getFlag(1);
+ spec.use_callback = !!getFlag(1);
+ spec.keep_orientation = !!getFlag(1);
+ spec.coalescing = !!getFlag(1);
+ spec.output_type = static_cast<JxlDataType>(getFlag(JXL_TYPE_FLOAT16));
+ spec.output_endianness = static_cast<JxlEndianness>(getFlag(JXL_BIG_ENDIAN));
+ spec.output_align = getFlag(16);
+ spec.decode_boxes = !!getFlag(1);
+
+ std::vector<uint8_t> pixels;
+ std::vector<uint8_t> jpeg;
+ std::vector<uint8_t> icc;
+ size_t xsize, ysize;
+ size_t max_pixels = 1 << 21;
+
+ const auto targets = hwy::SupportedAndGeneratedTargets();
+ hwy::SetSupportedTargetsForTest(targets[getFlag(targets.size() - 1)]);
+ DecodeJpegXl(data, size, max_pixels, spec, &pixels, &jpeg, &xsize, &ysize,
+ &icc);
+ hwy::SetSupportedTargetsForTest(0);
+
+ return 0;
+}
+
+} // namespace
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return TestOneInput(data, size);
+}
diff --git a/media/libjxl/src/tools/djxl_main.cc b/media/libjxl/src/tools/djxl_main.cc
new file mode 100644
index 0000000000..d1442d949c
--- /dev/null
+++ b/media/libjxl/src/tools/djxl_main.cc
@@ -0,0 +1,195 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <vector>
+
+#include "jxl/decode.h"
+#include "lib/jxl/aux_out.h"
+#include "lib/jxl/base/cache_aligned.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/profiler.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "tools/box/box.h"
+#include "tools/cmdline.h"
+#include "tools/codec_config.h"
+#include "tools/djxl.h"
+#include "tools/speed_stats.h"
+
+namespace jpegxl {
+namespace tools {
+namespace {
+
+int DecompressMain(int argc, const char* argv[]) {
+ DecompressArgs args;
+ CommandLineParser cmdline;
+ args.AddCommandLineOptions(&cmdline);
+
+ if (!cmdline.Parse(argc, argv)) {
+ // ValidateArgs already printed the actual error cause.
+ fprintf(stderr, "Use '%s -h' for more information\n", argv[0]);
+ return 1;
+ }
+
+ if (args.version) {
+ fprintf(stdout, "djxl %s\n",
+ CodecConfigString(JxlDecoderVersion()).c_str());
+ fprintf(stdout, "Copyright (c) the JPEG XL Project\n");
+ return 0;
+ }
+ if (!args.quiet) {
+ fprintf(stderr, "JPEG XL decoder %s\n",
+ CodecConfigString(JxlDecoderVersion()).c_str());
+ }
+
+ if (cmdline.HelpFlagPassed()) {
+ cmdline.PrintHelp();
+ return 0;
+ }
+
+ if (!args.ValidateArgs(cmdline)) {
+ // ValidateArgs already printed the actual error cause.
+ fprintf(stderr, "Use '%s -h' for more information\n", argv[0]);
+ return 1;
+ }
+
+ jxl::PaddedBytes compressed;
+ if (!jxl::ReadFile(args.file_in, &compressed)) {
+ fprintf(stderr, "Failed to read file: %s.\n", args.file_in);
+ return 1;
+ }
+ if (!args.quiet) {
+ fprintf(stderr, "Read %" PRIuS " compressed bytes.\n", compressed.size());
+ }
+
+ // If the file uses the box format container, unpack the boxes into
+ // `container`. Otherwise, fill `container.codestream` accordingly.
+ JpegXlContainer container;
+ if (IsContainerHeader(compressed.data(), compressed.size())) {
+ if (!DecodeJpegXlContainerOneShot(compressed.data(), compressed.size(),
+ &container)) {
+ fprintf(stderr, "Decoding container format failed.\n");
+ return 1;
+ }
+ } else {
+ container.codestream = std::move(compressed);
+ }
+
+ jxl::ThreadPoolInternal pool(args.num_threads);
+ SpeedStats stats;
+
+ // Quick test that this looks like a valid JXL file.
+ JxlSignature signature = JxlSignatureCheck(container.codestream.data(),
+ container.codestream.size());
+ if (signature == JXL_SIG_NOT_ENOUGH_BYTES || signature == JXL_SIG_INVALID) {
+ fprintf(stderr, "Unknown compressed image format (%u)\n", signature);
+ return 1;
+ }
+
+ if (!args.file_out && !args.quiet) {
+ fprintf(stderr,
+ "No output file specified.\n"
+ "Decoding will be performed, but the result will be discarded.\n");
+ }
+
+ jxl::AuxOut aux_out;
+
+ if (!args.decode_to_pixels) {
+ args.params.keep_dct = true;
+
+ jxl::PaddedBytes jpg_output;
+ bool success = true;
+ for (size_t i = 0; i < args.num_reps; ++i) {
+ success = success && DecompressJxlToJPEG(container, args, &pool,
+ &jpg_output, &stats);
+ }
+ if (!args.quiet && success) fprintf(stderr, "Reconstructed to JPEG.\n");
+
+ if (success && args.file_out != nullptr) {
+ if (!jxl::WriteFile(jpg_output, args.file_out)) {
+ fprintf(stderr, "Failed to write to \"%s\"\n", args.file_out);
+ return 1;
+ }
+ }
+ if (!success) {
+ if (!args.quiet) {
+ fprintf(stderr,
+ "Warning: could not decode losslessly to JPEG. Retrying with "
+ "--pixels_to_jpeg...\n");
+ }
+ args.decode_to_pixels = true;
+ }
+ }
+ if (args.decode_to_pixels) {
+ args.params.keep_dct = false;
+ jxl::CodecInOut io;
+ auto assign = [](const uint8_t* bytes, size_t size,
+ std::vector<uint8_t>& target) {
+ target.assign(bytes, bytes + size);
+ };
+ if (container.exif_size) {
+ assign(container.exif, container.exif_size, io.blobs.exif);
+ }
+ if (!container.xml.empty()) {
+ assign(container.xml[0].first, container.xml[0].second, io.blobs.xmp);
+ }
+ if (container.xml.size() > 1) {
+ fprintf(stderr,
+ "Warning: more than one XML box found, assuming first one is XMP "
+ "and ignoring others\n");
+ }
+ // Set JPEG quality.
+ // TODO(veluca): the decoder should set this value, and the argument should
+ // be an override.
+ // TODO(veluca): the decoder should directly produce a JPEG file, and this
+ // should not be necessary.
+ io.use_sjpeg = args.use_sjpeg;
+ io.jpeg_quality = args.jpeg_quality;
+
+ // Decode to pixels.
+ for (size_t i = 0; i < args.num_reps; ++i) {
+ if (!DecompressJxlToPixels(jxl::Span<const uint8_t>(container.codestream),
+ args.params, &pool, &io, &stats)) {
+ // Error is already reported by DecompressJxlToPixels.
+ return 1;
+ }
+ }
+ if (!args.quiet) fprintf(stderr, "Decoded to pixels.\n");
+ if (!WriteJxlOutput(args, args.file_out, io, &pool)) {
+ // Error is already reported by WriteJxlOutput.
+ return 1;
+ }
+
+ if (args.print_read_bytes) {
+ fprintf(stderr, "Decoded bytes: %" PRIuS "\n", io.Main().decoded_bytes());
+ }
+ }
+
+ if (!args.quiet) JXL_CHECK(stats.Print(pool.NumWorkerThreads()));
+
+ if (args.print_profile == jxl::Override::kOn) {
+ PROFILER_PRINT_RESULTS();
+ }
+ if (!args.quiet) jxl::CacheAligned::PrintStats();
+ return 0;
+}
+
+} // namespace
+} // namespace tools
+} // namespace jpegxl
+
+int main(int argc, const char* argv[]) {
+ return jpegxl::tools::DecompressMain(argc, argv);
+}
diff --git a/media/libjxl/src/tools/djxl_ng_main.cc b/media/libjxl/src/tools/djxl_ng_main.cc
new file mode 100644
index 0000000000..b2eec302a8
--- /dev/null
+++ b/media/libjxl/src/tools/djxl_ng_main.cc
@@ -0,0 +1,476 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <climits>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include "gflags/gflags.h"
+#include "jxl/codestream_header.h"
+#include "jxl/decode.h"
+#include "jxl/decode_cxx.h"
+#include "jxl/resizable_parallel_runner_cxx.h"
+#include "jxl/thread_parallel_runner.h"
+#include "jxl/thread_parallel_runner_cxx.h"
+#include "jxl/types.h"
+#include "lib/extras/dec/decode.h"
+#include "lib/extras/enc/pnm.h"
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/status.h"
+
+DECLARE_bool(help);
+DECLARE_bool(helpshort);
+
+DEFINE_int64(num_reps, 1, "How many times to decompress.");
+
+DEFINE_int64(num_threads, 0,
+ // TODO(firsching): Sync with team about changed meaning of 0 -
+ // was: No multithreaded workers. Is: use default number.
+ "Number of worker threads (0 == use machine default).");
+
+// TODO(firsching): wire this up.
+DEFINE_int32(bits_per_sample, 0, "0 = original (input) bit depth");
+
+// TODO(firsching): wire this up.
+DEFINE_bool(
+ tone_map, true,
+ "tone map the image to the luminance range indicated by --display_nits "
+ "instead of performing a naive 0-1 -> 0-1 conversion");
+
+// TODO(firsching): wire this up.
+DEFINE_string(display_nits, "0.f-255.",
+ "luminance range of the display to which to "
+ "tone-map; the lower bound can be omitted");
+
+// TODO(firsching): wire this up.
+DEFINE_double(preserve_saturation, 0.1,
+ "with --tone_map, how much to favor saturation over luminance");
+
+// TODO(firsching): wire this up; consider making empty string the default.
+DEFINE_string(color_space, "RGB_D65_SRG_Rel_Lin",
+ "defaults to original (input) color space");
+
+// TODO(firsching): wire this up.
+DEFINE_uint32(downsampling, 0,
+ "maximum permissible downsampling factor (values "
+ "greater than 16 will return the LQIP if available");
+
+// TODO(firsching): wire this up.
+DEFINE_bool(allow_partial_files, false, "allow decoding of truncated files");
+
+// TODO(firsching): wire this up.
+DEFINE_bool(allow_more_progressive_steps, false,
+ "allow decoding more progressive steps in truncated "
+ "files. No effect without --allow_partial_files");
+
+#if JPEGXL_ENABLE_JPEG
+// TODO(firsching): wire this up.
+DEFINE_bool(
+ pixels_to_jpeg, false,
+ "By default, if the input JPEG XL contains a recompressed JPEG file, djxl "
+ "reconstructs the exact original JPEG file. This flag causes the decoder "
+ "to instead decode the image to pixels and encode a new (lossy) JPEG. "
+ "The output file if provided must be a .jpg or .jpeg file.");
+
+// TODO(firsching): wire this up.
+DEFINE_uint32(jpeg_quality, 95,
+ "JPEG output quality. Setting an output quality "
+ "implies --pixels_to_jpeg.");
+#endif
+
+#if JPEGXL_ENABLE_SJPEG
+// TODO(firsching): wire this up.
+DEFINE_bool(use_sjpeg, false, "use sjpeg instead of libjpeg for JPEG output");
+#endif
+
+// TODO(firsching): wire this up.
+DEFINE_bool(print_read_bytes, false, "print total number of decoded bytes");
+
+// TODO(firsching): wire this up.
+DEFINE_bool(quiet, false, "silence output (except for errors)");
+
+bool ReadFile(const char* filename, std::vector<uint8_t>* out) {
+ FILE* file = fopen(filename, "rb");
+ if (!file) {
+ return false;
+ }
+
+ if (fseek(file, 0, SEEK_END) != 0) {
+ fclose(file);
+ return false;
+ }
+
+ long size = ftell(file);
+ // Avoid invalid file or directory.
+ if (size >= LONG_MAX || size < 0) {
+ fclose(file);
+ return false;
+ }
+
+ if (fseek(file, 0, SEEK_SET) != 0) {
+ fclose(file);
+ return false;
+ }
+
+ out->resize(size);
+ size_t readsize = fread(out->data(), 1, size, file);
+ if (fclose(file) != 0) {
+ return false;
+ }
+
+ return readsize == static_cast<size_t>(size);
+}
+
+bool WriteFile(const char* filename, const std::vector<uint8_t>& bytes) {
+ FILE* file = fopen(filename, "wb");
+ if (!file) {
+ fprintf(stderr,
+ "Could not open %s for writing\n"
+ "Error: %s",
+ filename, strerror(errno));
+ return false;
+ }
+ if (fwrite(bytes.data(), 1, bytes.size(), file) != bytes.size()) {
+ fprintf(stderr,
+ "Could not write to file\n"
+ "Error: %s",
+ strerror(errno));
+ return false;
+ }
+ if (fclose(file) != 0) {
+ fprintf(stderr,
+ "Could not close file\n"
+ "Error: %s",
+ strerror(errno));
+ return false;
+ }
+ return true;
+}
+
+int DecompressJxlReconstructJPEG(const std::vector<uint8_t>& compressed,
+ std::vector<uint8_t>& jpeg_bytes,
+ JxlDecoderPtr dec,
+ JxlThreadParallelRunnerPtr runner) {
+ if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(),
+ JxlThreadParallelRunner,
+ runner.get())) {
+ fprintf(stderr, "JxlEncoderSetParallelRunner failed\n");
+ return EXIT_FAILURE;
+ }
+
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSubscribeEvents(
+ dec.get(), JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE)) {
+ fprintf(stderr, "JxlDecoderSubscribeEvents failed\n");
+ return EXIT_FAILURE;
+ }
+ bool can_reconstruct_jpeg = false;
+ std::vector<uint8_t> jpeg_data_chunk(16384);
+ jpeg_bytes.resize(0);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size())) {
+ fprintf(stderr, "Decoder failed to set input\n");
+ return EXIT_FAILURE;
+ }
+ JxlDecoderCloseInput(dec.get());
+
+ for (;;) {
+ JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
+ if (status == JXL_DEC_ERROR) {
+ fprintf(stderr, "Failed to decode image\n");
+ return EXIT_FAILURE;
+ } else if (status == JXL_DEC_NEED_MORE_INPUT) {
+ fprintf(stderr, "Error, already provided all input\n");
+ return EXIT_FAILURE;
+ } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) {
+ can_reconstruct_jpeg = true;
+ // Decoding to JPEG.
+ if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec.get(),
+ jpeg_data_chunk.data(),
+ jpeg_data_chunk.size())) {
+ fprintf(stderr, "Decoder failed to set JPEG Buffer\n");
+ return EXIT_FAILURE;
+ }
+ } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) {
+ // Decoded a chunk to JPEG.
+ size_t used_jpeg_output =
+ jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get());
+ jpeg_bytes.insert(jpeg_bytes.end(), jpeg_data_chunk.data(),
+ jpeg_data_chunk.data() + used_jpeg_output);
+ if (used_jpeg_output == 0) {
+ // Chunk is too small.
+ jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2);
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec.get(),
+ jpeg_data_chunk.data(),
+ jpeg_data_chunk.size())) {
+ fprintf(stderr, "Decoder failed to set JPEG Buffer\n");
+ return EXIT_FAILURE;
+ }
+ } else if (status == JXL_DEC_SUCCESS) {
+ // Decoding finished successfully.
+ break;
+ } else if (status == JXL_DEC_FULL_IMAGE) {
+ } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ break;
+ } else {
+ fprintf(stderr, "Error: unexpected status: %d\n",
+ static_cast<int>(status));
+ return EXIT_FAILURE;
+ }
+ }
+ if (!can_reconstruct_jpeg) return EXIT_FAILURE;
+ size_t used_jpeg_output =
+ jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get());
+ jpeg_bytes.insert(jpeg_bytes.end(), jpeg_data_chunk.data(),
+ jpeg_data_chunk.data() + used_jpeg_output);
+ return EXIT_SUCCESS;
+}
+
+int DecompressJxlToPackedPixelFile(const std::vector<uint8_t>& compressed,
+ jxl::extras::PackedPixelFile& ppf,
+ JxlPixelFormat& format, JxlDecoderPtr dec,
+ JxlThreadParallelRunnerPtr runner) {
+ if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(),
+ JxlThreadParallelRunner,
+ runner.get())) {
+ fprintf(stderr, "JxlEncoderSetParallelRunner failed\n");
+ return EXIT_FAILURE;
+ }
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSubscribeEvents(dec.get(),
+ JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
+ JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)) {
+ fprintf(stderr, "JxlDecoderSubscribeEvents failed\n");
+ return EXIT_FAILURE;
+ }
+
+ // Reading compressed JPEG XL input and decoding to pixels
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size())) {
+ fprintf(stderr, "Decoder failed to set input\n");
+ return EXIT_FAILURE;
+ }
+ // TODO(firsching): handle boxes as well (exif, iptc, jumbf and xmp).
+ for (;;) {
+ JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
+ if (status == JXL_DEC_ERROR) {
+ fprintf(stderr, "Failed to decode image\n");
+ return EXIT_FAILURE;
+ } else if (status == JXL_DEC_NEED_MORE_INPUT) {
+ fprintf(stderr, "Error, already provided all input\n");
+ return EXIT_FAILURE;
+ } else if (status == JXL_DEC_BASIC_INFO) {
+ if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &ppf.info)) {
+ fprintf(stderr, "JxlDecoderGetBasicInfo failed\n");
+ return EXIT_FAILURE;
+ }
+ // Make some modifications to the format if the decoded data requires it.
+ if (ppf.info.num_color_channels != format.num_channels) {
+ format.num_channels = ppf.info.num_color_channels;
+ }
+ if (ppf.info.bits_per_sample > 8 &&
+ ppf.info.exponent_bits_per_sample == 0) {
+ format.data_type = JXL_TYPE_UINT16;
+ }
+ // TODO(firsching): handle extra channels
+ } else if (status == JXL_DEC_COLOR_ENCODING) {
+ size_t icc_size = 0;
+ // TODO(firsching) handle other targets as well.
+ JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_ORIGINAL;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetICCProfileSize(dec.get(), &format, target, &icc_size)) {
+ fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
+ }
+ if (icc_size != 0) {
+ ppf.icc.resize(icc_size);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetColorAsICCProfile(dec.get(), &format, target,
+ ppf.icc.data(), icc_size)) {
+ fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
+ return EXIT_FAILURE;
+ }
+ } else {
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetColorAsEncodedProfile(dec.get(), &format, target,
+ &ppf.color_encoding)) {
+ fprintf(stderr, "JxlDecoderGetColorAsEncodedProfile failed\n");
+ return EXIT_FAILURE;
+ }
+ }
+ } else if (status == JXL_DEC_FRAME) {
+ jxl::extras::PackedFrame frame(ppf.info.xsize, ppf.info.ysize, format);
+ ppf.frames.emplace_back(std::move(frame));
+ } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ size_t buffer_size;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) {
+ fprintf(stderr, "JxlDecoderImageOutBufferSize failed\n");
+ return EXIT_FAILURE;
+ }
+ if (buffer_size != ppf.frames.back().color.pixels_size) {
+ fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n",
+ buffer_size, ppf.frames.back().color.pixels_size);
+ return EXIT_FAILURE;
+ }
+
+ auto callback = [](void* opaque, size_t x, size_t y, size_t num_pixels,
+ const void* pixels) {
+ jxl::extras::PackedPixelFile* ppf =
+ reinterpret_cast<jxl::extras::PackedPixelFile*>(opaque);
+ uint8_t* pixels_buffer =
+ reinterpret_cast<uint8_t*>(ppf->frames.back().color.pixels());
+ size_t sample_size = ppf->frames.back().color.format.num_channels *
+ ppf->frames.back().color.BitsPerChannel(
+ ppf->frames.back().color.format.data_type) /
+ 8;
+ // TODO(firsching): take color profile into account and transform if
+ // needed here.
+ memcpy(pixels_buffer +
+ (ppf->frames.back().color.stride * y + sample_size * x),
+ pixels, num_pixels * sample_size);
+ };
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetImageOutCallback(dec.get(), &format, callback, &ppf)) {
+ fprintf(stderr, "JxlDecoderSetImageOutCallback failed\n");
+ return EXIT_FAILURE;
+ }
+ } else if (status == JXL_DEC_SUCCESS) {
+ // Decoding finished successfully.
+ break;
+ } else if (status == JXL_DEC_FULL_IMAGE) {
+ } else {
+ fprintf(stderr, "Error: unexpected status: %d\n",
+ static_cast<int>(status));
+ return EXIT_FAILURE;
+ }
+ }
+ return EXIT_SUCCESS;
+}
+
+int main(int argc, char** argv) {
+ std::cerr << "Warning: This is work in progress, consider using djxl "
+ "instead!\n";
+
+ gflags::SetUsageMessage("JPEG XL decoder");
+ uint32_t version = JxlDecoderVersion();
+ gflags::SetVersionString(std::to_string(version / 1000000) + "." +
+ std::to_string((version / 1000) % 1000) + "." +
+ std::to_string(version % 1000));
+ // TODO(firsching): rethink --help handling
+ gflags::ParseCommandLineNonHelpFlags(&argc, &argv, /*remove_flags=*/true);
+ if (FLAGS_help) {
+ FLAGS_help = false;
+ FLAGS_helpshort = true;
+ }
+ gflags::HandleCommandLineHelpFlags();
+
+ if (argc != 3) {
+ FLAGS_help = false;
+ FLAGS_helpshort = true;
+ gflags::HandleCommandLineHelpFlags();
+ return EXIT_FAILURE;
+ }
+ const char* filename_in = argv[1];
+ const char* filename_out = argv[2];
+ size_t num_reps = FLAGS_num_reps;
+
+ const char* extension = strrchr(filename_out, '.');
+ std::string base = extension == nullptr
+ ? std::string(filename_out)
+ : std::string(filename_out, extension - filename_out);
+ if (extension == nullptr) extension = "";
+ const jxl::extras::Codec codec = jxl::extras::CodecFromExtension(extension);
+
+ std::vector<uint8_t> compressed;
+ // Reading compressed JPEG XL input
+ if (!ReadFile(filename_in, &compressed)) {
+ fprintf(stderr, "couldn't load %s\n", filename_in);
+ return EXIT_FAILURE;
+ }
+
+ size_t num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads();
+ {
+ int64_t flag_num_worker_threads = FLAGS_num_threads;
+ if (flag_num_worker_threads != 0) {
+ num_worker_threads = flag_num_worker_threads;
+ }
+ }
+ auto dec = JxlDecoderMake(/*memory_manager=*/nullptr);
+ auto runner = JxlThreadParallelRunnerMake(
+ /*memory_manager=*/nullptr, num_worker_threads);
+ std::vector<uint8_t> bytes;
+ if (codec == jxl::extras::Codec::kJPG
+#if JPEGXL_ENABLE_JPEG
+ && !FLAGS_pixels_to_jpeg
+#endif
+ ) {
+ std::vector<uint8_t> bytes;
+ for (size_t i = 0; i < num_reps; ++i) {
+ if (DecompressJxlReconstructJPEG(compressed, bytes, std::move(dec),
+ std::move(runner)) != 0) {
+ return EXIT_FAILURE;
+ }
+ }
+ if (WriteFile(filename_out, bytes)) {
+ return EXIT_FAILURE;
+ };
+ // TODO(firsching): handle non-reconstruct JPEG
+ } else if (codec == jxl::extras::Codec::kPNM) {
+ JxlDataType datatype = JXL_TYPE_UINT8;
+ uint32_t num_channels = 3;
+ if (std::string(extension) == ".pfm") {
+ datatype = JXL_TYPE_FLOAT;
+ }
+ if (std::string(extension) == ".pgm") {
+ num_channels = 1;
+ }
+
+ JxlPixelFormat format = {num_channels, datatype, JXL_NATIVE_ENDIAN, 0};
+ jxl::extras::PackedPixelFile ppf;
+ if (DecompressJxlToPackedPixelFile(compressed, ppf, format, std::move(dec),
+ std::move(runner)) != 0) {
+ return EXIT_FAILURE;
+ }
+ if (ppf.info.exponent_bits_per_sample != 0) {
+ if (num_channels == 1 && ppf.info.num_color_channels == 3) {
+ JXL_WARNING("For color images, the filename should end with .ppm.\n");
+ }
+ if (num_channels == 3 && ppf.info.num_color_channels == 1) {
+ JXL_WARNING(
+ "For grayscale images, the filename should end with .pgm.\n");
+ }
+ if (ppf.info.bits_per_sample > 16) {
+ JXL_WARNING("PPM only supports up to 16 bits per sample");
+ }
+ }
+ const int digits = 1 + static_cast<int>(std::log10(std::max(
+ 1, static_cast<int>(ppf.frames.size() - 1))));
+ std::vector<char> output_filename;
+ output_filename.resize(base.size() + 1 + digits + strlen(extension) + 1);
+ for (size_t i = 0; i < ppf.frames.size(); i++) {
+ JXL_RETURN_IF_ERROR(jxl::extras::EncodeImagePNM(
+ ppf, ppf.frames[i].color.BitsPerChannel(format.data_type), nullptr, i,
+ &bytes));
+ snprintf(output_filename.data(), output_filename.size(), "%s-%0*zu%s",
+ base.c_str(), digits, i, extension);
+ if (!WriteFile(
+ ppf.frames.size() > 1 ? output_filename.data() : filename_out,
+ bytes)) {
+ return EXIT_FAILURE;
+ }
+ }
+ } else {
+ // TODO(firsching): handle other formats
+ }
+ return EXIT_SUCCESS;
+}
diff --git a/media/libjxl/src/tools/example_tree.txt b/media/libjxl/src/tools/example_tree.txt
new file mode 100644
index 0000000000..c4df6d4089
--- /dev/null
+++ b/media/libjxl/src/tools/example_tree.txt
@@ -0,0 +1,50 @@
+RCT 1 /* YCoCg */
+GroupShift 3 /* Group size is 128 << 3 == 1024 */
+Width 1024
+Height 1024
+Bitdepth 8
+/* FloatExpBits 3 */
+/* Alpha */
+/* Squeeze */
+/* XYB */
+/* CbYCr */
+
+
+if c > 0
+ /* Co, Cg: diagonal stripes */
+ if W > 50
+ - Set -50
+ - W + 5
+ /* Y: elementary cellular automaton */
+ if y > 0
+ if N > 0
+ if NW-N > -1
+ if N-NE > 0
+ - Set 0
+ - Set 255
+ if N-NE > 0
+ - Set 255
+ - Set 0
+ if NW-N > 0
+ if N-NE > -1
+ - Set 255
+ - Set 0
+ if N-NE > -1
+ - Set 0
+ - Set 255
+ /* First row initialization */
+ if x > 511
+ - Set 255
+ - Set 0
+
+Everything after the end of the tree is ignored.
+
+The tree above represents a cellular automaton on a subtly striped background.
+
+
+
+List of properties: c, g, y, x, |N|, |W|, N, W, W-WW-NW+NWW, W+N-NW, W-NW, NW-N, N-NE, N-NN, W-WW, WGH,
+ PrevAbs, Prev, PrevAbsErr, PrevErr, PPrevAbs, PPrev, PPrevAbsErr, PPrevErr
+
+List of predictors: Set, W, N, AvgW+N, Select, Gradient, Weighted, NE, NW, WW, AvgW+NW, AvgN+NW, AvgN+NE, AvgAll
+
diff --git a/media/libjxl/src/tools/fields_fuzzer.cc b/media/libjxl/src/tools/fields_fuzzer.cc
new file mode 100644
index 0000000000..87e143928b
--- /dev/null
+++ b/media/libjxl/src/tools/fields_fuzzer.cc
@@ -0,0 +1,85 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+
+#include "lib/jxl/dec_bit_reader.h"
+#include "lib/jxl/frame_header.h"
+#include "lib/jxl/headers.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/jpeg/jpeg_data.h"
+#include "lib/jxl/loop_filter.h"
+#include "lib/jxl/modular/encoding/context_predict.h"
+#include "lib/jxl/modular/encoding/encoding.h"
+#include "lib/jxl/modular/transform/transform.h"
+
+namespace jxl {
+
+int TestOneInput(const uint8_t* data, size_t size) {
+ // Global parameters used by some headers.
+ CodecMetadata codec_metadata;
+
+ // First byte controls which header to parse.
+ if (size == 0) return 0;
+ BitReader reader(Span<const uint8_t>(data + 1, size - 1));
+#define FUZZER_CASE_HEADER(number, classname, ...) \
+ case number: { \
+ classname header{__VA_ARGS__}; \
+ (void)Bundle::Read(&reader, &header); \
+ break; \
+ }
+ switch (data[0]) {
+ case 0: {
+ SizeHeader size_header;
+ (void)ReadSizeHeader(&reader, &size_header);
+ break;
+ }
+
+ case 1: {
+ ImageMetadata metadata;
+ (void)ReadImageMetadata(&reader, &metadata);
+ break;
+ }
+
+ FUZZER_CASE_HEADER(2, FrameHeader, &codec_metadata)
+ FUZZER_CASE_HEADER(3, jpeg::JPEGData)
+ FUZZER_CASE_HEADER(4, AnimationFrame, &codec_metadata)
+ FUZZER_CASE_HEADER(5, AnimationHeader)
+ FUZZER_CASE_HEADER(6, BitDepth)
+ FUZZER_CASE_HEADER(7, BlendingInfo)
+ FUZZER_CASE_HEADER(8, ColorEncoding)
+ FUZZER_CASE_HEADER(9, CustomTransferFunction)
+ FUZZER_CASE_HEADER(10, Customxy)
+ FUZZER_CASE_HEADER(11, ExtraChannelInfo)
+ FUZZER_CASE_HEADER(12, GroupHeader)
+ FUZZER_CASE_HEADER(13, weighted::Header)
+ FUZZER_CASE_HEADER(14, LoopFilter)
+ FUZZER_CASE_HEADER(15, LZ77Params)
+ FUZZER_CASE_HEADER(16, OpsinInverseMatrix)
+ FUZZER_CASE_HEADER(17, Passes)
+ FUZZER_CASE_HEADER(18, PreviewHeader)
+ FUZZER_CASE_HEADER(19, QuantizerParams)
+ FUZZER_CASE_HEADER(20, SqueezeParams)
+ FUZZER_CASE_HEADER(21, ToneMapping)
+ FUZZER_CASE_HEADER(22, Transform)
+ FUZZER_CASE_HEADER(23, YCbCrChromaSubsampling)
+
+ default: {
+ CustomTransformData transform_data;
+ transform_data.nonserialized_xyb_encoded = true;
+ (void)Bundle::Read(&reader, &transform_data);
+ break;
+ }
+ }
+ (void)reader.Close();
+
+ return 0;
+}
+
+} // namespace jxl
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return jxl::TestOneInput(data, size);
+}
diff --git a/media/libjxl/src/tools/flicker_test/CMakeLists.txt b/media/libjxl/src/tools/flicker_test/CMakeLists.txt
new file mode 100644
index 0000000000..efa4716a2e
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/CMakeLists.txt
@@ -0,0 +1,38 @@
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+find_package(Qt5 QUIET COMPONENTS Widgets)
+if (NOT Qt5_FOUND)
+ message(WARNING "Qt5 was not found. The flicker test tool will not be built.")
+ return()
+endif ()
+
+if (NOT TARGET icc_detect OR NOT TARGET image_loading)
+ message(WARNING "Comparison tool not built. The flicker test tool will not be built.")
+ return()
+endif ()
+
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOUIC ON)
+
+add_executable(flicker_test WIN32
+ main.cc
+ parameters.cc
+ parameters.h
+ setup.cc
+ setup.h
+ setup.ui
+ split_view.cc
+ split_view.h
+ test_window.cc
+ test_window.h
+ test_window.ui)
+
+target_link_libraries(flicker_test PUBLIC
+ Qt5::Widgets
+ image_loading
+ icc_detect
+)
diff --git a/media/libjxl/src/tools/flicker_test/main.cc b/media/libjxl/src/tools/flicker_test/main.cc
new file mode 100644
index 0000000000..67985a9638
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/main.cc
@@ -0,0 +1,22 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <QApplication>
+
+#include "tools/flicker_test/setup.h"
+#include "tools/flicker_test/test_window.h"
+
+int main(int argc, char** argv) {
+ QApplication application(argc, argv);
+
+ jxl::FlickerTestWizard wizard;
+ if (wizard.exec()) {
+ jxl::FlickerTestWindow test_window(wizard.parameters());
+ if (test_window.proceedWithTest()) {
+ test_window.showMaximized();
+ return application.exec();
+ }
+ }
+}
diff --git a/media/libjxl/src/tools/flicker_test/parameters.cc b/media/libjxl/src/tools/flicker_test/parameters.cc
new file mode 100644
index 0000000000..575edb0832
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/parameters.cc
@@ -0,0 +1,87 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/flicker_test/parameters.h"
+
+namespace jxl {
+
+namespace {
+
+constexpr char kPathsGroup[] = "paths";
+constexpr char kOriginalFolderKey[] = "originalFolder";
+constexpr char kAlteredFolderKey[] = "alteredFolder";
+constexpr char kOutputFileKey[] = "outputFile";
+
+constexpr char kTimingGroup[] = "timing";
+constexpr char kAdvanceTimeKey[] = "advanceTimeMSecs";
+constexpr char kViewingTimeKey[] = "viewingTimeSecs";
+constexpr char kBlankingTimeKey[] = "blankingTimeMSecs";
+constexpr char kGrayGroup[] = "gray";
+constexpr char kGrayKey[] = "enabled";
+constexpr char kGrayFadingTimeKey[] = "fadingTimeMSecs";
+constexpr char kGrayTimeKey[] = "timeMSecs";
+
+constexpr char kDisplayGroup[] = "display";
+constexpr char kIntensityTargetKey[] = "intensityTarget";
+constexpr char kSpacingKey[] = "spacing";
+
+} // namespace
+
+FlickerTestParameters FlickerTestParameters::loadFrom(
+ QSettings* const settings) {
+ FlickerTestParameters parameters;
+
+ settings->beginGroup(kPathsGroup);
+ parameters.originalFolder = settings->value(kOriginalFolderKey).toString();
+ parameters.alteredFolder = settings->value(kAlteredFolderKey).toString();
+ parameters.outputFile = settings->value(kOutputFileKey).toString();
+ settings->endGroup();
+
+ settings->beginGroup(kTimingGroup);
+ parameters.advanceTimeMSecs = settings->value(kAdvanceTimeKey, 100).toInt();
+ parameters.viewingTimeSecs = settings->value(kViewingTimeKey, 4).toInt();
+ parameters.blankingTimeMSecs = settings->value(kBlankingTimeKey, 250).toInt();
+ settings->beginGroup(kGrayGroup);
+ parameters.gray = settings->value(kGrayKey, false).toBool();
+ parameters.grayFadingTimeMSecs =
+ settings->value(kGrayFadingTimeKey, 100).toInt();
+ parameters.grayTimeMSecs = settings->value(kGrayTimeKey, 300).toInt();
+ settings->endGroup();
+ settings->endGroup();
+
+ settings->beginGroup(kDisplayGroup);
+ parameters.intensityTarget =
+ settings->value(kIntensityTargetKey, 250).toInt();
+ parameters.spacing = settings->value(kSpacingKey, 50).toInt();
+ settings->endGroup();
+
+ return parameters;
+}
+
+void FlickerTestParameters::saveTo(QSettings* const settings) const {
+ settings->beginGroup(kPathsGroup);
+ settings->setValue(kOriginalFolderKey, originalFolder);
+ settings->setValue(kAlteredFolderKey, alteredFolder);
+ settings->setValue(kOutputFileKey, outputFile);
+ settings->endGroup();
+
+ settings->beginGroup(kTimingGroup);
+ settings->setValue(kAdvanceTimeKey, advanceTimeMSecs);
+ settings->setValue(kViewingTimeKey, viewingTimeSecs);
+ settings->setValue(kBlankingTimeKey, blankingTimeMSecs);
+ settings->beginGroup(kGrayGroup);
+ settings->setValue(kGrayKey, gray);
+ settings->setValue(kGrayFadingTimeKey, grayFadingTimeMSecs);
+ settings->setValue(kGrayTimeKey, grayTimeMSecs);
+ settings->endGroup();
+ settings->endGroup();
+
+ settings->beginGroup(kDisplayGroup);
+ settings->setValue(kIntensityTargetKey, intensityTarget);
+ settings->setValue(kSpacingKey, spacing);
+ settings->endGroup();
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/flicker_test/parameters.h b/media/libjxl/src/tools/flicker_test/parameters.h
new file mode 100644
index 0000000000..a06399566d
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/parameters.h
@@ -0,0 +1,32 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_FLICKER_TEST_PARAMETERS_H_
+#define TOOLS_FLICKER_TEST_PARAMETERS_H_
+
+#include <QSettings>
+
+namespace jxl {
+
+struct FlickerTestParameters {
+ QString originalFolder;
+ QString alteredFolder;
+ QString outputFile;
+ int advanceTimeMSecs;
+ int viewingTimeSecs;
+ int blankingTimeMSecs;
+ bool gray;
+ int grayFadingTimeMSecs;
+ int grayTimeMSecs;
+ int intensityTarget;
+ int spacing;
+
+ static FlickerTestParameters loadFrom(QSettings* settings);
+ void saveTo(QSettings* settings) const;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_FLICKER_TEST_PARAMETERS_H_
diff --git a/media/libjxl/src/tools/flicker_test/setup.cc b/media/libjxl/src/tools/flicker_test/setup.cc
new file mode 100644
index 0000000000..bfcddd5fc0
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/setup.cc
@@ -0,0 +1,151 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/flicker_test/setup.h"
+
+#include <QCompleter>
+#include <QFileDialog>
+#include <QFileSystemModel>
+#include <QMessageBox>
+#include <QPushButton>
+
+namespace jxl {
+
+FlickerTestWizard::FlickerTestWizard(QWidget* const parent)
+ : QWizard(parent), settings_("JPEG XL project", "Flickering test") {
+ ui_.setupUi(this);
+
+ connect(ui_.grayFadingTime, SIGNAL(valueChanged(int)), this,
+ SLOT(updateTotalGrayTime()));
+ connect(ui_.grayTime, SIGNAL(valueChanged(int)), this,
+ SLOT(updateTotalGrayTime()));
+
+ ui_.timingButtonBox->button(QDialogButtonBox::RestoreDefaults)
+ ->setText(tr("Restore ISO/IEC 29170-2:2015 parameters"));
+
+ setButtonText(QWizard::FinishButton, tr("Start test"));
+
+ QCompleter* const completer = new QCompleter(this);
+ QFileSystemModel* const model = new QFileSystemModel(completer);
+ model->setRootPath("/");
+ model->setFilter(QDir::Dirs);
+ completer->setModel(model);
+ ui_.originalFolder->setCompleter(completer);
+ ui_.alteredFolder->setCompleter(completer);
+
+ const auto parameters = FlickerTestParameters::loadFrom(&settings_);
+ ui_.originalFolder->setText(parameters.originalFolder);
+ ui_.alteredFolder->setText(parameters.alteredFolder);
+ ui_.outputFile->setText(parameters.outputFile);
+ ui_.advanceTime->setValue(parameters.advanceTimeMSecs);
+ ui_.viewingTime->setValue(parameters.viewingTimeSecs);
+ ui_.blankingTime->setValue(parameters.blankingTimeMSecs);
+ ui_.grayFlickering->setChecked(parameters.gray);
+ ui_.grayFadingTime->setValue(parameters.grayFadingTimeMSecs);
+ ui_.grayTime->setValue(parameters.grayTimeMSecs);
+ ui_.intensityTarget->setValue(parameters.intensityTarget);
+ ui_.spacing->setValue(parameters.spacing);
+
+ QImage white(256, 256, QImage::Format_RGB32);
+ white.fill(Qt::white);
+ ui_.spacingDemo->setOriginalImage(white);
+ ui_.spacingDemo->setAlteredImage(white);
+
+ connect(this, &QDialog::accepted,
+ [&] { this->parameters().saveTo(&settings_); });
+}
+
+FlickerTestParameters FlickerTestWizard::parameters() const {
+ FlickerTestParameters result;
+ result.originalFolder = ui_.originalFolder->text();
+ result.alteredFolder = ui_.alteredFolder->text();
+ result.outputFile = ui_.outputFile->text();
+ result.advanceTimeMSecs = ui_.advanceTime->value();
+ result.viewingTimeSecs = ui_.viewingTime->value();
+ result.blankingTimeMSecs = ui_.blankingTime->value();
+ result.gray = ui_.grayFlickering->isChecked();
+ result.grayFadingTimeMSecs = ui_.grayFadingTime->value();
+ result.grayTimeMSecs = ui_.grayTime->value();
+ result.intensityTarget = ui_.intensityTarget->value();
+ result.spacing = ui_.spacing->value();
+ return result;
+}
+
+void FlickerTestWizard::on_originalFolderBrowseButton_clicked() {
+ const QString path = QFileDialog::getExistingDirectory(
+ this, tr("Folder with original images"), ui_.originalFolder->text());
+ if (!path.isEmpty()) {
+ ui_.originalFolder->setText(path);
+ }
+}
+
+void FlickerTestWizard::on_alteredFolderBrowseButton_clicked() {
+ const QString path = QFileDialog::getExistingDirectory(
+ this, tr("Folder with altered images"), ui_.alteredFolder->text());
+ if (!path.isEmpty()) {
+ ui_.alteredFolder->setText(path);
+ }
+}
+
+void FlickerTestWizard::on_outputFileBrowseButton_clicked() {
+ // The overwrite check is disabled here because it is carried out in
+ // `validateCurrentPage` (called when the user clicks the "Next" button) so
+ // that it also applies to automatically-reloaded settings.
+ const QString path = QFileDialog::getSaveFileName(
+ this, tr("CSV file in which to save the results"), ui_.outputFile->text(),
+ tr("CSV files (*.csv)"), /*selectedFilter=*/nullptr,
+ QFileDialog::DontConfirmOverwrite);
+ if (!path.isEmpty()) {
+ ui_.outputFile->setText(path);
+ }
+}
+
+void FlickerTestWizard::on_timingButtonBox_clicked(
+ QAbstractButton* const button) {
+ if (ui_.timingButtonBox->standardButton(button) ==
+ QDialogButtonBox::RestoreDefaults) {
+ ui_.advanceTime->setValue(100);
+ ui_.viewingTime->setValue(4);
+ ui_.blankingTime->setValue(250);
+ ui_.grayFlickering->setChecked(false);
+ }
+}
+
+void FlickerTestWizard::updateTotalGrayTime() {
+ ui_.totalGrayTimeLabel->setText(
+ tr("Total gray time: %L1&#8239;ms")
+ .arg(2 * ui_.grayFadingTime->value() + ui_.grayTime->value()));
+}
+
+bool FlickerTestWizard::validateCurrentPage() {
+ if (currentPage() == ui_.pathsPage && QFile::exists(ui_.outputFile->text())) {
+ QMessageBox messageBox(this);
+ messageBox.setIcon(QMessageBox::Warning);
+ messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
+ messageBox.setWindowTitle(tr("Output file already exists"));
+ messageBox.setText(tr("The selected output file \"%1\" already exists.")
+ .arg(ui_.outputFile->text()));
+ messageBox.setInformativeText(tr("Do you wish to overwrite it?"));
+ if (messageBox.exec() == QMessageBox::Cancel) {
+ return false;
+ }
+ } else if (currentPage() == ui_.timesPage) {
+ if (ui_.grayFlickering->isChecked() &&
+ 2 * ui_.grayFadingTime->value() + ui_.grayTime->value() >
+ ui_.advanceTime->value()) {
+ QMessageBox messageBox(this);
+ messageBox.setIcon(QMessageBox::Warning);
+ messageBox.setStandardButtons(QMessageBox::Ok);
+ messageBox.setWindowTitle(tr("Incompatible times selected"));
+ messageBox.setText(
+ tr("The total gray time is greater than the advance time."));
+ messageBox.exec();
+ return false;
+ }
+ }
+ return QWizard::validateCurrentPage();
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/flicker_test/setup.h b/media/libjxl/src/tools/flicker_test/setup.h
new file mode 100644
index 0000000000..0da78d60c8
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/setup.h
@@ -0,0 +1,44 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_FLICKER_TEST_SETUP_H_
+#define TOOLS_FLICKER_TEST_SETUP_H_
+
+#include <QWizard>
+
+#include "tools/flicker_test/parameters.h"
+#include "tools/flicker_test/ui_setup.h"
+
+namespace jxl {
+
+class FlickerTestWizard : public QWizard {
+ Q_OBJECT
+
+ public:
+ explicit FlickerTestWizard(QWidget* parent = nullptr);
+ ~FlickerTestWizard() override = default;
+
+ FlickerTestParameters parameters() const;
+
+ protected:
+ bool validateCurrentPage() override;
+
+ private slots:
+ void on_originalFolderBrowseButton_clicked();
+ void on_alteredFolderBrowseButton_clicked();
+ void on_outputFileBrowseButton_clicked();
+
+ void on_timingButtonBox_clicked(QAbstractButton* button);
+
+ void updateTotalGrayTime();
+
+ private:
+ Ui::FlickerTestWizard ui_;
+ QSettings settings_;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_FLICKER_TEST_SETUP_H_
diff --git a/media/libjxl/src/tools/flicker_test/setup.ui b/media/libjxl/src/tools/flicker_test/setup.ui
new file mode 100644
index 0000000000..055c7f750c
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/setup.ui
@@ -0,0 +1,422 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <comment>
+ Copyright (c) the JPEG XL Project Authors. All rights reserved.
+
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+ </comment>
+ <class>FlickerTestWizard</class>
+ <widget class="QWizard" name="FlickerTestWizard">
+ <property name="windowTitle">
+ <string>New flicker test</string>
+ </property>
+ <property name="options">
+ <set>QWizard::NoBackButtonOnStartPage</set>
+ </property>
+ <widget class="QWizardPage" name="pathsPage">
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="originalFolderPromptLabel">
+ <property name="text">
+ <string>Folder with the original images:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
+ <item>
+ <widget class="QLineEdit" name="originalFolder"/>
+ </item>
+ <item>
+ <widget class="QToolButton" name="originalFolderBrowseButton">
+ <property name="text">
+ <string>Browse…</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="alteredFolderPromptLabel">
+ <property name="text">
+ <string>Folder with the altered images:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0">
+ <item>
+ <widget class="QLineEdit" name="alteredFolder"/>
+ </item>
+ <item>
+ <widget class="QToolButton" name="alteredFolderBrowseButton">
+ <property name="text">
+ <string>Browse…</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="outputFilePromptLabel">
+ <property name="text">
+ <string>CSV file in which to save the results:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
+ <item>
+ <widget class="QLineEdit" name="outputFile"/>
+ </item>
+ <item>
+ <widget class="QToolButton" name="outputFileBrowseButton">
+ <property name="text">
+ <string>Browse…</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWizardPage" name="timesPage">
+ <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0,1">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,1">
+ <item>
+ <layout class="QFormLayout" name="formLayout_2">
+ <item row="0" column="0">
+ <widget class="QLabel" name="advanceTimePromptLabel">
+ <property name="text">
+ <string>Advance time:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="advanceTime">
+ <property name="suffix">
+ <string> ms</string>
+ </property>
+ <property name="minimum">
+ <number>100</number>
+ </property>
+ <property name="maximum">
+ <number>3000</number>
+ </property>
+ <property name="singleStep">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="viewingTimePromptLabel">
+ <property name="text">
+ <string>Viewing time (t&lt;sub&gt;VIEW&lt;/sub&gt;):</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="viewingTime">
+ <property name="specialValueText">
+ <string>no limit</string>
+ </property>
+ <property name="suffix">
+ <string> s</string>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>30</number>
+ </property>
+ <property name="value">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="blankingTimePromptLabel">
+ <property name="text">
+ <string>Blanking time (t&lt;sub&gt;BLANK&lt;/sub&gt;):</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="blankingTime">
+ <property name="suffix">
+ <string> ms</string>
+ </property>
+ <property name="minimum">
+ <number>50</number>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="singleStep">
+ <number>50</number>
+ </property>
+ <property name="value">
+ <number>250</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="grayFlickering">
+ <property name="title">
+ <string>Gray flickering</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <layout class="QFormLayout" name="formLayout_4">
+ <item row="0" column="0">
+ <widget class="QLabel" name="grayFadingTimePromptLabel">
+ <property name="text">
+ <string>Fading time to and from gray:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="grayFadingTime">
+ <property name="suffix">
+ <string> ms</string>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="singleStep">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="grayTimePromptLabel">
+ <property name="text">
+ <string>Time on gray:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="grayTime">
+ <property name="suffix">
+ <string> ms</string>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="singleStep">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>300</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="totalGrayTimeLabel">
+ <property name="text">
+ <string>Total gray time: 500 ms</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="timingButtonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::RestoreDefaults</set>
+ </property>
+ <property name="centerButtons">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWizardPage" name="intensityTargetPage">
+ <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="1,0,1">
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QFormLayout" name="formLayout_5">
+ <item row="0" column="0">
+ <widget class="QLabel" name="intensityTargetPromptLabel">
+ <property name="text">
+ <string>Display peak luminance:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="intensityTarget">
+ <property name="correctionMode">
+ <enum>QAbstractSpinBox::CorrectToNearestValue</enum>
+ </property>
+ <property name="suffix">
+ <string> cd/m²</string>
+ </property>
+ <property name="minimum">
+ <number>20</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="stepType">
+ <enum>QAbstractSpinBox::AdaptiveDecimalStepType</enum>
+ </property>
+ <property name="value">
+ <number>250</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWizardPage" name="spacingPage">
+ <layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0,0">
+ <item>
+ <widget class="jxl::SplitView" name="spacingDemo" native="true"/>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QFormLayout" name="formLayout_3">
+ <item row="0" column="0">
+ <widget class="QLabel" name="spacingPromptLabel">
+ <property name="text">
+ <string>Spacing between the images:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
+ <item>
+ <widget class="QSlider" name="spacing">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="value">
+ <number>50</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spacingSpinBox">
+ <property name="suffix">
+ <string> px</string>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="value">
+ <number>50</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>jxl::SplitView</class>
+ <extends>QWidget</extends>
+ <header>tools/flicker_test/split_view.h</header>
+ <container>1</container>
+ <slots>
+ <slot>setSpacing(int)</slot>
+ </slots>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>spacing</sender>
+ <signal>valueChanged(int)</signal>
+ <receiver>spacingDemo</receiver>
+ <slot>setSpacing(int)</slot>
+ </connection>
+ <connection>
+ <sender>spacing</sender>
+ <signal>valueChanged(int)</signal>
+ <receiver>spacingSpinBox</receiver>
+ <slot>setValue(int)</slot>
+ </connection>
+ <connection>
+ <sender>spacingSpinBox</sender>
+ <signal>valueChanged(int)</signal>
+ <receiver>spacing</receiver>
+ <slot>setValue(int)</slot>
+ </connection>
+ </connections>
+</ui>
diff --git a/media/libjxl/src/tools/flicker_test/split_view.cc b/media/libjxl/src/tools/flicker_test/split_view.cc
new file mode 100644
index 0000000000..3455d70bd7
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/split_view.cc
@@ -0,0 +1,167 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/flicker_test/split_view.h"
+
+#include <QMouseEvent>
+#include <QPainter>
+
+namespace jxl {
+
+SplitView::SplitView(QWidget* const parent)
+ : QWidget(parent), g_(std::random_device()()) {
+ blankingTimer_.setSingleShot(true);
+ blankingTimer_.setTimerType(Qt::PreciseTimer);
+ viewingTimer_.setSingleShot(true);
+ viewingTimer_.setTimerType(Qt::PreciseTimer);
+ flicker_.setLoopCount(-1);
+ connect(&blankingTimer_, &QTimer::timeout, this, &SplitView::startDisplaying);
+ connect(&flicker_, &QVariantAnimation::valueChanged, this, [&] {
+ if (gray_) {
+ update();
+ }
+ });
+ connect(&flicker_, &QAbstractAnimation::currentLoopChanged, [&] {
+ showingAltered_ = !showingAltered_;
+ update();
+ });
+ connect(&viewingTimer_, &QTimer::timeout, [&] {
+ flicker_.stop();
+ original_.fill(Qt::black);
+ altered_.fill(Qt::black);
+ update();
+ });
+}
+
+void SplitView::setOriginalImage(QImage image) {
+ original_ = QPixmap::fromImage(std::move(image));
+ updateMinimumSize();
+ update();
+}
+
+void SplitView::setAlteredImage(QImage image) {
+ altered_ = QPixmap::fromImage(std::move(image));
+ updateMinimumSize();
+ update();
+}
+
+void SplitView::setSpacing(int spacing) {
+ spacing_ = spacing;
+ updateMinimumSize();
+ update();
+}
+
+void SplitView::startTest(QString imageName, const int blankingTimeMSecs,
+ const int viewingTimeSecs, const int advanceTimeMSecs,
+ const bool gray, const int grayFadingTimeMSecs,
+ const int grayTimeMSecs) {
+ imageName_ = std::move(imageName);
+ std::bernoulli_distribution bernoulli;
+ originalSide_ = bernoulli(g_) ? Side::kLeft : Side::kRight;
+ viewingTimer_.setInterval(1000 * viewingTimeSecs);
+
+ flicker_.setDuration(advanceTimeMSecs);
+ gray_ = gray;
+ QVariantAnimation::KeyValues keyValues;
+ if (gray_) {
+ keyValues << QVariantAnimation::KeyValue(0., 0.f)
+ << QVariantAnimation::KeyValue(
+ static_cast<float>(grayFadingTimeMSecs) / advanceTimeMSecs,
+ 1.f)
+ << QVariantAnimation::KeyValue(
+ static_cast<float>(advanceTimeMSecs - grayTimeMSecs -
+ grayFadingTimeMSecs) /
+ advanceTimeMSecs,
+ 1.f)
+ << QVariantAnimation::KeyValue(
+ static_cast<float>(advanceTimeMSecs - grayTimeMSecs) /
+ advanceTimeMSecs,
+ 0.f)
+ << QVariantAnimation::KeyValue(1.f, 0.f);
+ } else {
+ keyValues << QVariantAnimation::KeyValue(0., 1.f)
+ << QVariantAnimation::KeyValue(1., 1.f);
+ }
+ flicker_.setKeyValues(keyValues);
+
+ state_ = State::kBlanking;
+ blankingTimer_.start(blankingTimeMSecs);
+}
+
+void SplitView::mousePressEvent(QMouseEvent* const event) {
+ if (state_ != State::kDisplaying) return;
+
+ if (leftRect_.contains(event->pos())) {
+ clicking_ = true;
+ clickedSide_ = Side::kLeft;
+ } else if (rightRect_.contains(event->pos())) {
+ clicking_ = true;
+ clickedSide_ = Side::kRight;
+ }
+}
+
+void SplitView::mouseReleaseEvent(QMouseEvent* const event) {
+ if (!clicking_) return;
+ clicking_ = false;
+
+ const int clickDelayMSecs = viewingStartTime_.elapsed();
+
+ if ((clickedSide_ == Side::kLeft && !leftRect_.contains(event->pos())) ||
+ (clickedSide_ == Side::kRight && !rightRect_.contains(event->pos()))) {
+ return;
+ }
+
+ flicker_.stop();
+ viewingTimer_.stop();
+ state_ = State::kBlanking;
+ update();
+
+ emit testResult(imageName_, originalSide_, clickedSide_, clickDelayMSecs);
+}
+
+void SplitView::paintEvent(QPaintEvent* const event) {
+ QPainter painter(this);
+ painter.fillRect(rect(), QColor(119, 119, 119));
+
+ if (state_ == State::kBlanking) return;
+
+ if (gray_ && flicker_.state() == QAbstractAnimation::Running) {
+ painter.setOpacity(flicker_.currentValue().toFloat());
+ }
+
+ const auto imageForSide = [&](const Side side) {
+ if (side == originalSide_) return &original_;
+ return showingAltered_ ? &altered_ : &original_;
+ };
+
+ QPixmap* const leftImage = imageForSide(Side::kLeft);
+ QPixmap* const rightImage = imageForSide(Side::kRight);
+
+ leftRect_ = leftImage->rect();
+ leftRect_.moveCenter(rect().center());
+ leftRect_.moveRight(rect().center().x() - spacing_ / 2 - spacing_ % 2);
+ painter.drawPixmap(leftRect_, *leftImage);
+
+ rightRect_ = rightImage->rect();
+ rightRect_.moveCenter(rect().center());
+ rightRect_.moveLeft(rect().center().x() + 1 + spacing_ / 2);
+ painter.drawPixmap(rightRect_, *rightImage);
+}
+
+void SplitView::startDisplaying() {
+ state_ = State::kDisplaying;
+ flicker_.start();
+ viewingStartTime_.start();
+ if (viewingTimer_.interval() > 0) {
+ viewingTimer_.start();
+ }
+}
+
+void SplitView::updateMinimumSize() {
+ setMinimumWidth(2 * std::max(original_.width(), altered_.width()) + spacing_);
+ setMinimumHeight(std::max(original_.height(), altered_.height()));
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/flicker_test/split_view.h b/media/libjxl/src/tools/flicker_test/split_view.h
new file mode 100644
index 0000000000..b4c7a1d8de
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/split_view.h
@@ -0,0 +1,84 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_FLICKER_TEST_SPLIT_VIEW_H_
+#define TOOLS_FLICKER_TEST_SPLIT_VIEW_H_
+
+#include <QElapsedTimer>
+#include <QImage>
+#include <QPixmap>
+#include <QTimer>
+#include <QVariantAnimation>
+#include <QWidget>
+#include <random>
+
+namespace jxl {
+
+class SplitView : public QWidget {
+ Q_OBJECT
+
+ public:
+ enum class Side {
+ kLeft,
+ kRight,
+ };
+ Q_ENUM(Side)
+
+ explicit SplitView(QWidget* parent = nullptr);
+ ~SplitView() override = default;
+
+ void setOriginalImage(QImage image);
+ void setAlteredImage(QImage image);
+
+ signals:
+ void testResult(const QString& imageName, Side flickeringSide,
+ Side clickedSide, int clickDelayMSecs);
+
+ public slots:
+ void setSpacing(int spacing);
+ void startTest(QString imageName, int blankingTimeMSecs, int viewingTimeSecs,
+ int advanceTimeMSecs, bool gray, int grayFadingTimeMSecs,
+ int grayTimeMSecs);
+
+ protected:
+ void mousePressEvent(QMouseEvent* event) override;
+ void mouseReleaseEvent(QMouseEvent* event) override;
+ void paintEvent(QPaintEvent* event) override;
+
+ private slots:
+ void startDisplaying();
+
+ private:
+ enum class State {
+ kBlanking,
+ kDisplaying,
+ };
+
+ void updateMinimumSize();
+
+ int spacing_ = 50;
+
+ std::mt19937 g_;
+
+ QString imageName_;
+ QPixmap original_, altered_;
+ Side originalSide_;
+ bool clicking_ = false;
+ Side clickedSide_;
+ QRect leftRect_, rightRect_;
+ State state_ = State::kDisplaying;
+ bool gray_ = false;
+ QTimer blankingTimer_;
+ QTimer viewingTimer_;
+ // Throughout each cycle, animates the opacity of the image being displayed
+ // between 0 and 1 if fading to gray is enabled.
+ QVariantAnimation flicker_;
+ bool showingAltered_ = true;
+ QElapsedTimer viewingStartTime_;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_FLICKER_TEST_SPLIT_VIEW_H_
diff --git a/media/libjxl/src/tools/flicker_test/test_window.cc b/media/libjxl/src/tools/flicker_test/test_window.cc
new file mode 100644
index 0000000000..f3827c56d0
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/test_window.cc
@@ -0,0 +1,184 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/flicker_test/test_window.h"
+
+#include <QDir>
+#include <QMessageBox>
+#include <QSet>
+#include <algorithm>
+#include <random>
+
+#include "tools/icc_detect/icc_detect.h"
+
+namespace jxl {
+
+FlickerTestWindow::FlickerTestWindow(FlickerTestParameters parameters,
+ QWidget* const parent)
+ : QMainWindow(parent),
+ monitorProfile_(GetMonitorIccProfile(this)),
+ parameters_(std::move(parameters)),
+ originalFolder_(parameters_.originalFolder, "*.png"),
+ alteredFolder_(parameters_.alteredFolder, "*.png"),
+ outputFile_(parameters_.outputFile) {
+ ui_.setupUi(this);
+ ui_.splitView->setSpacing(parameters_.spacing);
+ ui_.endLabel->setText(
+ tr("The test is complete and the results have been saved to \"%1\".")
+ .arg(parameters_.outputFile));
+ connect(ui_.startButton, &QAbstractButton::clicked, [&] {
+ ui_.stackedView->setCurrentWidget(ui_.splitView);
+ nextImage();
+ });
+ connect(ui_.splitView, &SplitView::testResult, this,
+ &FlickerTestWindow::processTestResult);
+
+ if (!outputFile_.open(QIODevice::WriteOnly)) {
+ QMessageBox messageBox;
+ messageBox.setIcon(QMessageBox::Critical);
+ messageBox.setStandardButtons(QMessageBox::Close);
+ messageBox.setWindowTitle(tr("Failed to open output file"));
+ messageBox.setInformativeText(
+ tr("Could not open \"%1\" for writing.").arg(outputFile_.fileName()));
+ messageBox.exec();
+ proceed_ = false;
+ return;
+ }
+ outputStream_.setDevice(&outputFile_);
+ outputStream_ << "image name,original side,clicked side,click delay (ms)\n";
+
+ if (monitorProfile_.isEmpty()) {
+ QMessageBox messageBox;
+ messageBox.setIcon(QMessageBox::Warning);
+ messageBox.setStandardButtons(QMessageBox::Ok);
+ messageBox.setWindowTitle(tr("No monitor profile found"));
+ messageBox.setText(
+ tr("No ICC profile appears to be associated with the display. It will "
+ "be assumed to match sRGB."));
+ messageBox.exec();
+ }
+
+ originalFolder_.setFilter(QDir::Files);
+ alteredFolder_.setFilter(QDir::Files);
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ auto originalImages = QSet<QString>::fromList(originalFolder_.entryList());
+ auto alteredImages = QSet<QString>::fromList(alteredFolder_.entryList());
+#else
+ const QStringList originalFolderEntries = originalFolder_.entryList();
+ QSet<QString> originalImages(originalFolderEntries.begin(),
+ originalFolderEntries.end());
+ const QStringList alteredFolderEntries = alteredFolder_.entryList();
+ QSet<QString> alteredImages(alteredFolderEntries.begin(),
+ alteredFolderEntries.end());
+#endif
+
+ auto onlyOriginal = originalImages - alteredImages,
+ onlyAltered = alteredImages - originalImages;
+ if (!onlyOriginal.isEmpty() || !onlyAltered.isEmpty()) {
+ QMessageBox messageBox;
+ messageBox.setIcon(QMessageBox::Warning);
+ messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
+ messageBox.setWindowTitle(tr("Image set mismatch"));
+ messageBox.setText(
+ tr("A mismatch has been detected between the original and altered "
+ "images."));
+ messageBox.setInformativeText(tr("Proceed with the test?"));
+ QStringList detailedTextParagraphs;
+ const QString itemFormat = tr("— %1\n");
+ if (!onlyOriginal.isEmpty()) {
+ QString originalList;
+ for (const QString& original : onlyOriginal) {
+ originalList += itemFormat.arg(original);
+ }
+ detailedTextParagraphs << tr("The following images were only found in "
+ "the originals folder:\n%1")
+ .arg(originalList);
+ }
+ if (!onlyAltered.isEmpty()) {
+ QString alteredList;
+ for (const QString& altered : onlyAltered) {
+ alteredList += itemFormat.arg(altered);
+ }
+ detailedTextParagraphs << tr("The following images were only found in "
+ "the altered images folder:\n%1")
+ .arg(alteredList);
+ }
+ messageBox.setDetailedText(detailedTextParagraphs.join("\n\n"));
+ if (messageBox.exec() == QMessageBox::Cancel) {
+ proceed_ = false;
+ return;
+ }
+ }
+
+ remainingImages_ = originalImages.intersect(alteredImages).values();
+ std::random_device rd;
+ std::mt19937 g(rd());
+ std::shuffle(remainingImages_.begin(), remainingImages_.end(), g);
+}
+
+void FlickerTestWindow::processTestResult(const QString& imageName,
+ const SplitView::Side originalSide,
+ const SplitView::Side clickedSide,
+ const int clickDelayMSecs) {
+ const auto sideToString = [](const SplitView::Side side) {
+ switch (side) {
+ case SplitView::Side::kLeft:
+ return "left";
+
+ case SplitView::Side::kRight:
+ return "right";
+ }
+ return "unknown";
+ };
+ outputStream_ << imageName << "," << sideToString(originalSide) << ","
+ << sideToString(clickedSide) << "," << clickDelayMSecs << "\n";
+
+ nextImage();
+}
+
+void FlickerTestWindow::nextImage() {
+ if (remainingImages_.empty()) {
+ outputStream_.flush();
+ ui_.stackedView->setCurrentWidget(ui_.finalPage);
+ return;
+ }
+ const QString image = remainingImages_.takeFirst();
+retry:
+ QImage originalImage =
+ loadImage(originalFolder_.absoluteFilePath(image), monitorProfile_,
+ parameters_.intensityTarget);
+ QImage alteredImage = loadImage(alteredFolder_.absoluteFilePath(image),
+ monitorProfile_, parameters_.intensityTarget);
+ if (originalImage.isNull() || alteredImage.isNull()) {
+ QMessageBox messageBox(this);
+ messageBox.setIcon(QMessageBox::Warning);
+ messageBox.setStandardButtons(QMessageBox::Retry | QMessageBox::Ignore |
+ QMessageBox::Abort);
+ messageBox.setWindowTitle(tr("Failed to load image"));
+ messageBox.setText(tr("Could not load image \"%1\".").arg(image));
+ switch (messageBox.exec()) {
+ case QMessageBox::Retry:
+ goto retry;
+
+ case QMessageBox::Ignore:
+ outputStream_ << image << ",,,\n";
+ return nextImage();
+
+ case QMessageBox::Abort:
+ ui_.stackedView->setCurrentWidget(ui_.finalPage);
+ return;
+ }
+ }
+
+ ui_.splitView->setOriginalImage(std::move(originalImage));
+ ui_.splitView->setAlteredImage(std::move(alteredImage));
+ ui_.splitView->startTest(
+ image, parameters_.blankingTimeMSecs, parameters_.viewingTimeSecs,
+ parameters_.advanceTimeMSecs, parameters_.gray,
+ parameters_.grayFadingTimeMSecs, parameters_.grayTimeMSecs);
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/flicker_test/test_window.h b/media/libjxl/src/tools/flicker_test/test_window.h
new file mode 100644
index 0000000000..1dfe5fca8b
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/test_window.h
@@ -0,0 +1,50 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_FLICKER_TEST_TEST_WINDOW_H_
+#define TOOLS_FLICKER_TEST_TEST_WINDOW_H_
+
+#include <QByteArray>
+#include <QDir>
+#include <QMainWindow>
+#include <QStringList>
+#include <QTextStream>
+
+#include "tools/comparison_viewer/image_loading.h"
+#include "tools/flicker_test/parameters.h"
+#include "tools/flicker_test/ui_test_window.h"
+
+namespace jxl {
+
+class FlickerTestWindow : public QMainWindow {
+ Q_OBJECT
+
+ public:
+ explicit FlickerTestWindow(FlickerTestParameters parameters,
+ QWidget* parent = nullptr);
+ ~FlickerTestWindow() override = default;
+
+ bool proceedWithTest() const { return proceed_; }
+
+ private slots:
+ void processTestResult(const QString& imageName, SplitView::Side originalSide,
+ SplitView::Side clickedSide, int clickDelayMSecs);
+
+ private:
+ void nextImage();
+
+ Ui::FlickerTestWindow ui_;
+ bool proceed_ = true;
+ const QByteArray monitorProfile_;
+ FlickerTestParameters parameters_;
+ QDir originalFolder_, alteredFolder_;
+ QFile outputFile_;
+ QTextStream outputStream_;
+ QStringList remainingImages_;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_FLICKER_TEST_TEST_WINDOW_H_
diff --git a/media/libjxl/src/tools/flicker_test/test_window.ui b/media/libjxl/src/tools/flicker_test/test_window.ui
new file mode 100644
index 0000000000..7eb26196fe
--- /dev/null
+++ b/media/libjxl/src/tools/flicker_test/test_window.ui
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <comment>
+ Copyright (c) the JPEG XL Project Authors. All rights reserved.
+
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+ </comment>
+ <class>FlickerTestWindow</class>
+ <widget class="QMainWindow" name="FlickerTestWindow">
+ <property name="windowTitle">
+ <string>Flicker test</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QStackedWidget" name="stackedView">
+ <widget class="QWidget" name="startPage">
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,1">
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,1">
+ <item>
+ <spacer name="spacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="startButton">
+ <property name="text">
+ <string>Start</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="spacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="jxl::SplitView" name="splitView"/>
+ <widget class="QWidget" name="finalPage">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0,1">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="endLabel">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignJustify|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>jxl::SplitView</class>
+ <extends>QWidget</extends>
+ <header>tools/flicker_test/split_view.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/media/libjxl/src/tools/fuzzer_corpus.cc b/media/libjxl/src/tools/fuzzer_corpus.cc
new file mode 100644
index 0000000000..0d66bd816e
--- /dev/null
+++ b/media/libjxl/src/tools/fuzzer_corpus.cc
@@ -0,0 +1,473 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#if defined(_WIN32) || defined(_WIN64)
+#include "third_party/dirent.h"
+#else
+#include <dirent.h>
+#include <unistd.h>
+#endif
+
+#include <algorithm>
+#include <functional>
+#include <iostream>
+#include <mutex>
+#include <random>
+#include <vector>
+
+#if JPEGXL_ENABLE_JPEG
+#include "lib/extras/enc/jpg.h"
+#endif
+#include "lib/jxl/aux_out.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/enc_ans.h"
+#include "lib/jxl/enc_cache.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_external_image.h"
+#include "lib/jxl/enc_file.h"
+#include "lib/jxl/enc_params.h"
+#include "lib/jxl/encode_internal.h"
+#include "lib/jxl/jpeg/enc_jpeg_data.h"
+#include "lib/jxl/modular/encoding/context_predict.h"
+
+namespace {
+
+const size_t kMaxWidth = 50000;
+const size_t kMaxHeight = 50000;
+const size_t kMaxPixels = 20 * (1 << 20); // 20 MP
+const size_t kMaxBitDepth = 24; // The maximum reasonable bit depth supported.
+
+std::mutex stderr_mutex;
+
+typedef std::function<uint8_t()> PixelGenerator;
+
+// ImageSpec needs to be a packed struct to allow us to use the raw memory of
+// the struct for hashing to create a consistent.
+#pragma pack(push, 1)
+struct ImageSpec {
+ bool Validate() const {
+ if (width > kMaxWidth || height > kMaxHeight ||
+ width * height > kMaxPixels) {
+ return false;
+ }
+ if (bit_depth > kMaxBitDepth || bit_depth == 0) return false;
+ if (num_frames == 0) return false;
+ // JPEG doesn't support all formats, so reconstructible JPEG isn't always
+ // valid.
+ if (is_reconstructible_jpeg && (bit_depth != 8 || num_channels != 3 ||
+ alpha_bit_depth != 0 || num_frames != 1))
+ return false;
+ return true;
+ }
+
+ friend std::ostream& operator<<(std::ostream& o, const ImageSpec& spec) {
+ o << "ImageSpec<"
+ << "size=" << spec.width << "x" << spec.height
+ << " * chan=" << spec.num_channels << " depth=" << spec.bit_depth
+ << " alpha=" << spec.alpha_bit_depth
+ << " (premult=" << spec.alpha_is_premultiplied
+ << ") x frames=" << spec.num_frames << " seed=" << spec.seed
+ << ", speed=" << static_cast<int>(spec.params.speed_tier)
+ << ", butteraugli=" << spec.params.butteraugli_distance
+ << ", modular_mode=" << spec.params.modular_mode
+ << ", lossy_palette=" << spec.params.lossy_palette
+ << ", noise=" << spec.params.noise << ", preview=" << spec.params.preview
+ << ", fuzzer_friendly=" << spec.fuzzer_friendly
+ << ", is_reconstructible_jpeg=" << spec.is_reconstructible_jpeg
+ << ", orientation=" << static_cast<int>(spec.orientation) << ">";
+ return o;
+ }
+
+ void SpecHash(uint8_t hash[16]) const {
+ const uint8_t* from = reinterpret_cast<const uint8_t*>(this);
+ std::seed_seq hasher(from, from + sizeof(*this));
+ uint32_t* to = reinterpret_cast<uint32_t*>(hash);
+ hasher.generate(to, to + 4);
+ }
+
+ uint64_t width = 256;
+ uint64_t height = 256;
+ // Number of channels *not* including alpha.
+ uint64_t num_channels = 3;
+ uint64_t bit_depth = 8;
+ // Bit depth for the alpha channel. A value of 0 means no alpha channel.
+ uint64_t alpha_bit_depth = 8;
+ int32_t alpha_is_premultiplied = false;
+
+ // Whether the ANS fuzzer friendly setting is currently enabled.
+ uint32_t fuzzer_friendly = false;
+
+ // Number of frames, all the frames will have the same size.
+ uint64_t num_frames = 1;
+
+ // The seed for the PRNG.
+ uint32_t seed = 7777;
+
+ // Flags used for compression. These are mapped to the CompressedParams.
+ struct CjxlParams {
+ float butteraugli_distance = 1.f;
+ // Must not use Weighted - see force_no_wp
+ jxl::Predictor modular_predictor = jxl::Predictor::Gradient;
+ jxl::ColorTransform color_transform = jxl::ColorTransform::kXYB;
+ jxl::SpeedTier speed_tier = jxl::SpeedTier::kTortoise;
+ bool modular_mode = false;
+ bool lossy_palette = false;
+ bool noise = false;
+ bool preview = false;
+ // CjxlParams is packed; re-add padding when sum of sizes of members is not
+ // multiple of 4.
+ // uint8_t padding_[0] = {};
+ } params;
+
+ uint32_t is_reconstructible_jpeg = false;
+ // Use 0xFFFFFFFF if any random spec is good; otherwise set the desired value.
+ uint32_t override_decoder_spec = 0xFFFFFFFF;
+ // Orientation.
+ uint8_t orientation = 0;
+ uint8_t padding_[3] = {};
+};
+#pragma pack(pop)
+static_assert(sizeof(ImageSpec) % 4 == 0, "Add padding to ImageSpec.");
+
+bool GenerateFile(const char* output_dir, const ImageSpec& spec,
+ bool regenerate, bool quiet) {
+ // Compute a checksum of the ImageSpec to name the file. This is just to keep
+ // the output of this program repeatable.
+ uint8_t checksum[16];
+ spec.SpecHash(checksum);
+ std::string hash_str(sizeof(checksum) * 2, ' ');
+ static const char* hex_chars = "0123456789abcdef";
+ for (size_t i = 0; i < sizeof(checksum); i++) {
+ hash_str[2 * i] = hex_chars[checksum[i] >> 4];
+ hash_str[2 * i + 1] = hex_chars[checksum[i] % 0x0f];
+ }
+ std::string output_fn = std::string(output_dir) + "/" + hash_str + ".jxl";
+
+ // Don't regenerate files if they already exist on disk to speed-up
+ // consecutive calls when --regenerate is not used.
+ struct stat st;
+ if (!regenerate && stat(output_fn.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
+ return true;
+ }
+
+ if (!quiet) {
+ std::unique_lock<std::mutex> lock(stderr_mutex);
+ std::cerr << "Generating " << spec << " as " << hash_str << std::endl;
+ }
+
+ jxl::CodecInOut io;
+ if (spec.bit_depth == 32) {
+ io.metadata.m.SetFloat32Samples();
+ } else {
+ io.metadata.m.SetUintSamples(spec.bit_depth);
+ }
+ io.metadata.m.SetAlphaBits(spec.alpha_bit_depth, spec.alpha_is_premultiplied);
+ io.metadata.m.orientation = spec.orientation;
+ io.dec_pixels = spec.width * spec.height;
+ io.frames.clear();
+ io.frames.reserve(spec.num_frames);
+
+ jxl::ColorEncoding c;
+ if (spec.num_channels == 1) {
+ c = jxl::ColorEncoding::LinearSRGB(true);
+ } else if (spec.num_channels == 3) {
+ c = jxl::ColorEncoding::SRGB();
+ }
+
+ uint8_t hash[16];
+ spec.SpecHash(hash);
+ std::mt19937 mt(spec.seed);
+
+ // Compress the image.
+ jxl::PaddedBytes compressed;
+
+ std::uniform_int_distribution<> dis(1, 6);
+ PixelGenerator gen = [&]() -> uint8_t { return dis(mt); };
+
+ for (uint32_t frame = 0; frame < spec.num_frames; frame++) {
+ jxl::ImageBundle ib(&io.metadata.m);
+ const bool has_alpha = spec.alpha_bit_depth != 0;
+ const size_t bytes_per_sample =
+ jxl::DivCeil(io.metadata.m.bit_depth.bits_per_sample, 8);
+ const size_t bytes_per_pixel =
+ bytes_per_sample *
+ (io.metadata.m.color_encoding.Channels() + has_alpha);
+ const size_t row_size = spec.width * bytes_per_pixel;
+ std::vector<uint8_t> img_data(row_size * spec.height, 0);
+ for (size_t y = 0; y < spec.height; y++) {
+ size_t pos = row_size * y;
+ for (size_t x = 0; x < spec.width; x++) {
+ for (size_t b = 0; b < bytes_per_pixel; b++) {
+ img_data[pos++] = gen();
+ }
+ }
+ }
+
+ const jxl::Span<const uint8_t> span(img_data.data(), img_data.size());
+ JXL_RETURN_IF_ERROR(ConvertFromExternal(
+ span, spec.width, spec.height, io.metadata.m.color_encoding,
+ bytes_per_pixel / bytes_per_sample,
+ /*alpha_is_premultiplied=*/spec.alpha_is_premultiplied,
+ io.metadata.m.bit_depth.bits_per_sample, JXL_LITTLE_ENDIAN,
+ false /* flipped_y */, nullptr, &ib, /*float_in=*/false, /*align=*/0));
+ io.frames.push_back(std::move(ib));
+ }
+
+ jxl::CompressParams params;
+ params.speed_tier = spec.params.speed_tier;
+
+#if JPEGXL_ENABLE_JPEG
+ if (spec.is_reconstructible_jpeg) {
+ // If this image is supposed to be a reconstructible JPEG, collect the JPEG
+ // metadata and encode it in the beginning of the compressed bytes.
+ jxl::PaddedBytes jpeg_bytes;
+ JXL_RETURN_IF_ERROR(EncodeImageJPG(
+ &io, jxl::extras::JpegEncoder::kLibJpeg, /*quality=*/70,
+ jxl::YCbCrChromaSubsampling(), /*pool=*/nullptr, &jpeg_bytes));
+ JXL_RETURN_IF_ERROR(jxl::jpeg::DecodeImageJPG(
+ jxl::Span<const uint8_t>(jpeg_bytes.data(), jpeg_bytes.size()), &io));
+ jxl::PaddedBytes jpeg_data;
+ JXL_RETURN_IF_ERROR(
+ EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data, params));
+ std::vector<uint8_t> header;
+ header.insert(header.end(), jxl::kContainerHeader,
+ jxl::kContainerHeader + sizeof(jxl::kContainerHeader));
+ jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false,
+ &header);
+ header.insert(header.end(), jpeg_data.data(),
+ jpeg_data.data() + jpeg_data.size());
+ jxl::AppendBoxHeader(jxl::MakeBoxType("jxlc"), 0, true, &header);
+ compressed.append(header);
+ }
+#endif
+
+ params.modular_mode = spec.params.modular_mode;
+ params.color_transform = spec.params.color_transform;
+ params.butteraugli_distance = spec.params.butteraugli_distance;
+ params.options.predictor = {spec.params.modular_predictor};
+ params.lossy_palette = spec.params.lossy_palette;
+ if (spec.params.preview) params.preview = jxl::Override::kOn;
+ if (spec.params.noise) params.noise = jxl::Override::kOn;
+
+ jxl::AuxOut aux_out;
+ jxl::PassesEncoderState passes_encoder_state;
+ // EncodeFile replaces output; pass a temporary storage for it.
+ jxl::PaddedBytes compressed_image;
+ bool ok =
+ jxl::EncodeFile(params, &io, &passes_encoder_state, &compressed_image,
+ jxl::GetJxlCms(), &aux_out, nullptr);
+ if (!ok) return false;
+ compressed.append(compressed_image);
+
+ // Append 4 bytes with the flags used by djxl_fuzzer to select the decoding
+ // output.
+ std::uniform_int_distribution<> dis256(0, 255);
+ if (spec.override_decoder_spec == 0xFFFFFFFF) {
+ for (size_t i = 0; i < 4; ++i) compressed.push_back(dis256(mt));
+ } else {
+ for (size_t i = 0; i < 4; ++i) {
+ compressed.push_back(spec.override_decoder_spec >> (8 * i));
+ }
+ }
+
+ if (!jxl::WriteFile(compressed, output_fn)) return 1;
+ if (!quiet) {
+ std::unique_lock<std::mutex> lock(stderr_mutex);
+ std::cerr << "Stored " << output_fn << " size: " << compressed.size()
+ << std::endl;
+ }
+
+ return true;
+}
+
+std::vector<ImageSpec::CjxlParams> CompressParamsList() {
+ std::vector<ImageSpec::CjxlParams> ret;
+
+ {
+ ImageSpec::CjxlParams params;
+ params.butteraugli_distance = 1.5;
+ ret.push_back(params);
+ }
+
+ {
+ // Lossless
+ ImageSpec::CjxlParams params;
+ params.modular_mode = true;
+ params.color_transform = jxl::ColorTransform::kNone;
+ params.butteraugli_distance = 0.f;
+ params.modular_predictor = {jxl::Predictor::Weighted};
+ ret.push_back(params);
+ }
+
+ return ret;
+}
+
+void Usage() {
+ fprintf(stderr,
+ "Use: fuzzer_corpus [-r] [-q] [-j THREADS] [output_dir]\n"
+ "\n"
+ " -r Regenerate files if already exist.\n"
+ " -q Be quiet.\n"
+ " -j THREADS Number of parallel jobs to run.\n");
+}
+
+} // namespace
+
+int main(int argc, const char** argv) {
+ const char* dest_dir = nullptr;
+ bool regenerate = false;
+ bool quiet = false;
+ int num_threads = std::thread::hardware_concurrency();
+ for (int optind = 1; optind < argc;) {
+ if (!strcmp(argv[optind], "-r")) {
+ regenerate = true;
+ optind++;
+ } else if (!strcmp(argv[optind], "-q")) {
+ quiet = true;
+ optind++;
+ } else if (!strcmp(argv[optind], "-j")) {
+ optind++;
+ if (optind < argc) {
+ num_threads = atoi(argv[optind++]);
+ } else {
+ fprintf(stderr, "-j needs an argument value.\n");
+ Usage();
+ return 1;
+ }
+ } else if (dest_dir == nullptr) {
+ dest_dir = argv[optind++];
+ } else {
+ fprintf(stderr, "Unknown parameter: \"%s\".\n", argv[optind]);
+ Usage();
+ return 1;
+ }
+ }
+ if (!dest_dir) {
+ dest_dir = "corpus";
+ }
+
+ struct stat st;
+ memset(&st, 0, sizeof(st));
+ if (stat(dest_dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
+ fprintf(stderr, "Output path \"%s\" is not a directory.\n", dest_dir);
+ Usage();
+ return 1;
+ }
+
+ // Create the corpus directory if doesn't already exist.
+ std::mt19937 mt(77777);
+
+ std::vector<std::pair<uint32_t, uint32_t>> image_sizes = {
+ {8, 8},
+ {32, 32},
+ {128, 128},
+ // Degenerated cases.
+ {10000, 1},
+ {10000, 2},
+ {1, 10000},
+ {2, 10000},
+ // Large case.
+ {555, 256},
+ {257, 513},
+ };
+ const std::vector<ImageSpec::CjxlParams> params_list = CompressParamsList();
+
+ ImageSpec spec;
+ // The ans_fuzzer_friendly setting is not thread safe and therefore done in
+ // an outer loop. This determines whether to use fuzzer-friendly ANS encoding.
+ for (uint32_t fuzzer_friendly = 0; fuzzer_friendly < 2; ++fuzzer_friendly) {
+ jxl::SetANSFuzzerFriendly(fuzzer_friendly);
+ spec.fuzzer_friendly = fuzzer_friendly;
+
+ std::vector<ImageSpec> specs;
+ for (auto img_size : image_sizes) {
+ spec.width = img_size.first;
+ spec.height = img_size.second;
+ for (uint32_t bit_depth : {1, 2, 8, 16}) {
+ spec.bit_depth = bit_depth;
+ for (uint32_t num_channels : {1, 3}) {
+ spec.num_channels = num_channels;
+ for (uint32_t alpha_bit_depth : {0, 8, 16}) {
+ spec.alpha_bit_depth = alpha_bit_depth;
+ if (bit_depth == 16 && alpha_bit_depth == 8) {
+ // This mode is not supported in CopyTo().
+ continue;
+ }
+ for (uint32_t num_frames : {1, 3}) {
+ spec.num_frames = num_frames;
+ for (uint32_t preview : {0, 1}) {
+#if JPEGXL_ENABLE_JPEG
+ for (bool reconstructible_jpeg : {false, true}) {
+ spec.is_reconstructible_jpeg = reconstructible_jpeg;
+#else // JPEGXL_ENABLE_JPEG
+ spec.is_reconstructible_jpeg = false;
+#endif // JPEGXL_ENABLE_JPEG
+ for (const auto& params : params_list) {
+ spec.params = params;
+
+ spec.params.preview = preview;
+ if (alpha_bit_depth) {
+ spec.alpha_is_premultiplied = mt() % 2;
+ }
+ if (spec.width * spec.height > 1000) {
+ // Increase the encoder speed for larger images.
+ spec.params.speed_tier = jxl::SpeedTier::kWombat;
+ }
+ spec.seed = mt() % 777777;
+ // Pick the orientation at random. It is orthogonal to all
+ // other features. Valid values are 1 to 8.
+ spec.orientation = 1 + (mt() % 8);
+ if (!spec.Validate()) {
+ if (!quiet) {
+ std::cerr << "Skipping " << spec << std::endl;
+ }
+ } else {
+ specs.push_back(spec);
+ }
+ }
+#if JPEGXL_ENABLE_JPEG
+ }
+#endif // JPEGXL_ENABLE_JPEG
+ }
+ }
+ }
+ }
+ }
+ }
+
+ specs.emplace_back(ImageSpec());
+ specs.back().params.lossy_palette = true;
+ specs.back().override_decoder_spec = 0;
+
+ specs.emplace_back(ImageSpec());
+ specs.back().params.noise = true;
+ specs.back().override_decoder_spec = 0;
+
+ jxl::ThreadPoolInternal pool{num_threads};
+ if (!RunOnPool(
+ &pool, 0, specs.size(), jxl::ThreadPool::NoInit,
+ [&specs, dest_dir, regenerate, quiet](const uint32_t task,
+ size_t /* thread */) {
+ const ImageSpec& spec = specs[task];
+ GenerateFile(dest_dir, spec, regenerate, quiet);
+ },
+ "FuzzerCorpus")) {
+ std::cerr << "Error generating fuzzer corpus" << std::endl;
+ return 1;
+ }
+ }
+ std::cerr << "Finished generating fuzzer corpus" << std::endl;
+ return 0;
+}
diff --git a/media/libjxl/src/tools/fuzzer_stub.cc b/media/libjxl/src/tools/fuzzer_stub.cc
new file mode 100644
index 0000000000..f984c00d48
--- /dev/null
+++ b/media/libjxl/src/tools/fuzzer_stub.cc
@@ -0,0 +1,45 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <fstream>
+#include <iostream>
+#include <iterator>
+#include <vector>
+
+#include "jxl/thread_parallel_runner.h"
+#include "jxl/thread_parallel_runner_cxx.h"
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);
+
+void ProcessInput(const char* filename) {
+ std::ifstream ifs(filename, std::ios::binary);
+ std::vector<char> contents((std::istreambuf_iterator<char>(ifs)),
+ std::istreambuf_iterator<char>());
+ ifs.close();
+ std::cout << "Processing " << filename << std::endl;
+ LLVMFuzzerTestOneInput(reinterpret_cast<uint8_t*>(contents.data()),
+ contents.size());
+}
+
+// Read files listed in args and pass their contents to "fuzzer".
+int main(int argc, const char* argv[]) {
+ if (argc == 2) {
+ // No threaded runner for single inputs.
+ ProcessInput(argv[1]);
+ } else if (argc > 2) {
+ auto runner = JxlThreadParallelRunnerMake(
+ nullptr, JxlThreadParallelRunnerDefaultNumWorkerThreads());
+ return JxlThreadParallelRunner(
+ runner.get(), argv,
+ /* init= */ +[](void*, size_t) -> JxlParallelRetCode { return 0; },
+ /* func= */
+ +[](void* opaque, uint32_t value, size_t) {
+ const char** proc_argv = static_cast<const char**>(opaque);
+ ProcessInput(proc_argv[value]);
+ },
+ /* start_range= */ 1, /* end_range= */ argc);
+ }
+ return 0;
+}
diff --git a/media/libjxl/src/tools/git_version.cmake b/media/libjxl/src/tools/git_version.cmake
new file mode 100644
index 0000000000..4d216e8f57
--- /dev/null
+++ b/media/libjxl/src/tools/git_version.cmake
@@ -0,0 +1,34 @@
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# git_version.cmake is a script which creates tools_version_git.h in the build
+# directory if building from a git repository.
+find_package(Git QUIET)
+
+# Check that this script was invoked with the necessary arguments.
+if(NOT IS_DIRECTORY "${JPEGXL_ROOT_DIR}")
+ message(FATAL_ERROR "JPEGXL_ROOT_DIR is invalid")
+endif()
+
+execute_process(
+ COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD
+ OUTPUT_VARIABLE GIT_REV
+ WORKING_DIRECTORY "${JPEGXL_ROOT_DIR}"
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET)
+
+# The define line in the file.
+set(JPEGXL_VERSION_DEFINE "#define JPEGXL_VERSION \"${GIT_REV}\"\n")
+
+# Update the header file only if needed.
+if(EXISTS "${DST}")
+ file(READ "${DST}" ORIG_DST)
+ if(NOT ORIG_DST STREQUAL JPEGXL_VERSION_DEFINE)
+ message(STATUS "Changing JPEGXL_VERSION to ${GIT_REV}")
+ file(WRITE "${DST}" "${JPEGXL_VERSION_DEFINE}")
+ endif()
+else()
+ file(WRITE "${DST}" "${JPEGXL_VERSION_DEFINE}")
+endif()
diff --git a/media/libjxl/src/tools/hdr/README.md b/media/libjxl/src/tools/hdr/README.md
new file mode 100644
index 0000000000..227b22b3e4
--- /dev/null
+++ b/media/libjxl/src/tools/hdr/README.md
@@ -0,0 +1,137 @@
+# HDR tools
+
+This directory contains a small set of command-line tools for HDR conversions,
+including to SDR.
+
+## Tone mapping
+
+`tools/tone_map` implements tone mapping as described in annex 5 of
+[Report ITU-R BT.2408-4](https://www.itu.int/pub/R-REP-BT.2408-4-2021), more
+specifically the YRGB variant. Since the result may contain out-of-gamut colors,
+it additionally does very basic gamut mapping. The balance between preserving
+saturation and preserving luminance can be controlled by passing a number
+between 0 and 1 using `--preserve_saturation`. The default is 0.1. Hue is never
+sacrificed.
+
+### Examples
+
+```shell
+# Tone maps a PQ image for a 300 cd/m² display, and writes the result as an SDR
+# (but still wide-gamut) image to be shown on such a display.
+$ tools/tone_map -t 300 ClassE_507.png ClassE_507_tone_mapped_300.png
+
+# The result can also be written as a PQ image itself:
+$ tools/tone_map -t 300 --pq ClassE_507.png ClassE_507_tone_mapped_300_pq.png
+
+# It is possible to specify the maximum luminance found in the image using
+# `--max_nits`. For OpenEXR input, it will override the `whiteLuminance` tag
+# which indicates the luminance of (1, 1, 1). For PQ, it will not affect the
+# luminance calculated from the signal, but it will tell the tone mapping how
+# much headroom to leave for highlights.
+$ tools/tone_map -m 4000 -t 300 ClassE_507.png ClassE_507_tone_mapped_300.png
+```
+
+## PQ to HLG conversion
+
+`tools/pq_to_hlg` performs conversion of a PQ image to HLG as described in
+section 6 of the aforementioned BT.2408-4. That is, the PQ image is first
+limited to 1000 cd/m² using the tone mapping mentioned above, and the result is
+treated as if it were the output of a reference 1000 cd/m² HLG display: such a
+display would have a system gamma of 1.2, and therefore, we can apply the
+HLG inverse OOTF with a gamma of 1.2 to get “back” to the linear scene-referred
+signal that would have produced that output on that reference display (and then
+encode it using the OETF).
+
+As with the tone mapping tool, the `--max_nits` and `--preserve_saturation`
+options can be used to guide the 1000 cd/m² limiting.
+
+### Example
+
+```shell
+$ tools/pq_to_hlg ClassE_507.png ClassE_507_hlg.png
+```
+
+## HLG rendering
+
+HLG is designed to look acceptable without specific processing on displays that
+expect a “traditional” SDR signal. Nevertheless, it is possible to optimize the
+appearance for specific viewing conditions by applying the HLG inverse OETF and
+then the OOTF with an appropriate system gamma. Here, the system gamma is
+computed using the extended model mentioned at the bottom of page 29 of
+[Report ITU-R BT.2390-9](https://www.itu.int/pub/R-REP-BT.2390-9-2021). That
+formula should work well over a wide range of display peak luminances.
+
+It is possible to specify not just the peak luminance of the target display
+(using `--target_nits`) but also the ambient luminance of the viewing
+environment using `--surround_nits`.
+
+As with the tone mapping tool, the result can be written as a PQ image. In that
+case, it would make sense, in further usage of `tools/tone_map` or
+`tools/pq_to_hlg`, to set `--max_nits` to the value that was passed as
+`--target_nits` to this tool. This also applies to the tone mapping tool.
+
+### Examples
+
+```shell
+# Renders an HLG image for a 300 cd/m² display in a 10 cd/m² room.
+$ tools/render_hlg -t 300 -s 10 ClassE_507_hlg.png ClassE_507_hlg_300.png
+
+# Renders it for a reference 1000 cd/m² display and writes the result as a PQ
+# image.
+$ tools/render_hlg -t 1000 --pq ClassE_507_hlg.png ClassE_507_hlg_pq.png
+
+# Informing pq_to_hlg about that maximum luminance then ensures proper
+# roundtripping as it will not needlessly tone map the highlights.
+$ tools/pq_to_hlg -m 1000 ClassE_507_hlg_pq.png ClassE_507_hlg_pq_hlg.png
+```
+
+## Display light to HLG
+
+By applying the inverse OOTF to a display-referred image, it is possible to
+compute the scene light, and from there the HLG signal, that would have
+produced that output on that display:
+
+```shell
+$ tools/display_to_hlg -m 600 -s 5 srgb_input.png hlg_output.png
+```
+
+This is the mathematical inverse of `tools/render_hlg`. Furthermore,
+`tools/pq_to_hlg` is equivalent to `tools/tone_map -t 1000` followed by
+`tools/display_to_hlg -m 1000`.
+
+# LUT generation
+
+There are additionally two tools that can be used to generate look-up tables
+for use with e.g. FFmpeg, ReShade, or DaVinci Resolve.
+
+The first of the two tools gives a starting point:
+
+```shell
+$ tools/generate_lut_template --lut_size=64 identity.ppm
+```
+
+From there, one can apply a chain of per-pixel transforms (including other
+LUTs) that the final LUT is intended to represent:
+
+```shell
+$ tools/pq_to_hlg identity.ppm pq_to_hlg.ppm
+$ tools/render_hlg -t 400 pq_to_hlg.ppm pq_to_400nit_rec2020.png
+$ convert pq_to_400nit_rec2020.png -profile /usr/share/color/icc/colord/Rec709.icc pq_to_400nit_rec709.png
+```
+
+From there, the PNG image can be used as-is with ReShade’s “LUT” shader
+(provided that the correct LUT size is set), or it can be converted to a
+[Cube](https://wwwimages2.adobe.com/content/dam/acom/en/products/speedgrade/cc/pdfs/cube-lut-specification-1.0.pdf)
+file for use in other software such as FFmpeg’s [lut3d](https://ffmpeg.org/ffmpeg-filters.html#lut3d-1)
+filter:
+
+```shell
+$ tools/texture_to_cube pq_to_400nit_rec709.png pq_to_400nit_rec709.cube
+$ ffmpeg -i pq_video.mkv -vf lut3d=pq_to_400nit_rec709.cube -colorspace bt709 -color_primaries bt709 -color_trc bt709 400nit_rec709_video.mkv
+```
+
+Note: instead of converting to a standard color space such as Rec. 709, it is
+also possible to convert to the color space of the specific display on which
+the content is to be shown, in which case the transformed content does not need
+any specific tagging and should be displayed directly without color management
+(for example using `ffplay`).
diff --git a/media/libjxl/src/tools/hdr/display_to_hlg.cc b/media/libjxl/src/tools/hdr/display_to_hlg.cc
new file mode 100644
index 0000000000..a2caef28c3
--- /dev/null
+++ b/media/libjxl/src/tools/hdr/display_to_hlg.cc
@@ -0,0 +1,85 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/hlg.h"
+#include "lib/extras/tone_mapping.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/enc_color_management.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+
+int main(int argc, const char** argv) {
+ jxl::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ float max_nits = 0;
+ auto max_nits_option = parser.AddOptionValue(
+ 'm', "max_nits", "nits", "maximum luminance of the display", &max_nits,
+ &jpegxl::tools::ParseFloat, 0);
+ float surround_nits = 5;
+ parser.AddOptionValue(
+ 's', "surround_nits", "nits",
+ "surround luminance of the viewing environment (default: 5)",
+ &surround_nits, &jpegxl::tools::ParseFloat, 0);
+ float preserve_saturation = .1f;
+ parser.AddOptionValue(
+ '\0', "preserve_saturation", "0..1",
+ "to what extent to try and preserve saturation over luminance if an "
+ "inverse gamma < 1 generates out-of-gamut colors",
+ &preserve_saturation, &jpegxl::tools::ParseFloat, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(max_nits_option)->matched()) {
+ fprintf(stderr,
+ "Missing required argument --max_nits.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::CodecInOut image;
+ JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image,
+ &pool));
+ image.metadata.m.SetIntensityTarget(max_nits);
+ JXL_CHECK(jxl::HlgInverseOOTF(
+ &image.Main(), jxl::GetHlgGamma(max_nits, surround_nits), &pool));
+ JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool));
+ image.metadata.m.SetIntensityTarget(301);
+
+ jxl::ColorEncoding hlg;
+ hlg.SetColorSpace(jxl::ColorSpace::kRGB);
+ hlg.primaries = jxl::Primaries::k2100;
+ hlg.white_point = jxl::WhitePoint::kD65;
+ hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG);
+ JXL_CHECK(hlg.CreateICC());
+ JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool));
+ image.metadata.m.color_encoding = hlg;
+ JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+}
diff --git a/media/libjxl/src/tools/hdr/generate_lut_template.cc b/media/libjxl/src/tools/hdr/generate_lut_template.cc
new file mode 100644
index 0000000000..45fc27ab56
--- /dev/null
+++ b/media/libjxl/src/tools/hdr/generate_lut_template.cc
@@ -0,0 +1,59 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+
+int main(int argc, const char** argv) {
+ jxl::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ size_t N = 64;
+ parser.AddOptionValue('N', "lut_size", "N", "linear size of the LUT", &N,
+ &jpegxl::tools::ParseUnsigned, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output LUT", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::Image3F image(N * N, N);
+ JXL_CHECK(jxl::RunOnPool(
+ &pool, 0, N, jxl::ThreadPool::NoInit,
+ [&](const uint32_t y, size_t /* thread */) {
+ const float g = static_cast<float>(y) / (N - 1);
+ float* const JXL_RESTRICT rows[3] = {
+ image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)};
+ for (size_t x = 0; x < N * N; ++x) {
+ rows[0][x] = static_cast<float>(x % N) / (N - 1);
+ rows[1][x] = g;
+ rows[2][x] = static_cast<float>(x / N) / (N - 1);
+ }
+ },
+ "GenerateTemplate"));
+
+ jxl::CodecInOut output;
+ output.SetFromImage(std::move(image), jxl::ColorEncoding::SRGB());
+ JXL_CHECK(jxl::EncodeToFile(output, jxl::ColorEncoding::SRGB(), 16,
+ output_filename, &pool));
+}
diff --git a/media/libjxl/src/tools/hdr/pq_to_hlg.cc b/media/libjxl/src/tools/hdr/pq_to_hlg.cc
new file mode 100644
index 0000000000..3b2125bf08
--- /dev/null
+++ b/media/libjxl/src/tools/hdr/pq_to_hlg.cc
@@ -0,0 +1,80 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/hlg.h"
+#include "lib/extras/tone_mapping.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/enc_color_management.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+
+int main(int argc, const char** argv) {
+ jxl::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ float max_nits = 0;
+ parser.AddOptionValue('m', "max_nits", "nits",
+ "maximum luminance in the image", &max_nits,
+ &jpegxl::tools::ParseFloat, 0);
+ float preserve_saturation = .1f;
+ parser.AddOptionValue(
+ 's', "preserve_saturation", "0..1",
+ "to what extent to try and preserve saturation over luminance",
+ &preserve_saturation, &jpegxl::tools::ParseFloat, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::CodecInOut image;
+ jxl::extras::ColorHints color_hints;
+ color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ");
+ JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ if (max_nits > 0) {
+ image.metadata.m.SetIntensityTarget(max_nits);
+ }
+ JXL_CHECK(jxl::ToneMapTo({0, 1000}, &image, &pool));
+ JXL_CHECK(jxl::HlgInverseOOTF(&image.Main(), 1.2f, &pool));
+ JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool));
+ // Peak luminance at which the system gamma is 1, since we are now in scene
+ // light, having applied the inverse OOTF ourselves to control the subsequent
+ // gamut mapping instead of leaving it to JxlCms below.
+ image.metadata.m.SetIntensityTarget(301);
+
+ jxl::ColorEncoding hlg;
+ hlg.SetColorSpace(jxl::ColorSpace::kRGB);
+ hlg.primaries = jxl::Primaries::k2100;
+ hlg.white_point = jxl::WhitePoint::kD65;
+ hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG);
+ JXL_CHECK(hlg.CreateICC());
+ JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool));
+ image.metadata.m.color_encoding = hlg;
+ JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+}
diff --git a/media/libjxl/src/tools/hdr/render_hlg.cc b/media/libjxl/src/tools/hdr/render_hlg.cc
new file mode 100644
index 0000000000..c8a239550f
--- /dev/null
+++ b/media/libjxl/src/tools/hdr/render_hlg.cc
@@ -0,0 +1,94 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/hlg.h"
+#include "lib/extras/tone_mapping.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/enc_color_management.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+
+int main(int argc, const char** argv) {
+ jxl::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ float target_nits = 0;
+ auto target_nits_option = parser.AddOptionValue(
+ 't', "target_nits", "nits", "peak luminance of the target display",
+ &target_nits, &jpegxl::tools::ParseFloat, 0);
+ float surround_nits = 5;
+ parser.AddOptionValue(
+ 's', "surround_nits", "nits",
+ "surround luminance of the viewing environment (default: 5)",
+ &surround_nits, &jpegxl::tools::ParseFloat, 0);
+ float preserve_saturation = .1f;
+ parser.AddOptionValue(
+ '\0', "preserve_saturation", "0..1",
+ "to what extent to try and preserve saturation over luminance if a gamma "
+ "< 1 generates out-of-gamut colors",
+ &preserve_saturation, &jpegxl::tools::ParseFloat, 0);
+ bool pq = false;
+ parser.AddOptionFlag('p', "pq",
+ "write the output with absolute luminance using PQ", &pq,
+ &jpegxl::tools::SetBooleanTrue, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(target_nits_option)->matched()) {
+ fprintf(stderr,
+ "Missing required argument --target_nits.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::CodecInOut image;
+ jxl::extras::ColorHints color_hints;
+ color_hints.Add("color_space", "RGB_D65_202_Rel_HLG");
+ JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ // Ensures that conversions to linear by JxlCms will not apply the OOTF as we
+ // apply it ourselves to control the subsequent gamut mapping.
+ image.metadata.m.SetIntensityTarget(301);
+ const float gamma = jxl::GetHlgGamma(target_nits, surround_nits);
+ fprintf(stderr, "Using a system gamma of %g\n", gamma);
+ JXL_CHECK(jxl::HlgOOTF(&image.Main(), gamma, &pool));
+ JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool));
+ image.metadata.m.SetIntensityTarget(target_nits);
+
+ jxl::ColorEncoding c_out = image.metadata.m.color_encoding;
+ if (pq) {
+ c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ);
+ } else {
+ c_out.tf.SetTransferFunction(jxl::TransferFunction::k709);
+ }
+ JXL_CHECK(c_out.CreateICC());
+ JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool));
+ image.metadata.m.color_encoding = c_out;
+ JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+}
diff --git a/media/libjxl/src/tools/hdr/texture_to_cube.cc b/media/libjxl/src/tools/hdr/texture_to_cube.cc
new file mode 100644
index 0000000000..a5e5af788d
--- /dev/null
+++ b/media/libjxl/src/tools/hdr/texture_to_cube.cc
@@ -0,0 +1,71 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+
+int main(int argc, const char** argv) {
+ jxl::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output Cube LUT", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::CodecInOut image;
+ JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image,
+ &pool));
+
+ JXL_CHECK(image.xsize() == image.ysize() * image.ysize());
+ const unsigned N = image.ysize();
+
+ FILE* const output = fopen(output_filename, "wb");
+ JXL_CHECK(output);
+
+ fprintf(output, "# Created by libjxl\n");
+ fprintf(output, "LUT_3D_SIZE %u\n", N);
+ fprintf(output, "DOMAIN_MIN 0.0 0.0 0.0\nDOMAIN_MAX 1.0 1.0 1.0\n\n");
+
+ for (size_t b = 0; b < N; ++b) {
+ for (size_t g = 0; g < N; ++g) {
+ const size_t y = g;
+ const float* const JXL_RESTRICT rows[3] = {
+ image.Main().color()->ConstPlaneRow(0, y) + N * b,
+ image.Main().color()->ConstPlaneRow(1, y) + N * b,
+ image.Main().color()->ConstPlaneRow(2, y) + N * b};
+ for (size_t r = 0; r < N; ++r) {
+ const size_t x = r;
+ fprintf(output, "%.6f %.6f %.6f\n", rows[0][x], rows[1][x], rows[2][x]);
+ }
+ }
+ }
+}
diff --git a/media/libjxl/src/tools/hdr/tone_map.cc b/media/libjxl/src/tools/hdr/tone_map.cc
new file mode 100644
index 0000000000..1ef3823c2c
--- /dev/null
+++ b/media/libjxl/src/tools/hdr/tone_map.cc
@@ -0,0 +1,89 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/tone_mapping.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/enc_color_management.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+
+int main(int argc, const char** argv) {
+ jxl::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ float max_nits = 0;
+ parser.AddOptionValue('m', "max_nits", "nits",
+ "maximum luminance in the image", &max_nits,
+ &jpegxl::tools::ParseFloat, 0);
+ float target_nits = 0;
+ auto target_nits_option = parser.AddOptionValue(
+ 't', "target_nits", "nits",
+ "peak luminance of the display for which to tone map", &target_nits,
+ &jpegxl::tools::ParseFloat, 0);
+ float preserve_saturation = .1f;
+ parser.AddOptionValue(
+ 's', "preserve_saturation", "0..1",
+ "to what extent to try and preserve saturation over luminance",
+ &preserve_saturation, &jpegxl::tools::ParseFloat, 0);
+ bool pq = false;
+ parser.AddOptionFlag('p', "pq",
+ "write the output with absolute luminance using PQ", &pq,
+ &jpegxl::tools::SetBooleanTrue, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(target_nits_option)->matched()) {
+ fprintf(stderr,
+ "Missing required argument --target_nits.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::CodecInOut image;
+ jxl::extras::ColorHints color_hints;
+ color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ");
+ JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ if (max_nits > 0) {
+ image.metadata.m.SetIntensityTarget(max_nits);
+ }
+ JXL_CHECK(jxl::ToneMapTo({0, target_nits}, &image, &pool));
+ JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool));
+
+ jxl::ColorEncoding c_out = image.metadata.m.color_encoding;
+ if (pq) {
+ c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ);
+ } else {
+ c_out.tf.SetTransferFunction(jxl::TransferFunction::k709);
+ }
+ JXL_CHECK(c_out.CreateICC());
+ JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool));
+ image.metadata.m.color_encoding = c_out;
+ JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+}
diff --git a/media/libjxl/src/tools/icc_codec_fuzzer.cc b/media/libjxl/src/tools/icc_codec_fuzzer.cc
new file mode 100644
index 0000000000..0af805c71a
--- /dev/null
+++ b/media/libjxl/src/tools/icc_codec_fuzzer.cc
@@ -0,0 +1,68 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/jxl/enc_icc_codec.h"
+#include "lib/jxl/icc_codec.h"
+
+namespace jxl {
+
+int TestOneInput(const uint8_t* data, size_t size) {
+#if defined(JXL_ICC_FUZZER_ONLY_WRITE)
+ bool read = false;
+#elif defined(JXL_ICC_FUZZER_ONLY_READ)
+ bool read = true;
+#else
+ // Decide whether to test the reader or the writer (both use parsing)
+ if (!size) return 0;
+ bool read = data[0] == 0;
+ data++;
+ size--;
+#endif
+
+#ifdef JXL_ICC_FUZZER_SLOW_TEST
+ // Including JPEG XL LZ77 and ANS compression. These are already fuzzed
+ // separately, so it is better to disable JXL_ICC_FUZZER_SLOW_TEST to focus on
+ // the ICC parsing.
+ if (read) {
+ // Reading parses the compressed format.
+ BitReader br(Span<const uint8_t>(data, size));
+ PaddedBytes result;
+ (void)ReadICC(&br, &result);
+ (void)br.Close();
+ } else {
+ // Writing parses the original ICC profile.
+ PaddedBytes icc;
+ icc.assign(data, data + size);
+ BitWriter writer;
+ AuxOut aux;
+ // Writing should support any random bytestream so must succeed, make
+ // fuzzer fail if not.
+ JXL_ASSERT(WriteICC(icc, &writer, 0, &aux));
+ }
+#else // JXL_ICC_FUZZER_SLOW_TEST
+ if (read) {
+ // Reading (unpredicting) parses the compressed format.
+ PaddedBytes result;
+ (void)UnpredictICC(data, size, &result);
+ } else {
+ // Writing (predicting) parses the original ICC profile.
+ PaddedBytes result;
+ // Writing should support any random bytestream so must succeed, make
+ // fuzzer fail if not.
+ JXL_ASSERT(PredictICC(data, size, &result));
+ PaddedBytes reconstructed;
+ JXL_ASSERT(UnpredictICC(result.data(), result.size(), &reconstructed));
+ JXL_ASSERT(reconstructed.size() == size);
+ JXL_ASSERT(memcmp(data, reconstructed.data(), size) == 0);
+ }
+#endif // JXL_ICC_FUZZER_SLOW_TEST
+ return 0;
+}
+
+} // namespace jxl
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return jxl::TestOneInput(data, size);
+}
diff --git a/media/libjxl/src/tools/icc_detect/icc_detect.h b/media/libjxl/src/tools/icc_detect/icc_detect.h
new file mode 100644
index 0000000000..9335d94e73
--- /dev/null
+++ b/media/libjxl/src/tools/icc_detect/icc_detect.h
@@ -0,0 +1,19 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_ICC_DETECT_ICC_DETECT_H_
+#define TOOLS_ICC_DETECT_ICC_DETECT_H_
+
+#include <QByteArray>
+#include <QWidget>
+
+namespace jxl {
+
+// Should be cached if possible.
+QByteArray GetMonitorIccProfile(const QWidget* widget);
+
+} // namespace jxl
+
+#endif // TOOLS_ICC_DETECT_ICC_DETECT_H_
diff --git a/media/libjxl/src/tools/icc_detect/icc_detect_empty.cc b/media/libjxl/src/tools/icc_detect/icc_detect_empty.cc
new file mode 100644
index 0000000000..abd4a953fa
--- /dev/null
+++ b/media/libjxl/src/tools/icc_detect/icc_detect_empty.cc
@@ -0,0 +1,14 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/icc_detect/icc_detect.h"
+
+namespace jxl {
+
+QByteArray GetMonitorIccProfile(const QWidget* const /*widget*/) {
+ return QByteArray();
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/icc_detect/icc_detect_win32.cc b/media/libjxl/src/tools/icc_detect/icc_detect_win32.cc
new file mode 100644
index 0000000000..39ac5eef48
--- /dev/null
+++ b/media/libjxl/src/tools/icc_detect/icc_detect_win32.cc
@@ -0,0 +1,64 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/icc_detect/icc_detect.h"
+
+#include <windows.h>
+
+#include <memory>
+#include <type_traits>
+
+namespace jxl {
+
+namespace {
+
+struct HandleDeleter {
+ void operator()(const HANDLE handle) const {
+ if (handle != INVALID_HANDLE_VALUE) {
+ CloseHandle(handle);
+ }
+ }
+};
+using HandleUniquePtr =
+ std::unique_ptr<std::remove_pointer<HANDLE>::type, HandleDeleter>;
+
+} // namespace
+
+QByteArray GetMonitorIccProfile(const QWidget* const widget) {
+ const HWND window = reinterpret_cast<HWND>(widget->effectiveWinId());
+ const HDC dc = GetDC(window);
+ wchar_t profile_path[MAX_PATH];
+ DWORD profile_path_size = MAX_PATH;
+ if (!GetICMProfileW(dc, &profile_path_size, profile_path)) {
+ ReleaseDC(window, dc);
+ return QByteArray();
+ }
+ ReleaseDC(window, dc);
+ HandleUniquePtr file(CreateFileW(profile_path, GENERIC_READ, FILE_SHARE_READ,
+ nullptr, OPEN_EXISTING,
+ FILE_FLAG_SEQUENTIAL_SCAN, nullptr));
+ if (file.get() == INVALID_HANDLE_VALUE) {
+ return QByteArray();
+ }
+ LARGE_INTEGER profile_size;
+ if (!GetFileSizeEx(file.get(), &profile_size)) {
+ return QByteArray();
+ }
+ HandleUniquePtr mapping(
+ CreateFileMappingW(file.get(), nullptr, PAGE_READONLY, 0, 0, nullptr));
+ if (mapping == nullptr) {
+ return QByteArray();
+ }
+ const char* const view = reinterpret_cast<const char*>(
+ MapViewOfFile(mapping.get(), FILE_MAP_READ, 0, 0, 0));
+ if (view == nullptr) {
+ return QByteArray();
+ }
+ QByteArray profile(view, profile_size.QuadPart);
+ UnmapViewOfFile(view);
+ return profile;
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/icc_detect/icc_detect_x11.cc b/media/libjxl/src/tools/icc_detect/icc_detect_x11.cc
new file mode 100644
index 0000000000..be1209e387
--- /dev/null
+++ b/media/libjxl/src/tools/icc_detect/icc_detect_x11.cc
@@ -0,0 +1,77 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/icc_detect/icc_detect.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <xcb/xcb.h>
+
+#include <QX11Info>
+#include <algorithm>
+#include <memory>
+
+namespace jxl {
+
+namespace {
+
+constexpr char kIccProfileAtomName[] = "_ICC_PROFILE";
+constexpr uint32_t kMaxIccProfileSize = 1 << 24;
+
+struct FreeDeleter {
+ void operator()(void* const p) const { std::free(p); }
+};
+template <typename T>
+using XcbUniquePtr = std::unique_ptr<T, FreeDeleter>;
+
+} // namespace
+
+QByteArray GetMonitorIccProfile(const QWidget* const widget) {
+ Q_UNUSED(widget)
+ xcb_connection_t* const connection = QX11Info::connection();
+ if (connection == nullptr) {
+ return QByteArray();
+ }
+ const int screen_number = QX11Info::appScreen();
+
+ const xcb_intern_atom_cookie_t atomRequest =
+ xcb_intern_atom(connection, /*only_if_exists=*/1,
+ sizeof kIccProfileAtomName - 1, kIccProfileAtomName);
+ const XcbUniquePtr<xcb_intern_atom_reply_t> atomReply(
+ xcb_intern_atom_reply(connection, atomRequest, nullptr));
+ if (atomReply == nullptr) {
+ return QByteArray();
+ }
+ const xcb_atom_t iccProfileAtom = atomReply->atom;
+
+ const xcb_screen_t* screen = nullptr;
+ int i = 0;
+ for (xcb_screen_iterator_t it =
+ xcb_setup_roots_iterator(xcb_get_setup(connection));
+ it.rem; xcb_screen_next(&it)) {
+ if (i == screen_number) {
+ screen = it.data;
+ break;
+ }
+ ++i;
+ }
+ if (screen == nullptr) {
+ return QByteArray();
+ }
+ const xcb_get_property_cookie_t profileRequest = xcb_get_property(
+ connection, /*_delete=*/0, screen->root, iccProfileAtom,
+ XCB_GET_PROPERTY_TYPE_ANY, /*long_offset=*/0, kMaxIccProfileSize);
+ const XcbUniquePtr<xcb_get_property_reply_t> profile(
+ xcb_get_property_reply(connection, profileRequest, nullptr));
+ if (profile == nullptr || profile->bytes_after > 0) {
+ return QByteArray();
+ }
+
+ return QByteArray(
+ reinterpret_cast<const char*>(xcb_get_property_value(profile.get())),
+ xcb_get_property_value_length(profile.get()));
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java
new file mode 100644
index 0000000000..440ef6edab
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java
@@ -0,0 +1,39 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package org.jpeg.jpegxl.wrapper;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+
+/** JPEG XL JNI decoder wrapper. */
+public class Decoder {
+ /** Utility library, disable object construction. */
+ private Decoder() {}
+
+ /** One-shot decoding. */
+ public static ImageData decode(Buffer data, PixelFormat pixelFormat) {
+ StreamInfo basicInfo = DecoderJni.getBasicInfo(data, pixelFormat);
+ if (basicInfo.status != Status.OK) {
+ throw new IllegalStateException("Decoding failed");
+ }
+ if (basicInfo.width < 0 || basicInfo.height < 0 || basicInfo.pixelsSize < 0
+ || basicInfo.iccSize < 0) {
+ throw new IllegalStateException("JNI has returned negative size");
+ }
+ Buffer pixels = ByteBuffer.allocateDirect(basicInfo.pixelsSize);
+ Buffer icc = ByteBuffer.allocateDirect(basicInfo.iccSize);
+ Status status = DecoderJni.getPixels(data, pixels, icc, pixelFormat);
+ if (status != Status.OK) {
+ throw new IllegalStateException("Decoding failed");
+ }
+ return new ImageData(basicInfo.width, basicInfo.height, pixels, icc, pixelFormat);
+ }
+
+ // TODO(eustas): accept byte-array as input.
+ public static StreamInfo decodeInfo(Buffer data) {
+ return DecoderJni.getBasicInfo(data, null);
+ }
+}
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java
new file mode 100644
index 0000000000..7a2f2bf7ed
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java
@@ -0,0 +1,73 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package org.jpeg.jpegxl.wrapper;
+
+import java.nio.Buffer;
+
+/**
+ * Low level JNI wrapper.
+ *
+ * This class is package-private, should be only be used by high level wrapper.
+ */
+class DecoderJni {
+ private static native void nativeGetBasicInfo(int[] context, Buffer data);
+ private static native void nativeGetPixels(int[] context, Buffer data, Buffer pixels, Buffer icc);
+
+ static Status makeStatus(int statusCode) {
+ switch (statusCode) {
+ case 0:
+ return Status.OK;
+ case -1:
+ return Status.INVALID_STREAM;
+ case 1:
+ return Status.NOT_ENOUGH_INPUT;
+ default:
+ throw new IllegalStateException("Unknown status code");
+ }
+ }
+
+ static StreamInfo makeStreamInfo(int[] context) {
+ StreamInfo result = new StreamInfo();
+ result.status = makeStatus(context[0]);
+ result.width = context[1];
+ result.height = context[2];
+ result.pixelsSize = context[3];
+ result.iccSize = context[4];
+ result.alphaBits = context[5];
+ return result;
+ }
+
+ /** Decode stream information. */
+ static StreamInfo getBasicInfo(Buffer data, PixelFormat pixelFormat) {
+ if (!data.isDirect()) {
+ throw new IllegalArgumentException("data must be direct buffer");
+ }
+ int[] context = new int[6];
+ context[0] = (pixelFormat == null) ? -1 : pixelFormat.ordinal();
+ nativeGetBasicInfo(context, data);
+ return makeStreamInfo(context);
+ }
+
+ /** One-shot decoding. */
+ static Status getPixels(Buffer data, Buffer pixels, Buffer icc, PixelFormat pixelFormat) {
+ if (!data.isDirect()) {
+ throw new IllegalArgumentException("data must be direct buffer");
+ }
+ if (!pixels.isDirect()) {
+ throw new IllegalArgumentException("pixels must be direct buffer");
+ }
+ if (!icc.isDirect()) {
+ throw new IllegalArgumentException("icc must be direct buffer");
+ }
+ int[] context = new int[1];
+ context[0] = pixelFormat.ordinal();
+ nativeGetPixels(context, data, pixels, icc);
+ return makeStatus(context[0]);
+ }
+
+ /** Utility library, disable object construction. */
+ private DecoderJni() {}
+}
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java
new file mode 100644
index 0000000000..44f038c789
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java
@@ -0,0 +1,127 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package org.jpeg.jpegxl.wrapper;
+
+import java.nio.ByteBuffer;
+
+public class DecoderTest {
+ static {
+ String jniLibrary = System.getProperty("org.jpeg.jpegxl.wrapper.lib");
+ if (jniLibrary != null) {
+ try {
+ System.load(new java.io.File(jniLibrary).getAbsolutePath());
+ } catch (UnsatisfiedLinkError ex) {
+ String message =
+ "If the nested exception message says that some standard library (stdc++, tcmalloc, etc.) was not found, "
+ + "it is likely that JDK discovered by the build system overrides library search path. "
+ + "Try specifying a different JDK via JAVA_HOME environment variable and doing a clean build.";
+ throw new RuntimeException(message, ex);
+ }
+ }
+ }
+
+ private static final int SIMPLE_IMAGE_DIM = 1024;
+ // Base64: "/wr6H0GRCAYBAGAASzgkunkeVbaSBu95EXDn0e7ABz2ShAMA"
+ private static final byte[] SIMPLE_IMAGE_BYTES = {-1, 10, -6, 31, 65, -111, 8, 6, 1, 0, 96, 0, 75,
+ 56, 36, -70, 121, 30, 85, -74, -110, 6, -17, 121, 17, 112, -25, -47, -18, -64, 7, 61, -110,
+ -124, 3, 0};
+
+ private static final int PIXEL_IMAGE_DIM = 1;
+ // Base64: "/woAELASCBAQABwASxLFgoUkDA=="
+ private static final byte[] PIXEL_IMAGE_BYTES = {
+ -1, 10, 0, 16, -80, 18, 8, 16, 16, 0, 28, 0, 75, 18, -59, -126, -123, 36, 12};
+
+ static ByteBuffer makeByteBuffer(byte[] src, int length) {
+ ByteBuffer buffer = ByteBuffer.allocateDirect(length);
+ buffer.put(src, 0, length);
+ return buffer;
+ }
+
+ static ByteBuffer makeSimpleImage() {
+ return makeByteBuffer(SIMPLE_IMAGE_BYTES, SIMPLE_IMAGE_BYTES.length);
+ }
+
+ static void checkSimpleImageData(ImageData imageData) {
+ if (imageData.width != SIMPLE_IMAGE_DIM) {
+ throw new IllegalStateException("invalid width");
+ }
+ if (imageData.height != SIMPLE_IMAGE_DIM) {
+ throw new IllegalStateException("invalid height");
+ }
+ int iccSize = imageData.icc.capacity();
+ // Do not expect ICC profile to be some exact size; currently it is 732
+ if (iccSize < 300 || iccSize > 1000) {
+ throw new IllegalStateException("unexpected ICC profile size");
+ }
+ }
+
+ static void checkPixelFormat(PixelFormat pixelFormat, int bytesPerPixel) {
+ ImageData imageData = Decoder.decode(makeSimpleImage(), pixelFormat);
+ checkSimpleImageData(imageData);
+ if (imageData.pixels.limit() != SIMPLE_IMAGE_DIM * SIMPLE_IMAGE_DIM * bytesPerPixel) {
+ throw new IllegalStateException("Unexpected pixels size");
+ }
+ }
+
+ static void testRgba() {
+ checkPixelFormat(PixelFormat.RGBA_8888, 4);
+ }
+
+ static void testRgbaF16() {
+ checkPixelFormat(PixelFormat.RGBA_F16, 8);
+ }
+
+ static void testRgb() {
+ checkPixelFormat(PixelFormat.RGB_888, 3);
+ }
+
+ static void testRgbF16() {
+ checkPixelFormat(PixelFormat.RGB_F16, 6);
+ }
+
+ static void checkGetInfo(ByteBuffer data, int dim, int alphaBits) {
+ StreamInfo streamInfo = Decoder.decodeInfo(data);
+ if (streamInfo.status != Status.OK) {
+ throw new IllegalStateException("Unexpected decoding error");
+ }
+ if (streamInfo.width != dim || streamInfo.height != dim) {
+ throw new IllegalStateException("Invalid width / height");
+ }
+ if (streamInfo.alphaBits != alphaBits) {
+ throw new IllegalStateException("Invalid alphaBits");
+ }
+ }
+
+ static void testGetInfoNoAlpha() {
+ checkGetInfo(makeSimpleImage(), SIMPLE_IMAGE_DIM, 0);
+ }
+
+ static void testGetInfoAlpha() {
+ checkGetInfo(makeByteBuffer(PIXEL_IMAGE_BYTES, PIXEL_IMAGE_BYTES.length), PIXEL_IMAGE_DIM, 8);
+ }
+
+ static void testNotEnoughInput() {
+ for (int i = 0; i < 6; ++i) {
+ ByteBuffer jxlData = makeByteBuffer(SIMPLE_IMAGE_BYTES, i);
+ StreamInfo streamInfo = Decoder.decodeInfo(jxlData);
+ if (streamInfo.status != Status.NOT_ENOUGH_INPUT) {
+ throw new IllegalStateException(
+ "Expected 'not enough input', but got " + streamInfo.status + " " + i);
+ }
+ }
+ }
+
+ // Simple executable to avoid extra dependencies.
+ public static void main(String[] args) {
+ testRgba();
+ testRgbaF16();
+ testRgb();
+ testRgbF16();
+ testGetInfoNoAlpha();
+ testGetInfoAlpha();
+ testNotEnoughInput();
+ }
+}
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java
new file mode 100644
index 0000000000..a449529a5a
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java
@@ -0,0 +1,25 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package org.jpeg.jpegxl.wrapper;
+
+import java.nio.Buffer;
+
+/** POJO that contains necessary image data (dimensions, pixels,...). */
+public class ImageData {
+ final int width;
+ final int height;
+ final Buffer pixels;
+ final Buffer icc;
+ final PixelFormat pixelFormat;
+
+ ImageData(int width, int height, Buffer pixels, Buffer icc, PixelFormat pixelFormat) {
+ this.width = width;
+ this.height = height;
+ this.pixels = pixels;
+ this.icc = icc;
+ this.pixelFormat = pixelFormat;
+ }
+}
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java
new file mode 100644
index 0000000000..5df1225740
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java
@@ -0,0 +1,13 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package org.jpeg.jpegxl.wrapper;
+
+public enum PixelFormat {
+ RGBA_8888, // 0
+ RGBA_F16, // 1
+ RGB_888, // 2
+ RGB_F16 // 3
+}
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Status.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Status.java
new file mode 100644
index 0000000000..a87206a166
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/Status.java
@@ -0,0 +1,17 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package org.jpeg.jpegxl.wrapper;
+
+public enum Status {
+ /** Operation was successful. */
+ OK,
+
+ /** So far stream was valid, but incomplete. */
+ NOT_ENOUGH_INPUT,
+
+ /** Stream is corrupted. */
+ INVALID_STREAM
+}
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java
new file mode 100644
index 0000000000..2419b37f23
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java
@@ -0,0 +1,18 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package org.jpeg.jpegxl.wrapper;
+
+/** POJO that wraps some fields of JxlBasicInfo. */
+public class StreamInfo {
+ public Status status;
+ public int width;
+ public int height;
+ public int alphaBits;
+
+ // package-private
+ int pixelsSize;
+ int iccSize;
+}
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc
new file mode 100644
index 0000000000..1b3847e078
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc
@@ -0,0 +1,276 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h"
+
+#include <jni.h>
+
+#include <cstdlib>
+
+#include "jxl/decode.h"
+#include "jxl/thread_parallel_runner.h"
+#include "lib/jxl/base/status.h"
+
+namespace {
+
+template <typename From, typename To>
+bool StaticCast(const From& from, To* to) {
+ To tmp = static_cast<To>(from);
+ // Check sign is preserved.
+ if ((from < 0 && tmp > 0) || (from > 0 && tmp < 0)) return false;
+ // Check value is preserved.
+ if (from != static_cast<From>(tmp)) return false;
+ *to = tmp;
+ return true;
+}
+
+bool BufferToSpan(JNIEnv* env, jobject buffer, uint8_t** data, size_t* size) {
+ if (buffer == nullptr) return true;
+
+ *data = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
+ if (*data == nullptr) return false;
+ return StaticCast(env->GetDirectBufferCapacity(buffer), size);
+}
+
+int ToStatusCode(const jxl::Status& status) {
+ if (status) return 0;
+ if (status.IsFatalError()) return -1;
+ return 1; // Non-fatal -> not enough input.
+}
+
+constexpr const size_t kLastPixelFormat = 3;
+constexpr const size_t kNoPixelFormat = static_cast<size_t>(-1);
+
+JxlPixelFormat ToPixelFormat(size_t pixel_format) {
+ if (pixel_format == 0) {
+ // RGBA, 4 x byte per pixel, no scanline padding.
+ return {/*num_channels=*/4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, /*align=*/0};
+ } else if (pixel_format == 1) {
+ // RGBA, 4 x float16 per pixel, no scanline padding.
+ return {/*num_channels=*/4, JXL_TYPE_FLOAT16, JXL_LITTLE_ENDIAN,
+ /*align=*/0};
+ } else if (pixel_format == 2) {
+ // RGB, 4 x byte per pixel, no scanline padding.
+ return {/*num_channels=*/3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, /*align=*/0};
+ } else if (pixel_format == 3) {
+ // RGB, 4 x float16 per pixel, no scanline padding.
+ return {/*num_channels=*/3, JXL_TYPE_FLOAT16, JXL_LITTLE_ENDIAN,
+ /*align=*/0};
+ } else {
+ abort();
+ return {0, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0};
+ }
+}
+
+jxl::Status DoDecode(JNIEnv* env, jobject data_buffer, size_t* info_pixels_size,
+ size_t* info_icc_size, JxlBasicInfo* info,
+ size_t pixel_format, jobject pixels_buffer,
+ jobject icc_buffer) {
+ if (data_buffer == nullptr) return JXL_FAILURE("No data buffer");
+
+ uint8_t* data = nullptr;
+ size_t data_size = 0;
+ if (!BufferToSpan(env, data_buffer, &data, &data_size)) {
+ return JXL_FAILURE("Failed to access data buffer");
+ }
+
+ uint8_t* pixels = nullptr;
+ size_t pixels_size = 0;
+ if (!BufferToSpan(env, pixels_buffer, &pixels, &pixels_size)) {
+ return JXL_FAILURE("Failed to access pixels buffer");
+ }
+
+ uint8_t* icc = nullptr;
+ size_t icc_size = 0;
+ if (!BufferToSpan(env, icc_buffer, &icc, &icc_size)) {
+ return JXL_FAILURE("Failed to access ICC buffer");
+ }
+
+ JxlDecoder* dec = JxlDecoderCreate(NULL);
+
+ constexpr size_t kNumThreads = 0; // Do everything in this thread.
+ void* runner = JxlThreadParallelRunnerCreate(NULL, kNumThreads);
+
+ struct Defer {
+ JxlDecoder* dec;
+ void* runner;
+ ~Defer() {
+ JxlThreadParallelRunnerDestroy(runner);
+ JxlDecoderDestroy(dec);
+ }
+ } defer{dec, runner};
+
+ auto status =
+ JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner);
+ if (status != JXL_DEC_SUCCESS) {
+ return JXL_FAILURE("Failed to set parallel runner");
+ }
+ status = JxlDecoderSubscribeEvents(
+ dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE | JXL_DEC_COLOR_ENCODING);
+ if (status != JXL_DEC_SUCCESS) {
+ return JXL_FAILURE("Failed to subscribe for events");
+ }
+ status = JxlDecoderSetInput(dec, data, data_size);
+ if (status != JXL_DEC_SUCCESS) {
+ return JXL_FAILURE("Failed to set input");
+ }
+ status = JxlDecoderProcessInput(dec);
+ if (status == JXL_DEC_NEED_MORE_INPUT) {
+ return JXL_STATUS(jxl::StatusCode::kNotEnoughBytes, "Not enough input");
+ }
+ if (status != JXL_DEC_BASIC_INFO) {
+ return JXL_FAILURE("Unexpected notification (want: basic info)");
+ }
+ if (info_pixels_size) {
+ JxlPixelFormat format = ToPixelFormat(pixel_format);
+ status = JxlDecoderImageOutBufferSize(dec, &format, info_pixels_size);
+ if (status != JXL_DEC_SUCCESS) {
+ return JXL_FAILURE("Failed to get pixels size");
+ }
+ }
+ if (info) {
+ status = JxlDecoderGetBasicInfo(dec, info);
+ if (status != JXL_DEC_SUCCESS) {
+ return JXL_FAILURE("Failed to get basic info");
+ }
+ }
+ status = JxlDecoderProcessInput(dec);
+ if (status != JXL_DEC_COLOR_ENCODING) {
+ return JXL_FAILURE("Unexpected notification (want: color encoding)");
+ }
+ if (info_icc_size) {
+ JxlPixelFormat format = ToPixelFormat(pixel_format);
+ status = JxlDecoderGetICCProfileSize(
+ dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, info_icc_size);
+ if (status != JXL_DEC_SUCCESS) *info_icc_size = 0;
+ }
+ if (icc && icc_size > 0) {
+ JxlPixelFormat format = ToPixelFormat(pixel_format);
+ status = JxlDecoderGetColorAsICCProfile(
+ dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, icc, icc_size);
+ if (status != JXL_DEC_SUCCESS) {
+ return JXL_FAILURE("Failed to get ICC");
+ }
+ }
+ if (pixels) {
+ JxlPixelFormat format = ToPixelFormat(pixel_format);
+ status = JxlDecoderProcessInput(dec);
+ if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ return JXL_FAILURE("Unexpected notification (want: need out buffer)");
+ }
+ status = JxlDecoderSetImageOutBuffer(dec, &format, pixels, pixels_size);
+ if (status != JXL_DEC_SUCCESS) {
+ return JXL_FAILURE("Failed to set out buffer");
+ }
+ status = JxlDecoderProcessInput(dec);
+ if (status != JXL_DEC_FULL_IMAGE) {
+ return JXL_FAILURE("Unexpected notification (want: full image)");
+ }
+ status = JxlDecoderProcessInput(dec);
+ if (status != JXL_DEC_SUCCESS) {
+ return JXL_FAILURE("Unexpected notification (want: success)");
+ }
+ }
+
+ return true;
+}
+
+#undef FAILURE
+
+} // namespace
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT void JNICALL
+Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetBasicInfo(
+ JNIEnv* env, jobject /*jobj*/, jintArray ctx, jobject data_buffer) {
+ jint context[6] = {0};
+ env->GetIntArrayRegion(ctx, 0, 1, context);
+
+ JxlBasicInfo info = {};
+ size_t pixels_size = 0;
+ size_t icc_size = 0;
+ size_t pixel_format = 0;
+
+ jxl::Status status = true;
+
+ if (status) {
+ pixel_format = context[0];
+ if (pixel_format == kNoPixelFormat) {
+ // OK
+ } else if (pixel_format > kLastPixelFormat) {
+ status = JXL_FAILURE("Unrecognized pixel format");
+ }
+ }
+
+ if (status) {
+ bool want_output_size = (pixel_format != kNoPixelFormat);
+ if (want_output_size) {
+ status = DoDecode(
+ env, data_buffer, &pixels_size, &icc_size, &info, pixel_format,
+ /* pixels_buffer= */ nullptr, /* icc_buffer= */ nullptr);
+ } else {
+ status =
+ DoDecode(env, data_buffer, /* info_pixels_size= */ nullptr,
+ /* info_icc_size= */ nullptr, &info, pixel_format,
+ /* pixels_buffer= */ nullptr, /* icc_buffer= */ nullptr);
+ }
+ }
+
+ if (status) {
+ bool ok = true;
+ ok &= StaticCast(info.xsize, context + 1);
+ ok &= StaticCast(info.ysize, context + 2);
+ ok &= StaticCast(pixels_size, context + 3);
+ ok &= StaticCast(icc_size, context + 4);
+ ok &= StaticCast(info.alpha_bits, context + 5);
+ if (!ok) status = JXL_FAILURE("Invalid value");
+ }
+
+ context[0] = ToStatusCode(status);
+
+ env->SetIntArrayRegion(ctx, 0, 6, context);
+}
+
+/**
+ * Get image pixel data.
+ *
+ * @param ctx {out_status} tuple
+ * @param data [in] Buffer with encoded JXL stream
+ * @param pixels [out] Buffer to place pixels to
+ */
+JNIEXPORT void JNICALL Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetPixels(
+ JNIEnv* env, jobject /* jobj */, jintArray ctx, jobject data_buffer,
+ jobject pixels_buffer, jobject icc_buffer) {
+ jint context[1] = {0};
+ env->GetIntArrayRegion(ctx, 0, 1, context);
+
+ size_t pixel_format = 0;
+
+ jxl::Status status = true;
+
+ if (status) {
+ // Unlike getBasicInfo, "no-pixel-format" is not supported.
+ pixel_format = context[0];
+ if (pixel_format > kLastPixelFormat) {
+ status = JXL_FAILURE("Unrecognized pixel format");
+ }
+ }
+
+ if (status) {
+ status = DoDecode(env, data_buffer, /* info_pixels_size= */ nullptr,
+ /* info_icc_size= */ nullptr, /* info= */ nullptr,
+ pixel_format, pixels_buffer, icc_buffer);
+ }
+
+ context[0] = ToStatusCode(status);
+ env->SetIntArrayRegion(ctx, 0, 1, context);
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h
new file mode 100644
index 0000000000..8237fc95a2
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h
@@ -0,0 +1,43 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_JNI_ORG_JPEG_JPEGXL_WRAPPER_DECODER_JNI
+#define TOOLS_JNI_ORG_JPEG_JPEGXL_WRAPPER_DECODER_JNI
+
+#include <jni.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Get basic image information (size, etc.)
+ *
+ * @param ctx {in_pixel_format_out_status, out_width, out_height, pixels_size,
+ * icc_size} tuple
+ * @param data [in] Buffer with encoded JXL stream
+ */
+JNIEXPORT void JNICALL
+Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetBasicInfo(JNIEnv* env,
+ jobject /*jobj*/,
+ jintArray ctx,
+ jobject data_buffer);
+
+/**
+ * Get image pixel data.
+ *
+ * @param ctx {in_pixel_format_out_status} tuple
+ * @param data [in] Buffer with encoded JXL stream
+ * @param pixels [out] Buffer to place pixels to
+ */
+JNIEXPORT void JNICALL Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetPixels(
+ JNIEnv* env, jobject /*jobj*/, jintArray ctx, jobject data_buffer,
+ jobject pixels_buffer, jobject icc_buffer);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TOOLS_JNI_ORG_JPEG_JPEGXL_WRAPPER_DECODER_JNI \ No newline at end of file
diff --git a/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc
new file mode 100644
index 0000000000..c5e6ba3e0f
--- /dev/null
+++ b/media/libjxl/src/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc
@@ -0,0 +1,52 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <jni.h>
+
+#include "tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static char* kGetBasicInfoName = const_cast<char*>("nativeGetBasicInfo");
+static char* kGetBasicInfoSig = const_cast<char*>("([ILjava/nio/Buffer;)V");
+static char* kGetPixelsName = const_cast<char*>("nativeGetPixels");
+static char* kGetPixelsInfoSig = const_cast<char*>(
+ "([ILjava/nio/Buffer;Ljava/nio/Buffer;Ljava/nio/Buffer;)V");
+
+#define JXL_JNI_METHOD(NAME) \
+ (reinterpret_cast<void*>( \
+ Java_org_jpeg_jpegxl_wrapper_DecoderJni_native##NAME))
+
+static const JNINativeMethod kDecoderMethods[] = {
+ {kGetBasicInfoName, kGetBasicInfoSig, JXL_JNI_METHOD(GetBasicInfo)},
+ {kGetPixelsName, kGetPixelsInfoSig, JXL_JNI_METHOD(GetPixels)}};
+
+static const size_t kNumDecoderMethods = 2;
+
+#undef JXL_JNI_METHOD
+
+JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+ JNIEnv* env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ return -1;
+ }
+
+ jclass clazz = env->FindClass("org/jpeg/jpegxl/wrapper/DecoderJni");
+ if (clazz == nullptr) {
+ return -1;
+ }
+
+ if (env->RegisterNatives(clazz, kDecoderMethods, kNumDecoderMethods) < 0) {
+ return -1;
+ }
+
+ return JNI_VERSION_1_6;
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/media/libjxl/src/tools/jxl_emcc.cc b/media/libjxl/src/tools/jxl_emcc.cc
new file mode 100644
index 0000000000..1951b30a5a
--- /dev/null
+++ b/media/libjxl/src/tools/jxl_emcc.cc
@@ -0,0 +1,66 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <cstring>
+
+#include "lib/extras/codec.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/dec_file.h"
+#include "lib/jxl/enc_cache.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_file.h"
+
+extern "C" {
+
+/* NOTA BENE: see file history to uncover how to decode HDR JPEGs to pixels. */
+
+/** Result: uint32_t 'size' followed by compressed image (JXL). */
+uint8_t* jxlCompress(const uint8_t* data, size_t size) {
+ jxl::PaddedBytes compressed;
+ jxl::CodecInOut io;
+ jxl::extras::Codec input_codec;
+ if (!jxl::SetFromBytes(jxl::Span<const uint8_t>(data, size), &io, nullptr,
+ &input_codec)) {
+ return nullptr;
+ }
+ jxl::CompressParams params;
+ jxl::PassesEncoderState passes_encoder_state;
+ if (!jxl::EncodeFile(params, &io, &passes_encoder_state, &compressed,
+ jxl::GetJxlCms(), nullptr, nullptr)) {
+ return nullptr;
+ }
+ size_t compressed_size = compressed.size();
+ uint8_t* result = reinterpret_cast<uint8_t*>(malloc(compressed_size + 4));
+ uint32_t* meta = reinterpret_cast<uint32_t*>(result);
+ meta[0] = compressed_size;
+ memcpy(result + 4, compressed.data(), compressed_size);
+ return result;
+}
+
+/** Result: uint32_t 'size' followed by decompressed image (JPG). */
+uint8_t* jxlDecompress(const uint8_t* data, size_t size) {
+ jxl::PaddedBytes decompressed;
+ jxl::CodecInOut io;
+ jxl::DecompressParams params;
+ if (!jxl::DecodeFile(params, jxl::Span<const uint8_t>(data, size), &io,
+ nullptr)) {
+ return nullptr;
+ }
+ io.use_sjpeg = false;
+ io.jpeg_quality = 100;
+ if (!jxl::Encode(io, jxl::extras::Codec::kJPG, io.Main().c_current(), 8,
+ &decompressed, nullptr)) {
+ return nullptr;
+ }
+ size_t decompressed_size = decompressed.size();
+ uint8_t* result = reinterpret_cast<uint8_t*>(malloc(decompressed_size + 4));
+ uint32_t* meta = reinterpret_cast<uint32_t*>(result);
+ meta[0] = decompressed_size;
+ memcpy(result + 4, decompressed.data(), decompressed_size);
+ return result;
+}
+
+} // extern "C"
diff --git a/media/libjxl/src/tools/jxl_from_tree.cc b/media/libjxl/src/tools/jxl_from_tree.cc
new file mode 100644
index 0000000000..aa85ff88bb
--- /dev/null
+++ b/media/libjxl/src/tools/jxl_from_tree.cc
@@ -0,0 +1,506 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <string.h>
+
+#include <fstream>
+#include <iostream>
+#include <unordered_map>
+
+#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/enc_cache.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_file.h"
+#include "lib/jxl/enc_frame.h"
+#include "lib/jxl/enc_heuristics.h"
+#include "lib/jxl/modular/encoding/context_predict.h"
+#include "lib/jxl/modular/encoding/enc_debug_tree.h"
+#include "lib/jxl/modular/encoding/enc_ma.h"
+#include "lib/jxl/modular/encoding/encoding.h"
+#include "lib/jxl/splines.h"
+
+namespace jxl {
+
+namespace {
+struct SplineData {
+ int32_t quantization_adjustment = 1;
+ std::vector<Spline> splines;
+};
+
+Splines SplinesFromSplineData(const SplineData& spline_data,
+ const ColorCorrelationMap& cmap) {
+ std::vector<QuantizedSpline> quantized_splines;
+ std::vector<Spline::Point> starting_points;
+ quantized_splines.reserve(spline_data.splines.size());
+ starting_points.reserve(spline_data.splines.size());
+ for (const Spline& spline : spline_data.splines) {
+ JXL_CHECK(!spline.control_points.empty());
+ quantized_splines.emplace_back(spline, spline_data.quantization_adjustment,
+ cmap.YtoXRatio(0), cmap.YtoBRatio(0));
+ starting_points.push_back(spline.control_points.front());
+ }
+ return Splines(spline_data.quantization_adjustment,
+ std::move(quantized_splines), std::move(starting_points));
+}
+
+template <typename F>
+bool ParseNode(F& tok, Tree& tree, SplineData& spline_data,
+ CompressParams& cparams, size_t& W, size_t& H, CodecInOut& io,
+ int& have_next, int& x0, int& y0) {
+ static const std::unordered_map<std::string, int> property_map = {
+ {"c", 0},
+ {"g", 1},
+ {"y", 2},
+ {"x", 3},
+ {"|N|", 4},
+ {"|W|", 5},
+ {"N", 6},
+ {"W", 7},
+ {"W-WW-NW+NWW", 8},
+ {"W+N-NW", 9},
+ {"W-NW", 10},
+ {"NW-N", 11},
+ {"N-NE", 12},
+ {"N-NN", 13},
+ {"W-WW", 14},
+ {"WGH", 15},
+ {"PrevAbs", 16},
+ {"Prev", 17},
+ {"PrevAbsErr", 18},
+ {"PrevErr", 19},
+ {"PPrevAbs", 20},
+ {"PPrev", 21},
+ {"PPrevAbsErr", 22},
+ {"PPrevErr", 23},
+ };
+ static const std::unordered_map<std::string, Predictor> predictor_map = {
+ {"Set", Predictor::Zero},
+ {"W", Predictor::Left},
+ {"N", Predictor::Top},
+ {"AvgW+N", Predictor::Average0},
+ {"Select", Predictor::Select},
+ {"Gradient", Predictor::Gradient},
+ {"Weighted", Predictor::Weighted},
+ {"NE", Predictor::TopRight},
+ {"NW", Predictor::TopLeft},
+ {"WW", Predictor::LeftLeft},
+ {"AvgW+NW", Predictor::Average1},
+ {"AvgN+NW", Predictor::Average2},
+ {"AvgN+NE", Predictor::Average3},
+ {"AvgAll", Predictor::Average4},
+ };
+ auto t = tok();
+ if (t == "if") {
+ // Decision node.
+ int p;
+ t = tok();
+ if (!property_map.count(t)) {
+ fprintf(stderr, "Unexpected property: %s\n", t.c_str());
+ return false;
+ }
+ p = property_map.at(t);
+ if ((t = tok()) != ">") {
+ fprintf(stderr, "Expected >, found %s\n", t.c_str());
+ return false;
+ }
+ t = tok();
+ size_t num = 0;
+ int split = std::stoi(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid splitval: %s\n", t.c_str());
+ return false;
+ }
+ size_t pos = tree.size();
+ tree.emplace_back(PropertyDecisionNode::Split(p, split, pos + 1));
+ JXL_RETURN_IF_ERROR(ParseNode(tok, tree, spline_data, cparams, W, H, io,
+ have_next, x0, y0));
+ tree[pos].rchild = tree.size();
+ } else if (t == "-") {
+ // Leaf
+ t = tok();
+ Predictor p;
+ if (!predictor_map.count(t)) {
+ fprintf(stderr, "Unexpected predictor: %s\n", t.c_str());
+ return false;
+ }
+ p = predictor_map.at(t);
+ t = tok();
+ bool subtract = false;
+ if (t == "-") {
+ subtract = true;
+ t = tok();
+ } else if (t == "+") {
+ t = tok();
+ }
+ size_t num = 0;
+ int offset = std::stoi(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid offset: %s\n", t.c_str());
+ return false;
+ }
+ if (subtract) offset = -offset;
+ tree.emplace_back(PropertyDecisionNode::Leaf(p, offset));
+ return true;
+ } else if (t == "Width") {
+ t = tok();
+ size_t num = 0;
+ W = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid width: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "Height") {
+ t = tok();
+ size_t num = 0;
+ H = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid height: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "/*") {
+ t = tok();
+ while (t != "*/" && t != "") t = tok();
+ } else if (t == "Squeeze") {
+ cparams.responsive = true;
+ } else if (t == "GroupShift") {
+ t = tok();
+ size_t num = 0;
+ cparams.modular_group_size_shift = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid GroupShift: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "XYB") {
+ cparams.color_transform = ColorTransform::kXYB;
+ } else if (t == "CbYCr") {
+ cparams.color_transform = ColorTransform::kYCbCr;
+ } else if (t == "RCT") {
+ t = tok();
+ size_t num = 0;
+ cparams.colorspace = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid RCT: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "Orientation") {
+ t = tok();
+ size_t num = 0;
+ io.metadata.m.orientation = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid Orientation: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "Alpha") {
+ io.metadata.m.SetAlphaBits(io.metadata.m.bit_depth.bits_per_sample);
+ ImageF alpha(W, H);
+ io.frames[0].SetAlpha(std::move(alpha), false);
+ } else if (t == "Bitdepth") {
+ t = tok();
+ size_t num = 0;
+ io.metadata.m.bit_depth.bits_per_sample = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid Bitdepth: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "FloatExpBits") {
+ t = tok();
+ size_t num = 0;
+ io.metadata.m.bit_depth.floating_point_sample = true;
+ io.metadata.m.bit_depth.exponent_bits_per_sample = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid FloatExpBits: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "FramePos") {
+ t = tok();
+ size_t num = 0;
+ x0 = std::stoi(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid FramePos x0: %s\n", t.c_str());
+ return false;
+ }
+ t = tok();
+ y0 = std::stoi(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid FramePos y0: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "NotLast") {
+ have_next = 1;
+ } else if (t == "Upsample") {
+ t = tok();
+ size_t num = 0;
+ cparams.resampling = std::stoul(t, &num);
+ if (num != t.size() ||
+ (cparams.resampling != 1 && cparams.resampling != 2 &&
+ cparams.resampling != 4 && cparams.resampling != 8)) {
+ fprintf(stderr, "Invalid Upsample: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "Upsample_EC") {
+ t = tok();
+ size_t num = 0;
+ cparams.ec_resampling = std::stoul(t, &num);
+ if (num != t.size() ||
+ (cparams.ec_resampling != 1 && cparams.ec_resampling != 2 &&
+ cparams.ec_resampling != 4 && cparams.ec_resampling != 8)) {
+ fprintf(stderr, "Invalid Upsample_EC: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "Animation") {
+ io.metadata.m.have_animation = true;
+ io.metadata.m.animation.tps_numerator = 1000;
+ io.metadata.m.animation.tps_denominator = 1;
+ io.frames[0].duration = 100;
+ } else if (t == "AnimationFPS") {
+ t = tok();
+ size_t num = 0;
+ io.metadata.m.animation.tps_numerator = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid numerator: %s\n", t.c_str());
+ return false;
+ }
+ t = tok();
+ num = 0;
+ io.metadata.m.animation.tps_denominator = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid denominator: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "Duration") {
+ t = tok();
+ size_t num = 0;
+ io.frames[0].duration = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid Duration: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "BlendMode") {
+ t = tok();
+ if (t == "kAdd") {
+ io.frames[0].blendmode = BlendMode::kAdd;
+ } else if (t == "kReplace") {
+ io.frames[0].blendmode = BlendMode::kReplace;
+ } else if (t == "kBlend") {
+ io.frames[0].blendmode = BlendMode::kBlend;
+ } else if (t == "kAlphaWeightedAdd") {
+ io.frames[0].blendmode = BlendMode::kAlphaWeightedAdd;
+ } else if (t == "kMul") {
+ io.frames[0].blendmode = BlendMode::kMul;
+ } else {
+ fprintf(stderr, "Invalid BlendMode: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "SplineQuantizationAdjustment") {
+ t = tok();
+ size_t num = 0;
+ spline_data.quantization_adjustment = std::stoul(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid SplineQuantizationAdjustment: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "Spline") {
+ Spline spline;
+ const auto ParseFloat = [&t, &tok](float& output) {
+ t = tok();
+ size_t num = 0;
+ output = std::stof(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid spline data: %s\n", t.c_str());
+ return false;
+ }
+ return true;
+ };
+ for (auto& dct : spline.color_dct) {
+ for (float& coefficient : dct) {
+ JXL_RETURN_IF_ERROR(ParseFloat(coefficient));
+ }
+ }
+ for (float& coefficient : spline.sigma_dct) {
+ JXL_RETURN_IF_ERROR(ParseFloat(coefficient));
+ }
+
+ while (true) {
+ t = tok();
+ if (t == "EndSpline") break;
+ size_t num = 0;
+ Spline::Point point;
+ point.x = std::stof(t, &num);
+ bool ok_x = num == t.size();
+ auto t_y = tok();
+ point.y = std::stof(t_y, &num);
+ if (!ok_x || num != t_y.size()) {
+ fprintf(stderr, "Invalid spline control point: %s %s\n", t.c_str(),
+ t_y.c_str());
+ return false;
+ }
+ spline.control_points.push_back(point);
+ }
+
+ if (spline.control_points.empty()) {
+ fprintf(stderr, "Spline with no control point\n");
+ return false;
+ }
+
+ spline_data.splines.push_back(std::move(spline));
+ } else if (t == "Gaborish") {
+ cparams.gaborish = jxl::Override::kOn;
+ } else if (t == "DeltaPalette") {
+ cparams.lossy_palette = true;
+ cparams.palette_colors = 0;
+ } else if (t == "EPF") {
+ t = tok();
+ size_t num = 0;
+ cparams.epf = std::stoul(t, &num);
+ if (num != t.size() || cparams.epf > 3) {
+ fprintf(stderr, "Invalid EPF: %s\n", t.c_str());
+ return false;
+ }
+ } else if (t == "Noise") {
+ cparams.manual_noise.resize(8);
+ for (size_t i = 0; i < 8; i++) {
+ t = tok();
+ size_t num = 0;
+ cparams.manual_noise[i] = std::stof(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid noise entry: %s\n", t.c_str());
+ return false;
+ }
+ }
+ } else if (t == "XYBFactors") {
+ cparams.manual_xyb_factors.resize(3);
+ for (size_t i = 0; i < 3; i++) {
+ t = tok();
+ size_t num = 0;
+ cparams.manual_xyb_factors[i] = std::stof(t, &num);
+ if (num != t.size()) {
+ fprintf(stderr, "Invalid XYB factor: %s\n", t.c_str());
+ return false;
+ }
+ }
+ } else {
+ fprintf(stderr, "Unexpected node type: %s\n", t.c_str());
+ return false;
+ }
+ JXL_RETURN_IF_ERROR(
+ ParseNode(tok, tree, spline_data, cparams, W, H, io, have_next, x0, y0));
+ return true;
+}
+
+class Heuristics : public DefaultEncoderHeuristics {
+ public:
+ bool CustomFixedTreeLossless(const jxl::FrameDimensions& frame_dim,
+ Tree* tree) override {
+ *tree = tree_;
+ return true;
+ }
+
+ explicit Heuristics(Tree tree) : tree_(std::move(tree)) {}
+
+ private:
+ Tree tree_;
+};
+} // namespace
+
+int JxlFromTree(const char* in, const char* out, const char* tree_out) {
+ Tree tree;
+ SplineData spline_data;
+ CompressParams cparams = {};
+ size_t width = 1024, height = 1024;
+ int x0 = 0, y0 = 0;
+ cparams.SetLossless();
+ cparams.resampling = 1;
+ cparams.ec_resampling = 1;
+ cparams.modular_group_size_shift = 3;
+ CodecInOut io;
+ int have_next = 0;
+
+ std::ifstream f(in);
+ auto tok = [&f]() {
+ std::string out;
+ f >> out;
+ return out;
+ };
+ if (!ParseNode(tok, tree, spline_data, cparams, width, height, io, have_next,
+ x0, y0)) {
+ return 1;
+ }
+
+ if (tree_out) {
+ PrintTree(tree, tree_out);
+ }
+ Image3F image(width, height);
+ io.SetFromImage(std::move(image), ColorEncoding::SRGB());
+ io.SetSize((width + x0) * cparams.resampling,
+ (height + y0) * cparams.resampling);
+ io.metadata.m.color_encoding.DecideIfWantICC();
+ cparams.options.zero_tokens = true;
+ cparams.palette_colors = 0;
+ cparams.channel_colors_pre_transform_percent = 0;
+ cparams.channel_colors_percent = 0;
+ cparams.patches = jxl::Override::kOff;
+ cparams.already_downsampled = true;
+ PaddedBytes compressed;
+
+ io.CheckMetadata();
+ BitWriter writer;
+
+ std::unique_ptr<CodecMetadata> metadata = jxl::make_unique<CodecMetadata>();
+ *metadata = io.metadata;
+ JXL_RETURN_IF_ERROR(metadata->size.Set(io.xsize(), io.ysize()));
+
+ metadata->m.xyb_encoded = cparams.color_transform == ColorTransform::kXYB;
+
+ JXL_RETURN_IF_ERROR(WriteHeaders(metadata.get(), &writer, nullptr));
+ writer.ZeroPadToByte();
+
+ while (true) {
+ PassesEncoderState enc_state;
+ enc_state.heuristics = make_unique<Heuristics>(tree);
+ enc_state.shared.image_features.splines =
+ SplinesFromSplineData(spline_data, enc_state.shared.cmap);
+
+ FrameInfo info;
+ info.is_last = !have_next;
+ if (!info.is_last) info.save_as_reference = 1;
+
+ io.frames[0].origin.x0 = x0;
+ io.frames[0].origin.y0 = y0;
+
+ JXL_RETURN_IF_ERROR(EncodeFrame(cparams, info, metadata.get(), io.frames[0],
+ &enc_state, GetJxlCms(), nullptr, &writer,
+ nullptr));
+ if (!have_next) break;
+ tree.clear();
+ spline_data.splines.clear();
+ have_next = 0;
+ if (!ParseNode(tok, tree, spline_data, cparams, width, height, io,
+ have_next, x0, y0)) {
+ return 1;
+ }
+ Image3F image(width, height);
+ io.SetFromImage(std::move(image), ColorEncoding::SRGB());
+ io.frames[0].blend = true;
+ }
+
+ compressed = std::move(writer).TakeBytes();
+
+ if (!WriteFile(compressed, out)) {
+ fprintf(stderr, "Failed to write to \"%s\"\n", out);
+ return 1;
+ }
+
+ return 0;
+}
+} // namespace jxl
+
+int main(int argc, char** argv) {
+ if ((argc != 3 && argc != 4) || !strcmp(argv[1], argv[2])) {
+ fprintf(stderr, "Usage: %s tree_in.txt out.jxl [tree_drawing]\n", argv[0]);
+ return 1;
+ }
+ return jxl::JxlFromTree(argv[1], argv[2], argc < 4 ? nullptr : argv[3]);
+}
diff --git a/media/libjxl/src/tools/jxlinfo.c b/media/libjxl/src/tools/jxlinfo.c
new file mode 100644
index 0000000000..0cced1740c
--- /dev/null
+++ b/media/libjxl/src/tools/jxlinfo.c
@@ -0,0 +1,461 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This example prints information from the main codestream header.
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "jxl/decode.h"
+
+int PrintBasicInfo(FILE* file, int verbose) {
+ uint8_t* data = NULL;
+ size_t data_size = 0;
+ // In how large chunks to read from the file and try decoding the basic info.
+ const size_t chunk_size = 2048;
+
+ JxlDecoder* dec = JxlDecoderCreate(NULL);
+ if (!dec) {
+ fprintf(stderr, "JxlDecoderCreate failed\n");
+ return 0;
+ }
+
+ JxlDecoderSetKeepOrientation(dec, 1);
+ JxlDecoderSetCoalescing(dec, JXL_FALSE);
+
+ if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(
+ dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
+ JXL_DEC_FRAME | JXL_DEC_BOX)) {
+ fprintf(stderr, "JxlDecoderSubscribeEvents failed\n");
+ JxlDecoderDestroy(dec);
+ return 0;
+ }
+
+ JxlBasicInfo info;
+ int seen_basic_info = 0;
+ JxlFrameHeader frame_header;
+ int framecount = 0;
+ float total_duration = 0.f;
+
+ for (;;) {
+ // The first time, this will output JXL_DEC_NEED_MORE_INPUT because no
+ // input is set yet, this is ok since the input is set when handling this
+ // event.
+ JxlDecoderStatus status = JxlDecoderProcessInput(dec);
+
+ if (status == JXL_DEC_ERROR) {
+ fprintf(stderr, "Decoder error\n");
+ break;
+ } else if (status == JXL_DEC_NEED_MORE_INPUT) {
+ // The first time there is nothing to release and it returns 0, but that
+ // is ok.
+ size_t remaining = JxlDecoderReleaseInput(dec);
+ // move any remaining bytes to the front if necessary
+ if (remaining != 0) {
+ memmove(data, data + data_size - remaining, remaining);
+ }
+ // resize the buffer to append one more chunk of data
+ // TODO(lode): avoid unnecessary reallocations
+ data = (uint8_t*)realloc(data, remaining + chunk_size);
+ // append bytes read from the file behind the remaining bytes
+ size_t read_size = fread(data + remaining, 1, chunk_size, file);
+ if (read_size == 0 && feof(file)) {
+ fprintf(stderr, "Unexpected EOF\n");
+ break;
+ }
+ data_size = remaining + read_size;
+ JxlDecoderSetInput(dec, data, data_size);
+ if (feof(file)) JxlDecoderCloseInput(dec);
+ } else if (status == JXL_DEC_SUCCESS) {
+ // Finished all processing.
+ break;
+ } else if (status == JXL_DEC_BASIC_INFO) {
+ if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec, &info)) {
+ fprintf(stderr, "JxlDecoderGetBasicInfo failed\n");
+ break;
+ }
+
+ seen_basic_info = 1;
+
+ printf("JPEG XL %s, %ux%u, %s",
+ info.have_animation ? "animation" : "image", info.xsize,
+ info.ysize,
+ info.uses_original_profile ? "(possibly) lossless" : "lossy");
+ printf(", %d-bit ", info.bits_per_sample);
+ if (info.exponent_bits_per_sample) {
+ printf("float (%d exponent bits) ", info.exponent_bits_per_sample);
+ }
+ int cmyk = 0, alpha = 0;
+ const char* const ec_type_names[7] = {"Alpha", "Depth", "Spotcolor",
+ "Selection", "Black", "CFA",
+ "Thermal"};
+ for (uint32_t i = 0; i < info.num_extra_channels; i++) {
+ JxlExtraChannelInfo extra;
+ if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &extra)) {
+ fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n");
+ break;
+ }
+ if (extra.type == JXL_CHANNEL_BLACK) cmyk = 1;
+ if (extra.type == JXL_CHANNEL_ALPHA) alpha = 1;
+ }
+ if (info.num_color_channels == 1)
+ printf("Grayscale");
+ else {
+ if (cmyk) {
+ printf("CMYK");
+ cmyk = 0;
+ } else if (alpha) {
+ printf("RGBA");
+ alpha = 0;
+ } else {
+ printf("RGB");
+ }
+ }
+ for (uint32_t i = 0; i < info.num_extra_channels; i++) {
+ JxlExtraChannelInfo extra;
+ if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &extra)) {
+ fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n");
+ break;
+ }
+ if (extra.type == JXL_CHANNEL_BLACK && cmyk == 0) {
+ cmyk = 1;
+ continue;
+ }
+ if (extra.type == JXL_CHANNEL_ALPHA && alpha == 0) {
+ alpha = 1;
+ continue;
+ }
+
+ printf("+%s", (extra.type < 7 ? ec_type_names[extra.type]
+ : (extra.type == JXL_CHANNEL_OPTIONAL
+ ? "UnknownOptional"
+ : "Unknown(OUTDATED libjxl!)")));
+ }
+ printf("\n");
+ if (verbose) {
+ printf("num_color_channels: %d\n", info.num_color_channels);
+ printf("num_extra_channels: %d\n", info.num_extra_channels);
+
+ for (uint32_t i = 0; i < info.num_extra_channels; i++) {
+ JxlExtraChannelInfo extra;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetExtraChannelInfo(dec, i, &extra)) {
+ fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n");
+ break;
+ }
+ printf("extra channel %u:\n", i);
+ printf(
+ " type: %s\n",
+ (extra.type < 7 ? ec_type_names[extra.type]
+ : (extra.type == JXL_CHANNEL_OPTIONAL
+ ? "Unknown but can be ignored"
+ : "Unknown, please update your libjxl")));
+ printf(" bits_per_sample: %u\n", extra.bits_per_sample);
+ if (extra.exponent_bits_per_sample > 0) {
+ printf(" float, with exponent_bits_per_sample: %u\n",
+ extra.exponent_bits_per_sample);
+ }
+ if (extra.dim_shift > 0) {
+ printf(" dim_shift: %u (upsampled %ux)\n", extra.dim_shift,
+ 1 << extra.dim_shift);
+ }
+ if (extra.name_length) {
+ char* name = malloc(extra.name_length + 1);
+ if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelName(
+ dec, i, name, extra.name_length + 1)) {
+ fprintf(stderr, "JxlDecoderGetExtraChannelName failed\n");
+ free(name);
+ break;
+ }
+ printf(" name: %s\n", name);
+ free(name);
+ }
+ if (extra.type == JXL_CHANNEL_ALPHA)
+ printf(" alpha_premultiplied: %d (%s)\n",
+ extra.alpha_premultiplied,
+ extra.alpha_premultiplied ? "Premultiplied"
+ : "Non-premultiplied");
+ if (extra.type == JXL_CHANNEL_SPOT_COLOR) {
+ printf(" spot_color: (%f, %f, %f) with opacity %f\n",
+ extra.spot_color[0], extra.spot_color[1],
+ extra.spot_color[2], extra.spot_color[3]);
+ }
+ if (extra.type == JXL_CHANNEL_CFA)
+ printf(" cfa_channel: %u\n", extra.cfa_channel);
+ }
+ }
+
+ if (info.intensity_target != 255.f || info.min_nits != 0.f ||
+ info.relative_to_max_display != 0 ||
+ info.relative_to_max_display != 0.f) {
+ printf("intensity_target: %f nits\n", info.intensity_target);
+ printf("min_nits: %f\n", info.min_nits);
+ printf("relative_to_max_display: %d\n", info.relative_to_max_display);
+ printf("linear_below: %f\n", info.linear_below);
+ }
+ if (verbose) printf("have_preview: %d\n", info.have_preview);
+ if (info.have_preview) {
+ printf("Preview image: %ux%u\n", info.preview.xsize,
+ info.preview.ysize);
+ }
+ if (verbose) printf("have_animation: %d\n", info.have_animation);
+ if (verbose && info.have_animation) {
+ printf("ticks per second (numerator / denominator): %u / %u\n",
+ info.animation.tps_numerator, info.animation.tps_denominator);
+ printf("num_loops: %u\n", info.animation.num_loops);
+ printf("have_timecodes: %d\n", info.animation.have_timecodes);
+ }
+ if (info.xsize != info.intrinsic_xsize ||
+ info.ysize != info.intrinsic_ysize || verbose) {
+ printf("Intrinsic dimensions: %ux%u\n", info.intrinsic_xsize,
+ info.intrinsic_ysize);
+ }
+ const char* const orientation_string[8] = {
+ "Normal", "Flipped horizontally",
+ "Upside down", "Flipped vertically",
+ "Transposed", "90 degrees clockwise",
+ "Anti-Transposed", "90 degrees counter-clockwise"};
+ if (info.orientation > 0 && info.orientation < 9) {
+ if (verbose || info.orientation > 1) {
+ printf("Orientation: %d (%s)\n", info.orientation,
+ orientation_string[info.orientation - 1]);
+ }
+ } else {
+ fprintf(stderr, "Invalid orientation\n");
+ }
+ } else if (status == JXL_DEC_COLOR_ENCODING) {
+ JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0};
+ printf("Color space: ");
+
+ JxlColorEncoding color_encoding;
+ if (JXL_DEC_SUCCESS ==
+ JxlDecoderGetColorAsEncodedProfile(dec, &format,
+ JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ &color_encoding)) {
+ const char* const cs_string[4] = {"RGB", "Grayscale", "XYB", "Unknown"};
+ const char* const wp_string[12] = {"", "D65", "Custom", "", "", "",
+ "", "", "", "", "E", "P3"};
+ const char* const pr_string[12] = {
+ "", "sRGB", "Custom", "", "", "", "", "", "", "Rec.2100", "", "P3"};
+ const char* const tf_string[19] = {
+ "", "709", "Unknown", "", "", "", "", "", "Linear", "",
+ "", "", "", "sRGB", "", "", "PQ", "DCI", "HLG"};
+ const char* const ri_string[4] = {"Perceptual", "Relative",
+ "Saturation", "Absolute"};
+ printf("%s, ", cs_string[color_encoding.color_space]);
+ printf("%s, ", wp_string[color_encoding.white_point]);
+ if (color_encoding.white_point == JXL_WHITE_POINT_CUSTOM) {
+ printf("white_point(x=%f,y=%f), ", color_encoding.white_point_xy[0],
+ color_encoding.white_point_xy[1]);
+ }
+ if (color_encoding.color_space == JXL_COLOR_SPACE_RGB ||
+ color_encoding.color_space == JXL_COLOR_SPACE_UNKNOWN) {
+ printf("%s primaries", pr_string[color_encoding.primaries]);
+ if (color_encoding.primaries == JXL_PRIMARIES_CUSTOM) {
+ printf(": red(x=%f,y=%f),", color_encoding.primaries_red_xy[0],
+ color_encoding.primaries_red_xy[1]);
+ printf(" green(x=%f,y=%f),", color_encoding.primaries_green_xy[0],
+ color_encoding.primaries_green_xy[1]);
+ printf(" blue(x=%f,y=%f)", color_encoding.primaries_blue_xy[0],
+ color_encoding.primaries_blue_xy[1]);
+ } else
+ printf(", ");
+ }
+ if (color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) {
+ printf("gamma(%f) transfer function, ", color_encoding.gamma);
+ } else {
+ printf("%s transfer function, ",
+ tf_string[color_encoding.transfer_function]);
+ }
+ printf("rendering intent: %s\n",
+ ri_string[color_encoding.rendering_intent]);
+
+ } else {
+ // The profile is not in JPEG XL encoded form, get as ICC profile
+ // instead.
+ size_t profile_size;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetICCProfileSize(dec, &format,
+ JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ &profile_size)) {
+ fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
+ continue;
+ }
+ printf("%" PRIu64 "-byte ICC profile, ", (uint64_t)profile_size);
+ if (profile_size < 132) {
+ fprintf(stderr, "ICC profile too small\n");
+ continue;
+ }
+ uint8_t* profile = (uint8_t*)malloc(profile_size);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetColorAsICCProfile(dec, &format,
+ JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ profile, profile_size)) {
+ fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
+ free(profile);
+ continue;
+ }
+ printf("CMM type: \"%.4s\", ", profile + 4);
+ printf("color space: \"%.4s\", ", profile + 16);
+ printf("rendering intent: %d\n", (int)profile[67]);
+ free(profile);
+ }
+ } else if (status == JXL_DEC_FRAME) {
+ if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &frame_header)) {
+ fprintf(stderr, "JxlDecoderGetFrameHeader failed\n");
+ break;
+ }
+ if (frame_header.duration == 0) {
+ if (frame_header.is_last && framecount == 0 &&
+ frame_header.name_length == 0)
+ continue;
+ printf("layer: ");
+ } else {
+ printf("frame: ");
+ }
+ framecount++;
+ if (frame_header.layer_info.have_crop) {
+ printf("%ux%u at position (%i,%i)", frame_header.layer_info.xsize,
+ frame_header.layer_info.ysize, frame_header.layer_info.crop_x0,
+ frame_header.layer_info.crop_y0);
+ } else {
+ printf("full image size");
+ }
+
+ float ms = frame_header.duration * 1000.f *
+ info.animation.tps_denominator / info.animation.tps_numerator;
+ total_duration += ms;
+ if (info.have_animation) {
+ printf(", duration: %.1f ms", ms);
+ if (info.animation.have_timecodes) {
+ printf(", time code: %X", frame_header.timecode);
+ }
+ }
+ if (frame_header.name_length) {
+ char* name = malloc(frame_header.name_length + 1);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetFrameName(dec, name, frame_header.name_length + 1)) {
+ fprintf(stderr, "JxlDecoderGetFrameName failed\n");
+ free(name);
+ break;
+ }
+ printf(", name: \"%s\"", name);
+ free(name);
+ }
+ printf("\n");
+ } else if (status == JXL_DEC_BOX) {
+ JxlBoxType type;
+ uint64_t size;
+ JxlDecoderGetBoxType(dec, type, JXL_FALSE);
+ JxlDecoderGetBoxSizeRaw(dec, &size);
+ if (verbose) {
+ printf("box: type: \"%c%c%c%c\" size: %" PRIu64 "\n", type[0], type[1],
+ type[2], type[3], (uint64_t)size);
+ }
+ if (!strncmp(type, "JXL ", 4)) {
+ printf("JPEG XL file format container (ISO/IEC 18181-2)\n");
+ } else if (!strncmp(type, "ftyp", 4)) {
+ } else if (!strncmp(type, "jxlc", 4)) {
+ } else if (!strncmp(type, "jxlp", 4)) {
+ } else if (!strncmp(type, "jxll", 4)) {
+ } else if (!strncmp(type, "jxli", 4)) {
+ printf("Frame index box present\n");
+ } else if (!strncmp(type, "jbrd", 4)) {
+ printf("JPEG bitstream reconstruction data available\n");
+ } else if (!strncmp(type, "jumb", 4) || !strncmp(type, "Exif", 4) ||
+ !strncmp(type, "xml ", 4)) {
+ printf("Uncompressed %c%c%c%c metadata: %" PRIu64 " bytes\n", type[0],
+ type[1], type[2], type[3], (uint64_t)size);
+
+ } else if (!strncmp(type, "brob", 4)) {
+ JxlDecoderGetBoxType(dec, type, JXL_TRUE);
+ printf("Brotli-compressed %c%c%c%c metadata: %" PRIu64
+ " compressed bytes\n",
+ type[0], type[1], type[2], type[3], (uint64_t)size);
+ } else {
+ printf("unknown box: type: \"%c%c%c%c\" size: %" PRIu64 "\n", type[0],
+ type[1], type[2], type[3], (uint64_t)size);
+ }
+ } else {
+ fprintf(stderr, "Unexpected decoder status\n");
+ break;
+ }
+ }
+ if (info.animation.num_loops > 1) total_duration *= info.animation.num_loops;
+ if (info.have_animation) {
+ printf("Animation length: %.3f seconds%s\n", total_duration * 0.001f,
+ (info.animation.num_loops ? "" : " (looping)"));
+ }
+ JxlDecoderDestroy(dec);
+ free(data);
+
+ return seen_basic_info;
+}
+
+static void print_usage(const char* name) {
+ fprintf(stderr,
+ "Usage: %s [-v] INPUT\n"
+ " INPUT input JPEG XL image filename(s)\n"
+ " -v more verbose output\n",
+ name);
+}
+
+static int print_basic_info_filename(const char* jxl_filename, int verbose) {
+ FILE* file = fopen(jxl_filename, "rb");
+ if (!file) {
+ fprintf(stderr, "Failed to read file: %s\n", jxl_filename);
+ return 1;
+ }
+ int status = PrintBasicInfo(file, verbose);
+ fclose(file);
+ if (!status) {
+ fprintf(stderr, "Error reading file: %s\n", jxl_filename);
+ return status;
+ }
+
+ return 0;
+}
+
+int main(int argc, char* argv[]) {
+ int verbose = 0, status = 0;
+ const char* const name = argv[0];
+
+ for (int i = 1; i < argc; i++) {
+ const char* const* help_opts =
+ (const char* const[]){"--help", "-h", "-?", NULL};
+ while (*help_opts) {
+ if (!strcmp(*help_opts++, argv[i])) {
+ print_usage(name);
+ return 0;
+ }
+ }
+ }
+
+ const char* const* verbose_opts =
+ (const char* const[]){"--verbose", "-v", NULL};
+ /* argc >= 2 gate prevents segfault on argc = 1 */
+ while (argc >= 2 && *verbose_opts) {
+ if (!strcmp(*verbose_opts++, argv[1])) {
+ verbose = 1;
+ argc--;
+ argv++;
+ break;
+ }
+ }
+
+ if (argc < 2) {
+ print_usage(name);
+ return 2;
+ }
+
+ while (argc-- >= 2) {
+ status |= print_basic_info_filename(*++argv, verbose);
+ }
+
+ return status;
+}
diff --git a/media/libjxl/src/tools/libjxl_test.c b/media/libjxl/src/tools/libjxl_test.c
new file mode 100644
index 0000000000..bb57c2d2b8
--- /dev/null
+++ b/media/libjxl/src/tools/libjxl_test.c
@@ -0,0 +1,17 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Program to test that we can link against the public API of libjpegxl from C.
+// This links against the shared libjpegxl library which doesn't expose any of
+// the internals of the jxl namespace.
+
+#include "jxl/decode.h"
+
+int main() {
+ if (!JxlDecoderVersion()) return 1;
+ JxlDecoder* dec = JxlDecoderCreate(NULL);
+ if (!dec) return 1;
+ JxlDecoderDestroy(dec);
+}
diff --git a/media/libjxl/src/tools/optimizer/simplex_fork.py b/media/libjxl/src/tools/optimizer/simplex_fork.py
new file mode 100755
index 0000000000..20de4c95c6
--- /dev/null
+++ b/media/libjxl/src/tools/optimizer/simplex_fork.py
@@ -0,0 +1,255 @@
+#!/usr/bin/python
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"""Implementation of simplex search for an external process.
+
+The external process gets the input vector through environment variables.
+Input of vector as setenv("VAR%dimension", val)
+Getting the optimized function with regexp match from stdout
+of the forked process.
+
+https://en.wikipedia.org/wiki/Nelder%E2%80%93Mead_method
+
+start as ./simplex_fork.py binary dimensions amount
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from six.moves import range
+import copy
+import os
+import random
+import re
+import subprocess
+import sys
+
+def Midpoint(simplex):
+ """Nelder-Mead-like simplex midpoint calculation."""
+ simplex.sort()
+ dim = len(simplex) - 1
+ retval = [None] + [0.0] * dim
+ for i in range(1, dim + 1):
+ for k in range(dim):
+ retval[i] += simplex[k][i]
+ retval[i] /= dim
+ return retval
+
+
+def Subtract(a, b):
+ """Vector arithmetic, with [0] being ignored."""
+ return [None if k == 0 else a[k] - b[k] for k in range(len(a))]
+
+def Add(a, b):
+ """Vector arithmetic, with [0] being ignored."""
+ return [None if k == 0 else a[k] + b[k] for k in range(len(a))]
+
+def Average(a, b):
+ """Vector arithmetic, with [0] being ignored."""
+ return [None if k == 0 else 0.5 * (a[k] + b[k]) for k in range(len(a))]
+
+
+eval_hash = {}
+
+def EvalCacheForget():
+ global eval_hash
+ eval_hash = {}
+
+def RandomizedJxlCodecs():
+ retval = []
+ minval = 0.5
+ maxval = 3.3
+ rangeval = maxval/minval
+ steps = 7
+ for i in range(steps):
+ mul = minval * rangeval**(float(i)/(steps - 1))
+ mul *= 0.99 + 0.05 * random.random()
+ retval.append("jxl:epf2:d%.3f" % mul)
+ steps = 7
+ for i in range(steps - 1):
+ mul = minval * rangeval**(float(i+0.5)/(steps - 1))
+ mul *= 0.99 + 0.05 * random.random()
+ retval.append("jxl:epf0:d%.3f" % mul)
+ return ",".join(retval)
+
+g_codecs = RandomizedJxlCodecs()
+
+def Eval(vec, binary_name, cached=True):
+ """Evaluates the objective function by forking a process.
+
+ Args:
+ vec: [0] will be set to the objective function, [1:] will
+ contain the vector position for the objective function.
+ binary_name: the name of the binary that evaluates the value.
+ """
+ global eval_hash
+ global g_codecs
+ key = ""
+ # os.environ["BUTTERAUGLI_OPTIMIZE"] = "1"
+ for i in range(300):
+ os.environ["VAR%d" % i] = "0"
+ for i in range(len(vec) - 1):
+ os.environ["VAR%d" % i] = str(vec[i + 1])
+ key += str(vec[i + 1]) + ":"
+ if cached and (key in eval_hash):
+ vec[0] = eval_hash[key]
+ return
+
+ process = subprocess.Popen(
+ (binary_name,
+ '--input',
+ '/usr/local/google/home/jyrki/mix_corpus/*.png',
+ '--error_pnorm=3',
+ '--more_columns',
+ '--codec', g_codecs),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=dict(os.environ))
+
+ # process.wait()
+ found_score = False
+ vec[0] = 1.0
+ dct2 = 0.0
+ dct4 = 0.0
+ dct16 = 0.0
+ dct32 = 0.0
+ n = 0
+ for line in process.communicate(input=None)[0].splitlines():
+ print("BE", line)
+ sys.stdout.flush()
+ if line[0:3] == b'jxl':
+ bpp = line.split()[3]
+ dist_pnorm = line.split()[7]
+ vec[0] *= float(dist_pnorm) * float(bpp) / 16.0
+ #vec[0] *= (float(dist_max) * float(bpp) / 16.0) ** 0.2
+ n += 1
+ found_score = True
+ distance = float(line.split()[0].split(b'd')[-1])
+ #faultybpp = 1.0 + 0.43 * ((float(bpp) * distance ** 0.74) - 1.57) ** 2
+ #vec[0] *= faultybpp
+
+ print("eval: ", vec)
+ if (vec[0] <= 0.0):
+ vec[0] = 1e30
+ if found_score:
+ eval_hash[key] = vec[0]
+ return
+ vec[0] = 1e33
+ return
+ # sys.exit("awful things happened")
+
+def Reflect(simplex, binary):
+ """Main iteration step of Nelder-Mead optimization. Modifies `simplex`."""
+ simplex.sort()
+ last = simplex[-1]
+ mid = Midpoint(simplex)
+ diff = Subtract(mid, last)
+ mirrored = Add(mid, diff)
+ Eval(mirrored, binary)
+ if mirrored[0] > simplex[-2][0]:
+ print("\nStill worst\n\n")
+ # Still the worst, shrink towards the best.
+ shrinking = Average(simplex[-1], simplex[0])
+ Eval(shrinking, binary)
+ print("\nshrinking...\n\n")
+ simplex[-1] = shrinking
+ return
+ if mirrored[0] < simplex[0][0]:
+ # new best
+ print("\nNew Best\n\n")
+ even_further = Add(mirrored, diff)
+ Eval(even_further, binary)
+ if even_further[0] < mirrored[0]:
+ print("\nEven Further\n\n")
+ mirrored = even_further
+ simplex[-1] = mirrored
+ # try to extend
+ return
+ else:
+ # not a best, not a worst point
+ simplex[-1] = mirrored
+
+
+def OneDimensionalSearch(simplex, shrink, index):
+ # last appended was better than the best so far, try to replace it
+ last_attempt = simplex[-1][:]
+ best = simplex[0]
+ if last_attempt[0] < best[0]:
+ # try expansion of the amount
+ diff = simplex[-1][index] - simplex[0][index]
+ simplex[-1][index] = simplex[0][index] + shrink * diff
+ Eval(simplex[-1], g_binary)
+ if simplex[-1][0] < last_attempt[0]:
+ # it got better
+ return True
+ elif last_attempt[0] >= 0:
+ diff = simplex[-1][index] - simplex[0][index]
+ simplex[-1][index] = simplex[0][index] - diff
+ Eval(simplex[-1], g_binary)
+ if simplex[-1][0] < last_attempt[0]:
+ # it got better
+ return True
+ simplex[-1] = last_attempt
+ return False
+
+def InitialSimplex(vec, dim, amount):
+ """Initialize the simplex at origin."""
+ EvalCacheForget()
+ best = vec[:]
+ Eval(best, g_binary)
+ retval = [best]
+ comp_order = list(range(1, dim + 1))
+ random.shuffle(comp_order)
+
+ for i in range(dim):
+ index = comp_order[i]
+ best = retval[0][:]
+ best[index] += amount
+ Eval(best, g_binary)
+ retval.append(best)
+ do_shrink = True
+ while OneDimensionalSearch(retval, 2.0, index):
+ print("OneDimensionalSearch-Grow")
+ while OneDimensionalSearch(retval, 1.1, index):
+ print("OneDimensionalSearch-SlowGrow")
+ do_shrink = False
+ if do_shrink:
+ while OneDimensionalSearch(retval, 0.9, index):
+ print("OneDimensionalSearch-SlowShrinking")
+ retval.sort()
+ return retval
+
+
+if len(sys.argv) != 4:
+ print("usage: ", sys.argv[0], "binary-name number-of-dimensions simplex-size")
+ exit(1)
+
+g_dim = int(sys.argv[2])
+g_amount = float(sys.argv[3])
+g_binary = sys.argv[1]
+g_simplex = InitialSimplex([None] + [0.0] * g_dim,
+ g_dim, 7.0 * g_amount)
+best = g_simplex[0][:]
+g_codecs = RandomizedJxlCodecs()
+g_simplex = InitialSimplex(best, g_dim, g_amount * 2.47)
+best = g_simplex[0][:]
+g_simplex = InitialSimplex(best, g_dim, g_amount)
+best = g_simplex[0][:]
+g_simplex = InitialSimplex(best, g_dim, g_amount * 0.33)
+best = g_simplex[0][:]
+
+for restarts in range(99999):
+ for ii in range(g_dim * 2):
+ g_simplex.sort()
+ print("reflect", ii, g_simplex[0])
+ Reflect(g_simplex, g_binary)
+
+ mulli = 0.1 + 15 * random.random()**2.0
+ g_codecs = RandomizedJxlCodecs()
+ print("\n\n\nRestart", restarts, "mulli", mulli)
+ g_simplex.sort()
+ best = g_simplex[0][:]
+ g_simplex = InitialSimplex(best, g_dim, g_amount * mulli)
diff --git a/media/libjxl/src/tools/ossfuzz-build.sh b/media/libjxl/src/tools/ossfuzz-build.sh
new file mode 100755
index 0000000000..b5fbb45b10
--- /dev/null
+++ b/media/libjxl/src/tools/ossfuzz-build.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# Helper builder file to replace the /src/build.sh one in oss-fuzz/
+
+if [[ -z "${FUZZING_ENGINE:-}" ]]; then
+ echo "Don't call this script directly. Use ./ci.sh ossfuzz_* commands" \
+ "instead." >&2
+ exit 1
+fi
+
+set -eux
+
+main() {
+ # Build the fuzzers in release mode but force the inclusion of JXL_DASSERT()
+ # checks.
+ build_args=(
+ -G Ninja
+ -DBUILD_TESTING=OFF
+ -DJPEGXL_ENABLE_BENCHMARK=OFF
+ -DJPEGXL_ENABLE_DEVTOOLS=ON
+ -DJPEGXL_ENABLE_EXAMPLES=OFF
+ -DJPEGXL_ENABLE_FUZZERS=ON
+ -DJPEGXL_ENABLE_MANPAGES=OFF
+ -DJPEGXL_ENABLE_SJPEG=OFF
+ -DJPEGXL_ENABLE_VIEWERS=OFF
+ -DCMAKE_BUILD_TYPE=Release
+ )
+ export CXXFLAGS="${CXXFLAGS} -DJXL_IS_DEBUG_BUILD=1"
+
+ mkdir -p ${WORK}
+ cd ${WORK}
+ cmake \
+ "${build_args[@]}" \
+ -DJPEGXL_FUZZER_LINK_FLAGS="${LIB_FUZZING_ENGINE}" \
+ "${SRC}/libjxl"
+
+ fuzzers=(
+ color_encoding_fuzzer
+ djxl_fuzzer
+ fields_fuzzer
+ icc_codec_fuzzer
+ rans_fuzzer
+ transforms_fuzzer
+ )
+ if [[ -n "${JPEGXL_EXTRA_ARGS:-}" ]]; then
+ # Extra arguments passed to ci.sh ossfuzz commands are treated as ninja
+ # targets. The environment variable is split into individual targets here,
+ # which might break if passing paths with spaces, which is an unlikely use
+ # case.
+ fuzzers=(${JPEGXL_EXTRA_ARGS})
+ echo "Building with targets: ${JPEGXL_EXTRA_ARGS}"
+ fi
+ ninja "${fuzzers[@]}"
+}
+
+# Build as the regular user if not already running as that user. This avoids
+# having root files in the build directory.
+if [[ -n "${JPEGXL_UID:-}" && "${JPEGXL_UID}" != $(id -u) ]]; then
+ userspec="${JPEGXL_UID}:${JPEGXL_GID}"
+ unset JPEGXL_UID
+ unset JPEGXL_GID
+ chroot --skip-chdir --userspec="${userspec}" \
+ / $(realpath "$0") "$@"
+ exit $?
+fi
+
+main "$@"
diff --git a/media/libjxl/src/tools/progressive_saliency.conf b/media/libjxl/src/tools/progressive_saliency.conf
new file mode 100644
index 0000000000..987651a431
--- /dev/null
+++ b/media/libjxl/src/tools/progressive_saliency.conf
@@ -0,0 +1,32 @@
+# Configuration parameters for progressive-saliency encoding.
+# (They are too many and too complex for command-line arguments.)
+
+# The total number of seconds for the simulated progressive-loading animation.
+simulated_progressive_loading_time_sec: 8.0
+
+# Time delay after the last progressive-loading step before the animation loops.
+simulated_progressive_loading_delay_until_looparound_sec: 10.0
+
+# The JPEG-XL encoding command, as one would pass it to the shell,
+# but with parameters ${HEATMAP_ARG}, ${INPUT}, ${OUTPUT}, ${STEPS}.
+jpegxl_encoder: cjpegxl pik ${INPUT} ${OUTPUT} --progressive --saliency_num_progressive_steps ${STEPS} --fast --saliency_threshold 0.8 ${HEATMAP_ARG}
+
+# The JPEG-XL encoding command, as one would pass it to the shell,
+# but with parameters ${INPUT}, ${OUTPUT}.
+jpegxl_decoder: djpegxl ${INPUT} ${OUTPUT}
+
+# The shell command to use for heatmap-generation.
+# This must adhere the calling conventions stated below.
+#
+# When called as:
+# {heatmap_command} {blocksize} {input_image_filename} {coarse_grained_input_filename} {output_heatmap_filename}
+# This must produce: {output_heatmap_filename} in a format that is readable by the JPEG-XL encoder, and provides one
+# grayscale value per image-block which encodes saliency - ideally in the form of block-percentiles.
+heatmap_command: ml_get_high_level_saliency
+
+# How much to blur each of the four progressive stages.
+blurring: 16x4 16x1.5 0x0 0x0
+
+# Whether to keep tempfiles.
+# Temporary files will be named by appending suffixes to the desired final output filename.
+keep_tempfiles: True
diff --git a/media/libjxl/src/tools/progressive_sizes.sh b/media/libjxl/src/tools/progressive_sizes.sh
new file mode 100755
index 0000000000..a1e808d381
--- /dev/null
+++ b/media/libjxl/src/tools/progressive_sizes.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+
+set -eu
+
+TMPDIR=$(mktemp -d)
+
+cleanup() {
+ rm -rf ${TMPDIR}
+}
+
+trap cleanup EXIT
+
+
+CJXL=$(realpath $(dirname "$0"))/../build/tools/cjxl
+DJXL=$(realpath $(dirname "$0"))/../build/tools/djxl
+
+${CJXL} "$@" ${TMPDIR}/x.jxl &>/dev/null
+S1=$(${DJXL} ${TMPDIR}/x.jxl --print_read_bytes -s 1 2>&1 | grep 'Decoded' | grep -o '[0-9]*')
+S2=$(${DJXL} ${TMPDIR}/x.jxl --print_read_bytes -s 2 2>&1 | grep 'Decoded' | grep -o '[0-9]*')
+S8=$(${DJXL} ${TMPDIR}/x.jxl --print_read_bytes -s 8 2>&1 | grep 'Decoded' | grep -o '[0-9]*')
+
+echo "8x: $S8 2x: $S2 1x: $S1"
diff --git a/media/libjxl/src/tools/rans_fuzzer.cc b/media/libjxl/src/tools/rans_fuzzer.cc
new file mode 100644
index 0000000000..7c78f0d1ca
--- /dev/null
+++ b/media/libjxl/src/tools/rans_fuzzer.cc
@@ -0,0 +1,46 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/jxl/dec_ans.h"
+#include "lib/jxl/entropy_coder.h"
+
+namespace jxl {
+
+int TestOneInput(const uint8_t* data, size_t size) {
+ if (size < 2) return 0;
+ size_t numContexts = data[0] * 256 * data[1] + 1;
+ data += 2;
+ size -= 2;
+
+ std::vector<uint8_t> context_map;
+ Status ret = true;
+ {
+ BitReader br(Span<const uint8_t>(data, size));
+ BitReaderScopedCloser br_closer(&br, &ret);
+ ANSCode code;
+ JXL_RETURN_IF_ERROR(
+ DecodeHistograms(&br, numContexts, &code, &context_map));
+ ANSSymbolReader ansreader(&code, &br);
+
+ // Limit the maximum amount of reads to avoid (valid) infinite loops.
+ const size_t maxreads = size * 8;
+ size_t numreads = 0;
+ int context = 0;
+ while (DivCeil(br.TotalBitsConsumed(), kBitsPerByte) < size &&
+ numreads <= maxreads) {
+ int code = ansreader.ReadHybridUint(context, &br, context_map);
+ context = code % numContexts;
+ numreads++;
+ }
+ }
+
+ return 0;
+}
+
+} // namespace jxl
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return jxl::TestOneInput(data, size);
+}
diff --git a/media/libjxl/src/tools/reference_zip.sh b/media/libjxl/src/tools/reference_zip.sh
new file mode 100755
index 0000000000..6a87344d2d
--- /dev/null
+++ b/media/libjxl/src/tools/reference_zip.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# Tool to create the reference software .zip package with its required
+# dependencies bundled.
+
+set -eu
+
+MYDIR=$(dirname $(realpath "$0"))
+
+# Temporary files cleanup hooks.
+CLEANUP_FILES=()
+cleanup() {
+ if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then
+ rm -fr "${CLEANUP_FILES[@]}"
+ fi
+}
+trap 'retcode=$?; { set +x; } 2>/dev/null; cleanup' INT TERM EXIT
+
+
+main() {
+ # Run from the repo's top level directory.
+ cd "${MYDIR[@]}/.."
+
+ local deps=(
+ third_party/brotli
+ third_party/highway
+ third_party/skcms
+ )
+
+ local ref_files=($(git ls-files))
+ for dep in "${deps[@]}"; do
+ local dep_files=($(git -C "${dep}" ls-files))
+ for dep_file in "${dep_files[@]}"; do
+ ref_files+=("${dep}/${dep_file}")
+ done
+ done
+
+ echo "Packaging ${#ref_files[@]} files..." >&2
+ local dest_zip="reference_package.zip"
+ rm -f "${dest_zip}"
+ printf '%s\n' "${ref_files[@]}" | zip -q -@ "${dest_zip}"
+
+ if [[ "${1:-}" == "test" ]]; then
+ echo "Testing on docker..." >&2
+ set -x
+ sudo docker run --rm -v "$(realpath ${dest_zip}):/home/pkg.zip:ro" \
+ ubuntu:20.04 <<EOF
+set -eux
+
+apt update
+DEBIAN_FRONTEND=noninteractive apt install -y build-essential zip cmake
+
+cd /home/
+unzip -q pkg.zip
+mkdir build
+cd build
+cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_SJPEG=OFF ..
+cmake --build . -- -j\$(nproc)
+
+tools/djxl ../third_party/testdata/jxl/blending/cropped_traffic_light.jxl test.png
+tools/cjxl ../third_party/testdata/third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg test.jxl
+tools/djxl test.jxl test.jpg
+EOF
+ set +x
+ fi
+ echo "${dest_zip} ready."
+}
+
+main "$@"
diff --git a/media/libjxl/src/tools/set_from_bytes_fuzzer.cc b/media/libjxl/src/tools/set_from_bytes_fuzzer.cc
new file mode 100644
index 0000000000..5eb9f750e0
--- /dev/null
+++ b/media/libjxl/src/tools/set_from_bytes_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "lib/extras/codec.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+
+namespace jxl {
+
+int TestOneInput(const uint8_t* data, size_t size) {
+ CodecInOut io;
+ io.constraints.dec_max_xsize = 1u << 16;
+ io.constraints.dec_max_ysize = 1u << 16;
+ io.constraints.dec_max_pixels = 1u << 22;
+ ThreadPoolInternal pool(0);
+
+ (void)SetFromBytes(Span<const uint8_t>(data, size), &io, &pool);
+
+ return 0;
+}
+
+} // namespace jxl
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return jxl::TestOneInput(data, size);
+}
diff --git a/media/libjxl/src/tools/speed_stats.cc b/media/libjxl/src/tools/speed_stats.cc
new file mode 100644
index 0000000000..3ab271f964
--- /dev/null
+++ b/media/libjxl/src/tools/speed_stats.cc
@@ -0,0 +1,117 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/speed_stats.h"
+
+#include <inttypes.h>
+#include <math.h>
+#include <stddef.h>
+#include <stdio.h>
+
+#include <algorithm>
+#include <string>
+
+namespace jpegxl {
+namespace tools {
+
+void SpeedStats::NotifyElapsed(double elapsed_seconds) {
+ JXL_ASSERT(elapsed_seconds > 0.0);
+ elapsed_.push_back(elapsed_seconds);
+}
+
+jxl::Status SpeedStats::GetSummary(SpeedStats::Summary* s) {
+ if (elapsed_.empty()) return JXL_FAILURE("Didn't call NotifyElapsed");
+
+ s->min = *std::min_element(elapsed_.begin(), elapsed_.end());
+ s->max = *std::max_element(elapsed_.begin(), elapsed_.end());
+
+ // Single rep
+ if (elapsed_.size() == 1) {
+ s->central_tendency = elapsed_[0];
+ s->variability = 0.0;
+ s->type = "";
+ return true;
+ }
+
+ // Two: skip first (noisier)
+ if (elapsed_.size() == 2) {
+ s->central_tendency = elapsed_[1];
+ s->variability = 0.0;
+ s->type = " second:";
+ return true;
+ }
+
+ // Prefer geomean unless numerically unreliable (too many reps)
+ if (pow(elapsed_[0], elapsed_.size()) < 1E100) {
+ double product = 1.0;
+ for (size_t i = 1; i < elapsed_.size(); ++i) {
+ product *= elapsed_[i];
+ }
+
+ s->central_tendency = pow(product, 1.0 / (elapsed_.size() - 1));
+ s->variability = 0.0;
+ s->type = " geomean:";
+ return true;
+ }
+
+ // Else: median
+ std::sort(elapsed_.begin(), elapsed_.end());
+ s->central_tendency = elapsed_.data()[elapsed_.size() / 2];
+ std::vector<double> deviations(elapsed_.size());
+ for (size_t i = 0; i < elapsed_.size(); i++) {
+ deviations[i] = fabs(elapsed_[i] - s->central_tendency);
+ }
+ std::nth_element(deviations.begin(),
+ deviations.begin() + deviations.size() / 2,
+ deviations.end());
+ s->variability = deviations[deviations.size() / 2];
+ s->type = "median: ";
+ return true;
+}
+
+namespace {
+
+std::string SummaryStat(double value, const char* unit,
+ const SpeedStats::Summary& s) {
+ if (value == 0.) return "";
+
+ char stat_str[100] = {'\0'};
+ const double value_tendency = value / s.central_tendency;
+ // Note flipped order: higher elapsed = lower mpps.
+ const double value_min = value / s.max;
+ const double value_max = value / s.min;
+
+ int ret = snprintf(stat_str, sizeof(stat_str), ",%s %.2f %s/s [%.2f, %.2f]",
+ s.type, value_tendency, unit, value_min, value_max);
+ (void)ret; // ret is unused when JXL_ASSERT is disabled.
+ JXL_ASSERT(ret < static_cast<int>(sizeof(stat_str)));
+ return stat_str;
+}
+
+} // namespace
+
+jxl::Status SpeedStats::Print(size_t worker_threads) {
+ Summary s;
+ JXL_RETURN_IF_ERROR(GetSummary(&s));
+ std::string mps_stats = SummaryStat(xsize_ * ysize_ * 1e-6, "MP", s);
+ std::string mbs_stats = SummaryStat(file_size_ * 1e-6, "MB", s);
+
+ char variability[20] = {'\0'};
+ if (s.variability != 0.0) {
+ snprintf(variability, sizeof(variability), " (var %.2f)", s.variability);
+ }
+
+ fprintf(stderr,
+ "%" PRIu64 " x %" PRIu64 "%s%s%s, %" PRIu64 " reps, %" PRIu64
+ " threads.\n",
+ static_cast<uint64_t>(xsize_), static_cast<uint64_t>(ysize_),
+ mps_stats.c_str(), mbs_stats.c_str(), variability,
+ static_cast<uint64_t>(elapsed_.size()),
+ static_cast<uint64_t>(worker_threads));
+ return true;
+}
+
+} // namespace tools
+} // namespace jpegxl
diff --git a/media/libjxl/src/tools/speed_stats.h b/media/libjxl/src/tools/speed_stats.h
new file mode 100644
index 0000000000..eec8a58586
--- /dev/null
+++ b/media/libjxl/src/tools/speed_stats.h
@@ -0,0 +1,63 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_SPEED_STATS_H_
+#define TOOLS_SPEED_STATS_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <vector>
+
+#include "lib/jxl/base/status.h"
+
+namespace jpegxl {
+namespace tools {
+
+class SpeedStats {
+ public:
+ void NotifyElapsed(double elapsed_seconds);
+
+ struct Summary {
+ // How central_tendency was computed - depends on number of reps.
+ const char* type;
+
+ // Elapsed time
+ double central_tendency;
+ double min;
+ double max;
+ double variability;
+ };
+
+ // Non-const, may sort elapsed_.
+ jxl::Status GetSummary(Summary* summary);
+
+ // Sets the image size to allow computing MP/s values.
+ void SetImageSize(size_t xsize, size_t ysize) {
+ xsize_ = xsize;
+ ysize_ = ysize;
+ }
+
+ // Sets the file size to allow computing MB/s values.
+ void SetFileSize(size_t file_size) { file_size_ = file_size; }
+
+ // Calls GetSummary and prints megapixels/sec. SetImageSize() must be called
+ // once before this can be used.
+ jxl::Status Print(size_t worker_threads);
+
+ private:
+ std::vector<double> elapsed_;
+ size_t xsize_ = 0;
+ size_t ysize_ = 0;
+
+ // Size of the source binary file, meaningful when decoding a recompressed
+ // JPEG.
+ size_t file_size_ = 0;
+};
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_SPEED_STATS_H_
diff --git a/media/libjxl/src/tools/ssimulacra.cc b/media/libjxl/src/tools/ssimulacra.cc
new file mode 100644
index 0000000000..9ce61b9c74
--- /dev/null
+++ b/media/libjxl/src/tools/ssimulacra.cc
@@ -0,0 +1,331 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Re-implementation of //tools/ssimulacra.tct using jxl's
+// ImageF library instead of opencv.
+
+#include "tools/ssimulacra.h"
+
+#include <cmath>
+
+#include "lib/jxl/gauss_blur.h"
+#include "lib/jxl/image_ops.h"
+
+namespace ssimulacra {
+namespace {
+
+using jxl::Image3F;
+using jxl::ImageF;
+
+static const float kC1 = 0.0001f;
+static const float kC2 = 0.0004f;
+static const int kNumScales = 6;
+// Premultiplied by chroma weight 0.2
+static const double kScaleWeights[kNumScales][3] = {
+ {0.04480, 0.00300, 0.00300}, {0.28560, 0.00896, 0.00896},
+ {0.30010, 0.05712, 0.05712}, {0.23630, 0.06002, 0.06002},
+ {0.13330, 0.06726, 0.06726}, {0.10000, 0.05000, 0.05000},
+};
+// Premultiplied by min weights 0.1, 0.005, 0.005
+const double kMinScaleWeights[kNumScales][3] = {
+ {0.02000, 0.00005, 0.00005}, {0.03000, 0.00025, 0.00025},
+ {0.02500, 0.00100, 0.00100}, {0.02000, 0.00150, 0.00150},
+ {0.01200, 0.00175, 0.00175}, {0.00500, 0.00175, 0.00175},
+};
+const double kEdgeWeight[3] = {1.5, 0.1, 0.1};
+const double kGridWeight[3] = {1.0, 0.1, 0.1};
+
+inline void Rgb2Lab(float r, float g, float b, float* L, float* A, float* B) {
+ const float epsilon = 0.00885645167903563081f;
+ const float s = 0.13793103448275862068f;
+ const float k = 7.78703703703703703703f;
+ float fx = (r * 0.43393624408206207259f + g * 0.37619779063650710152f +
+ b * 0.18983429773803261441f);
+ float fy = (r * 0.2126729f + g * 0.7151522f + b * 0.0721750f);
+ float fz = (r * 0.01775381083562901744f + g * 0.10945087235996326905f +
+ b * 0.87263921028466483011f);
+ const float gamma = 1.0f / 3.0f;
+ float X = (fx > epsilon) ? powf(fx, gamma) - s : k * fx;
+ float Y = (fy > epsilon) ? powf(fy, gamma) - s : k * fy;
+ float Z = (fz > epsilon) ? powf(fz, gamma) - s : k * fz;
+ *L = Y * 1.16f;
+ *A = (0.39181818181818181818f + 2.27272727272727272727f * (X - Y));
+ *B = (0.49045454545454545454f + 0.90909090909090909090f * (Y - Z));
+}
+
+Image3F Rgb2Lab(const Image3F& in) {
+ Image3F out(in.xsize(), in.ysize());
+ for (size_t y = 0; y < in.ysize(); ++y) {
+ const float* JXL_RESTRICT row_in0 = in.PlaneRow(0, y);
+ const float* JXL_RESTRICT row_in1 = in.PlaneRow(1, y);
+ const float* JXL_RESTRICT row_in2 = in.PlaneRow(2, y);
+ float* JXL_RESTRICT row_out0 = out.PlaneRow(0, y);
+ float* JXL_RESTRICT row_out1 = out.PlaneRow(1, y);
+ float* JXL_RESTRICT row_out2 = out.PlaneRow(2, y);
+
+ for (size_t x = 0; x < in.xsize(); ++x) {
+ Rgb2Lab(row_in0[x], row_in1[x], row_in2[x], &row_out0[x], &row_out1[x],
+ &row_out2[x]);
+ }
+ }
+ return out;
+}
+
+Image3F Downsample(const Image3F& in, size_t fx, size_t fy) {
+ const size_t out_xsize = (in.xsize() + fx - 1) / fx;
+ const size_t out_ysize = (in.ysize() + fy - 1) / fy;
+ Image3F out(out_xsize, out_ysize);
+ const float normalize = 1.0f / (fx * fy);
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t oy = 0; oy < out_ysize; ++oy) {
+ float* JXL_RESTRICT row_out = out.PlaneRow(c, oy);
+ for (size_t ox = 0; ox < out_xsize; ++ox) {
+ float sum = 0.0f;
+ for (size_t iy = 0; iy < fy; ++iy) {
+ for (size_t ix = 0; ix < fx; ++ix) {
+ const size_t x = std::min(ox * fx + ix, in.xsize() - 1);
+ const size_t y = std::min(oy * fy + iy, in.ysize() - 1);
+ sum += in.PlaneRow(c, y)[x];
+ }
+ }
+ row_out[ox] = sum * normalize;
+ }
+ }
+ }
+ return out;
+}
+
+void Multiply(const Image3F& a, const Image3F& b, Image3F* mul) {
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < a.ysize(); ++y) {
+ const float* JXL_RESTRICT in1 = a.PlaneRow(c, y);
+ const float* JXL_RESTRICT in2 = b.PlaneRow(c, y);
+ float* JXL_RESTRICT out = mul->PlaneRow(c, y);
+ for (size_t x = 0; x < a.xsize(); ++x) {
+ out[x] = in1[x] * in2[x];
+ }
+ }
+ }
+}
+
+void RowColAvgP2(const ImageF& in, double* rp2, double* cp2) {
+ std::vector<double> ravg(in.ysize());
+ std::vector<double> cavg(in.xsize());
+ for (size_t y = 0; y < in.ysize(); ++y) {
+ auto row = in.Row(y);
+ for (size_t x = 0; x < in.xsize(); ++x) {
+ const float val = row[x];
+ ravg[y] += val;
+ cavg[x] += val;
+ }
+ }
+ std::sort(ravg.begin(), ravg.end());
+ std::sort(cavg.begin(), cavg.end());
+ *rp2 = ravg[ravg.size() / 50] / in.xsize();
+ *cp2 = cavg[cavg.size() / 50] / in.ysize();
+}
+
+class StreamingAverage {
+ public:
+ void Add(const float v) {
+ // Numerically stable method.
+ double delta = v - result_;
+ n_ += 1;
+ result_ += delta / n_;
+ }
+
+ double Get() const { return result_; }
+
+ private:
+ double result_ = 0.0;
+ size_t n_ = 0;
+};
+
+void EdgeDiffMap(const Image3F& img1, const Image3F& mu1, const Image3F& img2,
+ const Image3F& mu2, Image3F* out, double* plane_avg) {
+ for (size_t c = 0; c < 3; ++c) {
+ StreamingAverage avg;
+ for (size_t y = 0; y < img1.ysize(); ++y) {
+ const float* JXL_RESTRICT row1 = img1.PlaneRow(c, y);
+ const float* JXL_RESTRICT row2 = img2.PlaneRow(c, y);
+ const float* JXL_RESTRICT rowm1 = mu1.PlaneRow(c, y);
+ const float* JXL_RESTRICT rowm2 = mu2.PlaneRow(c, y);
+ float* JXL_RESTRICT row_out = out->PlaneRow(c, y);
+ for (size_t x = 0; x < img1.xsize(); ++x) {
+ float edgediff = std::max(
+ std::abs(row2[x] - rowm2[x]) - std::abs(row1[x] - rowm1[x]), 0.0f);
+ row_out[x] = 1.0f - edgediff;
+ avg.Add(row_out[x]);
+ }
+ }
+ plane_avg[c] = avg.Get();
+ }
+}
+
+// Temporary storage for Gaussian blur, reused for multiple images.
+class Blur {
+ public:
+ Blur(const size_t xsize, const size_t ysize)
+ : rg_(jxl::CreateRecursiveGaussian(1.5)), temp_(xsize, ysize) {}
+
+ void operator()(const ImageF& in, ImageF* JXL_RESTRICT out) {
+ jxl::ThreadPool* null_pool = nullptr;
+ FastGaussian(rg_, in, null_pool, &temp_, out);
+ }
+
+ Image3F operator()(const Image3F& in) {
+ Image3F out(in.xsize(), in.ysize());
+ operator()(in.Plane(0), &out.Plane(0));
+ operator()(in.Plane(1), &out.Plane(1));
+ operator()(in.Plane(2), &out.Plane(2));
+ return out;
+ }
+
+ // Allows reusing across scales.
+ void ShrinkTo(const size_t xsize, const size_t ysize) {
+ temp_.ShrinkTo(xsize, ysize);
+ }
+
+ private:
+ hwy::AlignedUniquePtr<jxl::RecursiveGaussian> rg_;
+ ImageF temp_;
+};
+
+void SSIMMap(const Image3F& m1, const Image3F& m2, const Image3F& s11,
+ const Image3F& s22, const Image3F& s12, Image3F* out,
+ double* plane_averages) {
+ for (size_t c = 0; c < 3; ++c) {
+ StreamingAverage avg;
+ for (size_t y = 0; y < out->ysize(); ++y) {
+ const float* JXL_RESTRICT row_m1 = m1.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_m2 = m2.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s11 = s11.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s22 = s22.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s12 = s12.PlaneRow(c, y);
+ float* JXL_RESTRICT row_out = out->PlaneRow(c, y);
+ for (size_t x = 0; x < out->xsize(); ++x) {
+ float mu1 = row_m1[x];
+ float mu2 = row_m2[x];
+ float mu11 = mu1 * mu1;
+ float mu22 = mu2 * mu2;
+ float mu12 = mu1 * mu2;
+ float nom_m = 2 * mu12 + kC1;
+ float nom_s = 2 * (row_s12[x] - mu12) + kC2;
+ float denom_m = mu11 + mu22 + kC1;
+ float denom_s = (row_s11[x] - mu11) + (row_s22[x] - mu22) + kC2;
+ row_out[x] = (nom_m * nom_s) / (denom_m * denom_s);
+ avg.Add(row_out[x]);
+ }
+ }
+ plane_averages[c] = avg.Get();
+ }
+}
+
+} // namespace
+
+double Ssimulacra::Score() const {
+ double ssim = 0.0;
+ double ssim_max = 0.0;
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t scale = 0; scale < scales.size(); ++scale) {
+ ssim += kScaleWeights[scale][c] * scales[scale].avg_ssim[c];
+ ssim_max += kScaleWeights[scale][c];
+ ssim += kMinScaleWeights[scale][c] * scales[scale].min_ssim[c];
+ ssim_max += kMinScaleWeights[scale][c];
+ }
+ if (!simple) {
+ ssim += kEdgeWeight[c] * avg_edgediff[c];
+ ssim_max += kEdgeWeight[c];
+ ssim += kGridWeight[c] *
+ (row_p2[0][c] + row_p2[1][c] + col_p2[0][c] + col_p2[1][c]);
+ ssim_max += 4.0 * kGridWeight[c];
+ }
+ }
+ double dssim = ssim_max / ssim - 1.0;
+ return std::min(1.0, std::max(0.0, dssim));
+}
+
+inline void PrintItem(const char* name, int scale, const double* vals,
+ const double* w) {
+ printf("scale %d %s = [%.10f %.10f %.10f] w = [%.5f %.5f %.5f]\n", scale,
+ name, vals[0], vals[1], vals[2], w[0], w[1], w[2]);
+}
+
+void Ssimulacra::PrintDetails() const {
+ for (size_t s = 0; s < scales.size(); ++s) {
+ if (s < kNumScales) {
+ PrintItem("avg ssim", s, scales[s].avg_ssim, kScaleWeights[s]);
+ PrintItem("min ssim", s, scales[s].min_ssim, kMinScaleWeights[s]);
+ }
+ if (s == 0 && !simple) {
+ PrintItem("avg edif", s, avg_edgediff, kEdgeWeight);
+ PrintItem("rp2 ssim", s, &row_p2[0][0], kGridWeight);
+ PrintItem("cp2 ssim", s, &col_p2[0][0], kGridWeight);
+ PrintItem("rp2 edif", s, &row_p2[1][0], kGridWeight);
+ PrintItem("cp2 edif", s, &col_p2[1][0], kGridWeight);
+ }
+ }
+}
+
+Ssimulacra ComputeDiff(const Image3F& orig, const Image3F& distorted,
+ bool simple) {
+ Ssimulacra ssimulacra;
+
+ ssimulacra.simple = simple;
+ Image3F img1 = Rgb2Lab(orig);
+ Image3F img2 = Rgb2Lab(distorted);
+
+ Image3F mul(orig.xsize(), orig.ysize());
+ Blur blur(img1.xsize(), img1.ysize());
+
+ for (int scale = 0; scale < kNumScales; scale++) {
+ if (img1.xsize() < 8 || img1.ysize() < 8) {
+ break;
+ }
+ if (scale) {
+ img1 = Downsample(img1, 2, 2);
+ img2 = Downsample(img2, 2, 2);
+ }
+ mul.ShrinkTo(img1.xsize(), img2.ysize());
+ blur.ShrinkTo(img1.xsize(), img2.ysize());
+
+ Multiply(img1, img1, &mul);
+ Image3F sigma1_sq = blur(mul);
+
+ Multiply(img2, img2, &mul);
+ Image3F sigma2_sq = blur(mul);
+
+ Multiply(img1, img2, &mul);
+ Image3F sigma12 = blur(mul);
+
+ Image3F mu1 = blur(img1);
+ Image3F mu2 = blur(img2);
+ // Reuse mul as "ssim_map".
+ SsimulacraScale sscale;
+ SSIMMap(mu1, mu2, sigma1_sq, sigma2_sq, sigma12, &mul, sscale.avg_ssim);
+
+ const Image3F ssim_map = Downsample(mul, 4, 4);
+ for (size_t c = 0; c < 3; c++) {
+ float minval, maxval;
+ ImageMinMax(ssim_map.Plane(c), &minval, &maxval);
+ sscale.min_ssim[c] = static_cast<double>(minval);
+ }
+ ssimulacra.scales.push_back(sscale);
+
+ if (scale == 0 && !simple) {
+ Image3F* edgediff = &sigma1_sq; // reuse
+ EdgeDiffMap(img1, mu1, img2, mu2, edgediff, ssimulacra.avg_edgediff);
+ for (size_t c = 0; c < 3; c++) {
+ RowColAvgP2(ssim_map.Plane(c), &ssimulacra.row_p2[0][c],
+ &ssimulacra.col_p2[0][c]);
+ RowColAvgP2(edgediff->Plane(c), &ssimulacra.row_p2[1][c],
+ &ssimulacra.col_p2[1][c]);
+ }
+ }
+ }
+ return ssimulacra;
+}
+
+} // namespace ssimulacra
diff --git a/media/libjxl/src/tools/ssimulacra.h b/media/libjxl/src/tools/ssimulacra.h
new file mode 100644
index 0000000000..95fc9de903
--- /dev/null
+++ b/media/libjxl/src/tools/ssimulacra.h
@@ -0,0 +1,36 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_SSIMULACRA_H_
+#define TOOLS_SSIMULACRA_H_
+
+#include <vector>
+
+#include "lib/jxl/image.h"
+
+namespace ssimulacra {
+
+struct SsimulacraScale {
+ double avg_ssim[3];
+ double min_ssim[3];
+};
+
+struct Ssimulacra {
+ std::vector<SsimulacraScale> scales;
+ double avg_edgediff[3];
+ double row_p2[2][3];
+ double col_p2[2][3];
+ bool simple;
+
+ double Score() const;
+ void PrintDetails() const;
+};
+
+Ssimulacra ComputeDiff(const jxl::Image3F& orig, const jxl::Image3F& distorted,
+ bool simple);
+
+} // namespace ssimulacra
+
+#endif // TOOLS_SSIMULACRA_H_
diff --git a/media/libjxl/src/tools/ssimulacra.txt b/media/libjxl/src/tools/ssimulacra.txt
new file mode 100644
index 0000000000..cedda2ae13
--- /dev/null
+++ b/media/libjxl/src/tools/ssimulacra.txt
@@ -0,0 +1,382 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+ SSIMULACRA - Structural SIMilarity Unveiling Local And Compression Related Artifacts
+
+ Cloudinary's variant of DSSIM, based on Philipp Klaus Krause's adaptation of Rabah Mehdi's SSIM implementation,
+ using ideas from Kornel Lesinski's DSSIM implementation as well as several new ideas.
+
+
+
+
+ Changes compared to Krause's SSIM implementation:
+ - Use C++ OpenCV API
+ - Convert sRGB to linear RGB and then to L*a*b*, to get a perceptually more accurate color space
+ - Multi-scale (6 scales)
+ - Extra penalty for specific kinds of artifacts:
+ - local artifacts
+ - grid-like artifacts (blockiness)
+ - introducing edges where the original is smooth (blockiness / color banding / ringing / mosquito noise)
+
+ Known limitations:
+ - Color profiles are ignored; input images are assumed to be sRGB.
+ - Both input images need to have the same number of channels (Grayscale / RGB / RGBA)
+*/
+
+/*
+ This DSSIM program has been created by Philipp Klaus Krause based on
+ Rabah Mehdi's C++ implementation of SSIM (http://mehdi.rabah.free.fr/SSIM).
+ Originally it has been created for the VMV '09 paper
+ "ftc - floating precision texture compression" by Philipp Klaus Krause.
+
+ The latest version of this program can probably be found somewhere at
+ http://www.colecovision.eu.
+
+ It can be compiled using g++ -I/usr/include/opencv -lcv -lhighgui dssim.cpp
+ Make sure OpenCV is installed (e.g. for Debian/ubuntu: apt-get install
+ libcv-dev libhighgui-dev).
+
+ DSSIM is described in
+ "Structural Similarity-Based Object Tracking in Video Sequences" by Loza et al.
+ however setting all Ci to 0 as proposed there results in numerical instabilities.
+ Thus this implementation used the Ci from the SSIM implementation.
+ SSIM is described in
+ "Image quality assessment: from error visibility to structural similarity" by Wang et al.
+*/
+
+/*
+ Copyright (c) 2005, Rabah Mehdi <mehdi.rabah@gmail.com>
+
+ Feel free to use it as you want and to drop me a mail
+ if it has been useful to you. Please let me know if you enhance it.
+ I'm not responsible if this program destroy your life & blablabla :)
+
+ Copyright (c) 2009, Philipp Klaus Krause <philipp@colecovision.eu>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include <cv.hpp>
+#include <highgui.h>
+#include <stdio.h>
+#include <set>
+
+// comment this in to produce debug images that show the differences at each scale
+#define DEBUG_IMAGES 1
+using namespace std;
+using namespace cv;
+
+// All of the constants below are more or less arbitrary.
+// Some amount of tweaking/calibration was done, but there is certainly room for improvement.
+
+// SSIM constants. Original C2 was 0.0009, but a smaller value seems to work slightly better.
+const double C1 = 0.0001, C2 = 0.0004;
+
+// Weight of each scale. Somewhat arbitrary.
+// These are based on the values used in IW-SSIM and Kornel's DSSIM.
+// It seems weird to give so little weight to the full-size scale, but then again,
+// differences in more zoomed-out scales have more visual impact.
+// Anyway, these weights seem to work.
+// Added one more scale compared to IW-SSIM and Kornel's DSSIM.
+// Weights for chroma are modified to give more weight to larger scales (similar to Kornel's subsampled chroma)
+const float scale_weights[4][6] = {
+ // 1:1 1:2 1:4 1:8 1:16 1:32
+ {0.0448, 0.2856, 0.3001, 0.2363, 0.1333, 0.1 },
+ {0.015, 0.0448, 0.2856, 0.3001, 0.3363, 0.25 },
+ {0.015, 0.0448, 0.2856, 0.3001, 0.3363, 0.25 },
+ {0.0448, 0.2856, 0.3001, 0.2363, 0.1333, 0.1 },
+ };
+
+// higher value means more importance to chroma (weights above are multiplied by this factor for chroma and alpha)
+const double chroma_weight = 0.2;
+
+// Weights for the worst-case (minimum) score at each scale.
+// Higher value means more importance to worst artifacts, lower value means more importance to average artifacts.
+const float mscale_weights[4][6] = {
+ // 1:4 1:8 1:16 1:32 1:64 1:128
+ {0.2, 0.3, 0.25, 0.2, 0.12, 0.05},
+ {0.01, 0.05, 0.2, 0.3, 0.35, 0.35},
+ {0.01, 0.05, 0.2, 0.3, 0.35, 0.35},
+ {0.2, 0.3, 0.25, 0.2, 0.12, 0.05},
+ };
+
+
+// higher value means more importance to worst local artifacts
+const double min_weight[4] = {0.1,0.005,0.005,0.005};
+
+// higher value means more importance to artifact-edges (edges where original is smooth)
+const double extra_edges_weight[4] = {1.5, 0.1, 0.1, 0.5};
+
+// higher value means more importance to grid-like artifacts (blockiness)
+const double worst_grid_weight[2][4] =
+ { {1.0, 0.1, 0.1, 0.5}, // on ssim heatmap
+ {1.0, 0.1, 0.1, 0.5} }; // on extra_edges heatmap
+
+
+// Convert linear RGB to L*a*b* (all in 0..1 range)
+inline void rgb2lab(Vec3f &p) __attribute__ ((hot));
+inline void rgb2lab(Vec3f &p) {
+ const float epsilon = 0.00885645167903563081f;
+ const float s = 0.13793103448275862068f;
+ const float k = 7.78703703703703703703f;
+
+ // D65 adjustment included
+ float fx = (p[2] * 0.43393624408206207259f + p[1] * 0.37619779063650710152f + p[0] * .18983429773803261441f) ;
+ float fy = (p[2] * 0.2126729f + p[1] * 0.7151522f + p[0] * 0.0721750f);
+ float fz = (p[2] * 0.01775381083562901744f + p[1] * 0.10945087235996326905f + p[0] * 0.87263921028466483011f) ;
+
+ float X = (fx > epsilon) ? powf(fx,1.0f/3.0f) - s : k * fx;
+ float Y = (fy > epsilon) ? powf(fy,1.0f/3.0f) - s : k * fy;
+ float Z = (fz > epsilon) ? powf(fz,1.0f/3.0f) - s : k * fz;
+
+ p[0] = Y * 1.16f;
+ p[1] = (0.39181818181818181818f + 2.27272727272727272727f * (X - Y));
+ p[2] = (0.49045454545454545454f + 0.90909090909090909090f * (Y - Z));
+}
+
+
+int main(int argc, char** argv) {
+
+ if(argc!=3) {
+ fprintf(stderr, "Usage: %s orig_image distorted_image\n", argv[0]);
+ fprintf(stderr, "Returns a value between 0 (images are identical) and 1 (images are very different)\n");
+ fprintf(stderr, "If the value is above 0.1 (or so), the distortion is likely to be perceptible / annoying.\n");
+ fprintf(stderr, "If the value is below 0.01 (or so), the distortion is likely to be imperceptible.\n");
+ return(-1);
+ }
+
+ Scalar sC1 = {C1,C1,C1,C1}, sC2 = {C2,C2,C2,C2};
+
+ Mat img1, img2, img1_img2, img1_temp, img2_temp, img1_sq, img2_sq, mu1, mu2, mu1_sq, mu2_sq, mu1_mu2, sigma1_sq, sigma2_sq, sigma12, ssim_map;
+
+ // read and validate input images
+
+ img1_temp = imread(argv[1],-1);
+ img2_temp = imread(argv[2],-1);
+
+ int nChan = img1_temp.channels();
+ if (nChan != img2_temp.channels()) {
+ fprintf(stderr, "Image file %s has %i channels, while\n", argv[1], nChan);
+ fprintf(stderr, "image file %s has %i channels. Can't compare.\n", argv[2], img2_temp.channels());
+ return -1;
+ }
+ if (img1_temp.size() != img2_temp.size()) {
+ fprintf(stderr, "Image dimensions have to be identical.\n");
+ return -1;
+ }
+ if (img1_temp.cols < 8 || img1_temp.rows < 8) {
+ fprintf(stderr, "Image is too small; need at least 8 rows and columns.\n");
+ return -1;
+ }
+ int pixels = img1_temp.rows * img1_temp.cols;
+ if (nChan == 4) {
+ // blend to a gray background to have a fair comparison of semi-transparent RGB values
+ for( int i=0 ; i < pixels; i++ ) {
+ Vec4b & p = img1_temp.at<Vec4b>(i);
+ p[0] = (p[3]*p[0] + (255-p[3])*128 ) / 255;
+ p[1] = (p[3]*p[1] + (255-p[3])*128 ) / 255;
+ p[2] = (p[3]*p[2] + (255-p[3])*128 ) / 255;
+ }
+ for( int i=0 ; i < pixels; i++ ) {
+ Vec4b & p = img2_temp.at<Vec4b>(i);
+ p[0] = (p[3]*p[0] + (255-p[3])*128 ) / 255;
+ p[1] = (p[3]*p[1] + (255-p[3])*128 ) / 255;
+ p[2] = (p[3]*p[2] + (255-p[3])*128 ) / 255;
+ }
+ }
+
+
+ if (nChan > 1) {
+ // Create lookup table to convert 8-bit sRGB to linear RGB
+ Mat sRGB_gamma_LUT(1, 256, CV_32FC1);
+ for (int i = 0; i < 256; i++) {
+ float c = i / 255.0;
+ sRGB_gamma_LUT.at<float>(i) = (c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4));
+ }
+
+ // Convert from sRGB to linear RGB
+ LUT(img1_temp, sRGB_gamma_LUT, img1);
+ LUT(img2_temp, sRGB_gamma_LUT, img2);
+ } else {
+ img1 = Mat(img1_temp.rows, img1_temp.cols, CV_32FC1);
+ img2 = Mat(img1_temp.rows, img1_temp.cols, CV_32FC1);
+ }
+
+ // Convert from linear RGB to Lab in a 0..1 range
+ if (nChan == 3) {
+ for( int i=0 ; i < pixels; i++ ) rgb2lab(img1.at<Vec3f>(i));
+ for( int i=0 ; i < pixels; i++ ) rgb2lab(img2.at<Vec3f>(i));
+ } else if (nChan == 4) {
+ for( int i=0 ; i < pixels; i++ ) { Vec3f p = {img1.at<Vec4f>(i)[0],img1.at<Vec4f>(i)[1],img1.at<Vec4f>(i)[2]}; rgb2lab(p); img1.at<Vec4f>(i)[0] = p[0]; img1.at<Vec4f>(i)[1] = p[1]; img1.at<Vec4f>(i)[2] = p[2];}
+ for( int i=0 ; i < pixels; i++ ) { Vec3f p = {img2.at<Vec4f>(i)[0],img2.at<Vec4f>(i)[1],img2.at<Vec4f>(i)[2]}; rgb2lab(p); img2.at<Vec4f>(i)[0] = p[0]; img2.at<Vec4f>(i)[1] = p[1]; img2.at<Vec4f>(i)[2] = p[2];}
+ } else if (nChan == 1) {
+ for( int i=0 ; i < pixels; i++ ) { img1.at<float>(i) = img1_temp.at<uchar>(i)/255.0;}
+ for( int i=0 ; i < pixels; i++ ) { img2.at<float>(i) = img2_temp.at<uchar>(i)/255.0;}
+ } else {
+ fprintf(stderr, "Can only deal with Grayscale, RGB or RGBA input.\n");
+ return(-1);
+ }
+
+
+ double dssim=0, dssim_max=0;
+
+ for (int scale = 0; scale < 6; scale++) {
+
+ if (img1.cols < 8 || img1.rows < 8) break;
+ if (scale) {
+ // scale down 50% in each iteration.
+ resize(img1, img1, Size(), 0.5, 0.5, INTER_AREA);
+ resize(img2, img2, Size(), 0.5, 0.5, INTER_AREA);
+ }
+
+ // Standard SSIM computation
+ cv::pow( img1, 2, img1_sq );
+ cv::pow( img2, 2, img2_sq );
+
+ multiply( img1, img2, img1_img2, 1 );
+
+ GaussianBlur(img1, mu1, Size(11,11), 1.5);
+ GaussianBlur(img2, mu2, Size(11,11), 1.5);
+
+ cv::pow( mu1, 2, mu1_sq );
+ cv::pow( mu2, 2, mu2_sq );
+ multiply( mu1, mu2, mu1_mu2, 1 );
+
+ GaussianBlur(img1_sq, sigma1_sq, Size(11,11), 1.5);
+ addWeighted( sigma1_sq, 1, mu1_sq, -1, 0, sigma1_sq );
+
+ GaussianBlur(img2_sq, sigma2_sq, Size(11,11), 1.5);
+ addWeighted( sigma2_sq, 1, mu2_sq, -1, 0, sigma2_sq );
+
+ GaussianBlur(img1_img2, sigma12, Size(11,11), 1.5);
+ addWeighted( sigma12, 1, mu1_mu2, -1, 0, sigma12 );
+
+ ssim_map = ((2*mu1_mu2 + sC1).mul(2*sigma12 + sC2))/((mu1_sq + mu2_sq + sC1).mul(sigma1_sq + sigma2_sq + sC2));
+
+
+ // optional: write a nice debug image that shows the problematic areas
+#ifdef DEBUG_IMAGES
+ Mat ssim_image;
+ ssim_map.convertTo(ssim_image,CV_8UC3,255);
+ for( int i=0 ; i < ssim_image.rows * ssim_image.cols; i++ ) {
+ Vec3b &p = ssim_image.at<Vec3b>(i);
+ p = {(uchar)(255-p[2]),(uchar)(255-p[0]),(uchar)(255-p[1])};
+ }
+ imwrite("debug-scale"+to_string(scale)+".png",ssim_image);
+#endif
+
+
+ // average ssim over the entire image
+ Scalar avg = mean( ssim_map );
+ for(unsigned int i = 0; i < nChan; i++) {
+ printf("avg: %i %f\n",i,avg[i]);
+ dssim += (i>0?chroma_weight:1.0) * avg[i] * scale_weights[i][scale];
+ dssim_max += (i>0?chroma_weight:1.0) * scale_weights[i][scale];
+ }
+
+// resize(ssim_map, ssim_map, Size(), 0.5, 0.5, INTER_AREA);
+
+
+ // the edge/blockiness penalty is only done for the fullsize images
+ if (scale == 0) {
+
+ // asymmetric: penalty for introducing edges where there are none (e.g. blockiness), no penalty for smoothing away edges
+ Mat edgediff = max(abs(img2 - mu2) - abs(img1 - mu1), 0); // positive if img2 has an edge where img1 is smooth
+
+ // optional: write a nice debug image that shows the artifact edges
+#ifdef DEBUG_IMAGES
+ Mat edgediff_image;
+ edgediff.convertTo(edgediff_image,CV_8UC3,5000); // multiplying by more than 255 to make things easier to see
+ for( int i=0 ; i < pixels; i++ ) {
+ Vec3b &p = edgediff_image.at<Vec3b>(i);
+ p = {(uchar)(p[1]+p[2]),p[0],p[0]};
+ }
+ imwrite("debug-edgediff.png",edgediff_image);
+#endif
+
+ edgediff = Scalar(1.0,1.0,1.0,1.0) - edgediff;
+
+ avg = mean(edgediff);
+ for(unsigned int i = 0; i < nChan; i++) {
+ printf("extra_edges: %i %f\n",i,avg[i]);
+ dssim += extra_edges_weight[i] * avg[i];
+ dssim_max += extra_edges_weight[i];
+ }
+
+ // grid-like artifact detection
+ // do the things below twice: once for the SSIM map, once for the artifact-edge map
+ Mat errormap;
+ for(int twice=0; twice < 2; twice++) {
+ if (twice == 0) errormap = ssim_map;
+ else errormap = edgediff;
+
+ // Find the 2nd percentile worst row. If the compression uses blocks, there will be artifacts around the block edges,
+ // so even with 32x32 blocks, the 2nd percentile will likely be one of the rows with block borders
+ multiset<double> row_scores[4];
+ for (int y = 0; y < errormap.rows; y++) {
+ Mat roi = errormap(Rect(0,y,errormap.cols,1));
+ Scalar ravg = mean(roi);
+ for (unsigned int i = 0; i < nChan; i++) row_scores[i].insert(ravg[i]);
+ }
+ for(unsigned int i = 0; i < nChan; i++) {
+ int k=0; for (const double& s : row_scores[i]) { if (k++ >= errormap.rows/50) { dssim += worst_grid_weight[twice][i] * s;
+ printf("grid row %s %i: %f\n",(twice?"edgediff":"ssimmap"),i,s);
+
+ break; } }
+ dssim_max += worst_grid_weight[twice][i];
+ }
+ // Find the 2nd percentile worst column. Same concept as above.
+ multiset<double> col_scores[4];
+ for (int x = 0; x < errormap.cols; x++) {
+ Mat roi = errormap(Rect(x,0,1,errormap.rows));
+ Scalar cavg = mean(roi);
+ for (unsigned int i = 0; i < nChan; i++) col_scores[i].insert(cavg[i]);
+ }
+ for(unsigned int i = 0; i < nChan; i++) {
+ int k=0; for (const double& s : col_scores[i]) { if (k++ >= errormap.cols/50) { dssim += worst_grid_weight[twice][i] * s;
+ printf("grid col %s %i: %f\n",(twice?"edgediff":"ssimmap"),i,s);
+
+break; } }
+ dssim_max += worst_grid_weight[twice][i];
+ }
+ }
+ }
+
+ // worst ssim in a particular 4x4 block (larger blocks are considered too because of multi-scale)
+ resize(ssim_map, ssim_map, Size(), 0.25, 0.25, INTER_AREA);
+// resize(ssim_map, ssim_map, Size(), 0.5, 0.5, INTER_AREA);
+
+ Mat ssim_map_c[4];
+ split(ssim_map, ssim_map_c);
+ for (unsigned int i=0; i < nChan; i++) {
+ double minVal;
+ minMaxLoc(ssim_map_c[i], &minVal);
+ printf("worst %i: %f\n",i,minVal);
+ dssim += min_weight[i] * minVal * mscale_weights[i][scale];
+ dssim_max += min_weight[i] * mscale_weights[i][scale];
+ }
+
+ }
+
+
+ dssim = dssim_max / dssim - 1;
+ if (dssim < 0) dssim = 0; // should not happen
+ if (dssim > 1) dssim = 1; // very different images
+
+ printf("%.8f\n", dssim);
+
+ return(0);
+}
diff --git a/media/libjxl/src/tools/ssimulacra_main.cc b/media/libjxl/src/tools/ssimulacra_main.cc
new file mode 100644
index 0000000000..5b48fe22c7
--- /dev/null
+++ b/media/libjxl/src/tools/ssimulacra_main.cc
@@ -0,0 +1,67 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+
+#include "lib/extras/codec.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/enc_color_management.h"
+#include "tools/ssimulacra.h"
+
+namespace ssimulacra {
+namespace {
+
+int PrintUsage(char** argv) {
+ fprintf(stderr, "Usage: %s [-v] [-s] orig.png distorted.png\n", argv[0]);
+ return 1;
+}
+
+int Run(int argc, char** argv) {
+ if (argc < 2) return PrintUsage(argv);
+
+ bool verbose = false, simple = false;
+ int input_arg = 1;
+ if (!strcmp(argv[input_arg], "-v")) {
+ verbose = true;
+ input_arg++;
+ }
+ if (!strcmp(argv[input_arg], "-s")) {
+ simple = true;
+ input_arg++;
+ }
+ if (argc < input_arg + 2) return PrintUsage(argv);
+
+ jxl::CodecInOut io1;
+ jxl::CodecInOut io2;
+ JXL_CHECK(SetFromFile(argv[input_arg], jxl::extras::ColorHints(), &io1));
+ JXL_CHECK(SetFromFile(argv[input_arg + 1], jxl::extras::ColorHints(), &io2));
+ JXL_CHECK(io1.TransformTo(jxl::ColorEncoding::LinearSRGB(io1.Main().IsGray()),
+ jxl::GetJxlCms()));
+ JXL_CHECK(io2.TransformTo(jxl::ColorEncoding::LinearSRGB(io2.Main().IsGray()),
+ jxl::GetJxlCms()));
+
+ if (io1.xsize() != io2.xsize() || io1.ysize() != io2.ysize()) {
+ fprintf(stderr, "Image size mismatch\n");
+ return 1;
+ }
+ if (io1.xsize() < 8 || io1.ysize() < 8) {
+ fprintf(stderr, "Minimum image size is 8x8 pixels\n");
+ return 1;
+ }
+
+ Ssimulacra ssimulacra =
+ ComputeDiff(*io1.Main().color(), *io2.Main().color(), simple);
+
+ if (verbose) {
+ ssimulacra.PrintDetails();
+ }
+ printf("%.8f\n", ssimulacra.Score());
+ return 0;
+}
+
+} // namespace
+} // namespace ssimulacra
+
+int main(int argc, char** argv) { return ssimulacra::Run(argc, argv); }
diff --git a/media/libjxl/src/tools/tool_version.cc b/media/libjxl/src/tools/tool_version.cc
new file mode 100644
index 0000000000..152689dbe5
--- /dev/null
+++ b/media/libjxl/src/tools/tool_version.cc
@@ -0,0 +1,18 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/tool_version.h"
+
+#ifdef JPEGXL_VERSION_FROM_GIT
+#include "tool_version_git.h"
+#endif
+
+namespace jpegxl {
+namespace tools {
+
+const char* kJpegxlVersion = JPEGXL_VERSION;
+
+} // namespace tools
+} // namespace jpegxl
diff --git a/media/libjxl/src/tools/tool_version.h b/media/libjxl/src/tools/tool_version.h
new file mode 100644
index 0000000000..c6f7c16253
--- /dev/null
+++ b/media/libjxl/src/tools/tool_version.h
@@ -0,0 +1,22 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_TOOL_VERSION_H_
+#define TOOLS_TOOL_VERSION_H_
+
+#include <string>
+
+namespace jpegxl {
+namespace tools {
+
+// Package version as defined by the JPEGXL_VERSION macro. This is not the
+// library semantic versioning number, but instead additional information on the
+// tool version.
+extern const char* kJpegxlVersion;
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_TOOL_VERSION_H_
diff --git a/media/libjxl/src/tools/transforms_fuzzer.cc b/media/libjxl/src/tools/transforms_fuzzer.cc
new file mode 100644
index 0000000000..1ef08b2379
--- /dev/null
+++ b/media/libjxl/src/tools/transforms_fuzzer.cc
@@ -0,0 +1,146 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+
+#include "lib/jxl/base/random.h"
+#include "lib/jxl/dec_bit_reader.h"
+#include "lib/jxl/modular/encoding/encoding.h"
+#include "lib/jxl/modular/transform/transform.h"
+
+namespace jxl {
+
+namespace {
+void FillChannel(Channel& ch, Rng& rng) {
+ auto p = &ch.plane;
+ const size_t w = ch.w;
+ const size_t h = ch.h;
+ for (size_t y = 0; y < h; ++y) {
+ pixel_type* row = p->Row(y);
+ for (size_t x = 0; x < w; ++x) {
+ row[x] = rng.UniformU(0, 0x80000000);
+ }
+ }
+}
+template <typename T>
+void AssertEq(T a, T b) {
+ if (a != b) __builtin_trap();
+}
+} // namespace
+
+int TestOneInput(const uint8_t* data, size_t size) {
+ static Status nevermind = true;
+ BitReader reader(Span<const uint8_t>(data, size));
+ BitReaderScopedCloser reader_closer(&reader, &nevermind);
+
+ Rng rng(reader.ReadFixedBits<56>());
+
+ // One of {0, 1, _2_, 3}; "2" will be filtered out soon.
+ size_t nb_chans = static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x3;
+ size_t nb_extra = static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x7;
+ // 1..32
+ size_t bit_depth =
+ (static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x1F) + 1;
+ // {0, 1, 2, 3}
+ size_t log_upsampling =
+ (static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x3);
+ size_t upsampling = 1 << log_upsampling;
+
+ size_t w_orig = static_cast<size_t>(reader.ReadFixedBits<16>());
+ size_t h_orig = static_cast<size_t>(reader.ReadFixedBits<16>());
+ size_t w = DivCeil(w_orig, upsampling);
+ size_t h = DivCeil(h_orig, upsampling);
+
+ if ((nb_chans == 2) || ((nb_chans + nb_extra) == 0) || (w * h == 0) ||
+ ((w_orig * h_orig * (nb_chans + nb_extra)) > (1 << 23))) {
+ return 0;
+ }
+
+ std::vector<int> hshift;
+ std::vector<int> vshift;
+ std::vector<size_t> ec_upsampling;
+
+ for (size_t c = 0; c < nb_chans; c++) {
+ hshift.push_back(static_cast<int>(reader.ReadFixedBits<8>()) & 1);
+ vshift.push_back(static_cast<int>(reader.ReadFixedBits<8>()) & 1);
+ }
+
+ for (size_t ec = 0; ec < nb_extra; ec++) {
+ size_t log_ec_upsampling =
+ (static_cast<size_t>(reader.ReadFixedBits<8>()) & 0x3);
+ log_ec_upsampling = std::max(log_ec_upsampling, log_upsampling);
+ ec_upsampling.push_back(1 << log_ec_upsampling);
+ }
+
+ Image image(w, h, bit_depth, nb_chans + nb_extra);
+
+ for (size_t c = 0; c < nb_chans; c++) {
+ Channel& ch = image.channel[c];
+ ch.hshift = hshift[c];
+ ch.vshift = vshift[c];
+ ch.shrink(DivCeil(w, 1 << hshift[c]), DivCeil(h, 1 << vshift[c]));
+ }
+
+ for (size_t ec = 0; ec < nb_extra; ec++) {
+ Channel& ch = image.channel[ec + nb_chans];
+ size_t ch_up = ec_upsampling[ec];
+ int up_level = CeilLog2Nonzero(ch_up) - CeilLog2Nonzero(upsampling);
+ ch.shrink(DivCeil(w_orig, ch_up), DivCeil(h_orig, ch_up));
+ ch.hshift = ch.vshift = up_level;
+ }
+
+ GroupHeader header;
+ if (!Bundle::Read(&reader, &header)) return 0;
+ weighted::Header w_header;
+ if (!Bundle::Read(&reader, &w_header)) return 0;
+
+ // TODO(eustas): give it a try?
+ if (!reader.AllReadsWithinBounds()) return 0;
+
+ image.transform = header.transforms;
+ for (Transform& transform : image.transform) {
+ if (!transform.MetaApply(image)) return 0;
+ }
+ if (image.error) return 0;
+
+ ModularOptions options;
+ if (!ValidateChannelDimensions(image, options)) return 0;
+
+ for (size_t i = 0; i < image.channel.size(); ++i) {
+ FillChannel(image.channel[i], rng);
+ }
+
+ image.undo_transforms(w_header);
+
+ AssertEq(image.error, false);
+ AssertEq<size_t>(image.nb_meta_channels, 0);
+ AssertEq(image.channel.size(), nb_chans + nb_extra);
+
+ for (size_t c = 0; c < nb_chans; c++) {
+ const Channel& ch = image.channel[c];
+ AssertEq(ch.hshift, hshift[c]);
+ AssertEq(ch.vshift, vshift[c]);
+ AssertEq(ch.w, DivCeil(w, 1 << hshift[c]));
+ AssertEq(ch.h, DivCeil(h, 1 << vshift[c]));
+ }
+
+ for (size_t ec = 0; ec < nb_extra; ec++) {
+ const Channel& ch = image.channel[ec + nb_chans];
+ size_t ch_up = ec_upsampling[ec];
+ int up_level = CeilLog2Nonzero(ch_up) - CeilLog2Nonzero(upsampling);
+ AssertEq(ch.w, DivCeil(w_orig, ch_up));
+ AssertEq(ch.h, DivCeil(h_orig, ch_up));
+ AssertEq(ch.hshift, up_level);
+ AssertEq(ch.vshift, up_level);
+ }
+
+ return 0;
+}
+
+} // namespace jxl
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return jxl::TestOneInput(data, size);
+}
diff --git a/media/libjxl/src/tools/upscaling_coefficients/generate_upscaling_coefficients.py b/media/libjxl/src/tools/upscaling_coefficients/generate_upscaling_coefficients.py
new file mode 100755
index 0000000000..17c404d1cd
--- /dev/null
+++ b/media/libjxl/src/tools/upscaling_coefficients/generate_upscaling_coefficients.py
@@ -0,0 +1,242 @@
+#!/usr/bin/env python3
+
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"""Generates coefficients used in upscaling.
+
+Given an upscaling factor which can be 2, 4 or 8, we generate coefficients and
+indices for lib/jxl/image_metadata.cc in the format needed there.
+"""
+
+import argparse
+import itertools
+import numpy as np
+
+
+def compute_kernel(sigma):
+ """Gaussian-like kernel with standard deviation sigma."""
+ # This controls the length of the kernel.
+ m = 2.5
+ diff = int(max(1, m * abs(sigma)))
+ kernel = np.exp(-np.arange(-diff, diff + 1)**2 /(2 * sigma * sigma))
+ return kernel
+
+
+def convolution(pixels, kernel):
+ """Computes a horizontal convolution and transposes the result."""
+ y, x = pixels.shape
+ kernel_len = len(kernel)
+ offset = kernel_len // 2
+ scale = 1 / sum(kernel)
+ out_pixels = np.zeros(shape=(x, y), dtype=pixels.dtype)
+ for i, j in itertools.product(range(x), range(y)):
+ if kernel_len < i < x - kernel_len:
+ out_pixels[i, j] = scale * sum(
+ pixels[j, i - offset + k] * kernel[k] for k in range(kernel_len))
+ else:
+ out_pixels[i, j] = pixels[j, i]
+ return out_pixels
+
+
+def _super_sample(pixels, n):
+ return np.repeat(np.repeat(pixels, n, axis=0), n, axis=1)
+
+
+def _sub_sample(pixels, n):
+ x, y = pixels.shape
+ assert x%n == 0 and y%n == 0
+ return 1 / (n * n) * pixels.reshape(x // n, n, y // n, n).transpose(
+ [0, 2, 1, 3]).sum(axis=(2, 3))
+
+
+def smooth_4x4_corners(pixels):
+ """Generates a 4x4 upscaled image, to be smoothed afterwards."""
+ overshoot = 3.5
+ m = 1.0 / (4.0 - overshoot)
+ y_size, x_size = pixels.shape
+ for y, x in itertools.product(range(3, y_size - 3, 4),
+ range(3, x_size - 3, 4)):
+ ave = (
+ pixels[y, x] + pixels[y, x + 1] + pixels[y + 1, x] +
+ pixels[y + 1, x + 1])
+ off = 2
+ other = (ave - overshoot * pixels[y, x]) * m
+ pixels[y - off, x - off] -= (other - pixels[y, x])
+ pixels[y, x] = other
+
+ other = (ave - overshoot * pixels[y, x + 1]) * m
+ pixels[y - off, x + off + 1] -= (other - pixels[y, x + 1])
+ pixels[y, x + 1] = other
+
+ other = (ave - overshoot * pixels[y + 1, x]) * m
+ pixels[y + off + 1, x - off] -= (other - pixels[y + 1, x])
+ pixels[y + 1, x] = other
+
+ other = (ave - overshoot * pixels[y + 1, x + 1]) * m
+ pixels[y + off + 1][x + off + 1] -= (other - pixels[y + 1, x + 1])
+ pixels[y + 1, x + 1] = other
+
+ return pixels
+
+
+def smoothing(pixels):
+ new_pixels = smooth_4x4_corners(_super_sample(pixels, 4))
+ my_kernel = compute_kernel(2.5)
+ smooth_image = convolution(convolution(new_pixels, my_kernel), my_kernel)
+ return smooth_image
+
+
+upscaling = {
+ 2: lambda pixels: _sub_sample(smoothing(pixels), 2),
+ 4: smoothing,
+ 8: lambda pixels: _sub_sample(smoothing(smoothing(pixels)), 2)
+}
+
+
+def get_coeffs(upscaling_factor, kernel_size=5, normalized=True, dtype="float"):
+ """Returns 4-tensor of coefficients.
+
+ Args:
+ upscaling_factor: 2, 4, or 8
+ kernel_size: must be odd
+ normalized: if True, the kernel matrix adds to 1
+ dtype: type of numpy array to return
+
+ Returns:
+ A (upscaling_factor x upscaling_factor) matrix of
+ (kernel_size x kernel_size) matrices, describing the kernel for all pixels.
+ """
+
+ upscaling_method = upscaling[upscaling_factor]
+ patch_size = 2 * kernel_size + 1
+ matrix_bases = np.eye(
+ patch_size * patch_size, dtype=dtype).reshape(patch_size, patch_size,
+ patch_size, patch_size)
+
+ # takes some time...
+ smoothed_bases = np.array(
+ [[upscaling_method(matrix_bases[a, b])
+ for a in range(patch_size)]
+ for b in range(patch_size)])
+
+ middle = patch_size // 2
+ lower = middle - kernel_size // 2
+ upper = middle + kernel_size // 2 + 1
+ assert len(range(lower, upper)) == kernel_size
+ assert sum(range(lower, upper)) == kernel_size * middle
+
+ coefficients = np.array([[[[
+ smoothed_bases[i, j, upscaling_factor * middle + b,
+ upscaling_factor * middle + a]
+ for i in range(lower, upper)
+ ]
+ for j in range(lower, upper)]
+ for a in range(upscaling_factor)]
+ for b in range(upscaling_factor)])
+
+ if normalized:
+ return coefficients / coefficients.sum(axis=(2, 3))[..., np.newaxis,
+ np.newaxis]
+ else:
+ return coefficients
+
+
+def indices_matrix(upscaling_factor, kernel_size=5):
+ """Matrix containing indices with all symmetries."""
+ matrix = np.zeros(
+ shape=[upscaling_factor * kernel_size] * 2, dtype="int16")
+ # define a fundamental domain
+ counter = 1
+ for i in range((kernel_size * upscaling_factor) // 2):
+ for j in range(i, (kernel_size * upscaling_factor) // 2):
+ matrix[i, j] = counter
+ counter += 1
+
+ matrix_with_transpose = matrix + (matrix.transpose()) * (
+ matrix != matrix.transpose())
+ matrix_vertical = matrix_with_transpose + (
+ np.flip(matrix_with_transpose, axis=0) *
+ (matrix_with_transpose != np.flip(matrix_with_transpose, axis=0)))
+ matrix_horizontal = matrix_vertical + (
+ np.flip(matrix_vertical, axis=1) *
+ (matrix_vertical != np.flip(matrix_vertical, axis=1))) - 1
+ return matrix_horizontal
+
+
+def format_indices_matrix(upscaling_factor, kernel_size=5):
+ """Returns string of commented out numbers-only matrices."""
+ indices = indices_matrix(upscaling_factor)
+ output_str = []
+ for i in range(upscaling_factor // 2):
+ for j in range(kernel_size):
+ output_str.append("//")
+ for a in range(upscaling_factor // 2):
+ for b in range(kernel_size):
+ output_str.append(
+ f"{'{:x}'.format(int(indices[kernel_size*i + j][kernel_size*a + b])).rjust(2)} "
+ )
+ output_str.append(" ")
+ output_str.append("\n")
+ output_str.append("\n")
+ return "".join(output_str)
+
+
+def weights_arrays(upscaling_factor, kernel_size=5):
+ """Returns string describing array of depth 4."""
+ indices = indices_matrix(upscaling_factor)
+ return (
+ f"kernel[{upscaling_factor}][{upscaling_factor}][{kernel_size}][{kernel_size}]"
+ f" = {{" + ", \n".join("{\n" + ", \n\n".join(
+ ("{" + ", \n".join("{" + ", ".join(
+ f"weights[{str(indices[kernel_size*i + j][kernel_size*a + b])}]"
+ for b in range(kernel_size)) + "}"
+ for j in range(kernel_size)) + "}"
+ for a in range(upscaling_factor // 2))) + "\n}"
+ for i in range(upscaling_factor // 2)) + "}\n")
+
+
+def coefficients_list(upscaling_factor, kernel_size=5):
+ """Returns string describing coefficients."""
+ coeff_tensor = get_coeffs(upscaling_factor,
+ kernel_size).transpose([0, 2, 1, 3]).reshape(
+ kernel_size * upscaling_factor,
+ kernel_size * upscaling_factor)
+ my_weights = [
+ f'{"{:.8f}".format(coeff_tensor[i][j])}f'
+ for i in range((kernel_size * upscaling_factor) // 2)
+ for j in range(i, (kernel_size * upscaling_factor) // 2)
+ ]
+ return f"kWeights{upscaling_factor} = {{" + ", ".join(my_weights) + "};"
+
+
+def print_all_output(upscaling_factor):
+ print(format_indices_matrix(upscaling_factor))
+ print(coefficients_list(upscaling_factor), end="\n\n")
+ print(weights_arrays(upscaling_factor))
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Generates coefficients used in upscaling.")
+ parser.add_argument(
+ "upscaling_factor",
+ type=int,
+ help="upscaling factor, must be 2, 4 or 8.",
+ nargs="?",
+ default=None)
+
+ args = parser.parse_args()
+ upscaling_factor = args.upscaling_factor
+ if upscaling_factor:
+ print_all_output(upscaling_factor)
+ else:
+ for factor in [2, 4, 8]:
+ print(f"upscaling factor = {factor}")
+ print_all_output(factor)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/media/libjxl/src/tools/upscaling_coefficients/upscaler_demo.py b/media/libjxl/src/tools/upscaling_coefficients/upscaler_demo.py
new file mode 100644
index 0000000000..89f1320a74
--- /dev/null
+++ b/media/libjxl/src/tools/upscaling_coefficients/upscaler_demo.py
@@ -0,0 +1,814 @@
+#!/usr/bin/env python3
+
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"""Demo for upscaling.
+
+Given an upscaling factor which can be 2, 4 or 8 we demo upscaling an image by
+that factor.
+
+usage: upscaler_demo.py [-h] [--upscaling_factor N] input_filename output_filename
+
+Upscaling of an image by a factor of 2, 4 or 8.
+
+positional arguments:
+ input_filename of the PNG image to be upscaled.
+ output_filename where the upscaled image is written as PNG.
+
+optional arguments:
+ -h, --help show this help message and exit
+ --upscaling_factor N where N must be 2, 4 (default) or 8.
+"""
+from PIL import Image
+
+import argparse
+import numpy as np
+
+
+def convolution(pixels, kernel):
+ """
+ Returns the convolution of `pixels` with `kernel`.
+
+ Uses padding such that the shape of the returned convoluted array is the
+ same as the shape of `pixels`, scaled by the upscaling_factor implied by the
+ `kernel`.
+
+ Args:
+ pixels: A [heigth, width]- or [height, width, num_channels]-array
+ representing an image.
+
+ kernel: A [upscaling_factor, upscaling_factor, kernel_size,
+ kernel_size]-array used for the convolution.
+
+ Returns:
+ A [upscaling_factor*heigth, upscaling_factor*width]- or
+ [upscaling_factor*height, upscaling_factor*width, num_channels]-array representing the
+ convoluted upscaled image.
+ """
+ upscaling_factor, _, kernel_size, _ = kernel.shape
+ output_shape = list(pixels.shape)
+ output_shape[0] *= upscaling_factor
+ output_shape[1] *= upscaling_factor
+ shaped_pixels = pixels.reshape(pixels.shape[:2] + (-1,))
+ pad_width = kernel_size//2
+ padded_pixels = np.pad(
+ shaped_pixels, 2*[2*[pad_width]] + [[0, 0]], mode='edge')
+ x, y, _ = shaped_pixels.shape
+ convoluted = np.block([[np.einsum('rc...,RCrc->...RC',
+ padded_pixels[i - pad_width: i + pad_width + 1,
+ j - pad_width: j + pad_width + 1],
+ kernel)
+ for j in range(pad_width, pad_width + y)]
+ for i in range(pad_width, pad_width + x)])
+ return np.moveaxis(convoluted, 0, -1).reshape(output_shape)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Upscaling of an image by a factor of 2, 4 or 8.")
+ parser.add_argument(
+ "--upscaling_factor",
+ type=int,
+ help="where N must be 2, 4 (default) or 8.",
+ nargs=1,
+ default=[4],
+ metavar='N')
+
+ parser.add_argument(
+ "input_filename",
+ type=str,
+ help="of the PNG image to be upscaled."
+ )
+ parser.add_argument(
+ "output_filename",
+ type=str,
+ help="where the upscaled image is written as PNG."
+ )
+
+ args = parser.parse_args()
+ upscaling_factor = args.upscaling_factor[0]
+ kernel_size = 5
+ if upscaling_factor not in (2, 4, 8):
+ raise ValueError("upscaling_factor must be 2, 4 or 8.")
+ kernel = np.array(_get_scaling_kernels()[upscaling_factor])
+ assert kernel.shape == (
+ upscaling_factor, upscaling_factor, kernel_size, kernel_size)
+ orig_raw = Image.open(args.input_filename)
+ orig = orig_raw.convert('RGB') if orig_raw.mode == 'P' else orig_raw
+ upscaled_float = convolution(np.array(orig), kernel)
+
+ upscaled = Image.fromarray(
+ np.rint(np.clip(upscaled_float, 0, 255)).astype(np.uint8), orig.mode)
+ upscaled.save(args.output_filename)
+
+
+def _get_scaling_kernels():
+ return {2: [[[[-0.017162003089909145, -0.0345230259724203, -0.04022174342753632,
+ -0.029210135410064335, -0.006246448474415789], [-0.0345230259724203,
+ 0.14111091126932612, 0.28896754962953114, 0.0027871809188615613,
+ -0.016102674925096382], [-0.04022174342753632, 0.28896754962953114,
+ 0.5666155013385713, 0.037776067445408776, -0.01986694439461126],
+ [-0.029210135410064335, 0.0027871809188615535, 0.03777606744540877,
+ -0.031447310821961526, -0.011850679991269755], [-0.006246448474415788,
+ -0.01610267492509638, -0.019866944394611258, -0.011850679991269755,
+ -0.0021353894928012747]], [[-0.006246448474415787, -0.029210135410064328,
+ -0.040221743427536316, -0.034523025972420296, -0.01716200308990914],
+ [-0.01610267492509638, 0.0027871809188615582, 0.2889675496295311,
+ 0.1411109112693261, -0.034523025972420296], [-0.019866944394611254,
+ 0.037776067445408755, 0.5666155013385712, 0.2889675496295311,
+ -0.04022174342753631], [-0.011850679991269751, -0.03144731082196152,
+ 0.037776067445408755, 0.00278718091886156, -0.029210135410064324],
+ [-0.0021353894928012743, -0.011850679991269751, -0.01986694439461125,
+ -0.016102674925096375, -0.006246448474415786]]], [[[-0.006246448474415788,
+ -0.01610267492509638, -0.019866944394611258, -0.011850679991269755,
+ -0.0021353894928012747], [-0.02921013541006433, 0.002787180918861557,
+ 0.03777606744540876, -0.031447310821961526, -0.011850679991269755],
+ [-0.04022174342753632, 0.28896754962953114, 0.5666155013385712,
+ 0.037776067445408776, -0.019866944394611258], [-0.0345230259724203,
+ 0.14111091126932612, 0.28896754962953114, 0.0027871809188615595,
+ -0.016102674925096382], [-0.017162003089909145, -0.03452302597242031,
+ -0.04022174342753633, -0.029210135410064335, -0.006246448474415789]],
+ [[-0.0021353894928012747, -0.011850679991269755, -0.019866944394611258,
+ -0.01610267492509638, -0.006246448474415788], [-0.011850679991269755,
+ -0.031447310821961526, 0.03777606744540876, 0.002787180918861564,
+ -0.02921013541006433], [-0.019866944394611258, 0.037776067445408776,
+ 0.5666155013385712, 0.28896754962953114, -0.040221743427536316],
+ [-0.016102674925096382, 0.002787180918861556, 0.28896754962953114,
+ 0.14111091126932615, -0.0345230259724203], [-0.006246448474415789,
+ -0.029210135410064335, -0.04022174342753633, -0.0345230259724203,
+ -0.017162003089909145]]]],
+ 4: [[[[-0.024190672183733018, -0.03491987403959535, -0.036933511116288356,
+ -0.03094284535390427, -0.005297851729507614], [-0.03491987403959535,
+ 0.23651958284942343, 0.3339294481745815, -0.010735433431237009,
+ -0.013131808617501706], [-0.036933511116288356, 0.3339294481745815,
+ 0.4691419769580017, -0.0020927007975838127, -0.014845888917802386],
+ [-0.030942845353904277, -0.010735433431237024, -0.0020927007975838035,
+ -0.035516824721615874, -0.007548300818273063], [-0.005297851729507614,
+ -0.013131808617501708, -0.014845888917802386, -0.007548300818273063,
+ -0.0009165296078520004]], [[-0.01663431734052121, -0.03556862997573282,
+ -0.0388890539890255, -0.035168498619353575, -0.009894687488916538],
+ [-0.03556694367519552, 0.13048175192612746, 0.40103024797994685,
+ 0.03951149796198834, -0.02077584470399766], [-0.04064806042030105,
+ 0.18942529580147974, 0.5627989220290085, 0.06674400125646836,
+ -0.023354943007463536], [-0.0226791877794674, -0.023635779153108244,
+ 0.0031580414703133823, -0.03399097960642573, -0.013595188211470589],
+ [-0.003354666868160516, -0.011632944561351362, -0.016102939237729652,
+ -0.00974087766582541, -0.0019162161212866041]], [[-0.009894687488916542,
+ -0.03516849861935358, -0.03888905398902551, -0.035568629975732825,
+ -0.016634317340521215], [-0.020775844703997664, 0.03951149796198835,
+ 0.4010302479799469, 0.13048175192612743, -0.03556694367519553],
+ [-0.02335494300746354, 0.06674400125646836, 0.5627989220290086,
+ 0.18942529580147976, -0.04064806042030106], [-0.01359518821147059,
+ -0.033990979606425734, 0.003158041470313383, -0.02363577915310824,
+ -0.022679187779467407], [-0.0019162161212866043, -0.00974087766582541,
+ -0.016102939237729656, -0.011632944561351364, -0.0033546668681605166]],
+ [[-0.005297851729507613, -0.030942845353904264, -0.036933511116288356,
+ -0.034919874039595344, -0.024190672183733015], [-0.013131808617501703,
+ -0.010735433431237012, 0.33392944817458137, 0.23651958284942343,
+ -0.03491987403959533], [-0.014845888917802382, -0.0020927007975838153,
+ 0.4691419769580016, 0.33392944817458153, -0.03693351111628834],
+ [-0.007548300818273061, -0.03551682472161587, -0.0020927007975838053,
+ -0.010735433431237016, -0.030942845353904264], [-0.0009165296078520002,
+ -0.007548300818273061, -0.014845888917802382, -0.013131808617501704,
+ -0.005297851729507613]]], [[[-0.01663431734052122, -0.03556694367519555,
+ -0.040648060420301065, -0.02267918777946741, -0.0033546668681605175],
+ [-0.03556862997573284, 0.13048175192612746, 0.18942529580147982,
+ -0.023635779153108258, -0.011632944561351367], [-0.038889053989025514,
+ 0.401030247979947, 0.5627989220290087, 0.0031580414703133814,
+ -0.01610293923772966], [-0.03516849861935359, 0.03951149796198835,
+ 0.06674400125646837, -0.03399097960642574, -0.009740877665825412],
+ [-0.009894687488916542, -0.020775844703997664, -0.023354943007463547,
+ -0.01359518821147059, -0.0019162161212866046]], [[-0.01095445961681655,
+ -0.0319846366701879, -0.04455120920314033, -0.027997902912581793,
+ -0.006459118117528576], [-0.0319846366701879, 0.06390599280769027,
+ 0.22963887988104975, 0.006309810655924714, -0.018973492447769916],
+ [-0.04455120920314033, 0.2296388798810498, 0.67537268393182,
+ 0.08483369316914859, -0.025349935472536677], [-0.027997902912581786,
+ 0.006309810655924713, 0.08483369316914857, -0.02205197197850368,
+ -0.016679994683747115], [-0.006459118117528575, -0.018973492447769913,
+ -0.02534993547253667, -0.016679994683747115, -0.0038444335414517822]],
+ [[-0.006459118117528576, -0.02799790291258179, -0.04455120920314034,
+ -0.0319846366701879, -0.010954459616816552], [-0.01897349244776992,
+ 0.006309810655924714, 0.22963887988104978, 0.06390599280769028,
+ -0.03198463667018791], [-0.025349935472536677, 0.08483369316914859,
+ 0.6753726839318202, 0.22963887988104975, -0.04455120920314034],
+ [-0.016679994683747118, -0.022051971978503677, 0.08483369316914859,
+ 0.0063098106559247085, -0.02799790291258179], [-0.0038444335414517827,
+ -0.016679994683747115, -0.02534993547253667, -0.018973492447769916,
+ -0.006459118117528575]], [[-0.0033546668681605166, -0.022679187779467407,
+ -0.04064806042030106, -0.03556694367519554, -0.016634317340521218],
+ [-0.011632944561351364, -0.023635779153108254, 0.18942529580147976,
+ 0.13048175192612743, -0.035568629975732825], [-0.016102939237729656,
+ 0.0031580414703133762, 0.5627989220290086, 0.40103024797994696,
+ -0.038889053989025486], [-0.009740877665825409, -0.033990979606425734,
+ 0.06674400125646834, 0.03951149796198835, -0.035168498619353575],
+ [-0.0019162161212866041, -0.013595188211470589, -0.02335494300746354,
+ -0.02077584470399766, -0.00989468748891654]]], [[[-0.009894687488916542,
+ -0.020775844703997664, -0.023354943007463547, -0.01359518821147059,
+ -0.0019162161212866046], [-0.03516849861935359, 0.03951149796198835,
+ 0.06674400125646836, -0.03399097960642574, -0.009740877665825412],
+ [-0.03888905398902551, 0.401030247979947, 0.5627989220290087,
+ 0.0031580414703133814, -0.01610293923772966], [-0.03556862997573284,
+ 0.13048175192612746, 0.18942529580147982, -0.023635779153108258,
+ -0.011632944561351367], [-0.016634317340521218, -0.03556694367519555,
+ -0.040648060420301065, -0.022679187779467418, -0.0033546668681605175]],
+ [[-0.006459118117528575, -0.018973492447769916, -0.02534993547253667,
+ -0.016679994683747118, -0.0038444335414517827], [-0.02799790291258179,
+ 0.006309810655924723, 0.08483369316914856, -0.022051971978503684,
+ -0.016679994683747118], [-0.04455120920314034, 0.22963887988104978,
+ 0.6753726839318203, 0.0848336931691486, -0.02534993547253667],
+ [-0.03198463667018791, 0.06390599280769028, 0.22963887988104978,
+ 0.006309810655924709, -0.018973492447769923], [-0.010954459616816552,
+ -0.03198463667018791, -0.04455120920314034, -0.027997902912581796,
+ -0.006459118117528576]], [[-0.0038444335414517822, -0.01667999468374711,
+ -0.02534993547253667, -0.018973492447769913, -0.006459118117528575],
+ [-0.016679994683747115, -0.02205197197850368, 0.08483369316914854,
+ 0.006309810655924723, -0.027997902912581786], [-0.02534993547253667,
+ 0.08483369316914859, 0.6753726839318202, 0.22963887988104975,
+ -0.04455120920314033], [-0.01897349244776992, 0.006309810655924712,
+ 0.22963887988104975, 0.06390599280769027, -0.0319846366701879],
+ [-0.006459118117528576, -0.027997902912581786, -0.04455120920314033,
+ -0.0319846366701879, -0.01095445961681655]], [[-0.0019162161212866041,
+ -0.013595188211470589, -0.02335494300746354, -0.02077584470399766,
+ -0.00989468748891654], [-0.009740877665825409, -0.033990979606425734,
+ 0.06674400125646834, 0.03951149796198835, -0.03516849861935358],
+ [-0.016102939237729656, 0.0031580414703133762, 0.5627989220290086,
+ 0.40103024797994696, -0.03888905398902548], [-0.011632944561351364,
+ -0.023635779153108254, 0.18942529580147976, 0.1304817519261275,
+ -0.035568629975732825], [-0.0033546668681605166, -0.022679187779467414,
+ -0.04064806042030106, -0.03556694367519554, -0.016634317340521215]]],
+ [[[-0.005297851729507615, -0.013131808617501711, -0.01484588891780239,
+ -0.007548300818273065, -0.0009165296078520006], [-0.030942845353904277,
+ -0.010735433431237028, -0.0020927007975838087, -0.03551682472161588,
+ -0.007548300818273065], [-0.03693351111628837, 0.3339294481745815,
+ 0.4691419769580017, -0.002092700797583813, -0.01484588891780239],
+ [-0.03491987403959536, 0.23651958284942348, 0.33392944817458153,
+ -0.010735433431237012, -0.01313180861750171], [-0.024190672183733025,
+ -0.034919874039595365, -0.03693351111628837, -0.030942845353904277,
+ -0.005297851729507615]], [[-0.0033546668681605166, -0.011632944561351364,
+ -0.016102939237729656, -0.009740877665825412, -0.0019162161212866043],
+ [-0.022679187779467404, -0.023635779153108247, 0.003158041470313383,
+ -0.033990979606425734, -0.013595188211470589], [-0.04064806042030106,
+ 0.18942529580147982, 0.5627989220290085, 0.06674400125646837,
+ -0.023354943007463547], [-0.03556694367519553, 0.1304817519261275,
+ 0.4010302479799469, 0.03951149796198835, -0.020775844703997653],
+ [-0.016634317340521215, -0.035568629975732825, -0.038889053989025514,
+ -0.035168498619353575, -0.009894687488916542]], [[-0.0019162161212866048,
+ -0.009740877665825414, -0.01610293923772966, -0.011632944561351367,
+ -0.0033546668681605175], [-0.01359518821147059, -0.03399097960642574,
+ 0.0031580414703133836, -0.023635779153108254, -0.022679187779467407],
+ [-0.023354943007463554, 0.06674400125646839, 0.5627989220290086,
+ 0.18942529580147982, -0.040648060420301065], [-0.020775844703997657,
+ 0.03951149796198836, 0.401030247979947, 0.13048175192612746,
+ -0.035566943675195535], [-0.009894687488916544, -0.03516849861935359,
+ -0.03888905398902552, -0.03556862997573283, -0.016634317340521218]],
+ [[-0.0009165296078520004, -0.007548300818273063, -0.014845888917802386,
+ -0.013131808617501708, -0.005297851729507614], [-0.007548300818273063,
+ -0.035516824721615874, -0.0020927007975838083, -0.010735433431237009,
+ -0.03094284535390427], [-0.014845888917802386, -0.0020927007975838166,
+ 0.4691419769580016, 0.3339294481745815, -0.036933511116288356],
+ [-0.013131808617501706, -0.010735433431237014, 0.3339294481745815,
+ 0.23651958284942348, -0.03491987403959534], [-0.005297851729507614,
+ -0.03094284535390427, -0.03693351111628836, -0.03491987403959535,
+ -0.024190672183733018]]]],
+ 8: [[[[-0.029286133281073247, -0.03706352644207269, -0.0378381168526885,
+ -0.03324558280295302, -0.004476318148146651], [-0.0370635264420727,
+ 0.29895328454745274, 0.3575770812164143, -0.024475522375569658,
+ -0.010817484288013228], [-0.0378381168526885, 0.35757708121641435,
+ 0.42720050241527285, -0.0224893852885426, -0.01155272937910007],
+ [-0.03324558280295302, -0.024475522375569672, -0.022489385288542597,
+ -0.03680917952171095, -0.005422291349995999], [-0.00447631814814665,
+ -0.01081748428801323, -0.011552729379100074, -0.005422291349995998,
+ -0.00045072273860512197]], [[-0.02519406150475052, -0.037526010691823306,
+ -0.03901507994141054, -0.03663285147762567, -0.006466489422914399],
+ [-0.043145939817870266, 0.23903219477825294, 0.41119300519363017,
+ -0.005730455022054139, -0.014502394951723473], [-0.04562755195174026,
+ 0.28689495518965613, 0.4909386897413151, -7.890574314417001e-05,
+ -0.015459264122748742], [-0.029204772772557758, -0.02788574061911041,
+ -0.021181804710686657, -0.039424021044039116, -0.007755474877032563],
+ [-0.003601096394526256, -0.010202069931803576, -0.012319067611648214,
+ -0.006389875713059274, -0.0007159165805851706]], [[-0.020664074967504838,
+ -0.03838632575427139, -0.04002101086742024, -0.03900035414027985,
+ -0.009019734953997754], [-0.042468451339058966, 0.1756761813778118,
+ 0.45220642702382896, 0.02287757117854141, -0.019367833372750356],
+ [-0.045626588213857136, 0.21238920010551757, 0.5398093391410694,
+ 0.033694739393926816, -0.020702111700092594], [-0.024336140047717,
+ -0.03193943219458267, -0.020308275361446707, -0.04044013741654317,
+ -0.010740155274818487], [-0.002791220988040244, -0.009571146384946013,
+ -0.012883266171804216, -0.007309372111524051, -0.0010778269600400276]],
+ [[-0.016263925397518374, -0.039541478550530786, -0.04046620032608076,
+ -0.03979621423581153, -0.012244853215160445], [-0.03583254566206615,
+ 0.11572472115297627, 0.47416733354946305, 0.06284440084948137,
+ -0.026850659249274114], [-0.038669884759381434, 0.1422954970729258,
+ 0.5659339775075575, 0.08045180751196822, -0.028882977402423956],
+ [-0.01930821727497102, -0.03620398561701563, -0.019741250657301437,
+ -0.03919545281633189, -0.014560933634183603], [-0.0021015621671157305,
+ -0.008907053401106528, -0.013176682690936201, -0.008138951872408835,
+ -0.0015349087147535298]], [[-0.01224485321516044, -0.03979621423581152,
+ -0.04046620032608074, -0.03954147855053078, -0.016263925397518367],
+ [-0.0268506592492741, 0.06284440084948137, 0.47416733354946283,
+ 0.11572472115297619, -0.03583254566206614], [-0.028882977402423942,
+ 0.0804518075119682, 0.5659339775075574, 0.14229549707292571,
+ -0.03866988475938142], [-0.014560933634183596, -0.03919545281633188,
+ -0.01974125065730143, -0.03620398561701561, -0.019308217274971014],
+ [-0.0015349087147535291, -0.008138951872408828, -0.013176682690936196,
+ -0.008907053401106523, -0.002101562167115729]], [[-0.00901973495399775,
+ -0.039000354140279844, -0.040021010867420236, -0.03838632575427138,
+ -0.020664074967504838], [-0.019367833372750352, 0.02287757117854141,
+ 0.4522064270238289, 0.17567618137781174, -0.04246845133905896],
+ [-0.020702111700092587, 0.03369473939392681, 0.5398093391410693,
+ 0.21238920010551757, -0.04562658821385712], [-0.010740155274818485,
+ -0.04044013741654316, -0.020308275361446707, -0.031939432194582666,
+ -0.024336140047717], [-0.0010778269600400273, -0.007309372111524049,
+ -0.012883266171804212, -0.00957114638494601, -0.0027912209880402426]],
+ [[-0.006466489422914402, -0.03663285147762569, -0.03901507994141056,
+ -0.03752601069182331, -0.02519406150475053], [-0.014502394951723478,
+ -0.005730455022054147, 0.4111930051936302, 0.23903219477825297,
+ -0.04314593981787026], [-0.015459264122748746, -7.890574314417718e-05,
+ 0.4909386897413152, 0.2868949551896563, -0.045627551951740265],
+ [-0.007755474877032565, -0.03942402104403913, -0.021181804710686664,
+ -0.027885740619110408, -0.029204772772557765], [-0.0007159165805851706,
+ -0.006389875713059275, -0.012319067611648218, -0.01020206993180358,
+ -0.003601096394526257]], [[-0.00447631814814665, -0.03324558280295302,
+ -0.0378381168526885, -0.03706352644207268, -0.02928613328107324],
+ [-0.01081748428801323, -0.024475522375569672, 0.3575770812164142,
+ 0.2989532845474528, -0.03706352644207268], [-0.01155272937910007,
+ -0.02248938528854261, 0.42720050241527285, 0.35757708121641446,
+ -0.037838116852688494], [-0.005422291349995998, -0.03680917952171095,
+ -0.022489385288542604, -0.024475522375569658, -0.03324558280295301],
+ [-0.0004507227386051219, -0.005422291349995998, -0.01155272937910007,
+ -0.010817484288013232, -0.00447631814814665]]], [[[-0.025194061504750523,
+ -0.043145939817870266, -0.04562755195174026, -0.02920477277255776,
+ -0.0036010963945262565], [-0.037526010691823306, 0.23903219477825288,
+ 0.28689495518965624, -0.0278857406191104, -0.010202069931803576],
+ [-0.03901507994141054, 0.4111930051936302, 0.4909386897413151,
+ -0.021181804710686657, -0.01231906761164821], [-0.03663285147762567,
+ -0.005730455022054155, -7.890574314415865e-05, -0.039424021044039116,
+ -0.006389875713059272], [-0.0064664894229143986, -0.014502394951723471,
+ -0.015459264122748746, -0.00775547487703256, -0.0007159165805851706]],
+ [[-0.02128481178805433, -0.04173044153813555, -0.04831487472573022,
+ -0.03293190035303922, -0.005252595229206095], [-0.041730441538135564,
+ 0.18968272846778533, 0.3306368426789878, -0.013001053856678076,
+ -0.01372950329294693], [-0.04831487472573022, 0.3306368426789878,
+ 0.5640812622041927, 0.004583518760872409, -0.016482266055193047],
+ [-0.03293190035303923, -0.013001053856678086, 0.004583518760872414,
+ -0.040827417160105635, -0.009045186119473492], [-0.005252595229206096,
+ -0.01372950329294693, -0.016482266055193047, -0.00904518611947349,
+ -0.0011168422627331077]], [[-0.017203222289937238, -0.040527364499551265,
+ -0.050457063493932794, -0.036073170059570094, -0.007380297997922879],
+ [-0.0401746501391571, 0.13727831636454993, 0.3640223411093611,
+ 0.010278898793053761, -0.01832107424986819], [-0.04887867620762643,
+ 0.24585519478421125, 0.6202613509857569, 0.04314806591631964,
+ -0.022137366266623233], [-0.02790922286627615, -0.021178184193661707,
+ 0.007986619792820032, -0.039957113612285294, -0.012434273033433196],
+ [-0.00411203529942813, -0.012971303942569701, -0.01723725281482718,
+ -0.010225452530604957, -0.0016530642487971611]], [[-0.013417641633011908,
+ -0.03965629331558996, -0.051516162733405924, -0.038148863041386254,
+ -0.010058190693394595], [-0.03365072121724163, 0.08734505711506498,
+ 0.38194295165025005, 0.04338227748703876, -0.025259934728481214],
+ [-0.04158013952905281, 0.16637288777763284, 0.6502702298731253,
+ 0.0962163605307964, -0.031013880437287037], [-0.02231705358706074,
+ -0.02946265951499448, 0.009920547334197453, -0.03600283468483377,
+ -0.01684919502363355], [-0.003131096848010505, -0.012180160279609381,
+ -0.01763265975706309, -0.011256197301616299, -0.0023166274424323116]],
+ [[-0.01005819069339459, -0.03814886304138624, -0.0515161627334059,
+ -0.03965629331558995, -0.013417641633011903], [-0.025259934728481193,
+ 0.04338227748703876, 0.38194295165024994, 0.08734505711506492,
+ -0.033650721217241615], [-0.031013880437287013, 0.09621636053079634,
+ 0.6502702298731251, 0.1663728877776328, -0.04158013952905279],
+ [-0.016849195023633544, -0.03600283468483376, 0.009920547334197446,
+ -0.029462659514994466, -0.022317053587060723], [-0.0023166274424323103,
+ -0.01125619730161629, -0.017632659757063088, -0.012180160279609381,
+ -0.0031310968480105037]], [[-0.007380297997922879, -0.0360731700595701,
+ -0.0504570634939328, -0.040527364499551265, -0.01720322228993724],
+ [-0.01832107424986819, 0.010278898793053765, 0.36402234110936105,
+ 0.1372783163645499, -0.040174650139157095], [-0.022137366266623233,
+ 0.043148065916319624, 0.6202613509857569, 0.24585519478421133,
+ -0.04887867620762643], [-0.012434273033433196, -0.039957113612285294,
+ 0.007986619792820032, -0.0211781841936617, -0.027909222866276145],
+ [-0.0016530642487971611, -0.010225452530604957, -0.01723725281482718,
+ -0.012971303942569701, -0.004112035299428131]], [[-0.005252595229206095,
+ -0.03293190035303923, -0.04831487472573021, -0.04173044153813554,
+ -0.021284811788054327], [-0.013729503292946926, -0.013001053856678083,
+ 0.33063684267898774, 0.1896827284677853, -0.04173044153813554],
+ [-0.016482266055193047, 0.004583518760872396, 0.5640812622041926,
+ 0.33063684267898785, -0.0483148747257302], [-0.00904518611947349,
+ -0.04082741716010563, 0.0045835187608724145, -0.013001053856678076,
+ -0.03293190035303922], [-0.0011168422627331075, -0.009045186119473489,
+ -0.016482266055193043, -0.013729503292946926, -0.005252595229206093]],
+ [[-0.0036010963945262565, -0.029204772772557765, -0.04562755195174027,
+ -0.04314593981787027, -0.02519406150475053], [-0.01020206993180358,
+ -0.02788574061911041, 0.2868949551896562, 0.239032194778253,
+ -0.037526010691823306], [-0.012319067611648214, -0.02118180471068667,
+ 0.4909386897413152, 0.41119300519363033, -0.039015079941410534],
+ [-0.0063898757130592745, -0.03942402104403913, -7.890574314417213e-05,
+ -0.00573045502205414, -0.036632851477625676], [-0.0007159165805851707,
+ -0.007755474877032561, -0.015459264122748746, -0.014502394951723478,
+ -0.0064664894229143986]]], [[[-0.020664074967504838,
+ -0.042468451339058966, -0.04562658821385713, -0.024336140047717003,
+ -0.002791220988040243], [-0.03838632575427139, 0.1756761813778117,
+ 0.21238920010551754, -0.031939432194582666, -0.00957114638494601],
+ [-0.04002101086742023, 0.4522064270238289, 0.5398093391410693,
+ -0.020308275361446703, -0.012883266171804212], [-0.039000354140279844,
+ 0.022877571178541382, 0.03369473939392682, -0.04044013741654317,
+ -0.007309372111524049], [-0.00901973495399775, -0.019367833372750352,
+ -0.02070211170009259, -0.010740155274818482, -0.0010778269600400273]],
+ [[-0.017203222289937245, -0.040174650139157116, -0.04887867620762643,
+ -0.027909222866276156, -0.004112035299428132], [-0.040527364499551286,
+ 0.13727831636454993, 0.24585519478421136, -0.021178184193661704,
+ -0.01297130394256971], [-0.05045706349393281, 0.3640223411093611,
+ 0.6202613509857569, 0.007986619792820032, -0.017237252814827183],
+ [-0.03607317005957011, 0.010278898793053753, 0.04314806591631965,
+ -0.03995711361228531, -0.010225452530604962], [-0.007380297997922879,
+ -0.01832107424986819, -0.022137366266623236, -0.012434273033433195,
+ -0.0016530642487971616]], [[-0.013741489638205826, -0.037976197105406395,
+ -0.05142937279813894, -0.031173068184164848, -0.005819138225232018],
+ [-0.03797619710540639, 0.09628103622608752, 0.271299908889608,
+ -0.003537793416633379, -0.017341510615908634], [-0.05142937279813893,
+ 0.271299908889608, 0.6821432561027145, 0.050180479222290235,
+ -0.023208515458651935], [-0.031173068184164848, -0.003537793416633386,
+ 0.050180479222290235, -0.03637638898995256, -0.013943731217351598],
+ [-0.005819138225232015, -0.017341510615908627, -0.023208515458651935,
+ -0.013943731217351598, -0.002408535881464665]], [[-0.010640027531642969,
+ -0.03608088767126983, -0.05272168029782533, -0.03375669845324461,
+ -0.007955856657996018], [-0.03153980584721341, 0.05686230181726655,
+ 0.28500998074905703, 0.02230594207226229, -0.023749554287216885],
+ [-0.04383615619088237, 0.1845947431815049, 0.7151797455936234,
+ 0.10805612743320024, -0.03263677274980321], [-0.02511202585552835,
+ -0.017286364047844695, 0.054073310615547404, -0.028675684605006257,
+ -0.018931312997898533], [-0.004465109850519687, -0.01636186809305578,
+ -0.023770526019940293, -0.01522847594949364, -0.0033333443560800225]],
+ [[-0.007955856657996014, -0.033756698453244596, -0.052721680297825306,
+ -0.03608088767126982, -0.010640027531642969], [-0.023749554287216878,
+ 0.022305942072262296, 0.285009980749057, 0.056862301817266515,
+ -0.031539805847213394], [-0.0326367727498032, 0.10805612743320023,
+ 0.7151797455936234, 0.18459474318150482, -0.04383615619088237],
+ [-0.018931312997898533, -0.028675684605006243, 0.05407331061554739,
+ -0.0172863640478447, -0.025112025855528346], [-0.0033333443560800216,
+ -0.015228475949493633, -0.023770526019940286, -0.016361868093055777,
+ -0.004465109850519686]], [[-0.005819138225232017, -0.031173068184164845,
+ -0.05142937279813894, -0.037976197105406395, -0.013741489638205831],
+ [-0.01734151061590863, -0.003537793416633379, 0.27129990888960803,
+ 0.09628103622608748, -0.03797619710540639], [-0.023208515458651945,
+ 0.05018047922229022, 0.6821432561027146, 0.27129990888960803,
+ -0.05142937279813893], [-0.013943731217351596, -0.03637638898995256,
+ 0.050180479222290235, -0.003537793416633377, -0.03117306818416484],
+ [-0.0024085358814646654, -0.013943731217351596, -0.023208515458651942,
+ -0.01734151061590863, -0.005819138225232016]], [[-0.004112035299428132,
+ -0.02790922286627615, -0.04887867620762644, -0.04017465013915711,
+ -0.017203222289937245], [-0.012971303942569708, -0.021178184193661718,
+ 0.24585519478421136, 0.13727831636454993, -0.04052736449955127],
+ [-0.017237252814827183, 0.007986619792820018, 0.620261350985757,
+ 0.36402234110936116, -0.050457063493932794], [-0.01022545253060496,
+ -0.039957113612285315, 0.043148065916319644, 0.010278898793053767,
+ -0.0360731700595701], [-0.0016530642487971618, -0.012434273033433193,
+ -0.022137366266623233, -0.018321074249868192, -0.007380297997922878]],
+ [[-0.0027912209880402413, -0.02433614004771699, -0.04562658821385711,
+ -0.042468451339058945, -0.020664074967504827], [-0.009571146384946006,
+ -0.03193943219458265, 0.21238920010551743, 0.17567618137781163,
+ -0.038386325754271367], [-0.012883266171804209, -0.020308275361446707,
+ 0.5398093391410691, 0.4522064270238288, -0.0400210108674202],
+ [-0.007309372111524046, -0.040440137416543155, 0.033694739393926795,
+ 0.022877571178541393, -0.039000354140279817], [-0.001077826960040027,
+ -0.010740155274818476, -0.02070211170009258, -0.019367833372750345,
+ -0.009019734953997745]]], [[[-0.01626392539751837, -0.035832545662066145,
+ -0.03866988475938143, -0.01930821727497102, -0.0021015621671157296],
+ [-0.03954147855053079, 0.11572472115297622, 0.14229549707292574,
+ -0.03620398561701562, -0.008907053401106523], [-0.04046620032608075,
+ 0.47416733354946294, 0.5659339775075575, -0.019741250657301427,
+ -0.013176682690936197], [-0.039796214235811526, 0.06284440084948134,
+ 0.08045180751196822, -0.03919545281633188, -0.008138951872408828],
+ [-0.012244853215160443, -0.0268506592492741, -0.02888297740242396,
+ -0.014560933634183601, -0.0015349087147535293]], [[-0.013417641633011906,
+ -0.03365072121724163, -0.04158013952905282, -0.022317053587060733,
+ -0.003131096848010505], [-0.039656293315589966, 0.08734505711506495,
+ 0.16637288777763282, -0.029462659514994483, -0.012180160279609385],
+ [-0.051516162733405924, 0.3819429516502501, 0.6502702298731253,
+ 0.00992054733419745, -0.01763265975706309], [-0.03814886304138625,
+ 0.04338227748703875, 0.09621636053079638, -0.03600283468483378,
+ -0.011256197301616295], [-0.010058190693394593, -0.02525993472848121,
+ -0.03101388043728703, -0.016849195023633558, -0.0023166274424323116]],
+ [[-0.01064002753164297, -0.0315398058472134, -0.04383615619088237,
+ -0.02511202585552835, -0.004465109850519686], [-0.03608088767126983,
+ 0.05686230181726653, 0.18459474318150484, -0.0172863640478447,
+ -0.01636186809305578], [-0.05272168029782532, 0.2850099807490571,
+ 0.7151797455936235, 0.0540733106155474, -0.02377052601994029],
+ [-0.0337566984532446, 0.02230594207226228, 0.1080561274332002,
+ -0.028675684605006246, -0.01522847594949364], [-0.007955856657996014,
+ -0.02374955428721688, -0.03263677274980321, -0.018931312997898533,
+ -0.003333344356080022]], [[-0.008199753617852345, -0.02964168716094745,
+ -0.04499286779343149, -0.02745350495005966, -0.006124077091711166],
+ [-0.02964168716094745, 0.027274160377919326, 0.19446599876518117,
+ 0.0015983184753035505, -0.022324728394118268], [-0.04499286779343149,
+ 0.19446599876518125, 0.7498250634433566, 0.11452620166036631,
+ -0.03348047712449868], [-0.027453504950059663, 0.0015983184753035505,
+ 0.11452620166036631, -0.016056808817843177, -0.02070338975868157],
+ [-0.006124077091711163, -0.022324728394118268, -0.03348047712449867,
+ -0.02070338975868157, -0.004582234640385923]], [[-0.006124077091711165,
+ -0.027453504950059653, -0.04499286779343149, -0.02964168716094745,
+ -0.008199753617852345], [-0.022324728394118268, 0.0015983184753035479,
+ 0.19446599876518117, 0.027274160377919316, -0.02964168716094745],
+ [-0.03348047712449866, 0.11452620166036634, 0.7498250634433566,
+ 0.19446599876518117, -0.04499286779343149], [-0.020703389758681575,
+ -0.016056808817843174, 0.11452620166036631, 0.0015983184753035442,
+ -0.02745350495005966], [-0.004582234640385922, -0.020703389758681568,
+ -0.03348047712449866, -0.022324728394118268, -0.006124077091711163]],
+ [[-0.004465109850519687, -0.025112025855528342, -0.04383615619088236,
+ -0.03153980584721341, -0.01064002753164297], [-0.01636186809305578,
+ -0.017286364047844702, 0.18459474318150484, 0.056862301817266536,
+ -0.03608088767126984], [-0.023770526019940296, 0.054073310615547404,
+ 0.7151797455936236, 0.285009980749057, -0.05272168029782532],
+ [-0.015228475949493642, -0.028675684605006246, 0.10805612743320021,
+ 0.022305942072262292, -0.03375669845324461], [-0.003333344356080022,
+ -0.018931312997898533, -0.03263677274980321, -0.023749554287216885,
+ -0.007955856657996013]], [[-0.003131096848010504, -0.02231705358706072,
+ -0.04158013952905278, -0.03365072121724162, -0.013417641633011903],
+ [-0.01218016027960938, -0.029462659514994476, 0.16637288777763273,
+ 0.0873450571150649, -0.03965629331558995], [-0.017632659757063088,
+ 0.009920547334197435, 0.6502702298731252, 0.38194295165024994,
+ -0.051516162733405875], [-0.01125619730161629, -0.036002834684833764,
+ 0.09621636053079632, 0.04338227748703877, -0.03814886304138623],
+ [-0.0023166274424323103, -0.01684919502363354, -0.031013880437287016,
+ -0.025259934728481197, -0.010058190693394588]], [[-0.0021015621671157296,
+ -0.01930821727497101, -0.03866988475938142, -0.035832545662066145,
+ -0.016263925397518367], [-0.008907053401106521, -0.03620398561701562,
+ 0.14229549707292571, 0.11572472115297618, -0.03954147855053078],
+ [-0.013176682690936197, -0.019741250657301437, 0.5659339775075574,
+ 0.4741673335494629, -0.04046620032608073], [-0.008138951872408826,
+ -0.03919545281633188, 0.08045180751196819, 0.06284440084948135,
+ -0.039796214235811506], [-0.0015349087147535291, -0.014560933634183593,
+ -0.028882977402423952, -0.026850659249274097, -0.01224485321516044]]],
+ [[[-0.012244853215160442, -0.0268506592492741, -0.02888297740242396,
+ -0.0145609336341836, -0.0015349087147535293], [-0.039796214235811526,
+ 0.06284440084948134, 0.08045180751196819, -0.03919545281633189,
+ -0.008138951872408828], [-0.040466200326080747, 0.47416733354946294,
+ 0.5659339775075575, -0.019741250657301427, -0.013176682690936197],
+ [-0.03954147855053079, 0.1157247211529762, 0.14229549707292577,
+ -0.03620398561701562, -0.008907053401106523], [-0.016263925397518374,
+ -0.035832545662066145, -0.03866988475938143, -0.019308217274971024,
+ -0.00210156216711573]], [[-0.010058190693394592, -0.025259934728481204,
+ -0.031013880437287023, -0.016849195023633547, -0.0023166274424323103],
+ [-0.03814886304138625, 0.04338227748703876, 0.09621636053079634,
+ -0.036002834684833764, -0.011256197301616292], [-0.05151616273340591,
+ 0.38194295165025, 0.6502702298731253, 0.009920547334197446,
+ -0.017632659757063088], [-0.039656293315589966, 0.08734505711506492,
+ 0.16637288777763276, -0.029462659514994476, -0.012180160279609383],
+ [-0.013417641633011903, -0.03365072121724163, -0.041580139529052804,
+ -0.02231705358706074, -0.0031310968480105046]], [[-0.007955856657996016,
+ -0.02374955428721689, -0.03263677274980321, -0.01893131299789854,
+ -0.003333344356080023], [-0.03375669845324461, 0.02230594207226229,
+ 0.10805612743320021, -0.02867568460500625, -0.01522847594949364],
+ [-0.05272168029782533, 0.28500998074905703, 0.7151797455936236,
+ 0.05407331061554741, -0.0237705260199403], [-0.03608088767126984,
+ 0.05686230181726652, 0.18459474318150484, -0.017286364047844702,
+ -0.016361868093055783], [-0.01064002753164297, -0.03153980584721341,
+ -0.04383615619088236, -0.025112025855528356, -0.004465109850519687]],
+ [[-0.006124077091711165, -0.022324728394118264, -0.03348047712449866,
+ -0.020703389758681568, -0.004582234640385924], [-0.02745350495005966,
+ 0.0015983184753035565, 0.11452620166036628, -0.016056808817843167,
+ -0.020703389758681568], [-0.04499286779343149, 0.19446599876518122,
+ 0.7498250634433568, 0.1145262016603663, -0.03348047712449867],
+ [-0.02964168716094745, 0.027274160377919326, 0.1944659987651812,
+ 0.0015983184753035424, -0.022324728394118264], [-0.008199753617852345,
+ -0.02964168716094745, -0.04499286779343149, -0.027453504950059663,
+ -0.006124077091711164]], [[-0.004582234640385923, -0.02070338975868156,
+ -0.03348047712449866, -0.022324728394118268, -0.006124077091711164],
+ [-0.020703389758681568, -0.01605680881784317, 0.11452620166036626,
+ 0.0015983184753035535, -0.02745350495005966], [-0.03348047712449866,
+ 0.11452620166036631, 0.7498250634433566, 0.1944659987651812,
+ -0.04499286779343149], [-0.022324728394118268, 0.0015983184753035503,
+ 0.19446599876518122, 0.02727416037791932, -0.02964168716094745],
+ [-0.006124077091711164, -0.027453504950059653, -0.04499286779343149,
+ -0.02964168716094745, -0.008199753617852345]], [[-0.0033333443560800216,
+ -0.018931312997898523, -0.0326367727498032, -0.023749554287216878,
+ -0.007955856657996013], [-0.015228475949493635, -0.028675684605006243,
+ 0.10805612743320019, 0.022305942072262285, -0.0337566984532446],
+ [-0.02377052601994029, 0.05407331061554739, 0.7151797455936234,
+ 0.2850099807490569, -0.052721680297825306], [-0.016361868093055777,
+ -0.0172863640478447, 0.18459474318150482, 0.05686230181726653,
+ -0.03608088767126982], [-0.004465109850519686, -0.025112025855528346,
+ -0.04383615619088235, -0.03153980584721339, -0.010640027531642966]],
+ [[-0.002316627442432311, -0.01684919502363354, -0.031013880437287023,
+ -0.025259934728481207, -0.010058190693394592], [-0.011256197301616293,
+ -0.036002834684833764, 0.09621636053079634, 0.04338227748703875,
+ -0.03814886304138625], [-0.01763265975706309, 0.009920547334197437,
+ 0.6502702298731253, 0.38194295165025, -0.05151616273340589],
+ [-0.012180160279609383, -0.029462659514994483, 0.16637288777763276,
+ 0.08734505711506496, -0.03965629331558995], [-0.0031310968480105046,
+ -0.022317053587060733, -0.04158013952905281, -0.03365072121724162,
+ -0.013417641633011903]], [[-0.00153490871475353, -0.014560933634183603,
+ -0.028882977402423966, -0.02685065924927412, -0.012244853215160445],
+ [-0.008138951872408833, -0.039195452816331904, 0.08045180751196822,
+ 0.06284440084948137, -0.03979621423581154], [-0.013176682690936204,
+ -0.019741250657301448, 0.5659339775075575, 0.47416733354946317,
+ -0.04046620032608075], [-0.008907053401106528, -0.036203985617015634,
+ 0.1422954970729258, 0.1157247211529763, -0.0395414785505308],
+ [-0.002101562167115731, -0.01930821727497103, -0.03866988475938145,
+ -0.035832545662066166, -0.01626392539751838]]], [[[-0.00901973495399775,
+ -0.01936783337275035, -0.02070211170009259, -0.010740155274818482,
+ -0.001077826960040027], [-0.039000354140279844, 0.022877571178541386,
+ 0.033694739393926795, -0.04044013741654317, -0.007309372111524048],
+ [-0.040021010867420236, 0.452206427023829, 0.5398093391410694,
+ -0.020308275361446707, -0.01288326617180421], [-0.038386325754271394,
+ 0.1756761813778117, 0.21238920010551754, -0.031939432194582666,
+ -0.00957114638494601], [-0.020664074967504838, -0.042468451339058966,
+ -0.04562658821385713, -0.024336140047717003, -0.002791220988040243]],
+ [[-0.007380297997922877, -0.018321074249868192, -0.022137366266623233,
+ -0.012434273033433195, -0.0016530642487971611], [-0.0360731700595701,
+ 0.01027889879305376, 0.04314806591631964, -0.03995711361228531,
+ -0.010225452530604959], [-0.05045706349393281, 0.3640223411093611,
+ 0.6202613509857569, 0.007986619792820032, -0.017237252814827183],
+ [-0.04052736449955128, 0.13727831636454993, 0.24585519478421136,
+ -0.021178184193661704, -0.012971303942569708], [-0.017203222289937245,
+ -0.040174650139157116, -0.04887867620762645, -0.027909222866276163,
+ -0.004112035299428132]], [[-0.005819138225232015, -0.01734151061590863,
+ -0.02320851545865194, -0.0139437312173516, -0.0024085358814646654],
+ [-0.031173068184164845, -0.0035377934166333767, 0.05018047922229021,
+ -0.03637638898995257, -0.013943731217351596], [-0.05142937279813893,
+ 0.27129990888960803, 0.6821432561027146, 0.050180479222290235,
+ -0.023208515458651942], [-0.0379761971054064, 0.0962810362260875,
+ 0.27129990888960803, -0.0035377934166333793, -0.017341510615908634],
+ [-0.01374148963820583, -0.0379761971054064, -0.05142937279813894,
+ -0.031173068184164855, -0.005819138225232018]], [[-0.004465109850519687,
+ -0.016361868093055777, -0.023770526019940293, -0.015228475949493638,
+ -0.0033333443560800233], [-0.02511202585552834, -0.017286364047844695,
+ 0.054073310615547376, -0.028675684605006243, -0.018931312997898533],
+ [-0.04383615619088236, 0.1845947431815049, 0.7151797455936236,
+ 0.10805612743320021, -0.03263677274980321], [-0.03153980584721341,
+ 0.05686230181726654, 0.285009980749057, 0.02230594207226228,
+ -0.023749554287216885], [-0.010640027531642969, -0.03608088767126984,
+ -0.052721680297825334, -0.03375669845324461, -0.007955856657996016]],
+ [[-0.003333344356080022, -0.015228475949493635, -0.023770526019940282,
+ -0.016361868093055777, -0.004465109850519686], [-0.018931312997898526,
+ -0.02867568460500624, 0.054073310615547356, -0.01728636404784469,
+ -0.02511202585552834], [-0.0326367727498032, 0.10805612743320023,
+ 0.7151797455936234, 0.18459474318150484, -0.04383615619088235],
+ [-0.023749554287216878, 0.022305942072262296, 0.285009980749057,
+ 0.05686230181726651, -0.03153980584721339], [-0.007955856657996014,
+ -0.033756698453244596, -0.05272168029782531, -0.03608088767126983,
+ -0.010640027531642967]], [[-0.002408535881464665, -0.013943731217351592,
+ -0.023208515458651935, -0.017341510615908627, -0.005819138225232014],
+ [-0.013943731217351592, -0.03637638898995256, 0.050180479222290214,
+ -0.003537793416633366, -0.031173068184164845], [-0.02320851545865193,
+ 0.05018047922229023, 0.6821432561027146, 0.271299908889608,
+ -0.051429372798138924], [-0.017341510615908627, -0.003537793416633378,
+ 0.271299908889608, 0.0962810362260875, -0.03797619710540638],
+ [-0.005819138225232016, -0.03117306818416484, -0.05142937279813893,
+ -0.03797619710540639, -0.013741489638205826]], [[-0.0016530642487971614,
+ -0.012434273033433195, -0.022137366266623233, -0.01832107424986819,
+ -0.007380297997922878], [-0.01022545253060496, -0.03995711361228531,
+ 0.04314806591631963, 0.010278898793053765, -0.0360731700595701],
+ [-0.017237252814827183, 0.007986619792820022, 0.6202613509857569,
+ 0.3640223411093611, -0.05045706349393281], [-0.012971303942569706,
+ -0.021178184193661718, 0.24585519478421136, 0.13727831636454998,
+ -0.04052736449955127], [-0.004112035299428132, -0.027909222866276156,
+ -0.04887867620762645, -0.04017465013915711, -0.017203222289937245]],
+ [[-0.0010778269600400273, -0.010740155274818482, -0.02070211170009259,
+ -0.01936783337275035, -0.009019734953997749], [-0.007309372111524049,
+ -0.04044013741654317, 0.03369473939392679, 0.022877571178541403,
+ -0.039000354140279844], [-0.012883266171804212, -0.020308275361446713,
+ 0.5398093391410693, 0.4522064270238289, -0.04002101086742022],
+ [-0.00957114638494601, -0.031939432194582666, 0.2123892001055175,
+ 0.17567618137781177, -0.03838632575427138], [-0.002791220988040243,
+ -0.024336140047717003, -0.04562658821385713, -0.04246845133905897,
+ -0.020664074967504838]]], [[[-0.006466489422914399, -0.014502394951723476,
+ -0.015459264122748746, -0.007755474877032561, -0.0007159165805851706],
+ [-0.036632851477625676, -0.005730455022054154, -7.890574314417718e-05,
+ -0.03942402104403913, -0.006389875713059275], [-0.039015079941410555,
+ 0.4111930051936302, 0.4909386897413152, -0.021181804710686668,
+ -0.012319067611648218], [-0.03752601069182331, 0.239032194778253,
+ 0.2868949551896562, -0.027885740619110408, -0.010202069931803578],
+ [-0.025194061504750533, -0.04314593981787028, -0.04562755195174027,
+ -0.029204772772557768, -0.003601096394526257]], [[-0.005252595229206095,
+ -0.013729503292946928, -0.016482266055193047, -0.009045186119473492,
+ -0.0011168422627331079], [-0.03293190035303923, -0.013001053856678081,
+ 0.004583518760872412, -0.040827417160105635, -0.009045186119473489],
+ [-0.04831487472573022, 0.33063684267898774, 0.5640812622041926,
+ 0.004583518760872408, -0.01648226605519305], [-0.04173044153813555,
+ 0.18968272846778533, 0.3306368426789878, -0.01300105385667808,
+ -0.013729503292946928], [-0.02128481178805433, -0.041730441538135564,
+ -0.048314874725730234, -0.03293190035303923, -0.005252595229206095]],
+ [[-0.004112035299428132, -0.012971303942569708, -0.017237252814827186,
+ -0.010225452530604966, -0.0016530642487971618], [-0.027909222866276156,
+ -0.02117818419366171, 0.007986619792820025, -0.039957113612285315,
+ -0.0124342730334332], [-0.04887867620762645, 0.24585519478421144,
+ 0.620261350985757, 0.04314806591631966, -0.022137366266623243],
+ [-0.040174650139157116, 0.13727831636454998, 0.3640223411093612,
+ 0.010278898793053765, -0.018321074249868192], [-0.017203222289937245,
+ -0.04052736449955129, -0.05045706349393283, -0.03607317005957011,
+ -0.007380297997922882]], [[-0.0031310968480105046, -0.012180160279609381,
+ -0.017632659757063088, -0.011256197301616295, -0.0023166274424323108],
+ [-0.022317053587060723, -0.02946265951499447, 0.009920547334197437,
+ -0.036002834684833764, -0.016849195023633544], [-0.0415801395290528,
+ 0.16637288777763282, 0.6502702298731252, 0.09621636053079637,
+ -0.031013880437287027], [-0.03365072121724162, 0.08734505711506496,
+ 0.38194295165025005, 0.04338227748703875, -0.025259934728481197],
+ [-0.013417641633011903, -0.03965629331558996, -0.05151616273340592,
+ -0.03814886304138624, -0.010058190693394595]], [[-0.0023166274424323103,
+ -0.011256197301616293, -0.017632659757063088, -0.012180160279609383,
+ -0.0031310968480105046], [-0.016849195023633544, -0.036002834684833764,
+ 0.009920547334197428, -0.029462659514994466, -0.02231705358706073],
+ [-0.03101388043728702, 0.0962163605307964, 0.6502702298731252,
+ 0.1663728877776328, -0.041580139529052804], [-0.025259934728481197,
+ 0.043382277487038774, 0.38194295165025005, 0.08734505711506493,
+ -0.033650721217241615], [-0.010058190693394593, -0.03814886304138624,
+ -0.05151616273340592, -0.03965629331558995, -0.013417641633011903]],
+ [[-0.0016530642487971614, -0.01022545253060496, -0.017237252814827183,
+ -0.012971303942569706, -0.00411203529942813], [-0.012434273033433196,
+ -0.03995711361228531, 0.007986619792820018, -0.021178184193661697,
+ -0.027909222866276152], [-0.022137366266623233, 0.04314806591631965,
+ 0.6202613509857569, 0.24585519478421133, -0.04887867620762643],
+ [-0.018321074249868185, 0.010278898793053763, 0.3640223411093611,
+ 0.13727831636454993, -0.040174650139157095], [-0.00738029799792288,
+ -0.0360731700595701, -0.05045706349393282, -0.04052736449955128,
+ -0.01720322228993724]], [[-0.0011168422627331075, -0.009045186119473489,
+ -0.016482266055193047, -0.013729503292946922, -0.005252595229206093],
+ [-0.00904518611947349, -0.04082741716010563, 0.004583518760872401,
+ -0.01300105385667807, -0.032931900353039216], [-0.016482266055193047,
+ 0.004583518760872399, 0.5640812622041926, 0.33063684267898774,
+ -0.0483148747257302], [-0.013729503292946922, -0.01300105385667808,
+ 0.33063684267898774, 0.1896827284677853, -0.04173044153813553],
+ [-0.005252595229206094, -0.03293190035303922, -0.048314874725730206,
+ -0.04173044153813555, -0.021284811788054327]], [[-0.0007159165805851706,
+ -0.00775547487703256, -0.015459264122748746, -0.014502394951723471,
+ -0.006466489422914398], [-0.006389875713059273, -0.039424021044039116,
+ -7.890574314417675e-05, -0.0057304550220541334, -0.03663285147762567],
+ [-0.012319067611648212, -0.02118180471068667, 0.4909386897413151,
+ 0.4111930051936303, -0.03901507994141054], [-0.010202069931803573,
+ -0.02788574061911041, 0.28689495518965613, 0.23903219477825297,
+ -0.037526010691823306], [-0.0036010963945262557, -0.029204772772557758,
+ -0.04562755195174025, -0.043145939817870266, -0.025194061504750526]]],
+ [[[-0.004476318148146651, -0.010817484288013232, -0.011552729379100072,
+ -0.005422291349995998, -0.0004507227386051219], [-0.03324558280295301,
+ -0.02447552237556967, -0.022489385288542604, -0.03680917952171095,
+ -0.005422291349995998], [-0.0378381168526885, 0.35757708121641424,
+ 0.4272005024152728, -0.022489385288542604, -0.011552729379100074],
+ [-0.0370635264420727, 0.2989532845474528, 0.35757708121641435,
+ -0.024475522375569658, -0.010817484288013228], [-0.029286133281073247,
+ -0.037063526442072704, -0.037838116852688494, -0.03324558280295301,
+ -0.00447631814814665]], [[-0.0036010963945262574, -0.01020206993180358,
+ -0.012319067611648216, -0.006389875713059276, -0.0007159165805851708],
+ [-0.029204772772557765, -0.02788574061911041, -0.02118180471068666,
+ -0.03942402104403913, -0.007755474877032563], [-0.04562755195174027,
+ 0.2868949551896562, 0.490938689741315, -7.890574314416898e-05,
+ -0.015459264122748749], [-0.043145939817870266, 0.239032194778253,
+ 0.41119300519363033, -0.005730455022054142, -0.014502394951723474],
+ [-0.025194061504750533, -0.03752601069182331, -0.039015079941410555,
+ -0.036632851477625676, -0.0064664894229144]], [[-0.002791220988040242,
+ -0.009571146384946008, -0.012883266171804207, -0.007309372111524051,
+ -0.0010778269600400271], [-0.024336140047716993, -0.03193943219458266,
+ -0.020308275361446703, -0.040440137416543155, -0.010740155274818482],
+ [-0.04562658821385712, 0.2123892001055175, 0.5398093391410692,
+ 0.033694739393926816, -0.020702111700092587], [-0.042468451339058945,
+ 0.1756761813778117, 0.4522064270238289, 0.022877571178541396,
+ -0.019367833372750342], [-0.020664074967504824, -0.03838632575427138,
+ -0.040021010867420236, -0.03900035414027984, -0.00901973495399775]],
+ [[-0.0021015621671157296, -0.008907053401106525, -0.013176682690936196,
+ -0.008138951872408831, -0.0015349087147535293], [-0.019308217274971017,
+ -0.03620398561701561, -0.019741250657301434, -0.03919545281633189,
+ -0.014560933634183596], [-0.03866988475938143, 0.1422954970729258,
+ 0.5659339775075574, 0.0804518075119682, -0.028882977402423963],
+ [-0.035832545662066145, 0.11572472115297625, 0.47416733354946283,
+ 0.06284440084948135, -0.026850659249274097], [-0.01626392539751837,
+ -0.039541478550530786, -0.04046620032608076, -0.03979621423581152,
+ -0.012244853215160443]], [[-0.0015349087147535293, -0.008138951872408831,
+ -0.013176682690936201, -0.008907053401106528, -0.00210156216711573],
+ [-0.0145609336341836, -0.0391954528163319, -0.019741250657301437,
+ -0.03620398561701563, -0.01930821727497102], [-0.028882977402423963,
+ 0.08045180751196825, 0.5659339775075575, 0.14229549707292577,
+ -0.038669884759381434], [-0.0268506592492741, 0.06284440084948138,
+ 0.47416733354946305, 0.11572472115297622, -0.03583254566206615],
+ [-0.012244853215160445, -0.03979621423581153, -0.04046620032608077,
+ -0.03954147855053079, -0.016263925397518374]], [[-0.0010778269600400273,
+ -0.00730937211152405, -0.01288326617180421, -0.00957114638494601,
+ -0.002791220988040242], [-0.010740155274818482, -0.04044013741654317,
+ -0.020308275361446707, -0.031939432194582666, -0.024336140047717],
+ [-0.020702111700092594, 0.03369473939392682, 0.5398093391410693,
+ 0.21238920010551754, -0.04562658821385713], [-0.019367833372750342,
+ 0.022877571178541396, 0.452206427023829, 0.17567618137781177,
+ -0.04246845133905895], [-0.009019734953997752, -0.039000354140279844,
+ -0.040021010867420236, -0.03838632575427138, -0.02066407496750483]],
+ [[-0.0007159165805851705, -0.006389875713059274, -0.012319067611648212,
+ -0.010202069931803576, -0.0036010963945262565], [-0.007755474877032563,
+ -0.03942402104403912, -0.02118180471068666, -0.0278857406191104,
+ -0.029204772772557758], [-0.015459264122748742, -7.890574314417695e-05,
+ 0.4909386897413151, 0.2868949551896562, -0.04562755195174026],
+ [-0.01450239495172347, -0.0057304550220541465, 0.4111930051936302,
+ 0.23903219477825297, -0.04314593981787025], [-0.0064664894229144,
+ -0.03663285147762567, -0.03901507994141055, -0.037526010691823306,
+ -0.025194061504750526]], [[-0.0004507227386051219, -0.005422291349995998,
+ -0.01155272937910007, -0.01081748428801323, -0.00447631814814665],
+ [-0.005422291349995998, -0.03680917952171095, -0.022489385288542607,
+ -0.024475522375569655, -0.03324558280295302], [-0.011552729379100074,
+ -0.02248938528854261, 0.4272005024152728, 0.3575770812164143,
+ -0.0378381168526885], [-0.010817484288013228, -0.02447552237556967,
+ 0.3575770812164143, 0.29895328454745285, -0.03706352644207268],
+ [-0.00447631814814665, -0.03324558280295302, -0.0378381168526885,
+ -0.03706352644207269, -0.029286133281073243]]]]
+}
+
+if __name__ == "__main__":
+ main() \ No newline at end of file
diff --git a/media/libjxl/src/tools/viewer/CMakeLists.txt b/media/libjxl/src/tools/viewer/CMakeLists.txt
new file mode 100644
index 0000000000..7dbe5e3153
--- /dev/null
+++ b/media/libjxl/src/tools/viewer/CMakeLists.txt
@@ -0,0 +1,39 @@
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+find_package(Qt5 QUIET COMPONENTS Widgets)
+if (NOT Qt5_FOUND)
+ message(WARNING "Qt5 was not found. The directory viewer will not be built.")
+ return()
+endif ()
+
+if (NOT TARGET icc_detect)
+ message(WARNING "The directory viewer depends on the comparison tool and will also not be built.")
+ return ()
+endif ()
+
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOUIC ON)
+
+add_executable(viewer WIN32
+ load_jxl.cc
+ load_jxl.h
+ main.cc
+ viewer_window.cc
+ viewer_window.h
+ viewer_window.ui
+)
+target_include_directories(viewer PRIVATE
+ $<TARGET_PROPERTY:lcms2,INCLUDE_DIRECTORIES>
+ "${PROJECT_SOURCE_DIR}"
+)
+target_link_libraries(viewer
+ Qt5::Widgets
+ icc_detect
+ jxl
+ jxl_threads
+ lcms2
+)
diff --git a/media/libjxl/src/tools/viewer/load_jxl.cc b/media/libjxl/src/tools/viewer/load_jxl.cc
new file mode 100644
index 0000000000..7fd35d8224
--- /dev/null
+++ b/media/libjxl/src/tools/viewer/load_jxl.cc
@@ -0,0 +1,174 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/viewer/load_jxl.h"
+
+#include <stdint.h>
+
+#include <QElapsedTimer>
+#include <QFile>
+
+#include "jxl/decode.h"
+#include "jxl/decode_cxx.h"
+#include "jxl/thread_parallel_runner_cxx.h"
+#include "jxl/types.h"
+#include "lcms2.h"
+
+namespace jxl {
+
+namespace {
+
+struct CmsProfileCloser {
+ void operator()(const cmsHPROFILE profile) const {
+ if (profile != nullptr) {
+ cmsCloseProfile(profile);
+ }
+ }
+};
+using CmsProfileUniquePtr =
+ std::unique_ptr<std::remove_pointer<cmsHPROFILE>::type, CmsProfileCloser>;
+
+struct CmsTransformDeleter {
+ void operator()(const cmsHTRANSFORM transform) const {
+ if (transform != nullptr) {
+ cmsDeleteTransform(transform);
+ }
+ }
+};
+using CmsTransformUniquePtr =
+ std::unique_ptr<std::remove_pointer<cmsHTRANSFORM>::type,
+ CmsTransformDeleter>;
+
+} // namespace
+
+QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile,
+ qint64* elapsed_ns, bool* usedRequestedProfile) {
+ auto runner = JxlThreadParallelRunnerMake(
+ nullptr, JxlThreadParallelRunnerDefaultNumWorkerThreads());
+
+ auto dec = JxlDecoderMake(nullptr);
+
+#define EXPECT_TRUE(a) \
+ do { \
+ if (!(a)) { \
+ fprintf(stderr, "Assertion failure (%d): %s\n", __LINE__, #a); \
+ return QImage(); \
+ } \
+ } while (false)
+#define EXPECT_EQ(a, b) \
+ do { \
+ int a_ = a; \
+ int b_ = b; \
+ if (a_ != b_) { \
+ fprintf(stderr, "Assertion failure (%d): %s (%d) != %s (%d)\n", \
+ __LINE__, #a, a_, #b, b_); \
+ return QImage(); \
+ } \
+ } while (false)
+
+ EXPECT_EQ(JXL_DEC_SUCCESS,
+ JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO |
+ JXL_DEC_COLOR_ENCODING |
+ JXL_DEC_FULL_IMAGE));
+ QFile jpegXlFile(filename);
+ if (!jpegXlFile.open(QIODevice::ReadOnly)) {
+ return QImage();
+ }
+ const QByteArray jpegXlData = jpegXlFile.readAll();
+ if (jpegXlData.size() < 4) {
+ return QImage();
+ }
+
+ QElapsedTimer timer;
+ timer.start();
+ const uint8_t* jxl_data = reinterpret_cast<const uint8_t*>(jpegXlData.data());
+ size_t jxl_size = jpegXlData.size();
+ JxlDecoderSetInput(dec.get(), jxl_data, jxl_size);
+ EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec.get()));
+ JxlBasicInfo info;
+ EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec.get(), &info));
+ size_t pixel_count = info.xsize * info.ysize;
+
+ EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec.get()));
+ static const JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN,
+ 0};
+ size_t icc_size;
+ EXPECT_EQ(JXL_DEC_SUCCESS,
+ JxlDecoderGetICCProfileSize(
+ dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size));
+ std::vector<uint8_t> icc_profile(icc_size);
+ EXPECT_EQ(JXL_DEC_SUCCESS,
+ JxlDecoderGetColorAsICCProfile(
+ dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA,
+ icc_profile.data(), icc_profile.size()));
+
+ std::vector<float> float_pixels(pixel_count * 4);
+ EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec.get()));
+ EXPECT_EQ(JXL_DEC_SUCCESS,
+ JxlDecoderSetImageOutBuffer(dec.get(), &format, float_pixels.data(),
+ pixel_count * 4 * sizeof(float)));
+ EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec.get()));
+
+ std::vector<uint16_t> uint16_pixels(pixel_count * 4);
+ const thread_local cmsContext context = cmsCreateContext(nullptr, nullptr);
+ EXPECT_TRUE(context != nullptr);
+ const CmsProfileUniquePtr jxl_profile(cmsOpenProfileFromMemTHR(
+ context, icc_profile.data(), icc_profile.size()));
+ EXPECT_TRUE(jxl_profile != nullptr);
+ CmsProfileUniquePtr target_profile(cmsOpenProfileFromMemTHR(
+ context, targetIccProfile.data(), targetIccProfile.size()));
+ if (usedRequestedProfile != nullptr) {
+ *usedRequestedProfile = (target_profile != nullptr);
+ }
+ if (target_profile == nullptr) {
+ target_profile.reset(cmsCreate_sRGBProfileTHR(context));
+ }
+ EXPECT_TRUE(target_profile != nullptr);
+ CmsTransformUniquePtr transform(cmsCreateTransformTHR(
+ context, jxl_profile.get(), TYPE_RGBA_FLT, target_profile.get(),
+ TYPE_RGBA_16, INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_COPY_ALPHA));
+ EXPECT_TRUE(transform != nullptr);
+ cmsDoTransform(transform.get(), float_pixels.data(), uint16_pixels.data(),
+ pixel_count);
+ if (elapsed_ns != nullptr) *elapsed_ns = timer.nsecsElapsed();
+
+ QImage result(info.xsize, info.ysize,
+#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
+ info.alpha_premultiplied ? QImage::Format_RGBA64_Premultiplied
+ : QImage::Format_RGBA64
+#else
+ info.alpha_premultiplied ? QImage::Format_ARGB32_Premultiplied
+ : QImage::Format_ARGB32
+#endif
+ );
+
+ for (int y = 0; y < result.height(); ++y) {
+#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
+ QRgba64* const row = reinterpret_cast<QRgba64*>(result.scanLine(y));
+#else
+ QRgb* const row = reinterpret_cast<QRgb*>(result.scanLine(y));
+#endif
+ const uint16_t* const data = uint16_pixels.data() + result.width() * y * 4;
+ for (int x = 0; x < result.width(); ++x) {
+#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
+ row[x] = qRgba64(data[4 * x + 0], data[4 * x + 1], data[4 * x + 2],
+ data[4 * x + 3])
+#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0)
+ .toArgb32()
+#endif
+ ;
+#else
+ // Qt version older than 5.6 doesn't have a qRgba64.
+ row[x] = qRgba(data[4 * x + 0] * (255.f / 65535) + .5f,
+ data[4 * x + 1] * (255.f / 65535) + .5f,
+ data[4 * x + 2] * (255.f / 65535) + .5f,
+ data[4 * x + 3] * (255.f / 65535) + .5f);
+#endif
+ }
+ }
+ return result;
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/viewer/load_jxl.h b/media/libjxl/src/tools/viewer/load_jxl.h
new file mode 100644
index 0000000000..594f646f01
--- /dev/null
+++ b/media/libjxl/src/tools/viewer/load_jxl.h
@@ -0,0 +1,20 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_VIEWER_LOAD_JXL_H_
+#define TOOLS_VIEWER_LOAD_JXL_H_
+
+#include <QByteArray>
+#include <QImage>
+#include <QString>
+
+namespace jxl {
+
+QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile,
+ qint64* elapsed, bool* usedRequestedProfile = nullptr);
+
+} // namespace jxl
+
+#endif // TOOLS_VIEWER_LOAD_JXL_H_
diff --git a/media/libjxl/src/tools/viewer/main.cc b/media/libjxl/src/tools/viewer/main.cc
new file mode 100644
index 0000000000..d677888f61
--- /dev/null
+++ b/media/libjxl/src/tools/viewer/main.cc
@@ -0,0 +1,23 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <QApplication>
+
+#include "tools/viewer/viewer_window.h"
+
+int main(int argc, char** argv) {
+ QApplication application(argc, argv);
+ QStringList arguments = application.arguments();
+ arguments.removeFirst();
+
+ jxl::ViewerWindow window;
+ window.show();
+
+ if (!arguments.empty()) {
+ window.loadFilesAndDirectories(arguments);
+ }
+
+ return application.exec();
+}
diff --git a/media/libjxl/src/tools/viewer/viewer_window.cc b/media/libjxl/src/tools/viewer/viewer_window.cc
new file mode 100644
index 0000000000..530c2f0141
--- /dev/null
+++ b/media/libjxl/src/tools/viewer/viewer_window.cc
@@ -0,0 +1,130 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/viewer/viewer_window.h"
+
+#include <QElapsedTimer>
+#include <QFileDialog>
+#include <QFileInfo>
+#include <QKeyEvent>
+#include <QMessageBox>
+#include <QSet>
+
+#include "tools/icc_detect/icc_detect.h"
+#include "tools/viewer/load_jxl.h"
+
+namespace jxl {
+
+namespace {
+
+template <typename Output>
+void recursivelyAddSubEntries(const QFileInfo& info,
+ QSet<QString>* const visited,
+ Output* const output) {
+ if (visited->contains(info.absoluteFilePath())) return;
+ *visited << info.absoluteFilePath();
+ if (info.isDir()) {
+ QDir dir(info.absoluteFilePath());
+ for (const QFileInfo& entry : dir.entryInfoList(
+ QStringList() << "*.jxl",
+ QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot)) {
+ recursivelyAddSubEntries(entry, visited, output);
+ }
+ } else {
+ *output << info.absoluteFilePath();
+ }
+}
+
+} // namespace
+
+ViewerWindow::ViewerWindow(QWidget* const parent)
+ : QMainWindow(parent), monitorProfile_(GetMonitorIccProfile(this)) {
+ ui_.setupUi(this);
+ ui_.actionOpen->setShortcut(QKeySequence::Open);
+ ui_.actionExit->setShortcut(QKeySequence::Quit);
+}
+
+void ViewerWindow::loadFilesAndDirectories(QStringList entries) {
+ filenames_.clear();
+ QSet<QString> visited;
+ for (const QString& entry : entries) {
+ recursivelyAddSubEntries(entry, &visited, &filenames_);
+ }
+
+ const bool several = filenames_.size() > 1;
+ ui_.actionPreviousImage->setEnabled(several);
+ ui_.actionNextImage->setEnabled(several);
+
+ currentFileIndex_ = 0;
+ refreshImage();
+}
+
+void ViewerWindow::on_actionOpen_triggered() {
+ QFileDialog dialog(this, tr("Select JPEG XL files to open…"));
+ dialog.setFileMode(QFileDialog::ExistingFiles);
+ dialog.setNameFilter(tr("JPEG XL images (*.jxl);;All files (*)"));
+ if (dialog.exec()) {
+ loadFilesAndDirectories(dialog.selectedFiles());
+ }
+}
+
+void ViewerWindow::on_actionPreviousImage_triggered() {
+ currentFileIndex_ =
+ (currentFileIndex_ - 1 + filenames_.size()) % filenames_.size();
+ refreshImage();
+}
+
+void ViewerWindow::on_actionNextImage_triggered() {
+ currentFileIndex_ = (currentFileIndex_ + 1) % filenames_.size();
+ refreshImage();
+}
+
+void ViewerWindow::refreshImage() {
+ if (currentFileIndex_ < 0 || currentFileIndex_ >= filenames_.size()) {
+ return;
+ }
+
+ qint64 elapsed_ns;
+ bool usedRequestedProfile;
+ const QImage image =
+ loadJxlImage(filenames_[currentFileIndex_], monitorProfile_, &elapsed_ns,
+ &usedRequestedProfile);
+ if (image.isNull()) {
+ const QString message =
+ tr("Failed to load \"%1\".").arg(filenames_[currentFileIndex_]);
+ ui_.image->clear();
+ ui_.statusBar->showMessage(message);
+ QMessageBox errorDialog(this);
+ errorDialog.setIcon(QMessageBox::Critical);
+ errorDialog.setWindowTitle(tr("Failed to load image"));
+ errorDialog.setText(message);
+ errorDialog.exec();
+ return;
+ }
+
+ ui_.image->setPixmap(QPixmap::fromImage(image));
+ ui_.statusBar->showMessage(
+ tr("Loaded image %L1/%L2 (%3, %4×%5) in %L6ms (%L7 fps)")
+ .arg(currentFileIndex_ + 1)
+ .arg(filenames_.size())
+ .arg(filenames_[currentFileIndex_])
+ .arg(image.width())
+ .arg(image.height())
+ .arg(elapsed_ns / 1e6)
+ .arg(1e9 / elapsed_ns));
+
+ if (!usedRequestedProfile && !hasWarnedAboutMonitorProfile_) {
+ hasWarnedAboutMonitorProfile_ = true;
+ QMessageBox message(this);
+ message.setIcon(QMessageBox::Warning);
+ message.setWindowTitle(tr("No valid monitor profile found"));
+ message.setText(
+ tr("Failed to find a usable monitor profile. Images will be shown "
+ "assuming that the monitor's colorspace is sRGB."));
+ message.exec();
+ }
+}
+
+} // namespace jxl
diff --git a/media/libjxl/src/tools/viewer/viewer_window.h b/media/libjxl/src/tools/viewer/viewer_window.h
new file mode 100644
index 0000000000..42de5bc267
--- /dev/null
+++ b/media/libjxl/src/tools/viewer/viewer_window.h
@@ -0,0 +1,41 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_VIEWER_VIEWER_WINDOW_H_
+#define TOOLS_VIEWER_VIEWER_WINDOW_H_
+
+#include <QByteArray>
+#include <QMainWindow>
+#include <QStringList>
+
+#include "tools/viewer/ui_viewer_window.h"
+
+namespace jxl {
+
+class ViewerWindow : public QMainWindow {
+ Q_OBJECT
+ public:
+ explicit ViewerWindow(QWidget* parent = nullptr);
+
+ public slots:
+ void loadFilesAndDirectories(QStringList entries);
+
+ private slots:
+ void on_actionOpen_triggered();
+ void on_actionPreviousImage_triggered();
+ void on_actionNextImage_triggered();
+ void refreshImage();
+
+ private:
+ const QByteArray monitorProfile_;
+ Ui::ViewerWindow ui_;
+ QStringList filenames_;
+ int currentFileIndex_ = 0;
+ bool hasWarnedAboutMonitorProfile_ = false;
+};
+
+} // namespace jxl
+
+#endif // TOOLS_VIEWER_VIEWER_WINDOW_H_
diff --git a/media/libjxl/src/tools/viewer/viewer_window.ui b/media/libjxl/src/tools/viewer/viewer_window.ui
new file mode 100644
index 0000000000..9539890550
--- /dev/null
+++ b/media/libjxl/src/tools/viewer/viewer_window.ui
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <comment>
+ Copyright (c) the JPEG XL Project Authors. All rights reserved.
+
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+ </comment>
+ <class>ViewerWindow</class>
+ <widget class="QMainWindow" name="ViewerWindow">
+ <property name="windowTitle">
+ <string>JPEG XL Viewer</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QLabel" name="image">
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QMenuBar" name="menuBar">
+ <widget class="QMenu" name="menuFile">
+ <property name="title">
+ <string>&amp;File</string>
+ </property>
+ <addaction name="actionOpen"/>
+ <addaction name="separator"/>
+ <addaction name="actionExit"/>
+ </widget>
+ <addaction name="menuFile"/>
+ </widget>
+ <widget class="QStatusBar" name="statusBar"/>
+ <widget class="QToolBar" name="toolBar">
+ <property name="windowTitle">
+ <string>toolBar</string>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>TopToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionOpen"/>
+ <addaction name="actionPreviousImage"/>
+ <addaction name="actionNextImage"/>
+ </widget>
+ <action name="actionOpen">
+ <property name="icon">
+ <iconset theme="document-open"/>
+ </property>
+ <property name="text">
+ <string>&amp;Open…</string>
+ </property>
+ <property name="menuRole">
+ <enum>QAction::NoRole</enum>
+ </property>
+ </action>
+ <action name="actionExit">
+ <property name="icon">
+ <iconset theme="application-exit"/>
+ </property>
+ <property name="text">
+ <string>E&amp;xit</string>
+ </property>
+ <property name="menuRole">
+ <enum>QAction::QuitRole</enum>
+ </property>
+ </action>
+ <action name="actionPreviousImage">
+ <property name="icon">
+ <iconset theme="go-previous"/>
+ </property>
+ <property name="text">
+ <string>Previous image</string>
+ </property>
+ <property name="shortcut">
+ <string>Left</string>
+ </property>
+ </action>
+ <action name="actionNextImage">
+ <property name="icon">
+ <iconset theme="go-next"/>
+ </property>
+ <property name="text">
+ <string>Next image</string>
+ </property>
+ <property name="shortcut">
+ <string>Right</string>
+ </property>
+ </action>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>actionExit</sender>
+ <signal>triggered()</signal>
+ <receiver>ViewerWindow</receiver>
+ <slot>close()</slot>
+ </connection>
+ </connections>
+</ui>
diff --git a/media/libjxl/src/tools/xyb_range.cc b/media/libjxl/src/tools/xyb_range.cc
new file mode 100644
index 0000000000..1ce4882242
--- /dev/null
+++ b/media/libjxl/src/tools/xyb_range.cc
@@ -0,0 +1,80 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+
+#include <utility>
+
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_xyb.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+
+namespace jxl {
+namespace {
+
+void PrintXybRange() {
+ Image3F linear(1u << 16, 257);
+ for (int b = 0; b < 256; ++b) {
+ float* JXL_RESTRICT row0 = linear.PlaneRow(0, b + 1);
+ float* JXL_RESTRICT row1 = linear.PlaneRow(1, b + 1);
+ float* JXL_RESTRICT row2 = linear.PlaneRow(2, b + 1);
+ for (int r = 0; r < 256; ++r) {
+ for (int g = 0; g < 256; ++g) {
+ const int x = (r << 8) + g;
+ row0[x] = r;
+ row1[x] = g;
+ row2[x] = b;
+ }
+ }
+ }
+ CodecInOut io;
+ io.metadata.m.SetUintSamples(8);
+ io.metadata.m.color_encoding = ColorEncoding::LinearSRGB();
+ io.SetFromImage(std::move(linear), io.metadata.m.color_encoding);
+ const ImageBundle& ib = io.Main();
+ ThreadPool* null_pool = nullptr;
+ Image3F opsin(ib.xsize(), ib.ysize());
+ (void)ToXYB(ib, null_pool, &opsin, GetJxlCms());
+ for (size_t c = 0; c < 3; ++c) {
+ float minval = 1e10f;
+ float maxval = -1e10f;
+ int rgb_min = 0;
+ int rgb_max = 0;
+ for (int b = 0; b < 256; ++b) {
+ const float* JXL_RESTRICT row = opsin.PlaneRow(c, b);
+ for (int r = 0; r < 256; ++r) {
+ for (int g = 0; g < 256; ++g) {
+ float val = row[(r << 8) + g];
+ if (val < minval) {
+ minval = val;
+ rgb_min = (r << 16) + (g << 8) + b;
+ }
+ if (val > maxval) {
+ maxval = val;
+ rgb_max = (r << 16) + (g << 8) + b;
+ }
+ }
+ }
+ }
+ printf("Opsin image plane %" PRIuS
+ " range: [%8.4f, %8.4f] "
+ "center: %.12f, range: %.12f (RGBmin=%06x, RGBmax=%06x)\n",
+ c, minval, maxval, 0.5 * (minval + maxval), 0.5 * (maxval - minval),
+ rgb_min, rgb_max);
+ // Ensure our constants are at least as wide as those obtained from sRGB.
+ }
+}
+
+} // namespace
+} // namespace jxl
+
+int main() { jxl::PrintXybRange(); }