summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/BUILD1
-rw-r--r--tools/CMakeLists.txt202
-rw-r--r--tools/README.cjpeg_hdr.md73
-rw-r--r--tools/args.h53
-rw-r--r--tools/benchmark/benchmark_args.cc48
-rw-r--r--tools/benchmark/benchmark_args.h17
-rw-r--r--tools/benchmark/benchmark_codec.cc61
-rw-r--r--tools/benchmark/benchmark_codec.h34
-rw-r--r--tools/benchmark/benchmark_codec_avif.cc115
-rw-r--r--tools/benchmark/benchmark_codec_avif.h6
-rw-r--r--tools/benchmark/benchmark_codec_custom.cc128
-rw-r--r--tools/benchmark/benchmark_codec_custom.h7
-rw-r--r--tools/benchmark/benchmark_codec_jpeg.cc349
-rw-r--r--tools/benchmark/benchmark_codec_jpeg.h6
-rw-r--r--tools/benchmark/benchmark_codec_jxl.cc373
-rw-r--r--tools/benchmark/benchmark_codec_jxl.h6
-rw-r--r--tools/benchmark/benchmark_codec_png.cc49
-rw-r--r--tools/benchmark/benchmark_codec_png.h10
-rw-r--r--tools/benchmark/benchmark_codec_webp.cc80
-rw-r--r--tools/benchmark/benchmark_codec_webp.h6
-rw-r--r--tools/benchmark/benchmark_file_io.cc6
-rw-r--r--tools/benchmark/benchmark_file_io.h10
-rw-r--r--tools/benchmark/benchmark_stats.cc139
-rw-r--r--tools/benchmark/benchmark_stats.h26
-rw-r--r--tools/benchmark/benchmark_utils.cc42
-rw-r--r--tools/benchmark/benchmark_utils.h13
-rw-r--r--tools/benchmark/benchmark_xl.cc444
-rw-r--r--tools/box/CMakeLists.txt28
-rw-r--r--tools/box/box.cc285
-rw-r--r--tools/box/box.h113
-rw-r--r--tools/box/box_list_main.cc90
-rw-r--r--tools/box/box_test.cc76
-rwxr-xr-xtools/build_cleaner.py317
-rw-r--r--tools/butteraugli_main.cc89
-rw-r--r--tools/cjpeg_hdr.cc306
-rw-r--r--tools/cjpegli.cc270
-rw-r--r--tools/cjxl_fuzzer.cc13
-rw-r--r--tools/cjxl_main.cc1645
-rw-r--r--tools/cmdline.cc25
-rw-r--r--tools/cmdline.h58
-rw-r--r--tools/codec_config.h1
-rw-r--r--tools/color_encoding_fuzzer.cc10
-rw-r--r--tools/comparison_viewer/CMakeLists.txt20
-rw-r--r--tools/comparison_viewer/codec_comparison_window.cc6
-rw-r--r--tools/comparison_viewer/codec_comparison_window.h12
-rw-r--r--tools/comparison_viewer/codec_comparison_window.ui4
-rw-r--r--tools/comparison_viewer/compare_codecs.cc2
-rw-r--r--tools/comparison_viewer/compare_images.cc17
-rw-r--r--tools/comparison_viewer/image_loading.cc72
-rw-r--r--tools/comparison_viewer/image_loading.h10
-rw-r--r--tools/comparison_viewer/settings.cc6
-rw-r--r--tools/comparison_viewer/settings.h6
-rw-r--r--tools/comparison_viewer/split_image_renderer.cc47
-rw-r--r--tools/comparison_viewer/split_image_renderer.h6
-rw-r--r--tools/comparison_viewer/split_image_view.cc6
-rw-r--r--tools/comparison_viewer/split_image_view.h6
-rw-r--r--tools/comparison_viewer/split_image_view.ui4
-rwxr-xr-xtools/conformance/conformance.py62
-rw-r--r--tools/conformance/lcms2.py6
-rw-r--r--tools/decode_and_encode.cc30
-rw-r--r--tools/decode_basic_info_fuzzer.cc18
-rw-r--r--tools/djpegli.cc197
-rw-r--r--tools/djxl_fuzzer.cc41
-rw-r--r--tools/djxl_fuzzer_corpus.cc (renamed from tools/fuzzer_corpus.cc)79
-rw-r--r--tools/djxl_fuzzer_test.cc11
-rw-r--r--tools/djxl_main.cc425
-rw-r--r--tools/fast_lossless/.gitignore1
-rw-r--r--tools/fast_lossless/README.md10
-rwxr-xr-xtools/fast_lossless/build-android.sh27
-rwxr-xr-xtools/fast_lossless/build.sh27
-rwxr-xr-xtools/fast_lossless/cross_compile_aarch64.sh26
-rw-r--r--tools/fast_lossless/fast_lossless_main.cc113
-rw-r--r--tools/fast_lossless/pam-input.h292
-rw-r--r--tools/fields_fuzzer.cc28
-rw-r--r--tools/file_io.cc75
-rw-r--r--tools/file_io.h134
-rw-r--r--tools/flicker_test/CMakeLists.txt8
-rw-r--r--tools/flicker_test/main.cc5
-rw-r--r--tools/flicker_test/parameters.cc6
-rw-r--r--tools/flicker_test/parameters.h6
-rw-r--r--tools/flicker_test/setup.cc6
-rw-r--r--tools/flicker_test/setup.h6
-rw-r--r--tools/flicker_test/setup.ui7
-rw-r--r--tools/flicker_test/split_view.cc29
-rw-r--r--tools/flicker_test/split_view.h8
-rw-r--r--tools/flicker_test/test_window.cc6
-rw-r--r--tools/flicker_test/test_window.h6
-rw-r--r--tools/flicker_test/test_window.ui4
-rw-r--r--tools/fuzzer_stub.cc6
-rw-r--r--tools/hdr/README.md16
-rw-r--r--tools/hdr/display_to_hlg.cc25
-rw-r--r--tools/hdr/exr_to_pq.cc158
-rw-r--r--tools/hdr/generate_lut_template.cc11
-rw-r--r--tools/hdr/image_utils.h35
-rw-r--r--tools/hdr/local_tone_map.cc541
-rw-r--r--tools/hdr/pq_to_hlg.cc23
-rw-r--r--tools/hdr/render_hlg.cc23
-rw-r--r--tools/hdr/texture_to_cube.cc11
-rw-r--r--tools/hdr/tone_map.cc28
-rw-r--r--tools/icc_codec_fuzzer.cc30
-rw-r--r--tools/icc_detect/icc_detect.h6
-rw-r--r--tools/icc_detect/icc_detect_empty.cc6
-rw-r--r--tools/icc_detect/icc_detect_win32.cc6
-rw-r--r--tools/icc_detect/icc_detect_x11.cc27
-rw-r--r--tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java9
-rw-r--r--tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc97
-rw-r--r--tools/jpegli_dec_fuzzer.cc212
-rw-r--r--tools/jpegli_dec_fuzzer_corpus.cc365
-rw-r--r--tools/jxl_from_tree.cc77
-rw-r--r--tools/jxlinfo.c45
-rw-r--r--tools/libjxl_test.c4
-rwxr-xr-xtools/optimizer/apply_simplex.py111
-rwxr-xr-xtools/optimizer/simplex_fork.py33
-rwxr-xr-xtools/optimizer/update_jpegli_global_scale.py103
-rw-r--r--tools/rans_fuzzer.cc22
-rwxr-xr-xtools/scripts/bisector (renamed from tools/bisector)6
-rwxr-xr-xtools/scripts/build_cleaner.py270
-rwxr-xr-xtools/scripts/build_stats.py (renamed from tools/build_stats.py)38
-rwxr-xr-xtools/scripts/check_author.py (renamed from tools/check_author.py)4
-rwxr-xr-xtools/scripts/cjxl_bisect_bpp (renamed from tools/cjxl_bisect_bpp)5
-rwxr-xr-xtools/scripts/cjxl_bisect_size (renamed from tools/cjxl_bisect_size)5
-rwxr-xr-xtools/scripts/demo_progressive_saliency_encoding.py (renamed from tools/demo_progressive_saliency_encoding.py)0
-rw-r--r--tools/scripts/jpegli_tools_test.sh287
-rwxr-xr-xtools/scripts/jxl-eval.sh124
-rwxr-xr-xtools/scripts/ossfuzz-build.sh (renamed from tools/ossfuzz-build.sh)1
-rw-r--r--tools/scripts/progressive_saliency.conf (renamed from tools/progressive_saliency.conf)0
-rwxr-xr-xtools/scripts/progressive_sizes.sh (renamed from tools/progressive_sizes.sh)4
-rwxr-xr-xtools/scripts/reference_zip.sh (renamed from tools/reference_zip.sh)0
-rw-r--r--tools/scripts/roundtrip_test.sh (renamed from tools/roundtrip_test.sh)65
-rw-r--r--tools/scripts/test_cost-arm64-lowprecision.zipbin0 -> 68578 bytes
-rw-r--r--tools/scripts/test_cost-arm64.zipbin0 -> 68013 bytes
-rw-r--r--tools/scripts/test_cost-armhf.zipbin0 -> 68129 bytes
-rw-r--r--tools/scripts/test_cost-i386.zipbin0 -> 73964 bytes
-rw-r--r--tools/scripts/transform_sources_list.py76
-rw-r--r--tools/set_from_bytes_fuzzer.cc22
-rw-r--r--tools/speed_stats.cc37
-rw-r--r--tools/ssimulacra2.cc492
-rw-r--r--tools/ssimulacra2.h32
-rw-r--r--tools/ssimulacra2_main.cc83
-rw-r--r--tools/ssimulacra_main.cc39
-rw-r--r--tools/thread_pool_internal.h47
-rw-r--r--tools/transforms_fuzzer.cc51
-rw-r--r--tools/upscaling_coefficients/upscaler_demo.py4
-rw-r--r--tools/viewer/CMakeLists.txt8
-rw-r--r--tools/viewer/load_jxl.cc51
-rw-r--r--tools/viewer/load_jxl.h6
-rw-r--r--tools/viewer/main.cc2
-rw-r--r--tools/viewer/viewer_window.cc8
-rw-r--r--tools/viewer/viewer_window.h6
-rw-r--r--tools/wasm_demo/CMakeLists.txt64
-rw-r--r--tools/wasm_demo/README.md126
-rw-r--r--tools/wasm_demo/build_site.py145
-rw-r--r--tools/wasm_demo/client_worker.js99
-rw-r--r--tools/wasm_demo/jxl_decoder.cc (renamed from tools/jxl_emcc.cc)121
-rw-r--r--tools/wasm_demo/jxl_decoder.h48
-rw-r--r--tools/wasm_demo/jxl_decoder_test.js140
-rw-r--r--tools/wasm_demo/jxl_decompressor.cc117
-rw-r--r--tools/wasm_demo/jxl_decompressor.h34
-rw-r--r--tools/wasm_demo/manual_decode_demo.html340
-rw-r--r--tools/wasm_demo/netlify.toml19
-rw-r--r--tools/wasm_demo/netlify/edge-functions/precompressed.ts87
-rw-r--r--tools/wasm_demo/no_png.cc220
-rw-r--r--tools/wasm_demo/no_png.h24
-rw-r--r--tools/wasm_demo/one_line_demo.html20
-rw-r--r--tools/wasm_demo/one_line_demo_with_console.html34
-rw-r--r--tools/wasm_demo/service_worker.js317
-rw-r--r--tools/xyb_range.cc19
167 files changed, 9419 insertions, 3911 deletions
diff --git a/tools/BUILD b/tools/BUILD
new file mode 100644
index 0000000..58e0e9a
--- /dev/null
+++ b/tools/BUILD
@@ -0,0 +1 @@
+package(default_visibility = ["//:__subpackages__"])
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 934ed89..7701e4d 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -6,39 +6,39 @@
# ICC detection library used by the comparison and viewer tools.
if(JPEGXL_ENABLE_VIEWERS)
if(WIN32)
- find_package(Qt5 QUIET COMPONENTS Widgets)
- if (NOT Qt5_FOUND)
- message(WARNING "Qt5 was not found.")
+ find_package(Qt6 QUIET COMPONENTS Widgets)
+ if (NOT Qt6_FOUND)
+ message(WARNING "Qt6 was not found.")
else()
add_library(icc_detect STATIC EXCLUDE_FROM_ALL
icc_detect/icc_detect_win32.cc
icc_detect/icc_detect.h
)
target_include_directories(icc_detect PRIVATE "${PROJECT_SOURCE_DIR}")
- target_link_libraries(icc_detect PUBLIC Qt5::Widgets)
+ target_link_libraries(icc_detect PUBLIC Qt6::Widgets)
if(JPEGXL_DEP_LICENSE_DIR)
- configure_file("${JPEGXL_DEP_LICENSE_DIR}/libqt5widgets5/copyright"
- ${PROJECT_BINARY_DIR}/LICENSE.libqt5widgets5 COPYONLY)
+ configure_file("${JPEGXL_DEP_LICENSE_DIR}/libqt6widgets6/copyright"
+ ${PROJECT_BINARY_DIR}/LICENSE.libqt6widgets6 COPYONLY)
endif() # JPEGXL_DEP_LICENSE_DIR
endif()
elseif(APPLE)
- find_package(Qt5 QUIET COMPONENTS Widgets)
- if (Qt5_FOUND)
+ find_package(Qt6 QUIET COMPONENTS Widgets)
+ if (Qt6_FOUND)
add_library(icc_detect STATIC EXCLUDE_FROM_ALL
icc_detect/icc_detect_empty.cc
icc_detect/icc_detect.h
)
target_include_directories(icc_detect PRIVATE "${PROJECT_SOURCE_DIR}")
- target_link_libraries(icc_detect PUBLIC Qt5::Widgets)
+ target_link_libraries(icc_detect PUBLIC Qt6::Widgets)
else()
- message(WARNING "APPLE: Qt5 was not found.")
+ message(WARNING "APPLE: Qt6 was not found.")
endif()
else()
- find_package(Qt5 QUIET COMPONENTS Widgets X11Extras)
+ find_package(Qt6 QUIET COMPONENTS Widgets)
find_package(ECM QUIET NO_MODULE)
- if (NOT Qt5_FOUND OR NOT ECM_FOUND)
- if (NOT Qt5_FOUND)
- message(WARNING "Qt5 was not found.")
+ if (NOT Qt6_FOUND OR NOT ECM_FOUND)
+ if (NOT Qt6_FOUND)
+ message(WARNING "Qt6 was not found.")
else()
message(WARNING "extra-cmake-modules were not found.")
endif()
@@ -50,7 +50,7 @@ else()
icc_detect/icc_detect_x11.cc
icc_detect/icc_detect.h
)
- target_link_libraries(icc_detect PUBLIC jxl-static Qt5::Widgets Qt5::X11Extras XCB::XCB)
+ target_link_libraries(icc_detect PUBLIC jxl-internal Qt6::Widgets XCB::XCB)
endif ()
endif()
endif()
@@ -60,17 +60,18 @@ endif() # JPEGXL_ENABLE_VIEWERS
set(TOOL_BINARIES)
# Tools that depend on jxl internal functions.
set(INTERNAL_TOOL_BINARIES)
+set(FUZZER_CORPUS_BINARIES)
add_library(jxl_tool STATIC EXCLUDE_FROM_ALL
cmdline.cc
codec_config.cc
speed_stats.cc
- file_io.cc
tool_version.cc
+ ${JXL_CMS_OBJECTS}
)
target_compile_options(jxl_tool PUBLIC "${JPEGXL_INTERNAL_FLAGS}")
target_include_directories(jxl_tool PUBLIC "${PROJECT_SOURCE_DIR}")
-target_link_libraries(jxl_tool hwy)
+target_link_libraries(jxl_tool PUBLIC hwy)
# The JPEGXL_VERSION is set from the builders.
if(NOT DEFINED JPEGXL_VERSION OR JPEGXL_VERSION STREQUAL "")
@@ -125,7 +126,7 @@ if(JPEGXL_ENABLE_TOOLS)
add_executable(cjxl cjxl_main.cc)
target_link_libraries(cjxl
jxl
- jxl_extras_codec-static
+ jxl_extras_codec
jxl_threads
jxl_tool
)
@@ -135,14 +136,19 @@ if(JPEGXL_ENABLE_TOOLS)
add_executable(djxl djxl_main.cc)
target_link_libraries(djxl
jxl
- jxl_extras_codec-static
+ jxl_extras_codec
jxl_threads
jxl_tool
)
list(APPEND TOOL_BINARIES djxl)
- add_executable(cjpeg_hdr cjpeg_hdr.cc)
- list(APPEND INTERNAL_TOOL_BINARIES cjpeg_hdr)
+ if(JPEGXL_ENABLE_JPEGLI)
+ # Depends on parts of jxl_extras that are only built if libjpeg is found and
+ # jpegli is enabled.
+ add_executable(cjpegli cjpegli.cc)
+ add_executable(djpegli djpegli.cc)
+ list(APPEND INTERNAL_TOOL_BINARIES cjpegli djpegli)
+ endif()
add_executable(jxlinfo jxlinfo.c)
target_link_libraries(jxlinfo jxl)
@@ -160,33 +166,53 @@ endif() # JPEGXL_ENABLE_TOOLS
# Other developer tools.
if(JPEGXL_ENABLE_DEVTOOLS)
list(APPEND INTERNAL_TOOL_BINARIES
- fuzzer_corpus
butteraugli_main
decode_and_encode
display_to_hlg
+ exr_to_pq
pq_to_hlg
render_hlg
+ local_tone_map
tone_map
texture_to_cube
generate_lut_template
ssimulacra_main
+ ssimulacra2
xyb_range
jxl_from_tree
)
- add_executable(fuzzer_corpus fuzzer_corpus.cc)
-
add_executable(ssimulacra_main ssimulacra_main.cc ssimulacra.cc)
+ add_executable(ssimulacra2 ssimulacra2_main.cc ssimulacra2.cc)
add_executable(butteraugli_main butteraugli_main.cc)
add_executable(decode_and_encode decode_and_encode.cc)
add_executable(display_to_hlg hdr/display_to_hlg.cc)
+ add_executable(exr_to_pq hdr/exr_to_pq.cc)
add_executable(pq_to_hlg hdr/pq_to_hlg.cc)
add_executable(render_hlg hdr/render_hlg.cc)
+ add_executable(local_tone_map hdr/local_tone_map.cc)
add_executable(tone_map hdr/tone_map.cc)
add_executable(texture_to_cube hdr/texture_to_cube.cc)
add_executable(generate_lut_template hdr/generate_lut_template.cc)
add_executable(xyb_range xyb_range.cc)
add_executable(jxl_from_tree jxl_from_tree.cc)
+
+ list(APPEND FUZZER_CORPUS_BINARIES djxl_fuzzer_corpus)
+ add_executable(djxl_fuzzer_corpus djxl_fuzzer_corpus.cc)
+ target_link_libraries(djxl_fuzzer_corpus
+ jxl_extras-internal
+ jxl_testlib-internal
+ jxl_tool
+ )
+ if(JPEGXL_ENABLE_JPEGLI)
+ list(APPEND FUZZER_CORPUS_BINARIES jpegli_dec_fuzzer_corpus)
+ add_executable(jpegli_dec_fuzzer_corpus jpegli_dec_fuzzer_corpus.cc)
+ target_link_libraries(jpegli_dec_fuzzer_corpus
+ jpegli-static
+ jxl_tool
+ jxl_threads
+ )
+ endif()
endif() # JPEGXL_ENABLE_DEVTOOLS
# Benchmark tools.
@@ -205,8 +231,11 @@ if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS)
benchmark/benchmark_utils.h
benchmark/benchmark_codec_custom.cc
benchmark/benchmark_codec_custom.h
+ benchmark/benchmark_codec_jpeg.cc
+ benchmark/benchmark_codec_jpeg.h
benchmark/benchmark_codec_jxl.cc
benchmark/benchmark_codec_jxl.h
+ ssimulacra2.cc
../third_party/dirent.cc
)
target_link_libraries(benchmark_xl Threads::Threads)
@@ -215,14 +244,6 @@ if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS)
target_compile_definitions(benchmark_xl PRIVATE "-DHAS_GLOB=0")
endif() # MINGW
- find_package(JPEG)
- if(JPEG_FOUND)
- target_sources(benchmark_xl PRIVATE
- "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_jpeg.cc"
- "${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_jpeg.h"
- )
- endif ()
-
if(NOT JPEGXL_BUNDLE_LIBPNG)
find_package(PNG)
endif()
@@ -231,6 +252,7 @@ if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS)
"${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_png.cc"
"${CMAKE_CURRENT_LIST_DIR}/benchmark/benchmark_codec_png.h"
)
+ target_compile_definitions(benchmark_xl PRIVATE -DBENCHMARK_PNG)
endif()
find_package(PkgConfig)
@@ -245,11 +267,16 @@ if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS)
# Use the static version of webp if available.
find_library(WebP_STATIC_LINK_LIBRARY NAMES libwebp.a
PATHS "${WebP_LIBDIR}")
+ find_library(SharpYuv_STATIC_LINK_LIBRARY NAMES libsharpyuv.a
+ PATHS "${WebP_LIBDIR}")
if(NOT WebP_STATIC_LINK_LIBRARY)
message(WARNING "Using dynamic libwebp")
target_link_libraries(benchmark_xl PkgConfig::WebP)
else()
target_link_libraries(benchmark_xl "${WebP_STATIC_LINK_LIBRARY}")
+ if(SharpYuv_STATIC_LINK_LIBRARY)
+ target_link_libraries(benchmark_xl "${SharpYuv_STATIC_LINK_LIBRARY}")
+ endif()
target_include_directories(benchmark_xl
PRIVATE ${WebP_STATIC_INCLUDE_DIRS})
target_compile_options(benchmark_xl PRIVATE ${WebP_STATIC_CFLAGS_OTHER})
@@ -270,33 +297,46 @@ endif() # JPEGXL_ENABLE_BENCHMARK
# All tool binaries depend on "jxl" library and the tool helpers.
foreach(BINARY IN LISTS INTERNAL_TOOL_BINARIES)
target_link_libraries("${BINARY}"
- jxl_extras-static
+ jxl_extras-internal
+ jxl_threads
jxl_tool
)
endforeach()
-list(APPEND TOOL_BINARIES ${INTERNAL_TOOL_BINARIES})
+list(APPEND TOOL_BINARIES ${INTERNAL_TOOL_BINARIES} ${FUZZER_CORPUS_BINARIES})
foreach(BINARY IN LISTS TOOL_BINARIES)
- if(JPEGXL_EMSCRIPTEN)
- set_target_properties(${BINARY} PROPERTIES LINK_FLAGS "-s USE_LIBPNG=1")
+ if(EMSCRIPTEN)
+ set(JXL_WASM_TOOLS_LINK_FLAGS "\
+ -s USE_LIBPNG=1 \
+ -s ALLOW_MEMORY_GROWTH=1 \
+ -s USE_PTHREADS=1 \
+ -s PTHREAD_POOL_SIZE=16 \
+ ")
+ set_target_properties(${BINARY} PROPERTIES LINK_FLAGS "${JXL_WASM_TOOLS_LINK_FLAGS}")
endif()
endforeach()
install(TARGETS ${TOOL_BINARIES} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
message(STATUS "Building tools: ${TOOL_BINARIES}")
-set(FUZZER_BINARIES
- color_encoding_fuzzer
- decode_basic_info_fuzzer
- cjxl_fuzzer
- djxl_fuzzer
- icc_codec_fuzzer
- fields_fuzzer
- rans_fuzzer
- set_from_bytes_fuzzer
- transforms_fuzzer
-)
+# djxl_fuzzer builds even when not JPEGXL_ENABLE_TOOLS
+set(FUZZER_BINARIES djxl_fuzzer)
+if(JPEGXL_ENABLE_TOOLS)
+ list(APPEND FUZZER_BINARIES
+ color_encoding_fuzzer
+ decode_basic_info_fuzzer
+ cjxl_fuzzer
+ icc_codec_fuzzer
+ fields_fuzzer
+ rans_fuzzer
+ set_from_bytes_fuzzer
+ transforms_fuzzer
+ )
+if(JPEGXL_ENABLE_JPEGLI)
+ list(APPEND FUZZER_BINARIES jpegli_dec_fuzzer)
+endif()
+endif()
# Fuzzers.
foreach(FUZZER IN LISTS FUZZER_BINARIES)
@@ -314,12 +354,15 @@ foreach(FUZZER IN LISTS FUZZER_BINARIES)
target_include_directories("${BINARY}" PRIVATE "${CMAKE_SOURCE_DIR}")
if(FUZZER STREQUAL djxl_fuzzer)
target_link_libraries("${BINARY}"
- jxl_dec-static
- jxl_threads-static
+ jxl_dec-internal
+ jxl_threads
)
+ elseif(FUZZER STREQUAL jpegli_dec_fuzzer)
+ target_link_libraries("${BINARY}" jpegli-static)
else()
target_link_libraries("${BINARY}"
- jxl_extras-static
+ jxl_extras_nocodec-internal
+ jxl_testlib-internal
jxl_tool
)
endif()
@@ -370,37 +413,8 @@ add_subdirectory(comparison_viewer)
add_subdirectory(flicker_test)
endif()
-add_subdirectory(box)
add_subdirectory(conformance)
-
-
-if (JPEGXL_ENABLE_TOOLS AND JPEGXL_EMSCRIPTEN)
-# WASM API facade.
-add_executable(jxl_emcc jxl_emcc.cc)
-target_link_libraries(jxl_emcc
- jxl_extras-static
-)
-set_target_properties(jxl_emcc PROPERTIES LINK_FLAGS "\
- -O3\
- --closure 1 \
- -s TOTAL_MEMORY=75mb \
- -s USE_LIBPNG=1 \
- -s DISABLE_EXCEPTION_CATCHING=1 \
- -s MODULARIZE=1 \
- -s FILESYSTEM=0 \
- -s USE_PTHREADS=1 \
- -s PTHREAD_POOL_SIZE=4 \
- -s EXPORT_NAME=\"JxlCodecModule\"\
- -s \"EXPORTED_FUNCTIONS=[\
- _malloc,\
- _free,\
- _jxlCreateInstance,\
- _jxlDestroyInstance,\
- _jxlFlush,\
- _jxlProcessInput\
- ]\"\
-")
-endif () # JPEGXL_ENABLE_TOOLS AND JPEGXL_EMSCRIPTEN
+add_subdirectory(wasm_demo)
if(JPEGXL_ENABLE_JNI)
find_package(JNI QUIET)
@@ -412,7 +426,7 @@ if (JNI_FOUND AND Java_FOUND)
# decoder_jni_onload.cc might be necessary for Android; not used yet.
add_library(jxl_jni SHARED jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc)
target_include_directories(jxl_jni PRIVATE "${JNI_INCLUDE_DIRS}" "${PROJECT_SOURCE_DIR}")
- target_link_libraries(jxl_jni PUBLIC jxl_dec-static jxl_threads-static)
+ target_link_libraries(jxl_jni PUBLIC jxl_dec-internal jxl_threads)
if(NOT DEFINED JPEGXL_INSTALL_JNIDIR)
set(JPEGXL_INSTALL_JNIDIR ${CMAKE_INSTALL_LIBDIR})
endif()
@@ -455,15 +469,21 @@ endif() # JNI_FOUND & Java_FOUND
endif() # JPEGXL_ENABLE_JNI
# End-to-end tests for the tools
-if(BUILD_TESTING AND JPEGXL_ENABLE_TOOLS AND JPEGXL_ENABLE_DEVTOOLS AND JPEGXL_ENABLE_TRANSCODE_JPEG AND (NOT JPEGXL_ENABLE_JNI))
+if(JPEGXL_TEST_TOOLS)
find_program (BASH_PROGRAM bash)
-if(BASH_PROGRAM AND $<TARGET_EXISTS:cjxl> AND $<TARGET_EXISTS:djxl> AND $<TARGET_EXISTS:ssimulacra_main>)
- add_test(
- NAME roundtrip_test
- COMMAND ${BASH_PROGRAM} ${CMAKE_CURRENT_SOURCE_DIR}/roundtrip_test.sh
- ${CMAKE_BINARY_DIR})
- if (CMAKE_CROSSCOMPILING_EMULATOR)
- set_tests_properties(roundtrip_test PROPERTIES ENVIRONMENT "EMULATOR=${CMAKE_CROSSCOMPILING_EMULATOR}")
+if (BASH_PROGRAM)
+ set(TEST_SCRIPTS)
+ find_package(JPEG)
+ if (JPEG_FOUND AND JPEGXL_ENABLE_TRANSCODE_JPEG)
+ list(APPEND TEST_SCRIPTS roundtrip_test)
endif()
-endif()
-endif() # BUILD_TESTING
+ if (JPEG_FOUND AND JPEGXL_ENABLE_JPEGLI)
+ list(APPEND TEST_SCRIPTS jpegli_tools_test)
+ endif()
+ foreach(SCRIPT IN LISTS TEST_SCRIPTS)
+ add_test(NAME ${SCRIPT}
+ COMMAND ${BASH_PROGRAM} ${CMAKE_CURRENT_SOURCE_DIR}/scripts/${SCRIPT}.sh
+ ${CMAKE_BINARY_DIR})
+ endforeach()
+endif() # BASH_PROGRAM
+endif() # JPEGXL_TEST_TOOLS
diff --git a/tools/README.cjpeg_hdr.md b/tools/README.cjpeg_hdr.md
deleted file mode 100644
index bd7c793..0000000
--- a/tools/README.cjpeg_hdr.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# High bit depth JPEG encoder
-`cjpeg_hdr` is an (experimental) JPEG encoder that can preserve a higher bit
-depth than a traditional JPEG encoder. In particular, it may be used to produce
-HDR JPEGs that do not show obvious signs of banding.
-
-Note that at this point in time `cjpeg_hdr` does not attempt to actually
-*compress* the image - it behaves in the same way as a "quality 100" JPEG
-encoder would normally do, i.e. no quantization, to achieve the maximum
-possible visual quality. Moreover, no Huffman optimization is performed.
-
-## Generating HBD JPEGs
-Note: this and the following sections assume that `libjxl` has been built in
-the `build/` directory, either by using CMake or by running `./ci.sh opt`.
-
-It should be sufficient to run `build/tools/cjpeg_hdr input_image output.jpg`.
-Various input formats are supported, including NetBPM and (8- or 16-bit) PNG.
-
-If the PNG image includes a colour profile, it will be copied in the resulting
-JPEG image. If this colour profile approximates the PQ or HLG transfer curves,
-some applications will consider the resulting image to be HDR.
-
-To attach a PQ profile to an image without a colour profile (or with a
-different colour profile), the following command can be used:
-
-```
- build/tools/decode_and_encode input RGB_D65_202_Rel_PeQ output_with_pq.png 16
-```
-
-Similarly, to attach an HLG profile, the following command can be used
-
-```
- build/tools/decode_and_encode input RGB_D65_202_Rel_HLG output_with_pq.png 16
-```
-
-## Decoding HBD JPEGs
-HBD JPEGs are fully retrocompatible with libjpeg, and any JPEG viewer ought to
-be able to visualize them. Nonetheless, to achieve the best visual quality, a
-high bit depth decoder should be used.
-
-Such a decoder does not exist today. As a workaround, it is possible to do a
-lossless conversion to JPEG XL and then view the resulting image:
-
-```
- build/tools/cjxl --jpeg_transcode_disable_cfl hbd.jpeg hbd.jxl
-```
-
-The resulting JPEG XL file can be visualized, for example, in a browser,
-assuming that the corresponding flag is enabled in the settings.
-
-In particular, if the HBD JPEG has a PQ or HLG profile attached and the current
-display is an HDR display, Chrome ought to visualize the image as HDR content.
-
-It is also possible to convert the JPEG XL file back to a 16-bit PNG:
-
-```
- build/tools/djxl hbd.jxl --bits_per_sample=16 output.png
-```
-
-Note however that as of today (2 Nov 2021) Chrome does not interpret such a PNG
-as an HDR image, even if a PQ or HLG profile is attached. Thus, to display the
-HDR image correctly it is recommended to either display the JPEG XL image
-directly or to convert the PNG to a format that Chrome interprets as HDR, such
-as AVIF. This can be done with the following command for a PQ image:
-
-```
- avifenc -l -y 444 --depth 10 --cicp 9/16/9 image.png output.avif
-```
-
-and the following one for an HLG image:
-
-```
- avifenc -l -y 444 --depth 10 --cicp 9/18/9 image.png output.avif
-```
diff --git a/tools/args.h b/tools/args.h
index 7d04ce3..e34b75e 100644
--- a/tools/args.h
+++ b/tools/args.h
@@ -14,14 +14,12 @@
#include <string.h>
#include <string>
-#include <vector>
+#include <utility>
#include "lib/extras/dec/color_hints.h"
#include "lib/jxl/base/override.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h" // DecoderHints
-#include "lib/jxl/gaborish.h"
-#include "lib/jxl/modular/options.h"
+#include "tools/file_io.h"
namespace jpegxl {
namespace tools {
@@ -54,8 +52,8 @@ static inline bool ParseFloatPair(const char* arg,
return true;
}
-static inline bool ParseAndAppendKeyValue(const char* arg,
- jxl::extras::ColorHints* out) {
+template <typename Callback>
+static inline bool ParseAndAppendKeyValue(const char* arg, Callback* cb) {
const char* eq = strchr(arg, '=');
if (!eq) {
fprintf(stderr, "Expected argument as 'key=value' but received '%s'\n",
@@ -63,26 +61,7 @@ static inline bool ParseAndAppendKeyValue(const char* arg,
return false;
}
std::string key(arg, eq);
- out->Add(key, std::string(eq + 1));
- return true;
-}
-
-static inline bool ParsePredictor(const char* arg, jxl::Predictor* out) {
- char* end;
- uint64_t p = static_cast<uint64_t>(strtoull(arg, &end, 0));
- if (end[0] != '\0') {
- fprintf(stderr, "Invalid predictor: %s.\n", arg);
- return JXL_FAILURE("Args");
- }
- if (p >= jxl::kNumModularEncoderPredictors) {
- fprintf(stderr,
- "Invalid predictor value %" PRIu64 ", must be less than %" PRIu64
- ".\n",
- p, static_cast<uint64_t>(jxl::kNumModularEncoderPredictors));
- return JXL_FAILURE("Args");
- }
- *out = static_cast<jxl::Predictor>(p);
- return true;
+ return (*cb)(key, std::string(eq + 1));
}
static inline bool ParseCString(const char* arg, const char** out) {
@@ -95,6 +74,28 @@ static inline bool IncrementUnsigned(size_t* out) {
return true;
}
+struct ColorHintsProxy {
+ jxl::extras::ColorHints target;
+ bool operator()(const std::string& key, const std::string& value) {
+ if (key == "icc_pathname") {
+ std::vector<uint8_t> icc;
+ JXL_RETURN_IF_ERROR(ReadFile(value, &icc));
+ const char* data = reinterpret_cast<const char*>(icc.data());
+ target.Add("icc", std::string(data, data + icc.size()));
+ } else if (key == "exif" || key == "xmp" || key == "jumbf") {
+ std::vector<uint8_t> metadata;
+ JXL_RETURN_IF_ERROR(ReadFile(value, &metadata));
+ const char* data = reinterpret_cast<const char*>(metadata.data());
+ target.Add(key, std::string(data, data + metadata.size()));
+ } else if (key == "strip") {
+ target.Add(value, "");
+ } else {
+ target.Add(key, value);
+ }
+ return true;
+ }
+};
+
} // namespace tools
} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_args.cc b/tools/benchmark/benchmark_args.cc
index 2bd3eb8..cc2504a 100644
--- a/tools/benchmark/benchmark_args.cc
+++ b/tools/benchmark/benchmark_args.cc
@@ -16,12 +16,13 @@
#include "lib/extras/dec/color_description.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/color_encoding_internal.h"
-#include "lib/jxl/color_management.h"
-#include "tools/benchmark/benchmark_codec_jpeg.h" // for AddCommand..
+#include "tools/benchmark/benchmark_codec_custom.h" // for AddCommand..
+#include "tools/benchmark/benchmark_codec_jpeg.h" // for AddCommand..
#include "tools/benchmark/benchmark_codec_jxl.h"
-#if JPEGXL_ENABLE_APNG
+
+#ifdef BENCHMARK_PNG
#include "tools/benchmark/benchmark_codec_png.h"
-#endif
+#endif // BENCHMARK_PNG
#ifdef BENCHMARK_WEBP
#include "tools/benchmark/benchmark_codec_webp.h"
@@ -31,7 +32,8 @@
#include "tools/benchmark/benchmark_codec_avif.h"
#endif // BENCHMARK_AVIF
-namespace jxl {
+namespace jpegxl {
+namespace tools {
std::vector<std::string> SplitString(const std::string& s, char c) {
std::vector<std::string> result;
@@ -128,6 +130,7 @@ Status BenchmarkArgs::AddCommandLineOptions() {
AddDouble(&mul_output, "mul_output",
"If nonzero, multiplies linear sRGB by this and clamps to 255",
0.0);
+ AddFlag(&save_heatmap, "save_heatmap", "Saves the heatmap images.", true);
AddDouble(&heatmap_good, "heatmap_good",
"If greater than zero, use this as the good "
"threshold for creating heatmap images.",
@@ -143,6 +146,11 @@ Status BenchmarkArgs::AddCommandLineOptions() {
"Base64-encode the images in the HTML report rather than use "
"external file names. May cause very large HTML data size.",
false);
+ AddFlag(&html_report_use_decompressed, "html_report_use_decompressed",
+ "Show the compressed image as decompressed to --output_extension.",
+ true);
+ AddFlag(&html_report_add_heatmap, "html_report_add_heatmap",
+ "Add heatmaps to the image comparisons.", false);
AddFlag(
&markdown, "markdown",
@@ -186,13 +194,6 @@ Status BenchmarkArgs::AddCommandLineOptions() {
AddDouble(&error_pnorm, "error_pnorm",
"smallest p norm for pooling butteraugli values", 3.0);
- AddFloat(&ba_params.hf_asymmetry, "hf_asymmetry",
- "Multiplier for weighting HF artefacts more than features "
- "being smoothed out. 1.0 means no HF asymmetry. 0.3 is "
- "a good value to start exploring for asymmetry.",
- 0.8f);
- AddFlag(&profiler, "profiler", "If true, print profiler results.", false);
-
AddFlag(&show_progress, "show_progress",
"Show activity dots per completed file during benchmark.", false);
@@ -210,13 +211,13 @@ Status BenchmarkArgs::AddCommandLineOptions() {
"Distance numbers and compression speeds shown in the table are invalid.",
false);
+ if (!AddCommandLineOptionsCustomCodec(this)) return false;
if (!AddCommandLineOptionsJxlCodec(this)) return false;
-#ifdef BENCHMARK_JPEG
if (!AddCommandLineOptionsJPEGCodec(this)) return false;
-#endif // BENCHMARK_JPEG
-#if JPEGXL_ENABLE_APNG
+
+#ifdef BENCHMARK_PNG
if (!AddCommandLineOptionsPNGCodec(this)) return false;
-#endif
+#endif // BENCHMARK_PNG
#ifdef BENCHMARK_WEBP
if (!AddCommandLineOptionsWebPCodec(this)) return false;
#endif // BENCHMARK_WEBP
@@ -228,13 +229,12 @@ Status BenchmarkArgs::AddCommandLineOptions() {
}
Status BenchmarkArgs::ValidateArgs() {
- size_t bits_per_sample = 0; // unused
if (input.empty()) {
fprintf(stderr, "Missing --input filename(s).\n");
return false;
}
- if (extras::CodecFromExtension(output_extension, &bits_per_sample) ==
- extras::Codec::kUnknown) {
+ if (jxl::extras::CodecFromPath(output_extension) ==
+ jxl::extras::Codec::kUnknown) {
JXL_WARNING("Unrecognized output_extension %s, try .png",
output_extension.c_str());
return false; // already warned
@@ -245,14 +245,13 @@ Status BenchmarkArgs::ValidateArgs() {
if (!output_description.empty()) {
// Validate, but also create the profile (only needs to happen once).
JxlColorEncoding output_encoding_external;
- if (!ParseDescription(output_description, &output_encoding_external)) {
+ if (!jxl::ParseDescription(output_description, &output_encoding_external)) {
JXL_WARNING("Unrecognized output_description %s, try RGB_D65_SRG_Rel_Lin",
output_description.c_str());
return false; // already warned
}
- JXL_RETURN_IF_ERROR(jxl::ConvertExternalToInternalColorEncoding(
- output_encoding_external, &output_encoding));
- JXL_RETURN_IF_ERROR(output_encoding.CreateICC());
+ JXL_RETURN_IF_ERROR(output_encoding.FromExternal(output_encoding_external));
+ JXL_RETURN_IF_ERROR(!output_encoding.ICC().empty());
}
JXL_RETURN_IF_ERROR(ValidateArgsJxlCodec(this));
@@ -278,4 +277,5 @@ Status BenchmarkArgs::ValidateArgs() {
return true;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_args.h b/tools/benchmark/benchmark_args.h
index bebc0ac..bdc385c 100644
--- a/tools/benchmark/benchmark_args.h
+++ b/tools/benchmark/benchmark_args.h
@@ -23,7 +23,12 @@
#include "tools/args.h"
#include "tools/cmdline.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::ColorEncoding;
+using ::jxl::Override;
+using ::jxl::Status;
std::vector<std::string> SplitString(const std::string& s, char c);
@@ -108,7 +113,7 @@ struct BenchmarkArgs {
bool silent_errors;
bool save_compressed;
bool save_decompressed;
- std::string output_extension; // see CodecFromExtension
+ std::string output_extension; // see CodecFromPath
std::string output_description; // see ParseDescription
ColorEncoding output_encoding; // determined by output_description
@@ -126,8 +131,11 @@ struct BenchmarkArgs {
double heatmap_good;
double heatmap_bad;
+ bool save_heatmap;
bool write_html_report;
bool html_report_self_contained;
+ bool html_report_use_decompressed;
+ bool html_report_add_heatmap;
bool markdown;
bool more_columns;
@@ -143,9 +151,7 @@ struct BenchmarkArgs {
int num_samples;
int sample_dimensions;
- ButteraugliParams ba_params;
- bool profiler;
double error_pnorm;
bool show_progress;
@@ -169,6 +175,7 @@ struct BenchmarkArgs {
// Returns singleton
BenchmarkArgs* Args();
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_ARGS_H_
diff --git a/tools/benchmark/benchmark_codec.cc b/tools/benchmark/benchmark_codec.cc
index 230665b..c788aef 100644
--- a/tools/benchmark/benchmark_codec.cc
+++ b/tools/benchmark/benchmark_codec.cc
@@ -15,25 +15,23 @@
#include "lib/extras/time.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
-#include "lib/jxl/base/profiler.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/color_encoding_internal.h"
-#include "lib/jxl/color_management.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
#include "lib/jxl/image_ops.h"
#include "tools/benchmark/benchmark_args.h"
#include "tools/benchmark/benchmark_codec_custom.h"
-#ifdef JPEGXL_ENABLE_JPEG
#include "tools/benchmark/benchmark_codec_jpeg.h"
-#endif // JPEG_ENABLE_JPEG
#include "tools/benchmark/benchmark_codec_jxl.h"
-#include "tools/benchmark/benchmark_codec_png.h"
#include "tools/benchmark/benchmark_stats.h"
+#ifdef BENCHMARK_PNG
+#include "tools/benchmark/benchmark_codec_png.h"
+#endif // BENCHMARK_PNG
+
#ifdef BENCHMARK_WEBP
#include "tools/benchmark/benchmark_codec_webp.h"
#endif // BENCHMARK_WEBP
@@ -42,7 +40,10 @@
#include "tools/benchmark/benchmark_codec_avif.h"
#endif // BENCHMARK_AVIF
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::Image3F;
void ImageCodec::ParseParameters(const std::string& parameters) {
params_ = parameters;
@@ -75,26 +76,8 @@ Status ImageCodec::ParseParam(const std::string& param) {
return false;
}
butteraugli_target_ = butteraugli_target;
-
- // full hf asymmetry at high distance
- static const double kHighDistance = 2.5;
-
- // no hf asymmetry at low distance
- static const double kLowDistance = 0.6;
-
- if (butteraugli_target_ >= kHighDistance) {
- ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry;
- } else if (butteraugli_target_ >= kLowDistance) {
- float w =
- (butteraugli_target_ - kLowDistance) / (kHighDistance - kLowDistance);
- ba_params_.hf_asymmetry =
- args_.ba_params.hf_asymmetry * w + 1.0f * (1.0f - w);
- } else {
- ba_params_.hf_asymmetry = 1.0f;
- }
return true;
} else if (param[0] == 'r') {
- ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry;
bitrate_target_ = strtof(param.substr(1).c_str(), nullptr);
return true;
}
@@ -108,10 +91,9 @@ class NoneCodec : public ImageCodec {
Status ParseParam(const std::string& param) override { return true; }
Status Compress(const std::string& filename, const CodecInOut* io,
- ThreadPoolInternal* pool, std::vector<uint8_t>* compressed,
+ ThreadPool* pool, std::vector<uint8_t>* compressed,
jpegxl::tools::SpeedStats* speed_stats) override {
- PROFILER_ZONE("NoneCompress");
- const double start = Now();
+ const double start = jxl::Now();
// Encode image size so we "decompress" something of the same size, as
// required by butteraugli.
const uint32_t xsize = io->xsize();
@@ -119,17 +101,16 @@ class NoneCodec : public ImageCodec {
compressed->resize(8);
memcpy(compressed->data(), &xsize, 4);
memcpy(compressed->data() + 4, &ysize, 4);
- const double end = Now();
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start);
return true;
}
Status Decompress(const std::string& filename,
- const Span<const uint8_t> compressed,
- ThreadPoolInternal* pool, CodecInOut* io,
+ const Span<const uint8_t> compressed, ThreadPool* pool,
+ CodecInOut* io,
jpegxl::tools::SpeedStats* speed_stats) override {
- PROFILER_ZONE("NoneDecompress");
- const double start = Now();
+ const double start = jxl::Now();
JXL_ASSERT(compressed.size() == 8);
uint32_t xsize, ysize;
memcpy(&xsize, compressed.data(), 4);
@@ -139,7 +120,7 @@ class NoneCodec : public ImageCodec {
io->metadata.m.SetFloat32Samples();
io->metadata.m.color_encoding = ColorEncoding::SRGB();
io->SetFromImage(std::move(image), io->metadata.m.color_encoding);
- const double end = Now();
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start);
return true;
}
@@ -162,14 +143,12 @@ ImageCodecPtr CreateImageCodec(const std::string& description) {
} else if (name == "custom") {
result.reset(CreateNewCustomCodec(*Args()));
#endif
-#ifdef JPEGXL_ENABLE_JPEG
} else if (name == "jpeg") {
result.reset(CreateNewJPEGCodec(*Args()));
-#endif // BENCHMARK_JPEG
-#if JPEGXL_ENABLE_APNG
+#ifdef BENCHMARK_PNG
} else if (name == "png") {
result.reset(CreateNewPNGCodec(*Args()));
-#endif
+#endif // BENCHMARK_PNG
} else if (name == "none") {
result.reset(new NoneCodec(*Args()));
#ifdef BENCHMARK_WEBP
@@ -180,7 +159,8 @@ ImageCodecPtr CreateImageCodec(const std::string& description) {
} else if (name == "avif") {
result.reset(CreateNewAvifCodec(*Args()));
#endif // BENCHMARK_AVIF
- } else {
+ }
+ if (!result.get()) {
JXL_ABORT("Unknown image codec: %s", name.c_str());
}
result->set_description(description);
@@ -188,4 +168,5 @@ ImageCodecPtr CreateImageCodec(const std::string& description) {
return result;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_codec.h b/tools/benchmark/benchmark_codec.h
index e554fc2..eb19c35 100644
--- a/tools/benchmark/benchmark_codec.h
+++ b/tools/benchmark/benchmark_codec.h
@@ -12,12 +12,9 @@
#include <string>
#include <vector>
-#include "lib/jxl/aux_out.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/butteraugli/butteraugli.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/image.h"
@@ -26,8 +23,13 @@
#include "tools/benchmark/benchmark_stats.h"
#include "tools/cmdline.h"
#include "tools/speed_stats.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::CodecInOut;
+using ::jxl::Span;
// Thread-compatible.
class ImageCodec {
@@ -43,36 +45,26 @@ class ImageCodec {
void set_description(const std::string& desc) { description_ = desc; }
const std::string& description() const { return description_; }
- const ButteraugliParams& BaParams() const { return ba_params_; }
-
virtual void ParseParameters(const std::string& parameters);
virtual Status ParseParam(const std::string& param);
- // Returns true iff the codec instance (including parameters) can tolerate
- // ImageBundle c_current() != metadata()->color_encoding, and the possibility
- // of negative (out of gamut) pixel values.
- virtual bool IsColorAware() const { return false; }
-
- // Returns true iff the codec instance (including parameters) will operate
- // only with quantized DCT (JPEG) coefficients in input.
- virtual bool IsJpegTranscoder() const { return false; }
-
virtual Status Compress(const std::string& filename, const CodecInOut* io,
- ThreadPoolInternal* pool,
- std::vector<uint8_t>* compressed,
+ ThreadPool* pool, std::vector<uint8_t>* compressed,
jpegxl::tools::SpeedStats* speed_stats) = 0;
virtual Status Decompress(const std::string& filename,
const Span<const uint8_t> compressed,
- ThreadPoolInternal* pool, CodecInOut* io,
+ ThreadPool* pool, CodecInOut* io,
jpegxl::tools::SpeedStats* speed_stats) = 0;
virtual void GetMoreStats(BenchmarkStats* stats) {}
+ virtual bool IgnoreAlpha() const { return false; }
+
virtual Status CanRecompressJpeg() const { return false; }
virtual Status RecompressJpeg(const std::string& filename,
- const std::string& data,
+ const std::vector<uint8_t>& data,
std::vector<uint8_t>* compressed,
jpegxl::tools::SpeedStats* speed_stats) {
return false;
@@ -87,7 +79,6 @@ class ImageCodec {
float butteraugli_target_;
float q_target_;
float bitrate_target_;
- ButteraugliParams ba_params_;
std::string error_message_;
};
@@ -98,6 +89,7 @@ using ImageCodecPtr = std::unique_ptr<ImageCodec>;
// then ParseParameters of the codec gets called with the part behind the colon.
ImageCodecPtr CreateImageCodec(const std::string& description);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_H_
diff --git a/tools/benchmark/benchmark_codec_avif.cc b/tools/benchmark/benchmark_codec_avif.cc
index fbe36b5..0ff1968 100644
--- a/tools/benchmark/benchmark_codec_avif.cc
+++ b/tools/benchmark/benchmark_codec_avif.cc
@@ -5,15 +5,15 @@
#include "tools/benchmark/benchmark_codec_avif.h"
#include <avif/avif.h>
+#include <jxl/cms.h>
#include "lib/extras/time.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/dec_external_image.h"
#include "lib/jxl/enc_external_image.h"
#include "tools/cmdline.h"
+#include "tools/thread_pool_internal.h"
#define JXL_RETURN_IF_AVIF_ERROR(result) \
do { \
@@ -24,10 +24,32 @@
} \
} while (false)
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::Bytes;
+using ::jxl::CodecInOut;
+using ::jxl::IccBytes;
+using ::jxl::ImageBundle;
+using ::jxl::Primaries;
+using ::jxl::Span;
+using ::jxl::ThreadPool;
+using ::jxl::TransferFunction;
+using ::jxl::WhitePoint;
namespace {
+size_t GetNumThreads(ThreadPool* pool) {
+ size_t result = 0;
+ const auto count_threads = [&](const size_t num_threads) {
+ result = num_threads;
+ return true;
+ };
+ const auto no_op = [&](const uint32_t /*task*/, size_t /*thread*/) {};
+ (void)jxl::RunOnPool(pool, 0, 1, count_threads, no_op, "Compress");
+ return result;
+}
+
struct AvifArgs {
avifPixelFormat chroma_subsampling = AVIF_PIXEL_FORMAT_YUV444;
};
@@ -55,13 +77,13 @@ bool ParseChromaSubsampling(const char* arg, avifPixelFormat* subsampling) {
}
void SetUpAvifColor(const ColorEncoding& color, avifImage* const image) {
- bool need_icc = color.white_point != WhitePoint::kD65;
+ bool need_icc = (color.GetWhitePointType() != WhitePoint::kD65);
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT709;
if (!color.HasPrimaries()) {
need_icc = true;
} else {
- switch (color.primaries) {
+ switch (color.GetPrimariesType()) {
case Primaries::kSRGB:
image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709;
break;
@@ -76,7 +98,7 @@ void SetUpAvifColor(const ColorEncoding& color, avifImage* const image) {
}
}
- switch (color.tf.GetTransferFunction()) {
+ switch (color.Tf().GetTransferFunction()) {
case TransferFunction::kSRGB:
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
break;
@@ -102,40 +124,41 @@ void SetUpAvifColor(const ColorEncoding& color, avifImage* const image) {
Status ReadAvifColor(const avifImage* const image, ColorEncoding* const color) {
if (image->icc.size != 0) {
- PaddedBytes icc;
+ IccBytes icc;
icc.assign(image->icc.data, image->icc.data + image->icc.size);
- return color->SetICC(std::move(icc));
+ return color->SetICC(std::move(icc), JxlGetDefaultCms());
}
- color->white_point = WhitePoint::kD65;
+ JXL_RETURN_IF_ERROR(color->SetWhitePointType(WhitePoint::kD65));
switch (image->colorPrimaries) {
case AVIF_COLOR_PRIMARIES_BT709:
- color->primaries = Primaries::kSRGB;
+ JXL_RETURN_IF_ERROR(color->SetPrimariesType(Primaries::kSRGB));
break;
case AVIF_COLOR_PRIMARIES_BT2020:
- color->primaries = Primaries::k2100;
+ JXL_RETURN_IF_ERROR(color->SetPrimariesType(Primaries::k2100));
break;
default:
return JXL_FAILURE("unsupported avif primaries");
}
+ jxl::cms::CustomTransferFunction& tf = color->Tf();
switch (image->transferCharacteristics) {
case AVIF_TRANSFER_CHARACTERISTICS_BT470M:
- JXL_RETURN_IF_ERROR(color->tf.SetGamma(2.2));
+ JXL_RETURN_IF_ERROR(tf.SetGamma(2.2));
break;
case AVIF_TRANSFER_CHARACTERISTICS_BT470BG:
- JXL_RETURN_IF_ERROR(color->tf.SetGamma(2.8));
+ JXL_RETURN_IF_ERROR(tf.SetGamma(2.8));
break;
case AVIF_TRANSFER_CHARACTERISTICS_LINEAR:
- color->tf.SetTransferFunction(TransferFunction::kLinear);
+ tf.SetTransferFunction(TransferFunction::kLinear);
break;
case AVIF_TRANSFER_CHARACTERISTICS_SRGB:
- color->tf.SetTransferFunction(TransferFunction::kSRGB);
+ tf.SetTransferFunction(TransferFunction::kSRGB);
break;
case AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084:
- color->tf.SetTransferFunction(TransferFunction::kPQ);
+ tf.SetTransferFunction(TransferFunction::kPQ);
break;
case AVIF_TRANSFER_CHARACTERISTICS_HLG:
- color->tf.SetTransferFunction(TransferFunction::kHLG);
+ tf.SetTransferFunction(TransferFunction::kHLG);
break;
default:
return JXL_FAILURE("unsupported avif TRC");
@@ -213,10 +236,11 @@ class AvifCodec : public ImageCodec {
}
Status Compress(const std::string& filename, const CodecInOut* io,
- ThreadPoolInternal* pool, std::vector<uint8_t>* compressed,
- jpegxl::tools::SpeedStats* speed_stats) override {
+ ThreadPool* pool, std::vector<uint8_t>* compressed,
+ SpeedStats* speed_stats) override {
double elapsed_convert_image = 0;
- const double start = Now();
+ size_t max_threads = GetNumThreads(pool);
+ const double start = jxl::Now();
{
const auto depth =
std::min<int>(16, io->metadata.m.bit_depth.bits_per_sample);
@@ -229,10 +253,15 @@ class AvifCodec : public ImageCodec {
encoder->tileColsLog2 = log2_cols;
encoder->tileRowsLog2 = log2_rows;
encoder->speed = speed_;
- encoder->maxThreads = pool->NumThreads();
+ encoder->maxThreads = max_threads;
for (const auto& opts : codec_specific_options_) {
- avifEncoderSetCodecSpecificOption(encoder.get(), opts.first.c_str(),
- opts.second.c_str());
+#if AVIF_VERSION_MAJOR >= 1
+ JXL_RETURN_IF_AVIF_ERROR(avifEncoderSetCodecSpecificOption(
+ encoder.get(), opts.first.c_str(), opts.second.c_str()));
+#else
+ (void)avifEncoderSetCodecSpecificOption(
+ encoder.get(), opts.first.c_str(), opts.second.c_str());
+#endif
}
avifAddImageFlags add_image_flags = AVIF_ADD_IMAGE_FLAG_SINGLE;
if (io->metadata.m.have_animation) {
@@ -258,14 +287,14 @@ class AvifCodec : public ImageCodec {
avifRGBImageAllocatePixels(&rgb_image);
std::unique_ptr<avifRGBImage, void (*)(avifRGBImage*)> pixels_freer(
&rgb_image, &avifRGBImageFreePixels);
- const double start_convert_image = Now();
+ const double start_convert_image = jxl::Now();
JXL_RETURN_IF_ERROR(ConvertToExternal(
ib, depth, /*float_out=*/false,
/*num_channels=*/ib.HasAlpha() ? 4 : 3, JXL_NATIVE_ENDIAN,
/*stride=*/rgb_image.rowBytes, pool, rgb_image.pixels,
rgb_image.rowBytes * rgb_image.height,
/*out_callback=*/{}, jxl::Orientation::kIdentity));
- const double end_convert_image = Now();
+ const double end_convert_image = jxl::Now();
elapsed_convert_image += end_convert_image - start_convert_image;
JXL_RETURN_IF_AVIF_ERROR(avifImageRGBToYUV(image.get(), &rgb_image));
JXL_RETURN_IF_AVIF_ERROR(avifEncoderAddImage(
@@ -276,24 +305,23 @@ class AvifCodec : public ImageCodec {
compressed->assign(buffer.data, buffer.data + buffer.size);
avifRWDataFree(&buffer);
}
- const double end = Now();
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start - elapsed_convert_image);
return true;
}
Status Decompress(const std::string& filename,
- const Span<const uint8_t> compressed,
- ThreadPoolInternal* pool, CodecInOut* io,
- jpegxl::tools::SpeedStats* speed_stats) override {
+ const Span<const uint8_t> compressed, ThreadPool* pool,
+ CodecInOut* io, SpeedStats* speed_stats) override {
io->frames.clear();
- io->dec_pixels = 0;
+ size_t max_threads = GetNumThreads(pool);
double elapsed_convert_image = 0;
- const double start = Now();
+ const double start = jxl::Now();
{
std::unique_ptr<avifDecoder, void (*)(avifDecoder*)> decoder(
avifDecoderCreate(), &avifDecoderDestroy);
decoder->codecChoice = decoder_;
- decoder->maxThreads = pool->NumThreads();
+ decoder->maxThreads = max_threads;
JXL_RETURN_IF_AVIF_ERROR(avifDecoderSetIOMemory(
decoder.get(), compressed.data(), compressed.size()));
JXL_RETURN_IF_AVIF_ERROR(avifDecoderParse(decoder.get()));
@@ -316,27 +344,27 @@ class AvifCodec : public ImageCodec {
std::unique_ptr<avifRGBImage, void (*)(avifRGBImage*)> pixels_freer(
&rgb_image, &avifRGBImageFreePixels);
JXL_RETURN_IF_AVIF_ERROR(avifImageYUVToRGB(decoder->image, &rgb_image));
- const double start_convert_image = Now();
+ const double start_convert_image = jxl::Now();
{
+ JxlPixelFormat format = {
+ (has_alpha ? 4u : 3u),
+ (rgb_image.depth <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16),
+ JXL_NATIVE_ENDIAN, 0};
ImageBundle ib(&io->metadata.m);
JXL_RETURN_IF_ERROR(ConvertFromExternal(
- Span<const uint8_t>(rgb_image.pixels,
- rgb_image.height * rgb_image.rowBytes),
- rgb_image.width, rgb_image.height, color, (has_alpha ? 4 : 3),
- /*alpha_is_premultiplied=*/false, rgb_image.depth,
- JXL_NATIVE_ENDIAN, pool, &ib,
- /*float_in=*/false, /*align=*/0));
+ Bytes(rgb_image.pixels, rgb_image.height * rgb_image.rowBytes),
+ rgb_image.width, rgb_image.height, color, rgb_image.depth, format,
+ pool, &ib));
io->frames.push_back(std::move(ib));
- io->dec_pixels += rgb_image.width * rgb_image.height;
}
- const double end_convert_image = Now();
+ const double end_convert_image = jxl::Now();
elapsed_convert_image += end_convert_image - start_convert_image;
}
if (next_image != AVIF_RESULT_NO_IMAGES_REMAINING) {
JXL_RETURN_IF_AVIF_ERROR(next_image);
}
}
- const double end = Now();
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start - elapsed_convert_image);
return true;
}
@@ -355,4 +383,5 @@ ImageCodec* CreateNewAvifCodec(const BenchmarkArgs& args) {
return new AvifCodec(args);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_codec_avif.h b/tools/benchmark/benchmark_codec_avif.h
index b3dc38e..c3816cf 100644
--- a/tools/benchmark/benchmark_codec_avif.h
+++ b/tools/benchmark/benchmark_codec_avif.h
@@ -10,11 +10,13 @@
#include "tools/benchmark/benchmark_args.h"
#include "tools/benchmark/benchmark_codec.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
ImageCodec* CreateNewAvifCodec(const BenchmarkArgs& args);
// Registers the avif-specific command line options.
Status AddCommandLineOptionsAvifCodec(BenchmarkArgs* args);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_AVIF_H_
diff --git a/tools/benchmark/benchmark_codec_custom.cc b/tools/benchmark/benchmark_codec_custom.cc
index eefae6e..87fc04c 100644
--- a/tools/benchmark/benchmark_codec_custom.cc
+++ b/tools/benchmark/benchmark_codec_custom.cc
@@ -13,35 +13,51 @@
#include <fstream>
#include "lib/extras/codec.h"
+#include "lib/extras/dec/color_description.h"
#include "lib/extras/enc/apng.h"
#include "lib/extras/time.h"
-#include "lib/jxl/base/file_io.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/image_bundle.h"
#include "tools/benchmark/benchmark_utils.h"
+#include "tools/file_io.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
-namespace {
+namespace jpegxl {
+namespace tools {
-std::string GetBaseName(std::string filename) {
- std::string result = std::move(filename);
- result = basename(&result[0]);
- const size_t dot = result.rfind('.');
- if (dot != std::string::npos) {
- result.resize(dot);
- }
- return result;
+struct CustomCodecArgs {
+ std::string extension;
+ std::string colorspace;
+ bool quiet;
+};
+
+static CustomCodecArgs* const custom_args = new CustomCodecArgs;
+
+Status AddCommandLineOptionsCustomCodec(BenchmarkArgs* args) {
+ args->AddString(
+ &custom_args->extension, "custom_codec_extension",
+ "Converts input and output of codec to this file type (default: png).",
+ "png");
+ args->AddString(
+ &custom_args->colorspace, "custom_codec_colorspace",
+ "If not empty, converts input and output of codec to this colorspace.",
+ "");
+ args->AddFlag(&custom_args->quiet, "custom_codec_quiet",
+ "Whether stdin and stdout of custom codec should be shown.",
+ false);
+ return true;
}
+namespace {
+
// This uses `output_filename` to determine the name of the corresponding
// `.time` file.
template <typename F>
Status ReportCodecRunningTime(F&& function, std::string output_filename,
jpegxl::tools::SpeedStats* const speed_stats) {
- const double start = Now();
+ const double start = jxl::Now();
JXL_RETURN_IF_ERROR(function());
- const double end = Now();
+ const double end = jxl::Now();
const std::string time_filename =
GetBaseName(std::move(output_filename)) + ".time";
std::ifstream time_stream(time_filename);
@@ -64,21 +80,36 @@ class CustomCodec : public ImageCodec {
explicit CustomCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
Status ParseParam(const std::string& param) override {
+ if (param_index_ == 0) {
+ description_ = "";
+ }
switch (param_index_) {
case 0:
extension_ = param;
+ description_ += param;
break;
-
case 1:
compress_command_ = param;
+ description_ += std::string(":");
+ if (param.find_last_of('/') < param.size()) {
+ description_ += param.substr(param.find_last_of('/') + 1);
+ } else {
+ description_ += param;
+ }
break;
-
case 2:
decompress_command_ = param;
break;
-
default:
compress_args_.push_back(param);
+ description_ += std::string(":");
+ if (param.size() > 2 && param[0] == '-' && param[1] == '-') {
+ description_ += param.substr(2);
+ } else if (param.size() > 2 && param[0] == '-') {
+ description_ += param.substr(1);
+ } else {
+ description_ += param;
+ }
break;
}
++param_index_;
@@ -86,49 +117,68 @@ class CustomCodec : public ImageCodec {
}
Status Compress(const std::string& filename, const CodecInOut* io,
- ThreadPoolInternal* pool, std::vector<uint8_t>* compressed,
+ ThreadPool* pool, std::vector<uint8_t>* compressed,
jpegxl::tools::SpeedStats* speed_stats) override {
JXL_RETURN_IF_ERROR(param_index_ > 2);
const std::string basename = GetBaseName(filename);
- TemporaryFile png_file(basename, "png"), encoded_file(basename, extension_);
- std::string png_filename, encoded_filename;
- JXL_RETURN_IF_ERROR(png_file.GetFileName(&png_filename));
+ TemporaryFile in_file(basename, custom_args->extension);
+ TemporaryFile encoded_file(basename, extension_);
+ std::string in_filename, encoded_filename;
+ JXL_RETURN_IF_ERROR(in_file.GetFileName(&in_filename));
JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename));
saved_intensity_target_ = io->metadata.m.IntensityTarget();
const size_t bits = io->metadata.m.bit_depth.bits_per_sample;
- JXL_RETURN_IF_ERROR(
- EncodeToFile(*io, io->Main().c_current(), bits, png_filename, pool));
+ ColorEncoding c_enc = io->Main().c_current();
+ if (!custom_args->colorspace.empty()) {
+ JxlColorEncoding colorspace;
+ JXL_RETURN_IF_ERROR(
+ jxl::ParseDescription(custom_args->colorspace, &colorspace));
+ JXL_RETURN_IF_ERROR(c_enc.FromExternal(colorspace));
+ }
+ std::vector<uint8_t> encoded;
+ JXL_RETURN_IF_ERROR(Encode(*io, c_enc, bits, in_filename, &encoded, pool));
+ JXL_RETURN_IF_ERROR(WriteFile(in_filename, encoded));
std::vector<std::string> arguments = compress_args_;
- arguments.push_back(png_filename);
+ arguments.push_back(in_filename);
arguments.push_back(encoded_filename);
JXL_RETURN_IF_ERROR(ReportCodecRunningTime(
- [&, this] { return RunCommand(compress_command_, arguments); },
+ [&, this] {
+ return RunCommand(compress_command_, arguments, custom_args->quiet);
+ },
encoded_filename, speed_stats));
return ReadFile(encoded_filename, compressed);
}
Status Decompress(const std::string& filename,
- const Span<const uint8_t> compressed,
- ThreadPoolInternal* pool, CodecInOut* io,
+ const Span<const uint8_t> compressed, ThreadPool* pool,
+ CodecInOut* io,
jpegxl::tools::SpeedStats* speed_stats) override {
const std::string basename = GetBaseName(filename);
- TemporaryFile encoded_file(basename, extension_), png_file(basename, "png");
- std::string encoded_filename, png_filename;
+ TemporaryFile encoded_file(basename, extension_);
+ TemporaryFile out_file(basename, custom_args->extension);
+ std::string encoded_filename, out_filename;
JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename));
- JXL_RETURN_IF_ERROR(png_file.GetFileName(&png_filename));
+ JXL_RETURN_IF_ERROR(out_file.GetFileName(&out_filename));
- JXL_RETURN_IF_ERROR(WriteFile(compressed, encoded_filename));
+ JXL_RETURN_IF_ERROR(WriteFile(encoded_filename, compressed));
JXL_RETURN_IF_ERROR(ReportCodecRunningTime(
[&, this] {
return RunCommand(
decompress_command_,
- std::vector<std::string>{encoded_filename, png_filename});
+ std::vector<std::string>{encoded_filename, out_filename},
+ custom_args->quiet);
},
- png_filename, speed_stats));
+ out_filename, speed_stats));
+ jxl::extras::ColorHints hints;
+ if (!custom_args->colorspace.empty()) {
+ hints.Add("color_space", custom_args->colorspace);
+ }
+ std::vector<uint8_t> encoded;
+ JXL_RETURN_IF_ERROR(ReadFile(out_filename, &encoded));
JXL_RETURN_IF_ERROR(
- SetFromFile(png_filename, extras::ColorHints(), io, pool));
+ jxl::SetFromBytes(jxl::Bytes(encoded), hints, io, pool));
io->metadata.m.SetIntensityTarget(saved_intensity_target_);
return true;
}
@@ -148,14 +198,18 @@ ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args) {
return new CustomCodec(args);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#else
-namespace jxl {
+namespace jpegxl {
+namespace tools {
ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args) { return nullptr; }
+Status AddCommandLineOptionsCustomCodec(BenchmarkArgs* args) { return true; }
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // _MSC_VER
diff --git a/tools/benchmark/benchmark_codec_custom.h b/tools/benchmark/benchmark_codec_custom.h
index b2711cd..6e3d017 100644
--- a/tools/benchmark/benchmark_codec_custom.h
+++ b/tools/benchmark/benchmark_codec_custom.h
@@ -37,10 +37,13 @@
#include "tools/benchmark/benchmark_args.h"
#include "tools/benchmark/benchmark_codec.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
ImageCodec* CreateNewCustomCodec(const BenchmarkArgs& args);
+Status AddCommandLineOptionsCustomCodec(BenchmarkArgs* args);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_CUSTOM_H_
diff --git a/tools/benchmark/benchmark_codec_jpeg.cc b/tools/benchmark/benchmark_codec_jpeg.cc
index ae3215a..ae6abae 100644
--- a/tools/benchmark/benchmark_codec_jpeg.cc
+++ b/tools/benchmark/benchmark_codec_jpeg.cc
@@ -13,104 +13,363 @@
#include <numeric> // partial_sum
#include <string>
+#if JPEGXL_ENABLE_JPEGLI
+#include "lib/extras/dec/jpegli.h"
+#endif
#include "lib/extras/dec/jpg.h"
+#if JPEGXL_ENABLE_JPEGLI
+#include "lib/extras/enc/jpegli.h"
+#endif
#include "lib/extras/enc/jpg.h"
#include "lib/extras/packed_image.h"
#include "lib/extras/packed_image_convert.h"
#include "lib/extras/time.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/image_bundle.h"
+#include "tools/benchmark/benchmark_utils.h"
#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
-
-namespace {
+namespace jpegxl {
+namespace tools {
struct JPEGArgs {
- std::string jpeg_encoder = "libjpeg";
- std::string chroma_subsampling = "444";
+ std::string base_quant_fn;
+ float search_q_start;
+ float search_q_min;
+ float search_q_max;
+ float search_d_min;
+ float search_d_max;
+ int search_max_iters;
+ float search_tolerance;
+ float search_q_precision;
+ float search_first_iter_slope;
};
-JPEGArgs* const jpegargs = new JPEGArgs;
+static JPEGArgs* const jpegargs = new JPEGArgs;
-} // namespace
+#define SET_ENCODER_ARG(name) \
+ if (jpegargs->name > 0) { \
+ encoder->SetOption(#name, std::to_string(jpegargs->name)); \
+ }
Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args) {
- args->cmdline.AddOptionValue(
- '\0', "chroma_subsampling", "444/422/420/411",
- "default JPEG chroma subsampling (default: 444).",
- &jpegargs->chroma_subsampling, &jpegxl::tools::ParseString);
+ args->AddString(&jpegargs->base_quant_fn, "qtables",
+ "Custom base quantization tables.");
+ args->AddFloat(&jpegargs->search_q_start, "search_q_start",
+ "Starting quality for quality-to-target search", 0.0f);
+ args->AddFloat(&jpegargs->search_q_min, "search_q_min",
+ "Minimum quality for quality-to-target search", 0.0f);
+ args->AddFloat(&jpegargs->search_q_max, "search_q_max",
+ "Maximum quality for quality-to-target search", 0.0f);
+ args->AddFloat(&jpegargs->search_d_min, "search_d_min",
+ "Minimum distance for quality-to-target search", 0.0f);
+ args->AddFloat(&jpegargs->search_d_max, "search_d_max",
+ "Maximum distance for quality-to-target search", 0.0f);
+ args->AddFloat(&jpegargs->search_tolerance, "search_tolerance",
+ "Percentage value, if quality-to-target search result "
+ "relative error is within this, search stops.",
+ 0.0f);
+ args->AddFloat(&jpegargs->search_q_precision, "search_q_precision",
+ "If last quality change in quality-to-target search is "
+ "within this value, search stops.",
+ 0.0f);
+ args->AddFloat(&jpegargs->search_first_iter_slope, "search_first_iter_slope",
+ "Slope of first extrapolation step in quality-to-target "
+ "search.",
+ 0.0f);
+ args->AddSigned(&jpegargs->search_max_iters, "search_max_iters",
+ "Maximum search steps in quality-to-target search.", 0);
return true;
}
class JPEGCodec : public ImageCodec {
public:
- explicit JPEGCodec(const BenchmarkArgs& args) : ImageCodec(args) {
- jpeg_encoder_ = jpegargs->jpeg_encoder;
- chroma_subsampling_ = jpegargs->chroma_subsampling;
- }
+ explicit JPEGCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
Status ParseParam(const std::string& param) override {
+ if (param[0] == 'q' && ImageCodec::ParseParam(param)) {
+ enc_quality_set_ = true;
+ return true;
+ }
if (ImageCodec::ParseParam(param)) {
return true;
}
- if (param == "sjpeg") {
+ if (param == "sjpeg" || param.find("cjpeg") != std::string::npos) {
jpeg_encoder_ = param;
return true;
}
+#if JPEGXL_ENABLE_JPEGLI
+ if (param == "enc-jpegli") {
+ jpeg_encoder_ = "jpegli";
+ return true;
+ }
+#endif
if (param.compare(0, 3, "yuv") == 0) {
- if (param.size() != 6) return false;
chroma_subsampling_ = param.substr(3);
return true;
}
+ if (param.compare(0, 4, "psnr") == 0) {
+ psnr_target_ = std::stof(param.substr(4));
+ return true;
+ }
+ if (param[0] == 'p') {
+ progressive_id_ = strtol(param.substr(1).c_str(), nullptr, 10);
+ return true;
+ }
+ if (param == "fix") {
+ fix_codes_ = true;
+ return true;
+ }
+ if (param[0] == 'Q') {
+ libjpeg_quality_ = strtol(param.substr(1).c_str(), nullptr, 10);
+ return true;
+ }
+ if (param.compare(0, 3, "YUV") == 0) {
+ if (param.size() != 6) return false;
+ libjpeg_chroma_subsampling_ = param.substr(3);
+ return true;
+ }
+ if (param == "noaq") {
+ enable_adaptive_quant_ = false;
+ return true;
+ }
+#if JPEGXL_ENABLE_JPEGLI
+ if (param == "xyb") {
+ xyb_mode_ = true;
+ return true;
+ }
+ if (param == "std") {
+ use_std_tables_ = true;
+ return true;
+ }
+ if (param == "dec-jpegli") {
+ jpeg_decoder_ = "jpegli";
+ return true;
+ }
+ if (param.substr(0, 2) == "bd") {
+ bitdepth_ = strtol(param.substr(2).c_str(), nullptr, 10);
+ return true;
+ }
+ if (param.substr(0, 6) == "cquant") {
+ num_colors_ = strtol(param.substr(6).c_str(), nullptr, 10);
+ return true;
+ }
+#endif
return false;
}
+ bool IgnoreAlpha() const override { return true; }
+
Status Compress(const std::string& filename, const CodecInOut* io,
- ThreadPoolInternal* pool, std::vector<uint8_t>* compressed,
+ ThreadPool* pool, std::vector<uint8_t>* compressed,
jpegxl::tools::SpeedStats* speed_stats) override {
- extras::PackedPixelFile ppf;
- JxlPixelFormat format = {0, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0};
+ if (jpeg_encoder_.find("cjpeg") != std::string::npos) {
+// Not supported on Windows due to Linux-specific functions.
+// Not supported in Android NDK before API 28.
+#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && \
+ (!defined(__ANDROID_API__) || __ANDROID_API__ >= 28)
+ const std::string basename = GetBaseName(filename);
+ TemporaryFile in_file(basename, "pnm");
+ TemporaryFile encoded_file(basename, "jpg");
+ std::string in_filename, encoded_filename;
+ JXL_RETURN_IF_ERROR(in_file.GetFileName(&in_filename));
+ JXL_RETURN_IF_ERROR(encoded_file.GetFileName(&encoded_filename));
+ const size_t bits = io->metadata.m.bit_depth.bits_per_sample;
+ ColorEncoding c_enc = io->Main().c_current();
+ std::vector<uint8_t> encoded;
+ JXL_RETURN_IF_ERROR(
+ Encode(*io, c_enc, bits, in_filename, &encoded, pool));
+ JXL_RETURN_IF_ERROR(WriteFile(in_filename, encoded));
+ std::string compress_command = jpeg_encoder_;
+ std::vector<std::string> arguments;
+ arguments.push_back("-outfile");
+ arguments.push_back(encoded_filename);
+ arguments.push_back("-quality");
+ arguments.push_back(std::to_string(static_cast<int>(q_target_)));
+ arguments.push_back("-sample");
+ if (chroma_subsampling_ == "444") {
+ arguments.push_back("1x1");
+ } else if (chroma_subsampling_ == "420") {
+ arguments.push_back("2x2");
+ } else if (!chroma_subsampling_.empty()) {
+ return JXL_FAILURE("Unsupported chroma subsampling");
+ }
+ arguments.push_back("-optimize");
+ arguments.push_back(in_filename);
+ const double start = jxl::Now();
+ JXL_RETURN_IF_ERROR(RunCommand(compress_command, arguments, false));
+ const double end = jxl::Now();
+ speed_stats->NotifyElapsed(end - start);
+ return ReadFile(encoded_filename, compressed);
+#else
+ return JXL_FAILURE("Not supported on this build");
+#endif
+ }
+
+ jxl::extras::PackedPixelFile ppf;
+ size_t bits_per_sample = io->metadata.m.bit_depth.bits_per_sample;
+ JxlPixelFormat format = {
+ 0, // num_channels is ignored by the converter
+ bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, JXL_BIG_ENDIAN,
+ 0};
JXL_RETURN_IF_ERROR(ConvertCodecInOutToPackedPixelFile(
*io, format, io->metadata.m.color_encoding, pool, &ppf));
- extras::EncodedImage encoded;
- std::unique_ptr<extras::Encoder> encoder = extras::GetJPEGEncoder();
- std::ostringstream os;
- os << static_cast<int>(std::round(q_target_));
- encoder->SetOption("q", os.str());
- encoder->SetOption("jpeg_encoder", jpeg_encoder_);
- encoder->SetOption("chroma_subsampling", chroma_subsampling_);
- const double start = Now();
- JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool));
- const double end = Now();
- *compressed = encoded.bitstreams.back();
- speed_stats->NotifyElapsed(end - start);
+ double elapsed = 0.0;
+ if (jpeg_encoder_ == "jpegli") {
+#if JPEGXL_ENABLE_JPEGLI
+ jxl::extras::JpegSettings settings;
+ settings.xyb = xyb_mode_;
+ if (!xyb_mode_) {
+ settings.use_std_quant_tables = use_std_tables_;
+ }
+ if (enc_quality_set_) {
+ settings.quality = q_target_;
+ } else {
+ settings.distance = butteraugli_target_;
+ }
+ if (progressive_id_ >= 0) {
+ settings.progressive_level = progressive_id_;
+ }
+ if (psnr_target_ > 0) {
+ settings.psnr_target = psnr_target_;
+ }
+ if (jpegargs->search_tolerance > 0) {
+ settings.search_tolerance = 0.01f * jpegargs->search_tolerance;
+ }
+ if (jpegargs->search_d_min > 0) {
+ settings.min_distance = jpegargs->search_d_min;
+ }
+ if (jpegargs->search_d_max > 0) {
+ settings.max_distance = jpegargs->search_d_max;
+ }
+ settings.chroma_subsampling = chroma_subsampling_;
+ settings.use_adaptive_quantization = enable_adaptive_quant_;
+ settings.libjpeg_quality = libjpeg_quality_;
+ settings.libjpeg_chroma_subsampling = libjpeg_chroma_subsampling_;
+ settings.optimize_coding = !fix_codes_;
+ const double start = jxl::Now();
+ JXL_RETURN_IF_ERROR(
+ jxl::extras::EncodeJpeg(ppf, settings, pool, compressed));
+ const double end = jxl::Now();
+ elapsed = end - start;
+#endif
+ } else {
+ jxl::extras::EncodedImage encoded;
+ std::unique_ptr<jxl::extras::Encoder> encoder =
+ jxl::extras::GetJPEGEncoder();
+ if (!encoder) {
+ fprintf(stderr, "libjpeg codec is not supported\n");
+ return false;
+ }
+ std::ostringstream os;
+ os << static_cast<int>(std::round(q_target_));
+ encoder->SetOption("q", os.str());
+ encoder->SetOption("jpeg_encoder", jpeg_encoder_);
+ if (!chroma_subsampling_.empty()) {
+ encoder->SetOption("chroma_subsampling", chroma_subsampling_);
+ }
+ if (progressive_id_ >= 0) {
+ encoder->SetOption("progressive", std::to_string(progressive_id_));
+ }
+ if (libjpeg_quality_ > 0) {
+ encoder->SetOption("libjpeg_quality", std::to_string(libjpeg_quality_));
+ }
+ if (!libjpeg_chroma_subsampling_.empty()) {
+ encoder->SetOption("libjpeg_chroma_subsampling",
+ libjpeg_chroma_subsampling_);
+ }
+ if (fix_codes_) {
+ encoder->SetOption("optimize", "OFF");
+ }
+ if (!enable_adaptive_quant_) {
+ encoder->SetOption("adaptive_q", "OFF");
+ }
+ if (psnr_target_ > 0) {
+ encoder->SetOption("psnr", std::to_string(psnr_target_));
+ }
+ if (!jpegargs->base_quant_fn.empty()) {
+ encoder->SetOption("base_quant_fn", jpegargs->base_quant_fn);
+ }
+ SET_ENCODER_ARG(search_q_start);
+ SET_ENCODER_ARG(search_q_min);
+ SET_ENCODER_ARG(search_q_max);
+ SET_ENCODER_ARG(search_q_precision);
+ SET_ENCODER_ARG(search_tolerance);
+ SET_ENCODER_ARG(search_first_iter_slope);
+ SET_ENCODER_ARG(search_max_iters);
+ const double start = jxl::Now();
+ JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool));
+ const double end = jxl::Now();
+ elapsed = end - start;
+ *compressed = encoded.bitstreams.back();
+ }
+ speed_stats->NotifyElapsed(elapsed);
return true;
}
Status Decompress(const std::string& filename,
- const Span<const uint8_t> compressed,
- ThreadPoolInternal* pool, CodecInOut* io,
+ const Span<const uint8_t> compressed, ThreadPool* pool,
+ CodecInOut* io,
jpegxl::tools::SpeedStats* speed_stats) override {
- extras::PackedPixelFile ppf;
- const double start = Now();
- JXL_RETURN_IF_ERROR(DecodeImageJPG(compressed, extras::ColorHints(),
- SizeConstraints(), &ppf));
- const double end = Now();
- speed_stats->NotifyElapsed(end - start);
- JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
+ jxl::extras::PackedPixelFile ppf;
+ if (jpeg_decoder_ == "jpegli") {
+#if JPEGXL_ENABLE_JPEGLI
+ std::vector<uint8_t> jpeg_bytes(compressed.data(),
+ compressed.data() + compressed.size());
+ const double start = jxl::Now();
+ jxl::extras::JpegDecompressParams dparams;
+ dparams.output_data_type =
+ bitdepth_ > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8;
+ dparams.num_colors = num_colors_;
+ JXL_RETURN_IF_ERROR(
+ jxl::extras::DecodeJpeg(jpeg_bytes, dparams, pool, &ppf));
+ const double end = jxl::Now();
+ speed_stats->NotifyElapsed(end - start);
+#endif
+ } else {
+ const double start = jxl::Now();
+ jxl::extras::JPGDecompressParams dparams;
+ dparams.num_colors = num_colors_;
+ JXL_RETURN_IF_ERROR(
+ jxl::extras::DecodeImageJPG(compressed, jxl::extras::ColorHints(),
+ &ppf, /*constraints=*/nullptr, &dparams));
+ const double end = jxl::Now();
+ speed_stats->NotifyElapsed(end - start);
+ }
+ JXL_RETURN_IF_ERROR(
+ jxl::extras::ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
return true;
}
protected:
- std::string jpeg_encoder_;
+ // JPEG encoder and its parameters
+ std::string jpeg_encoder_ = "libjpeg";
std::string chroma_subsampling_;
+ int progressive_id_ = -1;
+ bool fix_codes_ = false;
+ float psnr_target_ = 0.0f;
+ bool enc_quality_set_ = false;
+ int libjpeg_quality_ = 0;
+ std::string libjpeg_chroma_subsampling_;
+#if JPEGXL_ENABLE_JPEGLI
+ bool xyb_mode_ = false;
+ bool use_std_tables_ = false;
+#endif
+ bool enable_adaptive_quant_ = true;
+ // JPEG decoder and its parameters
+ std::string jpeg_decoder_ = "libjpeg";
+ int num_colors_ = 0;
+#if JPEGXL_ENABLE_JPEGLI
+ size_t bitdepth_ = 8;
+#endif
};
ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args) {
return new JPEGCodec(args);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_codec_jpeg.h b/tools/benchmark/benchmark_codec_jpeg.h
index cd4b009..d9f0c35 100644
--- a/tools/benchmark/benchmark_codec_jpeg.h
+++ b/tools/benchmark/benchmark_codec_jpeg.h
@@ -10,11 +10,13 @@
#include "tools/benchmark/benchmark_args.h"
#include "tools/benchmark/benchmark_codec.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args);
// Registers the jpeg-specific command line options.
Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_JPEG_H_
diff --git a/tools/benchmark/benchmark_codec_jxl.cc b/tools/benchmark/benchmark_codec_jxl.cc
index 6557858..554115a 100644
--- a/tools/benchmark/benchmark_codec_jxl.cc
+++ b/tools/benchmark/benchmark_codec_jxl.cc
@@ -4,6 +4,9 @@
// license that can be found in the LICENSE file.
#include "tools/benchmark/benchmark_codec_jxl.h"
+#include <jxl/stats.h>
+#include <jxl/thread_parallel_runner_cxx.h>
+
#include <cstdint>
#include <cstdlib>
#include <functional>
@@ -12,46 +15,34 @@
#include <utility>
#include <vector>
-#include "jxl/thread_parallel_runner_cxx.h"
#include "lib/extras/codec.h"
#include "lib/extras/dec/jxl.h"
-#if JPEGXL_ENABLE_JPEG
+#include "lib/extras/enc/apng.h"
+#include "lib/extras/enc/encode.h"
#include "lib/extras/enc/jpg.h"
-#endif
+#include "lib/extras/enc/jxl.h"
#include "lib/extras/packed_image_convert.h"
#include "lib/extras/time.h"
-#include "lib/jxl/aux_out.h"
#include "lib/jxl/base/data_parallel.h"
#include "lib/jxl/base/override.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/codec_in_out.h"
-#include "lib/jxl/enc_cache.h"
-#include "lib/jxl/enc_color_management.h"
-#include "lib/jxl/enc_external_image.h"
-#include "lib/jxl/enc_file.h"
-#include "lib/jxl/enc_params.h"
-#include "lib/jxl/image_bundle.h"
-#include "lib/jxl/image_metadata.h"
-#include "lib/jxl/modular/encoding/encoding.h"
#include "tools/benchmark/benchmark_file_io.h"
#include "tools/benchmark/benchmark_stats.h"
#include "tools/cmdline.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
-// Output function for EncodeBrunsli.
-size_t OutputToBytes(void* data, const uint8_t* buf, size_t count) {
- PaddedBytes* output = reinterpret_cast<PaddedBytes*>(data);
- output->append(buf, buf + count);
- return count;
-}
+using ::jxl::Image3F;
+using ::jxl::extras::EncodedImage;
+using ::jxl::extras::Encoder;
+using ::jxl::extras::JXLCompressParams;
+using ::jxl::extras::JXLDecompressParams;
+using ::jxl::extras::PackedFrame;
+using ::jxl::extras::PackedPixelFile;
struct JxlArgs {
- double xmul;
- double quant_bias;
-
- bool use_ac_strategy;
bool qprogressive; // progressive with shift-quantization.
bool progressive;
int progressive_dc;
@@ -60,20 +51,12 @@ struct JxlArgs {
Override dots;
Override patches;
- bool log_search_state;
std::string debug_image_dir;
};
static JxlArgs* const jxlargs = new JxlArgs;
Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args) {
- args->AddDouble(&jxlargs->xmul, "xmul",
- "Multiplier for the difference in X channel in Butteraugli.",
- 1.0);
- args->AddDouble(&jxlargs->quant_bias, "quant_bias",
- "Bias border pixels during quantization by this ratio.", 0.0);
- args->AddFlag(&jxlargs->use_ac_strategy, "use_ac_strategy",
- "If true, AC strategy will be used.", false);
args->AddFlag(&jxlargs->qprogressive, "qprogressive",
"Enable quantized progressive mode for AC.", false);
args->AddFlag(&jxlargs->progressive, "progressive",
@@ -88,9 +71,6 @@ Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args) {
args->AddOverride(&jxlargs->patches, "patches",
"Enable(1)/disable(0) patch dictionary.");
- args->AddFlag(&jxlargs->log_search_state, "log_search_state",
- "Print out debug info for tortoise mode AQ loop.", false);
-
args->AddString(
&jxlargs->debug_image_dir, "debug_image_dir",
"If not empty, saves debug images for each "
@@ -101,37 +81,76 @@ Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args) {
Status ValidateArgsJxlCodec(BenchmarkArgs* args) { return true; }
+inline bool ParseEffort(const std::string& s, int* out) {
+ if (s == "lightning") {
+ *out = 1;
+ return true;
+ } else if (s == "thunder") {
+ *out = 2;
+ return true;
+ } else if (s == "falcon") {
+ *out = 3;
+ return true;
+ } else if (s == "cheetah") {
+ *out = 4;
+ return true;
+ } else if (s == "hare") {
+ *out = 5;
+ return true;
+ } else if (s == "fast" || s == "wombat") {
+ *out = 6;
+ return true;
+ } else if (s == "squirrel") {
+ *out = 7;
+ return true;
+ } else if (s == "kitten") {
+ *out = 8;
+ return true;
+ } else if (s == "guetzli" || s == "tortoise") {
+ *out = 9;
+ return true;
+ } else if (s == "glacier") {
+ *out = 10;
+ return true;
+ }
+ size_t st = static_cast<size_t>(strtoull(s.c_str(), nullptr, 0));
+ if (st <= 10 && st >= 1) {
+ *out = st;
+ return true;
+ }
+ return false;
+}
+
class JxlCodec : public ImageCodec {
public:
- explicit JxlCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
+ explicit JxlCodec(const BenchmarkArgs& args)
+ : ImageCodec(args), stats_(nullptr, JxlEncoderStatsDestroy) {}
Status ParseParam(const std::string& param) override {
const std::string kMaxPassesPrefix = "max_passes=";
const std::string kDownsamplingPrefix = "downsampling=";
const std::string kResamplingPrefix = "resampling=";
const std::string kEcResamplingPrefix = "ec_resampling=";
-
+ int val;
+ float fval;
if (param.substr(0, kResamplingPrefix.size()) == kResamplingPrefix) {
std::istringstream parser(param.substr(kResamplingPrefix.size()));
- parser >> cparams_.resampling;
+ int resampling;
+ parser >> resampling;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, resampling);
} else if (param.substr(0, kEcResamplingPrefix.size()) ==
kEcResamplingPrefix) {
std::istringstream parser(param.substr(kEcResamplingPrefix.size()));
- parser >> cparams_.ec_resampling;
+ int ec_resampling;
+ parser >> ec_resampling;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING,
+ ec_resampling);
} else if (ImageCodec::ParseParam(param)) {
- if (param[0] == 'd' && butteraugli_target_ == 0.0) {
- cparams_.SetLossless();
- }
+ // Nothing to do.
} else if (param == "uint8") {
uint8_ = true;
- } else if (param[0] == 'u') {
- char* end;
- cparams_.uniform_quant = strtof(param.c_str() + 1, &end);
- if (end == param.c_str() + 1 || *end != '\0') {
- return JXL_FAILURE("failed to parse uniform quant parameter %s",
- param.c_str());
- }
- ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry;
+ } else if (param[0] == 'D') {
+ cparams_.alpha_distance = strtof(param.substr(1).c_str(), nullptr);
} else if (param.substr(0, kMaxPassesPrefix.size()) == kMaxPassesPrefix) {
std::istringstream parser(param.substr(kMaxPassesPrefix.size()));
parser >> dparams_.max_passes;
@@ -139,159 +158,127 @@ class JxlCodec : public ImageCodec {
kDownsamplingPrefix) {
std::istringstream parser(param.substr(kDownsamplingPrefix.size()));
parser >> dparams_.max_downsampling;
- } else if (ParseSpeedTier(param, &cparams_.speed_tier)) {
- // Nothing to do.
+ } else if (ParseEffort(param, &val)) {
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, val);
} else if (param[0] == 'X') {
- cparams_.channel_colors_pre_transform_percent =
- strtol(param.substr(1).c_str(), nullptr, 10);
+ fval = strtof(param.substr(1).c_str(), nullptr);
+ cparams_.AddFloatOption(
+ JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, fval);
} else if (param[0] == 'Y') {
- cparams_.channel_colors_percent =
- strtol(param.substr(1).c_str(), nullptr, 10);
+ fval = strtof(param.substr(1).c_str(), nullptr);
+ cparams_.AddFloatOption(
+ JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, fval);
} else if (param[0] == 'p') {
- cparams_.palette_colors = strtol(param.substr(1).c_str(), nullptr, 10);
+ val = strtol(param.substr(1).c_str(), nullptr, 10);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_PALETTE_COLORS, val);
} else if (param == "lp") {
- cparams_.lossy_palette = true;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_LOSSY_PALETTE, 1);
} else if (param[0] == 'C') {
- cparams_.colorspace = strtol(param.substr(1).c_str(), nullptr, 10);
+ val = strtol(param.substr(1).c_str(), nullptr, 10);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, val);
} else if (param[0] == 'c') {
- cparams_.color_transform =
- (jxl::ColorTransform)strtol(param.substr(1).c_str(), nullptr, 10);
+ val = strtol(param.substr(1).c_str(), nullptr, 10);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM, val);
has_ctransform_ = true;
} else if (param[0] == 'I') {
- cparams_.options.nb_repeats = strtof(param.substr(1).c_str(), nullptr);
+ fval = strtof(param.substr(1).c_str(), nullptr);
+ cparams_.AddFloatOption(
+ JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, fval * 100.0);
} else if (param[0] == 'E') {
- cparams_.options.max_properties =
- strtof(param.substr(1).c_str(), nullptr);
+ val = strtol(param.substr(1).c_str(), nullptr, 10);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, val);
} else if (param[0] == 'P') {
- cparams_.options.predictor =
- static_cast<Predictor>(strtof(param.substr(1).c_str(), nullptr));
+ val = strtol(param.substr(1).c_str(), nullptr, 10);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, val);
} else if (param == "slow") {
- cparams_.options.nb_repeats = 2;
+ cparams_.AddFloatOption(
+ JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, 50.0);
} else if (param == "R") {
- cparams_.responsive = 1;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, 1);
} else if (param[0] == 'R') {
- cparams_.responsive = strtol(param.substr(1).c_str(), nullptr, 10);
+ val = strtol(param.substr(1).c_str(), nullptr, 10);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, val);
} else if (param == "m") {
- cparams_.modular_mode = true;
- cparams_.color_transform = jxl::ColorTransform::kNone;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR, 1);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM, 1); // kNone
+ modular_mode_ = true;
} else if (param.substr(0, 3) == "gab") {
- long gab = strtol(param.substr(3).c_str(), nullptr, 10);
- if (gab != 0 && gab != 1) {
+ val = strtol(param.substr(3).c_str(), nullptr, 10);
+ if (val != 0 && val != 1) {
return JXL_FAILURE("Invalid gab value");
}
- cparams_.gaborish = static_cast<Override>(gab);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_GABORISH, val);
} else if (param[0] == 'g') {
- long gsize = strtol(param.substr(1).c_str(), nullptr, 10);
- if (gsize < 0 || gsize > 3) {
+ val = strtol(param.substr(1).c_str(), nullptr, 10);
+ if (val < 0 || val > 3) {
return JXL_FAILURE("Invalid group size shift value");
}
- cparams_.modular_group_size_shift = gsize;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, val);
} else if (param == "plt") {
- cparams_.options.max_properties = 0;
- cparams_.options.nb_repeats = 0;
- cparams_.options.predictor = Predictor::Zero;
- cparams_.responsive = 0;
- cparams_.colorspace = 0;
- cparams_.channel_colors_pre_transform_percent = 0;
- cparams_.channel_colors_percent = 0;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, 0);
+ cparams_.AddFloatOption(
+ JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, 0.0f);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, 0);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, 0);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, 0);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT,
+ 0);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, 0);
} else if (param.substr(0, 3) == "epf") {
- cparams_.epf = strtol(param.substr(3).c_str(), nullptr, 10);
- if (cparams_.epf > 3) {
+ val = strtol(param.substr(3).c_str(), nullptr, 10);
+ if (val > 3) {
return JXL_FAILURE("Invalid epf value");
}
- } else if (param.substr(0, 2) == "nr") {
- normalize_bitrate_ = true;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_EPF, val);
} else if (param.substr(0, 16) == "faster_decoding=") {
- cparams_.decoding_speed_tier =
- strtol(param.substr(16).c_str(), nullptr, 10);
+ val = strtol(param.substr(16).c_str(), nullptr, 10);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_DECODING_SPEED, val);
} else {
return JXL_FAILURE("Unrecognized param");
}
return true;
}
- bool IsColorAware() const override {
- // Can't deal with negative values from color space conversion.
- if (cparams_.modular_mode) return false;
- if (normalize_bitrate_) return false;
- // Otherwise, input may be in any color space.
- return true;
- }
-
- bool IsJpegTranscoder() const override {
- // TODO(veluca): figure out when to turn this on.
- return false;
- }
-
Status Compress(const std::string& filename, const CodecInOut* io,
- ThreadPoolInternal* pool, std::vector<uint8_t>* compressed,
+ ThreadPool* pool, std::vector<uint8_t>* compressed,
jpegxl::tools::SpeedStats* speed_stats) override {
- if (!jxlargs->debug_image_dir.empty()) {
- cinfo_.dump_image = [](const CodecInOut& io, const std::string& path) {
- return EncodeToFile(io, path);
- };
- cinfo_.debug_prefix =
- JoinPath(jxlargs->debug_image_dir, FileBaseName(filename)) +
- ".jxl:" + params_ + ".dbg/";
- JXL_RETURN_IF_ERROR(MakeDir(cinfo_.debug_prefix));
+ PackedPixelFile ppf;
+ JxlPixelFormat format{0, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0};
+ JXL_RETURN_IF_ERROR(ConvertCodecInOutToPackedPixelFile(
+ *io, format, io->Main().c_current(), pool, &ppf));
+ cparams_.runner = pool->runner();
+ cparams_.runner_opaque = pool->runner_opaque();
+ cparams_.distance = butteraugli_target_;
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_NOISE, (int)jxlargs->noise);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_DOTS, (int)jxlargs->dots);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_PATCHES, (int)jxlargs->patches);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC,
+ jxlargs->progressive);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC,
+ jxlargs->qprogressive);
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC,
+ jxlargs->progressive_dc);
+ if (butteraugli_target_ > 0.f && modular_mode_ && !has_ctransform_) {
+ // Reset color transform to default XYB for lossy modular.
+ cparams_.AddOption(JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM, -1);
}
- cparams_.butteraugli_distance = butteraugli_target_;
- cparams_.target_bitrate = bitrate_target_;
-
- cparams_.dots = jxlargs->dots;
- cparams_.patches = jxlargs->patches;
-
- cparams_.progressive_mode = jxlargs->progressive;
- cparams_.qprogressive_mode = jxlargs->qprogressive;
- cparams_.progressive_dc = jxlargs->progressive_dc;
-
- cparams_.noise = jxlargs->noise;
-
- cparams_.quant_border_bias = static_cast<float>(jxlargs->quant_bias);
- cparams_.ba_params.hf_asymmetry = ba_params_.hf_asymmetry;
- cparams_.ba_params.xmul = static_cast<float>(jxlargs->xmul);
-
- if (cparams_.butteraugli_distance > 0.f &&
- cparams_.color_transform == ColorTransform::kNone &&
- cparams_.modular_mode && !has_ctransform_) {
- cparams_.color_transform = ColorTransform::kXYB;
+ std::string debug_prefix;
+ SetDebugImageCallback(filename, &debug_prefix, &cparams_);
+ if (args_.print_more_stats) {
+ stats_.reset(JxlEncoderStatsCreate());
+ cparams_.stats = stats_.get();
}
-
- cparams_.log_search_state = jxlargs->log_search_state;
-
-#if JPEGXL_ENABLE_JPEG
- if (normalize_bitrate_ && cparams_.butteraugli_distance > 0.0f) {
- extras::PackedPixelFile ppf;
- JxlPixelFormat format = {0, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0};
- JXL_RETURN_IF_ERROR(ConvertCodecInOutToPackedPixelFile(
- *io, format, io->metadata.m.color_encoding, pool, &ppf));
- extras::EncodedImage encoded;
- std::unique_ptr<extras::Encoder> encoder = extras::GetJPEGEncoder();
- encoder->SetOption("q", "95");
- JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool));
- float jpeg_bits = encoded.bitstreams.back().size() * kBitsPerByte;
- float jpeg_bitrate = jpeg_bits / (io->xsize() * io->ysize());
- // Formula fitted on jyrki31 corpus for distances between 1.0 and 8.0.
- cparams_.target_bitrate = (jpeg_bitrate * 0.36f /
- (0.6f * cparams_.butteraugli_distance + 0.4f));
- }
-#endif
-
- const double start = Now();
- PassesEncoderState passes_encoder_state;
- PaddedBytes compressed_padded;
- JXL_RETURN_IF_ERROR(EncodeFile(cparams_, io, &passes_encoder_state,
- &compressed_padded, GetJxlCms(), &cinfo_,
- pool));
- const double end = Now();
- compressed->assign(compressed_padded.begin(), compressed_padded.end());
+ const double start = jxl::Now();
+ JXL_RETURN_IF_ERROR(jxl::extras::EncodeImageJXL(
+ cparams_, ppf, /*jpeg_bytes=*/nullptr, compressed));
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start);
return true;
}
Status Decompress(const std::string& filename,
- const Span<const uint8_t> compressed,
- ThreadPoolInternal* pool, CodecInOut* io,
+ const Span<const uint8_t> compressed, ThreadPool* pool,
+ CodecInOut* io,
jpegxl::tools::SpeedStats* speed_stats) override {
dparams_.runner = pool->runner();
dparams_.runner_opaque = pool->runner_opaque();
@@ -304,35 +291,67 @@ class JxlCodec : public ImageCodec {
// originals, so we must set the option to keep the original orientation
// instead.
dparams_.keep_orientation = true;
- extras::PackedPixelFile ppf;
+ PackedPixelFile ppf;
size_t decoded_bytes;
- const double start = Now();
- JXL_RETURN_IF_ERROR(DecodeImageJXL(compressed.data(), compressed.size(),
- dparams_, &decoded_bytes, &ppf));
- const double end = Now();
+ const double start = jxl::Now();
+ JXL_RETURN_IF_ERROR(jxl::extras::DecodeImageJXL(
+ compressed.data(), compressed.size(), dparams_, &decoded_bytes, &ppf));
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start);
JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
return true;
}
void GetMoreStats(BenchmarkStats* stats) override {
- JxlStats jxl_stats;
- jxl_stats.num_inputs = 1;
- jxl_stats.aux_out = cinfo_;
- stats->jxl_stats.Assimilate(jxl_stats);
+ stats->jxl_stats.num_inputs += 1;
+ JxlEncoderStatsMerge(stats->jxl_stats.stats.get(), stats_.get());
}
protected:
- AuxOut cinfo_;
- CompressParams cparams_;
+ JXLCompressParams cparams_;
bool has_ctransform_ = false;
- extras::JXLDecompressParams dparams_;
+ bool modular_mode_ = false;
+ JXLDecompressParams dparams_;
bool uint8_ = false;
- bool normalize_bitrate_ = false;
+ std::unique_ptr<JxlEncoderStats, decltype(JxlEncoderStatsDestroy)*> stats_;
+
+ private:
+ void SetDebugImageCallback(const std::string& filename,
+ std::string* debug_prefix,
+ JXLCompressParams* cparams) {
+ if (jxlargs->debug_image_dir.empty()) return;
+ *debug_prefix = JoinPath(jxlargs->debug_image_dir, FileBaseName(filename)) +
+ ".jxl:" + params_ + ".dbg/";
+ JXL_CHECK(MakeDir(*debug_prefix));
+ cparams->debug_image_opaque = debug_prefix;
+ cparams->debug_image = [](void* opaque, const char* label, size_t xsize,
+ size_t ysize, const JxlColorEncoding* color,
+ const uint16_t* pixels) {
+ auto encoder = jxl::extras::GetAPNGEncoder();
+ JXL_CHECK(encoder);
+ PackedPixelFile debug_ppf;
+ JxlPixelFormat format{3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0};
+ PackedFrame frame(xsize, ysize, format);
+ memcpy(frame.color.pixels(), pixels, 6 * xsize * ysize);
+ debug_ppf.frames.emplace_back(std::move(frame));
+ debug_ppf.info.xsize = xsize;
+ debug_ppf.info.ysize = ysize;
+ debug_ppf.info.num_color_channels = 3;
+ debug_ppf.info.bits_per_sample = 16;
+ debug_ppf.color_encoding = *color;
+ EncodedImage encoded;
+ JXL_CHECK(encoder->Encode(debug_ppf, &encoded));
+ JXL_CHECK(!encoded.bitstreams.empty());
+ std::string* debug_prefix = reinterpret_cast<std::string*>(opaque);
+ std::string fn = *debug_prefix + std::string(label) + ".png";
+ WriteFile(fn, encoded.bitstreams[0]);
+ };
+ }
};
ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args) {
return new JxlCodec(args);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_codec_jxl.h b/tools/benchmark/benchmark_codec_jxl.h
index 12e9fef..967be26 100644
--- a/tools/benchmark/benchmark_codec_jxl.h
+++ b/tools/benchmark/benchmark_codec_jxl.h
@@ -12,12 +12,14 @@
#include "tools/benchmark/benchmark_args.h"
#include "tools/benchmark/benchmark_codec.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args);
// Registers the jxl-specific command line options.
Status AddCommandLineOptionsJxlCodec(BenchmarkArgs* args);
Status ValidateArgsJxlCodec(BenchmarkArgs* args);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_JXL_H_
diff --git a/tools/benchmark/benchmark_codec_png.cc b/tools/benchmark/benchmark_codec_png.cc
index b310b11..2886166 100644
--- a/tools/benchmark/benchmark_codec_png.cc
+++ b/tools/benchmark/benchmark_codec_png.cc
@@ -3,8 +3,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-#if JPEGXL_ENABLE_APNG
-
#include "tools/benchmark/benchmark_codec_png.h"
#include <stddef.h>
@@ -18,12 +16,14 @@
#include "lib/extras/packed_image.h"
#include "lib/extras/packed_image_convert.h"
#include "lib/extras/time.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_metadata.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
struct PNGArgs {
// Empty, no PNG-specific args currently.
@@ -41,36 +41,43 @@ class PNGCodec : public ImageCodec {
Status ParseParam(const std::string& param) override { return true; }
Status Compress(const std::string& filename, const CodecInOut* io,
- ThreadPoolInternal* pool, std::vector<uint8_t>* compressed,
+ ThreadPool* pool, std::vector<uint8_t>* compressed,
jpegxl::tools::SpeedStats* speed_stats) override {
const size_t bits = io->metadata.m.bit_depth.bits_per_sample;
- const double start = Now();
- JXL_RETURN_IF_ERROR(Encode(*io, extras::Codec::kPNG, io->Main().c_current(),
- bits, compressed, pool));
- const double end = Now();
+ const double start = jxl::Now();
+ JXL_RETURN_IF_ERROR(jxl::Encode(*io, jxl::extras::Codec::kPNG,
+ io->Main().c_current(), bits, compressed,
+ pool));
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start);
return true;
}
Status Decompress(const std::string& /*filename*/,
- const Span<const uint8_t> compressed,
- ThreadPoolInternal* pool, CodecInOut* io,
+ const Span<const uint8_t> compressed, ThreadPool* pool,
+ CodecInOut* io,
jpegxl::tools::SpeedStats* speed_stats) override {
- extras::PackedPixelFile ppf;
- const double start = Now();
- JXL_RETURN_IF_ERROR(extras::DecodeImageAPNG(
- compressed, extras::ColorHints(), SizeConstraints(), &ppf));
- const double end = Now();
+ jxl::extras::PackedPixelFile ppf;
+ const double start = jxl::Now();
+ JXL_RETURN_IF_ERROR(jxl::extras::DecodeImageAPNG(
+ compressed, jxl::extras::ColorHints(), &ppf));
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start);
- JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
+ JXL_RETURN_IF_ERROR(
+ jxl::extras::ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
return true;
}
};
ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args) {
- return new PNGCodec(args);
+ if (jxl::extras::GetAPNGEncoder() &&
+ jxl::extras::CanDecode(jxl::extras::Codec::kPNG)) {
+ return new PNGCodec(args);
+ } else {
+ return nullptr;
+ }
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
-#endif
diff --git a/tools/benchmark/benchmark_codec_png.h b/tools/benchmark/benchmark_codec_png.h
index 23d982e..8f29583 100644
--- a/tools/benchmark/benchmark_codec_png.h
+++ b/tools/benchmark/benchmark_codec_png.h
@@ -6,21 +6,19 @@
#ifndef TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_
#define TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_
-#if JPEGXL_ENABLE_APNG
-
#include <string>
#include "lib/jxl/base/status.h"
#include "tools/benchmark/benchmark_args.h"
#include "tools/benchmark/benchmark_codec.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args);
// Registers the png-specific command line options.
Status AddCommandLineOptionsPNGCodec(BenchmarkArgs* args);
-} // namespace jxl
-
-#endif
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_PNG_H_
diff --git a/tools/benchmark/benchmark_codec_webp.cc b/tools/benchmark/benchmark_codec_webp.cc
index 3b1bb26..926dee6 100644
--- a/tools/benchmark/benchmark_codec_webp.cc
+++ b/tools/benchmark/benchmark_codec_webp.cc
@@ -4,6 +4,7 @@
// license that can be found in the LICENSE file.
#include "tools/benchmark/benchmark_codec_webp.h"
+#include <jxl/cms.h>
#include <stdint.h>
#include <string.h>
#include <webp/decode.h>
@@ -15,35 +16,39 @@
#include "lib/extras/time.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
-#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/base/status.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/dec_external_image.h"
-#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_external_image.h"
#include "lib/jxl/enc_image_bundle.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
#include "lib/jxl/sanitizers.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::ImageBundle;
+using ::jxl::ImageMetadata;
+using ::jxl::ThreadPool;
// Sets image data from 8-bit sRGB pixel array in bytes.
// Amount of input bytes per pixel must be:
// (is_gray ? 1 : 3) + (has_alpha ? 1 : 0)
Status FromSRGB(const size_t xsize, const size_t ysize, const bool is_gray,
- const bool has_alpha, const bool alpha_is_premultiplied,
- const bool is_16bit, const JxlEndianness endianness,
- const uint8_t* pixels, const uint8_t* end, ThreadPool* pool,
- ImageBundle* ib) {
+ const bool has_alpha, const bool is_16bit,
+ const JxlEndianness endianness, const uint8_t* pixels,
+ const uint8_t* end, ThreadPool* pool, ImageBundle* ib) {
const ColorEncoding& c = ColorEncoding::SRGB(is_gray);
- const size_t bits_per_sample = (is_16bit ? 2 : 1) * kBitsPerByte;
+ const size_t bits_per_sample = (is_16bit ? 2 : 1) * jxl::kBitsPerByte;
+ const uint32_t num_channels = (is_gray ? 1 : 3) + (has_alpha ? 1 : 0);
+ JxlDataType data_type = is_16bit ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8;
+ JxlPixelFormat format = {num_channels, data_type, endianness, 0};
const Span<const uint8_t> span(pixels, end - pixels);
- return ConvertFromExternal(
- span, xsize, ysize, c, (is_gray ? 1 : 3) + (has_alpha ? 1 : 0),
- alpha_is_premultiplied, bits_per_sample, endianness, pool, ib,
- /*float_in=*/false, /*align=*/0);
+ return ConvertFromExternal(span, xsize, ysize, c, bits_per_sample, format,
+ pool, ib);
}
struct WebPArgs {
@@ -85,9 +90,9 @@ class WebPCodec : public ImageCodec {
}
Status Compress(const std::string& filename, const CodecInOut* io,
- ThreadPoolInternal* pool, std::vector<uint8_t>* compressed,
+ ThreadPool* pool, std::vector<uint8_t>* compressed,
jpegxl::tools::SpeedStats* speed_stats) override {
- const double start = Now();
+ const double start = jxl::Now();
const ImageBundle& ib = io->Main();
if (ib.HasAlpha() && ib.metadata()->GetAlphaBits() > 8) {
@@ -99,8 +104,8 @@ class WebPCodec : public ImageCodec {
ImageBundle store(&metadata);
const ImageBundle* transformed;
const ColorEncoding& c_desired = ColorEncoding::SRGB(false);
- JXL_RETURN_IF_ERROR(TransformIfNeeded(ib, c_desired, GetJxlCms(), pool,
- &store, &transformed));
+ JXL_RETURN_IF_ERROR(jxl::TransformIfNeeded(
+ ib, c_desired, *JxlGetDefaultCms(), pool, &store, &transformed));
size_t xsize = ib.oriented_xsize();
size_t ysize = ib.oriented_ysize();
size_t stride = xsize * num_chans;
@@ -153,14 +158,14 @@ class WebPCodec : public ImageCodec {
} else {
return false;
}
- const double end = Now();
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start);
return true;
}
Status Decompress(const std::string& filename,
- const Span<const uint8_t> compressed,
- ThreadPoolInternal* pool, CodecInOut* io,
+ const Span<const uint8_t> compressed, ThreadPool* pool,
+ CodecInOut* io,
jpegxl::tools::SpeedStats* speed_stats) override {
WebPDecoderConfig config;
#ifdef MEMORY_SANITIZER
@@ -177,11 +182,11 @@ class WebPCodec : public ImageCodec {
buf->colorspace = MODE_RGBA;
const uint8_t* webp_data = compressed.data();
const int webp_size = compressed.size();
- const double start = Now();
+ const double start = jxl::Now();
if (WebPDecode(webp_data, webp_size, &config) != VP8_STATUS_OK) {
return JXL_FAILURE("WebPDecode failed");
}
- const double end = Now();
+ const double end = jxl::Now();
speed_stats->NotifyElapsed(end - start);
JXL_CHECK(buf->u.RGBA.stride == buf->width * 4);
@@ -191,7 +196,7 @@ class WebPCodec : public ImageCodec {
const uint8_t* data_end = data_begin + buf->width * buf->height * 4;
// The image data is initialized by libwebp, which we are not instrumenting
// with msan.
- msan::UnpoisonMemory(data_begin, data_end - data_begin);
+ jxl::msan::UnpoisonMemory(data_begin, data_end - data_begin);
if (io->metadata.m.color_encoding.IsGray() != is_gray) {
// TODO(lode): either ensure is_gray matches what the color profile says,
// or set a correct color profile, e.g.
@@ -201,13 +206,11 @@ class WebPCodec : public ImageCodec {
return JXL_FAILURE("Color profile is-gray mismatch");
}
io->metadata.m.SetAlphaBits(8);
- const Status ok =
- FromSRGB(buf->width, buf->height, is_gray, has_alpha,
- /*alpha_is_premultiplied=*/false, /*is_16bit=*/false,
- JXL_LITTLE_ENDIAN, data_begin, data_end, pool, &io->Main());
+ const Status ok = FromSRGB(buf->width, buf->height, is_gray, has_alpha,
+ /*is_16bit=*/false, JXL_LITTLE_ENDIAN,
+ data_begin, data_end, pool, &io->Main());
WebPFreeDecBuffer(buf);
JXL_RETURN_IF_ERROR(ok);
- io->dec_pixels = buf->width * buf->height;
return true;
}
@@ -228,7 +231,9 @@ class WebPCodec : public ImageCodec {
std::vector<uint8_t>* compressed) {
compressed->clear();
WebPConfig config;
- WebPConfigInit(&config);
+ if (!WebPConfigInit(&config)) {
+ return JXL_FAILURE("WebPConfigInit failed");
+ }
JXL_ASSERT(!lossless_ || !near_lossless_); // can't have both
config.lossless = lossless_;
config.quality = quality;
@@ -243,7 +248,9 @@ class WebPCodec : public ImageCodec {
JXL_CHECK(WebPValidateConfig(&config));
WebPPicture pic;
- WebPPictureInit(&pic);
+ if (!WebPPictureInit(&pic)) {
+ return JXL_FAILURE("WebPPictureInit failed");
+ }
pic.width = static_cast<int>(xsize);
pic.height = static_cast<int>(ysize);
pic.writer = &WebPStringWrite;
@@ -251,9 +258,13 @@ class WebPCodec : public ImageCodec {
pic.custom_ptr = compressed;
if (num_chans == 3) {
- WebPPictureImportRGB(&pic, srgb.data(), 3 * xsize);
+ if (!WebPPictureImportRGB(&pic, srgb.data(), 3 * xsize)) {
+ return JXL_FAILURE("WebPPictureImportRGB failed");
+ }
} else {
- WebPPictureImportRGBA(&pic, srgb.data(), 4 * xsize);
+ if (!WebPPictureImportRGBA(&pic, srgb.data(), 4 * xsize)) {
+ return JXL_FAILURE("WebPPictureImportRGBA failed");
+ }
}
// WebP encoding may fail, for example, if the image is more than 16384
@@ -262,7 +273,7 @@ class WebPCodec : public ImageCodec {
WebPPictureFree(&pic);
// Compressed image data is initialized by libwebp, which we are not
// instrumenting with msan.
- msan::UnpoisonMemory(compressed->data(), compressed->size());
+ jxl::msan::UnpoisonMemory(compressed->data(), compressed->size());
return ok;
}
@@ -277,4 +288,5 @@ ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args) {
return new WebPCodec(args);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_codec_webp.h b/tools/benchmark/benchmark_codec_webp.h
index cd4c60f..37d3c58 100644
--- a/tools/benchmark/benchmark_codec_webp.h
+++ b/tools/benchmark/benchmark_codec_webp.h
@@ -13,11 +13,13 @@
#include "tools/benchmark/benchmark_args.h"
#include "tools/benchmark/benchmark_codec.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args);
// Registers the webp-specific command line options.
Status AddCommandLineOptionsWebPCodec(BenchmarkArgs* args);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_CODEC_WEBP_H_
diff --git a/tools/benchmark/benchmark_file_io.cc b/tools/benchmark/benchmark_file_io.cc
index c5db02b..b8acbfb 100644
--- a/tools/benchmark/benchmark_file_io.cc
+++ b/tools/benchmark/benchmark_file_io.cc
@@ -38,7 +38,8 @@
#define GLOB_TILDE 0
#endif
-namespace jxl {
+namespace jpegxl {
+namespace tools {
const char kPathSeparator = '/';
@@ -229,4 +230,5 @@ Status MatchFiles(const std::string& pattern, std::vector<std::string>* list) {
#endif // HAS_GLOB
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_file_io.h b/tools/benchmark/benchmark_file_io.h
index ecb8359..3c68acc 100644
--- a/tools/benchmark/benchmark_file_io.h
+++ b/tools/benchmark/benchmark_file_io.h
@@ -12,10 +12,13 @@
#include <string>
#include <vector>
-#include "lib/jxl/base/file_io.h"
#include "lib/jxl/base/status.h"
+#include "tools/file_io.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::Status;
// Checks if the file exists, either as file or as directory
bool PathExists(const std::string& fname);
@@ -48,6 +51,7 @@ Status MatchFiles(const std::string& pattern, std::vector<std::string>* list);
std::string JoinPath(const std::string& first, const std::string& second);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_FILE_IO_H_
diff --git a/tools/benchmark/benchmark_stats.cc b/tools/benchmark/benchmark_stats.cc
index f22e89c..87b9985 100644
--- a/tools/benchmark/benchmark_stats.cc
+++ b/tools/benchmark/benchmark_stats.cc
@@ -17,7 +17,55 @@
#include "lib/jxl/base/status.h"
#include "tools/benchmark/benchmark_args.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+#define ADD_NAME(val, name) \
+ case JXL_ENC_STAT_##val: \
+ return name
+const char* JxlStatsName(JxlEncoderStatsKey key) {
+ switch (key) {
+ ADD_NAME(HEADER_BITS, "Header bits");
+ ADD_NAME(TOC_BITS, "TOC bits");
+ ADD_NAME(DICTIONARY_BITS, "Patch dictionary bits");
+ ADD_NAME(SPLINES_BITS, "Splines bits");
+ ADD_NAME(NOISE_BITS, "Noise bits");
+ ADD_NAME(QUANT_BITS, "Quantizer bits");
+ ADD_NAME(MODULAR_TREE_BITS, "Modular tree bits");
+ ADD_NAME(MODULAR_GLOBAL_BITS, "Modular global bits");
+ ADD_NAME(DC_BITS, "DC bits");
+ ADD_NAME(MODULAR_DC_GROUP_BITS, "Modular DC group bits");
+ ADD_NAME(CONTROL_FIELDS_BITS, "Control field bits");
+ ADD_NAME(COEF_ORDER_BITS, "Coeff order bits");
+ ADD_NAME(AC_HISTOGRAM_BITS, "AC histogram bits");
+ ADD_NAME(AC_BITS, "AC token bits");
+ ADD_NAME(MODULAR_AC_GROUP_BITS, "Modular AC group bits");
+ ADD_NAME(NUM_SMALL_BLOCKS, "Number of small blocks");
+ ADD_NAME(NUM_DCT4X8_BLOCKS, "Number of 4x8 blocks");
+ ADD_NAME(NUM_AFV_BLOCKS, "Number of AFV blocks");
+ ADD_NAME(NUM_DCT8_BLOCKS, "Number of 8x8 blocks");
+ ADD_NAME(NUM_DCT8X32_BLOCKS, "Number of 8x32 blocks");
+ ADD_NAME(NUM_DCT16_BLOCKS, "Number of 16x16 blocks");
+ ADD_NAME(NUM_DCT16X32_BLOCKS, "Number of 16x32 blocks");
+ ADD_NAME(NUM_DCT32_BLOCKS, "Number of 32x32 blocks");
+ ADD_NAME(NUM_DCT32X64_BLOCKS, "Number of 32x64 blocks");
+ ADD_NAME(NUM_DCT64_BLOCKS, "Number of 64x64 blocks");
+ ADD_NAME(NUM_BUTTERAUGLI_ITERS, "Butteraugli iters");
+ default:
+ return "";
+ };
+ return "";
+}
+#undef ADD_NAME
+
+void JxlStats::Print() const {
+ for (int i = 0; i < JXL_ENC_NUM_STATS; ++i) {
+ JxlEncoderStatsKey key = static_cast<JxlEncoderStatsKey>(i);
+ size_t value = JxlEncoderStatsGet(stats.get(), key);
+ if (value) printf("%-25s %10" PRIuS "\n", JxlStatsName(key), value);
+ }
+}
+
namespace {
// Computes longest codec name from Args()->codec, for table alignment.
@@ -61,7 +109,7 @@ struct ColumnDescriptor {
bool more; // Whether to print only if more_columns is enabled
};
-static const ColumnDescriptor ExtraMetricDescriptor() {
+static ColumnDescriptor ExtraMetricDescriptor() {
ColumnDescriptor d{{"DO NOT USE"}, 12, 4, TYPE_POSITIVE_FLOAT, false};
return d;
}
@@ -81,21 +129,11 @@ std::vector<ColumnDescriptor> GetColumnDescriptors(size_t num_extra_metrics) {
{{"E MP/s"}, 8, 3, TYPE_POSITIVE_FLOAT, false},
{{"D MP/s"}, 8, 3, TYPE_POSITIVE_FLOAT, false},
{{"Max norm"}, 13, 8, TYPE_POSITIVE_FLOAT, false},
+ {{"SSIMULACRA2"}, 13, 8, TYPE_POSITIVE_FLOAT, false},
+ {{"PSNR"}, 7, 2, TYPE_POSITIVE_FLOAT, false},
{{"pnorm"}, 13, 8, TYPE_POSITIVE_FLOAT, false},
- {{"PSNR"}, 7, 2, TYPE_POSITIVE_FLOAT, true},
- {{"QABPP"}, 8, 3, TYPE_POSITIVE_FLOAT, true},
- {{"SmallB"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"DCT4x8"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"AFV"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"DCT8x8"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"8x16"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"8x32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"16"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"16x32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"32x64"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
- {{"64"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
{{"BPP*pnorm"}, 16, 12, TYPE_POSITIVE_FLOAT, false},
+ {{"QABPP"}, 8, 3, TYPE_POSITIVE_FLOAT, false},
{{"Bugs"}, 7, 5, TYPE_COUNT, false},
};
// clang-format on
@@ -124,7 +162,7 @@ static std::string FormatFloat(const ColumnDescriptor& label, double value) {
size_t point = result.rfind('.');
if (point != std::string::npos) {
int end = std::max<int>(point + 2, label.width - 1);
- result = result.substr(0, end);
+ result.resize(end);
}
}
return result;
@@ -148,9 +186,10 @@ void BenchmarkStats::Assimilate(const BenchmarkStats& victim) {
total_adj_compressed_size += victim.total_adj_compressed_size;
total_time_encode += victim.total_time_encode;
total_time_decode += victim.total_time_decode;
- max_distance = std::max(max_distance, victim.max_distance);
+ max_distance += pow(victim.max_distance, 2.0) * victim.total_input_pixels;
distance_p_norm += victim.distance_p_norm;
- distance_2 += victim.distance_2;
+ ssimulacra2 += victim.ssimulacra2;
+ psnr += victim.psnr;
distances.insert(distances.end(), victim.distances.begin(),
victim.distances.end());
total_errors += victim.total_errors;
@@ -166,13 +205,6 @@ void BenchmarkStats::Assimilate(const BenchmarkStats& victim) {
void BenchmarkStats::PrintMoreStats() const {
if (Args()->print_more_stats) {
jxl_stats.Print();
- size_t total_bits = jxl_stats.aux_out.TotalBits();
- size_t compressed_bits = total_compressed_size * kBitsPerByte;
- if (total_bits != compressed_bits) {
- printf("Total layer bits: %" PRIuS " vs total compressed bits: %" PRIuS
- " (%.2f%% accounted for)\n",
- total_bits, compressed_bits, total_bits * 100.0 / compressed_bits);
- }
}
if (Args()->print_distance_percentiles) {
std::vector<float> sorted = distances;
@@ -195,13 +227,12 @@ std::vector<ColumnValue> BenchmarkStats::ComputeColumns(
ComputeSpeed(total_input_pixels, total_time_encode);
const double decompression_speed =
ComputeSpeed(total_input_pixels, total_time_decode);
- // Already weighted, no need to divide by #channels.
- const double rmse = std::sqrt(distance_2 / total_input_pixels);
- const double psnr = total_compressed_size == 0 ? 0.0
- : (distance_2 == 0) ? 99.99
- : (20 * std::log10(1 / rmse));
- const double p_norm = distance_p_norm / total_input_pixels;
- const double bpp_p_norm = p_norm * comp_bpp;
+ const double psnr_avg = psnr / total_input_pixels;
+ const double p_norm_avg = distance_p_norm / total_input_pixels;
+ const double ssimulacra2_avg = ssimulacra2 / total_input_pixels;
+ const double bpp_p_norm = p_norm_avg * comp_bpp;
+
+ const double max_distance_avg = sqrt(max_distance / total_input_pixels);
std::vector<ColumnValue> values(
GetColumnDescriptors(extra_metrics.size()).size());
@@ -212,40 +243,15 @@ std::vector<ColumnValue> BenchmarkStats::ComputeColumns(
values[3].f = comp_bpp;
values[4].f = compression_speed;
values[5].f = decompression_speed;
- values[6].f = static_cast<double>(max_distance);
- values[7].f = p_norm;
- values[8].f = psnr;
- values[9].f = adj_comp_bpp;
- // The DCT2, DCT4, AFV and DCT4X8 are applied to an 8x8 block by having 4x4
- // DCT2X2s, 2x2 DCT4x4s/AFVs, or 2x1 DCT4X8s, filling the whole 8x8 blocks.
- // Thus we need to multiply the block count by 8.0 * 8.0 pixels for these
- // transforms.
- values[10].f = 100.f * jxl_stats.aux_out.num_small_blocks * 8.0 * 8.0 /
- total_input_pixels;
- values[11].f = 100.f * jxl_stats.aux_out.num_dct4x8_blocks * 8.0 * 8.0 /
- total_input_pixels;
- values[12].f =
- 100.f * jxl_stats.aux_out.num_afv_blocks * 8.0 * 8.0 / total_input_pixels;
- values[13].f = 100.f * jxl_stats.aux_out.num_dct8_blocks * 8.0 * 8.0 /
- total_input_pixels;
- values[14].f = 100.f * jxl_stats.aux_out.num_dct8x16_blocks * 8.0 * 16.0 /
- total_input_pixels;
- values[15].f = 100.f * jxl_stats.aux_out.num_dct8x32_blocks * 8.0 * 32.0 /
- total_input_pixels;
- values[16].f = 100.f * jxl_stats.aux_out.num_dct16_blocks * 16.0 * 16.0 /
- total_input_pixels;
- values[17].f = 100.f * jxl_stats.aux_out.num_dct16x32_blocks * 16.0 * 32.0 /
- total_input_pixels;
- values[18].f = 100.f * jxl_stats.aux_out.num_dct32_blocks * 32.0 * 32.0 /
- total_input_pixels;
- values[19].f = 100.f * jxl_stats.aux_out.num_dct32x64_blocks * 32.0 * 64.0 /
- total_input_pixels;
- values[20].f = 100.f * jxl_stats.aux_out.num_dct64_blocks * 64.0 * 64.0 /
- total_input_pixels;
- values[21].f = bpp_p_norm;
- values[22].i = total_errors;
+ values[6].f = static_cast<double>(max_distance_avg);
+ values[7].f = ssimulacra2_avg;
+ values[8].f = psnr_avg;
+ values[9].f = p_norm_avg;
+ values[10].f = bpp_p_norm;
+ values[11].f = adj_comp_bpp;
+ values[12].i = total_errors;
for (size_t i = 0; i < extra_metrics.size(); i++) {
- values[23 + i].f = extra_metrics[i] / total_input_files;
+ values[13 + i].f = extra_metrics[i] / total_input_files;
}
return values;
}
@@ -373,4 +379,5 @@ std::string PrintAggregate(
return PrintFormattedEntries(num_extra_metrics, result);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/benchmark/benchmark_stats.h b/tools/benchmark/benchmark_stats.h
index a23c4a1..deca72a 100644
--- a/tools/benchmark/benchmark_stats.h
+++ b/tools/benchmark/benchmark_stats.h
@@ -6,31 +6,30 @@
#ifndef TOOLS_BENCHMARK_BENCHMARK_STATS_H_
#define TOOLS_BENCHMARK_BENCHMARK_STATS_H_
+#include <jxl/stats.h>
#include <stddef.h>
#include <stdint.h>
+#include <memory>
#include <string>
#include <vector>
-#include "lib/jxl/aux_out.h"
-
-namespace jxl {
+namespace jpegxl {
+namespace tools {
std::string StringPrintf(const char* format, ...);
struct JxlStats {
- JxlStats() {
- num_inputs = 0;
- aux_out = AuxOut();
- }
+ JxlStats()
+ : num_inputs(0), stats(JxlEncoderStatsCreate(), JxlEncoderStatsDestroy) {}
void Assimilate(const JxlStats& victim) {
num_inputs += victim.num_inputs;
- aux_out.Assimilate(victim.aux_out);
+ JxlEncoderStatsMerge(stats.get(), victim.stats.get());
}
- void Print() const { aux_out.Print(num_inputs); }
+ void Print() const;
size_t num_inputs;
- AuxOut aux_out;
+ std::unique_ptr<JxlEncoderStats, decltype(JxlEncoderStatsDestroy)*> stats;
};
// The value of an entry in the table. Depending on the ColumnType, the string,
@@ -61,8 +60,8 @@ struct BenchmarkStats {
float max_distance = -1.0; // Max butteraugli score
// sum of 8th powers of butteraugli distmap pixels.
double distance_p_norm = 0.0;
- // sum of 2nd powers of differences between R, G, B.
- double distance_2 = 0.0;
+ double psnr = 0.0;
+ double ssimulacra2 = 0.0;
std::vector<float> distances;
size_t total_errors = 0;
JxlStats jxl_stats;
@@ -76,6 +75,7 @@ std::string PrintAggregate(
size_t num_extra_metrics,
const std::vector<std::vector<ColumnValue>>& aggregate);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_STATS_H_
diff --git a/tools/benchmark/benchmark_utils.cc b/tools/benchmark/benchmark_utils.cc
index 4b53131..11753f2 100644
--- a/tools/benchmark/benchmark_utils.cc
+++ b/tools/benchmark/benchmark_utils.cc
@@ -21,13 +21,13 @@
#include <fstream>
-#include "lib/jxl/base/file_io.h"
-#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/image_bundle.h"
+#include "tools/file_io.h"
extern char** environ;
-namespace jxl {
+namespace jpegxl {
+namespace tools {
TemporaryFile::TemporaryFile(std::string basename, std::string extension) {
const auto extension_size = 1 + extension.size();
temp_filename_ = std::move(basename) + "_XXXXXX." + std::move(extension);
@@ -50,8 +50,18 @@ Status TemporaryFile::GetFileName(std::string* const output) const {
return true;
}
+std::string GetBaseName(std::string filename) {
+ std::string result = std::move(filename);
+ result = basename(&result[0]);
+ const size_t dot = result.rfind('.');
+ if (dot != std::string::npos) {
+ result.resize(dot);
+ }
+ return result;
+}
+
Status RunCommand(const std::string& command,
- const std::vector<std::string>& arguments) {
+ const std::vector<std::string>& arguments, bool quiet) {
std::vector<char*> args;
args.reserve(arguments.size() + 2);
args.push_back(const_cast<char*>(command.c_str()));
@@ -60,18 +70,27 @@ Status RunCommand(const std::string& command,
}
args.push_back(nullptr);
pid_t pid;
- JXL_RETURN_IF_ERROR(posix_spawnp(&pid, command.c_str(), nullptr, nullptr,
- args.data(), environ) == 0);
+ posix_spawn_file_actions_t file_actions;
+ posix_spawn_file_actions_init(&file_actions);
+ if (quiet) {
+ posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO);
+ posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO);
+ }
+ JXL_RETURN_IF_ERROR(posix_spawnp(&pid, command.c_str(), &file_actions,
+ nullptr, args.data(), environ) == 0);
int wstatus;
waitpid(pid, &wstatus, 0);
+ posix_spawn_file_actions_destroy(&file_actions);
return WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == EXIT_SUCCESS;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#else
-namespace jxl {
+namespace jpegxl {
+namespace tools {
TemporaryFile::TemporaryFile(std::string basename, std::string extension) {}
TemporaryFile::~TemporaryFile() {}
@@ -80,11 +99,14 @@ Status TemporaryFile::GetFileName(std::string* const output) const {
return JXL_FAILURE("Not supported on this build");
}
+std::string GetBaseName(std::string filename) { return filename; }
+
Status RunCommand(const std::string& command,
- const std::vector<std::string>& arguments) {
+ const std::vector<std::string>& arguments, bool quiet) {
return JXL_FAILURE("Not supported on this build");
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // _MSC_VER
diff --git a/tools/benchmark/benchmark_utils.h b/tools/benchmark/benchmark_utils.h
index 027fa08..5df2bec 100644
--- a/tools/benchmark/benchmark_utils.h
+++ b/tools/benchmark/benchmark_utils.h
@@ -11,7 +11,10 @@
#include "lib/jxl/base/status.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::Status;
class TemporaryFile final {
public:
@@ -27,9 +30,13 @@ class TemporaryFile final {
std::string temp_filename_;
};
+std::string GetBaseName(std::string filename);
+
Status RunCommand(const std::string& command,
- const std::vector<std::string>& arguments);
+ const std::vector<std::string>& arguments,
+ bool quiet = false);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_BENCHMARK_BENCHMARK_UTILS_H_
diff --git a/tools/benchmark/benchmark_xl.cc b/tools/benchmark/benchmark_xl.cc
index fed5e9b..86d06a3 100644
--- a/tools/benchmark/benchmark_xl.cc
+++ b/tools/benchmark/benchmark_xl.cc
@@ -3,13 +3,15 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-#include <math.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
+#include <jxl/cms.h>
+#include <jxl/decode.h>
#include <algorithm>
+#include <cmath>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
#include <memory>
#include <mutex>
#include <numeric>
@@ -17,27 +19,22 @@
#include <utility>
#include <vector>
-#include "jxl/decode.h"
#include "lib/extras/codec.h"
#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/enc/apng.h"
+#include "lib/extras/metrics.h"
#include "lib/extras/time.h"
#include "lib/jxl/alpha.h"
-#include "lib/jxl/base/cache_aligned.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/file_io.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/printf_macros.h"
-#include "lib/jxl/base/profiler.h"
#include "lib/jxl/base/random.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/cache_aligned.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/enc_butteraugli_comparator.h"
-#include "lib/jxl/enc_butteraugli_pnorm.h"
-#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
#include "lib/jxl/image_ops.h"
@@ -48,32 +45,65 @@
#include "tools/benchmark/benchmark_stats.h"
#include "tools/benchmark/benchmark_utils.h"
#include "tools/codec_config.h"
+#include "tools/file_io.h"
#include "tools/speed_stats.h"
+#include "tools/ssimulacra2.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
namespace {
+using ::jxl::ButteraugliParams;
+using ::jxl::Bytes;
+using ::jxl::CodecInOut;
+using ::jxl::ColorEncoding;
+using ::jxl::Image3F;
+using ::jxl::ImageBundle;
+using ::jxl::ImageF;
+using ::jxl::Rng;
+using ::jxl::Status;
+using ::jxl::ThreadPool;
+
Status WriteImage(Image3F&& image, ThreadPool* pool,
const std::string& filename) {
CodecInOut io;
io.metadata.m.SetUintSamples(8);
io.metadata.m.color_encoding = ColorEncoding::SRGB();
io.SetFromImage(std::move(image), io.metadata.m.color_encoding);
- return EncodeToFile(io, filename, pool);
+ std::vector<uint8_t> encoded;
+ return Encode(io, filename, &encoded, pool) && WriteFile(filename, encoded);
}
Status ReadPNG(const std::string& filename, Image3F* image) {
CodecInOut io;
- JXL_CHECK(SetFromFile(filename, extras::ColorHints(), &io));
- *image = CopyImage(*io.Main().color());
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(ReadFile(filename, &encoded));
+ JXL_CHECK(
+ jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(), &io));
+ *image = Image3F(io.xsize(), io.ysize());
+ CopyImageTo(*io.Main().color(), image);
return true;
}
+std::string CodecToExtension(std::string codec_name, char sep) {
+ std::string result;
+ // Add in the parameters of the codec_name in reverse order, so that the
+ // name of the file format (e.g. jxl) is last.
+ int pos = static_cast<int>(codec_name.size()) - 1;
+ while (pos > 0) {
+ int prev = codec_name.find_last_of(sep, pos);
+ if (prev > pos) prev = -1;
+ result += '.' + codec_name.substr(prev + 1, pos - prev);
+ pos = prev - 1;
+ }
+ return result;
+}
+
void DoCompress(const std::string& filename, const CodecInOut& io,
const std::vector<std::string>& extra_metrics_commands,
- ImageCodec* codec, ThreadPoolInternal* inner_pool,
+ ImageCodec* codec, ThreadPool* inner_pool,
std::vector<uint8_t>* compressed, BenchmarkStats* s) {
- PROFILER_FUNC;
++s->total_input_files;
if (io.frames.size() != 1) {
@@ -104,7 +134,7 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
if (valid && !Args()->decode_only) {
for (size_t i = 0; i < Args()->encode_reps; ++i) {
if (codec->CanRecompressJpeg() && (ext == ".jpg" || ext == ".jpeg")) {
- std::string data_in;
+ std::vector<uint8_t> data_in;
JXL_CHECK(ReadFile(filename, &data_in));
JXL_CHECK(
codec->RecompressJpeg(filename, data_in, compressed, &speed_stats));
@@ -142,8 +172,8 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
if (valid) {
speed_stats = jpegxl::tools::SpeedStats();
for (size_t i = 0; i < Args()->decode_reps; ++i) {
- if (!codec->Decompress(filename, Span<const uint8_t>(*compressed),
- inner_pool, &io2, &speed_stats)) {
+ if (!codec->Decompress(filename, Bytes(*compressed), inner_pool, &io2,
+ &speed_stats)) {
if (!Args()->silent_errors) {
fprintf(stderr,
"%s failed to decompress encoded image. Original source:"
@@ -152,10 +182,13 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
}
valid = false;
}
-
- // io2.dec_pixels increases each time, but the total should be independent
- // of decode_reps, so only take the value from the first iteration.
- if (i == 0) s->total_input_pixels += io2.dec_pixels;
+ // TODO(veluca): this is a hack. codec->Decompress should set the bitdepth
+ // correctly, but for jxl it currently sets it from the pixel format (i.e.
+ // 32-bit float).
+ io2.metadata.m.bit_depth = io.metadata.m.bit_depth;
+ }
+ for (const auto& frame : io2.frames) {
+ s->total_input_pixels += frame.color().xsize() * frame.color().ysize();
}
JXL_CHECK(speed_stats.GetSummary(&summary));
s->total_time_decode += summary.central_tendency;
@@ -180,9 +213,7 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
valid = false;
}
- bool lossless = codec->IsJpegTranscoder();
- bool skip_butteraugli =
- Args()->skip_butteraugli || Args()->decode_only || lossless;
+ bool skip_butteraugli = Args()->skip_butteraugli || Args()->decode_only;
ImageF distmap;
float max_distance = 1.0f;
@@ -193,10 +224,9 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
ImageBundle& ib2 = io2.frames[i];
// Verify output
- PROFILER_ZONE("Benchmark stats");
float distance;
if (SameSize(ib1, ib2)) {
- ButteraugliParams params = codec->BaParams();
+ ButteraugliParams params;
if (ib1.metadata()->IntensityTarget() !=
ib2.metadata()->IntensityTarget()) {
fprintf(stderr,
@@ -210,21 +240,24 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
if (fabs(params.intensity_target - 255.0f) < 1e-3) {
params.intensity_target = 80.0;
}
- distance = ButteraugliDistance(ib1, ib2, params, GetJxlCms(), &distmap,
- inner_pool);
- // Ensure pixels in range 0-1
- s->distance_2 += ComputeDistance2(ib1, ib2, GetJxlCms());
+ distance =
+ ButteraugliDistance(ib1, ib2, params, *JxlGetDefaultCms(), &distmap,
+ inner_pool, codec->IgnoreAlpha());
} else {
// TODO(veluca): re-upsample and compute proper distance.
distance = 1e+4f;
distmap = ImageF(1, 1);
distmap.Row(0)[0] = distance;
- s->distance_2 += distance;
}
// Update stats
+ s->psnr +=
+ compressed->empty()
+ ? 0
+ : jxl::ComputePSNR(ib1, ib2, *JxlGetDefaultCms()) * input_pixels;
s->distance_p_norm +=
- ComputeDistanceP(distmap, Args()->ba_params, Args()->error_pnorm) *
+ ComputeDistanceP(distmap, ButteraugliParams(), Args()->error_pnorm) *
input_pixels;
+ s->ssimulacra2 += ComputeSSIMULACRA2(ib1, ib2).Score() * input_pixels;
s->max_distance = std::max(s->max_distance, distance);
s->distances.push_back(distance);
max_distance = std::max(max_distance, distance);
@@ -265,48 +298,44 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
std::string dir = FileDirName(filename);
std::string outdir =
Args()->output_dir.empty() ? dir + "/out" : Args()->output_dir;
- std::string compressed_fn = outdir + "/" + name;
- // Add in the parameters of the codec_name in reverse order, so that the
- // name of the file format (e.g. jxl) is last.
- int pos = static_cast<int>(codec_name.size()) - 1;
- while (pos > 0) {
- int prev = codec_name.find_last_of(':', pos);
- if (prev > pos) prev = -1;
- compressed_fn += '.' + codec_name.substr(prev + 1, pos - prev);
- pos = prev - 1;
- }
+ std::string compressed_fn =
+ outdir + "/" + name + CodecToExtension(codec_name, ':');
std::string decompressed_fn = compressed_fn + Args()->output_extension;
-#if JPEGXL_ENABLE_APNG
- std::string heatmap_fn = compressed_fn + ".heatmap.png";
-#else
- std::string heatmap_fn = compressed_fn + ".heatmap.ppm";
-#endif
+ std::string heatmap_fn;
+ if (jxl::extras::GetAPNGEncoder()) {
+ heatmap_fn = compressed_fn + ".heatmap.png";
+ } else {
+ heatmap_fn = compressed_fn + ".heatmap.ppm";
+ }
JXL_CHECK(MakeDir(outdir));
if (Args()->save_compressed) {
- std::string compressed_str(
- reinterpret_cast<const char*>(compressed->data()),
- compressed->size());
- JXL_CHECK(WriteFile(compressed_str, compressed_fn));
+ JXL_CHECK(WriteFile(compressed_fn, *compressed));
}
if (Args()->save_decompressed && valid) {
// For verifying HDR: scale output.
if (Args()->mul_output != 0.0) {
fprintf(stderr, "WARNING: scaling outputs by %f\n", Args()->mul_output);
JXL_CHECK(ib2.TransformTo(ColorEncoding::LinearSRGB(ib2.IsGray()),
- GetJxlCms(), inner_pool));
+ *JxlGetDefaultCms(), inner_pool));
ScaleImage(static_cast<float>(Args()->mul_output), ib2.color());
}
- JXL_CHECK(EncodeToFile(io2, *c_desired,
- ib2.metadata()->bit_depth.bits_per_sample,
- decompressed_fn));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(Encode(io2, *c_desired,
+ ib2.metadata()->bit_depth.bits_per_sample,
+ decompressed_fn, &encoded));
+ JXL_CHECK(WriteFile(decompressed_fn, encoded));
if (!skip_butteraugli) {
- float good = Args()->heatmap_good > 0.0f ? Args()->heatmap_good
- : ButteraugliFuzzyInverse(1.5);
- float bad = Args()->heatmap_bad > 0.0f ? Args()->heatmap_bad
- : ButteraugliFuzzyInverse(0.5);
- JXL_CHECK(WriteImage(CreateHeatMapImage(distmap, good, bad), inner_pool,
- heatmap_fn));
+ float good = Args()->heatmap_good > 0.0f
+ ? Args()->heatmap_good
+ : jxl::ButteraugliFuzzyInverse(1.5);
+ float bad = Args()->heatmap_bad > 0.0f
+ ? Args()->heatmap_bad
+ : jxl::ButteraugliFuzzyInverse(0.5);
+ if (Args()->save_heatmap) {
+ JXL_CHECK(WriteImage(CreateHeatMapImage(distmap, good, bad),
+ inner_pool, heatmap_fn));
+ }
}
}
}
@@ -324,10 +353,13 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
// Convert everything to non-linear SRGB - this is what most metrics expect.
const ColorEncoding& c_desired = ColorEncoding::SRGB(io.Main().IsGray());
- JXL_CHECK(EncodeToFile(io, c_desired,
- io.metadata.m.bit_depth.bits_per_sample, tmp_in_fn));
- JXL_CHECK(EncodeToFile(
- io2, c_desired, io.metadata.m.bit_depth.bits_per_sample, tmp_out_fn));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(Encode(io, c_desired, io.metadata.m.bit_depth.bits_per_sample,
+ tmp_in_fn, &encoded));
+ JXL_CHECK(WriteFile(tmp_in_fn, encoded));
+ JXL_CHECK(Encode(io2, c_desired, io.metadata.m.bit_depth.bits_per_sample,
+ tmp_out_fn, &encoded));
+ JXL_CHECK(WriteFile(tmp_out_fn, encoded));
if (io.metadata.m.IntensityTarget() != io2.metadata.m.IntensityTarget()) {
fprintf(stderr,
"WARNING: original and decoded have different intensity targets "
@@ -371,7 +403,7 @@ void DoCompress(const std::string& filename, const CodecInOut& io,
// Makes a base64 data URI for embedded image in HTML
std::string Base64Image(const std::string& filename) {
- PaddedBytes bytes;
+ std::vector<uint8_t> bytes;
if (!ReadFile(filename, &bytes)) {
return "";
}
@@ -406,12 +438,13 @@ void WriteHtmlReport(const std::string& codec_desc,
const std::vector<std::string>& fnames,
const std::vector<const Task*>& tasks,
const std::vector<const CodecInOut*>& images,
- bool self_contained) {
+ bool add_heatmap, bool self_contained) {
std::string toggle_js =
"<script type=\"text/javascript\">\n"
" var codecname = '" +
codec_desc + "';\n";
- toggle_js += R"(
+ if (add_heatmap) {
+ toggle_js += R"(
var maintitle = codecname + ' - click images to toggle, press space to' +
' toggle all, h to toggle all heatmaps. Zoom in with CTRL+wheel or' +
' CTRL+plus.';
@@ -435,7 +468,7 @@ void WriteHtmlReport(const std::string& codec_desc,
hm.style.display = 'block';
}
}
- function toggle3(i) {
+ function toggle(i) {
for (index = counter.length; index <= i; index++) {
counter.push(1);
}
@@ -460,6 +493,48 @@ void WriteHtmlReport(const std::string& codec_desc,
};
</script>
)";
+ } else {
+ toggle_js += R"(
+ var maintitle = codecname + ' - click images to toggle, press space to' +
+ ' toggle all. Zoom in with CTRL+wheel or CTRL+plus.';
+ document.title = maintitle;
+ var counter = [];
+ function setState(i, s) {
+ var preview = document.getElementById("preview" + i);
+ var orig = document.getElementById("orig" + i);
+ if (s == 0) {
+ preview.style.display = 'none';
+ orig.style.display = 'block';
+ } else if (s == 1) {
+ preview.style.display = 'block';
+ orig.style.display = 'none';
+ }
+ }
+ function toggle(i) {
+ for (index = counter.length; index <= i; index++) {
+ counter.push(1);
+ }
+ setState(i, counter[i]);
+ counter[i] = 1 - counter[i];
+ document.title = maintitle;
+ }
+ var toggleall_state = 1;
+ document.body.onkeydown = function(e) {
+ // space (32) to toggle orig/compr
+ if (e.keyCode == 32) {
+ var divs = document.getElementsByTagName('div');
+ toggleall_state = 1 - toggleall_state;
+ document.title = codecname + ' - ' + (toggleall_state == 0 ?
+ 'originals' : 'compressed');
+ for (var i = 0; i < divs.length; i++) {
+ setState(i, toggleall_state);
+ }
+ return false;
+ }
+ };
+</script>
+)";
+ }
std::string out_html;
std::string outdir;
out_html += "<body bgcolor=\"#000\">\n";
@@ -471,8 +546,12 @@ void WriteHtmlReport(const std::string& codec_desc,
std::string name = FileBaseName(fnames[i]);
std::string dir = FileDirName(fnames[i]);
outdir = Args()->output_dir.empty() ? dir + "/out" : Args()->output_dir;
- std::string name_out = name + "." + codec_name + Args()->output_extension;
- std::string heatmap_out = name + "." + codec_name + ".heatmap.png";
+ std::string name_out = name + CodecToExtension(codec_name, '_');
+ if (Args()->html_report_use_decompressed) {
+ name_out += Args()->output_extension;
+ }
+ std::string heatmap_out =
+ name + CodecToExtension(codec_name, '_') + ".heatmap.png";
std::string fname_orig = fnames[i];
std::string fname_out = outdir + "/" + name_out;
@@ -500,28 +579,24 @@ void WriteHtmlReport(const std::string& codec_desc,
double max_dist = tasks[i]->stats.max_distance;
std::string compressed_title = StringPrintf(
"compressed. bpp: %f, pnorm: %f, max dist: %f", bpp, pnorm, max_dist);
- out_html += "<div onclick=\"toggle3(" + number +
+ out_html += "<div onclick=\"toggle(" + number +
");\" style=\"display:inline-block;width:" + html_width +
";height:" + html_height +
";\">\n"
" <img title=\"" +
compressed_title + "\" id=\"preview" + number + "\" src=";
- out_html += "\"" + url_out + "\"";
- out_html +=
- " style=\"display:block;\"/>\n"
- " <img title=\"original\" id=\"orig" +
- number + "\" src=";
- out_html += "\"" + url_orig + "\"";
- out_html +=
- " style=\"display:none;\"/>\n"
- " <img title=\"heatmap\" id=\"hm" +
- number + "\" src=";
- out_html += "\"" + url_heatmap + "\"";
- out_html += " style=\"display:none;\"/>\n</div>\n";
+ out_html += "\"" + url_out + "\"style=\"display:block;\"/>\n";
+ out_html += " <img title=\"original\" id=\"orig" + number + "\" src=";
+ out_html += "\"" + url_orig + "\"style=\"display:none;\"/>\n";
+ if (add_heatmap) {
+ out_html = " <img title=\"heatmap\" id=\"hm" + number + "\" src=";
+ out_html += "\"" + url_heatmap + "\"style=\"display:none;\"/>\n";
+ }
+ out_html += "</div>\n";
}
out_html += "</body>\n";
out_html += toggle_js;
- JXL_CHECK(WriteFile(out_html, outdir + "/index." + codec_name + ".html"));
+ JXL_CHECK(WriteFile(outdir + "/index." + codec_name + ".html", out_html));
}
// Prints the detailed and aggregate statistics, in the correct order but as
@@ -552,7 +627,6 @@ struct StatPrinter {
}
void TaskDone(size_t task_index, const Task& t) {
- PROFILER_FUNC;
std::lock_guard<std::mutex> guard(mutex);
tasks_done_++;
if (Args()->print_details || Args()->show_progress) {
@@ -603,17 +677,13 @@ struct StatPrinter {
double comp_bpp =
t.stats.total_compressed_size * 8.0 / t.stats.total_input_pixels;
double p_norm = t.stats.distance_p_norm / t.stats.total_input_pixels;
+ double psnr = t.stats.psnr / t.stats.total_input_pixels;
+ double ssimulacra2 = t.stats.ssimulacra2 / t.stats.total_input_pixels;
double bpp_p_norm = p_norm * comp_bpp;
const double adj_comp_bpp =
t.stats.total_adj_compressed_size * 8.0 / t.stats.total_input_pixels;
- const double rmse =
- std::sqrt(t.stats.distance_2 / t.stats.total_input_pixels);
- const double psnr = t.stats.total_compressed_size == 0 ? 0.0
- : (t.stats.distance_2 == 0)
- ? 99.99
- : (20 * std::log10(1 / rmse));
size_t pixels = t.stats.total_input_pixels;
const double enc_mps =
@@ -646,10 +716,11 @@ struct StatPrinter {
printf(
"error:%" PRIdS " size:%8" PRIdS " pixels:%9" PRIdS
" enc_speed:%8.8f dec_speed:%8.8f bpp:%10.8f dist:%10.8f"
- " psnr:%10.8f p:%10.8f bppp:%10.8f qabpp:%10.8f ",
+ " psnr:%10.8f ssimulacra2:%.2f p:%10.8f bppp:%10.8f "
+ "qabpp:%10.8f ",
t.stats.total_errors, t.stats.total_compressed_size, pixels, enc_mps,
- dec_mps, comp_bpp, t.stats.max_distance, psnr, p_norm, bpp_p_norm,
- adj_comp_bpp);
+ dec_mps, comp_bpp, t.stats.max_distance, psnr, ssimulacra2, p_norm,
+ bpp_p_norm, adj_comp_bpp);
for (size_t i = 0; i < t.stats.extra_metrics.size(); i++) {
printf(" %s:%.8f", (*extra_metrics_names_)[i].c_str(),
t.stats.extra_metrics[i]);
@@ -660,7 +731,6 @@ struct StatPrinter {
}
void PrintStats(const std::string& method, size_t idx_method) {
- PROFILER_FUNC;
// Assimilate all tasks with the same idx_method.
BenchmarkStats method_stats;
std::vector<const CodecInOut*> images;
@@ -680,6 +750,7 @@ struct StatPrinter {
if (Args()->write_html_report) {
WriteHtmlReport(method, *fnames_, tasks, images,
+ Args()->save_heatmap && Args()->html_report_add_heatmap,
Args()->html_report_self_contained);
}
@@ -741,27 +812,21 @@ class Benchmark {
static int Run() {
int ret = EXIT_SUCCESS;
{
- PROFILER_FUNC;
-
const StringVec methods = GetMethods();
const StringVec extra_metrics_names = GetExtraMetricsNames();
const StringVec extra_metrics_commands = GetExtraMetricsCommands();
const StringVec fnames = GetFilenames();
- bool all_color_aware;
- bool jpeg_transcoding_requested;
// (non-const because Task.stats are updated)
- std::vector<Task> tasks = CreateTasks(methods, fnames, &all_color_aware,
- &jpeg_transcoding_requested);
+ std::vector<Task> tasks = CreateTasks(methods, fnames);
std::unique_ptr<ThreadPoolInternal> pool;
std::vector<std::unique_ptr<ThreadPoolInternal>> inner_pools;
- InitThreads(static_cast<int>(tasks.size()), &pool, &inner_pools);
+ InitThreads(tasks.size(), &pool, &inner_pools);
- const std::vector<CodecInOut> loaded_images = LoadImages(
- fnames, all_color_aware, jpeg_transcoding_requested, pool.get());
+ const std::vector<CodecInOut> loaded_images = LoadImages(fnames, &*pool);
if (RunTasks(methods, extra_metrics_names, extra_metrics_commands, fnames,
- loaded_images, pool.get(), inner_pools, &tasks) != 0) {
+ loaded_images, &*pool, inner_pools, &tasks) != 0) {
ret = EXIT_FAILURE;
if (!Args()->silent_errors) {
fprintf(stderr, "There were error(s) in the benchmark.\n");
@@ -769,24 +834,23 @@ class Benchmark {
}
}
- // Must have exited profiler zone above before calling.
- if (Args()->profiler) {
- PROFILER_PRINT_RESULTS();
- }
- CacheAligned::PrintStats();
+ jxl::CacheAligned::PrintStats();
return ret;
}
private:
- static int NumOuterThreads(const int num_hw_threads, const int num_tasks) {
- int num_threads = Args()->num_threads;
+ static size_t NumOuterThreads(const size_t num_hw_threads,
+ const size_t num_tasks) {
// Default to #cores
- if (num_threads < 0) num_threads = num_hw_threads;
+ size_t num_threads = num_hw_threads;
+ if (Args()->num_threads >= 0) {
+ num_threads = static_cast<size_t>(Args()->num_threads);
+ }
// As a safety precaution, limit the number of threads to 4x the number of
// available CPUs.
num_threads =
- std::min<int>(num_threads, 4 * std::thread::hardware_concurrency());
+ std::min<size_t>(num_threads, 4 * std::thread::hardware_concurrency());
// Don't create more threads than there are tasks (pointless/wasteful).
num_threads = std::min(num_threads, num_tasks);
@@ -797,14 +861,21 @@ class Benchmark {
return num_threads;
}
- static int NumInnerThreads(const int num_hw_threads, const int num_threads) {
- int num_inner = Args()->inner_threads;
+ static int NumInnerThreads(const size_t num_hw_threads,
+ const size_t num_threads) {
+ size_t num_inner;
// Default: distribute remaining cores among tasks.
- if (num_inner < 0) {
- const int cores_for_outer = num_hw_threads - num_threads;
- num_inner =
- num_threads == 0 ? num_hw_threads : cores_for_outer / num_threads;
+ if (Args()->inner_threads < 0) {
+ if (num_threads == 0) {
+ num_inner = num_hw_threads;
+ } else if (num_hw_threads <= num_threads) {
+ num_inner = 1;
+ } else {
+ num_inner = (num_hw_threads - num_threads) / num_threads;
+ }
+ } else {
+ num_inner = static_cast<size_t>(Args()->inner_threads);
}
// Just one thread is counterproductive.
@@ -814,20 +885,21 @@ class Benchmark {
}
static void InitThreads(
- const int num_tasks, std::unique_ptr<ThreadPoolInternal>* pool,
+ size_t num_tasks, std::unique_ptr<ThreadPoolInternal>* pool,
std::vector<std::unique_ptr<ThreadPoolInternal>>* inner_pools) {
- const int num_hw_threads = std::thread::hardware_concurrency();
- const int num_threads = NumOuterThreads(num_hw_threads, num_tasks);
- const int num_inner = NumInnerThreads(num_hw_threads, num_threads);
+ const size_t num_hw_threads = std::thread::hardware_concurrency();
+ const size_t num_threads = NumOuterThreads(num_hw_threads, num_tasks);
+ const size_t num_inner = NumInnerThreads(num_hw_threads, num_threads);
fprintf(stderr,
- "%d total threads, %d tasks, %d threads, %d inner threads\n",
+ "%" PRIuS " total threads, %" PRIuS " tasks, %" PRIuS
+ " threads, %" PRIuS " inner threads\n",
num_hw_threads, num_tasks, num_threads, num_inner);
pool->reset(new ThreadPoolInternal(num_threads));
// Main thread OR worker threads in pool each get a possibly empty nested
// pool (helps use all available cores when #tasks < #threads)
- for (size_t i = 0; i < (*pool)->NumThreads(); ++i) {
+ for (size_t i = 0; i < std::max<size_t>(num_threads, 1); ++i) {
inner_pools->emplace_back(new ThreadPoolInternal(num_inner));
}
}
@@ -938,76 +1010,55 @@ class Benchmark {
}
// (Load only once, not for every codec)
- static std::vector<CodecInOut> LoadImages(
- const StringVec& fnames, const bool all_color_aware,
- const bool jpeg_transcoding_requested, ThreadPool* pool) {
- PROFILER_FUNC;
+ static std::vector<CodecInOut> LoadImages(const StringVec& fnames,
+ ThreadPool* pool) {
std::vector<CodecInOut> loaded_images;
loaded_images.resize(fnames.size());
- JXL_CHECK(RunOnPool(
- pool, 0, static_cast<uint32_t>(fnames.size()), ThreadPool::NoInit,
- [&](const uint32_t task, size_t /*thread*/) {
- const size_t i = static_cast<size_t>(task);
- Status ok = true;
-
- if (!Args()->decode_only) {
- PaddedBytes encoded;
- ok = ReadFile(fnames[i], &encoded) &&
- (jpeg_transcoding_requested
- ? jpeg::DecodeImageJPG(Span<const uint8_t>(encoded),
- &loaded_images[i])
- : SetFromBytes(Span<const uint8_t>(encoded),
- Args()->color_hints, &loaded_images[i]));
- if (ok && Args()->intensity_target != 0) {
- loaded_images[i].metadata.m.SetIntensityTarget(
- Args()->intensity_target);
- }
- }
- if (!ok) {
- if (!Args()->silent_errors) {
- fprintf(stderr, "Failed to load image %s\n", fnames[i].c_str());
- }
- return;
- }
-
- if (!Args()->decode_only && all_color_aware) {
- const bool is_gray = loaded_images[i].Main().IsGray();
- const ColorEncoding& c_desired = ColorEncoding::LinearSRGB(is_gray);
- if (!loaded_images[i].TransformTo(c_desired, GetJxlCms(),
- /*pool=*/nullptr)) {
- JXL_ABORT("Failed to transform to lin. sRGB %s",
- fnames[i].c_str());
- }
- }
+ const auto process_image = [&](const uint32_t task, size_t /*thread*/) {
+ const size_t i = static_cast<size_t>(task);
+ Status ok = true;
+
+ if (!Args()->decode_only) {
+ std::vector<uint8_t> encoded;
+ ok = ReadFile(fnames[i], &encoded);
+ if (ok) {
+ ok = jxl::SetFromBytes(Bytes(encoded), Args()->color_hints,
+ &loaded_images[i]);
+ }
+ if (ok && Args()->intensity_target != 0) {
+ loaded_images[i].metadata.m.SetIntensityTarget(
+ Args()->intensity_target);
+ }
+ }
+ if (!ok) {
+ if (!Args()->silent_errors) {
+ fprintf(stderr, "Failed to load image %s\n", fnames[i].c_str());
+ }
+ return;
+ }
- if (!Args()->decode_only && Args()->override_bitdepth != 0) {
- if (Args()->override_bitdepth == 32) {
- loaded_images[i].metadata.m.SetFloat32Samples();
- } else {
- loaded_images[i].metadata.m.SetUintSamples(
- Args()->override_bitdepth);
- }
- }
- },
- "Load images"));
+ if (!Args()->decode_only && Args()->override_bitdepth != 0) {
+ if (Args()->override_bitdepth == 32) {
+ loaded_images[i].metadata.m.SetFloat32Samples();
+ } else {
+ loaded_images[i].metadata.m.SetUintSamples(Args()->override_bitdepth);
+ }
+ }
+ };
+ JXL_CHECK(jxl::RunOnPool(pool, 0, static_cast<uint32_t>(fnames.size()),
+ ThreadPool::NoInit, process_image, "Load images"));
return loaded_images;
}
static std::vector<Task> CreateTasks(const StringVec& methods,
- const StringVec& fnames,
- bool* all_color_aware,
- bool* jpeg_transcoding_requested) {
+ const StringVec& fnames) {
std::vector<Task> tasks;
tasks.reserve(methods.size() * fnames.size());
- *all_color_aware = true;
- *jpeg_transcoding_requested = false;
for (size_t idx_image = 0; idx_image < fnames.size(); ++idx_image) {
for (size_t idx_method = 0; idx_method < methods.size(); ++idx_method) {
tasks.emplace_back();
Task& t = tasks.back();
t.codec = CreateImageCodec(methods[idx_method]);
- *all_color_aware &= t.codec->IsColorAware();
- *jpeg_transcoding_requested |= t.codec->IsJpegTranscoder();
t.idx_image = idx_image;
t.idx_method = idx_method;
// t.stats is default-initialized.
@@ -1021,10 +1072,9 @@ class Benchmark {
static size_t RunTasks(
const StringVec& methods, const StringVec& extra_metrics_names,
const StringVec& extra_metrics_commands, const StringVec& fnames,
- const std::vector<CodecInOut>& loaded_images, ThreadPoolInternal* pool,
+ const std::vector<CodecInOut>& loaded_images, ThreadPool* pool,
const std::vector<std::unique_ptr<ThreadPoolInternal>>& inner_pools,
std::vector<Task>* tasks) {
- PROFILER_FUNC;
StatPrinter printer(methods, extra_metrics_names, fnames, *tasks);
if (Args()->print_details_csv) {
// Print CSV header
@@ -1038,7 +1088,7 @@ class Benchmark {
}
std::vector<uint64_t> errors_thread;
- JXL_CHECK(RunOnPool(
+ JXL_CHECK(jxl::RunOnPool(
pool, 0, tasks->size(),
[&](const size_t num_threads) {
// Reduce false sharing by only writing every 8th slot (64 bytes).
@@ -1051,14 +1101,15 @@ class Benchmark {
t.image = &image;
std::vector<uint8_t> compressed;
DoCompress(fnames[t.idx_image], image, extra_metrics_commands,
- t.codec.get(), inner_pools[thread].get(), &compressed,
+ t.codec.get(), &*inner_pools[thread], &compressed,
&t.stats);
printer.TaskDone(i, t);
errors_thread[8 * thread] += t.stats.total_errors;
},
"Benchmark tasks"));
if (Args()->show_progress) fprintf(stderr, "\n");
- return std::accumulate(errors_thread.begin(), errors_thread.end(), 0);
+ return std::accumulate(errors_thread.begin(), errors_thread.end(),
+ size_t(0));
}
};
@@ -1085,6 +1136,9 @@ int BenchmarkMain(int argc, const char** argv) {
}
} // namespace
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
-int main(int argc, const char** argv) { return jxl::BenchmarkMain(argc, argv); }
+int main(int argc, const char** argv) {
+ return jpegxl::tools::BenchmarkMain(argc, argv);
+}
diff --git a/tools/box/CMakeLists.txt b/tools/box/CMakeLists.txt
deleted file mode 100644
index c79add0..0000000
--- a/tools/box/CMakeLists.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (c) the JPEG XL Project Authors. All rights reserved.
-#
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-add_library(box STATIC EXCLUDE_FROM_ALL
- box.cc
- box.h
-)
-# This library can be included into position independent binaries.
-set_target_properties(box PROPERTIES POSITION_INDEPENDENT_CODE TRUE)
-target_link_libraries(box
- jxl-static
- jxl_threads-static
-)
-target_include_directories(box
- PRIVATE
- "${PROJECT_SOURCE_DIR}"
-)
-
-if(JPEGXL_ENABLE_DEVTOOLS)
-add_executable(box_list
- box_list_main.cc
-)
-target_link_libraries(box_list
- box
-)
-endif() # JPEGXL_ENABLE_DEVTOOLS
diff --git a/tools/box/box.cc b/tools/box/box.cc
deleted file mode 100644
index db73c7c..0000000
--- a/tools/box/box.cc
+++ /dev/null
@@ -1,285 +0,0 @@
-// Copyright (c) the JPEG XL Project Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-#include "tools/box/box.h"
-
-#include "lib/jxl/base/byte_order.h" // for GetMaximumBrunsliEncodedSize
-#include "lib/jxl/jpeg/dec_jpeg_data.h"
-#include "lib/jxl/jpeg/jpeg_data.h"
-
-namespace jpegxl {
-namespace tools {
-
-namespace {
-// Checks if a + b > size, taking possible integer overflow into account.
-bool OutOfBounds(size_t a, size_t b, size_t size) {
- size_t pos = a + b;
- if (pos > size) return true;
- if (pos < a) return true; // overflow happened
- return false;
-}
-} // namespace
-
-// Parses the header of a BMFF box. Returns the result in a Box struct.
-// Sets the position to the end of the box header after parsing. The data size
-// is output if known, or must be handled by the caller and runs until the end
-// of the container file if not known.
-jxl::Status ParseBoxHeader(const uint8_t** next_in, size_t* available_in,
- Box* box) {
- size_t pos = 0;
- size_t size = *available_in;
- const uint8_t* in = *next_in;
-
- if (OutOfBounds(pos, 8, size)) return JXL_FAILURE("out of bounds");
-
- const size_t initial_pos = pos;
-
- // Total box_size including this header itself.
- uint64_t box_size = LoadBE32(in + pos);
- pos += 4;
- if (box_size == 1) {
- // If the size is 1, it indicates extended size read from 64-bit integer.
- if (OutOfBounds(pos, 8, size)) return JXL_FAILURE("out of bounds");
- box_size = LoadBE64(in + pos);
- pos += 8;
- }
- memcpy(box->type, in + pos, 4);
- pos += 4;
- if (!memcmp("uuid", box->type, 4)) {
- if (OutOfBounds(pos, 16, size)) return JXL_FAILURE("out of bounds");
- memcpy(box->extended_type, in + pos, 16);
- pos += 16;
- }
-
- // This is the end of the box header, the box data begins here. Handle
- // the data size now.
- const size_t data_pos = pos;
- const size_t header_size = data_pos - initial_pos;
-
- if (box_size != 0) {
- if (box_size < header_size) {
- return JXL_FAILURE("invalid box size");
- }
- box->data_size_given = true;
- box->data_size = box_size - header_size;
- } else {
- // The size extends to the end of the file. We don't necessarily know the
- // end of the file here, since the input size may be only part of the full
- // container file. Indicate the size is not given, the caller must handle
- // this.
- box->data_size_given = false;
- box->data_size = 0;
- }
-
- // The remaining bytes are the data. If the box is a full box, the first
- // bytes of the data have a certain structure but this is to be handled by
- // the caller for the appropriate box type.
- *next_in += pos;
- *available_in -= pos;
-
- return true;
-}
-
-jxl::Status AppendBoxHeader(const Box& box, jxl::PaddedBytes* out) {
- bool use_extended = !memcmp("uuid", box.type, 4);
-
- uint64_t box_size = 0;
- bool large_size = false;
- if (box.data_size_given) {
- box_size = box.data_size + 8 + (use_extended ? 16 : 0);
- if (box_size >= 0x100000000ull) {
- large_size = true;
- }
- }
-
- out->resize(out->size() + 4);
- StoreBE32(large_size ? 1 : box_size, &out->back() - 4 + 1);
-
- out->resize(out->size() + 4);
- memcpy(&out->back() - 4 + 1, box.type, 4);
-
- if (large_size) {
- out->resize(out->size() + 8);
- StoreBE64(box_size, &out->back() - 8 + 1);
- }
-
- if (use_extended) {
- out->resize(out->size() + 16);
- memcpy(&out->back() - 16 + 1, box.extended_type, 16);
- }
-
- return true;
-}
-
-bool IsContainerHeader(const uint8_t* data, size_t size) {
- const uint8_t box_header[] = {0, 0, 0, 0xc, 'J', 'X',
- 'L', ' ', 0xd, 0xa, 0x87, 0xa};
- if (size < sizeof(box_header)) return false;
- return memcmp(box_header, data, sizeof(box_header)) == 0;
-}
-
-jxl::Status DecodeJpegXlContainerOneShot(const uint8_t* data, size_t size,
- JpegXlContainer* container) {
- const uint8_t* in = data;
- size_t available_in = size;
-
- container->exif = nullptr;
- container->exif_size = 0;
- container->exfc = nullptr;
- container->exfc_size = 0;
- container->xml.clear();
- container->xmlc.clear();
- container->jumb = nullptr;
- container->jumb_size = 0;
- container->codestream.clear();
- container->jpeg_reconstruction = nullptr;
- container->jpeg_reconstruction_size = 0;
-
- size_t box_index = 0;
-
- while (available_in != 0) {
- Box box;
- if (!ParseBoxHeader(&in, &available_in, &box)) {
- return JXL_FAILURE("Invalid box header");
- }
-
- size_t data_size = box.data_size_given ? box.data_size : available_in;
-
- if (box.data_size > available_in) {
- return JXL_FAILURE("Unexpected end of file");
- }
-
- if (box_index == 0) {
- // TODO(lode): leave out magic signature box?
- // Must be magic signature box.
- if (memcmp("JXL ", box.type, 4) != 0) {
- return JXL_FAILURE("Invalid magic signature");
- }
- if (box.data_size != 4) return JXL_FAILURE("Invalid magic signature");
- if (in[0] != 0xd || in[1] != 0xa || in[2] != 0x87 || in[3] != 0xa) {
- return JXL_FAILURE("Invalid magic signature");
- }
- } else if (box_index == 1) {
- // Must be ftyp box.
- if (memcmp("ftyp", box.type, 4) != 0) {
- return JXL_FAILURE("Invalid ftyp");
- }
- if (box.data_size != 12) return JXL_FAILURE("Invalid ftyp");
- const char* expected = "jxl \0\0\0\0jxl ";
- if (memcmp(expected, in, 12) != 0) return JXL_FAILURE("Invalid ftyp");
- } else if (!memcmp("jxli", box.type, 4)) {
- // TODO(lode): parse JXL frame index box
- if (!container->codestream.empty()) {
- return JXL_FAILURE("frame index must come before codestream");
- }
- } else if (!memcmp("jxlc", box.type, 4)) {
- container->codestream.append(in, in + data_size);
- } else if (!memcmp("jxlp", box.type, 4)) {
- if (data_size < 4) return JXL_FAILURE("Invalid jxlp");
- // TODO(jon): don't just ignore the counter
- container->codestream.append(in + 4, in + data_size);
- } else if (!memcmp("Exif", box.type, 4)) {
- if (data_size < 4) return JXL_FAILURE("Invalid Exif");
- uint32_t tiff_header_offset = LoadBE32(in);
- if (tiff_header_offset > data_size - 4)
- return JXL_FAILURE("Invalid Exif tiff header offset");
- container->exif = in + 4 + tiff_header_offset;
- container->exif_size = data_size - 4 - tiff_header_offset;
- } else if (!memcmp("Exfc", box.type, 4)) {
- container->exfc = in;
- container->exfc_size = data_size;
- } else if (!memcmp("xml ", box.type, 4)) {
- container->xml.emplace_back(in, data_size);
- } else if (!memcmp("xmlc", box.type, 4)) {
- container->xmlc.emplace_back(in, data_size);
- } else if (!memcmp("jumb", box.type, 4)) {
- container->jumb = in;
- container->jumb_size = data_size;
- } else if (!memcmp("jbrd", box.type, 4)) {
- container->jpeg_reconstruction = in;
- container->jpeg_reconstruction_size = data_size;
- } else {
- // Do nothing: box not recognized here but may be recognizable by
- // other software.
- }
-
- in += data_size;
- available_in -= data_size;
- box_index++;
- }
-
- return true;
-}
-
-static jxl::Status AppendBoxAndData(const char type[4], const uint8_t* data,
- size_t data_size, jxl::PaddedBytes* out,
- bool exif = false) {
- Box box;
- memcpy(box.type, type, 4);
- box.data_size = data_size + (exif ? 4 : 0);
- box.data_size_given = true;
- JXL_RETURN_IF_ERROR(AppendBoxHeader(box, out));
- // for Exif: always use tiff header offset 0
- if (exif)
- for (int i = 0; i < 4; i++) out->push_back(0);
- out->append(data, data + data_size);
- return true;
-}
-
-jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container,
- jxl::PaddedBytes* out) {
- const unsigned char header[] = {0, 0, 0, 0xc, 'J', 'X', 'L', ' ',
- 0xd, 0xa, 0x87, 0xa, 0, 0, 0, 0x14,
- 'f', 't', 'y', 'p', 'j', 'x', 'l', ' ',
- 0, 0, 0, 0, 'j', 'x', 'l', ' '};
- size_t header_size = sizeof(header);
- out->append(header, header + header_size);
-
- if (container.exif) {
- JXL_RETURN_IF_ERROR(AppendBoxAndData("Exif", container.exif,
- container.exif_size, out, true));
- }
-
- if (container.exfc) {
- JXL_RETURN_IF_ERROR(
- AppendBoxAndData("Exfc", container.exfc, container.exfc_size, out));
- }
-
- for (size_t i = 0; i < container.xml.size(); i++) {
- JXL_RETURN_IF_ERROR(AppendBoxAndData("xml ", container.xml[i].first,
- container.xml[i].second, out));
- }
-
- for (size_t i = 0; i < container.xmlc.size(); i++) {
- JXL_RETURN_IF_ERROR(AppendBoxAndData("xmlc", container.xmlc[i].first,
- container.xmlc[i].second, out));
- }
-
- if (container.jpeg_reconstruction) {
- JXL_RETURN_IF_ERROR(AppendBoxAndData("jbrd", container.jpeg_reconstruction,
- container.jpeg_reconstruction_size,
- out));
- }
-
- if (!container.codestream.empty()) {
- JXL_RETURN_IF_ERROR(AppendBoxAndData("jxlc", container.codestream.data(),
- container.codestream.size(), out));
- } else {
- return JXL_FAILURE("must have primary image frame");
- }
-
- if (container.jumb) {
- JXL_RETURN_IF_ERROR(
- AppendBoxAndData("jumb", container.jumb, container.jumb_size, out));
- }
-
- return true;
-}
-
-// TODO(veluca): the format defined here encode some things multiple times. Fix
-// that.
-
-} // namespace tools
-} // namespace jpegxl
diff --git a/tools/box/box.h b/tools/box/box.h
deleted file mode 100644
index 4cc3058..0000000
--- a/tools/box/box.h
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (c) the JPEG XL Project Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Tools for reading from / writing to ISOBMFF format for JPEG XL.
-
-#ifndef TOOLS_BOX_BOX_H_
-#define TOOLS_BOX_BOX_H_
-
-#include <string>
-#include <vector>
-
-#include "lib/jxl/base/padded_bytes.h"
-#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h"
-#include "lib/jxl/enc_file.h"
-
-namespace jpegxl {
-namespace tools {
-
-// A top-level box in the box format.
-struct Box {
- // The type of the box.
- // If "uuid", use extended_type instead
- char type[4];
-
- // The extended_type is only used when type == "uuid".
- // Extended types are not used in JXL. However, the box format itself
- // supports this so they are handled correctly.
- char extended_type[16];
-
- // Size of the data, excluding box header. The box ends, and next box
- // begins, at data + size. May not be used if data_size_given is false.
- uint64_t data_size;
-
- // If the size is not given, the datasize extends to the end of the file.
- // If this field is false, the size field may not be used.
- bool data_size_given;
-};
-
-// Parses the header of a BMFF box. Returns the result in a Box struct.
-// Updates next_in and available_in to point at the data in the box, directly
-// after the header.
-// Sets the data_size if known, or must be handled by the caller and runs until
-// the end of the container file if not known.
-// NOTE: available_in should be at least 8 up to 32 bytes to parse the
-// header without error.
-jxl::Status ParseBoxHeader(const uint8_t** next_in, size_t* available_in,
- Box* box);
-
-// TODO(lode): streaming C API
-jxl::Status AppendBoxHeader(const Box& box, jxl::PaddedBytes* out);
-
-// NOTE: after DecodeJpegXlContainerOneShot, the exif etc. pointers point to
-// regions within the input data passed to that function.
-struct JpegXlContainer {
- // Exif metadata, or null if not present in the container.
- // The exif data has the format of 'Exif block' as defined in
- // ISO/IEC23008-12:2017 Clause A.2.1
- // Here we assume the tiff header offset is 0 and store only the
- // actual Exif data (starting with the tiff header MM or II)
- // TODO(lode): support the theoretical case of multiple exif boxes
- const uint8_t* exif = nullptr; // Not owned
- size_t exif_size = 0;
-
- // Brotli-compressed exif metadata, if present. The data points to the brotli
- // compressed stream, it is not decompressed here.
- const uint8_t* exfc = nullptr; // Not owned
- size_t exfc_size = 0;
-
- // XML boxes for XMP. There may be multiple XML boxes.
- // Each entry points to XML location and provides size.
- // The memory is not owned.
- // TODO(lode): for C API, cannot use std::vector.
- std::vector<std::pair<const uint8_t*, size_t>> xml;
-
- // Brotli-compressed xml boxes. The bytes are given in brotli-compressed form
- // and are not decompressed here.
- std::vector<std::pair<const uint8_t*, size_t>> xmlc;
-
- // JUMBF superbox data, or null if not present in the container.
- // The parsing of the nested boxes inside is not handled here.
- const uint8_t* jumb = nullptr; // Not owned
- size_t jumb_size = 0;
-
- // TODO(lode): add frame index data
-
- // JPEG reconstruction data, or null if not present in the container.
- const uint8_t* jpeg_reconstruction = nullptr;
- size_t jpeg_reconstruction_size = 0;
-
- // The main JPEG XL codestream, of which there must be 1 in the container.
- jxl::PaddedBytes codestream;
-};
-
-// Returns whether `data` starts with a container header; definitely returns
-// false if `size` is less than 12 bytes.
-bool IsContainerHeader(const uint8_t* data, size_t size);
-
-// NOTE: the input data must remain valid as long as `container` is used,
-// because its exif etc. pointers point to that data.
-jxl::Status DecodeJpegXlContainerOneShot(const uint8_t* data, size_t size,
- JpegXlContainer* container);
-
-// TODO(lode): streaming C API
-jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container,
- jxl::PaddedBytes* out);
-
-} // namespace tools
-} // namespace jpegxl
-
-#endif // TOOLS_BOX_BOX_H_
diff --git a/tools/box/box_list_main.cc b/tools/box/box_list_main.cc
deleted file mode 100644
index 40ca910..0000000
--- a/tools/box/box_list_main.cc
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) the JPEG XL Project Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// This binary tool lists the boxes of any box-based format (JPEG XL,
-// JPEG 2000, MP4, ...).
-// This exists as a test for manual verification, rather than an actual tool.
-
-#include <stdint.h>
-#include <stdio.h>
-#include <string.h>
-
-#include "lib/jxl/base/file_io.h"
-#include "lib/jxl/base/override.h"
-#include "lib/jxl/base/padded_bytes.h"
-#include "lib/jxl/base/printf_macros.h"
-#include "lib/jxl/base/status.h"
-#include "tools/box/box.h"
-
-namespace jpegxl {
-namespace tools {
-
-int RunMain(int argc, const char* argv[]) {
- if (argc < 2) {
- fprintf(stderr, "Usage: %s <filename>", argv[0]);
- return 1;
- }
-
- jxl::PaddedBytes compressed;
- if (!jxl::ReadFile(argv[1], &compressed)) return 1;
- fprintf(stderr, "Read %" PRIuS " compressed bytes\n", compressed.size());
-
- const uint8_t* in = compressed.data();
- size_t available_in = compressed.size();
-
- fprintf(stderr, "File size: %" PRIuS "\n", compressed.size());
-
- while (available_in != 0) {
- const uint8_t* start = in;
- Box box;
- if (!ParseBoxHeader(&in, &available_in, &box)) {
- fprintf(stderr, "Failed at %" PRIuS "\n",
- compressed.size() - available_in);
- break;
- }
-
- size_t data_size = box.data_size_given ? box.data_size : available_in;
- size_t header_size = in - start;
- size_t box_size = header_size + data_size;
-
- for (size_t i = 0; i < sizeof(box.type); i++) {
- char c = box.type[i];
- if (c < 32 || c > 127) {
- printf("Unprintable character in box type, likely not a box file.\n");
- return 0;
- }
- }
-
- printf("box: \"%.4s\" box_size:%" PRIuS " data_size:%" PRIuS, box.type,
- box_size, data_size);
- if (!memcmp("uuid", box.type, 4)) {
- printf(" -- extended type:\"%.16s\"", box.extended_type);
- }
- if (!memcmp("ftyp", box.type, 4) && data_size > 4) {
- std::string ftype(in, in + 4);
- printf(" -- ftype:\"%s\"", ftype.c_str());
- }
- printf("\n");
-
- if (data_size > available_in) {
- fprintf(
- stderr, "Unexpected end of file %" PRIuS " %" PRIuS " %" PRIuS "\n",
- static_cast<size_t>(box.data_size), available_in, compressed.size());
- break;
- }
-
- in += data_size;
- available_in -= data_size;
- }
-
- return 0;
-}
-
-} // namespace tools
-} // namespace jpegxl
-
-int main(int argc, const char* argv[]) {
- return jpegxl::tools::RunMain(argc, argv);
-}
diff --git a/tools/box/box_test.cc b/tools/box/box_test.cc
deleted file mode 100644
index 3146bcf..0000000
--- a/tools/box/box_test.cc
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) the JPEG XL Project Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-#include "tools/box/box.h"
-
-#include <stdint.h>
-#include <stdio.h>
-#include <string.h>
-
-#include "gtest/gtest.h"
-#include "lib/jxl/base/file_io.h"
-#include "lib/jxl/base/override.h"
-#include "lib/jxl/base/padded_bytes.h"
-#include "lib/jxl/base/status.h"
-
-TEST(BoxTest, BoxTest) {
- size_t test_size = 256;
- jxl::PaddedBytes exif(test_size);
- jxl::PaddedBytes xml0(test_size);
- jxl::PaddedBytes xml1(test_size);
- jxl::PaddedBytes jumb(test_size);
- jxl::PaddedBytes codestream(test_size);
- // Generate arbitrary data for the codestreams: the test is not testing
- // the contents of them but whether they are preserved in the container.
- uint8_t v = 0;
- for (size_t i = 0; i < test_size; ++i) {
- exif[i] = v++;
- xml0[i] = v++;
- xml1[i] = v++;
- jumb[i] = v++;
- codestream[i] = v++;
- }
-
- jpegxl::tools::JpegXlContainer container;
- container.exif = exif.data();
- container.exif_size = exif.size();
- container.xml.emplace_back(xml0.data(), xml0.size());
- container.xml.emplace_back(xml1.data(), xml1.size());
- container.xmlc.emplace_back(xml1.data(), xml1.size());
- container.jumb = jumb.data();
- container.jumb_size = jumb.size();
- container.codestream = std::move(codestream);
-
- jxl::PaddedBytes file;
- EXPECT_EQ(true,
- jpegxl::tools::EncodeJpegXlContainerOneShot(container, &file));
-
- jpegxl::tools::JpegXlContainer container2;
- EXPECT_EQ(true, jpegxl::tools::DecodeJpegXlContainerOneShot(
- file.data(), file.size(), &container2));
-
- EXPECT_EQ(exif.size(), container2.exif_size);
- EXPECT_EQ(0, memcmp(exif.data(), container2.exif, container2.exif_size));
- EXPECT_EQ(2u, container2.xml.size());
- if (container2.xml.size() == 2) {
- EXPECT_EQ(xml0.size(), container2.xml[0].second);
- EXPECT_EQ(0, memcmp(xml0.data(), container2.xml[0].first,
- container2.xml[0].second));
- EXPECT_EQ(xml1.size(), container2.xml[1].second);
- EXPECT_EQ(0, memcmp(xml1.data(), container2.xml[1].first,
- container2.xml[1].second));
- }
- EXPECT_EQ(1u, container2.xmlc.size());
- if (container2.xmlc.size() == 1) {
- EXPECT_EQ(xml1.size(), container2.xmlc[0].second);
- EXPECT_EQ(0, memcmp(xml1.data(), container2.xmlc[0].first,
- container2.xmlc[0].second));
- }
- EXPECT_EQ(jumb.size(), container2.jumb_size);
- EXPECT_EQ(0, memcmp(jumb.data(), container2.jumb, container2.jumb_size));
- EXPECT_EQ(container.codestream.size(), container2.codestream.size());
- EXPECT_EQ(0, memcmp(container.codestream.data(), container2.codestream.data(),
- container2.codestream.size()));
-}
diff --git a/tools/build_cleaner.py b/tools/build_cleaner.py
deleted file mode 100755
index 76857d7..0000000
--- a/tools/build_cleaner.py
+++ /dev/null
@@ -1,317 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (c) the JPEG XL Project Authors. All rights reserved.
-#
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-
-"""build_cleaner.py: Update build files.
-
-This tool keeps certain parts of the build files up to date.
-"""
-
-import argparse
-import collections
-import locale
-import os
-import re
-import subprocess
-import sys
-import tempfile
-
-
-def RepoFiles(src_dir):
- """Return the list of files from the source git repository"""
- git_bin = os.environ.get('GIT_BIN', 'git')
- files = subprocess.check_output([git_bin, '-C', src_dir, 'ls-files'])
- ret = files.decode(locale.getpreferredencoding()).splitlines()
- ret.sort()
- return ret
-
-def GetPrefixLibFiles(repo_files, prefix, suffixes=('.h', '.cc', '.ui')):
- """Gets the library files that start with the prefix and end with source
- code suffix."""
- prefix_files = [
- fn for fn in repo_files
- if fn.startswith(prefix) and any(fn.endswith(suf) for suf in suffixes)]
- return prefix_files
-
-# Type holding the different types of sources in libjxl:
-# * decoder and common sources,
-# * encoder-only sources,
-# * tests-only sources,
-# * google benchmark sources,
-# * threads library sources,
-# * extras library sources,
-# * libjxl (encoder+decoder) public include/ headers and
-# * threads public include/ headers.
-JxlSources = collections.namedtuple(
- 'JxlSources', ['dec', 'enc', 'test', 'gbench', 'threads',
- 'extras', 'jxl_public_hdrs', 'threads_public_hdrs'])
-
-def SplitLibFiles(repo_files):
- """Splits the library files into the different groups.
-
- """
- testonly = (
- 'testdata.h', 'test_utils.h', 'test_image.h', '_test.h', '_test.cc',
- # _testonly.* files are library code used in tests only.
- '_testonly.h', '_testonly.cc'
- )
- main_srcs = GetPrefixLibFiles(repo_files, 'lib/jxl/')
- extras_srcs = GetPrefixLibFiles(repo_files, 'lib/extras/')
- test_srcs = [fn for fn in main_srcs
- if any(patt in fn for patt in testonly)]
- lib_srcs = [fn for fn in main_srcs
- if not any(patt in fn for patt in testonly)]
-
- # Google benchmark sources.
- gbench_srcs = sorted(fn for fn in lib_srcs + extras_srcs
- if fn.endswith('_gbench.cc'))
- lib_srcs = [fn for fn in lib_srcs if fn not in gbench_srcs]
- # Exclude optional codecs from extras.
- exclude_extras = [
- '/dec/gif',
- '/dec/apng', '/enc/apng',
- '/dec/exr', '/enc/exr',
- '/dec/jpg', '/enc/jpg',
- ]
- extras_srcs = [fn for fn in extras_srcs if fn not in gbench_srcs and
- not any(patt in fn for patt in testonly) and
- not any(patt in fn for patt in exclude_extras)]
-
-
- enc_srcs = [fn for fn in lib_srcs
- if os.path.basename(fn).startswith('enc_') or
- os.path.basename(fn).startswith('butteraugli')]
- enc_srcs.extend([
- "lib/jxl/encode.cc",
- "lib/jxl/encode_internal.h",
- "lib/jxl/gaborish.cc",
- "lib/jxl/gaborish.h",
- "lib/jxl/huffman_tree.cc",
- "lib/jxl/huffman_tree.h",
- # Only the inlines in linalg.h header are used in the decoder.
- # TODO(deymo): split out encoder only linalg.h functions.
- "lib/jxl/linalg.cc",
- "lib/jxl/optimize.cc",
- "lib/jxl/optimize.h",
- "lib/jxl/progressive_split.cc",
- "lib/jxl/progressive_split.h",
- # TODO(deymo): Add luminance.cc and luminance.h here too. Currently used
- # by aux_out.h.
- ])
- # Temporarily remove enc_bit_writer from the encoder sources: a lot of
- # decoder source code still needs to be split up into encoder and decoder.
- # Including the enc_bit_writer in the decoder allows to build a working
- # libjxl_dec library.
- # TODO(lode): remove the dependencies of the decoder on enc_bit_writer and
- # remove enc_bit_writer from the dec_srcs again.
- enc_srcs.remove("lib/jxl/enc_bit_writer.cc")
- enc_srcs.remove("lib/jxl/enc_bit_writer.h")
- enc_srcs.sort()
-
- enc_srcs_set = set(enc_srcs)
- lib_srcs = [fn for fn in lib_srcs if fn not in enc_srcs_set]
-
- # The remaining of the files are in the dec_library.
- dec_srcs = lib_srcs
-
- thread_srcs = GetPrefixLibFiles(repo_files, 'lib/threads/')
- thread_srcs = [fn for fn in thread_srcs
- if not any(patt in fn for patt in testonly)]
- public_hdrs = GetPrefixLibFiles(repo_files, 'lib/include/jxl/')
-
- threads_public_hdrs = [fn for fn in public_hdrs if '_parallel_runner' in fn]
- jxl_public_hdrs = list(sorted(set(public_hdrs) - set(threads_public_hdrs)))
- return JxlSources(dec_srcs, enc_srcs, test_srcs, gbench_srcs, thread_srcs,
- extras_srcs, jxl_public_hdrs, threads_public_hdrs)
-
-
-def CleanFile(args, filename, pattern_data_list):
- """Replace a pattern match with new data in the passed file.
-
- Given a regular expression pattern with a single () match, it runs the regex
- over the passed filename and replaces the match () with the new data. If
- args.update is set, it will update the file with the new contents, otherwise
- it will return True when no changes were needed.
-
- Multiple pairs of (regular expression, new data) can be passed to the
- pattern_data_list parameter and will be applied in order.
-
- The regular expression must match at least once in the file.
- """
- filepath = os.path.join(args.src_dir, filename)
- with open(filepath, 'r') as f:
- src_text = f.read()
-
- if not pattern_data_list:
- return True
-
- new_text = src_text
-
- for pattern, data in pattern_data_list:
- offset = 0
- chunks = []
- for match in re.finditer(pattern, new_text):
- chunks.append(new_text[offset:match.start(1)])
- offset = match.end(1)
- chunks.append(data)
- if not chunks:
- raise Exception('Pattern not found for %s: %r' % (filename, pattern))
- chunks.append(new_text[offset:])
- new_text = ''.join(chunks)
-
- if new_text == src_text:
- return True
-
- if args.update:
- print('Updating %s' % filename)
- with open(filepath, 'w') as f:
- f.write(new_text)
- return True
- else:
- with tempfile.NamedTemporaryFile(
- mode='w', prefix=os.path.basename(filename)) as new_file:
- new_file.write(new_text)
- new_file.flush()
- subprocess.call(
- ['diff', '-u', filepath, '--label', 'a/' + filename, new_file.name,
- '--label', 'b/' + filename])
- return False
-
-
-def BuildCleaner(args):
- repo_files = RepoFiles(args.src_dir)
- ok = True
-
- # jxl version
- with open(os.path.join(args.src_dir, 'lib/CMakeLists.txt'), 'r') as f:
- cmake_text = f.read()
-
- gni_patterns = []
- for varname in ('JPEGXL_MAJOR_VERSION', 'JPEGXL_MINOR_VERSION',
- 'JPEGXL_PATCH_VERSION'):
- # Defined in CMakeLists.txt as "set(varname 1234)"
- match = re.search(r'set\(' + varname + r' ([0-9]+)\)', cmake_text)
- version_value = match.group(1)
- gni_patterns.append((r'"' + varname + r'=([0-9]+)"', version_value))
-
- jxl_src = SplitLibFiles(repo_files)
-
- # libjxl
- jxl_cmake_patterns = []
- jxl_cmake_patterns.append(
- (r'set\(JPEGXL_INTERNAL_SOURCES_DEC\n([^\)]+)\)',
- ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.dec)))
- jxl_cmake_patterns.append(
- (r'set\(JPEGXL_INTERNAL_SOURCES_ENC\n([^\)]+)\)',
- ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.enc)))
- ok = CleanFile(
- args, 'lib/jxl.cmake',
- jxl_cmake_patterns) and ok
-
- ok = CleanFile(
- args, 'lib/jxl_benchmark.cmake',
- [(r'set\(JPEGXL_INTERNAL_SOURCES_GBENCH\n([^\)]+)\)',
- ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.gbench))]) and ok
-
- gni_patterns.append((
- r'libjxl_dec_sources = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.dec)))
- gni_patterns.append((
- r'libjxl_enc_sources = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.enc)))
- gni_patterns.append((
- r'libjxl_gbench_sources = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.gbench)))
-
-
- tests = [fn[len('lib/'):] for fn in jxl_src.test if fn.endswith('_test.cc')]
- testlib = [fn[len('lib/'):] for fn in jxl_src.test
- if not fn.endswith('_test.cc')]
- gni_patterns.append((
- r'libjxl_tests_sources = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn for fn in tests)))
- gni_patterns.append((
- r'libjxl_testlib_sources = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn for fn in testlib)))
-
- # libjxl_threads
- ok = CleanFile(
- args, 'lib/jxl_threads.cmake',
- [(r'set\(JPEGXL_THREADS_SOURCES\n([^\)]+)\)',
- ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.threads))]) and ok
-
- gni_patterns.append((
- r'libjxl_threads_sources = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.threads)))
-
- # libjxl_extras
- ok = CleanFile(
- args, 'lib/jxl_extras.cmake',
- [(r'set\(JPEGXL_EXTRAS_SOURCES\n([^\)]+)\)',
- ''.join(' %s\n' % fn[len('lib/'):] for fn in jxl_src.extras))]) and ok
-
- gni_patterns.append((
- r'libjxl_extras_sources = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn[len('lib/'):] for fn in jxl_src.extras)))
-
- # libjxl_profiler
- profiler_srcs = [fn[len('lib/'):] for fn in repo_files
- if fn.startswith('lib/profiler')]
- ok = CleanFile(
- args, 'lib/jxl_profiler.cmake',
- [(r'set\(JPEGXL_PROFILER_SOURCES\n([^\)]+)\)',
- ''.join(' %s\n' % fn for fn in profiler_srcs))]) and ok
-
- gni_patterns.append((
- r'libjxl_profiler_sources = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn for fn in profiler_srcs)))
-
- # Public headers.
- gni_patterns.append((
- r'libjxl_public_headers = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn[len('lib/'):]
- for fn in jxl_src.jxl_public_hdrs)))
- gni_patterns.append((
- r'libjxl_threads_public_headers = \[\n([^\]]+)\]',
- ''.join(' "%s",\n' % fn[len('lib/'):]
- for fn in jxl_src.threads_public_hdrs)))
-
-
- # Update the list of tests. CMake version include test files in other libs,
- # not just in libjxl.
- tests = [fn[len('lib/'):] for fn in repo_files
- if fn.endswith('_test.cc') and fn.startswith('lib/')]
- ok = CleanFile(
- args, 'lib/jxl_tests.cmake',
- [(r'set\(TEST_FILES\n([^\)]+) ### Files before this line',
- ''.join(' %s\n' % fn for fn in tests))]) and ok
- ok = CleanFile(
- args, 'lib/jxl_tests.cmake',
- [(r'set\(TESTLIB_FILES\n([^\)]+)\)',
- ''.join(' %s\n' % fn for fn in testlib))]) and ok
-
- # Update lib.gni
- ok = CleanFile(args, 'lib/lib.gni', gni_patterns) and ok
-
- return ok
-
-
-def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--src-dir',
- default=os.path.realpath(os.path.join(
- os.path.dirname(__file__), '..')),
- help='path to the build directory')
- parser.add_argument('--update', default=False, action='store_true',
- help='update the build files instead of only checking')
- args = parser.parse_args()
- if not BuildCleaner(args):
- print('Build files need update.')
- sys.exit(2)
-
-
-if __name__ == '__main__':
- main()
diff --git a/tools/butteraugli_main.cc b/tools/butteraugli_main.cc
index 247ade8..436d290 100644
--- a/tools/butteraugli_main.cc
+++ b/tools/butteraugli_main.cc
@@ -3,6 +3,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/cms.h>
#include <stdint.h>
#include <stdio.h>
@@ -11,57 +12,70 @@
#include "lib/extras/codec.h"
#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/metrics.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/file_io.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/butteraugli/butteraugli.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/color_encoding_internal.h"
-#include "lib/jxl/color_management.h"
#include "lib/jxl/enc_butteraugli_comparator.h"
-#include "lib/jxl/enc_butteraugli_pnorm.h"
-#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
#include "lib/jxl/image_ops.h"
+#include "tools/file_io.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
namespace {
+using jpegxl::tools::ThreadPoolInternal;
+using jxl::ButteraugliParams;
+using jxl::CodecInOut;
+using jxl::ColorEncoding;
+using jxl::Image3F;
+using jxl::ImageF;
+using jxl::Status;
+
Status WriteImage(Image3F&& image, const std::string& filename) {
ThreadPoolInternal pool(4);
CodecInOut io;
io.metadata.m.SetUintSamples(8);
io.metadata.m.color_encoding = ColorEncoding::SRGB();
io.SetFromImage(std::move(image), io.metadata.m.color_encoding);
- return EncodeToFile(io, filename, &pool);
+
+ std::vector<uint8_t> encoded;
+ return jxl::Encode(io, filename, &encoded, &pool) &&
+ jpegxl::tools::WriteFile(filename, encoded);
}
Status RunButteraugli(const char* pathname1, const char* pathname2,
const std::string& distmap_filename,
+ const std::string& raw_distmap_filename,
const std::string& colorspace_hint, double p,
float intensity_target) {
- extras::ColorHints color_hints;
+ jxl::extras::ColorHints color_hints;
if (!colorspace_hint.empty()) {
color_hints.Add("color_space", colorspace_hint);
}
- CodecInOut io1;
+ const char* pathname[2] = {pathname1, pathname2};
+ CodecInOut io[2];
ThreadPoolInternal pool(4);
- if (!SetFromFile(pathname1, color_hints, &io1, &pool)) {
- fprintf(stderr, "Failed to read image from %s\n", pathname1);
- return false;
- }
-
- CodecInOut io2;
- if (!SetFromFile(pathname2, color_hints, &io2, &pool)) {
- fprintf(stderr, "Failed to read image from %s\n", pathname2);
- return false;
+ for (size_t i = 0; i < 2; ++i) {
+ std::vector<uint8_t> encoded;
+ if (!jpegxl::tools::ReadFile(pathname[i], &encoded)) {
+ fprintf(stderr, "Failed to read image from %s\n", pathname[i]);
+ return false;
+ }
+ if (!jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &io[i], &pool)) {
+ fprintf(stderr, "Failed to decode image from %s\n", pathname[i]);
+ return false;
+ }
}
+ CodecInOut& io1 = io[0];
+ CodecInOut& io2 = io[1];
if (io1.xsize() != io2.xsize()) {
fprintf(stderr, "Width mismatch: %" PRIuS " %" PRIuS "\n", io1.xsize(),
io2.xsize());
@@ -75,33 +89,43 @@ Status RunButteraugli(const char* pathname1, const char* pathname2,
ImageF distmap;
ButteraugliParams ba_params;
- ba_params.hf_asymmetry = 0.8f;
+ ba_params.hf_asymmetry = 1.0f;
ba_params.xmul = 1.0f;
ba_params.intensity_target = intensity_target;
- const float distance = ButteraugliDistance(io1.Main(), io2.Main(), ba_params,
- GetJxlCms(), &distmap, &pool);
+ const float distance = jxl::ButteraugliDistance(
+ io1.Main(), io2.Main(), ba_params, *JxlGetDefaultCms(), &distmap, &pool);
printf("%.10f\n", distance);
- double pnorm = ComputeDistanceP(distmap, ba_params, p);
+ double pnorm = jxl::ComputeDistanceP(distmap, ba_params, p);
printf("%g-norm: %f\n", p, pnorm);
if (!distmap_filename.empty()) {
- float good = ButteraugliFuzzyInverse(1.5);
- float bad = ButteraugliFuzzyInverse(0.5);
- JXL_CHECK(
- WriteImage(CreateHeatMapImage(distmap, good, bad), distmap_filename));
+ float good = jxl::ButteraugliFuzzyInverse(1.5);
+ float bad = jxl::ButteraugliFuzzyInverse(0.5);
+ JXL_CHECK(WriteImage(jxl::CreateHeatMapImage(distmap, good, bad),
+ distmap_filename));
+ }
+ if (!raw_distmap_filename.empty()) {
+ FILE* out = fopen(raw_distmap_filename.c_str(), "wb");
+ JXL_CHECK(out != nullptr);
+ fprintf(out, "Pf\n%" PRIuS " %" PRIuS "\n-1.0\n", distmap.xsize(),
+ distmap.ysize());
+ for (size_t y = distmap.ysize(); y-- > 0;) {
+ fwrite(distmap.Row(y), 4, distmap.xsize(), out);
+ }
+ fclose(out);
}
return true;
}
} // namespace
-} // namespace jxl
int main(int argc, char** argv) {
if (argc < 3) {
fprintf(stderr,
"Usage: %s <reference> <distorted>\n"
" [--distmap <distmap>]\n"
+ " [--rawdistmap <distmap.pfm>]\n"
" [--intensity_target <intensity_target>]\n"
" [--colorspace <colorspace_hint>]\n"
" [--pnorm <pth norm>]\n"
@@ -114,12 +138,15 @@ int main(int argc, char** argv) {
return 1;
}
std::string distmap;
+ std::string raw_distmap;
std::string colorspace;
double p = 3;
float intensity_target = 80.0; // sRGB intensity target.
for (int i = 3; i < argc; i++) {
if (std::string(argv[i]) == "--distmap" && i + 1 < argc) {
distmap = argv[++i];
+ } else if (std::string(argv[i]) == "--rawdistmap" && i + 1 < argc) {
+ raw_distmap = argv[++i];
} else if (std::string(argv[i]) == "--colorspace" && i + 1 < argc) {
colorspace = argv[++i];
} else if (std::string(argv[i]) == "--intensity_target" && i + 1 < argc) {
@@ -137,8 +164,6 @@ int main(int argc, char** argv) {
}
}
- return jxl::RunButteraugli(argv[1], argv[2], distmap, colorspace, p,
- intensity_target)
- ? 0
- : 1;
+ return !RunButteraugli(argv[1], argv[2], distmap, raw_distmap, colorspace, p,
+ intensity_target);
}
diff --git a/tools/cjpeg_hdr.cc b/tools/cjpeg_hdr.cc
deleted file mode 100644
index cfe272e..0000000
--- a/tools/cjpeg_hdr.cc
+++ /dev/null
@@ -1,306 +0,0 @@
-// Copyright (c) the JPEG XL Project Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-#include <stdio.h>
-#include <stdlib.h>
-
-#include <tuple>
-
-#undef HWY_TARGET_INCLUDE
-#define HWY_TARGET_INCLUDE "tools/cjpeg_hdr.cc"
-#include <hwy/foreach_target.h>
-#include <hwy/highway.h>
-
-#include "lib/extras/codec.h"
-#include "lib/jxl/base/file_io.h"
-#include "lib/jxl/base/padded_bytes.h"
-#include "lib/jxl/codec_in_out.h"
-#include "lib/jxl/common.h"
-#include "lib/jxl/enc_adaptive_quantization.h"
-#include "lib/jxl/enc_color_management.h"
-#include "lib/jxl/enc_transforms.h"
-#include "lib/jxl/enc_xyb.h"
-#include "lib/jxl/image.h"
-#include "lib/jxl/image_bundle.h"
-#include "lib/jxl/image_metadata.h"
-#include "lib/jxl/image_ops.h"
-#include "lib/jxl/jpeg/dec_jpeg_data_writer.h"
-#include "lib/jxl/quant_weights.h"
-
-HWY_BEFORE_NAMESPACE();
-namespace jpegxl {
-namespace tools {
-namespace HWY_NAMESPACE {
-void FillJPEGData(const jxl::Image3F& ycbcr, const jxl::PaddedBytes& icc,
- const jxl::ImageF& quant_field,
- const jxl::FrameDimensions& frame_dim,
- jxl::jpeg::JPEGData* out) {
- // JFIF
- out->marker_order.push_back(0xE0);
- out->app_data.emplace_back(std::vector<uint8_t>{
- 0xe0, // Marker
- 0, 16, // Length
- 'J', 'F', 'I', 'F', '\0', // ID
- 1, 1, // Version (1.1)
- 0, // No density units
- 0, 1, 0, 1, // Pixel density 1
- 0, 0 // No thumbnail
- });
- // ICC
- if (!icc.empty()) {
- out->marker_order.push_back(0xE2);
- std::vector<uint8_t> icc_marker(17 + icc.size());
- icc_marker[0] = 0xe2;
- icc_marker[1] = (icc_marker.size() - 1) >> 8;
- icc_marker[2] = (icc_marker.size() - 1) & 0xFF;
- memcpy(&icc_marker[3], "ICC_PROFILE", 12);
- icc_marker[15] = 1;
- icc_marker[16] = 1;
- memcpy(&icc_marker[17], icc.data(), icc.size());
- out->app_data.push_back(std::move(icc_marker));
- }
-
- // DQT
- out->marker_order.emplace_back(0xdb);
- out->quant.resize(2);
- out->quant[0].is_last = false;
- out->quant[0].index = 0;
- out->quant[1].is_last = true;
- out->quant[1].index = 1;
- jxl::DequantMatrices dequant;
-
- // mozjpeg q99
- int qluma[64] = {
- 1, 1, 1, 1, 1, 1, 1, 2, //
- 1, 1, 1, 1, 1, 1, 1, 2, //
- 1, 1, 1, 1, 1, 1, 2, 3, //
- 1, 1, 1, 1, 1, 1, 2, 3, //
- 1, 1, 1, 1, 1, 2, 3, 4, //
- 1, 1, 1, 1, 2, 2, 3, 5, //
- 1, 1, 2, 2, 3, 3, 5, 6, //
- 2, 2, 3, 3, 4, 5, 6, 8, //
- };
- // mozjpeg q95
- int qchroma[64] = {
- 2, 2, 2, 2, 3, 4, 6, 9, //
- 2, 2, 2, 3, 3, 4, 5, 8, //
- 2, 2, 2, 3, 4, 6, 9, 14, //
- 2, 3, 3, 4, 5, 7, 11, 16, //
- 3, 3, 4, 5, 7, 9, 13, 19, //
- 4, 4, 6, 7, 9, 12, 17, 24, //
- 6, 5, 9, 11, 13, 17, 23, 31, //
- 9, 8, 14, 16, 19, 24, 31, 42, //
- };
- // Disable quantization for now.
- std::fill(std::begin(qluma), std::end(qluma), 1);
- std::fill(std::begin(qchroma), std::end(qchroma), 1);
-
- memcpy(out->quant[0].values.data(), qluma, sizeof(qluma));
- memcpy(out->quant[1].values.data(), qchroma, sizeof(qchroma));
-
- // SOF
- out->marker_order.emplace_back(0xc2);
- out->components.resize(3);
- out->height = frame_dim.ysize;
- out->width = frame_dim.xsize_padded;
- out->components[0].id = 1;
- out->components[1].id = 2;
- out->components[2].id = 3;
- out->components[0].h_samp_factor = out->components[1].h_samp_factor =
- out->components[2].h_samp_factor = out->components[0].v_samp_factor =
- out->components[1].v_samp_factor = out->components[2].v_samp_factor =
- 1;
- out->components[0].width_in_blocks = out->components[1].width_in_blocks =
- out->components[2].width_in_blocks = frame_dim.xsize_blocks;
- out->components[0].quant_idx = 0;
- out->components[1].quant_idx = 1;
- out->components[2].quant_idx = 1;
- out->components[0].coeffs.resize(frame_dim.xsize_blocks *
- frame_dim.ysize_blocks * 64);
- out->components[1].coeffs.resize(frame_dim.xsize_blocks *
- frame_dim.ysize_blocks * 64);
- out->components[2].coeffs.resize(frame_dim.xsize_blocks *
- frame_dim.ysize_blocks * 64);
-
- HWY_ALIGN float scratch_space[2 * 64];
-
- for (size_t c = 0; c < 3; c++) {
- int* qt = c == 0 ? qluma : qchroma;
- for (size_t by = 0; by < frame_dim.ysize_blocks; by++) {
- for (size_t bx = 0; bx < frame_dim.xsize_blocks; bx++) {
- float deadzone = 0.5f / quant_field.Row(by)[bx];
- // Disable quantization for now.
- deadzone = 0;
- auto q = [&](float coeff, size_t x, size_t y) -> int {
- size_t pos = x * 8 + y;
- float scoeff = coeff / qt[pos];
- if (pos == 0) {
- return std::round(scoeff);
- }
- if (std::abs(scoeff) < deadzone) return 0;
- if (std::abs(scoeff) < 2 * deadzone && x + y >= 7) return 0;
- return std::round(scoeff);
- };
- HWY_ALIGN float dct[64];
- TransformFromPixels(jxl::AcStrategy::Type::DCT,
- ycbcr.PlaneRow(c, 8 * by) + 8 * bx,
- ycbcr.PixelsPerRow(), dct, scratch_space);
- for (size_t iy = 0; iy < 8; iy++) {
- for (size_t ix = 0; ix < 8; ix++) {
- float coeff = dct[iy * 8 + ix] * 2040; // not a typo
- out->components[c]
- .coeffs[(frame_dim.xsize_blocks * by + bx) * 64 + ix * 8 + iy] =
- q(coeff, ix, iy);
- }
- }
- }
- }
- }
-
- // DHT
- // TODO: optimize
- out->marker_order.emplace_back(0xC4);
- out->huffman_code.resize(2);
- out->huffman_code[0].slot_id = 0x00; // DC
- out->huffman_code[0].counts = {{0, 0, 0, 0, 13}};
- std::iota(out->huffman_code[0].values.begin(),
- out->huffman_code[0].values.end(), 0);
- out->huffman_code[0].is_last = false;
-
- out->huffman_code[1].slot_id = 0x10; // AC
- out->huffman_code[1].counts = {{0, 0, 0, 0, 0, 0, 0, 0, 255}};
- std::iota(out->huffman_code[1].values.begin(),
- out->huffman_code[1].values.end(), 0);
- out->huffman_code[1].is_last = true;
-
- // SOS
- for (size_t _ = 0; _ < 7; _++) {
- out->marker_order.emplace_back(0xDA);
- }
- out->scan_info.resize(7);
- // DC
- // comp id, DC tbl, AC tbl
- out->scan_info[0].num_components = 3;
- out->scan_info[0].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0},
- jxl::jpeg::JPEGComponentScanInfo{1, 0, 0},
- jxl::jpeg::JPEGComponentScanInfo{2, 0, 0}}};
- out->scan_info[0].Ss = 0;
- out->scan_info[0].Se = 0;
- out->scan_info[0].Ah = out->scan_info[0].Al = 0;
- // AC 1 - highest bits
- out->scan_info[1].num_components = 1;
- out->scan_info[1].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0}}};
- out->scan_info[1].Ss = 1;
- out->scan_info[1].Se = 63;
- out->scan_info[1].Ah = 0;
- out->scan_info[1].Al = 1;
-
- // Copy for X / B-Y
- out->scan_info[2] = out->scan_info[1];
- out->scan_info[2].components[0].comp_idx = 1;
- out->scan_info[3] = out->scan_info[1];
- out->scan_info[3].components[0].comp_idx = 2;
-
- // AC 2 - lowest bit
- out->scan_info[4].num_components = 1;
- out->scan_info[4].components = {{jxl::jpeg::JPEGComponentScanInfo{0, 0, 0}}};
- out->scan_info[4].Ss = 1;
- out->scan_info[4].Se = 63;
- out->scan_info[4].Ah = 1;
- out->scan_info[4].Al = 0;
-
- // Copy for X / B-Y
- out->scan_info[5] = out->scan_info[4];
- out->scan_info[5].components[0].comp_idx = 1;
- out->scan_info[6] = out->scan_info[4];
- out->scan_info[6].components[0].comp_idx = 2;
-
- // EOI
- out->marker_order.push_back(0xd9);
-}
-} // namespace HWY_NAMESPACE
-} // namespace tools
-} // namespace jpegxl
-HWY_AFTER_NAMESPACE();
-
-#if HWY_ONCE
-
-namespace jpegxl {
-namespace tools {
-
-HWY_EXPORT(FillJPEGData);
-
-int HBDJPEGMain(int argc, const char* argv[]) {
- if (argc < 3) {
- fprintf(stderr, "Usage: %s input output.jpg\n", argv[0]);
- return 1;
- }
- fprintf(stderr, "Compressing %s to %s\n", argv[1], argv[2]);
- jxl::CodecInOut io;
- if (!jxl::SetFromFile(argv[1], jxl::extras::ColorHints{}, &io)) {
- fprintf(stderr, "Failed to read image %s.\n", argv[1]);
- return 1;
- }
- jxl::Image3F ycbcr(jxl::RoundUpToBlockDim(io.xsize()),
- jxl::RoundUpToBlockDim(io.ysize()));
- ycbcr.ShrinkTo(io.xsize(), io.ysize());
- jxl::FrameDimensions frame_dim;
- frame_dim.Set(io.xsize(), io.ysize(), 0, 0, 0, false, 1);
- for (size_t y = 0; y < ycbcr.ysize(); y++) {
- for (size_t x = 0; x < ycbcr.xsize(); x++) {
- float r = io.Main().color()->PlaneRow(0, y)[x];
- float g = io.Main().color()->PlaneRow(1, y)[x];
- float b = io.Main().color()->PlaneRow(2, y)[x];
- ycbcr.PlaneRow(0, y)[x] =
- 0.299 * r + 0.587 * g + 0.114 * b - (128. / 255.);
- ycbcr.PlaneRow(1, y)[x] = -0.168736 * r - 0.331264 * g + 0.5 * b;
- ycbcr.PlaneRow(2, y)[x] = 0.5 * r - 0.418688 * g - 0.081312 * b;
- }
- }
- jxl::Image3F rgb2(ycbcr.xsize(), ycbcr.ysize());
- jxl::Image3F ycbcr2(ycbcr.xsize(), ycbcr.ysize());
- for (size_t y = 0; y < ycbcr.ysize(); y++) {
- for (size_t x = 0; x < ycbcr.xsize(); x++) {
- ycbcr2.PlaneRow(0, y)[x] = ycbcr.PlaneRow(1, y)[x];
- ycbcr2.PlaneRow(1, y)[x] = ycbcr.PlaneRow(0, y)[x];
- ycbcr2.PlaneRow(2, y)[x] = ycbcr.PlaneRow(2, y)[x];
- }
- }
- jxl::YcbcrToRgb(ycbcr2, &rgb2, jxl::Rect(ycbcr));
-
- PadImageToBlockMultipleInPlace(&ycbcr);
-
- jxl::Image3F opsin(jxl::RoundUpToBlockDim(io.xsize()),
- jxl::RoundUpToBlockDim(io.ysize()));
- opsin.ShrinkTo(io.xsize(), io.ysize());
- jxl::ToXYB(io.Main(), nullptr, &opsin, jxl::GetJxlCms());
- PadImageToBlockMultipleInPlace(&opsin);
- jxl::ImageF mask;
- jxl::ImageF qf =
- InitialQuantField(1.0, opsin, frame_dim, nullptr, 1.0, &mask);
-
- jxl::CodecInOut out;
- out.Main().jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>();
- HWY_DYNAMIC_DISPATCH(FillJPEGData)
- (ycbcr, io.metadata.m.color_encoding.ICC(), qf, frame_dim,
- out.Main().jpeg_data.get());
- jxl::PaddedBytes output;
- if (!jxl::jpeg::EncodeImageJPGCoefficients(&out, &output)) {
- return 1;
- }
- if (!jxl::WriteFile(output, argv[2])) {
- fprintf(stderr, "Failed to write to \"%s\"\n", argv[2]);
- return 1;
- }
- return 0;
-}
-
-} // namespace tools
-} // namespace jpegxl
-
-int main(int argc, const char** argv) {
- return jpegxl::tools::HBDJPEGMain(argc, argv);
-}
-#endif
diff --git a/tools/cjpegli.cc b/tools/cjpegli.cc
new file mode 100644
index 0000000..4088e27
--- /dev/null
+++ b/tools/cjpegli.cc
@@ -0,0 +1,270 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <vector>
+
+#include "lib/extras/dec/decode.h"
+#include "lib/extras/enc/jpegli.h"
+#include "lib/extras/time.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/span.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/speed_stats.h"
+
+namespace jpegxl {
+namespace tools {
+namespace {
+
+struct Args {
+ void AddCommandLineOptions(CommandLineParser* cmdline) {
+ std::string input_help("the input can be ");
+ if (jxl::extras::CanDecode(jxl::extras::Codec::kPNG)) {
+ input_help.append("PNG, APNG, ");
+ }
+ if (jxl::extras::CanDecode(jxl::extras::Codec::kGIF)) {
+ input_help.append("GIF, ");
+ }
+ if (jxl::extras::CanDecode(jxl::extras::Codec::kEXR)) {
+ input_help.append("EXR, ");
+ }
+ input_help.append("PPM, PFM, or PGX");
+ cmdline->AddPositionalOption("INPUT", /* required = */ true, input_help,
+ &file_in);
+ cmdline->AddPositionalOption("OUTPUT", /* required = */ true,
+ "the compressed JPG output file", &file_out);
+
+ cmdline->AddOptionFlag('\0', "disable_output",
+ "No output file will be written (for benchmarking)",
+ &disable_output, &SetBooleanTrue, 1);
+
+ cmdline->AddOptionValue(
+ 'x', "dec-hints", "key=value",
+ "color_space indicates the ColorEncoding, see Description();\n"
+ " icc_pathname refers to a binary file containing an ICC profile.",
+ &color_hints_proxy, &ParseAndAppendKeyValue<ColorHintsProxy>, 1);
+
+ opt_distance_id = cmdline->AddOptionValue(
+ 'd', "distance", "maxError",
+ "Max. butteraugli distance, lower = higher quality.\n"
+ " 1.0 = visually lossless (default).\n"
+ " Recommended range: 0.5 .. 3.0. Allowed range: 0.0 ... 25.0.\n"
+ " Mutually exclusive with --quality and --target_size.",
+ &settings.distance, &ParseFloat);
+
+ opt_quality_id = cmdline->AddOptionValue(
+ 'q', "quality", "QUALITY",
+ "Quality setting (is remapped to --distance)."
+ " Default is quality 90.\n"
+ " Quality values roughly match libjpeg quality.\n"
+ " Recommended range: 68 .. 96. Allowed range: 1 .. 100.\n"
+ " Mutually exclusive with --distance and --target_size.",
+ &quality, &ParseSigned);
+
+ cmdline->AddOptionValue('\0', "chroma_subsampling", "444|440|422|420",
+ "Chroma subsampling setting.",
+ &settings.chroma_subsampling, &ParseString);
+
+ cmdline->AddOptionValue(
+ 'p', "progressive_level", "N",
+ "Progressive level setting. Range: 0 .. 2.\n"
+ " Default: 2. Higher number is more scans, 0 means sequential.",
+ &settings.progressive_level, &ParseSigned);
+
+ cmdline->AddOptionFlag('\0', "xyb", "Convert to XYB colorspace",
+ &settings.xyb, &SetBooleanTrue, 1);
+
+ cmdline->AddOptionFlag(
+ '\0', "std_quant",
+ "Use quantization tables based on Annex K of the JPEG standard.",
+ &settings.use_std_quant_tables, &SetBooleanTrue, 1);
+
+ cmdline->AddOptionFlag(
+ '\0', "noadaptive_quantization", "Disable adaptive quantization.",
+ &settings.use_adaptive_quantization, &SetBooleanFalse, 1);
+
+ cmdline->AddOptionFlag(
+ '\0', "fixed_code",
+ "Disable Huffman code optimization. Must be used together with -p 0.",
+ &settings.optimize_coding, &SetBooleanFalse, 1);
+
+ cmdline->AddOptionValue(
+ '\0', "target_size", "N",
+ "If non-zero, set target size in bytes. This is useful for image \n"
+ " quality comparisons, but makes encoding speed up to 20x slower.\n"
+ " Mutually exclusive with --distance and --quality.",
+ &settings.target_size, &ParseUnsigned, 2);
+
+ cmdline->AddOptionValue('\0', "num_reps", "N",
+ "How many times to compress. (For benchmarking).",
+ &num_reps, &ParseUnsigned, 1);
+
+ cmdline->AddOptionFlag('\0', "quiet", "Suppress informative output", &quiet,
+ &SetBooleanTrue, 1);
+
+ cmdline->AddOptionFlag(
+ 'v', "verbose",
+ "Verbose output; can be repeated, also applies to help (!).", &verbose,
+ &SetBooleanTrue);
+ }
+
+ const char* file_in = nullptr;
+ const char* file_out = nullptr;
+ bool disable_output = false;
+ ColorHintsProxy color_hints_proxy;
+ jxl::extras::JpegSettings settings;
+ int quality = 90;
+ size_t num_reps = 1;
+ bool quiet = false;
+ bool verbose = false;
+ // References (ids) of specific options to check if they were matched.
+ CommandLineParser::OptionId opt_distance_id = -1;
+ CommandLineParser::OptionId opt_quality_id = -1;
+};
+
+bool ValidateArgs(const Args& args) {
+ const jxl::extras::JpegSettings& settings = args.settings;
+ if (settings.distance < 0.0 || settings.distance > 25.0) {
+ fprintf(stderr, "Invalid --distance argument\n");
+ return false;
+ }
+ if (args.quality <= 0 || args.quality > 100) {
+ fprintf(stderr, "Invalid --quality argument\n");
+ return false;
+ }
+ std::string cs = settings.chroma_subsampling;
+ if (!cs.empty() && cs != "444" && cs != "440" && cs != "422" && cs != "420") {
+ fprintf(stderr, "Invalid --chroma_subsampling argument\n");
+ return false;
+ }
+ if (settings.progressive_level < 0 || settings.progressive_level > 2) {
+ fprintf(stderr, "Invalid --progressive_level argument\n");
+ return false;
+ }
+ if (settings.progressive_level > 0 && !settings.optimize_coding) {
+ fprintf(stderr, "--fixed_code must be used together with -p 0\n");
+ return false;
+ }
+ return true;
+}
+
+bool SetDistance(const Args& args, const CommandLineParser& cmdline,
+ jxl::extras::JpegSettings* settings) {
+ bool distance_set = cmdline.GetOption(args.opt_distance_id)->matched();
+ bool quality_set = cmdline.GetOption(args.opt_quality_id)->matched();
+ int num_quality_settings = (distance_set ? 1 : 0) + (quality_set ? 1 : 0) +
+ (args.settings.target_size > 0 ? 1 : 0);
+ if (num_quality_settings > 1) {
+ fprintf(
+ stderr,
+ "Only one of --distance, --quality, or --target_size can be set.\n");
+ return false;
+ }
+ if (quality_set) {
+ settings->quality = args.quality;
+ }
+ return true;
+}
+
+int CJpegliMain(int argc, const char* argv[]) {
+ Args args;
+ CommandLineParser cmdline;
+ args.AddCommandLineOptions(&cmdline);
+
+ if (!cmdline.Parse(argc, const_cast<const char**>(argv))) {
+ // Parse already printed the actual error cause.
+ fprintf(stderr, "Use '%s -h' for more information.\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ if (cmdline.HelpFlagPassed() || !args.file_in) {
+ cmdline.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!args.file_out && !args.disable_output) {
+ fprintf(stderr,
+ "No output file specified and --disable_output flag not passed.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (args.disable_output && !args.quiet) {
+ fprintf(stderr,
+ "Encoding will be performed, but the result will be discarded.\n");
+ }
+
+ std::vector<uint8_t> input_bytes;
+ if (!ReadFile(args.file_in, &input_bytes)) {
+ fprintf(stderr, "Failed to read input image %s\n", args.file_in);
+ return EXIT_FAILURE;
+ }
+
+ jxl::extras::PackedPixelFile ppf;
+ if (!jxl::extras::DecodeBytes(jxl::Bytes(input_bytes),
+ args.color_hints_proxy.target, &ppf)) {
+ fprintf(stderr, "Failed to decode input image %s\n", args.file_in);
+ return EXIT_FAILURE;
+ }
+
+ if (!args.quiet) {
+ fprintf(stderr, "Read %ux%u image, %" PRIuS " bytes.\n", ppf.info.xsize,
+ ppf.info.ysize, input_bytes.size());
+ }
+
+ if (!ValidateArgs(args) || !SetDistance(args, cmdline, &args.settings)) {
+ return EXIT_FAILURE;
+ }
+
+ if (!args.quiet) {
+ const jxl::extras::JpegSettings& s = args.settings;
+ fprintf(stderr, "Encoding [%s%s d%.3f%s %sAQ p%d %s]\n",
+ s.xyb ? "XYB" : "YUV", s.chroma_subsampling.c_str(), s.distance,
+ s.use_std_quant_tables ? " StdQuant" : "",
+ s.use_adaptive_quantization ? "" : "no", s.progressive_level,
+ s.optimize_coding ? "OPT" : "FIX");
+ }
+
+ jpegxl::tools::SpeedStats stats;
+ std::vector<uint8_t> jpeg_bytes;
+ for (size_t num_rep = 0; num_rep < args.num_reps; ++num_rep) {
+ const double t0 = jxl::Now();
+ if (!jxl::extras::EncodeJpeg(ppf, args.settings, nullptr, &jpeg_bytes)) {
+ fprintf(stderr, "jpegli encoding failed\n");
+ return EXIT_FAILURE;
+ }
+ const double t1 = jxl::Now();
+ stats.NotifyElapsed(t1 - t0);
+ stats.SetImageSize(ppf.info.xsize, ppf.info.ysize);
+ }
+
+ if (args.file_out && !args.disable_output) {
+ if (!WriteFile(args.file_out, jpeg_bytes)) {
+ fprintf(stderr, "Could not write jpeg to %s\n", args.file_out);
+ return EXIT_FAILURE;
+ }
+ }
+ if (!args.quiet) {
+ fprintf(stderr, "Compressed to %" PRIuS " bytes ", jpeg_bytes.size());
+ const size_t num_pixels = ppf.info.xsize * ppf.info.ysize;
+ const double bpp =
+ static_cast<double>(jpeg_bytes.size() * jxl::kBitsPerByte) / num_pixels;
+ fprintf(stderr, "(%.3f bpp).\n", bpp);
+ stats.Print(1);
+ }
+ return EXIT_SUCCESS;
+}
+
+} // namespace
+} // namespace tools
+} // namespace jpegxl
+
+int main(int argc, const char** argv) {
+ return jpegxl::tools::CJpegliMain(argc, argv);
+}
diff --git a/tools/cjxl_fuzzer.cc b/tools/cjxl_fuzzer.cc
index f3a1d9f..4577143 100644
--- a/tools/cjxl_fuzzer.cc
+++ b/tools/cjxl_fuzzer.cc
@@ -3,22 +3,21 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/encode.h>
+#include <jxl/encode_cxx.h>
+#include <jxl/thread_parallel_runner.h>
+#include <jxl/thread_parallel_runner_cxx.h>
#include <limits.h>
#include <stdint.h>
-#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <functional>
+#include <hwy/targets.h>
#include <random>
#include <vector>
-#include "hwy/targets.h"
-#include "jxl/encode.h"
-#include "jxl/encode_cxx.h"
-#include "jxl/thread_parallel_runner.h"
-#include "jxl/thread_parallel_runner_cxx.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/test_image.h"
@@ -120,7 +119,7 @@ bool EncodeJpegXl(const FuzzSpec& spec) {
// Reading compressed output
JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT;
while (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
- std::vector<uint8_t> buf(spec.output_buffer_size);
+ std::vector<uint8_t> buf(spec.output_buffer_size + 32);
uint8_t* next_out = buf.data();
size_t avail_out = buf.size();
process_result = JxlEncoderProcessOutput(enc, &next_out, &avail_out);
diff --git a/tools/cjxl_main.cc b/tools/cjxl_main.cc
index e43bb27..de1e118 100644
--- a/tools/cjxl_main.cc
+++ b/tools/cjxl_main.cc
@@ -11,36 +11,41 @@
// also require a change to the range-check here. The advantage is
// that this minimizes the size of libjxl.
-#include <stdint.h>
-
+#include <jxl/codestream_header.h>
+#include <jxl/encode.h>
+#include <jxl/encode_cxx.h>
+#include <jxl/thread_parallel_runner.h>
+#include <jxl/thread_parallel_runner_cxx.h>
+#include <jxl/types.h>
+
+#include <algorithm>
+#include <cerrno>
#include <cmath>
+#include <cstdint>
+#include <cstdio>
#include <cstdlib>
+#include <cstring>
#include <functional>
#include <iostream>
+#include <memory>
#include <sstream>
#include <string>
#include <thread>
#include <type_traits>
#include <vector>
-#include "jxl/codestream_header.h"
-#include "jxl/encode.h"
-#include "jxl/encode_cxx.h"
-#include "jxl/thread_parallel_runner.h"
-#include "jxl/thread_parallel_runner_cxx.h"
-#include "jxl/types.h"
#include "lib/extras/dec/apng.h"
#include "lib/extras/dec/color_hints.h"
-#include "lib/extras/dec/gif.h"
-#include "lib/extras/dec/jpg.h"
-#include "lib/extras/dec/pgx.h"
+#include "lib/extras/dec/decode.h"
#include "lib/extras/dec/pnm.h"
+#include "lib/extras/enc/jxl.h"
#include "lib/extras/time.h"
+#include "lib/jxl/base/common.h"
#include "lib/jxl/base/override.h"
#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/exif.h"
-#include "lib/jxl/size_constraints.h"
#include "tools/args.h"
#include "tools/cmdline.h"
#include "tools/codec_config.h"
@@ -52,12 +57,11 @@ namespace tools {
namespace {
inline bool ParsePhotonNoiseParameter(const char* arg, float* out) {
- return strncmp(arg, "ISO", 3) == 0 && ParseFloat(arg + 3, out) && *out > 0;
+ return ParseFloat(arg, out) && *out >= 0;
}
inline bool ParseIntensityTarget(const char* arg, float* out) {
return ParseFloat(arg, out) && *out > 0;
}
-
} // namespace
enum CjxlRetCode : int {
@@ -75,141 +79,116 @@ enum CjxlRetCode : int {
struct CompressArgs {
// CompressArgs() = default;
void AddCommandLineOptions(CommandLineParser* cmdline) {
+ std::string input_help("the input can be ");
+ if (jxl::extras::CanDecode(jxl::extras::Codec::kPNG)) {
+ input_help.append("PNG, APNG, ");
+ }
+ if (jxl::extras::CanDecode(jxl::extras::Codec::kGIF)) {
+ input_help.append("GIF, ");
+ }
+ if (jxl::extras::CanDecode(jxl::extras::Codec::kJPG)) {
+ input_help.append("JPEG, ");
+ } else {
+ input_help.append("JPEG (lossless recompression only), ");
+ }
+ if (jxl::extras::CanDecode(jxl::extras::Codec::kEXR)) {
+ input_help.append("EXR, ");
+ }
+ input_help.append("PPM, PFM, PAM, PGX, or JXL");
// Positional arguments.
- cmdline->AddPositionalOption("INPUT", /* required = */ true,
- "the input can be "
-#if JPEGXL_ENABLE_APNG
- "PNG, APNG, "
-#endif
-#if JPEGXL_ENABLE_GIF
- "GIF, "
-#endif
-#if JPEGXL_ENABLE_JPEG
- "JPEG, "
-#else
- "JPEG (lossless recompression only), "
-#endif
-#if JPEGXL_ENABLE_EXR
- "EXR, "
-#endif
- "PPM, PFM, or PGX",
+ cmdline->AddPositionalOption("INPUT", /* required = */ true, input_help,
&file_in);
- cmdline->AddPositionalOption(
- "OUTPUT", /* required = */ true,
- "the compressed JXL output file (can be omitted for benchmarking)",
- &file_out);
+ cmdline->AddPositionalOption("OUTPUT", /* required = */ true,
+ "the compressed JXL output file", &file_out);
// Flags.
- // TODO(lode): also add options to add exif/xmp/other metadata in the
- // container.
- cmdline->AddOptionValue('\0', "container", "0|1",
- "0 = Do not encode using container format (strip "
- "Exif/XMP/JPEG bitstream reconstruction data)."
- "1 = Force using container format \n"
- "(default: use only if needed).\n",
- &container, &ParseOverride, 1);
- cmdline->AddOptionValue(
- '\0', "jpeg_store_metadata", "0|1",
- ("If --lossless_jpeg=1, store JPEG reconstruction "
- "metadata in the JPEG XL container "
- "(for lossless reconstruction of the JPEG codestream)."
- "(default: 1)"),
- &jpeg_store_metadata, &ParseUnsigned, 2);
+ cmdline->AddHelpText("\nBasic options:", 0);
// Target distance/size/bpp
opt_distance_id = cmdline->AddOptionValue(
- 'd', "distance", "maxError",
- "Max. butteraugli distance, lower = higher quality.\n"
+ 'd', "distance", "DISTANCE",
+ "Target visual distance in JND units, lower = higher quality.\n"
" 0.0 = mathematically lossless. Default for already-lossy input "
"(JPEG/GIF).\n"
" 1.0 = visually lossless. Default for other input.\n"
- " Recommended range: 0.5 .. 3.0. Mutually exclusive with --quality.",
+ " Recommended range: 0.5 .. 3.0. Allowed range: 0.0 ... 25.0. "
+ "Mutually exclusive with --quality.",
&distance, &ParseFloat);
// High-level options
opt_quality_id = cmdline->AddOptionValue(
'q', "quality", "QUALITY",
- "Quality setting (is remapped to --distance). Range: -inf .. 100.\n"
- " 100 = mathematically lossless. Default for already-lossy input "
- "(JPEG/GIF).\n"
- " Other input gets encoded as per --distance default.\n"
- " Positive quality values roughly match libjpeg quality.\n"
- " Mutually exclusive with --distance.",
+ "Quality setting, higher value = higher quality. This is internally "
+ "mapped to --distance.\n"
+ " 100 = mathematically lossless. 90 = visually lossless.\n"
+ " Quality values roughly match libjpeg quality.\n"
+ " Recommended range: 68 .. 96. Allowed range: 0 .. 100. Mutually "
+ "exclusive with --distance.",
&quality, &ParseFloat);
cmdline->AddOptionValue(
'e', "effort", "EFFORT",
"Encoder effort setting. Range: 1 .. 9.\n"
- " Default: 7. Higher number is more effort (slower).",
- &effort, &ParseUnsigned, -1);
+ " Default: 7. Higher numbers allow more computation "
+ "at the expense of time.\n"
+ " For lossless, generally it will produce smaller files.\n"
+ " For lossy, higher effort should more accurately reach "
+ "the target quality.",
+ &effort, &ParseUnsigned);
- cmdline->AddOptionValue(
- '\0', "brotli_effort", "B_EFFORT",
- "Brotli effort setting. Range: 0 .. 11.\n"
- " Default: 9. Higher number is more effort (slower).",
- &brotli_effort, &ParseUnsigned, -1);
-
- cmdline->AddOptionValue(
- '\0', "faster_decoding", "0|1|2|3|4",
- "Favour higher decoding speed. 0 = default, higher "
- "values give higher speed at the expense of quality",
- &faster_decoding, &ParseUnsigned, 2);
+ cmdline->AddOptionFlag('V', "version",
+ "Print encoder library version number and exit.",
+ &version, &SetBooleanTrue);
+ cmdline->AddOptionFlag('\0', "quiet", "Be more silent", &quiet,
+ &SetBooleanTrue);
+ cmdline->AddOptionFlag('v', "verbose",
+ "Verbose output; can be repeated and also applies "
+ "to help (!).",
+ &verbose, &SetBooleanTrue);
+
+ cmdline->AddHelpText("\nAdvanced options:", 1);
+
+ opt_alpha_distance_id = cmdline->AddOptionValue(
+ 'a', "alpha_distance", "A_DISTANCE",
+ "Target visual distance for the alpha channel, lower = higher "
+ "quality.\n"
+ " 0.0 = mathematically lossless. 1.0 = visually lossless.\n"
+ " Default is to use the same value as for the color image.\n"
+ " Recommended range: 0.5 .. 3.0. Allowed range: 0.0 ... 25.0.",
+ &alpha_distance, &ParseFloat, 1);
cmdline->AddOptionFlag('p', "progressive",
- "Enable progressive/responsive decoding.",
- &progressive, &SetBooleanTrue);
-
- cmdline->AddOptionValue('\0', "premultiply", "-1|0|1",
- "Force premultiplied (associated) alpha.",
- &premultiply, &ParseSigned, 1);
-
- cmdline->AddOptionValue(
- '\0', "keep_invisible", "0|1",
- "force disable/enable preserving color of invisible "
- "pixels (default: 1 if lossless, 0 if lossy).",
- &keep_invisible, &ParseOverride, 1);
+ "Enable (more) progressive/responsive decoding.",
+ &progressive, &SetBooleanTrue, 1);
cmdline->AddOptionValue(
'\0', "group_order", "0|1",
"Order in which 256x256 groups are stored "
- "in the codestream for progressive rendering. "
- "Value not provided means 'encoder default', 0 means 'scanline order', "
- "1 means 'center-first order'.",
+ "in the codestream for progressive rendering.\n"
+ " 0 = scanline order, 1 = center-first order. Default: 0.",
&group_order, &ParseOverride, 1);
cmdline->AddOptionValue(
- '\0', "center_x", "0..XSIZE",
- "Determines the horizontal position of center for the center-first "
- "group order. The value -1 means 'use the middle of the image', "
- "other values [0..xsize) set this to a particular coordinate.",
- &center_x, &ParseInt64, 1);
+ '\0', "container", "0|1",
+ "0 = Avoid the container format unless it is needed (default)\n"
+ " 1 = Force using the container format even if it is not needed.",
+ &container, &ParseOverride, 1);
- cmdline->AddOptionValue(
- '\0', "center_y", "0..YSIZE",
- "Determines the vertical position of center for the center-first "
- "group order. The value -1 means 'use the middle of the image', "
- "other values [0..ysize) set this to a particular coordinate.",
- &center_y, &ParseInt64, 1);
-
- // Flags.
- cmdline->AddOptionFlag('\0', "progressive_ac",
- "Use the progressive mode for AC.", &progressive_ac,
- &SetBooleanTrue, 1);
-
- opt_qprogressive_ac_id = cmdline->AddOptionFlag(
- '\0', "qprogressive_ac",
- "Use the progressive mode for AC with shift quantization.",
- &qprogressive_ac, &SetBooleanTrue, 1);
+ cmdline->AddOptionValue('\0', "compress_boxes", "0|1",
+ "Disable/enable Brotli compression for metadata "
+ "boxes. Default is 1 (enabled).",
+ &compress_boxes, &ParseOverride, 1);
cmdline->AddOptionValue(
- '\0', "progressive_dc", "num_dc_frames",
- "Progressive-DC setting. Valid values are: -1, 0, 1, 2.",
- &progressive_dc, &ParseSigned, 1);
+ '\0', "brotli_effort", "B_EFFORT",
+ "Brotli effort setting. Range: 0 .. 11.\n"
+ " Default: 9. Higher number is more effort (slower).",
+ &brotli_effort, &ParseUnsigned, 1);
cmdline->AddOptionValue(
'm', "modular", "0|1",
- "Use modular mode (not provided = encoder chooses, 0 = enforce VarDCT, "
+ "Use modular mode (default = encoder chooses, 0 = enforce VarDCT, "
"1 = enforce modular mode).",
&modular, &ParseOverride, 1);
@@ -217,206 +196,288 @@ struct CompressArgs {
opt_lossless_jpeg_id = cmdline->AddOptionValue(
'j', "lossless_jpeg", "0|1",
"If the input is JPEG, losslessly transcode JPEG, "
- "rather than using reencode pixels.",
+ "rather than using reencode pixels. Default is 1 (losslessly "
+ "transcode)",
&lossless_jpeg, &ParseUnsigned, 1);
cmdline->AddOptionValue(
- '\0', "jpeg_reconstruction_cfl", "0|1",
- "Enable/disable chroma-from-luma (CFL) for lossless "
- "JPEG reconstruction.",
- &jpeg_reconstruction_cfl, &ParseOverride, 2);
-
- cmdline->AddOptionValue(
'\0', "num_threads", "N",
"Number of worker threads (-1 == use machine default, "
"0 == do not use multithreading).",
&num_threads, &ParseSigned, 1);
- cmdline->AddOptionValue('\0', "num_reps", "N",
- "How many times to compress. (For benchmarking).",
- &num_reps, &ParseUnsigned, 1);
-
cmdline->AddOptionValue(
- '\0', "photon_noise", "ISO3200",
- "Adds noise to the image emulating photographic film noise. "
- "The higher the given number, the grainier the image will be. "
- "As an example, a value of 100 gives low noise whereas a value "
- "of 3200 gives a lot of noise. The default value is 0.",
+ '\0', "photon_noise_iso", "ISO_FILM_SPEED",
+ "Adds noise to the image emulating photographic film or sensor noise.\n"
+ " Higher number = grainier image, e.g. 100 gives a low amount of "
+ "noise,\n"
+ " 3200 gives a lot of noise. Default is 0.",
&photon_noise_iso, &ParsePhotonNoiseParameter, 1);
cmdline->AddOptionValue(
- '\0', "dots", "0|1",
- "Force disable/enable dots generation. "
- "(not provided = default, 0 = disable, 1 = enable).",
- &dots, &ParseOverride, 1);
+ '\0', "intensity_target", "N",
+ "Upper bound on the intensity level present in the image, in nits.\n"
+ " Default is 0, which means 'choose a sensible default "
+ "value based on the color encoding.",
+ &intensity_target, &ParseIntensityTarget, 1);
cmdline->AddOptionValue(
- '\0', "patches", "0|1",
- "Force disable/enable patches generation. "
- "(not provided = default, 0 = disable, 1 = enable).",
- &patches, &ParseOverride, 1);
+ 'x', "dec-hints", "key=value",
+ "This is useful for 'raw' formats like PPM that cannot store "
+ "colorspace information\n"
+ " and metadata, or to strip or modify metadata in formats that do.\n"
+ " The key 'color_space' indicates an enumerated ColorEncoding, for "
+ "example:\n"
+ " -x color_space=RGB_D65_SRG_Per_SRG is sRGB with perceptual "
+ "rendering intent\n"
+ " -x color_space=RGB_D65_202_Rel_PeQ is Rec.2100 PQ with relative "
+ "rendering intent\n"
+ " The key 'icc_pathname' refers to a binary file containing an ICC "
+ "profile.\n"
+ " The keys 'exif', 'xmp', and 'jumbf' refer to a binary file "
+ "containing metadata;\n"
+ " existing metadata of the same type will be overwritten.\n"
+ " Specific metadata can be stripped using e.g. -x strip=exif",
+ &color_hints_proxy, &ParseAndAppendKeyValue<ColorHintsProxy>, 1);
+
+ cmdline->AddHelpText("\nExpert options:", 2);
+
+ cmdline->AddOptionValue(
+ '\0', "jpeg_store_metadata", "0|1",
+ ("If --lossless_jpeg=1, store JPEG reconstruction "
+ "metadata in the JPEG XL container.\n"
+ " This allows reconstruction of the JPEG codestream. Default: 1."),
+ &jpeg_store_metadata, &ParseUnsigned, 2);
+
+ cmdline->AddOptionValue('\0', "codestream_level", "K",
+ "The codestream level. Either `-1`, `5` or `10`.",
+ &codestream_level, &ParseInt64, 2);
+
+ cmdline->AddOptionValue('\0', "faster_decoding", "0|1|2|3|4",
+ "0 = default, higher values improve decode speed "
+ "at the expense of quality or density.",
+ &faster_decoding, &ParseUnsigned, 2);
+
+ cmdline->AddOptionValue('\0', "premultiply", "-1|0|1",
+ "Force premultiplied (associated) alpha.",
+ &premultiply, &ParseSigned, 2);
+
+ cmdline->AddOptionValue('\0', "keep_invisible", "0|1",
+ "disable/enable preserving color of invisible "
+ "pixels (default: 1 if lossless, 0 if lossy).",
+ &keep_invisible, &ParseOverride, 2);
cmdline->AddOptionValue(
- '\0', "resampling", "-1|1|2|4|8",
- "Resampling for extra channels. Default of -1 applies resampling only "
- "for low quality. Value 1 does no downsampling (1x1), 2 does 2x2 "
- "downsampling, 4 is for 4x4 downsampling, and 8 for 8x8 downsampling.",
- &resampling, &ParseSigned, 0);
+ '\0', "center_x", "-1..XSIZE",
+ "Determines the horizontal position of center for the center-first "
+ "group order.\n"
+ " Default -1 means 'middle of the image', "
+ "values [0..xsize) set this to a particular coordinate.",
+ &center_x, &ParseInt64, 2);
+
+ cmdline->AddOptionValue(
+ '\0', "center_y", "-1..YSIZE",
+ "Determines the vertical position of center for the center-first "
+ "group order.\n"
+ " Default -1 means 'middle of the image', "
+ "values [0..ysize) set this to a particular coordinate.",
+ &center_y, &ParseInt64, 2);
+
+ // Flags.
+ cmdline->AddOptionFlag('\0', "progressive_ac",
+ "Use the progressive mode for AC.", &progressive_ac,
+ &SetBooleanTrue, 2);
+
+ cmdline->AddOptionFlag(
+ '\0', "qprogressive_ac",
+ "Use the progressive mode for AC with shift quantization.",
+ &qprogressive_ac, &SetBooleanTrue, 2);
cmdline->AddOptionValue(
- '\0', "ec_resampling", "-1|1|2|4|8",
- "Resampling for extra channels. Default of -1 applies resampling only "
- "for low quality. Value 1 does no downsampling (1x1), 2 does 2x2 "
- "downsampling, 4 is for 4x4 downsampling, and 8 for 8x8 downsampling.",
- &ec_resampling, &ParseSigned, 2);
+ '\0', "progressive_dc", "num_dc_frames",
+ "Progressive-DC setting. Valid values are: -1, 0, 1, 2.",
+ &progressive_dc, &ParseInt64, 2);
+
+ cmdline->AddOptionValue('\0', "resampling", "-1|1|2|4|8",
+ "Resampling for color channels. Default of -1 "
+ "applies resampling only for very low quality.\n"
+ " 1 = downsampling (1x1), 2 = 2x2 downsampling, "
+ "4 = 4x4 downsampling, 8 = 8x8 downsampling.",
+ &resampling, &ParseInt64, 2);
+
+ cmdline->AddOptionValue('\0', "ec_resampling", "-1|1|2|4|8",
+ "Resampling for extra channels. Same as "
+ "--resampling but for extra channels like alpha.",
+ &ec_resampling, &ParseInt64, 2);
cmdline->AddOptionFlag('\0', "already_downsampled",
- "Do not downsample the given input before encoding, "
+ "Do not downsample before encoding, "
"but still signal that the decoder should upsample.",
&already_downsampled, &SetBooleanTrue, 2);
cmdline->AddOptionValue(
+ '\0', "upsampling_mode", "-1|0|1",
+ "Upsampling mode the decoder should use. Mostly useful in combination "
+ "with --already_downsampled. Value -1 means default (non-separable "
+ "upsampling), 0 means nearest neighbor (useful for pixel art)",
+ &upsampling_mode, &ParseInt64, 2);
+
+ cmdline->AddOptionValue(
'\0', "epf", "-1|0|1|2|3",
- "Edge preserving filter level, -1 to 3. "
- "Value -1 means: default (encoder chooses), 0 to 3 set a strength.",
- &epf, &ParseSigned, 1);
+ "Edge preserving filter level, 0-3. "
+ "Default -1 means encoder chooses, 0-3 set a strength.",
+ &epf, &ParseInt64, 2);
+
+ cmdline->AddOptionValue('\0', "gaborish", "0|1",
+ "Force disable/enable the gaborish filter. Default "
+ "is 'encoder chooses'",
+ &gaborish, &ParseOverride, 2);
+
+ cmdline->AddOptionValue('\0', "override_bitdepth", "BITDEPTH",
+ "Default is zero (use the input image bit depth); "
+ "if nonzero, override the bit depth",
+ &override_bitdepth, &ParseUnsigned, 2);
+
+ cmdline->AddHelpText("\nOptions for experimentation / benchmarking:", 3);
+
+ cmdline->AddOptionValue('\0', "noise", "0|1",
+ "Force disable/enable adaptive noise generation "
+ "(experimental). Default "
+ "is 'encoder chooses'",
+ &noise, &ParseOverride, 3);
cmdline->AddOptionValue(
- '\0', "gaborish", "0|1",
- "Force disable/enable the gaborish filter. "
- "(not provided = default, 0 = disable, 1 = enable).",
- &gaborish, &ParseOverride, 1);
+ '\0', "jpeg_reconstruction_cfl", "0|1",
+ "Enable/disable chroma-from-luma (CFL) for lossless "
+ "JPEG reconstruction.",
+ &jpeg_reconstruction_cfl, &ParseOverride, 3);
+
+ cmdline->AddOptionValue('\0', "num_reps", "N",
+ "How many times to compress. (For benchmarking).",
+ &num_reps, &ParseUnsigned, 3);
+
+ cmdline->AddOptionFlag('\0', "streaming_input",
+ "Enable streaming processing of the input file "
+ "(works only for PPM and PGM input files).",
+ &streaming_input, &SetBooleanTrue, 3);
+ cmdline->AddOptionFlag('\0', "streaming_output",
+ "Enable incremental writing of the output file.",
+ &streaming_output, &SetBooleanTrue, 3);
+ cmdline->AddOptionFlag('\0', "disable_output",
+ "No output file will be written (for benchmarking)",
+ &disable_output, &SetBooleanTrue, 3);
cmdline->AddOptionValue(
- '\0', "intensity_target", "N",
- "Upper bound on the intensity level present in the image in nits. "
- "Leaving this set to its default of 0 lets libjxl choose a sensible "
- "default "
- "value based on the color encoding.",
- &intensity_target, &ParseIntensityTarget, 1);
+ '\0', "dots", "0|1",
+ "Force disable/enable dots generation. "
+ "(not provided = default, 0 = disable, 1 = enable).",
+ &dots, &ParseOverride, 3);
cmdline->AddOptionValue(
- 'x', "dec-hints", "key=value",
- "color_space indicates the ColorEncoding, see Description();\n"
- "icc_pathname refers to a binary file containing an ICC profile.",
- &color_hints, &ParseAndAppendKeyValue, 1);
+ '\0', "patches", "0|1",
+ "Force disable/enable patches generation. "
+ "(not provided = default, 0 = disable, 1 = enable).",
+ &patches, &ParseOverride, 3);
cmdline->AddOptionValue(
- '\0', "override_bitdepth", "0=use from image, 1-32=override",
- "If nonzero, store the given bit depth in the JPEG XL file metadata"
- " (1-32), instead of using the bit depth from the original input"
- " image.",
- &override_bitdepth, &ParseUnsigned, 2);
+ '\0', "frame_indexing", "INDICES",
+ // TODO(tfish): Add a more convenient vanilla alternative.
+ "INDICES is of the form '^(0*|1[01]*)'. The i-th position indicates "
+ "whether the\n"
+ " i-th frame will be indexed in the frame index box.",
+ &frame_indexing, &ParseString, 3);
+
+ cmdline->AddOptionFlag('\0', "allow_expert_options",
+ "Allow specifying advanced options; this allows "
+ "setting effort to 10, for\n"
+ " somewhat better lossless compression at the "
+ "cost of a massive speed hit.",
+ &allow_expert_options, &SetBooleanTrue, 3);
+
+ cmdline->AddHelpText("\nModular mode options:", 4);
// modular mode options
cmdline->AddOptionValue(
- 'I', "iterations", "F",
- "[modular encoding] Fraction of pixels used to learn MA trees as "
- "a percentage. -1 = default, 0 = no MA and fast decode, 50 = "
- "default value, 100 = all."
- "Higher values use more encoder memory.",
- &modular_ma_tree_learning_percent, &ParseFloat, 2);
+ 'I', "iterations", "PERCENT",
+ "Percentage of pixels used to learn MA trees. Higher values use\n"
+ " more encoder memory and can result in better compression. Default "
+ "of -1 means\n"
+ " the encoder chooses. Zero means no MA trees are used.",
+ &modular_ma_tree_learning_percent, &ParseFloat, 4);
cmdline->AddOptionValue(
'C', "modular_colorspace", "K",
- ("[modular encoding] color transform: -1=default, 0=RGB (none), "
- "1-41=RCT (6=YCoCg, default: try several, depending on speed)"),
- &modular_colorspace, &ParseSigned, 1);
+ ("Color transform: -1 = default (try several per group, depending\n"
+ " on effort), 0 = RGB (none), 1-41 = fixed RCT (6 = YCoCg)."),
+ &modular_colorspace, &ParseInt64, 4);
opt_modular_group_size_id = cmdline->AddOptionValue(
'g', "modular_group_size", "K",
- "[modular encoding] group size: -1 == default. 0 => 128, "
- "1 => 256, 2 => 512, 3 => 1024",
- &modular_group_size, &ParseSigned, 1);
+ "Group size: -1 = default (let the encoder choose),\n"
+ " 0 = 128x128, 1 = 256x256, 2 = 512x512, 3 = 1024x1024.",
+ &modular_group_size, &ParseInt64, 4);
cmdline->AddOptionValue(
'P', "modular_predictor", "K",
- "[modular encoding] predictor(s) to use: 0=zero, "
- "1=left, 2=top, 3=avg0, 4=select, 5=gradient, 6=weighted, "
- "7=topright, 8=topleft, 9=leftleft, 10=avg1, 11=avg2, 12=avg3, "
- "13=toptop predictive average "
- "14=mix 5 and 6, 15=mix everything. If unset, uses default 14, "
- "at slowest speed default 15.",
- &modular_predictor, &ParseSigned, 1);
+ "Predictor(s) to use: 0=zero, 1=left, 2=top, 3=avg0, 4=select,\n"
+ " 5=gradient, 6=weighted, 7=topright, 8=topleft, 9=leftleft, "
+ "10=avg1, 11=avg2, 12=avg3,\n"
+ " 13=toptop predictive average, 14=mix 5 and 6, 15=mix everything.\n"
+ " Default is 14 at effort < 9 and 15 at effort 9.",
+ &modular_predictor, &ParseInt64, 4);
cmdline->AddOptionValue(
'E', "modular_nb_prev_channels", "K",
- "[modular encoding] number of extra MA tree properties to use",
- &modular_nb_prev_channels, &ParseSigned, 2);
+ "Number of extra (previous-channel) MA tree properties to use.",
+ &modular_nb_prev_channels, &ParseInt64, 4);
cmdline->AddOptionValue(
'\0', "modular_palette_colors", "K",
- "[modular encoding] Use color palette if number of colors is smaller "
- "than or equal to this, or -1 to use the encoder default.",
- &modular_palette_colors, &ParseSigned, 1);
+ "Use palette if number of colors is smaller than or equal to this.",
+ &modular_palette_colors, &ParseInt64, 4);
cmdline->AddOptionFlag(
'\0', "modular_lossy_palette",
- "[modular encoding] quantize to a palette that has fewer entries than "
- "would be necessary for perfect preservation; for the time being, it "
- "is "
- "recommended to set --palette=0 with this option to use the default "
- "palette only",
- &modular_lossy_palette, &SetBooleanTrue, 1);
-
- cmdline->AddOptionValue(
- 'X', "pre-compact", "PERCENT",
- "[modular encoding] Use Global channel palette if the number of "
- "colors is smaller than this percentage of range. "
- "Use 0-100 to set an explicit percentage, -1 to use the encoder "
- "default.",
- &modular_channel_colors_global_percent, &ParseFloat, 2);
+ "Use delta palette in a lossy way; it is recommended to also\n"
+ " set --modular_palette_colors=0 with this "
+ "option to use the default palette only.",
+ &modular_lossy_palette, &SetBooleanTrue, 4);
+
+ cmdline->AddOptionValue('X', "pre-compact", "PERCENT",
+ "Use global channel palette if the number of "
+ "sample values is smaller\n"
+ " than this percentage of the nominal range. ",
+ &modular_channel_colors_global_percent, &ParseFloat,
+ 4);
cmdline->AddOptionValue(
'Y', "post-compact", "PERCENT",
- "[modular encoding] Use Local (per-group) channel palette if the "
- "number "
- "of colors is smaller than this percentage of range. Use 0-100 to set "
- "an explicit percentage, -1 to use the encoder default.",
- &modular_channel_colors_group_percent, &ParseFloat, 2);
-
- cmdline->AddOptionValue('\0', "codestream_level", "K",
- "The codestream level. Either `-1`, `5` or `10`.",
- &codestream_level, &ParseSigned, 2);
-
- opt_responsive_id = cmdline->AddOptionValue(
- 'R', "responsive", "K",
- "[modular encoding] do Squeeze transform, 0=false, "
- "1=true (default: true if lossy, false if lossless)",
- &responsive, &ParseSigned, 1);
-
- cmdline->AddOptionFlag('V', "version",
- "Print encoder library version number and exit.",
- &version, &SetBooleanTrue, 1);
-
- cmdline->AddOptionFlag('\0', "quiet", "Be more silent", &quiet,
- &SetBooleanTrue, 1);
-
- cmdline->AddOptionValue(
- '\0', "frame_indexing", "string",
- // TODO(tfish): Add a more convenient vanilla alternative.
- "If non-empty, a string matching '^(0*|1[01]*)'. If this string has a "
- "'1' in i-th position, then the i-th frame will be indexed in "
- "the frame index box.",
- &frame_indexing, &ParseString, 1);
-
- cmdline->AddOptionFlag(
- 'v', "verbose",
- "Verbose output; can be repeated, also applies to help (!).", &verbose,
- &SetBooleanTrue);
+ "Use local (per-group) channel palette if the "
+ "number of sample values is\n"
+ " smaller than this percentage of the nominal range.",
+ &modular_channel_colors_group_percent, &ParseFloat, 4);
+
+ opt_responsive_id =
+ cmdline->AddOptionValue('R', "responsive", "K",
+ "Do the Squeeze transform, 0=false, "
+ "1=true (default: 1 if lossy, 0 if lossless)",
+ &responsive, &ParseInt64, 4);
}
// Common flags.
bool version = false;
jxl::Override container = jxl::Override::kDefault;
bool quiet = false;
+ bool disable_output = false;
const char* file_in = nullptr;
const char* file_out = nullptr;
jxl::Override print_profile = jxl::Override::kDefault;
+ bool streaming_input = false;
+ bool streaming_output = false;
// Decoding source image flags
- jxl::extras::ColorHints color_hints;
+ ColorHintsProxy color_hints_proxy;
// JXL flags
size_t override_bitdepth = 0;
@@ -424,9 +485,6 @@ struct CompressArgs {
size_t num_reps = 1;
float intensity_target = 0;
- // Filename for the user provided saliency-map.
- std::string saliency_map_filename;
-
// Whether to perform lossless transcoding with kVarDCT or kJPEG encoding.
// If true, attempts to load JPEG coefficients instead of pixels.
// Reset to false if input image is not a JPEG.
@@ -440,10 +498,11 @@ struct CompressArgs {
bool progressive = false;
bool progressive_ac = false;
bool qprogressive_ac = false;
- int32_t progressive_dc = -1;
+ int64_t progressive_dc = -1;
bool modular_lossy_palette = false;
int32_t premultiply = -1;
bool already_downsampled = false;
+ int64_t upsampling_mode = -1;
jxl::Override jpeg_reconstruction_cfl = jxl::Override::kDefault;
jxl::Override modular = jxl::Override::kDefault;
jxl::Override keep_invisible = jxl::Override::kDefault;
@@ -451,38 +510,40 @@ struct CompressArgs {
jxl::Override patches = jxl::Override::kDefault;
jxl::Override gaborish = jxl::Override::kDefault;
jxl::Override group_order = jxl::Override::kDefault;
+ jxl::Override compress_boxes = jxl::Override::kDefault;
+ jxl::Override noise = jxl::Override::kDefault;
size_t faster_decoding = 0;
- int32_t resampling = -1;
- int32_t ec_resampling = -1;
- int32_t epf = -1;
+ int64_t resampling = -1;
+ int64_t ec_resampling = -1;
+ int64_t epf = -1;
int64_t center_x = -1;
int64_t center_y = -1;
- int32_t modular_group_size = -1;
- int32_t modular_predictor = -1;
- int32_t modular_colorspace = -1;
+ int64_t modular_group_size = -1;
+ int64_t modular_predictor = -1;
+ int64_t modular_colorspace = -1;
float modular_channel_colors_global_percent = -1.f;
float modular_channel_colors_group_percent = -1.f;
- int32_t modular_palette_colors = -1;
- int32_t modular_nb_prev_channels = -1;
+ int64_t modular_palette_colors = -1;
+ int64_t modular_nb_prev_channels = -1;
float modular_ma_tree_learning_percent = -1.f;
float photon_noise_iso = 0;
- int32_t codestream_level = -1;
- int32_t responsive = -1;
+ int64_t codestream_level = -1;
+ int64_t responsive = -1;
float distance = 1.0;
+ float alpha_distance = 1.0;
size_t effort = 7;
size_t brotli_effort = 9;
std::string frame_indexing;
- // Will get passed on to AuxOut.
- // jxl::InspectorImage3F inspector_image3f;
+ bool allow_expert_options = false;
// References (ids) of specific options to check if they were matched.
CommandLineParser::OptionId opt_lossless_jpeg_id = -1;
CommandLineParser::OptionId opt_responsive_id = -1;
CommandLineParser::OptionId opt_distance_id = -1;
+ CommandLineParser::OptionId opt_alpha_distance_id = -1;
CommandLineParser::OptionId opt_quality_id = -1;
- CommandLineParser::OptionId opt_qprogressive_ac_id = -1;
CommandLineParser::OptionId opt_modular_group_size_id = -1;
};
@@ -506,144 +567,430 @@ std::string DistanceFromArgs(const CompressArgs& args) {
}
void PrintMode(jxl::extras::PackedPixelFile& ppf, const double decode_mps,
- size_t num_bytes, const CompressArgs& args) {
+ size_t num_bytes, const CompressArgs& args,
+ jpegxl::tools::CommandLineParser& cmdline) {
const char* mode = ModeFromArgs(args);
const std::string distance = DistanceFromArgs(args);
if (args.lossless_jpeg) {
- fprintf(stderr, "Read JPEG image with %" PRIuS " bytes.\n", num_bytes);
+ cmdline.VerbosePrintf(1, "Read JPEG image with %" PRIuS " bytes.\n",
+ num_bytes);
} else {
- fprintf(stderr,
- "Read %" PRIuS "x%" PRIuS " image, %" PRIuS " bytes, %.1f MP/s\n",
- static_cast<size_t>(ppf.info.xsize),
- static_cast<size_t>(ppf.info.ysize), num_bytes, decode_mps);
+ cmdline.VerbosePrintf(
+ 1, "Read %" PRIuS "x%" PRIuS " image, %" PRIuS " bytes, %.1f MP/s\n",
+ static_cast<size_t>(ppf.info.xsize),
+ static_cast<size_t>(ppf.info.ysize), num_bytes, decode_mps);
}
- fprintf(stderr, "Encoding [%s%s, %s, effort: %" PRIuS,
- (args.container == jxl::Override::kOn ? "Container | " : ""), mode,
- distance.c_str(), args.effort);
+ cmdline.VerbosePrintf(
+ 0, "Encoding [%s%s, %s, effort: %" PRIuS,
+ (args.container == jxl::Override::kOn ? "Container | " : ""), mode,
+ distance.c_str(), args.effort);
if (args.container == jxl::Override::kOn) {
if (args.lossless_jpeg && args.jpeg_store_metadata)
- fprintf(stderr, " | JPEG reconstruction data");
+ cmdline.VerbosePrintf(0, " | JPEG reconstruction data");
if (!ppf.metadata.exif.empty())
- fprintf(stderr, " | %" PRIuS "-byte Exif", ppf.metadata.exif.size());
+ cmdline.VerbosePrintf(0, " | %" PRIuS "-byte Exif",
+ ppf.metadata.exif.size());
if (!ppf.metadata.xmp.empty())
- fprintf(stderr, " | %" PRIuS "-byte XMP", ppf.metadata.xmp.size());
+ cmdline.VerbosePrintf(0, " | %" PRIuS "-byte XMP",
+ ppf.metadata.xmp.size());
if (!ppf.metadata.jumbf.empty())
- fprintf(stderr, " | %" PRIuS "-byte JUMBF", ppf.metadata.jumbf.size());
+ cmdline.VerbosePrintf(0, " | %" PRIuS "-byte JUMBF",
+ ppf.metadata.jumbf.size());
}
- fprintf(stderr, "], \n");
+ cmdline.VerbosePrintf(0, "]\n");
}
-} // namespace tools
-} // namespace jpegxl
+bool IsJPG(const std::vector<uint8_t>& image_data) {
+ return (image_data.size() >= 2 && image_data[0] == 0xFF &&
+ image_data[1] == 0xD8);
+}
-namespace {
+using flag_check_fn = std::function<std::string(int64_t)>;
+using flag_check_float_fn = std::function<std::string(float)>;
template <typename T>
-void SetFlagFrameOptionOrDie(const char* flag_name, T flag_value,
- JxlEncoderFrameSettings* frame_settings,
- JxlEncoderFrameSettingId encoder_option) {
- if (JXL_ENC_SUCCESS !=
- (std::is_same<T, float>::value
- ? JxlEncoderFrameSettingsSetFloatOption(frame_settings,
- encoder_option, flag_value)
- : JxlEncoderFrameSettingsSetOption(frame_settings, encoder_option,
- flag_value))) {
- std::cerr << "Setting encoder option from flag --" << flag_name
- << " failed." << std::endl;
+void ProcessFlag(
+ const char* flag_name, T flag_value,
+ JxlEncoderFrameSettingId encoder_option,
+ jxl::extras::JXLCompressParams* params,
+ flag_check_fn flag_check = [](T x) { return std::string(); }) {
+ std::string error = flag_check(flag_value);
+ if (!error.empty()) {
+ std::cerr << "Invalid flag value for --" << flag_name << ": " << error
+ << std::endl;
exit(EXIT_FAILURE);
}
+ params->options.emplace_back(
+ jxl::extras::JXLOption(encoder_option, flag_value, 0));
}
-void SetDistanceFromFlags(JxlEncoderFrameSettings* jxl_encoder_frame_settings,
- jpegxl::tools::CommandLineParser* cmdline,
- jpegxl::tools::CompressArgs* args,
+void ProcessBoolFlag(jxl::Override flag_value,
+ JxlEncoderFrameSettingId encoder_option,
+ jxl::extras::JXLCompressParams* params) {
+ if (flag_value != jxl::Override::kDefault) {
+ int64_t value = flag_value == jxl::Override::kOn ? 1 : 0;
+ params->options.emplace_back(
+ jxl::extras::JXLOption(encoder_option, value, 0));
+ }
+}
+
+void SetDistanceFromFlags(CommandLineParser* cmdline, CompressArgs* args,
+ jxl::extras::JXLCompressParams* params,
const jxl::extras::Codec& codec) {
bool distance_set = cmdline->GetOption(args->opt_distance_id)->matched();
+ bool alpha_distance_set =
+ cmdline->GetOption(args->opt_alpha_distance_id)->matched();
bool quality_set = cmdline->GetOption(args->opt_quality_id)->matched();
+ if ((distance_set && (args->distance != 0.0)) && args->lossless_jpeg) {
+ std::cerr << "Must not set non-zero distance in combination with "
+ "--lossless_jpeg=1, which is set by default."
+ << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ if ((quality_set && (args->quality != 100)) && args->lossless_jpeg) {
+ std::cerr << "Must not set quality below 100 in combination with "
+ "--lossless_jpeg=1, which is set by default"
+ << std::endl;
+ exit(EXIT_FAILURE);
+ }
if (quality_set) {
if (distance_set) {
std::cerr << "Must not set both --distance and --quality." << std::endl;
exit(EXIT_FAILURE);
}
- double distance = args->quality >= 100 ? 0.0
- : args->quality >= 30
- ? 0.1 + (100 - args->quality) * 0.09
- : 6.4 + pow(2.5, (30 - args->quality) / 5.0) / 6.25;
- args->distance = distance;
+ args->distance = JxlEncoderDistanceFromQuality(args->quality);
distance_set = true;
}
+
if (!distance_set) {
bool lossy_input = (codec == jxl::extras::Codec::kJPG ||
codec == jxl::extras::Codec::kGIF);
args->distance = lossy_input ? 0.0 : 1.0;
+ } else if (args->distance > 0) {
+ args->lossless_jpeg = 0;
}
- if (JXL_ENC_SUCCESS !=
- JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, args->distance)) {
- std::cerr << "Setting frame distance failed." << std::endl;
+ params->distance = args->distance;
+ params->alpha_distance =
+ alpha_distance_set ? args->alpha_distance : params->distance;
+}
+
+void ProcessFlags(const jxl::extras::Codec codec,
+ const jxl::extras::PackedPixelFile& ppf,
+ const std::vector<uint8_t>* jpeg_bytes,
+ CommandLineParser* cmdline, CompressArgs* args,
+ jxl::extras::JXLCompressParams* params) {
+ // Tuning flags.
+ ProcessBoolFlag(args->modular, JXL_ENC_FRAME_SETTING_MODULAR, params);
+ ProcessBoolFlag(args->keep_invisible, JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE,
+ params);
+ ProcessBoolFlag(args->dots, JXL_ENC_FRAME_SETTING_DOTS, params);
+ ProcessBoolFlag(args->patches, JXL_ENC_FRAME_SETTING_PATCHES, params);
+ ProcessBoolFlag(args->gaborish, JXL_ENC_FRAME_SETTING_GABORISH, params);
+ ProcessBoolFlag(args->group_order, JXL_ENC_FRAME_SETTING_GROUP_ORDER, params);
+ ProcessBoolFlag(args->noise, JXL_ENC_FRAME_SETTING_NOISE, params);
+
+ params->allow_expert_options = args->allow_expert_options;
+
+ if (!args->frame_indexing.empty()) {
+ bool must_be_all_zeros = args->frame_indexing[0] != '1';
+ for (char c : args->frame_indexing) {
+ if (c == '1') {
+ if (must_be_all_zeros) {
+ std::cerr << "Invalid --frame_indexing. If the first character is "
+ "'0', all must be '0'."
+ << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ } else if (c != '0') {
+ std::cerr << "Invalid --frame_indexing. Must match the pattern "
+ "'^(0*|1[01]*)$'."
+ << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ }
+ }
+
+ ProcessFlag(
+ "effort", static_cast<int64_t>(args->effort),
+ JXL_ENC_FRAME_SETTING_EFFORT, params, [args](int64_t x) -> std::string {
+ if (args->allow_expert_options) {
+ return (1 <= x && x <= 10) ? "" : "Valid range is {1, 2, ..., 10}.";
+ } else {
+ return (1 <= x && x <= 9) ? "" : "Valid range is {1, 2, ..., 9}.";
+ }
+ });
+ ProcessFlag("brotli_effort", static_cast<int64_t>(args->brotli_effort),
+ JXL_ENC_FRAME_SETTING_BROTLI_EFFORT, params,
+ [](int64_t x) -> std::string {
+ return (-1 <= x && x <= 11)
+ ? ""
+ : "Valid range is {-1, 0, 1, ..., 11}.";
+ });
+ ProcessFlag(
+ "epf", args->epf, JXL_ENC_FRAME_SETTING_EPF, params,
+ [](int64_t x) -> std::string {
+ return (-1 <= x && x <= 3) ? "" : "Valid range is {-1, 0, 1, 2, 3}.\n";
+ });
+ ProcessFlag("faster_decoding", static_cast<int64_t>(args->faster_decoding),
+ JXL_ENC_FRAME_SETTING_DECODING_SPEED, params,
+ [](int64_t x) -> std::string {
+ return (0 <= x && x <= 4) ? ""
+ : "Valid range is {0, 1, 2, 3, 4}.\n";
+ });
+ ProcessFlag("resampling", args->resampling, JXL_ENC_FRAME_SETTING_RESAMPLING,
+ params, [](int64_t x) -> std::string {
+ return (x == -1 || x == 1 || x == 2 || x == 4 || x == 8)
+ ? ""
+ : "Valid values are {-1, 1, 2, 4, 8}.\n";
+ });
+ ProcessFlag("ec_resampling", args->ec_resampling,
+ JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, params,
+ [](int64_t x) -> std::string {
+ return (x == -1 || x == 1 || x == 2 || x == 4 || x == 8)
+ ? ""
+ : "Valid values are {-1, 1, 2, 4, 8}.\n";
+ });
+ ProcessFlag("photon_noise_iso", args->photon_noise_iso,
+ JXL_ENC_FRAME_SETTING_PHOTON_NOISE, params);
+ ProcessFlag("already_downsampled",
+ static_cast<int64_t>(args->already_downsampled),
+ JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED, params);
+ if (args->already_downsampled) params->already_downsampled = args->resampling;
+
+ SetDistanceFromFlags(cmdline, args, params, codec);
+
+ if (args->group_order != jxl::Override::kOn &&
+ (args->center_x != -1 || args->center_y != -1)) {
+ std::cerr << "Invalid flag combination. Setting --center_x or --center_y "
+ << "requires setting --group_order=1" << std::endl;
exit(EXIT_FAILURE);
}
-}
+ ProcessFlag("center_x", args->center_x,
+ JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X, params,
+ [](int64_t x) -> std::string {
+ if (x < -1) {
+ return "Valid values are: -1 or [0 .. xsize).";
+ }
+ return "";
+ });
+ ProcessFlag("center_y", args->center_y,
+ JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y, params,
+ [](int64_t x) -> std::string {
+ if (x < -1) {
+ return "Valid values are: -1 or [0 .. ysize).";
+ }
+ return "";
+ });
+
+ // Progressive/responsive mode settings.
+ bool responsive_set = cmdline->GetOption(args->opt_responsive_id)->matched();
+
+ ProcessFlag("progressive_dc", args->progressive_dc,
+ JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, params,
+ [](int64_t x) -> std::string {
+ return (-1 <= x && x <= 2) ? ""
+ : "Valid range is {-1, 0, 1, 2}.\n";
+ });
+ ProcessFlag("progressive_ac", static_cast<int64_t>(args->progressive_ac),
+ JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, params);
+
+ if (args->progressive) {
+ args->qprogressive_ac = true;
+ args->responsive = 1;
+ responsive_set = true;
+ }
+ if (responsive_set) {
+ ProcessFlag("responsive", args->responsive,
+ JXL_ENC_FRAME_SETTING_RESPONSIVE, params);
+ }
+ if (args->qprogressive_ac) {
+ ProcessFlag("qprogressive_ac", static_cast<int64_t>(1),
+ JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC, params);
+ }
-using flag_check_fn = std::function<std::string(int64_t)>;
-using flag_check_float_fn = std::function<std::string(float)>;
+ // Modular mode related.
+ ProcessFlag("modular_group_size", args->modular_group_size,
+ JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, params,
+ [](int64_t x) -> std::string {
+ return (-1 <= x && x <= 3)
+ ? ""
+ : "Invalid --modular_group_size. Valid "
+ "range is {-1, 0, 1, 2, 3}.\n";
+ });
+ ProcessFlag("modular_predictor", args->modular_predictor,
+ JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, params,
+ [](int64_t x) -> std::string {
+ return (-1 <= x && x <= 15)
+ ? ""
+ : "Invalid --modular_predictor. Valid "
+ "range is {-1, 0, 1, ..., 15}.\n";
+ });
+ ProcessFlag("modular_colorspace", args->modular_colorspace,
+ JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, params,
+ [](int64_t x) -> std::string {
+ return (-1 <= x && x <= 41)
+ ? ""
+ : "Invalid --modular_colorspace. Valid range is "
+ "{-1, 0, 1, ..., 41}.\n";
+ });
+ ProcessFlag("modular_ma_tree_learning_percent",
+ args->modular_ma_tree_learning_percent,
+ JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, params,
+ [](float x) -> std::string {
+ return -1 <= x && x <= 100
+ ? ""
+ : "Invalid --modular_ma_tree_learning_percent, Valid"
+ "rang is [-1, 100].\n";
+ });
+ ProcessFlag("modular_nb_prev_channels", args->modular_nb_prev_channels,
+ JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, params,
+ [](int64_t x) -> std::string {
+ return (-1 <= x && x <= 11)
+ ? ""
+ : "Invalid --modular_nb_prev_channels. Valid "
+ "range is {-1, 0, 1, ..., 11}.\n";
+ });
+ if (args->modular_lossy_palette) {
+ if (args->progressive || args->qprogressive_ac) {
+ fprintf(stderr,
+ "WARNING: --modular_lossy_palette is ignored in "
+ "progressive mode.\n");
+ args->modular_lossy_palette = false;
+ }
+ }
+ ProcessFlag("modular_lossy_palette",
+ static_cast<int64_t>(args->modular_lossy_palette),
+ JXL_ENC_FRAME_SETTING_LOSSY_PALETTE, params);
+ ProcessFlag("modular_palette_colors", args->modular_palette_colors,
+ JXL_ENC_FRAME_SETTING_PALETTE_COLORS, params,
+ [](int64_t x) -> std::string {
+ return -1 <= x ? ""
+ : "Invalid --modular_palette_colors, must "
+ "be -1 or non-negative\n";
+ });
+ ProcessFlag("modular_channel_colors_global_percent",
+ args->modular_channel_colors_global_percent,
+ JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, params,
+ [](float x) -> std::string {
+ return (-1 <= x && x <= 100)
+ ? ""
+ : "Invalid --modular_channel_colors_global_percent. "
+ "Valid "
+ "range is [-1, 100].\n";
+ });
+ ProcessFlag("modular_channel_colors_group_percent",
+ args->modular_channel_colors_group_percent,
+ JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, params,
+ [](float x) -> std::string {
+ return (-1 <= x && x <= 100)
+ ? ""
+ : "Invalid --modular_channel_colors_group_percent. "
+ "Valid "
+ "range is [-1, 100].\n";
+ });
+
+ if (args->num_threads < -1) {
+ std::cerr
+ << "Invalid flag value for --num_threads: must be -1, 0 or positive."
+ << std::endl;
+ exit(EXIT_FAILURE);
+ }
+ // JPEG specific options.
+ if (jpeg_bytes) {
+ ProcessBoolFlag(args->jpeg_reconstruction_cfl,
+ JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL, params);
+ ProcessBoolFlag(args->compress_boxes,
+ JXL_ENC_FRAME_SETTING_JPEG_COMPRESS_BOXES, params);
+ }
+ // Set per-frame options.
+ for (size_t num_frame = 0; num_frame < ppf.num_frames(); ++num_frame) {
+ if (num_frame < args->frame_indexing.size() &&
+ args->frame_indexing[num_frame] == '1') {
+ int64_t value = 1;
+ params->options.emplace_back(
+ jxl::extras::JXLOption(JXL_ENC_FRAME_INDEX_BOX, value, num_frame));
+ }
+ }
+ // Copy over the rest of the non-option params.
+ params->use_container = args->container == jxl::Override::kOn;
+ params->jpeg_store_metadata = args->jpeg_store_metadata;
+ params->intensity_target = args->intensity_target;
+ params->override_bitdepth = args->override_bitdepth;
+ params->codestream_level = args->codestream_level;
+ params->premultiply = args->premultiply;
+ params->compress_boxes = args->compress_boxes != jxl::Override::kOff;
+ params->upsampling_mode = args->upsampling_mode;
+ if (codec == jxl::extras::Codec::kPNM &&
+ ppf.info.exponent_bits_per_sample == 0) {
+ params->input_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM;
+ }
-bool IsJPG(const std::vector<uint8_t>& image_data) {
- return (image_data.size() >= 2 && image_data[0] == 0xFF &&
- image_data[1] == 0xD8);
+ // If a metadata field is set to an empty value, it is stripped.
+ // Make sure we also strip it when the input image is read with AddJPEGFrame
+ (void)args->color_hints_proxy.target.Foreach(
+ [&params](const std::string& key,
+ const std::string& value) -> jxl::Status {
+ if (value == "") {
+ if (key == "exif") params->jpeg_strip_exif = true;
+ if (key == "xmp") params->jpeg_strip_xmp = true;
+ if (key == "jumbf") params->jpeg_strip_jumbf = true;
+ }
+ return true;
+ });
}
-// TODO(tfish): Replace with non-C-API library function.
-// Implementation is in extras/.
-jxl::Status GetPixeldata(const std::vector<uint8_t>& image_data,
- const jxl::extras::ColorHints& color_hints,
- jxl::extras::PackedPixelFile& ppf,
- jxl::extras::Codec& codec) {
- // Any valid encoding is larger (ensures codecs can read the first few bytes).
- constexpr size_t kMinBytes = 9;
-
- if (image_data.size() < kMinBytes) return JXL_FAILURE("Input too small.");
- jxl::Span<const uint8_t> encoded(image_data);
-
- ppf.info.orientation = JXL_ORIENT_IDENTITY;
- jxl::SizeConstraints size_constraints;
-
- const auto choose_codec = [&]() {
-#if JPEGXL_ENABLE_APNG
- if (jxl::extras::DecodeImageAPNG(encoded, color_hints, size_constraints,
- &ppf)) {
- return jxl::extras::Codec::kPNG;
- }
-#endif
- if (jxl::extras::DecodeImagePGX(encoded, color_hints, size_constraints,
- &ppf)) {
- return jxl::extras::Codec::kPGX;
- } else if (jxl::extras::DecodeImagePNM(encoded, color_hints,
- size_constraints, &ppf)) {
- return jxl::extras::Codec::kPNM;
+struct JxlOutputProcessor {
+ bool SetOutputPath(const std::string& path) {
+ outfile.reset(new FileWrapper(path, "wb"));
+ if (!*outfile) {
+ fprintf(stderr,
+ "Could not open %s for writing\n"
+ "Error: %s",
+ path.c_str(), strerror(errno));
+ return false;
}
-#if JPEGXL_ENABLE_GIF
- if (jxl::extras::DecodeImageGIF(encoded, color_hints, size_constraints,
- &ppf)) {
- return jxl::extras::Codec::kGIF;
+ return true;
+ }
+
+ JxlEncoderOutputProcessor GetOutputProcessor() {
+ return JxlEncoderOutputProcessor{this, GetBuffer, ReleaseBuffer, Seek,
+ SetFinalizedPosition};
+ }
+
+ static void* GetBuffer(void* opaque, size_t* size) {
+ JxlOutputProcessor* self = reinterpret_cast<JxlOutputProcessor*>(opaque);
+ self->output.resize(*size);
+ return self->output.data();
+ }
+
+ static void ReleaseBuffer(void* opaque, size_t written_bytes) {
+ JxlOutputProcessor* self = reinterpret_cast<JxlOutputProcessor*>(opaque);
+ if (*self->outfile && fwrite(self->output.data(), 1, written_bytes,
+ *self->outfile) != written_bytes) {
+ JXL_WARNING("Failed to write %" PRIuS " bytes to output", written_bytes);
}
-#endif
-#if JPEGXL_ENABLE_JPEG
- if (jxl::extras::DecodeImageJPG(encoded, color_hints, size_constraints,
- &ppf)) {
- return jxl::extras::Codec::kJPG;
+ self->output.clear();
+ }
+
+ static void Seek(void* opaque, uint64_t position) {
+ JxlOutputProcessor* self = reinterpret_cast<JxlOutputProcessor*>(opaque);
+ if (*self->outfile && fseek(*self->outfile, position, SEEK_SET) != 0) {
+ JXL_WARNING("Failed to seek output.");
}
-#endif
- // TODO(tfish): Bring back EXR and PSD.
- return jxl::extras::Codec::kUnknown;
- };
- codec = choose_codec();
- if (codec == jxl::extras::Codec::kUnknown) {
- return JXL_FAILURE("Codecs failed to decode input.");
}
- return true;
-}
-} // namespace
+ static void SetFinalizedPosition(void* opaque, uint64_t finalized_position) {
+ JxlOutputProcessor* self = reinterpret_cast<JxlOutputProcessor*>(opaque);
+ self->finalized_position = finalized_position;
+ }
+
+ std::vector<uint8_t> output;
+ size_t finalized_position = 0;
+ std::unique_ptr<FileWrapper> outfile;
+};
+
+} // namespace tools
+} // namespace jpegxl
int main(int argc, char** argv) {
std::string version = jpegxl::tools::CodecConfigString(JxlEncoderVersion());
@@ -672,9 +1019,15 @@ int main(int argc, char** argv) {
return jpegxl::tools::CjxlRetCode::OK;
}
- if (!args.file_out && !args.quiet) {
+ if (!args.file_out && !args.disable_output) {
+ std::cerr
+ << "No output file specified and --disable_output flag not passed."
+ << std::endl;
+ exit(EXIT_FAILURE);
+ }
+
+ if (args.file_out && args.disable_output && !args.quiet) {
fprintf(stderr,
- "No output file specified.\n"
"Encoding will be performed, but the result will be discarded.\n");
}
@@ -682,533 +1035,151 @@ int main(int argc, char** argv) {
// Depending on flags-settings, we want to either load a JPEG and
// faithfully convert it to JPEG XL, or load (JPEG or non-JPEG)
// pixel data.
- std::vector<uint8_t> image_data;
- jxl::extras::PackedPixelFile ppf;
- jxl::extras::Codec codec = jxl::extras::Codec::kUnknown;
- double decode_mps = 0;
- size_t pixels = 0;
- if (!jpegxl::tools::ReadFile(args.file_in, &image_data)) {
+ jpegxl::tools::FileWrapper f(args.file_in, "rb");
+ if (!f) {
std::cerr << "Reading image data failed." << std::endl;
exit(EXIT_FAILURE);
}
- if (!IsJPG(image_data)) args.lossless_jpeg = 0;
- if (!args.lossless_jpeg) {
- const double t0 = jxl::Now();
- jxl::Status status = GetPixeldata(image_data, args.color_hints, ppf, codec);
- if (!status) {
- std::cerr << "Getting pixel data." << std::endl;
+ jxl::extras::JXLCompressParams params;
+ jxl::extras::PackedPixelFile ppf;
+ jxl::extras::Codec codec = jxl::extras::Codec::kUnknown;
+ std::vector<uint8_t> image_data;
+ std::vector<uint8_t>* jpeg_bytes = nullptr;
+ jxl::extras::ChunkedPNMDecoder pnm_dec;
+ size_t pixels = 0;
+ if (args.streaming_input) {
+ pnm_dec.f = f;
+ if (!DecodeImagePNM(&pnm_dec, args.color_hints_proxy.target, &ppf)) {
+ std::cerr << "PNM decoding failed." << std::endl;
exit(EXIT_FAILURE);
}
- if (ppf.frames.empty()) {
- std::cerr << "No frames on input file." << std::endl;
+ codec = jxl::extras::Codec::kPNM;
+ args.lossless_jpeg = 0;
+ pixels = ppf.info.xsize * ppf.info.ysize;
+ } else {
+ double decode_mps = 0;
+ if (!jpegxl::tools::ReadFile(f, &image_data)) {
+ std::cerr << "Reading image data failed." << std::endl;
exit(EXIT_FAILURE);
}
+ if (!jpegxl::tools::IsJPG(image_data)) args.lossless_jpeg = 0;
+ ProcessFlags(codec, ppf, jpeg_bytes, &cmdline, &args, &params);
+ if (!args.lossless_jpeg) {
+ const double t0 = jxl::Now();
+ jxl::Status status = jxl::extras::DecodeBytes(
+ jxl::Bytes(image_data), args.color_hints_proxy.target, &ppf, nullptr,
+ &codec);
- const double t1 = jxl::Now();
- pixels = ppf.info.xsize * ppf.info.ysize;
- decode_mps = pixels * ppf.info.num_color_channels * 1E-6 / (t1 - t0);
- }
-
- JxlEncoderPtr enc = JxlEncoderMake(/*memory_manager=*/nullptr);
- JxlEncoder* jxl_encoder = enc.get();
- JxlThreadParallelRunnerPtr runner;
- std::vector<uint8_t> compressed;
- size_t num_worker_threads;
- jpegxl::tools::SpeedStats stats;
- for (size_t num_rep = 0; num_rep < args.num_reps; ++num_rep) {
- const double t0 = jxl::Now();
- JxlEncoderReset(jxl_encoder);
- if (args.num_threads != 0) {
- num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads();
- {
- int64_t flag_num_worker_threads = args.num_threads;
- if (flag_num_worker_threads > -1) {
- num_worker_threads = flag_num_worker_threads;
- }
- }
- if (runner == nullptr) {
- runner = JxlThreadParallelRunnerMake(
- /*memory_manager=*/nullptr, num_worker_threads);
- }
- if (JXL_ENC_SUCCESS !=
- JxlEncoderSetParallelRunner(jxl_encoder, JxlThreadParallelRunner,
- runner.get())) {
- std::cerr << "JxlEncoderSetParallelRunner failed." << std::endl;
- return EXIT_FAILURE;
- }
- }
- JxlEncoderFrameSettings* jxl_encoder_frame_settings =
- JxlEncoderFrameSettingsCreate(jxl_encoder, nullptr);
-
- auto process_flag = [&jxl_encoder_frame_settings](
- const char* flag_name, int64_t flag_value,
- JxlEncoderFrameSettingId encoder_option,
- const flag_check_fn& flag_check) {
- std::string error = flag_check(flag_value);
- if (!error.empty()) {
- std::cerr << "Invalid flag value for --" << flag_name << ": " << error
- << std::endl;
+ if (!status) {
+ std::cerr << "Getting pixel data failed." << std::endl;
exit(EXIT_FAILURE);
}
- SetFlagFrameOptionOrDie(flag_name, flag_value, jxl_encoder_frame_settings,
- encoder_option);
- };
- auto process_float_flag = [&jxl_encoder_frame_settings](
- const char* flag_name, float flag_value,
- JxlEncoderFrameSettingId encoder_option,
- const flag_check_float_fn& flag_check) {
- std::string error = flag_check(flag_value);
- if (!error.empty()) {
- std::cerr << "Invalid flag value for --" << flag_name << ": " << error
- << std::endl;
+ if (ppf.frames.empty()) {
+ std::cerr << "No frames on input file." << std::endl;
exit(EXIT_FAILURE);
}
- SetFlagFrameOptionOrDie(flag_name, flag_value, jxl_encoder_frame_settings,
- encoder_option);
- };
-
- auto process_bool_flag = [&jxl_encoder_frame_settings](
- const char* flag_name,
- jxl::Override flag_value,
- JxlEncoderFrameSettingId encoder_option) {
- if (flag_value != jxl::Override::kDefault) {
- SetFlagFrameOptionOrDie(flag_name,
- flag_value == jxl::Override::kOn ? 1 : 0,
- jxl_encoder_frame_settings, encoder_option);
- }
- };
-
- { // Processing tuning flags.
- process_bool_flag("modular", args.modular, JXL_ENC_FRAME_SETTING_MODULAR);
- process_bool_flag("keep_invisible", args.keep_invisible,
- JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE);
- process_bool_flag("dots", args.dots, JXL_ENC_FRAME_SETTING_DOTS);
- process_bool_flag("patches", args.patches, JXL_ENC_FRAME_SETTING_PATCHES);
- process_bool_flag("gaborish", args.gaborish,
- JXL_ENC_FRAME_SETTING_GABORISH);
- process_bool_flag("group_order", args.group_order,
- JXL_ENC_FRAME_SETTING_GROUP_ORDER);
-
- if (!args.frame_indexing.empty()) {
- bool must_be_all_zeros = args.frame_indexing[0] != '1';
- for (char c : args.frame_indexing) {
- if (c == '1') {
- if (must_be_all_zeros) {
- std::cerr
- << "Invalid --frame_indexing. If the first character is "
- "'0', all must be '0'."
- << std::endl;
- return EXIT_FAILURE;
- }
- } else if (c != '0') {
- std::cerr << "Invalid --frame_indexing. Must match the pattern "
- "'^(0*|1[01]*)$'."
- << std::endl;
- return EXIT_FAILURE;
- }
- }
- }
-
- process_flag(
- "effort", args.effort, JXL_ENC_FRAME_SETTING_EFFORT,
- [](int64_t x) -> std::string {
- return (1 <= x && x <= 9) ? "" : "Valid range is {1, 2, ..., 9}.";
- });
- process_flag(
- "brotli_effort", args.brotli_effort,
- JXL_ENC_FRAME_SETTING_BROTLI_EFFORT, [](int64_t x) -> std::string {
- return (-1 <= x && x <= 11) ? ""
- : "Valid range is {-1, 0, 1, ..., 11}.";
- });
- process_flag("epf", args.epf, JXL_ENC_FRAME_SETTING_EPF,
- [](int64_t x) -> std::string {
- return (-1 <= x && x <= 3)
- ? ""
- : "Valid range is {-1, 0, 1, 2, 3}.\n";
- });
- process_flag(
- "faster_decoding", args.faster_decoding,
- JXL_ENC_FRAME_SETTING_DECODING_SPEED, [](int64_t x) -> std::string {
- return (0 <= x && x <= 4) ? ""
- : "Valid range is {0, 1, 2, 3, 4}.\n";
- });
- process_flag("resampling", args.resampling,
- JXL_ENC_FRAME_SETTING_RESAMPLING,
- [](int64_t x) -> std::string {
- return (x == -1 || x == 1 || x == 4 || x == 8)
- ? ""
- : "Valid values are {-1, 1, 2, 4, 8}.\n";
- });
- process_flag("ec_resampling", args.ec_resampling,
- JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING,
- [](int64_t x) -> std::string {
- return (x == -1 || x == 1 || x == 4 || x == 8)
- ? ""
- : "Valid values are {-1, 1, 2, 4, 8}.\n";
- });
- SetFlagFrameOptionOrDie("photon_noise_iso", args.photon_noise_iso,
- jxl_encoder_frame_settings,
- JXL_ENC_FRAME_SETTING_PHOTON_NOISE);
- SetFlagFrameOptionOrDie("already_downsampled",
- static_cast<int32_t>(args.already_downsampled),
- jxl_encoder_frame_settings,
- JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED);
- SetDistanceFromFlags(jxl_encoder_frame_settings, &cmdline, &args, codec);
-
- if (args.group_order != jxl::Override::kOn &&
- (args.center_x != -1 || args.center_y != -1)) {
- std::cerr
- << "Invalid flag combination. Setting --center_x or --center_y "
- << "requires setting --group_order=1" << std::endl;
- return EXIT_FAILURE;
- }
- process_flag("center_x", args.center_x,
- JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X,
- [](int64_t x) -> std::string {
- if (x < -1) {
- return "Valid values are: -1 or [0 .. xsize).";
- }
- return "";
- });
- process_flag("center_y", args.center_y,
- JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y,
- [](int64_t x) -> std::string {
- if (x < -1) {
- return "Valid values are: -1 or [0 .. ysize).";
- }
- return "";
- });
+ pixels = ppf.info.xsize * ppf.info.ysize;
+ const double t1 = jxl::Now();
+ decode_mps = pixels * ppf.info.num_color_channels * 1E-6 / (t1 - t0);
}
- { // Progressive/responsive mode settings.
- bool qprogressive_ac_set =
- cmdline.GetOption(args.opt_qprogressive_ac_id)->matched();
- int32_t qprogressive_ac = args.qprogressive_ac ? 1 : 0;
- bool responsive_set =
- cmdline.GetOption(args.opt_responsive_id)->matched();
- int32_t responsive = args.responsive ? 1 : 0;
-
- process_flag(
- "progressive_dc", args.progressive_dc,
- JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, [](int64_t x) -> std::string {
- return (-1 <= x && x <= 2) ? "" : "Valid range is {-1, 0, 1, 2}.\n";
- });
- SetFlagFrameOptionOrDie(
- "progressive_ac", static_cast<int32_t>(args.progressive_ac),
- jxl_encoder_frame_settings, JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC);
-
- if (args.progressive) {
- qprogressive_ac = 1;
- qprogressive_ac_set = true;
- responsive = 1;
- responsive_set = true;
- }
- if (responsive_set) {
- SetFlagFrameOptionOrDie("responsive", responsive,
- jxl_encoder_frame_settings,
- JXL_ENC_FRAME_SETTING_RESPONSIVE);
- }
- if (qprogressive_ac_set) {
- SetFlagFrameOptionOrDie("qprogressive_ac", qprogressive_ac,
- jxl_encoder_frame_settings,
- JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC);
- }
- }
- { // Modular mode related.
- // TODO(firsching): consider doing more validation after image size is
- // known, i.e. set to 512 if 256 would be silly using
- // opt_modular_group_size_id.
- process_flag("modular_group_size", args.modular_group_size,
- JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE,
- [](int64_t x) -> std::string {
- return (-1 <= x && x <= 3)
- ? ""
- : "Invalid --modular_group_size. Valid "
- "range is {-1, 0, 1, 2, 3}.\n";
- });
- process_flag("modular_predictor", args.modular_predictor,
- JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR,
- [](int64_t x) -> std::string {
- return (-1 <= x && x <= 15)
- ? ""
- : "Invalid --modular_predictor. Valid "
- "range is {-1, 0, 1, ..., 15}.\n";
- });
- process_flag(
- "modular_colorspace", args.modular_colorspace,
- JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE,
- [](int64_t x) -> std::string {
- return (-1 <= x && x <= 41)
- ? ""
- : "Invalid --modular_colorspace. Valid range is "
- "{-1, 0, 1, ..., 41}.\n";
- });
- process_float_flag(
- "modular_ma_tree_learning_percent",
- args.modular_ma_tree_learning_percent,
- JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT,
- [](float x) -> std::string {
- return -1 <= x && x <= 100
- ? ""
- : "Invalid --modular_ma_tree_learning_percent, Valid"
- "rang is [-1, 100].\n";
- });
- process_flag("modular_nb_prev_channels", args.modular_nb_prev_channels,
- JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS,
- [](int64_t x) -> std::string {
- return (-1 <= x && x <= 11)
- ? ""
- : "Invalid --modular_nb_prev_channels. Valid "
- "range is {-1, 0, 1, ..., 11}.\n";
- });
- SetFlagFrameOptionOrDie("modular_lossy_palette",
- static_cast<int32_t>(args.modular_lossy_palette),
- jxl_encoder_frame_settings,
- JXL_ENC_FRAME_SETTING_LOSSY_PALETTE);
- process_flag("modular_palette_colors", args.modular_palette_colors,
- JXL_ENC_FRAME_SETTING_PALETTE_COLORS,
- [](int64_t x) -> std::string {
- return -1 <= x ? ""
- : "Invalid --modular_palette_colors, must "
- "be -1 or non-negative\n";
- });
- process_float_flag(
- "modular_channel_colors_global_percent",
- args.modular_channel_colors_global_percent,
- JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT,
- [](float x) -> std::string {
- return (-1 <= x && x <= 100)
- ? ""
- : "Invalid --modular_channel_colors_global_percent. "
- "Valid "
- "range is [-1, 100].\n";
- });
- process_float_flag(
- "modular_channel_colors_group_percent",
- args.modular_channel_colors_group_percent,
- JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT,
- [](float x) -> std::string {
- return (-1 <= x && x <= 100)
- ? ""
- : "Invalid --modular_channel_colors_group_percent. "
- "Valid "
- "range is [-1, 100].\n";
- });
- }
-
- bool use_container = args.container == jxl::Override::kOn;
- if (!ppf.metadata.exif.empty() || !ppf.metadata.xmp.empty() ||
- !ppf.metadata.jumbf.empty() || !ppf.metadata.iptc.empty() ||
- (args.lossless_jpeg && args.jpeg_store_metadata)) {
- use_container = true;
- }
- if (use_container) args.container = jxl::Override::kOn;
-
- if (!ppf.metadata.exif.empty()) {
- jxl::InterpretExif(ppf.metadata.exif, &ppf.info.orientation);
- }
-
- if (JXL_ENC_SUCCESS !=
- JxlEncoderUseContainer(jxl_encoder, static_cast<int>(use_container))) {
- std::cerr << "JxlEncoderUseContainer failed." << std::endl;
- return EXIT_FAILURE;
+ if (!args.quiet) {
+ PrintMode(ppf, decode_mps, image_data.size(), args, cmdline);
}
- if (num_rep == 0 && !args.quiet)
- PrintMode(ppf, decode_mps, image_data.size(), args);
-
- if (args.lossless_jpeg && IsJPG(image_data)) {
+ if (args.lossless_jpeg && jpegxl::tools::IsJPG(image_data)) {
if (!cmdline.GetOption(args.opt_lossless_jpeg_id)->matched()) {
std::cerr << "Note: Implicit-default for JPEG is lossless-transcoding. "
<< "To silence this message, set --lossless_jpeg=(1|0)."
<< std::endl;
}
- if (args.jpeg_store_metadata) {
- if (JXL_ENC_SUCCESS !=
- JxlEncoderStoreJPEGMetadata(jxl_encoder, JXL_TRUE)) {
- std::cerr << "Storing JPEG metadata failed. " << std::endl;
- return EXIT_FAILURE;
- }
- }
- process_bool_flag("jpeg_reconstruction_cfl", args.jpeg_reconstruction_cfl,
- JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL);
- if (JXL_ENC_SUCCESS != JxlEncoderAddJPEGFrame(jxl_encoder_frame_settings,
- image_data.data(),
- image_data.size())) {
- std::cerr << "JxlEncoderAddJPEGFrame() failed." << std::endl;
- return EXIT_FAILURE;
- }
- } else { // Do JxlEncoderAddImageFrame().
- size_t num_alpha_channels = 0; // Adjusted below.
- {
- JxlBasicInfo basic_info = ppf.info;
- if (basic_info.alpha_bits > 0) num_alpha_channels = 1;
- basic_info.intensity_target = args.intensity_target;
- basic_info.num_extra_channels = num_alpha_channels;
- basic_info.num_color_channels = ppf.info.num_color_channels;
- const bool lossless = args.distance == 0;
- basic_info.uses_original_profile = lossless;
- if (args.override_bitdepth != 0) {
- basic_info.bits_per_sample = args.override_bitdepth;
- basic_info.exponent_bits_per_sample =
- args.override_bitdepth == 32 ? 8 : 0;
- }
- if (JXL_ENC_SUCCESS !=
- JxlEncoderSetCodestreamLevel(jxl_encoder, args.codestream_level)) {
- std::cerr << "Setting --codestream_level failed." << std::endl;
- return EXIT_FAILURE;
- }
- if (JXL_ENC_SUCCESS !=
- JxlEncoderSetBasicInfo(jxl_encoder, &basic_info)) {
- std::cerr << "JxlEncoderSetBasicInfo() failed." << std::endl;
- return EXIT_FAILURE;
- }
- if (lossless &&
- JXL_ENC_SUCCESS != JxlEncoderSetFrameLossless(
- jxl_encoder_frame_settings, JXL_TRUE)) {
- std::cerr << "JxlEncoderSetFrameLossless() failed." << std::endl;
- return EXIT_FAILURE;
- }
- }
+ jpeg_bytes = &image_data;
+ }
+ }
- if (!ppf.icc.empty()) {
- if (JXL_ENC_SUCCESS != JxlEncoderSetICCProfile(jxl_encoder,
- ppf.icc.data(),
- ppf.icc.size())) {
- std::cerr << "JxlEncoderSetICCProfile() failed." << std::endl;
- return EXIT_FAILURE;
- }
- } else {
- if (JXL_ENC_SUCCESS !=
- JxlEncoderSetColorEncoding(jxl_encoder, &ppf.color_encoding)) {
- std::cerr << "JxlEncoderSetColorEncoding() failed." << std::endl;
- return EXIT_FAILURE;
- }
- }
+ ProcessFlags(codec, ppf, jpeg_bytes, &cmdline, &args, &params);
- for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) {
- const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame];
- const jxl::extras::PackedImage& pimage = pframe.color;
- JxlPixelFormat ppixelformat = pimage.format;
- {
- if (JXL_ENC_SUCCESS !=
- JxlEncoderSetFrameHeader(jxl_encoder_frame_settings,
- &pframe.frame_info)) {
- std::cerr << "JxlEncoderSetFrameHeader() failed." << std::endl;
- return EXIT_FAILURE;
- }
- }
- if (num_frame < args.frame_indexing.size() &&
- args.frame_indexing[num_frame] == '1') {
- if (JXL_ENC_SUCCESS !=
- JxlEncoderFrameSettingsSetOption(jxl_encoder_frame_settings,
- JXL_ENC_FRAME_INDEX_BOX, 1)) {
- std::cerr << "Setting option JXL_ENC_FRAME_INDEX_BOX failed."
- << std::endl;
- return EXIT_FAILURE;
- }
- }
- JxlEncoderStatus enc_status;
- {
- if (num_alpha_channels > 0) {
- JxlExtraChannelInfo extra_channel_info;
- JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA,
- &extra_channel_info);
- enc_status = JxlEncoderSetExtraChannelInfo(jxl_encoder, 0,
- &extra_channel_info);
- if (JXL_ENC_SUCCESS != enc_status) {
- std::cerr << "JxlEncoderSetExtraChannelInfo() failed."
- << std::endl;
- return EXIT_FAILURE;
- }
- if (args.premultiply != -1) {
- if (args.premultiply != 0 && args.premultiply != 1) {
- std::cerr << "Flag --premultiply must be one of: -1, 0, 1."
- << std::endl;
- return EXIT_FAILURE;
- }
- extra_channel_info.alpha_premultiplied = args.premultiply;
- }
- // We take the extra channel blend info frame_info, but don't do
- // clamping.
- JxlBlendInfo extra_channel_blend_info =
- pframe.frame_info.layer_info.blend_info;
- extra_channel_blend_info.clamp = JXL_FALSE;
- JxlEncoderSetExtraChannelBlendInfo(jxl_encoder_frame_settings, 0,
- &extra_channel_blend_info);
- }
- enc_status =
- JxlEncoderAddImageFrame(jxl_encoder_frame_settings, &ppixelformat,
- pimage.pixels(), pimage.pixels_size);
- if (JXL_ENC_SUCCESS != enc_status) {
- std::cerr << "JxlEncoderAddImageFrame() failed." << std::endl;
- return EXIT_FAILURE;
- }
- // Only set extra channel buffer if is is provided non-interleaved.
- if (!pframe.extra_channels.empty()) {
- enc_status = JxlEncoderSetExtraChannelBuffer(
- jxl_encoder_frame_settings, &ppixelformat,
- pframe.extra_channels[0].pixels(),
- pframe.extra_channels[0].stride *
- pframe.extra_channels[0].ysize,
- 0);
- if (JXL_ENC_SUCCESS != enc_status) {
- std::cerr << "JxlEncoderSetExtraChannelBuffer() failed."
- << std::endl;
- return EXIT_FAILURE;
- }
- }
- }
- }
+ if (!ppf.metadata.exif.empty()) {
+ jxl::InterpretExif(ppf.metadata.exif, &ppf.info.orientation);
+ }
+
+ if (!ppf.metadata.exif.empty() || !ppf.metadata.xmp.empty() ||
+ !ppf.metadata.jumbf.empty() || !ppf.metadata.iptc.empty() ||
+ (args.lossless_jpeg && args.jpeg_store_metadata)) {
+ if (args.container == jxl::Override::kDefault) {
+ args.container = jxl::Override::kOn;
+ } else if (args.container == jxl::Override::kOff) {
+ cmdline.VerbosePrintf(
+ 1, "Stripping all metadata due to explicit container=0\n");
+ ppf.metadata.exif.clear();
+ ppf.metadata.xmp.clear();
+ ppf.metadata.jumbf.clear();
+ ppf.metadata.iptc.clear();
+ args.jpeg_store_metadata = 0;
}
- JxlEncoderCloseInput(jxl_encoder);
- // Reading compressed output
- compressed.clear();
- compressed.resize(4096);
- uint8_t* next_out = compressed.data();
- size_t avail_out = compressed.size() - (next_out - compressed.data());
- JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT;
- while (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
- process_result =
- JxlEncoderProcessOutput(jxl_encoder, &next_out, &avail_out);
- if (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
- size_t offset = next_out - compressed.data();
- compressed.resize(compressed.size() * 2);
- next_out = compressed.data() + offset;
- avail_out = compressed.size() - offset;
- }
+ }
+
+ size_t num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads();
+ int64_t flag_num_worker_threads = args.num_threads;
+ if (flag_num_worker_threads > -1) {
+ num_worker_threads = flag_num_worker_threads;
+ }
+ JxlThreadParallelRunnerPtr runner = JxlThreadParallelRunnerMake(
+ /*memory_manager=*/nullptr, num_worker_threads);
+ params.runner = JxlThreadParallelRunner;
+ params.runner_opaque = runner.get();
+
+ jpegxl::tools::SpeedStats stats;
+ jpegxl::tools::JxlOutputProcessor output_processor;
+ if (args.streaming_output) {
+ if (args.file_out && !args.disable_output &&
+ !output_processor.SetOutputPath(args.file_out)) {
+ return EXIT_FAILURE;
}
- compressed.resize(next_out - compressed.data());
- if (JXL_ENC_SUCCESS != process_result) {
- std::cerr << "JxlEncoderProcessOutput failed." << std::endl;
+ params.output_processor = output_processor.GetOutputProcessor();
+ }
+ std::vector<uint8_t> compressed;
+ for (size_t num_rep = 0; num_rep < args.num_reps; ++num_rep) {
+ const double t0 = jxl::Now();
+ if (!EncodeImageJXL(params, ppf, jpeg_bytes,
+ args.streaming_output ? nullptr : &compressed)) {
+ fprintf(stderr, "EncodeImageJXL() failed.\n");
return EXIT_FAILURE;
}
-
const double t1 = jxl::Now();
stats.NotifyElapsed(t1 - t0);
stats.SetImageSize(ppf.info.xsize, ppf.info.ysize);
}
+ size_t compressed_size = args.streaming_output
+ ? output_processor.finalized_position
+ : compressed.size();
- if (args.file_out) {
+ if (!args.streaming_output && args.file_out && !args.disable_output) {
if (!jpegxl::tools::WriteFile(args.file_out, compressed)) {
std::cerr << "Could not write jxl file." << std::endl;
return EXIT_FAILURE;
}
}
if (!args.quiet) {
- const double bpp =
- static_cast<double>(compressed.size() * jxl::kBitsPerByte) / pixels;
- fprintf(stderr, "Compressed to %" PRIuS " bytes ", compressed.size());
+ if (compressed_size < 100000) {
+ cmdline.VerbosePrintf(0, "Compressed to %" PRIuS " bytes ",
+ compressed_size);
+ } else {
+ cmdline.VerbosePrintf(0, "Compressed to %.1f kB ",
+ compressed_size * 0.001);
+ }
// For lossless jpeg-reconstruction, we don't print some stats, since we
// don't have easy access to the image dimensions.
if (args.container == jxl::Override::kOn) {
- fprintf(stderr, "including container ");
+ cmdline.VerbosePrintf(0, "including container ");
}
if (!args.lossless_jpeg) {
- fprintf(stderr, "(%.3f bpp%s).\n", bpp / ppf.frames.size(),
- ppf.frames.size() == 1 ? "" : "/frame");
+ const double bpp =
+ static_cast<double>(compressed_size * jxl::kBitsPerByte) / pixels;
+ cmdline.VerbosePrintf(0, "(%.3f bpp%s).\n", bpp / ppf.num_frames(),
+ ppf.num_frames() == 1 ? "" : "/frame");
JXL_CHECK(stats.Print(num_worker_threads));
} else {
- fprintf(stderr, "\n");
+ cmdline.VerbosePrintf(0, "\n");
}
}
return EXIT_SUCCESS;
diff --git a/tools/cmdline.cc b/tools/cmdline.cc
index f777c94..29e4da8 100644
--- a/tools/cmdline.cc
+++ b/tools/cmdline.cc
@@ -29,19 +29,30 @@ void CommandLineParser::PrintHelp() const {
fprintf(out, " [OPTIONS...]\n");
bool showed_all = true;
+ int max_verbosity = 0;
for (const auto& option : options_) {
+ max_verbosity = std::max(option->verbosity_level(), max_verbosity);
if (option->verbosity_level() > verbosity) {
showed_all = false;
continue;
}
+ if (option->help_only()) {
+ fprintf(out, "%s\n", option->help_text());
+ continue;
+ }
fprintf(out, " %s\n", option->help_flags().c_str());
const char* help_text = option->help_text();
if (help_text) {
fprintf(out, " %s\n", help_text);
}
}
- fprintf(out, " -h, --help\n Prints this help message%s.\n",
- (showed_all ? "" : " (use -v to see more options)"));
+ fprintf(out, "\n -h, --help\n Prints this help message. ");
+ if (showed_all) {
+ fprintf(out, "All options are shown above.\n");
+ } else {
+ fprintf(out, "Add -v (up to a total of %i times) to see more options.\n",
+ max_verbosity);
+ }
}
bool CommandLineParser::Parse(int argc, const char* argv[]) {
@@ -91,5 +102,15 @@ bool CommandLineParser::Parse(int argc, const char* argv[]) {
return true;
}
+void CommandLineParser::VerbosePrintf(int min_verbosity, const char* format,
+ ...) const {
+ if (min_verbosity > verbosity) return;
+ va_list args;
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ fflush(stderr);
+ va_end(args);
+}
+
} // namespace tools
} // namespace jpegxl
diff --git a/tools/cmdline.h b/tools/cmdline.h
index 9b730e6..994341d 100644
--- a/tools/cmdline.h
+++ b/tools/cmdline.h
@@ -6,6 +6,7 @@
#ifndef TOOLS_CMDLINE_H_
#define TOOLS_CMDLINE_H_
+#include <stdarg.h>
#include <stdio.h>
#include <string.h>
@@ -19,7 +20,7 @@ namespace tools {
class CommandLineParser {
public:
- typedef size_t OptionId;
+ typedef int OptionId;
// An abstract class for defining command line options.
class CmdOptionInterface {
@@ -53,16 +54,24 @@ class CommandLineParser {
// Returns whether the option should be displayed as required in the help
// output. No effect on validation.
virtual bool required() const = 0;
+
+ // Returns whether the option is not really an option but just help text
+ virtual bool help_only() const = 0;
};
+ // Add help text
+ void AddHelpText(const char* help_text, int verbosity_level = 0) {
+ options_.emplace_back(new CmdHelpText(help_text, verbosity_level));
+ }
+
// Add a positional argument. Returns the id of the added option or
// kOptionError on error.
// The "required" flag indicates whether the parameter is mandatory or
// optional, but is only used for how it is displayed in the command line
// help.
OptionId AddPositionalOption(const char* name, bool required,
- const char* help_text, const char** storage,
- int verbosity_level = 0) {
+ const std::string& help_text,
+ const char** storage, int verbosity_level = 0) {
options_.emplace_back(new CmdOptionPositional(name, help_text, storage,
verbosity_level, required));
return options_.size() - 1;
@@ -113,11 +122,44 @@ class CommandLineParser {
// Return the remaining positional args
std::vector<const char*> PositionalArgs() const;
+ // Conditionally print a message to stderr
+ void VerbosePrintf(int min_verbosity, const char* format, ...) const;
+
private:
+ // Help text only.
+ class CmdHelpText : public CmdOptionInterface {
+ public:
+ CmdHelpText(const char* help_text, int verbosity_level)
+ : help_text_(help_text), verbosity_level_(verbosity_level) {}
+
+ std::string help_flags() const override { return ""; }
+ const char* help_text() const override { return help_text_; }
+ int verbosity_level() const override { return verbosity_level_; }
+ bool matched() const override { return false; }
+
+ bool Match(const char* arg, bool parse_options) const override {
+ return false;
+ }
+
+ bool Parse(const int argc, const char* argv[], int* i) override {
+ return true;
+ }
+
+ bool positional() const override { return false; }
+
+ bool required() const override { return false; }
+
+ bool help_only() const override { return true; }
+
+ private:
+ const char* help_text_;
+ const int verbosity_level_;
+ };
+
// A positional argument.
class CmdOptionPositional : public CmdOptionInterface {
public:
- CmdOptionPositional(const char* name, const char* help_text,
+ CmdOptionPositional(const char* name, const std::string& help_text,
const char** storage, int verbosity_level,
bool required)
: name_(name),
@@ -127,7 +169,7 @@ class CommandLineParser {
required_(required) {}
std::string help_flags() const override { return name_; }
- const char* help_text() const override { return help_text_; }
+ const char* help_text() const override { return help_text_.c_str(); }
int verbosity_level() const override { return verbosity_level_; }
bool matched() const override { return matched_; }
@@ -150,9 +192,11 @@ class CommandLineParser {
bool required() const override { return required_; }
+ bool help_only() const override { return false; }
+
private:
const char* name_;
- const char* help_text_;
+ const std::string help_text_;
const char** storage_;
const int verbosity_level_;
const bool required_;
@@ -252,6 +296,8 @@ class CommandLineParser {
return false;
}
+ bool help_only() const override { return false; }
+
private:
// Returns whether arg matches the short_name flag of this option.
bool MatchShort(const char* arg) const {
diff --git a/tools/codec_config.h b/tools/codec_config.h
index a4f79a6..8d1c73f 100644
--- a/tools/codec_config.h
+++ b/tools/codec_config.h
@@ -7,6 +7,7 @@
#define TOOLS_CODEC_CONFIG_H_
#include <stdint.h>
+
#include <string>
namespace jpegxl {
diff --git a/tools/color_encoding_fuzzer.cc b/tools/color_encoding_fuzzer.cc
index 087bd8b..d73dc4f 100644
--- a/tools/color_encoding_fuzzer.cc
+++ b/tools/color_encoding_fuzzer.cc
@@ -7,18 +7,20 @@
#include "lib/extras/dec/color_description.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
int TestOneInput(const uint8_t* data, size_t size) {
std::string description(reinterpret_cast<const char*>(data), size);
JxlColorEncoding c;
- (void)ParseDescription(description, &c);
+ (void)jxl::ParseDescription(description, &c);
return 0;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- return jxl::TestOneInput(data, size);
+ return jpegxl::tools::TestOneInput(data, size);
}
diff --git a/tools/comparison_viewer/CMakeLists.txt b/tools/comparison_viewer/CMakeLists.txt
index b5b5fa7..3c548d0 100644
--- a/tools/comparison_viewer/CMakeLists.txt
+++ b/tools/comparison_viewer/CMakeLists.txt
@@ -3,9 +3,9 @@
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
-find_package(Qt5 QUIET COMPONENTS Concurrent Widgets)
-if (NOT Qt5_FOUND)
- message(WARNING "Qt5 was not found. The comparison tool will not be built.")
+find_package(Qt6 QUIET COMPONENTS Concurrent Widgets)
+if (NOT Qt6_FOUND)
+ message(WARNING "Qt6 was not found. The comparison tool will not be built.")
return()
endif ()
@@ -28,10 +28,10 @@ target_include_directories(image_loading PRIVATE
$<TARGET_PROPERTY:lcms2,INCLUDE_DIRECTORIES>
)
target_link_libraries(image_loading PUBLIC
- Qt5::Widgets
- jxl-static
- jxl_threads-static
- jxl_extras-static
+ Qt6::Widgets
+ jxl-internal
+ jxl_threads
+ jxl_extras-internal
lcms2
)
@@ -51,8 +51,8 @@ add_executable(compare_codecs WIN32
)
target_link_libraries(compare_codecs
image_loading
- Qt5::Concurrent
- Qt5::Widgets
+ Qt6::Concurrent
+ Qt6::Widgets
icc_detect
)
@@ -69,6 +69,6 @@ add_executable(compare_images WIN32
)
target_link_libraries(compare_images
image_loading
- Qt5::Widgets
+ Qt6::Widgets
icc_detect
)
diff --git a/tools/comparison_viewer/codec_comparison_window.cc b/tools/comparison_viewer/codec_comparison_window.cc
index 9bf6253..0ecd579 100644
--- a/tools/comparison_viewer/codec_comparison_window.cc
+++ b/tools/comparison_viewer/codec_comparison_window.cc
@@ -31,7 +31,8 @@
#include "tools/comparison_viewer/split_image_view.h"
#include "tools/icc_detect/icc_detect.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
static constexpr char kPngSuffix[] = "png";
@@ -313,4 +314,5 @@ void CodecComparisonWindow::browseDirectory(const QDir& directory, int depth) {
}
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/comparison_viewer/codec_comparison_window.h b/tools/comparison_viewer/codec_comparison_window.h
index b157a5a..bb23314 100644
--- a/tools/comparison_viewer/codec_comparison_window.h
+++ b/tools/comparison_viewer/codec_comparison_window.h
@@ -12,18 +12,19 @@
#include <QSet>
#include <QString>
-#include "lib/jxl/base/padded_bytes.h"
-#include "lib/jxl/common.h"
+#include "lib/jxl/base/common.h"
#include "tools/comparison_viewer/ui_codec_comparison_window.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
class CodecComparisonWindow : public QMainWindow {
Q_OBJECT
public:
explicit CodecComparisonWindow(
- const QString& directory, float intensityTarget = kDefaultIntensityTarget,
+ const QString& directory,
+ float intensityTarget = jxl::kDefaultIntensityTarget,
QWidget* parent = nullptr);
~CodecComparisonWindow() override = default;
@@ -72,6 +73,7 @@ class CodecComparisonWindow : public QMainWindow {
const QByteArray monitorIccProfile_;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_COMPARISON_VIEWER_CODEC_COMPARISON_WINDOW_H_
diff --git a/tools/comparison_viewer/codec_comparison_window.ui b/tools/comparison_viewer/codec_comparison_window.ui
index 1fbda6a..85ba810 100644
--- a/tools/comparison_viewer/codec_comparison_window.ui
+++ b/tools/comparison_viewer/codec_comparison_window.ui
@@ -152,14 +152,14 @@
</layout>
</item>
<item>
- <widget class="jxl::SplitImageView" name="splitImageView" native="true"/>
+ <widget class="jpegxl::tools::SplitImageView" name="splitImageView" native="true"/>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
- <class>jxl::SplitImageView</class>
+ <class>jpegxl::tools::SplitImageView</class>
<extends>QWidget</extends>
<header>split_image_view.h</header>
<container>1</container>
diff --git a/tools/comparison_viewer/compare_codecs.cc b/tools/comparison_viewer/compare_codecs.cc
index 932765e..3ab4c8d 100644
--- a/tools/comparison_viewer/compare_codecs.cc
+++ b/tools/comparison_viewer/compare_codecs.cc
@@ -66,7 +66,7 @@ int main(int argc, char** argv) {
for (const QString& folder : folders) {
auto* const window =
- new jxl::CodecComparisonWindow(folder, intensityTarget);
+ new jpegxl::tools::CodecComparisonWindow(folder, intensityTarget);
window->setAttribute(Qt::WA_DeleteOnClose);
window->show();
}
diff --git a/tools/comparison_viewer/compare_images.cc b/tools/comparison_viewer/compare_images.cc
index cf39f88..321b2c4 100644
--- a/tools/comparison_viewer/compare_images.cc
+++ b/tools/comparison_viewer/compare_images.cc
@@ -87,13 +87,14 @@ int main(int argc, char** argv) {
parser.showHelp(EXIT_FAILURE);
}
- jxl::SplitImageView view;
+ jpegxl::tools::SplitImageView view;
- const QByteArray monitorIccProfile = jxl::GetMonitorIccProfile(&view);
+ const QByteArray monitorIccProfile =
+ jpegxl::tools::GetMonitorIccProfile(&view);
const QString leftImagePath = arguments.takeFirst();
- QImage leftImage = jxl::loadImage(leftImagePath, monitorIccProfile,
- intensityTarget, colorSpaceHint);
+ QImage leftImage = jpegxl::tools::loadImage(leftImagePath, monitorIccProfile,
+ intensityTarget, colorSpaceHint);
if (leftImage.isNull()) {
displayLoadingError(leftImagePath);
return EXIT_FAILURE;
@@ -101,8 +102,8 @@ int main(int argc, char** argv) {
view.setLeftImage(std::move(leftImage));
const QString rightImagePath = arguments.takeFirst();
- QImage rightImage = jxl::loadImage(rightImagePath, monitorIccProfile,
- intensityTarget, colorSpaceHint);
+ QImage rightImage = jpegxl::tools::loadImage(
+ rightImagePath, monitorIccProfile, intensityTarget, colorSpaceHint);
if (rightImage.isNull()) {
displayLoadingError(rightImagePath);
return EXIT_FAILURE;
@@ -111,8 +112,8 @@ int main(int argc, char** argv) {
if (!arguments.empty()) {
const QString middleImagePath = arguments.takeFirst();
- QImage middleImage = jxl::loadImage(middleImagePath, monitorIccProfile,
- intensityTarget, colorSpaceHint);
+ QImage middleImage = jpegxl::tools::loadImage(
+ middleImagePath, monitorIccProfile, intensityTarget, colorSpaceHint);
if (middleImage.isNull()) {
displayLoadingError(middleImagePath);
return EXIT_FAILURE;
diff --git a/tools/comparison_viewer/image_loading.cc b/tools/comparison_viewer/image_loading.cc
index 55bebb8..4a44dec 100644
--- a/tools/comparison_viewer/image_loading.cc
+++ b/tools/comparison_viewer/image_loading.cc
@@ -5,39 +5,56 @@
#include "tools/comparison_viewer/image_loading.h"
+#include <jxl/cms.h>
+
#include <QRgb>
#include <QThread>
+#include <cstdint>
+#include <vector>
#include "lib/extras/codec.h"
#include "lib/extras/dec/color_hints.h"
-#include "lib/jxl/base/file_io.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/color_management.h"
-#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_metadata.h"
+#include "tools/file_io.h"
+#include "tools/thread_pool_internal.h"
#include "tools/viewer/load_jxl.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using jxl::CodecInOut;
+using jxl::ColorEncoding;
+using jxl::IccBytes;
+using jxl::Image3F;
+using jxl::ImageBundle;
+using jxl::Rect;
+using jxl::Span;
+using jxl::Status;
+using jxl::ThreadPool;
+using jxl::extras::ColorHints;
namespace {
-Status loadFromFile(const QString& filename,
- const extras::ColorHints& color_hints,
+Status loadFromFile(const QString& filename, const ColorHints& color_hints,
CodecInOut* const decoded, ThreadPool* const pool) {
- PaddedBytes compressed;
- JXL_RETURN_IF_ERROR(ReadFile(filename.toStdString(), &compressed));
+ std::vector<uint8_t> compressed;
+ JXL_RETURN_IF_ERROR(
+ jpegxl::tools::ReadFile(filename.toStdString(), &compressed));
const Span<const uint8_t> compressed_span(compressed);
- return SetFromBytes(compressed_span, color_hints, decoded, pool, nullptr);
+ return jxl::SetFromBytes(compressed_span, color_hints, decoded, pool,
+ nullptr);
}
} // namespace
bool canLoadImageWithExtension(QString extension) {
extension = extension.toLower();
- size_t bitsPerSampleUnused;
- return extension == "jxl" || extension == "j" || extension == "brn" ||
- extras::CodecFromExtension("." + extension.toStdString(),
- &bitsPerSampleUnused) !=
- jxl::extras::Codec::kUnknown;
+ if (extension == "jxl" || extension == "j" || extension == "brn") {
+ return true;
+ }
+ const auto codec = jxl::extras::CodecFromPath("." + extension.toStdString());
+ return codec != jxl::extras::Codec::kUnknown;
}
QImage loadImage(const QString& filename, const QByteArray& targetIccProfile,
@@ -51,7 +68,7 @@ QImage loadImage(const QString& filename, const QByteArray& targetIccProfile,
static ThreadPoolInternal pool(QThread::idealThreadCount());
CodecInOut decoded;
- extras::ColorHints color_hints;
+ ColorHints color_hints;
if (!sourceColorSpaceHint.isEmpty()) {
color_hints.Add("color_space", sourceColorSpaceHint.toStdString());
}
@@ -62,22 +79,28 @@ QImage loadImage(const QString& filename, const QByteArray& targetIccProfile,
const ImageBundle& ib = decoded.Main();
ColorEncoding targetColorSpace;
- PaddedBytes icc;
- icc.assign(reinterpret_cast<const uint8_t*>(targetIccProfile.data()),
- reinterpret_cast<const uint8_t*>(targetIccProfile.data() +
- targetIccProfile.size()));
- if (!targetColorSpace.SetICC(std::move(icc))) {
+ bool use_fallback_profile = true;
+ if (!targetIccProfile.isEmpty()) {
+ IccBytes icc;
+ icc.assign(reinterpret_cast<const uint8_t*>(targetIccProfile.data()),
+ reinterpret_cast<const uint8_t*>(targetIccProfile.data() +
+ targetIccProfile.size()));
+ use_fallback_profile =
+ !targetColorSpace.SetICC(std::move(icc), JxlGetDefaultCms());
+ }
+ if (use_fallback_profile) {
targetColorSpace = ColorEncoding::SRGB(ib.IsGray());
}
Image3F converted;
- if (!ib.CopyTo(Rect(ib), targetColorSpace, GetJxlCms(), &converted, &pool)) {
+ if (!ib.CopyTo(Rect(ib), targetColorSpace, *JxlGetDefaultCms(), &converted,
+ &pool)) {
return QImage();
}
QImage image(converted.xsize(), converted.ysize(), QImage::Format_ARGB32);
const auto ScaleAndClamp = [](const float x) {
- return Clamp1(x * 255 + .5f, 0.f, 255.f);
+ return jxl::Clamp1(x * 255 + .5f, 0.f, 255.f);
};
if (ib.HasAlpha()) {
@@ -108,4 +131,5 @@ QImage loadImage(const QString& filename, const QByteArray& targetIccProfile,
return image;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/comparison_viewer/image_loading.h b/tools/comparison_viewer/image_loading.h
index 89b37d1..37baaef 100644
--- a/tools/comparison_viewer/image_loading.h
+++ b/tools/comparison_viewer/image_loading.h
@@ -10,9 +10,10 @@
#include <QImage>
#include <QString>
-#include "lib/jxl/common.h"
+#include "lib/jxl/base/common.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
// `extension` should not include the dot.
bool canLoadImageWithExtension(QString extension);
@@ -21,9 +22,10 @@ bool canLoadImageWithExtension(QString extension);
// specified. Thread-hostile.
QImage loadImage(const QString& filename,
const QByteArray& targetIccProfile = QByteArray(),
- float intensityTarget = kDefaultIntensityTarget,
+ float intensityTarget = jxl::kDefaultIntensityTarget,
const QString& sourceColorSpaceHint = QString());
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_COMPARISON_VIEWER_IMAGE_LOADING_H_
diff --git a/tools/comparison_viewer/settings.cc b/tools/comparison_viewer/settings.cc
index 9ef117b..ca5c0c9 100644
--- a/tools/comparison_viewer/settings.cc
+++ b/tools/comparison_viewer/settings.cc
@@ -5,7 +5,8 @@
#include "tools/comparison_viewer/settings.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
SettingsDialog::SettingsDialog(QWidget* const parent)
: QDialog(parent), settings_("JPEG XL project", "Comparison tool") {
@@ -48,4 +49,5 @@ void SettingsDialog::settingsToUi() {
ui_.grayTime->setValue(renderingSettings_.grayMSecs);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/comparison_viewer/settings.h b/tools/comparison_viewer/settings.h
index bd91f71..a54cd87 100644
--- a/tools/comparison_viewer/settings.h
+++ b/tools/comparison_viewer/settings.h
@@ -12,7 +12,8 @@
#include "tools/comparison_viewer/split_image_renderer.h"
#include "tools/comparison_viewer/ui_settings.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
class SettingsDialog : public QDialog {
Q_OBJECT
@@ -35,6 +36,7 @@ class SettingsDialog : public QDialog {
SplitImageRenderingSettings renderingSettings_;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_COMPARISON_VIEWER_SETTINGS_H_
diff --git a/tools/comparison_viewer/split_image_renderer.cc b/tools/comparison_viewer/split_image_renderer.cc
index acade64..911229c 100644
--- a/tools/comparison_viewer/split_image_renderer.cc
+++ b/tools/comparison_viewer/split_image_renderer.cc
@@ -5,10 +5,6 @@
#include "tools/comparison_viewer/split_image_renderer.h"
-#include <algorithm>
-#include <cmath>
-#include <utility>
-
#include <QEvent>
#include <QGuiApplication>
#include <QPainter>
@@ -16,8 +12,12 @@
#include <QPen>
#include <QPoint>
#include <QRect>
+#include <algorithm>
+#include <cmath>
+#include <utility>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
SplitImageRenderer::SplitImageRenderer(QWidget* const parent)
: QWidget(parent) {
@@ -32,16 +32,19 @@ SplitImageRenderer::SplitImageRenderer(QWidget* const parent)
void SplitImageRenderer::setLeftImage(QImage image) {
leftImage_ = QPixmap::fromImage(std::move(image));
+ leftImage_.setDevicePixelRatio(devicePixelRatio());
updateMinimumSize();
update();
}
void SplitImageRenderer::setRightImage(QImage image) {
rightImage_ = QPixmap::fromImage(std::move(image));
+ rightImage_.setDevicePixelRatio(devicePixelRatio());
updateMinimumSize();
update();
}
void SplitImageRenderer::setMiddleImage(QImage image) {
middleImage_ = QPixmap::fromImage(std::move(image));
+ middleImage_.setDevicePixelRatio(devicePixelRatio());
updateMinimumSize();
update();
}
@@ -181,7 +184,8 @@ void SplitImageRenderer::paintEvent(QPaintEvent* const event) {
painter.transform().inverted().map(QPointF(middleX_, 0.)).x();
QRectF middleRect = middleImage_.rect();
middleRect.setWidth(middleWidth);
- middleRect.moveCenter(QPointF(transformedMiddleX, middleRect.center().y()));
+ middleRect.moveCenter(QPointF(transformedMiddleX * devicePixelRatio(),
+ middleRect.center().y()));
middleRect.setLeft(std::round(middleRect.left()));
middleRect.setRight(std::round(middleRect.right()));
@@ -191,24 +195,30 @@ void SplitImageRenderer::paintEvent(QPaintEvent* const event) {
QRectF rightRect = rightImage_.rect();
rightRect.setLeft(middleRect.right());
- painter.drawPixmap(leftRect, leftImage_, leftRect);
- painter.drawPixmap(rightRect, rightImage_, rightRect);
- painter.drawPixmap(middleRect, middleImage_, middleRect);
+ painter.drawPixmap(QPointF(), leftImage_, leftRect);
+ painter.drawPixmap(middleRect.topLeft() / devicePixelRatio(), middleImage_,
+ middleRect);
+ painter.drawPixmap(rightRect.topLeft() / devicePixelRatio(), rightImage_,
+ rightRect);
QPen middlePen;
middlePen.setStyle(Qt::DotLine);
painter.setPen(middlePen);
- painter.drawLine(leftRect.topRight(), leftRect.bottomRight());
- painter.drawLine(rightRect.topLeft(), rightRect.bottomLeft());
+ painter.drawLine(leftRect.topRight() / devicePixelRatio(),
+ leftRect.bottomRight() / devicePixelRatio());
+ painter.drawLine(rightRect.topLeft() / devicePixelRatio(),
+ rightRect.bottomLeft() / devicePixelRatio());
}
void SplitImageRenderer::updateMinimumSize() {
- const int imagesWidth = std::max(
- std::max(leftImage_.width(), rightImage_.width()), middleImage_.width());
- const int imagesHeight =
- std::max(std::max(leftImage_.height(), rightImage_.height()),
- middleImage_.height());
- setMinimumSize(scale_ * QSize(imagesWidth, imagesHeight));
+ const QSizeF leftSize = leftImage_.deviceIndependentSize();
+ const QSizeF rightSize = rightImage_.deviceIndependentSize();
+ const QSizeF middleSize = middleImage_.deviceIndependentSize();
+ const qreal imagesWidth = std::max(
+ std::max(leftSize.width(), rightSize.width()), middleSize.width());
+ const qreal imagesHeight = std::max(
+ std::max(leftSize.height(), rightSize.height()), middleSize.height());
+ setMinimumSize((scale_ * QSizeF(imagesWidth, imagesHeight)).toSize());
}
void SplitImageRenderer::setRenderingMode(const RenderingMode newMode) {
@@ -236,4 +246,5 @@ void SplitImageRenderer::setRenderingMode(const RenderingMode newMode) {
emit renderingModeChanged(mode_);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/comparison_viewer/split_image_renderer.h b/tools/comparison_viewer/split_image_renderer.h
index decb407..5d3029a 100644
--- a/tools/comparison_viewer/split_image_renderer.h
+++ b/tools/comparison_viewer/split_image_renderer.h
@@ -15,7 +15,8 @@
#include <QWheelEvent>
#include <QWidget>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
struct SplitImageRenderingSettings {
int fadingMSecs;
@@ -85,6 +86,7 @@ class SplitImageRenderer : public QWidget {
double scale_ = 1.;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_RENDERER_H_
diff --git a/tools/comparison_viewer/split_image_view.cc b/tools/comparison_viewer/split_image_view.cc
index 76c8edc..9c27f46 100644
--- a/tools/comparison_viewer/split_image_view.cc
+++ b/tools/comparison_viewer/split_image_view.cc
@@ -11,7 +11,8 @@
#include "tools/comparison_viewer/split_image_renderer.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
SplitImageView::SplitImageView(QWidget* const parent) : QWidget(parent) {
ui_.setupUi(this);
@@ -68,4 +69,5 @@ void SplitImageView::on_settingsButton_clicked() {
}
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/comparison_viewer/split_image_view.h b/tools/comparison_viewer/split_image_view.h
index 4978750..b9c3536 100644
--- a/tools/comparison_viewer/split_image_view.h
+++ b/tools/comparison_viewer/split_image_view.h
@@ -11,7 +11,8 @@
#include "tools/comparison_viewer/settings.h"
#include "tools/comparison_viewer/ui_split_image_view.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
class SplitImageView : public QWidget {
Q_OBJECT
@@ -35,6 +36,7 @@ class SplitImageView : public QWidget {
SettingsDialog settings_;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_COMPARISON_VIEWER_SPLIT_IMAGE_VIEW_H_
diff --git a/tools/comparison_viewer/split_image_view.ui b/tools/comparison_viewer/split_image_view.ui
index 0755a58..f3b80c9 100644
--- a/tools/comparison_viewer/split_image_view.ui
+++ b/tools/comparison_viewer/split_image_view.ui
@@ -17,7 +17,7 @@
<property name="widgetResizable">
<bool>true</bool>
</property>
- <widget class="jxl::SplitImageRenderer" name="splitImageRenderer"/>
+ <widget class="jpegxl::tools::SplitImageRenderer" name="splitImageRenderer"/>
</widget>
</item>
<item>
@@ -130,7 +130,7 @@
</widget>
<customwidgets>
<customwidget>
- <class>jxl::SplitImageRenderer</class>
+ <class>jpegxl::tools::SplitImageRenderer</class>
<extends>QWidget</extends>
<header>split_image_renderer.h</header>
<container>1</container>
diff --git a/tools/conformance/conformance.py b/tools/conformance/conformance.py
index 15158bc..e4be865 100755
--- a/tools/conformance/conformance.py
+++ b/tools/conformance/conformance.py
@@ -12,6 +12,7 @@ import argparse
import json
import numpy
import os
+import shutil
import subprocess
import sys
import tempfile
@@ -166,7 +167,13 @@ def ConformanceTestRunner(args):
for reference_basename, decoded_filename in exact_tests:
reference_filename = os.path.join(test_dir,
reference_basename)
- ok = ok & CompareBinaries(reference_filename, decoded_filename)
+ binary_ok = CompareBinaries(reference_filename,
+ decoded_filename)
+ if not binary_ok and args.update_on_failure:
+ os.unlink(reference_filename)
+ shutil.copy2(decoded_filename, reference_filename)
+ binary_ok = True
+ ok = ok & binary_ok
# Validate metadata.
with open(meta_filename, 'r') as f:
@@ -182,36 +189,50 @@ def ConformanceTestRunner(args):
with open(reference_icc, 'rb') as f:
reference_icc = f.read()
- reference_npy = os.path.join(test_dir, 'reference_image.npy')
- decoded_npy = os.path.join(work_dir, 'decoded_image.npy')
+ reference_npy_fn = os.path.join(test_dir, 'reference_image.npy')
+ decoded_npy_fn = os.path.join(work_dir, 'decoded_image.npy')
- if not os.path.exists(decoded_npy):
+ if not os.path.exists(decoded_npy_fn):
ok = Failure('File not decoded: decoded_image.npy')
continue
- reference_npy = numpy.load(reference_npy)
- decoded_npy = numpy.load(decoded_npy)
+ reference_npy = numpy.load(reference_npy_fn)
+ decoded_npy = numpy.load(decoded_npy_fn)
+ frames_ok = True
for i, fd in enumerate(descriptor['frames']):
- ok = ok & CompareNPY(reference_npy, reference_icc, decoded_npy,
- decoded_icc, i, fd['rms_error'],
- fd['peak_error'])
+ frames_ok = frames_ok & CompareNPY(
+ reference_npy, reference_icc, decoded_npy,
+ decoded_icc, i, fd['rms_error'],
+ fd['peak_error'])
+
+ if not frames_ok and args.update_on_failure:
+ os.unlink(reference_npy_fn)
+ shutil.copy2(decoded_npy_fn, reference_npy_fn)
+ frames_ok = True
+ ok = ok & frames_ok
if 'preview' in descriptor:
- reference_npy = os.path.join(test_dir,
- 'reference_preview.npy')
- decoded_npy = os.path.join(work_dir, 'decoded_preview.npy')
+ reference_npy_fn = os.path.join(test_dir,
+ 'reference_preview.npy')
+ decoded_npy_fn = os.path.join(work_dir,
+ 'decoded_preview.npy')
- if not os.path.exists(decoded_npy):
+ if not os.path.exists(decoded_npy_fn):
ok = Failure(
'File not decoded: decoded_preview.npy')
- reference_npy = numpy.load(reference_npy)
- decoded_npy = numpy.load(decoded_npy)
- ok = ok & CompareNPY(reference_npy, reference_icc, decoded_npy,
- decoded_icc, 0,
- descriptor['preview']['rms_error'],
- descriptor['preview']['peak_error'])
+ reference_npy = numpy.load(reference_npy_fn)
+ decoded_npy = numpy.load(decoded_npy_fn)
+ preview_ok = CompareNPY(reference_npy, reference_icc,
+ decoded_npy, decoded_icc, 0,
+ descriptor['preview']['rms_error'],
+ descriptor['preview']['peak_error'])
+ if not preview_ok & args.update_on_failure:
+ os.unlink(reference_npy_fn)
+ shutil.copy2(decoded_npy_fn, reference_npy_fn)
+ preview_ok = True
+ ok = ok & preview_ok
return ok
@@ -228,6 +249,9 @@ def main():
required=True,
help=('path to the corpus directory or corpus descriptor'
' text file.'))
+ parser.add_argument(
+ '--update_on_failure', action='store_true',
+ help='If set, updates reference files on failing checks.')
args = parser.parse_args()
if not ConformanceTestRunner(args):
sys.exit(1)
diff --git a/tools/conformance/lcms2.py b/tools/conformance/lcms2.py
index f8313cd..09f6334 100644
--- a/tools/conformance/lcms2.py
+++ b/tools/conformance/lcms2.py
@@ -8,8 +8,12 @@ import ctypes
from numpy.ctypeslib import ndpointer
import numpy
import os
+import platform
-lcms2_lib_path = os.getenv("LCMS2_LIB_PATH", "liblcms2.so.2")
+IS_OSX = (platform.system() == "Darwin")
+
+default_libcms2_lib_path = ["liblcms2.so.2", "liblcms2.2.dylib"][IS_OSX]
+lcms2_lib_path = os.getenv("LCMS2_LIB_PATH", default_libcms2_lib_path)
lcms2_lib = ctypes.cdll.LoadLibrary(lcms2_lib_path)
native_open_profile = lcms2_lib.cmsOpenProfileFromMem
diff --git a/tools/decode_and_encode.cc b/tools/decode_and_encode.cc
index 59b1d6d..3c1d8e9 100644
--- a/tools/decode_and_encode.cc
+++ b/tools/decode_and_encode.cc
@@ -9,11 +9,12 @@
#include "lib/extras/codec.h"
#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/codec_in_out.h"
+#include "tools/file_io.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
namespace {
// Reads an input file (typically PNM) with color_space hint and writes to an
@@ -27,16 +28,26 @@ int Convert(int argc, char** argv) {
const std::string& desc = argv[2];
const std::string& pathname_out = argv[3];
- CodecInOut io;
- extras::ColorHints color_hints;
- ThreadPoolInternal pool(4);
+ std::vector<uint8_t> encoded_in;
+ if (!jpegxl::tools::ReadFile(pathname_in, &encoded_in)) {
+ fprintf(stderr, "Failed to read image from %s\n", pathname_in.c_str());
+ return 1;
+ }
+ jxl::CodecInOut io;
+ jxl::extras::ColorHints color_hints;
+ jpegxl::tools::ThreadPoolInternal pool(4);
color_hints.Add("color_space", desc);
- if (!SetFromFile(pathname_in, color_hints, &io, &pool)) {
- fprintf(stderr, "Failed to read %s\n", pathname_in.c_str());
+ if (!jxl::SetFromBytes(jxl::Bytes(encoded_in), color_hints, &io, &pool)) {
+ fprintf(stderr, "Failed to decode %s\n", pathname_in.c_str());
return 1;
}
- if (!EncodeToFile(io, pathname_out, &pool)) {
+ std::vector<uint8_t> encoded_out;
+ if (!jxl::Encode(io, pathname_out, &encoded_out, &pool)) {
+ fprintf(stderr, "Failed to encode %s\n", pathname_out.c_str());
+ return 1;
+ }
+ if (!jpegxl::tools::WriteFile(pathname_out, encoded_out)) {
fprintf(stderr, "Failed to write %s\n", pathname_out.c_str());
return 1;
}
@@ -45,6 +56,5 @@ int Convert(int argc, char** argv) {
}
} // namespace
-} // namespace jxl
-int main(int argc, char** argv) { return jxl::Convert(argc, argv); }
+int main(int argc, char** argv) { return Convert(argc, argv); }
diff --git a/tools/decode_basic_info_fuzzer.cc b/tools/decode_basic_info_fuzzer.cc
index 59f7089..8e97ff6 100644
--- a/tools/decode_basic_info_fuzzer.cc
+++ b/tools/decode_basic_info_fuzzer.cc
@@ -3,11 +3,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/decode.h>
#include <stdint.h>
-#include "jxl/decode.h"
-
-namespace jxl {
+namespace jpegxl {
+namespace tools {
int TestOneInput(const uint8_t* data, size_t size) {
JxlDecoderStatus status;
@@ -40,19 +40,19 @@ int TestOneInput(const uint8_t* data, size_t size) {
return 0;
}
- JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0};
- JxlDecoderGetColorAsEncodedProfile(
- dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, nullptr);
+ JxlDecoderGetColorAsEncodedProfile(dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ nullptr);
size_t dec_profile_size;
- JxlDecoderGetICCProfileSize(dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ JxlDecoderGetICCProfileSize(dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL,
&dec_profile_size);
JxlDecoderDestroy(dec);
return 0;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- return jxl::TestOneInput(data, size);
+ return jpegxl::tools::TestOneInput(data, size);
}
diff --git a/tools/djpegli.cc b/tools/djpegli.cc
new file mode 100644
index 0000000..bac55e1
--- /dev/null
+++ b/tools/djpegli.cc
@@ -0,0 +1,197 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/dec/jpegli.h"
+#include "lib/extras/enc/apng.h"
+#include "lib/extras/enc/encode.h"
+#include "lib/extras/time.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/speed_stats.h"
+
+namespace jpegxl {
+namespace tools {
+namespace {
+
+struct Args {
+ void AddCommandLineOptions(CommandLineParser* cmdline) {
+ std::string output_help("The output can be ");
+ if (jxl::extras::GetAPNGEncoder()) {
+ output_help.append("PNG, ");
+ }
+ output_help.append("PFM or PPM/PGM/PNM");
+ cmdline->AddPositionalOption("INPUT", /* required = */ true,
+ "The JPG input file.", &file_in);
+
+ cmdline->AddPositionalOption("OUTPUT", /* required = */ true, output_help,
+ &file_out);
+ cmdline->AddOptionFlag('\0', "disable_output",
+ "No output file will be written (for benchmarking)",
+ &disable_output, &SetBooleanTrue);
+
+ cmdline->AddOptionValue('\0', "bitdepth", "8|16",
+ "Sets the output bitdepth for integer based "
+ "formats, can be 8 (default) "
+ "or 16. Has no impact on PFM output.",
+ &bitdepth, &ParseUnsigned);
+
+ cmdline->AddOptionValue('\0', "num_reps", "N",
+ "Sets the number of times to decompress the image. "
+ "Used for benchmarking, the default is 1.",
+ &num_reps, &ParseUnsigned);
+
+ cmdline->AddOptionFlag('\0', "quiet", "Silence output (except for errors).",
+ &quiet, &SetBooleanTrue);
+ }
+
+ const char* file_in = nullptr;
+ const char* file_out = nullptr;
+ bool disable_output = false;
+ size_t bitdepth = 8;
+ size_t num_reps = 1;
+ bool quiet = false;
+};
+
+bool ValidateArgs(const Args& args) {
+ if (args.bitdepth != 8 && args.bitdepth != 16) {
+ fprintf(stderr, "Invalid --bitdepth argument\n");
+ return false;
+ }
+ return true;
+}
+
+void SetDecompressParams(const Args& args, const std::string& extension,
+ jxl::extras::JpegDecompressParams* params) {
+ if (extension == ".pfm") {
+ params->output_data_type = JXL_TYPE_FLOAT;
+ params->output_endianness = JXL_BIG_ENDIAN;
+ } else if (args.bitdepth == 16) {
+ params->output_data_type = JXL_TYPE_UINT16;
+ params->output_endianness = JXL_BIG_ENDIAN;
+ }
+ if (extension == ".pgm") {
+ params->force_grayscale = true;
+ } else if (extension == ".ppm") {
+ params->force_rgb = true;
+ }
+}
+
+int DJpegliMain(int argc, const char* argv[]) {
+ Args args;
+ CommandLineParser cmdline;
+ args.AddCommandLineOptions(&cmdline);
+
+ if (!cmdline.Parse(argc, const_cast<const char**>(argv))) {
+ // Parse already printed the actual error cause.
+ fprintf(stderr, "Use '%s -h' for more information.\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ if (cmdline.HelpFlagPassed() || !args.file_in) {
+ cmdline.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!args.file_out && !args.disable_output) {
+ fprintf(stderr,
+ "No output file specified and --disable_output flag not passed.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (args.disable_output && !args.quiet) {
+ fprintf(stderr,
+ "Decoding will be performed, but the result will be discarded.\n");
+ }
+
+ if (!ValidateArgs(args)) {
+ return EXIT_FAILURE;
+ }
+
+ std::vector<uint8_t> jpeg_bytes;
+ if (!ReadFile(args.file_in, &jpeg_bytes)) {
+ fprintf(stderr, "Failed to read input image %s\n", args.file_in);
+ return EXIT_FAILURE;
+ }
+
+ if (!args.quiet) {
+ fprintf(stderr, "Read %" PRIuS " compressed bytes.\n", jpeg_bytes.size());
+ }
+
+ std::string filename_out;
+ std::string extension;
+ if (args.file_out) {
+ filename_out = std::string(args.file_out);
+ size_t pos = filename_out.find_last_of('.');
+ if (pos >= filename_out.size()) {
+ fprintf(stderr, "Unrecognized output extension.\n");
+ return EXIT_FAILURE;
+ }
+ extension = filename_out.substr(pos);
+ }
+
+ jxl::extras::JpegDecompressParams dparams;
+ SetDecompressParams(args, extension, &dparams);
+
+ jxl::extras::PackedPixelFile ppf;
+ jpegxl::tools::SpeedStats stats;
+ for (size_t num_rep = 0; num_rep < args.num_reps; ++num_rep) {
+ const double t0 = jxl::Now();
+ if (!jxl::extras::DecodeJpeg(jpeg_bytes, dparams, nullptr, &ppf)) {
+ fprintf(stderr, "jpegli decoding failed\n");
+ return EXIT_FAILURE;
+ }
+ const double t1 = jxl::Now();
+ stats.NotifyElapsed(t1 - t0);
+ stats.SetImageSize(ppf.info.xsize, ppf.info.ysize);
+ }
+
+ if (!args.quiet) {
+ stats.Print(1);
+ }
+
+ if (args.disable_output) {
+ return EXIT_SUCCESS;
+ }
+
+ if (extension == ".pnm") {
+ extension = ppf.info.num_color_channels == 3 ? ".ppm" : ".pgm";
+ }
+
+ std::unique_ptr<jxl::extras::Encoder> encoder =
+ jxl::extras::Encoder::FromExtension(extension);
+ if (encoder == nullptr) {
+ fprintf(stderr, "Can't decode to the file extension '%s'\n",
+ extension.c_str());
+ return EXIT_FAILURE;
+ }
+ jxl::extras::EncodedImage encoded_image;
+ if (!encoder->Encode(ppf, &encoded_image) ||
+ encoded_image.bitstreams.empty()) {
+ fprintf(stderr, "Encode failed\n");
+ return EXIT_FAILURE;
+ }
+ if (!WriteFile(filename_out, encoded_image.bitstreams[0])) {
+ fprintf(stderr, "Failed to write output file %s\n", filename_out.c_str());
+ return EXIT_FAILURE;
+ }
+
+ return EXIT_SUCCESS;
+}
+
+} // namespace
+} // namespace tools
+} // namespace jpegxl
+
+int main(int argc, const char* argv[]) {
+ return jpegxl::tools::DJpegliMain(argc, argv);
+}
diff --git a/tools/djxl_fuzzer.cc b/tools/djxl_fuzzer.cc
index a03472a..4691eb4 100644
--- a/tools/djxl_fuzzer.cc
+++ b/tools/djxl_fuzzer.cc
@@ -3,24 +3,22 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/decode.h>
+#include <jxl/decode_cxx.h>
+#include <jxl/thread_parallel_runner.h>
+#include <jxl/thread_parallel_runner_cxx.h>
#include <limits.h>
#include <stdint.h>
-#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
+#include <hwy/targets.h>
#include <map>
#include <mutex>
#include <random>
#include <vector>
-#include "hwy/targets.h"
-#include "jxl/decode.h"
-#include "jxl/decode_cxx.h"
-#include "jxl/thread_parallel_runner.h"
-#include "jxl/thread_parallel_runner_cxx.h"
-
namespace {
// Externally visible value to ensure pixels are used in the fuzzer.
@@ -81,10 +79,10 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels,
auto dec = JxlDecoderMake(nullptr);
if (JXL_DEC_SUCCESS !=
JxlDecoderSubscribeEvents(
- dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_EXTENSIONS |
- JXL_DEC_COLOR_ENCODING | JXL_DEC_PREVIEW_IMAGE |
- JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE |
- JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_BOX)) {
+ dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
+ JXL_DEC_PREVIEW_IMAGE | JXL_DEC_FRAME |
+ JXL_DEC_FULL_IMAGE | JXL_DEC_JPEG_RECONSTRUCTION |
+ JXL_DEC_BOX)) {
return false;
}
if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(),
@@ -111,7 +109,6 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels,
}
bool seen_basic_info = false;
- bool seen_extensions = false;
bool seen_color_encoding = false;
bool seen_preview = false;
bool seen_need_image_out = false;
@@ -213,6 +210,7 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels,
return false;
}
} else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) {
+ if (want_preview) abort(); // expected preview before frame
if (spec.jpeg_to_pixels) abort();
if (!seen_jpeg_reconstruction) abort();
seen_jpeg_need_more_output = true;
@@ -274,12 +272,6 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels,
}
Consume(ec_name.cbegin(), ec_name.cend());
}
- } else if (status == JXL_DEC_EXTENSIONS) {
- if (!seen_basic_info) abort(); // expected basic info first
- if (seen_color_encoding) abort(); // should happen after this
- if (seen_extensions) abort(); // already seen extensions
- seen_extensions = true;
- // TODO(eustas): get extensions?
} else if (status == JXL_DEC_COLOR_ENCODING) {
if (!seen_basic_info) abort(); // expected basic info first
if (seen_color_encoding) abort(); // already seen color encoding
@@ -288,14 +280,13 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels,
// Get the ICC color profile of the pixel data
size_t icc_size;
if (JXL_DEC_SUCCESS !=
- JxlDecoderGetICCProfileSize(
- dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) {
+ JxlDecoderGetICCProfileSize(dec.get(), JXL_COLOR_PROFILE_TARGET_DATA,
+ &icc_size)) {
return false;
}
icc_profile->resize(icc_size);
if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile(
- dec.get(), &format,
- JXL_COLOR_PROFILE_TARGET_DATA,
+ dec.get(), JXL_COLOR_PROFILE_TARGET_DATA,
icc_profile->data(), icc_profile->size())) {
return false;
}
@@ -313,6 +304,7 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels,
}
}
} else if (status == JXL_DEC_PREVIEW_IMAGE) {
+ // TODO(eustas): test JXL_DEC_NEED_PREVIEW_OUT_BUFFER
if (seen_preview) abort();
if (!want_preview) abort();
if (!seen_color_encoding) abort();
@@ -404,7 +396,10 @@ bool DecodeJpegXl(const uint8_t* jxl, size_t size, size_t max_pixels,
}
}
} else if (status == JXL_DEC_JPEG_RECONSTRUCTION) {
- if (want_preview) abort(); // expected preview before frame
+ // Do not check preview precedence here, since this event only declares
+ // that JPEG is going to be decoded; though, when first byte of JPEG
+ // arrives (JXL_DEC_JPEG_NEED_MORE_OUTPUT) it is certain that preview
+ // should have been produced already.
if (seen_jpeg_reconstruction) abort();
seen_jpeg_reconstruction = true;
if (!spec.jpeg_to_pixels) {
diff --git a/tools/fuzzer_corpus.cc b/tools/djxl_fuzzer_corpus.cc
index 159256c..73c7eae 100644
--- a/tools/fuzzer_corpus.cc
+++ b/tools/djxl_fuzzer_corpus.cc
@@ -15,6 +15,8 @@
#include <unistd.h>
#endif
+#include <jxl/cms.h>
+
#include <algorithm>
#include <functional>
#include <iostream>
@@ -22,25 +24,21 @@
#include <random>
#include <vector>
-#if JPEGXL_ENABLE_JPEG
#include "lib/extras/codec.h"
-#endif
-#include "lib/jxl/aux_out.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/file_io.h"
#include "lib/jxl/base/override.h"
#include "lib/jxl/base/span.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/enc_ans.h"
#include "lib/jxl/enc_cache.h"
-#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_external_image.h"
-#include "lib/jxl/enc_file.h"
#include "lib/jxl/enc_params.h"
#include "lib/jxl/encode_internal.h"
#include "lib/jxl/jpeg/enc_jpeg_data.h"
#include "lib/jxl/modular/encoding/context_predict.h"
+#include "lib/jxl/test_utils.h" // TODO(eustas): cut this dependency
+#include "tools/file_io.h"
+#include "tools/thread_pool_internal.h"
namespace {
@@ -175,7 +173,6 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec,
}
io.metadata.m.SetAlphaBits(spec.alpha_bit_depth, spec.alpha_is_premultiplied);
io.metadata.m.orientation = spec.orientation;
- io.dec_pixels = spec.width * spec.height;
io.frames.clear();
io.frames.reserve(spec.num_frames);
@@ -214,46 +211,43 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec,
}
}
}
-
+ uint32_t num_channels = bytes_per_pixel / bytes_per_sample;
+ JxlDataType data_type =
+ bytes_per_sample == 1 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16;
+ JxlPixelFormat format = {num_channels, data_type, JXL_LITTLE_ENDIAN, 0};
const jxl::Span<const uint8_t> span(img_data.data(), img_data.size());
JXL_RETURN_IF_ERROR(ConvertFromExternal(
span, spec.width, spec.height, io.metadata.m.color_encoding,
- bytes_per_pixel / bytes_per_sample,
- /*alpha_is_premultiplied=*/spec.alpha_is_premultiplied,
- io.metadata.m.bit_depth.bits_per_sample, JXL_LITTLE_ENDIAN, nullptr,
- &ib, /*float_in=*/false, /*align=*/0));
+ io.metadata.m.bit_depth.bits_per_sample, format, nullptr, &ib));
io.frames.push_back(std::move(ib));
}
jxl::CompressParams params;
params.speed_tier = spec.params.speed_tier;
-#if JPEGXL_ENABLE_JPEG
if (spec.is_reconstructible_jpeg) {
// If this image is supposed to be a reconstructible JPEG, collect the JPEG
// metadata and encode it in the beginning of the compressed bytes.
std::vector<uint8_t> jpeg_bytes;
io.jpeg_quality = 70;
- JXL_RETURN_IF_ERROR(jxl::Encode(io, jxl::extras::Codec::kJPG,
- io.metadata.m.color_encoding,
- /*bits_per_sample=*/8, &jpeg_bytes,
- /*pool=*/nullptr));
+ JXL_QUIET_RETURN_IF_ERROR(jxl::Encode(io, jxl::extras::Codec::kJPG,
+ io.metadata.m.color_encoding,
+ /*bits_per_sample=*/8, &jpeg_bytes,
+ /*pool=*/nullptr));
JXL_RETURN_IF_ERROR(jxl::jpeg::DecodeImageJPG(
- jxl::Span<const uint8_t>(jpeg_bytes.data(), jpeg_bytes.size()), &io));
- jxl::PaddedBytes jpeg_data;
+ jxl::Bytes(jpeg_bytes.data(), jpeg_bytes.size()), &io));
+ std::vector<uint8_t> jpeg_data;
JXL_RETURN_IF_ERROR(
EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data, params));
std::vector<uint8_t> header;
- header.insert(header.end(), jxl::kContainerHeader,
- jxl::kContainerHeader + sizeof(jxl::kContainerHeader));
+ header.insert(header.end(), jxl::kContainerHeader.begin(),
+ jxl::kContainerHeader.end());
jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false,
&header);
- header.insert(header.end(), jpeg_data.data(),
- jpeg_data.data() + jpeg_data.size());
+ jxl::Bytes(jpeg_data).AppendTo(&header);
jxl::AppendBoxHeader(jxl::MakeBoxType("jxlc"), 0, true, &header);
compressed.append(header);
}
-#endif
params.modular_mode = spec.params.modular_mode;
params.color_transform = spec.params.color_transform;
@@ -263,13 +257,11 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec,
if (spec.params.preview) params.preview = jxl::Override::kOn;
if (spec.params.noise) params.noise = jxl::Override::kOn;
- jxl::AuxOut aux_out;
jxl::PassesEncoderState passes_encoder_state;
// EncodeFile replaces output; pass a temporary storage for it.
- jxl::PaddedBytes compressed_image;
- bool ok =
- jxl::EncodeFile(params, &io, &passes_encoder_state, &compressed_image,
- jxl::GetJxlCms(), &aux_out, nullptr);
+ std::vector<uint8_t> compressed_image;
+ bool ok = jxl::test::EncodeFile(params, &io, &passes_encoder_state,
+ &compressed_image);
if (!ok) return false;
compressed.append(compressed_image);
@@ -284,7 +276,7 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec,
}
}
- if (!jxl::WriteFile(compressed, output_fn)) return 1;
+ if (!jpegxl::tools::WriteFile(output_fn, compressed)) return 1;
if (!quiet) {
std::unique_lock<std::mutex> lock(stderr_mutex);
std::cerr << "Stored " << output_fn << " size: " << compressed.size()
@@ -331,7 +323,7 @@ int main(int argc, const char** argv) {
const char* dest_dir = nullptr;
bool regenerate = false;
bool quiet = false;
- int num_threads = std::thread::hardware_concurrency();
+ size_t num_threads = std::thread::hardware_concurrency();
for (int optind = 1; optind < argc;) {
if (!strcmp(argv[optind], "-r")) {
regenerate = true;
@@ -410,12 +402,8 @@ int main(int argc, const char** argv) {
for (uint32_t num_frames : {1, 3}) {
spec.num_frames = num_frames;
for (uint32_t preview : {0, 1}) {
-#if JPEGXL_ENABLE_JPEG
for (bool reconstructible_jpeg : {false, true}) {
spec.is_reconstructible_jpeg = reconstructible_jpeg;
-#else // JPEGXL_ENABLE_JPEG
- spec.is_reconstructible_jpeg = false;
-#endif // JPEGXL_ENABLE_JPEG
for (const auto& params : params_list) {
spec.params = params;
@@ -439,9 +427,7 @@ int main(int argc, const char** argv) {
specs.push_back(spec);
}
}
-#if JPEGXL_ENABLE_JPEG
}
-#endif // JPEGXL_ENABLE_JPEG
}
}
}
@@ -457,15 +443,14 @@ int main(int argc, const char** argv) {
specs.back().params.noise = true;
specs.back().override_decoder_spec = 0;
- jxl::ThreadPoolInternal pool{num_threads};
- if (!RunOnPool(
- &pool, 0, specs.size(), jxl::ThreadPool::NoInit,
- [&specs, dest_dir, regenerate, quiet](const uint32_t task,
- size_t /* thread */) {
- const ImageSpec& spec = specs[task];
- GenerateFile(dest_dir, spec, regenerate, quiet);
- },
- "FuzzerCorpus")) {
+ jpegxl::tools::ThreadPoolInternal pool{num_threads};
+ const auto generate = [&specs, dest_dir, regenerate, quiet](
+ const uint32_t task, size_t /* thread */) {
+ const ImageSpec& spec = specs[task];
+ GenerateFile(dest_dir, spec, regenerate, quiet);
+ };
+ if (!RunOnPool(&pool, 0, specs.size(), jxl::ThreadPool::NoInit, generate,
+ "FuzzerCorpus")) {
std::cerr << "Error generating fuzzer corpus" << std::endl;
return 1;
}
diff --git a/tools/djxl_fuzzer_test.cc b/tools/djxl_fuzzer_test.cc
index e5b35c9..1b16584 100644
--- a/tools/djxl_fuzzer_test.cc
+++ b/tools/djxl_fuzzer_test.cc
@@ -3,15 +3,16 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/thread_parallel_runner.h>
+#include <jxl/thread_parallel_runner_cxx.h>
+
+#include <cstdint>
#include <sstream>
#include <string>
#include <vector>
-#include "gtest/gtest.h"
-#include "jxl/thread_parallel_runner.h"
-#include "jxl/thread_parallel_runner_cxx.h"
#include "lib/jxl/test_utils.h"
-#include "lib/jxl/testdata.h"
+#include "lib/jxl/testing.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);
@@ -39,6 +40,6 @@ TEST_P(DjxlFuzzerTest, TestOne) {
std::ostringstream os;
os << "oss-fuzz/clusterfuzz-testcase-minimized-djxl_fuzzer-" << id;
printf("Testing %s\n", os.str().c_str());
- const jxl::PaddedBytes input = jxl::ReadTestData(os.str());
+ const std::vector<uint8_t> input = jxl::test::ReadTestData(os.str());
LLVMFuzzerTestOneInput(input.data(), input.size());
}
diff --git a/tools/djxl_main.cc b/tools/djxl_main.cc
index 44971c0..9abe0b6 100644
--- a/tools/djxl_main.cc
+++ b/tools/djxl_main.cc
@@ -3,7 +3,13 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/decode.h>
+#include <jxl/thread_parallel_runner.h>
+#include <jxl/thread_parallel_runner_cxx.h>
+#include <jxl/types.h>
+
#include <climits>
+#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
@@ -14,13 +20,14 @@
#include <string>
#include <vector>
-#include "jxl/decode.h"
-#include "jxl/thread_parallel_runner.h"
-#include "jxl/thread_parallel_runner_cxx.h"
-#include "jxl/types.h"
+#include "lib/extras/alpha_blend.h"
+#include "lib/extras/codec.h"
#include "lib/extras/dec/decode.h"
#include "lib/extras/dec/jxl.h"
+#include "lib/extras/enc/apng.h"
#include "lib/extras/enc/encode.h"
+#include "lib/extras/enc/exr.h"
+#include "lib/extras/enc/jpg.h"
#include "lib/extras/enc/pnm.h"
#include "lib/extras/packed_image.h"
#include "lib/extras/time.h"
@@ -37,115 +44,178 @@ struct DecompressArgs {
DecompressArgs() = default;
void AddCommandLineOptions(CommandLineParser* cmdline) {
- cmdline->AddPositionalOption("INPUT", /* required = */ true,
- "The compressed input file.", &file_in);
-
- cmdline->AddPositionalOption("OUTPUT", /* required = */ true,
- "The output can be (A)PNG with ICC, JPG, or "
- "PPM/PFM.",
+ std::string output_help("The output format can be ");
+ if (jxl::extras::GetAPNGEncoder()) {
+ output_help.append("PNG, APNG, ");
+ }
+ if (jxl::extras::GetJPEGEncoder()) {
+ output_help.append("JPEG, ");
+ } else {
+ output_help.append("JPEG (lossless reconstruction only), ");
+ }
+ if (jxl::extras::GetEXREncoder()) {
+ output_help.append("EXR, ");
+ }
+ output_help.append(
+ "PGM (for greyscale input), PPM (for color input), PNM, PFM, or PAM.\n"
+ " To extract metadata, use output format EXIF, XMP, or JUMBF.\n"
+ " The format is selected based on extension ('filename.png') or "
+ "prefix ('png:filename').\n"
+ " Use '-' for output to stdout (e.g. 'ppm:-')");
+ cmdline->AddPositionalOption(
+ "INPUT", /* required = */ true,
+ "The compressed input file (JXL). Use '-' for input from stdin.",
+ &file_in);
+
+ cmdline->AddPositionalOption("OUTPUT", /* required = */ true, output_help,
&file_out);
+ cmdline->AddHelpText("\nBasic options:", 0);
+
cmdline->AddOptionFlag('V', "version", "Print version number and exit.",
- &version, &SetBooleanTrue);
+ &version, &SetBooleanTrue, 0);
+ cmdline->AddOptionFlag('\0', "quiet", "Silence output (except for errors).",
+ &quiet, &SetBooleanTrue, 0);
+ cmdline->AddOptionFlag('v', "verbose",
+ "Verbose output; can be repeated and also applies "
+ "to help (!).",
+ &verbose, &SetBooleanTrue);
- cmdline->AddOptionValue('\0', "num_reps", "N",
- "Sets the number of times to decompress the image. "
- "Used for benchmarking, the default is 1.",
- &num_reps, &ParseUnsigned);
+ cmdline->AddHelpText("\nAdvanced options:", 1);
cmdline->AddOptionValue('\0', "num_threads", "N",
- "Sets the number of threads to use. The default 0 "
- "value means the machine default.",
- &num_threads, &ParseUnsigned);
-
- cmdline->AddOptionValue('\0', "bits_per_sample", "N",
- "Sets the output bit depth. The default 0 value "
- "means the original (input) bit depth.",
- &bits_per_sample, &ParseUnsigned);
+ "Number of worker threads (-1 == use machine "
+ "default, 0 == do not use multithreading).",
+ &num_threads, &ParseSigned, 1);
+
+ opt_bits_per_sample_id = cmdline->AddOptionValue(
+ '\0', "bits_per_sample", "N",
+ "Sets the output bit depth. The value 0 (default for PNM) "
+ "means the original (input) bit depth.\n"
+ " The value -1 (default for other codecs) means it depends on the "
+ "output format capabilities\n"
+ " and the input bit depth (e.g. decoding a 12-bit image to PNG will "
+ "produce a 16-bit PNG).",
+ &bits_per_sample, &ParseSigned, 1);
cmdline->AddOptionValue('\0', "display_nits", "N",
"If set to a non-zero value, tone maps the image "
"the given peak display luminance.",
- &display_nits, &ParseDouble);
-
- cmdline->AddOptionValue('\0', "color_space", "COLORSPACE_DESC",
- "Sets the output color space of the image. This "
- "flag has no effect if the image is not XYB "
- "encoded.",
- &color_space, &ParseString);
-
- cmdline->AddOptionValue('s', "downsampling", "N",
- "If set and the input JXL stream is progressive "
- "and contains hints for target downsampling "
- "ratios, the decoder will skip any progressive "
- "passes that are not needed to produce a partially "
- "decoded image intended for this downsampling "
- "ratio.",
- &downsampling, &ParseUint32);
+ &display_nits, &ParseDouble, 1);
+
+ cmdline->AddOptionValue(
+ '\0', "color_space", "COLORSPACE_DESC",
+ "Sets the desired output color space of the image. For example:\n"
+ " --color_space=RGB_D65_SRG_Per_SRG is sRGB with perceptual "
+ "rendering intent\n"
+ " --color_space=RGB_D65_202_Rel_PeQ is Rec.2100 PQ with relative "
+ "rendering intent",
+ &color_space, &ParseString, 1);
+
+ cmdline->AddOptionValue('s', "downsampling", "1|2|4|8",
+ "If the input JXL stream is contains hints for "
+ "target downsampling ratios,\n"
+ " only decode what is needed to produce an "
+ "image intended for this downsampling ratio.",
+ &downsampling, &ParseUint32, 1);
cmdline->AddOptionFlag('\0', "allow_partial_files",
"Allow decoding of truncated files.",
- &allow_partial_files, &SetBooleanTrue);
+ &allow_partial_files, &SetBooleanTrue, 1);
+
+ if (jxl::extras::GetJPEGEncoder()) {
+ cmdline->AddOptionFlag(
+ 'j', "pixels_to_jpeg",
+ "By default, if the input JXL is a recompressed JPEG file, "
+ "djxl reconstructs that JPEG file.\n"
+ " This flag causes the decoder to instead decode to pixels and "
+ "encode a new (lossy) JPEG.",
+ &pixels_to_jpeg, &SetBooleanTrue, 1);
+
+ opt_jpeg_quality_id = cmdline->AddOptionValue(
+ 'q', "jpeg_quality", "N",
+ "Sets the JPEG output quality, default is 95. "
+ "Setting this option implies --pixels_to_jpeg.",
+ &jpeg_quality, &ParseUnsigned, 1);
+ }
+
+ cmdline->AddHelpText("\nOptions for experimentation / benchmarking:", 2);
+
+ cmdline->AddOptionValue('\0', "num_reps", "N",
+ "Sets the number of times to decompress the image. "
+ "Useful for benchmarking. Default is 1.",
+ &num_reps, &ParseUnsigned, 2);
+
+ cmdline->AddOptionFlag('\0', "disable_output",
+ "No output file will be written (for benchmarking)",
+ &disable_output, &SetBooleanTrue, 2);
+
+ cmdline->AddOptionFlag('\0', "output_extra_channels",
+ "If set, all extra channels will be written either "
+ "as part of the main output file (e.g. alpha "
+ "channel in png) or as separate output files with "
+ "suffix -ecN in their names. If not set, the "
+ "(first) alpha channel will only be written when "
+ "the output format supports alpha channels and all "
+ "other extra channels won't be decoded. Files are "
+ "concatenated when outputting to stdout.",
+ &output_extra_channels, &SetBooleanTrue, 2);
-#if JPEGXL_ENABLE_JPEG
cmdline->AddOptionFlag(
- 'j', "pixels_to_jpeg",
- "By default, if the input JPEG XL contains a recompressed JPEG file, "
- "djxl reconstructs the exact original JPEG file. This flag causes the "
- "decoder to instead decode the image to pixels and encode a new "
- "(lossy) JPEG. The output file if provided must be a .jpg or .jpeg "
- "file.",
- &pixels_to_jpeg, &SetBooleanTrue);
-
- opt_jpeg_quality_id = cmdline->AddOptionValue(
- 'q', "jpeg_quality", "N",
- "Sets the JPEG output quality, default is 95. Setting an output "
- "quality implies --pixels_to_jpeg.",
- &jpeg_quality, &ParseUnsigned);
-#endif
-
-#if JPEGXL_ENABLE_SJPEG
+ '\0', "output_frames",
+ "If set, all frames will be written either as part of the main output "
+ "file if that supports animation, or as separate output files with "
+ "suffix -N in their names. Files are concatenated when outputting to "
+ "stdout.",
+ &output_frames, &SetBooleanTrue, 2);
+
cmdline->AddOptionFlag('\0', "use_sjpeg",
"Use sjpeg instead of libjpeg for JPEG output.",
- &use_sjpeg, &SetBooleanTrue);
-#endif
+ &use_sjpeg, &SetBooleanTrue, 2);
cmdline->AddOptionFlag('\0', "norender_spotcolors",
- "Disables rendering spot colors.",
- &render_spotcolors, &SetBooleanFalse);
+ "Disables rendering of spot colors.",
+ &render_spotcolors, &SetBooleanFalse, 2);
cmdline->AddOptionValue('\0', "preview_out", "FILENAME",
"If specified, writes the preview image to this "
"file.",
- &preview_out, &ParseString);
+ &preview_out, &ParseString, 2);
cmdline->AddOptionValue(
'\0', "icc_out", "FILENAME",
"If specified, writes the ICC profile of the decoded image to "
"this file.",
- &icc_out, &ParseString);
+ &icc_out, &ParseString, 2);
cmdline->AddOptionValue(
'\0', "orig_icc_out", "FILENAME",
"If specified, writes the ICC profile of the original image to "
- "this file. This can be different from the ICC profile of the "
- "decoded image if --color_space was specified, or if the image "
- "was XYB encoded and the color conversion to the original "
- "profile was not supported by the decoder.",
- &orig_icc_out, &ParseString);
-
- cmdline->AddOptionValue(
- '\0', "metadata_out", "FILENAME",
- "If specified, writes decoded metadata info to this file in "
- "JSON format. Used by the conformance test script",
- &metadata_out, &ParseString);
+ "this file\n"
+ " This can be different from the ICC profile of the "
+ "decoded image if --color_space was specified.",
+ &orig_icc_out, &ParseString, 2);
+
+ cmdline->AddOptionValue('\0', "metadata_out", "FILENAME",
+ "If specified, writes metadata info to a JSON "
+ "file. Used by the conformance test script",
+ &metadata_out, &ParseString, 2);
+
+ cmdline->AddOptionValue('\0', "background", "#NNNNNN",
+ "Specifies the background color for the "
+ "--alpha_blend option. Recognized values are "
+ "'black', 'white' (default), or '#NNNNNN'",
+ &background_spec, &ParseString, 2);
+
+ cmdline->AddOptionFlag('\0', "alpha_blend",
+ "Blends alpha channel with the color image using "
+ "background color specified by --background "
+ "(default is white).",
+ &alpha_blend, &SetBooleanTrue, 2);
cmdline->AddOptionFlag('\0', "print_read_bytes",
"Print total number of decoded bytes.",
- &print_read_bytes, &SetBooleanTrue);
-
- cmdline->AddOptionFlag('\0', "quiet", "Silence output (except for errors).",
- &quiet, &SetBooleanTrue);
+ &print_read_bytes, &SetBooleanTrue, 2);
}
// Validate the passed arguments, checking whether all passed options are
@@ -155,15 +225,23 @@ struct DecompressArgs {
fprintf(stderr, "Missing INPUT filename.\n");
return false;
}
+ if (num_threads < -1) {
+ fprintf(
+ stderr,
+ "Invalid flag value for --num_threads: must be -1, 0 or positive.\n");
+ return false;
+ }
return true;
}
const char* file_in = nullptr;
const char* file_out = nullptr;
bool version = false;
+ bool verbose = false;
size_t num_reps = 1;
- size_t num_threads = 0;
- size_t bits_per_sample = 0;
+ bool disable_output = false;
+ int32_t num_threads = -1;
+ int bits_per_sample = -1;
double display_nits = 0.0;
std::string color_space;
uint32_t downsampling = 0;
@@ -172,13 +250,18 @@ struct DecompressArgs {
size_t jpeg_quality = 95;
bool use_sjpeg = false;
bool render_spotcolors = true;
+ bool output_extra_channels = false;
+ bool output_frames = false;
std::string preview_out;
std::string icc_out;
std::string orig_icc_out;
std::string metadata_out;
+ std::string background_spec = "white";
+ bool alpha_blend = false;
bool print_read_bytes = false;
bool quiet = false;
// References (ids) of specific options to check if they were matched.
+ CommandLineParser::OptionId opt_bits_per_sample_id = -1;
CommandLineParser::OptionId opt_jpeg_quality_id = -1;
};
@@ -192,20 +275,21 @@ bool WriteOptionalOutput(const std::string& filename,
if (filename.empty() || bytes.empty()) {
return true;
}
- return jpegxl::tools::WriteFile(filename.data(), bytes);
+ return jpegxl::tools::WriteFile(filename, bytes);
}
-std::string Filename(const std::string& base, const std::string& extension,
+std::string Filename(const std::string& filename, const std::string& extension,
int layer_index, int frame_index, int num_layers,
int num_frames) {
+ if (filename == "-") return "-";
auto digits = [](int n) { return 1 + static_cast<int>(std::log10(n)); };
- std::string out = base;
+ std::string out = filename;
if (num_frames > 1) {
std::vector<char> buf(2 + digits(num_frames));
snprintf(buf.data(), buf.size(), "-%0*d", digits(num_frames), frame_index);
out.append(buf.data());
}
- if (num_layers > 1) {
+ if (num_layers > 1 && layer_index > 0) {
std::vector<char> buf(4 + digits(num_layers));
snprintf(buf.data(), buf.size(), "-ec%0*d", digits(num_layers),
layer_index);
@@ -213,12 +297,49 @@ std::string Filename(const std::string& base, const std::string& extension,
}
if (extension == ".ppm" && layer_index > 0) {
out.append(".pgm");
- } else {
+ } else if ((num_frames > 1) || (num_layers > 1 && layer_index > 0)) {
out.append(extension);
}
return out;
}
+void AddFormatsWithAlphaChannel(std::vector<JxlPixelFormat>* formats) {
+ auto add_format = [&](JxlPixelFormat format) {
+ for (auto f : *formats) {
+ if (memcmp(&f, &format, sizeof(format)) == 0) return;
+ }
+ formats->push_back(format);
+ };
+ size_t num_formats = formats->size();
+ for (size_t i = 0; i < num_formats; ++i) {
+ JxlPixelFormat format = (*formats)[i];
+ if (format.num_channels == 1 || format.num_channels == 3) {
+ ++format.num_channels;
+ add_format(format);
+ }
+ }
+}
+
+bool ParseBackgroundColor(const std::string& background_desc,
+ float background[3]) {
+ if (background_desc == "black") {
+ background[0] = background[1] = background[2] = 0.0f;
+ return true;
+ }
+ if (background_desc == "white") {
+ background[0] = background[1] = background[2] = 1.0f;
+ return true;
+ }
+ if (background_desc.size() != 7 || background_desc[0] != '#') {
+ return false;
+ }
+ uint32_t color = std::stoi(background_desc.substr(1), nullptr, 16);
+ background[0] = ((color >> 16) & 0xff) * (1.0f / 255);
+ background[1] = ((color >> 8) & 0xff) * (1.0f / 255);
+ background[2] = (color & 0xff) * (1.0f / 255);
+ return true;
+}
+
bool DecompressJxlReconstructJPEG(const jpegxl::tools::DecompressArgs& args,
const std::vector<uint8_t>& compressed,
void* runner,
@@ -227,6 +348,7 @@ bool DecompressJxlReconstructJPEG(const jpegxl::tools::DecompressArgs& args,
const double t0 = jxl::Now();
jxl::extras::PackedPixelFile ppf; // for JxlBasicInfo
jxl::extras::JXLDecompressParams dparams;
+ dparams.allow_partial_input = args.allow_partial_files;
dparams.runner = JxlThreadParallelRunner;
dparams.runner_opaque = runner;
if (!jxl::extras::DecodeImageJXL(compressed.data(), compressed.size(),
@@ -257,6 +379,13 @@ bool DecompressJxlToPackedPixelFile(
dparams.runner = JxlThreadParallelRunner;
dparams.runner_opaque = runner;
dparams.allow_partial_input = args.allow_partial_files;
+ dparams.need_icc = !args.icc_out.empty();
+ if (args.bits_per_sample == 0) {
+ dparams.output_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM;
+ } else if (args.bits_per_sample > 0) {
+ dparams.output_bitdepth.type = JXL_BIT_DEPTH_CUSTOM;
+ dparams.output_bitdepth.bits_per_sample = args.bits_per_sample;
+ }
const double t0 = jxl::Now();
if (!jxl::extras::DecodeImageJXL(compressed.data(), compressed.size(),
dparams, decoded_bytes, ppf)) {
@@ -293,7 +422,7 @@ int main(int argc, const char* argv[]) {
fprintf(stderr, "JPEG XL decoder %s\n", version.c_str());
}
- if (cmdline.HelpFlagPassed()) {
+ if (cmdline.HelpFlagPassed() || !args.file_in) {
cmdline.PrintHelp();
return EXIT_SUCCESS;
}
@@ -311,29 +440,31 @@ int main(int argc, const char* argv[]) {
return EXIT_FAILURE;
}
if (!args.quiet) {
- fprintf(stderr, "Read %" PRIuS " compressed bytes.\n", compressed.size());
+ cmdline.VerbosePrintf(1, "Read %" PRIuS " compressed bytes.\n",
+ compressed.size());
}
- if (!args.file_out && !args.quiet) {
+ if (!args.file_out && !args.disable_output) {
+ std::cerr
+ << "No output file specified and --disable_output flag not passed."
+ << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ if (args.file_out && args.disable_output && !args.quiet) {
fprintf(stderr,
- "No output file specified.\n"
"Decoding will be performed, but the result will be discarded.\n");
}
std::string filename_out;
- std::string base;
+ std::string filename;
std::string extension;
- if (args.file_out) {
+ jxl::extras::Codec codec = jxl::extras::Codec::kUnknown;
+ if (args.file_out && !args.disable_output) {
filename_out = std::string(args.file_out);
- size_t pos = filename_out.find_last_of('.');
- if (pos < filename_out.size()) {
- base = filename_out.substr(0, pos);
- extension = filename_out.substr(pos);
- } else {
- base = filename_out;
- }
+ codec = jxl::extras::CodecFromPath(
+ filename_out, /* bits_per_sample */ nullptr, &filename, &extension);
}
- const jxl::extras::Codec codec = jxl::extras::CodecFromExtension(extension);
if (codec == jxl::extras::Codec::kEXR) {
std::string force_colorspace = "RGB_D65_SRG_Rel_Lin";
if (!args.color_space.empty() && args.color_space != force_colorspace) {
@@ -341,12 +472,17 @@ int main(int argc, const char* argv[]) {
}
args.color_space = force_colorspace;
}
+ if (codec == jxl::extras::Codec::kPNM && extension != ".pfm" &&
+ (args.opt_jpeg_quality_id < 0 ||
+ !cmdline.GetOption(args.opt_jpeg_quality_id)->matched())) {
+ args.bits_per_sample = 0;
+ }
jpegxl::tools::SpeedStats stats;
size_t num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads();
{
int64_t flag_num_worker_threads = args.num_threads;
- if (flag_num_worker_threads != 0) {
+ if (flag_num_worker_threads > -1) {
num_worker_threads = flag_num_worker_threads;
}
}
@@ -354,12 +490,11 @@ int main(int argc, const char* argv[]) {
/*memory_manager=*/nullptr, num_worker_threads);
bool decode_to_pixels = (codec != jxl::extras::Codec::kJPG);
-#if JPEGXL_ENABLE_JPEG
- if (args.pixels_to_jpeg ||
- cmdline.GetOption(args.opt_jpeg_quality_id)->matched()) {
+ if (args.opt_jpeg_quality_id >= 0 &&
+ (args.pixels_to_jpeg ||
+ cmdline.GetOption(args.opt_jpeg_quality_id)->matched())) {
decode_to_pixels = true;
}
-#endif
size_t num_reps = args.num_reps;
if (!decode_to_pixels) {
@@ -380,9 +515,8 @@ int main(int argc, const char* argv[]) {
}
}
if (!bytes.empty()) {
- if (!args.quiet) fprintf(stderr, "Reconstructed to JPEG.\n");
- if (!filename_out.empty() &&
- !jpegxl::tools::WriteFile(filename_out.c_str(), bytes)) {
+ if (!args.quiet) cmdline.VerbosePrintf(0, "Reconstructed to JPEG.\n");
+ if (!filename_out.empty() && !jpegxl::tools::WriteFile(filename, bytes)) {
return EXIT_FAILURE;
}
}
@@ -398,6 +532,9 @@ int main(int argc, const char* argv[]) {
return EXIT_FAILURE;
}
accepted_formats = encoder->AcceptedFormats();
+ if (args.alpha_blend) {
+ AddFormatsWithAlphaChannel(&accepted_formats);
+ }
}
jxl::extras::PackedPixelFile ppf;
size_t decoded_bytes = 0;
@@ -409,57 +546,63 @@ int main(int argc, const char* argv[]) {
return EXIT_FAILURE;
}
}
- if (!args.quiet) fprintf(stderr, "Decoded to pixels.\n");
+ if (!args.quiet) cmdline.VerbosePrintf(0, "Decoded to pixels.\n");
if (args.print_read_bytes) {
fprintf(stderr, "Decoded bytes: %" PRIuS "\n", decoded_bytes);
}
- if (extension == ".pfm") {
- ppf.info.bits_per_sample = 32;
- } else if (args.bits_per_sample > 0) {
- ppf.info.bits_per_sample = args.bits_per_sample;
- }
-#if JPEGXL_ENABLE_JPEG
+ // When --disable_output was parsed, `filename_out` is empty and we don't
+ // need to write files.
if (encoder) {
+ if (args.alpha_blend) {
+ float background[3];
+ if (!ParseBackgroundColor(args.background_spec, background)) {
+ fprintf(stderr, "Invalid background color %s\n",
+ args.background_spec.c_str());
+ }
+ AlphaBlend(&ppf, background);
+ }
std::ostringstream os;
os << args.jpeg_quality;
encoder->SetOption("q", os.str());
- }
-#endif
-#if JPEGXL_ENABLE_SJPEG
- if (encoder && args.use_sjpeg) {
- encoder->SetOption("jpeg_encoder", "sjpeg");
- }
-#endif
- jxl::extras::EncodedImage encoded_image;
- if (encoder) {
+ if (args.use_sjpeg) {
+ encoder->SetOption("jpeg_encoder", "sjpeg");
+ }
+ jxl::extras::EncodedImage encoded_image;
+ if (!args.quiet) cmdline.VerbosePrintf(2, "Encoding decoded image\n");
if (!encoder->Encode(ppf, &encoded_image)) {
fprintf(stderr, "Encode failed\n");
return EXIT_FAILURE;
}
- }
- size_t nlayers = 1 + encoded_image.extra_channel_bitstreams.size();
- size_t nframes = encoded_image.bitstreams.size();
- for (size_t i = 0; i < nlayers; ++i) {
- for (size_t j = 0; j < nframes; ++j) {
- const std::vector<uint8_t>& bitstream =
- (i == 0 ? encoded_image.bitstreams[j]
- : encoded_image.extra_channel_bitstreams[i - 1][j]);
- std::string fn = Filename(base, extension, i, j, nlayers, nframes);
- if (!jpegxl::tools::WriteFile(fn.c_str(), bitstream)) {
- return EXIT_FAILURE;
+ size_t nlayers = args.output_extra_channels
+ ? 1 + encoded_image.extra_channel_bitstreams.size()
+ : 1;
+ size_t nframes = args.output_frames ? encoded_image.bitstreams.size() : 1;
+ for (size_t i = 0; i < nlayers; ++i) {
+ for (size_t j = 0; j < nframes; ++j) {
+ const std::vector<uint8_t>& bitstream =
+ (i == 0 ? encoded_image.bitstreams[j]
+ : encoded_image.extra_channel_bitstreams[i - 1][j]);
+ std::string fn =
+ Filename(filename, extension, i, j, nlayers, nframes);
+ if (!jpegxl::tools::WriteFile(fn.c_str(), bitstream)) {
+ return EXIT_FAILURE;
+ }
+ if (!args.quiet)
+ cmdline.VerbosePrintf(1, "Wrote output to %s\n", fn.c_str());
}
}
- }
- if (!WriteOptionalOutput(args.preview_out,
- encoded_image.preview_bitstream) ||
- !WriteOptionalOutput(args.icc_out, ppf.icc) ||
- !WriteOptionalOutput(args.orig_icc_out, ppf.orig_icc) ||
- !WriteOptionalOutput(args.metadata_out, encoded_image.metadata)) {
- return EXIT_FAILURE;
+ if (!WriteOptionalOutput(args.preview_out,
+ encoded_image.preview_bitstream) ||
+ !WriteOptionalOutput(args.icc_out, ppf.icc) ||
+ !WriteOptionalOutput(args.orig_icc_out, ppf.orig_icc) ||
+ !WriteOptionalOutput(args.metadata_out, encoded_image.metadata)) {
+ return EXIT_FAILURE;
+ }
}
}
if (!args.quiet) {
stats.Print(num_worker_threads);
}
+
return EXIT_SUCCESS;
}
diff --git a/tools/fast_lossless/.gitignore b/tools/fast_lossless/.gitignore
new file mode 100644
index 0000000..567609b
--- /dev/null
+++ b/tools/fast_lossless/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/tools/fast_lossless/README.md b/tools/fast_lossless/README.md
new file mode 100644
index 0000000..5f99c13
--- /dev/null
+++ b/tools/fast_lossless/README.md
@@ -0,0 +1,10 @@
+# Fast-lossless
+This is a script to compile a standalone version of a JXL encoder that supports
+lossless compression, up to 16 bits, of 1- to 4-channel images and animations; it is
+very fast and compression is slightly worse than PNG for 8-bit nonphoto content
+and better or much better than PNG for all other situations.
+
+The main encoder is made out of two files, `lib/jxl/enc_fast_lossless.{cc,h}`;
+it automatically selects and runs a SIMD implementation supported by your CPU.
+
+This folder contains an example build script and `main` file.
diff --git a/tools/fast_lossless/build-android.sh b/tools/fast_lossless/build-android.sh
new file mode 100755
index 0000000..c155b21
--- /dev/null
+++ b/tools/fast_lossless/build-android.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+set -e
+
+DIR=$(realpath "$(dirname "$0")")
+
+mkdir -p /tmp/build-android
+cd /tmp/build-android
+
+CXX="$ANDROID_NDK"/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang++
+if ! command -v "$CXX" >/dev/null ; then
+ printf >&2 '%s: Android C++ compiler not found, is ANDROID_NDK set properly?\n' "${0##*/}"
+ exit 1
+fi
+
+[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp'
+[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h'
+[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c
+
+"$CXX" -O3 \
+ -I. lodepng.o \
+ -I"${DIR}"/../../ \
+ "${DIR}"/../../lib/jxl/enc_fast_lossless.cc "${DIR}"/fast_lossless_main.cc \
+ -o fast_lossless
diff --git a/tools/fast_lossless/build.sh b/tools/fast_lossless/build.sh
new file mode 100755
index 0000000..e2c0aa3
--- /dev/null
+++ b/tools/fast_lossless/build.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+set -e
+
+DIR=$(realpath "$(dirname "$0")")
+mkdir -p "$DIR"/build
+cd "$DIR"/build
+
+# set CXX to clang++ if not set in the environment
+CXX="${CXX-clang++}"
+if ! command -v "$CXX" >/dev/null ; then
+ printf >&2 '%s: C++ compiler not found\n' "${0##*/}"
+ exit 1
+fi
+
+[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp'
+[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h'
+[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c
+
+"$CXX" -O3 \
+ -I. -g lodepng.o \
+ -I"$DIR"/../../ \
+ "$DIR"/../../lib/jxl/enc_fast_lossless.cc "$DIR"/fast_lossless_main.cc \
+ -o fast_lossless
diff --git a/tools/fast_lossless/cross_compile_aarch64.sh b/tools/fast_lossless/cross_compile_aarch64.sh
new file mode 100755
index 0000000..a5e6aa2
--- /dev/null
+++ b/tools/fast_lossless/cross_compile_aarch64.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+set -e
+
+DIR=$(realpath "$(dirname "$0")")
+mkdir -p "$DIR"/build-aarch64
+cd "$DIR"/build-aarch64
+
+CXX="${CXX-aarch64-linux-gnu-c++}"
+if ! command -v "$CXX" >/dev/null ; then
+ printf >&2 '%s: C++ compiler not found\n' "${0##*/}"
+ exit 1
+fi
+
+[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp'
+[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h'
+[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c
+
+"$CXX" -O3 -static \
+ -I. lodepng.o \
+ -I"$DIR"/../../ \
+ "$DIR"/../../lib/jxl/enc_fast_lossless.cc "$DIR"/fast_lossless_main.cc \
+ -o fast_lossless
diff --git a/tools/fast_lossless/fast_lossless_main.cc b/tools/fast_lossless/fast_lossless_main.cc
new file mode 100644
index 0000000..b59051d
--- /dev/null
+++ b/tools/fast_lossless/fast_lossless_main.cc
@@ -0,0 +1,113 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <atomic>
+#include <chrono>
+#include <thread>
+#include <vector>
+
+#include "lib/jxl/enc_fast_lossless.h"
+#include "lodepng.h"
+#include "pam-input.h"
+
+int main(int argc, char** argv) {
+ if (argc < 3) {
+ fprintf(stderr,
+ "Usage: %s in.png out.jxl [effort] [num_reps] [num_threads]\n",
+ argv[0]);
+ return 1;
+ }
+
+ const char* in = argv[1];
+ const char* out = argv[2];
+ int effort = argc >= 4 ? atoi(argv[3]) : 2;
+ size_t num_reps = argc >= 5 ? atoi(argv[4]) : 1;
+ size_t num_threads = argc >= 6 ? atoi(argv[5]) : 0;
+
+ if (effort < 0 || effort > 127) {
+ fprintf(
+ stderr,
+ "Effort should be between 0 and 127 (default is 2, more is slower)\n");
+ return 1;
+ }
+
+ unsigned char* png;
+ unsigned w, h;
+ size_t nb_chans = 4, bitdepth = 8;
+
+ unsigned error = lodepng_decode32_file(&png, &w, &h, in);
+
+ size_t width = w, height = h;
+ if (error && !DecodePAM(in, &png, &width, &height, &nb_chans, &bitdepth)) {
+ fprintf(stderr, "lodepng error %u: %s\n", error, lodepng_error_text(error));
+ return 1;
+ }
+
+ auto parallel_runner = [](void* num_threads_ptr, void* opaque,
+ void fun(void*, size_t), size_t count) {
+ size_t num_threads = *(size_t*)num_threads_ptr;
+ if (num_threads == 0) {
+ num_threads = std::thread::hardware_concurrency();
+ }
+ if (num_threads > count) {
+ num_threads = count;
+ }
+ if (num_threads == 1) {
+ for (size_t i = 0; i < count; i++) {
+ fun(opaque, i);
+ }
+ } else {
+ std::atomic<int> task{0};
+ std::vector<std::thread> threads;
+ for (size_t i = 0; i < num_threads; i++) {
+ threads.push_back(std::thread([count, opaque, fun, &task]() {
+ while (true) {
+ int t = task++;
+ if (t >= count) break;
+ fun(opaque, t);
+ }
+ }));
+ }
+ for (auto& t : threads) t.join();
+ }
+ };
+
+ size_t encoded_size = 0;
+ unsigned char* encoded = nullptr;
+ size_t stride = width * nb_chans * (bitdepth > 8 ? 2 : 1);
+
+ auto start = std::chrono::high_resolution_clock::now();
+ for (size_t _ = 0; _ < num_reps; _++) {
+ free(encoded);
+ encoded_size = JxlFastLosslessEncode(
+ png, width, stride, height, nb_chans, bitdepth,
+ /*big_endian=*/true, effort, &encoded, &num_threads, +parallel_runner);
+ }
+ auto stop = std::chrono::high_resolution_clock::now();
+ if (num_reps > 1) {
+ float us =
+ std::chrono::duration_cast<std::chrono::microseconds>(stop - start)
+ .count();
+ size_t pixels = size_t{width} * size_t{height} * num_reps;
+ float mps = pixels / us;
+ fprintf(stderr, "%10.3f MP/s\n", mps);
+ fprintf(stderr, "%10.3f bits/pixel\n",
+ encoded_size * 8.0 / float(width) / float(height));
+ }
+
+ FILE* o = fopen(out, "wb");
+ if (!o) {
+ fprintf(stderr, "error opening %s: %s\n", out, strerror(errno));
+ return 1;
+ }
+ if (fwrite(encoded, 1, encoded_size, o) != encoded_size) {
+ fprintf(stderr, "error writing to %s: %s\n", out, strerror(errno));
+ }
+ fclose(o);
+}
diff --git a/tools/fast_lossless/pam-input.h b/tools/fast_lossless/pam-input.h
new file mode 100644
index 0000000..b5a0233
--- /dev/null
+++ b/tools/fast_lossless/pam-input.h
@@ -0,0 +1,292 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+bool error_msg(const char* message) {
+ fprintf(stderr, "%s\n", message);
+ return false;
+}
+#define return_on_error(X) \
+ if (!X) return false;
+
+size_t Log2(uint32_t value) { return 31 - __builtin_clz(value); }
+
+struct HeaderPNM {
+ size_t xsize;
+ size_t ysize;
+ bool is_gray; // PGM
+ bool has_alpha; // PAM
+ size_t bits_per_sample;
+};
+
+class Parser {
+ public:
+ explicit Parser(uint8_t* data, size_t length)
+ : pos_(data), end_(data + length) {}
+
+ // Sets "pos" to the first non-header byte/pixel on success.
+ bool ParseHeader(HeaderPNM* header, const uint8_t** pos) {
+ // codec.cc ensures we have at least two bytes => no range check here.
+ if (pos_[0] != 'P') return false;
+ const uint8_t type = pos_[1];
+ pos_ += 2;
+
+ switch (type) {
+ case '5':
+ header->is_gray = true;
+ return ParseHeaderPNM(header, pos);
+
+ case '6':
+ header->is_gray = false;
+ return ParseHeaderPNM(header, pos);
+
+ case '7':
+ return ParseHeaderPAM(header, pos);
+ }
+ return false;
+ }
+
+ // Exposed for testing
+ bool ParseUnsigned(size_t* number) {
+ if (pos_ == end_) return error_msg("PNM: reached end before number");
+ if (!IsDigit(*pos_)) return error_msg("PNM: expected unsigned number");
+
+ *number = 0;
+ while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
+ *number *= 10;
+ *number += *pos_ - '0';
+ ++pos_;
+ }
+
+ return true;
+ }
+
+ bool ParseSigned(double* number) {
+ if (pos_ == end_) return error_msg("PNM: reached end before signed");
+
+ if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) {
+ return error_msg("PNM: expected signed number");
+ }
+
+ // Skip sign
+ const bool is_neg = *pos_ == '-';
+ if (is_neg || *pos_ == '+') {
+ ++pos_;
+ if (pos_ == end_) return error_msg("PNM: reached end before digits");
+ }
+
+ // Leading digits
+ *number = 0.0;
+ while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
+ *number *= 10;
+ *number += *pos_ - '0';
+ ++pos_;
+ }
+
+ // Decimal places?
+ if (pos_ < end_ && *pos_ == '.') {
+ ++pos_;
+ double place = 0.1;
+ while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
+ *number += (*pos_ - '0') * place;
+ place *= 0.1;
+ ++pos_;
+ }
+ }
+
+ if (is_neg) *number = -*number;
+ return true;
+ }
+
+ private:
+ static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; }
+ static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; }
+ static bool IsWhitespace(const uint8_t c) {
+ return IsLineBreak(c) || c == '\t' || c == ' ';
+ }
+
+ bool SkipBlank() {
+ if (pos_ == end_) return error_msg("PNM: reached end before blank");
+ const uint8_t c = *pos_;
+ if (c != ' ' && c != '\n') return error_msg("PNM: expected blank");
+ ++pos_;
+ return true;
+ }
+
+ bool SkipSingleWhitespace() {
+ if (pos_ == end_) return error_msg("PNM: reached end before whitespace");
+ if (!IsWhitespace(*pos_)) return error_msg("PNM: expected whitespace");
+ ++pos_;
+ return true;
+ }
+
+ bool SkipWhitespace() {
+ if (pos_ == end_) return error_msg("PNM: reached end before whitespace");
+ if (!IsWhitespace(*pos_) && *pos_ != '#') {
+ return error_msg("PNM: expected whitespace/comment");
+ }
+
+ while (pos_ < end_ && IsWhitespace(*pos_)) {
+ ++pos_;
+ }
+
+ // Comment(s)
+ while (pos_ != end_ && *pos_ == '#') {
+ while (pos_ != end_ && !IsLineBreak(*pos_)) {
+ ++pos_;
+ }
+ // Newline(s)
+ while (pos_ != end_ && IsLineBreak(*pos_)) pos_++;
+ }
+
+ while (pos_ < end_ && IsWhitespace(*pos_)) {
+ ++pos_;
+ }
+ return true;
+ }
+
+ bool MatchString(const char* keyword) {
+ const uint8_t* ppos = pos_;
+ while (*keyword) {
+ if (ppos >= end_) return error_msg("PAM: unexpected end of input");
+ if (*keyword != *ppos) return false;
+ ppos++;
+ keyword++;
+ }
+ pos_ = ppos;
+ return_on_error(SkipWhitespace());
+ return true;
+ }
+
+ bool ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) {
+ size_t num_channels = 3;
+ size_t max_val = 255;
+ while (!MatchString("ENDHDR")) {
+ return_on_error(SkipWhitespace());
+ if (MatchString("WIDTH")) {
+ return_on_error(ParseUnsigned(&header->xsize));
+ } else if (MatchString("HEIGHT")) {
+ return_on_error(ParseUnsigned(&header->ysize));
+ } else if (MatchString("DEPTH")) {
+ return_on_error(ParseUnsigned(&num_channels));
+ } else if (MatchString("MAXVAL")) {
+ return_on_error(ParseUnsigned(&max_val));
+ } else if (MatchString("TUPLTYPE")) {
+ if (MatchString("RGB_ALPHA")) {
+ header->has_alpha = true;
+ } else if (MatchString("RGB")) {
+ } else if (MatchString("GRAYSCALE_ALPHA")) {
+ header->has_alpha = true;
+ header->is_gray = true;
+ } else if (MatchString("GRAYSCALE")) {
+ header->is_gray = true;
+ } else if (MatchString("BLACKANDWHITE_ALPHA")) {
+ header->has_alpha = true;
+ header->is_gray = true;
+ max_val = 1;
+ } else if (MatchString("BLACKANDWHITE")) {
+ header->is_gray = true;
+ max_val = 1;
+ } else {
+ return error_msg("PAM: unknown TUPLTYPE");
+ }
+ } else {
+ return error_msg("PAM: unknown header keyword");
+ }
+ }
+ if (num_channels !=
+ (header->has_alpha ? 1 : 0) + (header->is_gray ? 1 : 3)) {
+ return error_msg("PAM: bad DEPTH");
+ }
+ if (max_val == 0 || max_val >= 65536) {
+ return error_msg("PAM: bad MAXVAL");
+ }
+ header->bits_per_sample = Log2(max_val + 1);
+
+ *pos = pos_;
+ return true;
+ }
+
+ bool ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) {
+ return_on_error(SkipWhitespace());
+ return_on_error(ParseUnsigned(&header->xsize));
+
+ return_on_error(SkipWhitespace());
+ return_on_error(ParseUnsigned(&header->ysize));
+
+ return_on_error(SkipWhitespace());
+ size_t max_val;
+ return_on_error(ParseUnsigned(&max_val));
+ if (max_val == 0 || max_val >= 65536) {
+ return error_msg("PNM: bad MaxVal");
+ }
+ header->bits_per_sample = Log2(max_val + 1);
+
+ return_on_error(SkipSingleWhitespace());
+
+ *pos = pos_;
+ return true;
+ }
+
+ const uint8_t* pos_;
+ const uint8_t* const end_;
+};
+
+bool load_file(unsigned char** out, size_t* outsize, const char* filename) {
+ FILE* file;
+ file = fopen(filename, "rb");
+ if (!file) return false;
+ if (fseek(file, 0, SEEK_END) != 0) {
+ fclose(file);
+ return false;
+ }
+ *outsize = ftell(file);
+ if (*outsize == LONG_MAX || *outsize < 9 || fseek(file, 0, SEEK_SET)) {
+ fclose(file);
+ return false;
+ }
+ *out = (unsigned char*)malloc(*outsize);
+ if (!(*out)) {
+ fclose(file);
+ return false;
+ }
+ size_t readsize;
+ readsize = fread(*out, 1, *outsize, file);
+ fclose(file);
+ if (readsize != *outsize) return false;
+ return true;
+}
+
+bool DecodePAM(const char* filename, uint8_t** buffer, size_t* w, size_t* h,
+ size_t* nb_chans, size_t* bitdepth) {
+ unsigned char* in_file;
+ size_t in_size;
+ if (!load_file(&in_file, &in_size, filename))
+ return error_msg("Could not read input file");
+ Parser parser(in_file, in_size);
+ HeaderPNM header = {};
+ const uint8_t* pos = nullptr;
+ if (!parser.ParseHeader(&header, &pos)) return false;
+
+ if (header.bits_per_sample == 0 || header.bits_per_sample > 16) {
+ return error_msg("PNM: bits_per_sample invalid (can do at most 16-bit)");
+ }
+ *w = header.xsize;
+ *h = header.ysize;
+ *bitdepth = header.bits_per_sample;
+ *nb_chans = (header.is_gray ? 1 : 3) + (header.has_alpha ? 1 : 0);
+
+ size_t pnm_remaining_size = in_file + in_size - pos;
+ size_t buffer_size = *w * *h * *nb_chans * (*bitdepth > 8 ? 2 : 1);
+ if (pnm_remaining_size < buffer_size) {
+ return error_msg("PNM file too small");
+ }
+ *buffer = (uint8_t*)malloc(buffer_size);
+ memcpy(*buffer, pos, buffer_size);
+ return true;
+}
diff --git a/tools/fields_fuzzer.cc b/tools/fields_fuzzer.cc
index 87e1439..09ea89c 100644
--- a/tools/fields_fuzzer.cc
+++ b/tools/fields_fuzzer.cc
@@ -5,6 +5,7 @@
#include <stdint.h>
+#include "lib/jxl/dec_ans.h"
#include "lib/jxl/dec_bit_reader.h"
#include "lib/jxl/frame_header.h"
#include "lib/jxl/headers.h"
@@ -15,7 +16,15 @@
#include "lib/jxl/modular/encoding/encoding.h"
#include "lib/jxl/modular/transform/transform.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::BitReader;
+using ::jxl::Bytes;
+using ::jxl::CodecMetadata;
+using ::jxl::CustomTransformData;
+using ::jxl::ImageMetadata;
+using ::jxl::SizeHeader;
int TestOneInput(const uint8_t* data, size_t size) {
// Global parameters used by some headers.
@@ -23,23 +32,23 @@ int TestOneInput(const uint8_t* data, size_t size) {
// First byte controls which header to parse.
if (size == 0) return 0;
- BitReader reader(Span<const uint8_t>(data + 1, size - 1));
+ BitReader reader(Bytes(data + 1, size - 1));
#define FUZZER_CASE_HEADER(number, classname, ...) \
case number: { \
- classname header{__VA_ARGS__}; \
- (void)Bundle::Read(&reader, &header); \
+ ::jxl::classname header{__VA_ARGS__}; \
+ (void)jxl::Bundle::Read(&reader, &header); \
break; \
}
switch (data[0]) {
case 0: {
SizeHeader size_header;
- (void)ReadSizeHeader(&reader, &size_header);
+ (void)jxl::ReadSizeHeader(&reader, &size_header);
break;
}
case 1: {
ImageMetadata metadata;
- (void)ReadImageMetadata(&reader, &metadata);
+ (void)jxl::ReadImageMetadata(&reader, &metadata);
break;
}
@@ -69,7 +78,7 @@ int TestOneInput(const uint8_t* data, size_t size) {
default: {
CustomTransformData transform_data;
transform_data.nonserialized_xyb_encoded = true;
- (void)Bundle::Read(&reader, &transform_data);
+ (void)jxl::Bundle::Read(&reader, &transform_data);
break;
}
}
@@ -78,8 +87,9 @@ int TestOneInput(const uint8_t* data, size_t size) {
return 0;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- return jxl::TestOneInput(data, size);
+ return jpegxl::tools::TestOneInput(data, size);
}
diff --git a/tools/file_io.cc b/tools/file_io.cc
deleted file mode 100644
index bc7f3b1..0000000
--- a/tools/file_io.cc
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (c) the JPEG XL Project Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-#include "tools/file_io.h"
-
-#include <errno.h>
-#include <limits.h>
-#include <stdio.h>
-#include <string.h>
-
-namespace jpegxl {
-namespace tools {
-
-bool ReadFile(const char* filename, std::vector<uint8_t>* out) {
- FILE* file = fopen(filename, "rb");
- if (!file) {
- return false;
- }
-
- if (fseek(file, 0, SEEK_END) != 0) {
- fclose(file);
- return false;
- }
-
- long size = ftell(file);
- // Avoid invalid file or directory.
- if (size >= LONG_MAX || size < 0) {
- fclose(file);
- return false;
- }
-
- if (fseek(file, 0, SEEK_SET) != 0) {
- fclose(file);
- return false;
- }
-
- out->resize(size);
- size_t readsize = fread(out->data(), 1, size, file);
- if (fclose(file) != 0) {
- return false;
- }
-
- return readsize == static_cast<size_t>(size);
-}
-
-bool WriteFile(const char* filename, const std::vector<uint8_t>& bytes) {
- FILE* file = fopen(filename, "wb");
- if (!file) {
- fprintf(stderr,
- "Could not open %s for writing\n"
- "Error: %s",
- filename, strerror(errno));
- return false;
- }
- if (fwrite(bytes.data(), 1, bytes.size(), file) != bytes.size()) {
- fprintf(stderr,
- "Could not write to file\n"
- "Error: %s",
- strerror(errno));
- return false;
- }
- if (fclose(file) != 0) {
- fprintf(stderr,
- "Could not close file\n"
- "Error: %s",
- strerror(errno));
- return false;
- }
- return true;
-}
-
-} // namespace tools
-} // namespace jpegxl
diff --git a/tools/file_io.h b/tools/file_io.h
index 959b79d..7d9f15d 100644
--- a/tools/file_io.h
+++ b/tools/file_io.h
@@ -6,16 +6,146 @@
#ifndef TOOLS_FILE_IO_H_
#define TOOLS_FILE_IO_H_
+#include <errno.h>
+#include <limits.h>
#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <list>
+#include <string>
#include <vector>
+#include "lib/jxl/base/compiler_specific.h"
+
namespace jpegxl {
namespace tools {
-bool ReadFile(const char* filename, std::vector<uint8_t>* out);
+namespace {
+
+// RAII, ensures files are closed even when returning early.
+class FileWrapper {
+ public:
+ FileWrapper(const FileWrapper& other) = delete;
+ FileWrapper& operator=(const FileWrapper& other) = delete;
+
+ explicit FileWrapper(const std::string& pathname, const char* mode)
+ : file_(pathname == "-" ? (mode[0] == 'r' ? stdin : stdout)
+ : fopen(pathname.c_str(), mode)),
+ close_on_delete_(pathname != "-") {
+#ifdef _WIN32
+ struct __stat64 s = {};
+ const int err = _stat64(pathname.c_str(), &s);
+ const bool is_file = (s.st_mode & S_IFREG) != 0;
+#else
+ struct stat s = {};
+ const int err = stat(pathname.c_str(), &s);
+ const bool is_file = S_ISREG(s.st_mode);
+#endif
+ if (err == 0 && is_file) {
+ size_ = s.st_size;
+ }
+ }
+
+ ~FileWrapper() {
+ if (file_ != nullptr && close_on_delete_) {
+ const int err = fclose(file_);
+ if (err) {
+ fprintf(stderr,
+ "Could not close file\n"
+ "Error: %s",
+ strerror(errno));
+ }
+ }
+ }
+
+ // We intend to use FileWrapper as a replacement of FILE.
+ // NOLINTNEXTLINE(google-explicit-constructor)
+ operator FILE*() const { return file_; }
+
+ int64_t size() { return size_; }
+
+ private:
+ FILE* const file_;
+ bool close_on_delete_ = true;
+ int64_t size_ = -1;
+};
+
+} // namespace
+
+template <typename ContainerType>
+static inline bool ReadFile(FileWrapper& f, ContainerType* JXL_RESTRICT bytes) {
+ if (!f) return false;
+
+ // Get size of file in bytes
+ const int64_t size = f.size();
+ if (size < 0) {
+ // Size is unknown, loop reading chunks until EOF.
+ bytes->clear();
+ std::list<std::vector<uint8_t>> chunks;
+
+ size_t total_size = 0;
+ while (true) {
+ std::vector<uint8_t> chunk(16 * 1024);
+ const size_t bytes_read = fread(chunk.data(), 1, chunk.size(), f);
+ if (ferror(f) || bytes_read > chunk.size()) {
+ return false;
+ }
+
+ chunk.resize(bytes_read);
+ total_size += bytes_read;
+ if (bytes_read != 0) {
+ chunks.emplace_back(std::move(chunk));
+ }
+ if (feof(f)) {
+ break;
+ }
+ }
+ bytes->resize(total_size);
+ size_t pos = 0;
+ for (const auto& chunk : chunks) {
+ memcpy(bytes->data() + pos, chunk.data(), chunk.size());
+ pos += chunk.size();
+ }
+ } else {
+ // Size is known, read the file directly.
+ bytes->resize(static_cast<size_t>(size));
+
+ const size_t bytes_read = fread(bytes->data(), 1, bytes->size(), f);
+ if (bytes_read != static_cast<size_t>(size)) return false;
+ }
+
+ return true;
+}
+
+template <typename ContainerType>
+static inline bool ReadFile(const std::string& filename,
+ ContainerType* JXL_RESTRICT bytes) {
+ FileWrapper f(filename, "rb");
+ return ReadFile(f, bytes);
+}
-bool WriteFile(const char* filename, const std::vector<uint8_t>& bytes);
+template <typename ContainerType>
+static inline bool WriteFile(const std::string& filename,
+ const ContainerType& bytes) {
+ FileWrapper file(filename, "wb");
+ if (!file) {
+ fprintf(stderr,
+ "Could not open %s for writing\n"
+ "Error: %s",
+ filename.c_str(), strerror(errno));
+ return false;
+ }
+ if (fwrite(bytes.data(), 1, bytes.size(), file) != bytes.size()) {
+ fprintf(stderr,
+ "Could not write to file\n"
+ "Error: %s",
+ strerror(errno));
+ return false;
+ }
+ return true;
+}
} // namespace tools
} // namespace jpegxl
diff --git a/tools/flicker_test/CMakeLists.txt b/tools/flicker_test/CMakeLists.txt
index efa4716..427a34f 100644
--- a/tools/flicker_test/CMakeLists.txt
+++ b/tools/flicker_test/CMakeLists.txt
@@ -3,9 +3,9 @@
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
-find_package(Qt5 QUIET COMPONENTS Widgets)
-if (NOT Qt5_FOUND)
- message(WARNING "Qt5 was not found. The flicker test tool will not be built.")
+find_package(Qt6 QUIET COMPONENTS Widgets)
+if (NOT Qt6_FOUND)
+ message(WARNING "Qt6 was not found. The flicker test tool will not be built.")
return()
endif ()
@@ -32,7 +32,7 @@ add_executable(flicker_test WIN32
test_window.ui)
target_link_libraries(flicker_test PUBLIC
- Qt5::Widgets
+ Qt6::Widgets
image_loading
icc_detect
)
diff --git a/tools/flicker_test/main.cc b/tools/flicker_test/main.cc
index 67985a9..9617765 100644
--- a/tools/flicker_test/main.cc
+++ b/tools/flicker_test/main.cc
@@ -11,12 +11,13 @@
int main(int argc, char** argv) {
QApplication application(argc, argv);
- jxl::FlickerTestWizard wizard;
+ jpegxl::tools::FlickerTestWizard wizard;
if (wizard.exec()) {
- jxl::FlickerTestWindow test_window(wizard.parameters());
+ jpegxl::tools::FlickerTestWindow test_window(wizard.parameters());
if (test_window.proceedWithTest()) {
test_window.showMaximized();
return application.exec();
}
}
+ return 0;
}
diff --git a/tools/flicker_test/parameters.cc b/tools/flicker_test/parameters.cc
index 575edb0..460867b 100644
--- a/tools/flicker_test/parameters.cc
+++ b/tools/flicker_test/parameters.cc
@@ -5,7 +5,8 @@
#include "tools/flicker_test/parameters.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
namespace {
@@ -84,4 +85,5 @@ void FlickerTestParameters::saveTo(QSettings* const settings) const {
settings->endGroup();
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/flicker_test/parameters.h b/tools/flicker_test/parameters.h
index a063995..777d479 100644
--- a/tools/flicker_test/parameters.h
+++ b/tools/flicker_test/parameters.h
@@ -8,7 +8,8 @@
#include <QSettings>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
struct FlickerTestParameters {
QString originalFolder;
@@ -27,6 +28,7 @@ struct FlickerTestParameters {
void saveTo(QSettings* settings) const;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_FLICKER_TEST_PARAMETERS_H_
diff --git a/tools/flicker_test/setup.cc b/tools/flicker_test/setup.cc
index bfcddd5..ff17286 100644
--- a/tools/flicker_test/setup.cc
+++ b/tools/flicker_test/setup.cc
@@ -11,7 +11,8 @@
#include <QMessageBox>
#include <QPushButton>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
FlickerTestWizard::FlickerTestWizard(QWidget* const parent)
: QWizard(parent), settings_("JPEG XL project", "Flickering test") {
@@ -148,4 +149,5 @@ bool FlickerTestWizard::validateCurrentPage() {
return QWizard::validateCurrentPage();
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/flicker_test/setup.h b/tools/flicker_test/setup.h
index 0da78d6..e034e28 100644
--- a/tools/flicker_test/setup.h
+++ b/tools/flicker_test/setup.h
@@ -11,7 +11,8 @@
#include "tools/flicker_test/parameters.h"
#include "tools/flicker_test/ui_setup.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
class FlickerTestWizard : public QWizard {
Q_OBJECT
@@ -39,6 +40,7 @@ class FlickerTestWizard : public QWizard {
QSettings settings_;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_FLICKER_TEST_SETUP_H_
diff --git a/tools/flicker_test/setup.ui b/tools/flicker_test/setup.ui
index 055c7f7..44b850c 100644
--- a/tools/flicker_test/setup.ui
+++ b/tools/flicker_test/setup.ui
@@ -11,6 +11,9 @@
<property name="windowTitle">
<string>New flicker test</string>
</property>
+ <property name="wizardStyle">
+ <enum>QWizard::ClassicStyle</enum>
+ </property>
<property name="options">
<set>QWizard::NoBackButtonOnStartPage</set>
</property>
@@ -328,7 +331,7 @@
<widget class="QWizardPage" name="spacingPage">
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0,0">
<item>
- <widget class="jxl::SplitView" name="spacingDemo" native="true"/>
+ <widget class="jpegxl::tools::SplitView" name="spacingDemo" native="true"/>
</item>
<item>
<spacer name="verticalSpacer_2">
@@ -389,7 +392,7 @@
</widget>
<customwidgets>
<customwidget>
- <class>jxl::SplitView</class>
+ <class>jpegxl::tools::SplitView</class>
<extends>QWidget</extends>
<header>tools/flicker_test/split_view.h</header>
<container>1</container>
diff --git a/tools/flicker_test/split_view.cc b/tools/flicker_test/split_view.cc
index 3455d70..87df95e 100644
--- a/tools/flicker_test/split_view.cc
+++ b/tools/flicker_test/split_view.cc
@@ -8,7 +8,8 @@
#include <QMouseEvent>
#include <QPainter>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
SplitView::SplitView(QWidget* const parent)
: QWidget(parent), g_(std::random_device()()) {
@@ -37,12 +38,14 @@ SplitView::SplitView(QWidget* const parent)
void SplitView::setOriginalImage(QImage image) {
original_ = QPixmap::fromImage(std::move(image));
+ original_.setDevicePixelRatio(devicePixelRatio());
updateMinimumSize();
update();
}
void SplitView::setAlteredImage(QImage image) {
altered_ = QPixmap::fromImage(std::move(image));
+ altered_.setDevicePixelRatio(devicePixelRatio());
updateMinimumSize();
update();
}
@@ -139,15 +142,17 @@ void SplitView::paintEvent(QPaintEvent* const event) {
QPixmap* const leftImage = imageForSide(Side::kLeft);
QPixmap* const rightImage = imageForSide(Side::kRight);
- leftRect_ = leftImage->rect();
+ leftRect_ = QRectF(QPoint(), leftImage->deviceIndependentSize());
leftRect_.moveCenter(rect().center());
- leftRect_.moveRight(rect().center().x() - spacing_ / 2 - spacing_ % 2);
- painter.drawPixmap(leftRect_, *leftImage);
+ leftRect_.moveRight(rect().center().x() -
+ (spacing_ / 2 + spacing_ % 2) / devicePixelRatio());
+ painter.drawPixmap(leftRect_.topLeft(), *leftImage);
- rightRect_ = rightImage->rect();
+ rightRect_ = QRectF(QPoint(), rightImage->deviceIndependentSize());
rightRect_.moveCenter(rect().center());
- rightRect_.moveLeft(rect().center().x() + 1 + spacing_ / 2);
- painter.drawPixmap(rightRect_, *rightImage);
+ rightRect_.moveLeft(rect().center().x() +
+ (spacing_ / 2) / devicePixelRatio());
+ painter.drawPixmap(rightRect_.topLeft(), *rightImage);
}
void SplitView::startDisplaying() {
@@ -160,8 +165,12 @@ void SplitView::startDisplaying() {
}
void SplitView::updateMinimumSize() {
- setMinimumWidth(2 * std::max(original_.width(), altered_.width()) + spacing_);
- setMinimumHeight(std::max(original_.height(), altered_.height()));
+ setMinimumWidth(2 * std::max(original_.deviceIndependentSize().width(),
+ altered_.deviceIndependentSize().width()) +
+ spacing_ / devicePixelRatio());
+ setMinimumHeight(std::max(original_.deviceIndependentSize().height(),
+ altered_.deviceIndependentSize().height()));
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/flicker_test/split_view.h b/tools/flicker_test/split_view.h
index b4c7a1d..37c5f7e 100644
--- a/tools/flicker_test/split_view.h
+++ b/tools/flicker_test/split_view.h
@@ -14,7 +14,8 @@
#include <QWidget>
#include <random>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
class SplitView : public QWidget {
Q_OBJECT
@@ -67,7 +68,7 @@ class SplitView : public QWidget {
Side originalSide_;
bool clicking_ = false;
Side clickedSide_;
- QRect leftRect_, rightRect_;
+ QRectF leftRect_, rightRect_;
State state_ = State::kDisplaying;
bool gray_ = false;
QTimer blankingTimer_;
@@ -79,6 +80,7 @@ class SplitView : public QWidget {
QElapsedTimer viewingStartTime_;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_FLICKER_TEST_SPLIT_VIEW_H_
diff --git a/tools/flicker_test/test_window.cc b/tools/flicker_test/test_window.cc
index f3827c5..c21ca6f 100644
--- a/tools/flicker_test/test_window.cc
+++ b/tools/flicker_test/test_window.cc
@@ -13,7 +13,8 @@
#include "tools/icc_detect/icc_detect.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
FlickerTestWindow::FlickerTestWindow(FlickerTestParameters parameters,
QWidget* const parent)
@@ -181,4 +182,5 @@ retry:
parameters_.grayFadingTimeMSecs, parameters_.grayTimeMSecs);
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/flicker_test/test_window.h b/tools/flicker_test/test_window.h
index 1dfe5fc..ad712af 100644
--- a/tools/flicker_test/test_window.h
+++ b/tools/flicker_test/test_window.h
@@ -16,7 +16,8 @@
#include "tools/flicker_test/parameters.h"
#include "tools/flicker_test/ui_test_window.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
class FlickerTestWindow : public QMainWindow {
Q_OBJECT
@@ -45,6 +46,7 @@ class FlickerTestWindow : public QMainWindow {
QStringList remainingImages_;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_FLICKER_TEST_TEST_WINDOW_H_
diff --git a/tools/flicker_test/test_window.ui b/tools/flicker_test/test_window.ui
index 7eb2619..bd42873 100644
--- a/tools/flicker_test/test_window.ui
+++ b/tools/flicker_test/test_window.ui
@@ -64,7 +64,7 @@
</item>
</layout>
</widget>
- <widget class="jxl::SplitView" name="splitView"/>
+ <widget class="jpegxl::tools::SplitView" name="splitView"/>
<widget class="QWidget" name="finalPage">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
@@ -104,7 +104,7 @@
</widget>
<customwidgets>
<customwidget>
- <class>jxl::SplitView</class>
+ <class>jpegxl::tools::SplitView</class>
<extends>QWidget</extends>
<header>tools/flicker_test/split_view.h</header>
<container>1</container>
diff --git a/tools/fuzzer_stub.cc b/tools/fuzzer_stub.cc
index f984c00..2f30e9e 100644
--- a/tools/fuzzer_stub.cc
+++ b/tools/fuzzer_stub.cc
@@ -3,14 +3,14 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/thread_parallel_runner.h>
+#include <jxl/thread_parallel_runner_cxx.h>
+
#include <fstream>
#include <iostream>
#include <iterator>
#include <vector>
-#include "jxl/thread_parallel_runner.h"
-#include "jxl/thread_parallel_runner_cxx.h"
-
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);
void ProcessInput(const char* filename) {
diff --git a/tools/hdr/README.md b/tools/hdr/README.md
index 227b22b..85eb1bd 100644
--- a/tools/hdr/README.md
+++ b/tools/hdr/README.md
@@ -99,6 +99,22 @@ This is the mathematical inverse of `tools/render_hlg`. Furthermore,
`tools/pq_to_hlg` is equivalent to `tools/tone_map -t 1000` followed by
`tools/display_to_hlg -m 1000`.
+## OpenEXR to PQ
+
+`tools/exr_to_pq` converts an OpenEXR image into a Rec. 2020 + PQ image, which
+can be saved as a PNG or PPM file. Luminance information is taken from the
+`whiteLuminance` tag if the input has it, and otherwise defaults to treating
+(1, 1, 1) as 100 cd/m². It is also possible to override this using the
+`--luminance` (`-l`) flag, in two different ways:
+
+```shell
+# Specifies that the brightest pixel in the image happens to be 1500 cd/m².
+$ tools/exr_to_pq --luminance='max=1500' input.exr output.png
+
+# Specifies that (1, 1, 1) in the input file is 203 cd/m².
+$ tools/exr_to_pq --luminance='white=203' input.exr output.png
+```
+
# LUT generation
There are additionally two tools that can be used to generate look-up tables
diff --git a/tools/hdr/display_to_hlg.cc b/tools/hdr/display_to_hlg.cc
index a2caef2..8fa8fde 100644
--- a/tools/hdr/display_to_hlg.cc
+++ b/tools/hdr/display_to_hlg.cc
@@ -9,13 +9,15 @@
#include "lib/extras/codec.h"
#include "lib/extras/hlg.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/base/span.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
float max_nits = 0;
@@ -64,9 +66,11 @@ int main(int argc, const char** argv) {
return EXIT_FAILURE;
}
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
jxl::CodecInOut image;
- JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image,
- &pool));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(),
+ &image, &pool));
image.metadata.m.SetIntensityTarget(max_nits);
JXL_CHECK(jxl::HlgInverseOOTF(
&image.Main(), jxl::GetHlgGamma(max_nits, surround_nits), &pool));
@@ -75,11 +79,12 @@ int main(int argc, const char** argv) {
jxl::ColorEncoding hlg;
hlg.SetColorSpace(jxl::ColorSpace::kRGB);
- hlg.primaries = jxl::Primaries::k2100;
- hlg.white_point = jxl::WhitePoint::kD65;
- hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG);
+ JXL_CHECK(hlg.SetPrimariesType(jxl::Primaries::k2100));
+ JXL_CHECK(hlg.SetWhitePointType(jxl::WhitePoint::kD65));
+ hlg.Tf().SetTransferFunction(jxl::TransferFunction::kHLG);
JXL_CHECK(hlg.CreateICC());
- JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool));
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, hlg, &pool));
image.metadata.m.color_encoding = hlg;
- JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/hdr/exr_to_pq.cc b/tools/hdr/exr_to_pq.cc
new file mode 100644
index 0000000..c7ce1b7
--- /dev/null
+++ b/tools/hdr/exr_to_pq.cc
@@ -0,0 +1,158 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/decode.h"
+#include "lib/extras/packed_image_convert.h"
+#include "lib/jxl/cms/jxl_cms_internal.h"
+#include "lib/jxl/image_bundle.h"
+#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
+
+namespace {
+
+struct LuminanceInfo {
+ enum class Kind { kWhite, kMaximum };
+ Kind kind = Kind::kWhite;
+ float luminance = 100.f;
+};
+
+bool ParseLuminanceInfo(const char* argument, LuminanceInfo* luminance_info) {
+ if (strncmp(argument, "white=", 6) == 0) {
+ luminance_info->kind = LuminanceInfo::Kind::kWhite;
+ argument += 6;
+ } else if (strncmp(argument, "max=", 4) == 0) {
+ luminance_info->kind = LuminanceInfo::Kind::kMaximum;
+ argument += 4;
+ } else {
+ fprintf(stderr,
+ "Invalid prefix for luminance info, expected white= or max=\n");
+ return false;
+ }
+ return jpegxl::tools::ParseFloat(argument, &luminance_info->luminance);
+}
+
+} // namespace
+
+int main(int argc, const char** argv) {
+ jpegxl::tools::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ LuminanceInfo luminance_info;
+ auto luminance_option =
+ parser.AddOptionValue('l', "luminance", "<max|white=N>",
+ "luminance information (defaults to whiteLuminance "
+ "header if present, otherwise to white=100)",
+ &luminance_info, &ParseLuminanceInfo, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::extras::PackedPixelFile ppf;
+ std::vector<uint8_t> input_bytes;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &input_bytes));
+ JXL_CHECK(jxl::extras::DecodeBytes(jxl::Bytes(input_bytes),
+ jxl::extras::ColorHints(), &ppf));
+
+ jxl::CodecInOut image;
+ JXL_CHECK(
+ jxl::extras::ConvertPackedPixelFileToCodecInOut(ppf, &pool, &image));
+ image.metadata.m.bit_depth.exponent_bits_per_sample = 0;
+ jxl::ColorEncoding linear_rec_2020 = image.Main().c_current();
+ JXL_CHECK(linear_rec_2020.SetPrimariesType(jxl::Primaries::k2100));
+ linear_rec_2020.Tf().SetTransferFunction(jxl::TransferFunction::kLinear);
+ JXL_CHECK(linear_rec_2020.CreateICC());
+ JXL_CHECK(
+ jpegxl::tools::TransformCodecInOutTo(image, linear_rec_2020, &pool));
+
+ float primaries_xyz[9];
+ const jxl::PrimariesCIExy p = image.Main().c_current().GetPrimaries();
+ const jxl::CIExy wp = image.Main().c_current().GetWhitePoint();
+ JXL_CHECK(jxl::PrimariesToXYZ(p.r.x, p.r.y, p.g.x, p.g.y, p.b.x, p.b.y, wp.x,
+ wp.y, primaries_xyz));
+
+ float max_value = 0.f;
+ float max_relative_luminance = 0.f;
+ float white_luminance = ppf.info.intensity_target != 0 &&
+ !parser.GetOption(luminance_option)->matched()
+ ? ppf.info.intensity_target
+ : luminance_info.kind == LuminanceInfo::Kind::kWhite
+ ? luminance_info.luminance
+ : 0.f;
+ bool out_of_gamut = false;
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const rows[3] = {image.Main().color()->ConstPlaneRow(0, y),
+ image.Main().color()->ConstPlaneRow(1, y),
+ image.Main().color()->ConstPlaneRow(2, y)};
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ if (!out_of_gamut &&
+ (rows[0][x] < 0 || rows[1][x] < 0 || rows[2][x] < 0)) {
+ out_of_gamut = true;
+ fprintf(stderr,
+ "WARNING: found colors outside of the Rec. 2020 gamut.\n");
+ }
+ max_value = std::max(
+ max_value, std::max(rows[0][x], std::max(rows[1][x], rows[2][x])));
+ const float luminance = primaries_xyz[1] * rows[0][x] +
+ primaries_xyz[4] * rows[1][x] +
+ primaries_xyz[7] * rows[2][x];
+ if (luminance_info.kind == LuminanceInfo::Kind::kMaximum &&
+ luminance > max_relative_luminance) {
+ max_relative_luminance = luminance;
+ white_luminance = luminance_info.luminance / luminance;
+ }
+ }
+ }
+ jxl::ScaleImage(1.f / max_value, image.Main().color());
+ white_luminance *= max_value;
+ image.metadata.m.SetIntensityTarget(white_luminance);
+ if (white_luminance > 10000) {
+ fprintf(stderr,
+ "WARNING: the image is too bright for PQ (would need (1, 1, 1) to "
+ "be %g cd/m^2).\n",
+ white_luminance);
+ } else {
+ fprintf(stderr,
+ "The resulting image should be compressed with "
+ "--intensity_target=%g.\n",
+ white_luminance);
+ }
+
+ jxl::ColorEncoding pq = image.Main().c_current();
+ pq.Tf().SetTransferFunction(jxl::TransferFunction::kPQ);
+ JXL_CHECK(pq.CreateICC());
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, pq, &pool));
+ image.metadata.m.color_encoding = pq;
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
+}
diff --git a/tools/hdr/generate_lut_template.cc b/tools/hdr/generate_lut_template.cc
index 626d54f..da8ecee 100644
--- a/tools/hdr/generate_lut_template.cc
+++ b/tools/hdr/generate_lut_template.cc
@@ -7,12 +7,13 @@
#include <stdlib.h>
#include "lib/extras/codec.h"
-#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/image_metadata.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
size_t N = 64;
@@ -55,6 +56,8 @@ int main(int argc, const char** argv) {
jxl::CodecInOut output;
output.metadata.m.bit_depth.bits_per_sample = 16;
output.SetFromImage(std::move(image), jxl::ColorEncoding::SRGB());
- JXL_CHECK(jxl::EncodeToFile(output, jxl::ColorEncoding::SRGB(), 16,
- output_filename, &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jxl::Encode(output, jxl::ColorEncoding::SRGB(), 16, output_filename,
+ &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/hdr/image_utils.h b/tools/hdr/image_utils.h
new file mode 100644
index 0000000..901c2b6
--- /dev/null
+++ b/tools/hdr/image_utils.h
@@ -0,0 +1,35 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_HDR_IMAGE_UTILS_H_
+#define TOOLS_HDR_IMAGE_UTILS_H_
+
+#include <jxl/cms.h>
+#include <jxl/cms_interface.h>
+
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/image_bundle.h"
+
+namespace jpegxl {
+namespace tools {
+
+static inline jxl::Status TransformCodecInOutTo(
+ jxl::CodecInOut& io, const jxl::ColorEncoding& c_desired,
+ jxl::ThreadPool* pool) {
+ const JxlCmsInterface& cms = *JxlGetDefaultCms();
+ if (io.metadata.m.have_preview) {
+ JXL_RETURN_IF_ERROR(io.preview_frame.TransformTo(c_desired, cms, pool));
+ }
+ for (jxl::ImageBundle& ib : io.frames) {
+ JXL_RETURN_IF_ERROR(ib.TransformTo(c_desired, cms, pool));
+ }
+ return true;
+}
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_HDR_IMAGE_UTILS_H_
diff --git a/tools/hdr/local_tone_map.cc b/tools/hdr/local_tone_map.cc
new file mode 100644
index 0000000..b6582a6
--- /dev/null
+++ b/tools/hdr/local_tone_map.cc
@@ -0,0 +1,541 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <jxl/cms.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/tone_mapping.h"
+#include "lib/jxl/convolve.h"
+#include "lib/jxl/enc_gamma_correct.h"
+#include "lib/jxl/image_bundle.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+#include "tools/thread_pool_internal.h"
+
+namespace jxl {
+namespace {
+
+constexpr WeightsSeparable5 kPyramidFilter = {
+ {HWY_REP4(.375f), HWY_REP4(.25f), HWY_REP4(.0625f)},
+ {HWY_REP4(.375f), HWY_REP4(.25f), HWY_REP4(.0625f)}};
+
+template <typename Tin, typename Tout>
+void Subtract(const Image3<Tin>& image1, const Image3<Tin>& image2,
+ Image3<Tout>* out) {
+ const size_t xsize = image1.xsize();
+ const size_t ysize = image1.ysize();
+ JXL_CHECK(xsize == image2.xsize());
+ JXL_CHECK(ysize == image2.ysize());
+
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < ysize; ++y) {
+ const Tin* const JXL_RESTRICT row1 = image1.ConstPlaneRow(c, y);
+ const Tin* const JXL_RESTRICT row2 = image2.ConstPlaneRow(c, y);
+ Tout* const JXL_RESTRICT row_out = out->PlaneRow(c, y);
+ for (size_t x = 0; x < xsize; ++x) {
+ row_out[x] = row1[x] - row2[x];
+ }
+ }
+ }
+}
+
+// Adds `what` of the size of `rect` to `to` in the position of `rect`.
+template <typename Tin, typename Tout>
+void AddTo(const Rect& rect, const Image3<Tin>& what, Image3<Tout>* to) {
+ const size_t xsize = what.xsize();
+ const size_t ysize = what.ysize();
+ JXL_ASSERT(xsize == rect.xsize());
+ JXL_ASSERT(ysize == rect.ysize());
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < ysize; ++y) {
+ const Tin* JXL_RESTRICT row_what = what.ConstPlaneRow(c, y);
+ Tout* JXL_RESTRICT row_to = rect.PlaneRow(to, c, y);
+ for (size_t x = 0; x < xsize; ++x) {
+ row_to[x] += row_what[x];
+ }
+ }
+ }
+}
+
+template <typename T>
+Plane<T> Product(const Plane<T>& a, const Plane<T>& b) {
+ Plane<T> c(a.xsize(), a.ysize());
+ for (size_t y = 0; y < a.ysize(); ++y) {
+ const T* const JXL_RESTRICT row_a = a.Row(y);
+ const T* const JXL_RESTRICT row_b = b.Row(y);
+ T* const JXL_RESTRICT row_c = c.Row(y);
+ for (size_t x = 0; x < a.xsize(); ++x) {
+ row_c[x] = row_a[x] * row_b[x];
+ }
+ }
+ return c;
+}
+
+// Expects sRGB input.
+// Will call consumer(x, y, contrast) for each pixel.
+template <typename Consumer>
+void Contrast(const jxl::Image3F& image, const Consumer& consumer,
+ ThreadPool* const pool) {
+ static constexpr WeightsSymmetric3 kLaplacianWeights = {
+ {HWY_REP4(-4)}, {HWY_REP4(1)}, {HWY_REP4(0)}};
+ ImageF grayscale(image.xsize(), image.ysize());
+ static constexpr float kLuminances[3] = {0.2126, 0.7152, 0.0722};
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const JXL_RESTRICT input_rows[3] = {
+ image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)};
+ float* const JXL_RESTRICT row = grayscale.Row(y);
+
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ row[x] = LinearToSrgb8Direct(
+ kLuminances[0] * Srgb8ToLinearDirect(input_rows[0][x]) +
+ kLuminances[1] * Srgb8ToLinearDirect(input_rows[1][x]) +
+ kLuminances[2] * Srgb8ToLinearDirect(input_rows[2][x]));
+ }
+ }
+
+ ImageF laplacian(image.xsize(), image.ysize());
+ Symmetric3(grayscale, Rect(grayscale), kLaplacianWeights, pool, &laplacian);
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const JXL_RESTRICT row = laplacian.ConstRow(y);
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ consumer(x, y, std::abs(row[x]));
+ }
+ }
+}
+
+template <typename Consumer>
+void Saturation(const jxl::Image3F& image, const Consumer& consumer) {
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const JXL_RESTRICT rows[3] = {
+ image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)};
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ // TODO(sboukortt): experiment with other methods of computing the
+ // saturation, e.g. C*/L* in LUV/LCh.
+ const float mean = (1.f / 3) * (rows[0][x] + rows[1][x] + rows[2][x]);
+ const float deviations[3] = {rows[0][x] - mean, rows[1][x] - mean,
+ rows[2][x] - mean};
+ consumer(x, y,
+ std::sqrt((1.f / 3) * (deviations[0] * deviations[0] +
+ deviations[1] * deviations[1] +
+ deviations[2] * deviations[2])));
+ }
+ }
+}
+
+template <typename Consumer>
+void MidToneness(const jxl::Image3F& image, const float sigma,
+ const Consumer& consumer) {
+ const float inv_sigma_squared = 1.f / (sigma * sigma);
+ const auto Gaussian = [inv_sigma_squared](const float x) {
+ return std::exp(-.5f * (x - .5f) * (x - .5f) * inv_sigma_squared);
+ };
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const JXL_RESTRICT rows[3] = {
+ image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)};
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ consumer(
+ x, y,
+ Gaussian(rows[0][x]) * Gaussian(rows[1][x]) * Gaussian(rows[2][x]));
+ }
+ }
+}
+
+ImageF ComputeWeights(const jxl::Image3F& image, const float contrast_weight,
+ const float saturation_weight,
+ const float midtoneness_weight,
+ const float midtoneness_sigma, ThreadPool* const pool) {
+ ImageF log_weights(image.xsize(), image.ysize());
+ ZeroFillImage(&log_weights);
+
+ if (contrast_weight > 0) {
+ Contrast(
+ image,
+ [&log_weights, contrast_weight](const size_t x, const size_t y,
+ const float weight) {
+ log_weights.Row(y)[x] = contrast_weight * std::log(weight);
+ },
+ pool);
+ }
+
+ if (saturation_weight > 0) {
+ Saturation(image, [&log_weights, saturation_weight](
+ const size_t x, const size_t y, const float weight) {
+ log_weights.Row(y)[x] += saturation_weight * std::log(weight);
+ });
+ }
+
+ if (midtoneness_weight > 0) {
+ MidToneness(image, midtoneness_sigma,
+ [&log_weights, midtoneness_weight](
+ const size_t x, const size_t y, const float weight) {
+ log_weights.Row(y)[x] +=
+ midtoneness_weight * std::log(weight);
+ });
+ }
+
+ ImageF weights = std::move(log_weights);
+
+ for (size_t y = 0; y < weights.ysize(); ++y) {
+ float* const JXL_RESTRICT row = weights.Row(y);
+ for (size_t x = 0; x < weights.xsize(); ++x) {
+ row[x] = std::exp(row[x]);
+ }
+ }
+
+ return weights;
+}
+
+std::vector<ImageF> ComputeWeights(const std::vector<Image3F>& images,
+ const float contrast_weight,
+ const float saturation_weight,
+ const float midtoneness_weight,
+ const float midtoneness_sigma,
+ ThreadPool* const pool) {
+ std::vector<ImageF> weights;
+ weights.reserve(images.size());
+ for (const Image3F& image : images) {
+ if (image.xsize() != images.front().xsize() ||
+ image.ysize() != images.front().ysize()) {
+ return {};
+ }
+ weights.push_back(ComputeWeights(image, contrast_weight, saturation_weight,
+ midtoneness_weight, midtoneness_sigma,
+ pool));
+ }
+
+ std::vector<float*> rows(images.size());
+ for (size_t y = 0; y < images.front().ysize(); ++y) {
+ for (size_t i = 0; i < images.size(); ++i) {
+ rows[i] = weights[i].Row(y);
+ }
+ for (size_t x = 0; x < images.front().xsize(); ++x) {
+ float sum = 1e-9f;
+ for (size_t i = 0; i < images.size(); ++i) {
+ sum += rows[i][x];
+ }
+ const float ratio = 1.f / sum;
+ for (size_t i = 0; i < images.size(); ++i) {
+ rows[i][x] *= ratio;
+ }
+ }
+ }
+
+ return weights;
+}
+
+ImageF Downsample(const ImageF& image, ThreadPool* const pool) {
+ ImageF filtered(image.xsize(), image.ysize());
+ Separable5(image, Rect(image), kPyramidFilter, pool, &filtered);
+ ImageF result(DivCeil(image.xsize(), 2), DivCeil(image.ysize(), 2));
+ for (size_t y = 0; y < result.ysize(); ++y) {
+ const float* const JXL_RESTRICT filtered_row = filtered.ConstRow(2 * y);
+ float* const JXL_RESTRICT row = result.Row(y);
+ for (size_t x = 0; x < result.xsize(); ++x) {
+ row[x] = filtered_row[2 * x];
+ }
+ }
+ return result;
+}
+
+Image3F Downsample(const Image3F& image, ThreadPool* const pool) {
+ return Image3F(Downsample(image.Plane(0), pool),
+ Downsample(image.Plane(1), pool),
+ Downsample(image.Plane(2), pool));
+}
+
+Image3F PadImageMirror(const Image3F& in, const size_t xborder,
+ const size_t yborder) {
+ size_t xsize = in.xsize();
+ size_t ysize = in.ysize();
+ Image3F out(xsize + 2 * xborder, ysize + 2 * yborder);
+ if (xborder > xsize || yborder > ysize) {
+ for (size_t c = 0; c < 3; c++) {
+ for (int32_t y = 0; y < static_cast<int32_t>(out.ysize()); y++) {
+ float* row_out = out.PlaneRow(c, y);
+ const float* row_in = in.PlaneRow(
+ c, Mirror(y - static_cast<int32_t>(yborder), in.ysize()));
+ for (int32_t x = 0; x < static_cast<int32_t>(out.xsize()); x++) {
+ int32_t xin = Mirror(x - static_cast<int32_t>(xborder), in.xsize());
+ row_out[x] = row_in[xin];
+ }
+ }
+ }
+ return out;
+ }
+ CopyImageTo(Rect(in), in, Rect(xborder, yborder, xsize, ysize), &out);
+ for (size_t c = 0; c < 3; c++) {
+ // Horizontal pad.
+ for (size_t y = 0; y < ysize; y++) {
+ for (size_t x = 0; x < xborder; x++) {
+ out.PlaneRow(c, y + yborder)[x] =
+ in.ConstPlaneRow(c, y)[xborder - x - 1];
+ out.PlaneRow(c, y + yborder)[x + xsize + xborder] =
+ in.ConstPlaneRow(c, y)[xsize - 1 - x];
+ }
+ }
+ // Vertical pad.
+ for (size_t y = 0; y < yborder; y++) {
+ memcpy(out.PlaneRow(c, y), out.ConstPlaneRow(c, 2 * yborder - 1 - y),
+ out.xsize() * sizeof(float));
+ memcpy(out.PlaneRow(c, y + ysize + yborder),
+ out.ConstPlaneRow(c, ysize + yborder - 1 - y),
+ out.xsize() * sizeof(float));
+ }
+ }
+ return out;
+}
+
+Image3F Upsample(const Image3F& image, const bool odd_width,
+ const bool odd_height, ThreadPool* const pool) {
+ const Image3F padded = PadImageMirror(image, 1, 1);
+ Image3F upsampled(2 * padded.xsize(), 2 * padded.ysize());
+ ZeroFillImage(&upsampled);
+ for (int c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < padded.ysize(); ++y) {
+ const float* const JXL_RESTRICT padded_row = padded.ConstPlaneRow(c, y);
+ float* const JXL_RESTRICT row = upsampled.PlaneRow(c, 2 * y);
+ for (size_t x = 0; x < padded.xsize(); ++x) {
+ row[2 * x] = 4 * padded_row[x];
+ }
+ }
+ }
+ Image3F filtered(upsampled.xsize(), upsampled.ysize());
+ for (int c = 0; c < 3; ++c) {
+ Separable5(upsampled.Plane(c), Rect(upsampled), kPyramidFilter, pool,
+ &filtered.Plane(c));
+ }
+ Image3F result(2 * image.xsize() - (odd_width ? 1 : 0),
+ 2 * image.ysize() - (odd_height ? 1 : 0));
+ CopyImageTo(Rect(2, 2, result.xsize(), result.ysize()), filtered,
+ Rect(result), &result);
+ return result;
+}
+
+std::vector<ImageF> GaussianPyramid(ImageF image, int num_levels,
+ ThreadPool* pool) {
+ std::vector<ImageF> pyramid(num_levels);
+ for (int i = 0; i < num_levels - 1; ++i) {
+ ImageF downsampled = Downsample(image, pool);
+ pyramid[i] = std::move(image);
+ image = std::move(downsampled);
+ }
+ pyramid[num_levels - 1] = std::move(image);
+ return pyramid;
+}
+
+std::vector<Image3F> LaplacianPyramid(Image3F image, int num_levels,
+ ThreadPool* pool) {
+ std::vector<Image3F> pyramid(num_levels);
+ for (int i = 0; i < num_levels - 1; ++i) {
+ Image3F downsampled = Downsample(image, pool);
+ const bool odd_width = image.xsize() % 2 != 0;
+ const bool odd_height = image.ysize() % 2 != 0;
+ Subtract(image, Upsample(downsampled, odd_width, odd_height, pool), &image);
+ pyramid[i] = std::move(image);
+ image = std::move(downsampled);
+ }
+ pyramid[num_levels - 1] = std::move(image);
+ return pyramid;
+}
+
+Image3F ReconstructFromLaplacianPyramid(std::vector<Image3F> pyramid,
+ ThreadPool* const pool) {
+ Image3F result = std::move(pyramid.back());
+ pyramid.pop_back();
+ for (auto it = pyramid.rbegin(); it != pyramid.rend(); ++it) {
+ const bool odd_width = it->xsize() % 2 != 0;
+ const bool odd_height = it->ysize() % 2 != 0;
+ result = Upsample(result, odd_width, odd_height, pool);
+ AddTo(Rect(result), *it, &result);
+ }
+ return result;
+}
+
+// Exposure fusion algorithm as described in:
+// https://mericam.github.io/exposure_fusion/
+//
+// That is, given n images of identical size: for each pixel coordinate, one
+// weight per input image is computed, indicating how much each input image will
+// contribute to the result. There are therefore n weight maps, the sum of which
+// is 1 at every pixel.
+//
+// Those weights are then applied at various scales rather than directly at full
+// resolution. To understand how, it helps to familiarize oneself with Laplacian
+// and Gaussian pyramids, as described in "The Laplacian Pyramid as a Compact
+// Image Code" by P. Burt and E. Adelson:
+// http://persci.mit.edu/pub_pdfs/pyramid83.pdf
+//
+// A Gaussian pyramid of k levels is a sequence of k images in which the first
+// image is the original image and each following level is a low-pass-filtered
+// version of the previous one. A Laplacian pyramid is obtained from a Gaussian
+// pyramid by:
+//
+// laplacian_pyramid[i] = gaussian_pyramid[i] − gaussian_pyramid[i + 1].
+// (The last item of the Laplacian pyramid is just the last one from the
+// Gaussian pyramid without subtraction.)
+//
+// From there, the original image can be reconstructed by adding all the images
+// from the Laplacian pyramid together. (If desired, the Gaussian pyramid can be
+// reconstructed as well by storing the cumulative sums starting from the end.)
+//
+// Having established that, the application of the weight images is done by
+// constructing a Laplacian pyramid for each input image, as well as a Gaussian
+// pyramid for each weight image, and then constructing a Laplacian pyramid such
+// that:
+//
+// pyramid[i] = sum(laplacian_pyramids[j][i] .* weight_gaussian_pyramids[j][i]
+// for j in 1..n)
+//
+// And then reconstructing an image from the pyramid thus obtained.
+Image3F ExposureFusion(std::vector<Image3F> images, int num_levels,
+ const float contrast_weight,
+ const float saturation_weight,
+ const float midtoneness_weight,
+ const float midtoneness_sigma, ThreadPool* const pool) {
+ std::vector<ImageF> weights =
+ ComputeWeights(images, contrast_weight, saturation_weight,
+ midtoneness_weight, midtoneness_sigma, pool);
+
+ std::vector<Image3F> pyramid(num_levels);
+ for (size_t i = 0; i < images.size(); ++i) {
+ const std::vector<ImageF> weight_pyramid =
+ GaussianPyramid(std::move(weights[i]), num_levels, pool);
+ const std::vector<Image3F> image_pyramid =
+ LaplacianPyramid(std::move(images[i]), num_levels, pool);
+
+ for (int k = 0; k < num_levels; ++k) {
+ Image3F product(Product(weight_pyramid[k], image_pyramid[k].Plane(0)),
+ Product(weight_pyramid[k], image_pyramid[k].Plane(1)),
+ Product(weight_pyramid[k], image_pyramid[k].Plane(2)));
+ if (pyramid[k].xsize() == 0) {
+ pyramid[k] = std::move(product);
+ } else {
+ AddTo(Rect(product), product, &pyramid[k]);
+ }
+ }
+ }
+
+ return ReconstructFromLaplacianPyramid(std::move(pyramid), pool);
+}
+
+} // namespace
+} // namespace jxl
+
+int main(int argc, const char** argv) {
+ jpegxl::tools::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ float max_nits = 0;
+ parser.AddOptionValue('m', "max_nits", "nits",
+ "maximum luminance in the image", &max_nits,
+ &jpegxl::tools::ParseFloat, 0);
+ float preserve_saturation = .1f;
+ parser.AddOptionValue(
+ 's', "preserve_saturation", "0..1",
+ "to what extent to try and preserve saturation over luminance",
+ &preserve_saturation, &jpegxl::tools::ParseFloat, 0);
+ int64_t num_levels = -1;
+ parser.AddOptionValue('l', "num_levels", "1..",
+ "number of levels in the pyramid", &num_levels,
+ &jpegxl::tools::ParseInt64, 0);
+ float contrast_weight = 0.f;
+ parser.AddOptionValue('c', "contrast_weight", "0..",
+ "importance of contrast when computing weights",
+ &contrast_weight, &jpegxl::tools::ParseFloat, 0);
+ float saturation_weight = .2f;
+ parser.AddOptionValue('a', "saturation_weight", "0..",
+ "importance of saturation when computing weights",
+ &saturation_weight, &jpegxl::tools::ParseFloat, 0);
+ float midtoneness_weight = 1.f;
+ parser.AddOptionValue('t', "midtoneness_weight", "0..",
+ "importance of \"midtoneness\" when computing weights",
+ &midtoneness_weight, &jpegxl::tools::ParseFloat, 0);
+ float midtoneness_sigma = .2f;
+ parser.AddOptionValue('g', "midtoneness_sigma", "0..",
+ "spread of the function that computes midtoneness",
+ &midtoneness_sigma, &jpegxl::tools::ParseFloat, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::CodecInOut image;
+ jxl::extras::ColorHints color_hints;
+ color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ");
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool));
+
+ if (max_nits > 0) {
+ image.metadata.m.SetIntensityTarget(max_nits);
+ } else {
+ max_nits = image.metadata.m.IntensityTarget();
+ }
+
+ std::vector<jxl::Image3F> input_images;
+
+ if (max_nits <= 4 * jxl::kDefaultIntensityTarget) {
+ jxl::CodecInOut sRGB_image;
+ jxl::Image3F color(image.xsize(), image.ysize());
+ CopyImageTo(*image.Main().color(), &color);
+ sRGB_image.SetFromImage(std::move(color), image.Main().c_current());
+ JXL_CHECK(sRGB_image.Main().TransformTo(jxl::ColorEncoding::SRGB(),
+ *JxlGetDefaultCms(), &pool));
+ input_images.push_back(std::move(*sRGB_image.Main().color()));
+ }
+
+ for (int i = 0; i < 4; ++i) {
+ const float target = std::ldexp(jxl::kDefaultIntensityTarget, 2 - i);
+ if (target >= max_nits) continue;
+ jxl::CodecInOut tone_mapped_image;
+ jxl::Image3F color(image.xsize(), image.ysize());
+ CopyImageTo(*image.Main().color(), &color);
+ tone_mapped_image.SetFromImage(std::move(color), image.Main().c_current());
+ tone_mapped_image.metadata.m.SetIntensityTarget(
+ image.metadata.m.IntensityTarget());
+ JXL_CHECK(jxl::ToneMapTo({0, target}, &tone_mapped_image, &pool));
+ JXL_CHECK(jxl::GamutMap(&tone_mapped_image, preserve_saturation, &pool));
+ JXL_CHECK(tone_mapped_image.Main().TransformTo(jxl::ColorEncoding::SRGB(),
+ *JxlGetDefaultCms(), &pool));
+ input_images.push_back(std::move(*tone_mapped_image.Main().color()));
+ }
+
+ if (num_levels < 1) {
+ num_levels = jxl::FloorLog2Nonzero(std::min(image.xsize(), image.ysize()));
+ }
+
+ jxl::Image3F fused = jxl::ExposureFusion(
+ std::move(input_images), num_levels, contrast_weight, saturation_weight,
+ midtoneness_weight, midtoneness_sigma, &pool);
+
+ jxl::CodecInOut output;
+ output.SetFromImage(std::move(fused), jxl::ColorEncoding::SRGB());
+
+ JXL_CHECK(jxl::Encode(output, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
+}
diff --git a/tools/hdr/pq_to_hlg.cc b/tools/hdr/pq_to_hlg.cc
index 3b2125b..ea47a6b 100644
--- a/tools/hdr/pq_to_hlg.cc
+++ b/tools/hdr/pq_to_hlg.cc
@@ -9,13 +9,13 @@
#include "lib/extras/codec.h"
#include "lib/extras/hlg.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/enc_color_management.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
float max_nits = 0;
@@ -56,10 +56,14 @@ int main(int argc, const char** argv) {
jxl::CodecInOut image;
jxl::extras::ColorHints color_hints;
color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ");
- JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool));
if (max_nits > 0) {
image.metadata.m.SetIntensityTarget(max_nits);
}
+ const jxl::Primaries original_primaries =
+ image.Main().c_current().GetPrimariesType();
JXL_CHECK(jxl::ToneMapTo({0, 1000}, &image, &pool));
JXL_CHECK(jxl::HlgInverseOOTF(&image.Main(), 1.2f, &pool));
JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool));
@@ -70,11 +74,12 @@ int main(int argc, const char** argv) {
jxl::ColorEncoding hlg;
hlg.SetColorSpace(jxl::ColorSpace::kRGB);
- hlg.primaries = jxl::Primaries::k2100;
- hlg.white_point = jxl::WhitePoint::kD65;
- hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG);
+ JXL_CHECK(hlg.SetPrimariesType(original_primaries));
+ JXL_CHECK(hlg.SetWhitePointType(jxl::WhitePoint::kD65));
+ hlg.Tf().SetTransferFunction(jxl::TransferFunction::kHLG);
JXL_CHECK(hlg.CreateICC());
- JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool));
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, hlg, &pool));
image.metadata.m.color_encoding = hlg;
- JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/hdr/render_hlg.cc b/tools/hdr/render_hlg.cc
index c8a2395..cca43b1 100644
--- a/tools/hdr/render_hlg.cc
+++ b/tools/hdr/render_hlg.cc
@@ -9,13 +9,13 @@
#include "lib/extras/codec.h"
#include "lib/extras/hlg.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/enc_color_management.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
float target_nits = 0;
@@ -71,7 +71,9 @@ int main(int argc, const char** argv) {
jxl::CodecInOut image;
jxl::extras::ColorHints color_hints;
color_hints.Add("color_space", "RGB_D65_202_Rel_HLG");
- JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool));
// Ensures that conversions to linear by JxlCms will not apply the OOTF as we
// apply it ourselves to control the subsequent gamut mapping.
image.metadata.m.SetIntensityTarget(301);
@@ -82,13 +84,12 @@ int main(int argc, const char** argv) {
image.metadata.m.SetIntensityTarget(target_nits);
jxl::ColorEncoding c_out = image.metadata.m.color_encoding;
- if (pq) {
- c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ);
- } else {
- c_out.tf.SetTransferFunction(jxl::TransferFunction::k709);
- }
+ jxl::cms::TransferFunction tf =
+ pq ? jxl::TransferFunction::kPQ : jxl::TransferFunction::kSRGB;
+ c_out.Tf().SetTransferFunction(tf);
JXL_CHECK(c_out.CreateICC());
- JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool));
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, c_out, &pool));
image.metadata.m.color_encoding = c_out;
- JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/hdr/texture_to_cube.cc b/tools/hdr/texture_to_cube.cc
index a5e5af7..0d9f731 100644
--- a/tools/hdr/texture_to_cube.cc
+++ b/tools/hdr/texture_to_cube.cc
@@ -7,12 +7,13 @@
#include <stdlib.h>
#include "lib/extras/codec.h"
-#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/image_bundle.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
const char* input_filename = nullptr;
@@ -42,8 +43,10 @@ int main(int argc, const char** argv) {
}
jxl::CodecInOut image;
- JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image,
- &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(),
+ &image, &pool));
JXL_CHECK(image.xsize() == image.ysize() * image.ysize());
const unsigned N = image.ysize();
diff --git a/tools/hdr/tone_map.cc b/tools/hdr/tone_map.cc
index 1ef3823..67fea48 100644
--- a/tools/hdr/tone_map.cc
+++ b/tools/hdr/tone_map.cc
@@ -8,13 +8,14 @@
#include "lib/extras/codec.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/enc_color_management.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
float max_nits = 0;
@@ -69,7 +70,9 @@ int main(int argc, const char** argv) {
jxl::CodecInOut image;
jxl::extras::ColorHints color_hints;
color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ");
- JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool));
if (max_nits > 0) {
image.metadata.m.SetIntensityTarget(max_nits);
}
@@ -77,13 +80,18 @@ int main(int argc, const char** argv) {
JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool));
jxl::ColorEncoding c_out = image.metadata.m.color_encoding;
- if (pq) {
- c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ);
- } else {
- c_out.tf.SetTransferFunction(jxl::TransferFunction::k709);
+ jxl::cms::TransferFunction tf =
+ pq ? jxl::TransferFunction::kPQ : jxl::TransferFunction::kSRGB;
+
+ if (jxl::extras::CodecFromPath(output_filename) == jxl::extras::Codec::kEXR) {
+ tf = jxl::TransferFunction::kLinear;
+ image.metadata.m.SetFloat16Samples();
}
+ c_out.Tf().SetTransferFunction(tf);
+
JXL_CHECK(c_out.CreateICC());
- JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool));
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, c_out, &pool));
image.metadata.m.color_encoding = c_out;
- JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/icc_codec_fuzzer.cc b/tools/icc_codec_fuzzer.cc
index 0af805c..410ece1 100644
--- a/tools/icc_codec_fuzzer.cc
+++ b/tools/icc_codec_fuzzer.cc
@@ -6,7 +6,15 @@
#include "lib/jxl/enc_icc_codec.h"
#include "lib/jxl/icc_codec.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::PaddedBytes;
+
+#ifdef JXL_ICC_FUZZER_SLOW_TEST
+using ::jxl::BitReader;
+using ::jxl::Span;
+#endif
int TestOneInput(const uint8_t* data, size_t size) {
#if defined(JXL_ICC_FUZZER_ONLY_WRITE)
@@ -27,33 +35,32 @@ int TestOneInput(const uint8_t* data, size_t size) {
// the ICC parsing.
if (read) {
// Reading parses the compressed format.
- BitReader br(Span<const uint8_t>(data, size));
- PaddedBytes result;
- (void)ReadICC(&br, &result);
+ BitReader br(Bytes(data, size));
+ std::vector<uint8_t> result;
+ (void)jxl::test::ReadICC(&br, &result);
(void)br.Close();
} else {
// Writing parses the original ICC profile.
PaddedBytes icc;
icc.assign(data, data + size);
BitWriter writer;
- AuxOut aux;
// Writing should support any random bytestream so must succeed, make
// fuzzer fail if not.
- JXL_ASSERT(WriteICC(icc, &writer, 0, &aux));
+ JXL_ASSERT(jxl::WriteICC(icc, &writer, 0, nullptr));
}
#else // JXL_ICC_FUZZER_SLOW_TEST
if (read) {
// Reading (unpredicting) parses the compressed format.
PaddedBytes result;
- (void)UnpredictICC(data, size, &result);
+ (void)jxl::UnpredictICC(data, size, &result);
} else {
// Writing (predicting) parses the original ICC profile.
PaddedBytes result;
// Writing should support any random bytestream so must succeed, make
// fuzzer fail if not.
- JXL_ASSERT(PredictICC(data, size, &result));
+ JXL_ASSERT(jxl::PredictICC(data, size, &result));
PaddedBytes reconstructed;
- JXL_ASSERT(UnpredictICC(result.data(), result.size(), &reconstructed));
+ JXL_ASSERT(jxl::UnpredictICC(result.data(), result.size(), &reconstructed));
JXL_ASSERT(reconstructed.size() == size);
JXL_ASSERT(memcmp(data, reconstructed.data(), size) == 0);
}
@@ -61,8 +68,9 @@ int TestOneInput(const uint8_t* data, size_t size) {
return 0;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- return jxl::TestOneInput(data, size);
+ return jpegxl::tools::TestOneInput(data, size);
}
diff --git a/tools/icc_detect/icc_detect.h b/tools/icc_detect/icc_detect.h
index 9335d94..deca6d7 100644
--- a/tools/icc_detect/icc_detect.h
+++ b/tools/icc_detect/icc_detect.h
@@ -9,11 +9,13 @@
#include <QByteArray>
#include <QWidget>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
// Should be cached if possible.
QByteArray GetMonitorIccProfile(const QWidget* widget);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_ICC_DETECT_ICC_DETECT_H_
diff --git a/tools/icc_detect/icc_detect_empty.cc b/tools/icc_detect/icc_detect_empty.cc
index abd4a95..421ac50 100644
--- a/tools/icc_detect/icc_detect_empty.cc
+++ b/tools/icc_detect/icc_detect_empty.cc
@@ -5,10 +5,12 @@
#include "tools/icc_detect/icc_detect.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
QByteArray GetMonitorIccProfile(const QWidget* const /*widget*/) {
return QByteArray();
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/icc_detect/icc_detect_win32.cc b/tools/icc_detect/icc_detect_win32.cc
index 39ac5ee..f06e688 100644
--- a/tools/icc_detect/icc_detect_win32.cc
+++ b/tools/icc_detect/icc_detect_win32.cc
@@ -10,7 +10,8 @@
#include <memory>
#include <type_traits>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
namespace {
@@ -61,4 +62,5 @@ QByteArray GetMonitorIccProfile(const QWidget* const widget) {
return profile;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/icc_detect/icc_detect_x11.cc b/tools/icc_detect/icc_detect_x11.cc
index be1209e..e67b30e 100644
--- a/tools/icc_detect/icc_detect_x11.cc
+++ b/tools/icc_detect/icc_detect_x11.cc
@@ -3,17 +3,23 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// clang-format off
#include "tools/icc_detect/icc_detect.h"
+// clang-format on
#include <stdint.h>
#include <stdlib.h>
#include <xcb/xcb.h>
-#include <QX11Info>
-#include <algorithm>
#include <memory>
-namespace jxl {
+// clang-format off
+#include <QApplication>
+#include <X11/Xlib.h>
+// clang-format on
+
+namespace jpegxl {
+namespace tools {
namespace {
@@ -30,11 +36,17 @@ using XcbUniquePtr = std::unique_ptr<T, FreeDeleter>;
QByteArray GetMonitorIccProfile(const QWidget* const widget) {
Q_UNUSED(widget)
- xcb_connection_t* const connection = QX11Info::connection();
+ auto* const qX11App =
+ qGuiApp->nativeInterface<QNativeInterface::QX11Application>();
+ if (qX11App == nullptr) {
+ return QByteArray();
+ }
+ xcb_connection_t* const connection = qX11App->connection();
if (connection == nullptr) {
return QByteArray();
}
- const int screen_number = QX11Info::appScreen();
+
+ const int screenNumber = DefaultScreen(qX11App->display());
const xcb_intern_atom_cookie_t atomRequest =
xcb_intern_atom(connection, /*only_if_exists=*/1,
@@ -51,7 +63,7 @@ QByteArray GetMonitorIccProfile(const QWidget* const widget) {
for (xcb_screen_iterator_t it =
xcb_setup_roots_iterator(xcb_get_setup(connection));
it.rem; xcb_screen_next(&it)) {
- if (i == screen_number) {
+ if (i == screenNumber) {
screen = it.data;
break;
}
@@ -74,4 +86,5 @@ QByteArray GetMonitorIccProfile(const QWidget* const widget) {
xcb_get_property_value_length(profile.get()));
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java b/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java
index 440ef6e..7bdd6a7 100644
--- a/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java
+++ b/tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java
@@ -32,7 +32,14 @@ public class Decoder {
return new ImageData(basicInfo.width, basicInfo.height, pixels, icc, pixelFormat);
}
- // TODO(eustas): accept byte-array as input.
+ public static StreamInfo decodeInfo(byte[] data) {
+ return decodeInfo(ByteBuffer.wrap(data));
+ }
+
+ public static StreamInfo decodeInfo(byte[] data, int offset, int length) {
+ return decodeInfo(ByteBuffer.wrap(data, offset, length));
+ }
+
public static StreamInfo decodeInfo(Buffer data) {
return DecoderJni.getBasicInfo(data, null);
}
diff --git a/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc b/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc
index 1b3847e..d61464e 100644
--- a/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc
+++ b/tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc
@@ -6,13 +6,11 @@
#include "tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h"
#include <jni.h>
+#include <jxl/decode.h>
+#include <jxl/thread_parallel_runner.h>
#include <cstdlib>
-#include "jxl/decode.h"
-#include "jxl/thread_parallel_runner.h"
-#include "lib/jxl/base/status.h"
-
namespace {
template <typename From, typename To>
@@ -34,11 +32,11 @@ bool BufferToSpan(JNIEnv* env, jobject buffer, uint8_t** data, size_t* size) {
return StaticCast(env->GetDirectBufferCapacity(buffer), size);
}
-int ToStatusCode(const jxl::Status& status) {
- if (status) return 0;
- if (status.IsFatalError()) return -1;
- return 1; // Non-fatal -> not enough input.
-}
+enum class Status { OK = 0, FATAL_ERROR = -1, NOT_ENOUGH_INPUT = 1 };
+
+bool IsOk(Status status) { return status == Status::OK; }
+
+#define FAILURE(M) Status::FATAL_ERROR
constexpr const size_t kLastPixelFormat = 3;
constexpr const size_t kNoPixelFormat = static_cast<size_t>(-1);
@@ -64,28 +62,27 @@ JxlPixelFormat ToPixelFormat(size_t pixel_format) {
}
}
-jxl::Status DoDecode(JNIEnv* env, jobject data_buffer, size_t* info_pixels_size,
- size_t* info_icc_size, JxlBasicInfo* info,
- size_t pixel_format, jobject pixels_buffer,
- jobject icc_buffer) {
- if (data_buffer == nullptr) return JXL_FAILURE("No data buffer");
+Status DoDecode(JNIEnv* env, jobject data_buffer, size_t* info_pixels_size,
+ size_t* info_icc_size, JxlBasicInfo* info, size_t pixel_format,
+ jobject pixels_buffer, jobject icc_buffer) {
+ if (data_buffer == nullptr) return FAILURE("No data buffer");
uint8_t* data = nullptr;
size_t data_size = 0;
if (!BufferToSpan(env, data_buffer, &data, &data_size)) {
- return JXL_FAILURE("Failed to access data buffer");
+ return FAILURE("Failed to access data buffer");
}
uint8_t* pixels = nullptr;
size_t pixels_size = 0;
if (!BufferToSpan(env, pixels_buffer, &pixels, &pixels_size)) {
- return JXL_FAILURE("Failed to access pixels buffer");
+ return FAILURE("Failed to access pixels buffer");
}
uint8_t* icc = nullptr;
size_t icc_size = 0;
if (!BufferToSpan(env, icc_buffer, &icc, &icc_size)) {
- return JXL_FAILURE("Failed to access ICC buffer");
+ return FAILURE("Failed to access ICC buffer");
}
JxlDecoder* dec = JxlDecoderCreate(NULL);
@@ -105,80 +102,76 @@ jxl::Status DoDecode(JNIEnv* env, jobject data_buffer, size_t* info_pixels_size,
auto status =
JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner);
if (status != JXL_DEC_SUCCESS) {
- return JXL_FAILURE("Failed to set parallel runner");
+ return FAILURE("Failed to set parallel runner");
}
status = JxlDecoderSubscribeEvents(
dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE | JXL_DEC_COLOR_ENCODING);
if (status != JXL_DEC_SUCCESS) {
- return JXL_FAILURE("Failed to subscribe for events");
+ return FAILURE("Failed to subscribe for events");
}
status = JxlDecoderSetInput(dec, data, data_size);
if (status != JXL_DEC_SUCCESS) {
- return JXL_FAILURE("Failed to set input");
+ return FAILURE("Failed to set input");
}
status = JxlDecoderProcessInput(dec);
if (status == JXL_DEC_NEED_MORE_INPUT) {
- return JXL_STATUS(jxl::StatusCode::kNotEnoughBytes, "Not enough input");
+ return Status::NOT_ENOUGH_INPUT;
}
if (status != JXL_DEC_BASIC_INFO) {
- return JXL_FAILURE("Unexpected notification (want: basic info)");
+ return FAILURE("Unexpected notification (want: basic info)");
}
if (info_pixels_size) {
JxlPixelFormat format = ToPixelFormat(pixel_format);
status = JxlDecoderImageOutBufferSize(dec, &format, info_pixels_size);
if (status != JXL_DEC_SUCCESS) {
- return JXL_FAILURE("Failed to get pixels size");
+ return FAILURE("Failed to get pixels size");
}
}
if (info) {
status = JxlDecoderGetBasicInfo(dec, info);
if (status != JXL_DEC_SUCCESS) {
- return JXL_FAILURE("Failed to get basic info");
+ return FAILURE("Failed to get basic info");
}
}
status = JxlDecoderProcessInput(dec);
if (status != JXL_DEC_COLOR_ENCODING) {
- return JXL_FAILURE("Unexpected notification (want: color encoding)");
+ return FAILURE("Unexpected notification (want: color encoding)");
}
if (info_icc_size) {
- JxlPixelFormat format = ToPixelFormat(pixel_format);
- status = JxlDecoderGetICCProfileSize(
- dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, info_icc_size);
+ status = JxlDecoderGetICCProfileSize(dec, JXL_COLOR_PROFILE_TARGET_DATA,
+ info_icc_size);
if (status != JXL_DEC_SUCCESS) *info_icc_size = 0;
}
if (icc && icc_size > 0) {
- JxlPixelFormat format = ToPixelFormat(pixel_format);
- status = JxlDecoderGetColorAsICCProfile(
- dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, icc, icc_size);
+ status = JxlDecoderGetColorAsICCProfile(dec, JXL_COLOR_PROFILE_TARGET_DATA,
+ icc, icc_size);
if (status != JXL_DEC_SUCCESS) {
- return JXL_FAILURE("Failed to get ICC");
+ return FAILURE("Failed to get ICC");
}
}
if (pixels) {
JxlPixelFormat format = ToPixelFormat(pixel_format);
status = JxlDecoderProcessInput(dec);
if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
- return JXL_FAILURE("Unexpected notification (want: need out buffer)");
+ return FAILURE("Unexpected notification (want: need out buffer)");
}
status = JxlDecoderSetImageOutBuffer(dec, &format, pixels, pixels_size);
if (status != JXL_DEC_SUCCESS) {
- return JXL_FAILURE("Failed to set out buffer");
+ return FAILURE("Failed to set out buffer");
}
status = JxlDecoderProcessInput(dec);
if (status != JXL_DEC_FULL_IMAGE) {
- return JXL_FAILURE("Unexpected notification (want: full image)");
+ return FAILURE("Unexpected notification (want: full image)");
}
status = JxlDecoderProcessInput(dec);
if (status != JXL_DEC_SUCCESS) {
- return JXL_FAILURE("Unexpected notification (want: success)");
+ return FAILURE("Unexpected notification (want: success)");
}
}
- return true;
+ return Status::OK;
}
-#undef FAILURE
-
} // namespace
#ifdef __cplusplus
@@ -196,18 +189,18 @@ Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetBasicInfo(
size_t icc_size = 0;
size_t pixel_format = 0;
- jxl::Status status = true;
+ Status status = Status::OK;
- if (status) {
+ if (IsOk(status)) {
pixel_format = context[0];
if (pixel_format == kNoPixelFormat) {
// OK
} else if (pixel_format > kLastPixelFormat) {
- status = JXL_FAILURE("Unrecognized pixel format");
+ status = FAILURE("Unrecognized pixel format");
}
}
- if (status) {
+ if (IsOk(status)) {
bool want_output_size = (pixel_format != kNoPixelFormat);
if (want_output_size) {
status = DoDecode(
@@ -221,17 +214,17 @@ Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetBasicInfo(
}
}
- if (status) {
+ if (IsOk(status)) {
bool ok = true;
ok &= StaticCast(info.xsize, context + 1);
ok &= StaticCast(info.ysize, context + 2);
ok &= StaticCast(pixels_size, context + 3);
ok &= StaticCast(icc_size, context + 4);
ok &= StaticCast(info.alpha_bits, context + 5);
- if (!ok) status = JXL_FAILURE("Invalid value");
+ if (!ok) status = FAILURE("Invalid value");
}
- context[0] = ToStatusCode(status);
+ context[0] = static_cast<int>(status);
env->SetIntArrayRegion(ctx, 0, 6, context);
}
@@ -251,26 +244,28 @@ JNIEXPORT void JNICALL Java_org_jpeg_jpegxl_wrapper_DecoderJni_nativeGetPixels(
size_t pixel_format = 0;
- jxl::Status status = true;
+ Status status = Status::OK;
- if (status) {
+ if (IsOk(status)) {
// Unlike getBasicInfo, "no-pixel-format" is not supported.
pixel_format = context[0];
if (pixel_format > kLastPixelFormat) {
- status = JXL_FAILURE("Unrecognized pixel format");
+ status = FAILURE("Unrecognized pixel format");
}
}
- if (status) {
+ if (IsOk(status)) {
status = DoDecode(env, data_buffer, /* info_pixels_size= */ nullptr,
/* info_icc_size= */ nullptr, /* info= */ nullptr,
pixel_format, pixels_buffer, icc_buffer);
}
- context[0] = ToStatusCode(status);
+ context[0] = static_cast<int>(status);
env->SetIntArrayRegion(ctx, 0, 1, context);
}
+#undef FAILURE
+
#ifdef __cplusplus
}
#endif
diff --git a/tools/jpegli_dec_fuzzer.cc b/tools/jpegli_dec_fuzzer.cc
new file mode 100644
index 0000000..12464c6
--- /dev/null
+++ b/tools/jpegli_dec_fuzzer.cc
@@ -0,0 +1,212 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <setjmp.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <hwy/targets.h>
+#include <vector>
+
+#include "lib/jpegli/decode.h"
+
+namespace {
+
+// Externally visible value to ensure pixels are used in the fuzzer.
+int external_code = 0;
+
+template <typename It>
+void Consume(const It& begin, const It& end) {
+ for (auto it = begin; it < end; ++it) {
+ if (*it == 0) {
+ external_code ^= ~0;
+ } else {
+ external_code ^= *it;
+ }
+ }
+}
+
+// Options for the fuzzing
+struct FuzzSpec {
+ size_t chunk_size;
+ JpegliDataType output_type;
+ JpegliEndianness output_endianness;
+ int crop_output;
+};
+
+static constexpr uint8_t kFakeEoiMarker[2] = {0xff, 0xd9};
+static constexpr size_t kNumSourceBuffers = 4;
+
+class SourceManager {
+ public:
+ SourceManager(const uint8_t* data, size_t len, size_t max_chunk_size)
+ : data_(data), len_(len), max_chunk_size_(max_chunk_size) {
+ pub_.skip_input_data = skip_input_data;
+ pub_.resync_to_restart = jpegli_resync_to_restart;
+ pub_.term_source = term_source;
+ pub_.init_source = init_source;
+ pub_.fill_input_buffer = fill_input_buffer;
+ if (max_chunk_size_ == 0) max_chunk_size_ = len;
+ buffers_.resize(kNumSourceBuffers, std::vector<uint8_t>(max_chunk_size_));
+ Reset();
+ }
+
+ void Reset() {
+ pub_.next_input_byte = nullptr;
+ pub_.bytes_in_buffer = 0;
+ pos_ = 0;
+ chunk_idx_ = 0;
+ }
+
+ private:
+ jpeg_source_mgr pub_;
+ const uint8_t* data_;
+ size_t len_;
+ size_t chunk_idx_;
+ size_t pos_;
+ size_t max_chunk_size_;
+ std::vector<std::vector<uint8_t>> buffers_;
+
+ static void init_source(j_decompress_ptr cinfo) {}
+
+ static boolean fill_input_buffer(j_decompress_ptr cinfo) {
+ auto src = reinterpret_cast<SourceManager*>(cinfo->src);
+ if (src->pos_ < src->len_) {
+ size_t remaining = src->len_ - src->pos_;
+ size_t chunk_size = std::min(remaining, src->max_chunk_size_);
+ size_t next_idx = ++src->chunk_idx_ % kNumSourceBuffers;
+ // Larger number of chunks causes fuzzer timuout.
+ if (src->chunk_idx_ >= (1u << 15)) {
+ chunk_size = remaining;
+ next_idx = src->buffers_.size();
+ src->buffers_.emplace_back(chunk_size);
+ }
+ uint8_t* next_buffer = src->buffers_[next_idx].data();
+ memcpy(next_buffer, src->data_ + src->pos_, chunk_size);
+ src->pub_.next_input_byte = next_buffer;
+ src->pub_.bytes_in_buffer = chunk_size;
+ } else {
+ src->pub_.next_input_byte = kFakeEoiMarker;
+ src->pub_.bytes_in_buffer = 2;
+ src->len_ += 2;
+ }
+ src->pos_ += src->pub_.bytes_in_buffer;
+ return TRUE;
+ }
+
+ static void skip_input_data(j_decompress_ptr cinfo, long num_bytes) {
+ auto src = reinterpret_cast<SourceManager*>(cinfo->src);
+ if (num_bytes <= 0) {
+ return;
+ }
+ if (src->pub_.bytes_in_buffer >= static_cast<size_t>(num_bytes)) {
+ src->pub_.bytes_in_buffer -= num_bytes;
+ src->pub_.next_input_byte += num_bytes;
+ } else {
+ src->pos_ += num_bytes - src->pub_.bytes_in_buffer;
+ src->pub_.bytes_in_buffer = 0;
+ }
+ }
+
+ static void term_source(j_decompress_ptr cinfo) {}
+};
+
+bool DecodeJpeg(const uint8_t* data, size_t size, size_t max_pixels,
+ const FuzzSpec& spec, std::vector<uint8_t>* pixels,
+ size_t* xsize, size_t* ysize) {
+ SourceManager src(data, size, spec.chunk_size);
+ jpeg_decompress_struct cinfo;
+ const auto try_catch_block = [&]() -> bool {
+ jpeg_error_mgr jerr;
+ jmp_buf env;
+ cinfo.err = jpegli_std_error(&jerr);
+ if (setjmp(env)) {
+ return false;
+ }
+ cinfo.client_data = reinterpret_cast<void*>(&env);
+ cinfo.err->error_exit = [](j_common_ptr cinfo) {
+ jmp_buf* env = reinterpret_cast<jmp_buf*>(cinfo->client_data);
+ jpegli_destroy(cinfo);
+ longjmp(*env, 1);
+ };
+ cinfo.err->emit_message = [](j_common_ptr cinfo, int msg_level) {};
+ jpegli_create_decompress(&cinfo);
+ cinfo.src = reinterpret_cast<jpeg_source_mgr*>(&src);
+ jpegli_read_header(&cinfo, TRUE);
+ *xsize = cinfo.image_width;
+ *ysize = cinfo.image_height;
+ size_t num_pixels = *xsize * *ysize;
+ if (num_pixels > max_pixels) return false;
+ jpegli_set_output_format(&cinfo, spec.output_type, spec.output_endianness);
+ jpegli_start_decompress(&cinfo);
+ if (spec.crop_output) {
+ JDIMENSION xoffset = cinfo.output_width / 3;
+ JDIMENSION xsize_cropped = cinfo.output_width / 3;
+ jpegli_crop_scanline(&cinfo, &xoffset, &xsize_cropped);
+ }
+
+ size_t bytes_per_sample = jpegli_bytes_per_sample(spec.output_type);
+ size_t stride =
+ bytes_per_sample * cinfo.output_components * cinfo.output_width;
+ size_t buffer_size = *ysize * stride;
+ pixels->resize(buffer_size);
+ for (size_t y = 0; y < *ysize; ++y) {
+ JSAMPROW rows[] = {pixels->data() + y * stride};
+ jpegli_read_scanlines(&cinfo, rows, 1);
+ }
+ Consume(pixels->cbegin(), pixels->cend());
+ jpegli_finish_decompress(&cinfo);
+ return true;
+ };
+ bool success = try_catch_block();
+ jpegli_destroy_decompress(&cinfo);
+ return success;
+}
+
+int TestOneInput(const uint8_t* data, size_t size) {
+ if (size < 4) return 0;
+ uint32_t flags = 0;
+ size_t used_flag_bits = 0;
+ memcpy(&flags, data + size - 4, 4);
+ size -= 4;
+
+ const auto getFlag = [&flags, &used_flag_bits](size_t max_value) {
+ size_t limit = 1;
+ while (limit <= max_value) {
+ limit <<= 1;
+ used_flag_bits++;
+ if (used_flag_bits > 32) abort();
+ }
+ uint32_t result = flags % limit;
+ flags /= limit;
+ return result % (max_value + 1);
+ };
+
+ FuzzSpec spec;
+ spec.output_type = static_cast<JpegliDataType>(getFlag(JPEGLI_TYPE_UINT16));
+ spec.output_endianness =
+ static_cast<JpegliEndianness>(getFlag(JPEGLI_BIG_ENDIAN));
+ uint32_t chunks = getFlag(15);
+ spec.chunk_size = chunks ? 1u << (chunks - 1) : 0;
+ spec.crop_output = getFlag(1);
+
+ std::vector<uint8_t> pixels;
+ size_t xsize, ysize;
+ size_t max_pixels = 1 << 21;
+
+ const auto targets = hwy::SupportedAndGeneratedTargets();
+ hwy::SetSupportedTargetsForTest(targets[getFlag(targets.size() - 1)]);
+ DecodeJpeg(data, size, max_pixels, spec, &pixels, &xsize, &ysize);
+ hwy::SetSupportedTargetsForTest(0);
+
+ return 0;
+}
+
+} // namespace
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return TestOneInput(data, size);
+}
diff --git a/tools/jpegli_dec_fuzzer_corpus.cc b/tools/jpegli_dec_fuzzer_corpus.cc
new file mode 100644
index 0000000..0963e66
--- /dev/null
+++ b/tools/jpegli_dec_fuzzer_corpus.cc
@@ -0,0 +1,365 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <setjmp.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#if defined(_WIN32) || defined(_WIN64)
+#include "third_party/dirent.h"
+#else
+#include <dirent.h>
+#include <unistd.h>
+#endif
+
+#include <algorithm>
+#include <iostream>
+#include <mutex>
+#include <random>
+#include <vector>
+
+#include "lib/jpegli/encode.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/random.h"
+#include "tools/file_io.h"
+#include "tools/thread_pool_internal.h"
+
+namespace {
+
+const size_t kMaxWidth = 50000;
+const size_t kMaxHeight = 50000;
+const size_t kMaxPixels = 20 * (1 << 20); // 20 MP
+
+std::mutex stderr_mutex;
+
+std::vector<uint8_t> GetSomeTestImage(size_t xsize, size_t ysize,
+ size_t num_channels, uint16_t seed) {
+ // Cause more significant image difference for successive seeds.
+ jxl::Rng generator(seed);
+
+ // Returns random integer in interval [0, max_value)
+ auto rng = [&generator](size_t max_value) -> size_t {
+ return generator.UniformU(0, max_value);
+ };
+
+ // Dark background gradient color
+ uint16_t r0 = rng(32768);
+ uint16_t g0 = rng(32768);
+ uint16_t b0 = rng(32768);
+ uint16_t r1 = rng(32768);
+ uint16_t g1 = rng(32768);
+ uint16_t b1 = rng(32768);
+
+ // Circle with different color
+ size_t circle_x = rng(xsize);
+ size_t circle_y = rng(ysize);
+ size_t circle_r = rng(std::min(xsize, ysize));
+
+ // Rectangle with random noise
+ size_t rect_x0 = rng(xsize);
+ size_t rect_y0 = rng(ysize);
+ size_t rect_x1 = rng(xsize);
+ size_t rect_y1 = rng(ysize);
+ if (rect_x1 < rect_x0) std::swap(rect_x0, rect_y1);
+ if (rect_y1 < rect_y0) std::swap(rect_y0, rect_y1);
+
+ size_t num_pixels = xsize * ysize;
+ std::vector<uint8_t> pixels(num_pixels * num_channels);
+ // Create pixel content to test.
+ for (size_t y = 0; y < ysize; y++) {
+ for (size_t x = 0; x < xsize; x++) {
+ uint16_t r = r0 * (ysize - y - 1) / ysize + r1 * y / ysize;
+ uint16_t g = g0 * (ysize - y - 1) / ysize + g1 * y / ysize;
+ uint16_t b = b0 * (ysize - y - 1) / ysize + b1 * y / ysize;
+ // put some shape in there for visual debugging
+ if ((x - circle_x) * (x - circle_x) + (y - circle_y) * (y - circle_y) <
+ circle_r * circle_r) {
+ r = (65535 - x * y) ^ seed;
+ g = (x << 8) + y + seed;
+ b = (y << 8) + x * seed;
+ } else if (x > rect_x0 && x < rect_x1 && y > rect_y0 && y < rect_y1) {
+ r = rng(65536);
+ g = rng(65536);
+ b = rng(65536);
+ }
+ size_t i = (y * xsize + x) * num_channels;
+ pixels[i + 0] = (r >> 8);
+ if (num_channels == 3) {
+ pixels[i + 1] = (g >> 8);
+ pixels[i + 2] = (b >> 8);
+ }
+ }
+ }
+ return pixels;
+}
+
+// ImageSpec needs to be a packed struct to allow us to use the raw memory of
+// the struct for hashing to create a consistent id.
+#pragma pack(push, 1)
+struct ImageSpec {
+ bool Validate() const {
+ if (width > kMaxWidth || height > kMaxHeight ||
+ width * height > kMaxPixels) {
+ return false;
+ }
+ return true;
+ }
+
+ friend std::ostream& operator<<(std::ostream& o, const ImageSpec& spec) {
+ o << "ImageSpec<"
+ << "size=" << spec.width << "x" << spec.height
+ << " * chan=" << spec.num_channels << " q=" << spec.quality
+ << " p=" << spec.progressive_level << " r=" << spec.restart_interval
+ << ">";
+ return o;
+ }
+
+ void SpecHash(uint8_t hash[16]) const {
+ const uint8_t* from = reinterpret_cast<const uint8_t*>(this);
+ std::seed_seq hasher(from, from + sizeof(*this));
+ uint32_t* to = reinterpret_cast<uint32_t*>(hash);
+ hasher.generate(to, to + 4);
+ }
+
+ uint32_t width = 256;
+ uint32_t height = 256;
+ uint32_t num_channels = 3;
+ uint32_t quality = 90;
+ uint32_t sampling = 0x11111111;
+ uint32_t progressive_level = 2;
+ uint32_t restart_interval = 0;
+ uint32_t fraction = 100;
+ // The seed for the PRNG.
+ uint32_t seed = 7777;
+};
+#pragma pack(pop)
+static_assert(sizeof(ImageSpec) % 4 == 0, "Add padding to ImageSpec.");
+
+bool EncodeWithJpegli(const ImageSpec& spec, const std::vector<uint8_t>& pixels,
+ std::vector<uint8_t>* compressed) {
+ uint8_t* buffer = nullptr;
+ unsigned long buffer_size = 0;
+ jpeg_compress_struct cinfo;
+ const auto try_catch_block = [&]() -> bool {
+ jpeg_error_mgr jerr;
+ jmp_buf env;
+ cinfo.err = jpegli_std_error(&jerr);
+ if (setjmp(env)) {
+ return false;
+ }
+ cinfo.client_data = reinterpret_cast<void*>(&env);
+ cinfo.err->error_exit = [](j_common_ptr cinfo) {
+ (*cinfo->err->output_message)(cinfo);
+ jmp_buf* env = reinterpret_cast<jmp_buf*>(cinfo->client_data);
+ jpegli_destroy(cinfo);
+ longjmp(*env, 1);
+ };
+ jpegli_create_compress(&cinfo);
+ jpegli_mem_dest(&cinfo, &buffer, &buffer_size);
+ cinfo.image_width = spec.width;
+ cinfo.image_height = spec.height;
+ cinfo.input_components = spec.num_channels;
+ cinfo.in_color_space = spec.num_channels == 1 ? JCS_GRAYSCALE : JCS_RGB;
+ jpegli_set_defaults(&cinfo);
+ jpegli_set_quality(&cinfo, spec.quality, TRUE);
+ uint32_t sampling = spec.sampling;
+ for (int c = 0; c < cinfo.num_components; ++c) {
+ cinfo.comp_info[c].h_samp_factor = sampling & 0xf;
+ cinfo.comp_info[c].v_samp_factor = (sampling >> 4) & 0xf;
+ sampling >>= 8;
+ }
+ jpegli_set_progressive_level(&cinfo, spec.progressive_level);
+ cinfo.restart_interval = spec.restart_interval;
+ jpegli_start_compress(&cinfo, TRUE);
+ size_t stride = cinfo.image_width * cinfo.input_components;
+ std::vector<uint8_t> row_bytes(stride);
+ for (size_t y = 0; y < cinfo.image_height; ++y) {
+ memcpy(&row_bytes[0], &pixels[y * stride], stride);
+ JSAMPROW row[] = {row_bytes.data()};
+ jpegli_write_scanlines(&cinfo, row, 1);
+ }
+ jpegli_finish_compress(&cinfo);
+ return true;
+ };
+ bool success = try_catch_block();
+ jpegli_destroy_compress(&cinfo);
+ if (success) {
+ buffer_size = buffer_size * spec.fraction / 100;
+ compressed->assign(buffer, buffer + buffer_size);
+ }
+ if (buffer) std::free(buffer);
+ return success;
+}
+
+bool GenerateFile(const char* output_dir, const ImageSpec& spec,
+ bool regenerate, bool quiet) {
+ // Compute a checksum of the ImageSpec to name the file. This is just to keep
+ // the output of this program repeatable.
+ uint8_t checksum[16];
+ spec.SpecHash(checksum);
+ std::string hash_str(sizeof(checksum) * 2, ' ');
+ static const char* hex_chars = "0123456789abcdef";
+ for (size_t i = 0; i < sizeof(checksum); i++) {
+ hash_str[2 * i] = hex_chars[checksum[i] >> 4];
+ hash_str[2 * i + 1] = hex_chars[checksum[i] % 0x0f];
+ }
+ std::string output_fn = std::string(output_dir) + "/" + hash_str + ".jpg";
+
+ // Don't regenerate files if they already exist on disk to speed-up
+ // consecutive calls when --regenerate is not used.
+ struct stat st;
+ if (!regenerate && stat(output_fn.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
+ return true;
+ }
+
+ if (!quiet) {
+ std::unique_lock<std::mutex> lock(stderr_mutex);
+ std::cerr << "Generating " << spec << " as " << hash_str << std::endl;
+ }
+
+ uint8_t hash[16];
+ spec.SpecHash(hash);
+ std::mt19937 mt(spec.seed);
+
+ std::vector<uint8_t> pixels =
+ GetSomeTestImage(spec.width, spec.height, spec.num_channels, spec.seed);
+ std::vector<uint8_t> compressed;
+ JXL_CHECK(EncodeWithJpegli(spec, pixels, &compressed));
+
+ // Append 4 bytes with the flags used by jpegli_dec_fuzzer to select the
+ // decoding output.
+ std::uniform_int_distribution<> dis256(0, 255);
+ for (size_t i = 0; i < 4; ++i) {
+ compressed.push_back(dis256(mt));
+ }
+
+ if (!jpegxl::tools::WriteFile(output_fn, compressed)) {
+ return false;
+ }
+ if (!quiet) {
+ std::unique_lock<std::mutex> lock(stderr_mutex);
+ std::cerr << "Stored " << output_fn << " size: " << compressed.size()
+ << std::endl;
+ }
+
+ return true;
+}
+
+void Usage() {
+ fprintf(stderr,
+ "Use: fuzzer_corpus [-r] [-q] [-j THREADS] [output_dir]\n"
+ "\n"
+ " -r Regenerate files if already exist.\n"
+ " -q Be quiet.\n"
+ " -j THREADS Number of parallel jobs to run.\n");
+}
+
+} // namespace
+
+int main(int argc, const char** argv) {
+ const char* dest_dir = nullptr;
+ bool regenerate = false;
+ bool quiet = false;
+ size_t num_threads = std::thread::hardware_concurrency();
+ for (int optind = 1; optind < argc;) {
+ if (!strcmp(argv[optind], "-r")) {
+ regenerate = true;
+ optind++;
+ } else if (!strcmp(argv[optind], "-q")) {
+ quiet = true;
+ optind++;
+ } else if (!strcmp(argv[optind], "-j")) {
+ optind++;
+ if (optind < argc) {
+ num_threads = atoi(argv[optind++]);
+ } else {
+ fprintf(stderr, "-j needs an argument value.\n");
+ Usage();
+ return 1;
+ }
+ } else if (dest_dir == nullptr) {
+ dest_dir = argv[optind++];
+ } else {
+ fprintf(stderr, "Unknown parameter: \"%s\".\n", argv[optind]);
+ Usage();
+ return 1;
+ }
+ }
+ if (!dest_dir) {
+ dest_dir = "corpus";
+ }
+
+ struct stat st;
+ memset(&st, 0, sizeof(st));
+ if (stat(dest_dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
+ fprintf(stderr, "Output path \"%s\" is not a directory.\n", dest_dir);
+ Usage();
+ return 1;
+ }
+
+ std::mt19937 mt(77777);
+
+ std::vector<std::pair<uint32_t, uint32_t>> image_sizes = {
+ {8, 8}, {32, 32}, {128, 128}, {10000, 1}, {10000, 2}, {1, 10000},
+ {2, 10000}, {555, 256}, {257, 513}, {512, 265}, {264, 520},
+ };
+ std::vector<uint32_t> sampling_ratios = {
+ 0x11111111, // 444
+ 0x11111112, // 422
+ 0x11111121, // 440
+ 0x11111122, // 420
+ 0x11222211, // luma subsampling
+ };
+
+ ImageSpec spec;
+ std::vector<ImageSpec> specs;
+ for (auto img_size : image_sizes) {
+ spec.width = img_size.first;
+ spec.height = img_size.second;
+ for (uint32_t num_channels : {1, 3}) {
+ spec.num_channels = num_channels;
+ for (uint32_t sampling : sampling_ratios) {
+ spec.sampling = sampling;
+ if (num_channels == 1 && sampling != 0x11111111) continue;
+ for (uint32_t restart : {0, 1, 1024}) {
+ spec.restart_interval = restart;
+ for (uint32_t prog_level : {0, 1, 2}) {
+ spec.progressive_level = prog_level;
+ for (uint32_t quality : {10, 90, 100}) {
+ spec.quality = quality;
+ for (uint32_t fraction : {10, 70, 100}) {
+ spec.fraction = fraction;
+ spec.seed = mt() % 777777;
+ if (!spec.Validate()) {
+ if (!quiet) {
+ std::cerr << "Skipping " << spec << std::endl;
+ }
+ } else {
+ specs.push_back(spec);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ jpegxl::tools::ThreadPoolInternal pool{num_threads};
+ const auto generate = [&specs, dest_dir, regenerate, quiet](
+ const uint32_t task, size_t /* thread */) {
+ const ImageSpec& spec = specs[task];
+ GenerateFile(dest_dir, spec, regenerate, quiet);
+ };
+ if (!RunOnPool(&pool, 0, specs.size(), jxl::ThreadPool::NoInit, generate,
+ "FuzzerCorpus")) {
+ std::cerr << "Error generating fuzzer corpus" << std::endl;
+ return 1;
+ }
+ std::cerr << "Finished generating fuzzer corpus" << std::endl;
+ return 0;
+}
diff --git a/tools/jxl_from_tree.cc b/tools/jxl_from_tree.cc
index aa85ff8..92a0874 100644
--- a/tools/jxl_from_tree.cc
+++ b/tools/jxl_from_tree.cc
@@ -3,17 +3,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/cms.h>
#include <stdio.h>
#include <string.h>
#include <fstream>
#include <iostream>
+#include <istream>
#include <unordered_map>
-#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/enc_cache.h"
-#include "lib/jxl/enc_color_management.h"
-#include "lib/jxl/enc_file.h"
+#include "lib/jxl/enc_fields.h"
#include "lib/jxl/enc_frame.h"
#include "lib/jxl/enc_heuristics.h"
#include "lib/jxl/modular/encoding/context_predict.h"
@@ -21,8 +22,33 @@
#include "lib/jxl/modular/encoding/enc_ma.h"
#include "lib/jxl/modular/encoding/encoding.h"
#include "lib/jxl/splines.h"
+#include "lib/jxl/test_utils.h" // TODO(eustas): cut this dependency
+#include "tools/file_io.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::BitWriter;
+using ::jxl::BlendMode;
+using ::jxl::CodecInOut;
+using ::jxl::CodecMetadata;
+using ::jxl::ColorCorrelationMap;
+using ::jxl::ColorEncoding;
+using ::jxl::ColorTransform;
+using ::jxl::CompressParams;
+using ::jxl::DefaultEncoderHeuristics;
+using ::jxl::FrameDimensions;
+using ::jxl::FrameInfo;
+using ::jxl::Image3F;
+using ::jxl::ImageF;
+using ::jxl::PaddedBytes;
+using ::jxl::PassesEncoderState;
+using ::jxl::Predictor;
+using ::jxl::PropertyDecisionNode;
+using ::jxl::QuantizedSpline;
+using ::jxl::Spline;
+using ::jxl::Splines;
+using ::jxl::Tree;
namespace {
struct SplineData {
@@ -196,7 +222,7 @@ bool ParseNode(F& tok, Tree& tree, SplineData& spline_data,
} else if (t == "Alpha") {
io.metadata.m.SetAlphaBits(io.metadata.m.bit_depth.bits_per_sample);
ImageF alpha(W, H);
- io.frames[0].SetAlpha(std::move(alpha), false);
+ io.frames[0].SetAlpha(std::move(alpha));
} else if (t == "Bitdepth") {
t = tok();
size_t num = 0;
@@ -392,7 +418,7 @@ bool ParseNode(F& tok, Tree& tree, SplineData& spline_data,
class Heuristics : public DefaultEncoderHeuristics {
public:
- bool CustomFixedTreeLossless(const jxl::FrameDimensions& frame_dim,
+ bool CustomFixedTreeLossless(const FrameDimensions& frame_dim,
Tree* tree) override {
*tree = tree_;
return true;
@@ -412,16 +438,24 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) {
size_t width = 1024, height = 1024;
int x0 = 0, y0 = 0;
cparams.SetLossless();
+ cparams.responsive = false;
cparams.resampling = 1;
cparams.ec_resampling = 1;
cparams.modular_group_size_shift = 3;
CodecInOut io;
int have_next = 0;
- std::ifstream f(in);
+ std::istream* f = &std::cin;
+ std::ifstream file;
+
+ if (strcmp(in, "-")) {
+ file.open(in, std::ifstream::in);
+ f = &file;
+ }
+
auto tok = [&f]() {
std::string out;
- f >> out;
+ *f >> out;
return out;
};
if (!ParseNode(tok, tree, spline_data, cparams, width, height, io, have_next,
@@ -436,7 +470,7 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) {
io.SetFromImage(std::move(image), ColorEncoding::SRGB());
io.SetSize((width + x0) * cparams.resampling,
(height + y0) * cparams.resampling);
- io.metadata.m.color_encoding.DecideIfWantICC();
+ io.metadata.m.color_encoding.DecideIfWantICC(*JxlGetDefaultCms());
cparams.options.zero_tokens = true;
cparams.palette_colors = 0;
cparams.channel_colors_pre_transform_percent = 0;
@@ -452,14 +486,14 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) {
*metadata = io.metadata;
JXL_RETURN_IF_ERROR(metadata->size.Set(io.xsize(), io.ysize()));
- metadata->m.xyb_encoded = cparams.color_transform == ColorTransform::kXYB;
+ metadata->m.xyb_encoded = (cparams.color_transform == ColorTransform::kXYB);
- JXL_RETURN_IF_ERROR(WriteHeaders(metadata.get(), &writer, nullptr));
+ JXL_RETURN_IF_ERROR(WriteCodestreamHeaders(metadata.get(), &writer, nullptr));
writer.ZeroPadToByte();
while (true) {
PassesEncoderState enc_state;
- enc_state.heuristics = make_unique<Heuristics>(tree);
+ enc_state.heuristics = jxl::make_unique<Heuristics>(tree);
enc_state.shared.image_features.splines =
SplinesFromSplineData(spline_data, enc_state.shared.cmap);
@@ -469,14 +503,16 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) {
io.frames[0].origin.x0 = x0;
io.frames[0].origin.y0 = y0;
+ info.clamp = false;
- JXL_RETURN_IF_ERROR(EncodeFrame(cparams, info, metadata.get(), io.frames[0],
- &enc_state, GetJxlCms(), nullptr, &writer,
- nullptr));
+ JXL_RETURN_IF_ERROR(jxl::EncodeFrame(
+ cparams, info, metadata.get(), io.frames[0], &enc_state,
+ *JxlGetDefaultCms(), nullptr, &writer, nullptr));
if (!have_next) break;
tree.clear();
spline_data.splines.clear();
have_next = 0;
+ cparams.manual_noise.clear();
if (!ParseNode(tok, tree, spline_data, cparams, width, height, io,
have_next, x0, y0)) {
return 1;
@@ -488,19 +524,22 @@ int JxlFromTree(const char* in, const char* out, const char* tree_out) {
compressed = std::move(writer).TakeBytes();
- if (!WriteFile(compressed, out)) {
+ if (!WriteFile(out, compressed)) {
fprintf(stderr, "Failed to write to \"%s\"\n", out);
return 1;
}
return 0;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
int main(int argc, char** argv) {
- if ((argc != 3 && argc != 4) || !strcmp(argv[1], argv[2])) {
+ if ((argc != 3 && argc != 4) ||
+ (strcmp(argv[1], "-") && !strcmp(argv[1], argv[2]))) {
fprintf(stderr, "Usage: %s tree_in.txt out.jxl [tree_drawing]\n", argv[0]);
return 1;
}
- return jxl::JxlFromTree(argv[1], argv[2], argc < 4 ? nullptr : argv[3]);
+ return jpegxl::tools::JxlFromTree(argv[1], argv[2],
+ argc < 4 ? nullptr : argv[3]);
}
diff --git a/tools/jxlinfo.c b/tools/jxlinfo.c
index d8d67e7..e7d23ee 100644
--- a/tools/jxlinfo.c
+++ b/tools/jxlinfo.c
@@ -6,13 +6,12 @@
// This example prints information from the main codestream header.
#include <inttypes.h>
+#include <jxl/decode.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include "jxl/decode.h"
-
int PrintBasicInfo(FILE* file, int verbose) {
uint8_t* data = NULL;
size_t data_size = 0;
@@ -90,7 +89,7 @@ int PrintBasicInfo(FILE* file, int verbose) {
if (info.exponent_bits_per_sample) {
printf("float (%d exponent bits) ", info.exponent_bits_per_sample);
}
- int cmyk = 0, alpha = 0;
+ int cmyk = 0;
const char* const ec_type_names[] = {
"Alpha", "Depth", "Spotcolor", "Selection", "Black",
"CFA", "Thermal", "Reserved0", "Reserved1", "Reserved2",
@@ -105,17 +104,12 @@ int PrintBasicInfo(FILE* file, int verbose) {
break;
}
if (extra.type == JXL_CHANNEL_BLACK) cmyk = 1;
- if (extra.type == JXL_CHANNEL_ALPHA) alpha = 1;
}
if (info.num_color_channels == 1)
printf("Grayscale");
else {
if (cmyk) {
- printf("CMYK");
- cmyk = 0;
- } else if (alpha) {
- printf("RGBA");
- alpha = 0;
+ printf("CMY");
} else {
printf("RGB");
}
@@ -126,15 +120,6 @@ int PrintBasicInfo(FILE* file, int verbose) {
fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n");
break;
}
- if (extra.type == JXL_CHANNEL_BLACK && cmyk == 0) {
- cmyk = 1;
- continue;
- }
- if (extra.type == JXL_CHANNEL_ALPHA && alpha == 0) {
- alpha = 1;
- continue;
- }
-
printf("+%s", (extra.type < ec_type_names_size
? ec_type_names[extra.type]
: "Unknown, please update your libjxl"));
@@ -229,14 +214,12 @@ int PrintBasicInfo(FILE* file, int verbose) {
fprintf(stderr, "Invalid orientation\n");
}
} else if (status == JXL_DEC_COLOR_ENCODING) {
- JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0};
printf("Color space: ");
JxlColorEncoding color_encoding;
if (JXL_DEC_SUCCESS ==
- JxlDecoderGetColorAsEncodedProfile(dec, &format,
- JXL_COLOR_PROFILE_TARGET_ORIGINAL,
- &color_encoding)) {
+ JxlDecoderGetColorAsEncodedProfile(
+ dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &color_encoding)) {
const char* const cs_string[4] = {"RGB", "Grayscale", "XYB", "Unknown"};
const char* const wp_string[12] = {"", "D65", "Custom", "", "", "",
"", "", "", "", "E", "P3"};
@@ -280,8 +263,7 @@ int PrintBasicInfo(FILE* file, int verbose) {
// instead.
size_t profile_size;
if (JXL_DEC_SUCCESS !=
- JxlDecoderGetICCProfileSize(dec, &format,
- JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ JxlDecoderGetICCProfileSize(dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL,
&profile_size)) {
fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
continue;
@@ -292,10 +274,9 @@ int PrintBasicInfo(FILE* file, int verbose) {
continue;
}
uint8_t* profile = (uint8_t*)malloc(profile_size);
- if (JXL_DEC_SUCCESS !=
- JxlDecoderGetColorAsICCProfile(dec, &format,
- JXL_COLOR_PROFILE_TARGET_ORIGINAL,
- profile, profile_size)) {
+ if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile(
+ dec, JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ profile, profile_size)) {
fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
free(profile);
continue;
@@ -326,11 +307,11 @@ int PrintBasicInfo(FILE* file, int verbose) {
} else {
printf("full image size");
}
-
- float ms = frame_header.duration * 1000.f *
- info.animation.tps_denominator / info.animation.tps_numerator;
- total_duration += ms;
if (info.have_animation) {
+ float ms = frame_header.duration * 1000.f *
+ info.animation.tps_denominator /
+ info.animation.tps_numerator;
+ total_duration += ms;
printf(", duration: %.1f ms", ms);
if (info.animation.have_timecodes) {
printf(", time code: %X", frame_header.timecode);
diff --git a/tools/libjxl_test.c b/tools/libjxl_test.c
index bb57c2d..f56a1fa 100644
--- a/tools/libjxl_test.c
+++ b/tools/libjxl_test.c
@@ -7,9 +7,9 @@
// This links against the shared libjpegxl library which doesn't expose any of
// the internals of the jxl namespace.
-#include "jxl/decode.h"
+#include <jxl/decode.h>
-int main() {
+int main(void) {
if (!JxlDecoderVersion()) return 1;
JxlDecoder* dec = JxlDecoderCreate(NULL);
if (!dec) return 1;
diff --git a/tools/optimizer/apply_simplex.py b/tools/optimizer/apply_simplex.py
new file mode 100755
index 0000000..273305b
--- /dev/null
+++ b/tools/optimizer/apply_simplex.py
@@ -0,0 +1,111 @@
+#!/usr/bin/python
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"""apply_simplex.py: Updates constants based on results of simplex search.
+
+To use this tool, the simplex search parameters must we wrapped in a bias(n)
+function call that returns the value of the VARn environment variable. The
+tool reads a text file containing the simplex definition that simplex_fork.py
+has written, and updates the target source files by substituting the bias(n)
+function calls with the (n+1)th coordinate of the simplex vector, and also
+simplifies these expressions by evaluating them to a sinlge floating point
+literal.
+
+The tool recognizes and evaluates the following expressions:
+ <constant> + bias(n),
+ <constant> * bias(n),
+ <constant> + <coeff> * bias(n).
+
+The --keep_bias command-line flag can be used to continue an aborted simplex
+search. This will keep the same bias(n) terms in the code, but would update the
+surronding constants.
+
+The --index_min and --index_max flags can be used to update only a subset of the
+bias(n) parameters.
+"""
+
+import argparse
+import re
+import sys
+
+def ParseSimplex(fn):
+ """Returns the simplex definition written by simplex_fork.py"""
+
+ with open(fn, "r") as f:
+ line = f.readline()
+ vec = eval(line)
+ return vec
+
+
+def PythonExpr(c_expr):
+ """Removes the f at the end of float literals"""
+
+ def repl(m):
+ return m.group(1)
+
+ return re.sub("(\d+)f", repl, c_expr)
+
+
+def UpdateSourceFile(fn, vec, keep_bias, id_min, id_max, minval):
+ """Updates expressions containing a bias(N) term."""
+
+ with open(fn, "r") as f:
+ lines_in = f.readlines()
+ lines_out = []
+ rbias = "(bias\((\d+)\))"
+ r = " -?\d+\.\d+f?( (\+|-|\*) (\d+\.\d+f? \* )?" + rbias + ")"
+ for line in lines_in:
+ line_out = line
+ x = re.search(r, line)
+ if x:
+ id = int(x.group(5))
+ if id >= id_min and id <= id_max:
+ expr = re.sub(rbias, str(vec[id + 1]), x.group(0))
+ val = eval(PythonExpr(expr))
+ if minval and val < minval:
+ val = minval
+ expr_out = " " + str(val) + "f"
+ if keep_bias:
+ expr_out += x.group(1)
+ line_out = re.sub(r, expr_out, line)
+ lines_out.append(line_out)
+
+ with open(fn, "w") as f:
+ f.writelines(lines_out)
+ f.close()
+
+
+def ApplySimplex(args):
+ """Main entry point of the program after parsing parameters."""
+
+ vec = ParseSimplex(args.simplex)
+ for fn in args.target:
+ UpdateSourceFile(fn, vec, args.keep_bias, args.index_min, args.index_max,
+ args.minval)
+ return 0
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('target', type=str, nargs='+',
+ help='source file(s) to update')
+ parser.add_argument('--simplex', default='best_simplex.txt',
+ help='simplex to apply to the code')
+ parser.add_argument('--keep_bias', default=False, action='store_true',
+ help='keep the bias term in the code, can be used to ' +
+ 'continue simplex search')
+ parser.add_argument('--index_min', type=int, default=0,
+ help='start index of the simplex to apply')
+ parser.add_argument('--index_max', type=int, default=9999,
+ help='last index of the simplex to apply')
+ parser.add_argument('--minval', type=float, default=None,
+ help='apply a minimum to expression results')
+ args = parser.parse_args()
+ sys.exit(ApplySimplex(args))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/optimizer/simplex_fork.py b/tools/optimizer/simplex_fork.py
index 20de4c9..f29e190 100755
--- a/tools/optimizer/simplex_fork.py
+++ b/tools/optimizer/simplex_fork.py
@@ -53,6 +53,7 @@ def Average(a, b):
eval_hash = {}
+g_best_val = None
def EvalCacheForget():
global eval_hash
@@ -60,19 +61,18 @@ def EvalCacheForget():
def RandomizedJxlCodecs():
retval = []
- minval = 0.5
- maxval = 3.3
+ minval = 0.2
+ maxval = 9.3
rangeval = maxval/minval
- steps = 7
+ steps = 13
for i in range(steps):
mul = minval * rangeval**(float(i)/(steps - 1))
mul *= 0.99 + 0.05 * random.random()
- retval.append("jxl:epf2:d%.3f" % mul)
- steps = 7
+ retval.append("jxl:d%.4f" % mul)
for i in range(steps - 1):
mul = minval * rangeval**(float(i+0.5)/(steps - 1))
mul *= 0.99 + 0.05 * random.random()
- retval.append("jxl:epf0:d%.3f" % mul)
+ retval.append("jxl:d%.4f" % mul)
return ",".join(retval)
g_codecs = RandomizedJxlCodecs()
@@ -87,6 +87,7 @@ def Eval(vec, binary_name, cached=True):
"""
global eval_hash
global g_codecs
+ global g_best_val
key = ""
# os.environ["BUTTERAUGLI_OPTIMIZE"] = "1"
for i in range(300):
@@ -101,8 +102,8 @@ def Eval(vec, binary_name, cached=True):
process = subprocess.Popen(
(binary_name,
'--input',
- '/usr/local/google/home/jyrki/mix_corpus/*.png',
- '--error_pnorm=3',
+ '/usr/local/google/home/jyrki/newcorpus/split/*.png',
+ '--error_pnorm=3.0',
'--more_columns',
'--codec', g_codecs),
stdout=subprocess.PIPE,
@@ -122,20 +123,26 @@ def Eval(vec, binary_name, cached=True):
sys.stdout.flush()
if line[0:3] == b'jxl':
bpp = line.split()[3]
- dist_pnorm = line.split()[7]
+ dist_pnorm = line.split()[9]
+ dist_max = line.split()[6]
vec[0] *= float(dist_pnorm) * float(bpp) / 16.0
- #vec[0] *= (float(dist_max) * float(bpp) / 16.0) ** 0.2
+ #vec[0] *= (float(dist_max) * float(bpp) / 16.0) ** 0.01
n += 1
found_score = True
distance = float(line.split()[0].split(b'd')[-1])
- #faultybpp = 1.0 + 0.43 * ((float(bpp) * distance ** 0.74) - 1.57) ** 2
- #vec[0] *= faultybpp
+ faultybpp = 1.0 + 0.43 * ((float(bpp) * distance ** 0.69) - 1.64) ** 2
+ vec[0] *= faultybpp
print("eval: ", vec)
if (vec[0] <= 0.0):
vec[0] = 1e30
if found_score:
eval_hash[key] = vec[0]
+ if not g_best_val or vec[0] < g_best_val:
+ g_best_val = vec[0]
+ print("\nSaving best simplex\n")
+ with open("best_simplex.txt", "w") as f:
+ print(vec, file=f)
return
vec[0] = 1e33
return
@@ -242,7 +249,7 @@ g_simplex = InitialSimplex(best, g_dim, g_amount * 0.33)
best = g_simplex[0][:]
for restarts in range(99999):
- for ii in range(g_dim * 2):
+ for ii in range(g_dim * 5):
g_simplex.sort()
print("reflect", ii, g_simplex[0])
Reflect(g_simplex, g_binary)
diff --git a/tools/optimizer/update_jpegli_global_scale.py b/tools/optimizer/update_jpegli_global_scale.py
new file mode 100755
index 0000000..1a57c59
--- /dev/null
+++ b/tools/optimizer/update_jpegli_global_scale.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"""Script to update jpegli global scale after a change affecting quality.
+
+start as ./update_jpegli_global_scale.py build <corpus-dir>
+"""
+
+import os
+import re
+import subprocess
+import sys
+
+def SourceFileName():
+ return "lib/jpegli/quant.cc"
+
+def ScalePattern(scale_type):
+ return "constexpr float kGlobalScale" + scale_type + " = ";
+
+def CodecName(scale_type):
+ if scale_type == "YCbCr":
+ return "jpeg:enc-jpegli:q90"
+ elif scale_type == "XYB":
+ return "jpeg:enc-jpegli:xyb:q90"
+ else:
+ raise Exception("Unknown scale type %s" % scale_type)
+
+def ReadGlobalScale(scale_type):
+ pattern = ScalePattern(scale_type)
+ with open(SourceFileName()) as f:
+ for line in f.read().splitlines():
+ if line.startswith(pattern):
+ return float(line[len(pattern):-2])
+ raise Exception("Global scale %s not found." % scale_type)
+
+
+def UpdateGlobalScale(scale_type, new_val):
+ pattern = ScalePattern(scale_type)
+ found_pattern = False
+ fdata = ""
+ with open(SourceFileName()) as f:
+ for line in f.read().splitlines():
+ if line.startswith(pattern):
+ fdata += pattern + "%.8ff;\n" % new_val
+ found_pattern = True
+ else:
+ fdata += line + "\n"
+ if not found_pattern:
+ raise Exception("Global scale %s not found." % scale_type)
+ with open(SourceFileName(), "w") as f:
+ f.write(fdata)
+ f.close()
+
+def EvalPnorm(build_dir, corpus_dir, codec):
+ compile_args = ["ninja", "-C", build_dir, "tools/benchmark_xl"]
+ try:
+ subprocess.check_output(compile_args)
+ except:
+ subprocess.check_call(compile_args)
+ process = subprocess.Popen(
+ (os.path.join(build_dir, "tools/benchmark_xl"),
+ "--input", os.path.join(corpus_dir, "*.png"),
+ "--codec", codec),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ (out, err) = process.communicate(input=None)
+ for line in out.splitlines():
+ if line.startswith(codec):
+ return float(line.split()[8])
+ raise Exception("Unexpected benchmark output:\n%sstderr:\n%s" % (out, err))
+
+
+if len(sys.argv) != 3:
+ print("usage: ", sys.argv[0], "build-dir corpus-dir")
+ exit(1)
+
+build_dir = sys.argv[1]
+corpus_dir = sys.argv[2]
+
+jpeg_pnorm = EvalPnorm(build_dir, corpus_dir, "jpeg:q90")
+
+print("Libjpeg pnorm: %.8f" % jpeg_pnorm)
+
+for scale_type in ["YCbCr", "XYB"]:
+ scale = ReadGlobalScale(scale_type)
+ best_scale = scale
+ best_rel_error = 100.0
+ for i in range(10):
+ jpegli_pnorm = EvalPnorm(build_dir, corpus_dir, CodecName(scale_type))
+ rel_error = abs(jpegli_pnorm / jpeg_pnorm - 1)
+ print("[%-5s] scale: %.8f pnorm: %.8f error: %.8f" %
+ (scale_type, scale, jpegli_pnorm, rel_error))
+ if rel_error < best_rel_error:
+ best_rel_error = rel_error
+ best_scale = scale
+ if rel_error < 0.0001:
+ break
+ scale = scale * jpeg_pnorm / jpegli_pnorm
+ UpdateGlobalScale(scale_type, scale)
+ UpdateGlobalScale(scale_type, best_scale)
diff --git a/tools/rans_fuzzer.cc b/tools/rans_fuzzer.cc
index 7c78f0d..16fa99a 100644
--- a/tools/rans_fuzzer.cc
+++ b/tools/rans_fuzzer.cc
@@ -3,10 +3,21 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
#include "lib/jxl/dec_ans.h"
+#include "lib/jxl/dec_bit_reader.h"
#include "lib/jxl/entropy_coder.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::ANSCode;
+using ::jxl::ANSSymbolReader;
+using ::jxl::BitReader;
+using ::jxl::BitReaderScopedCloser;
+using ::jxl::Bytes;
+using ::jxl::Status;
int TestOneInput(const uint8_t* data, size_t size) {
if (size < 2) return 0;
@@ -17,7 +28,7 @@ int TestOneInput(const uint8_t* data, size_t size) {
std::vector<uint8_t> context_map;
Status ret = true;
{
- BitReader br(Span<const uint8_t>(data, size));
+ BitReader br(Bytes(data, size));
BitReaderScopedCloser br_closer(&br, &ret);
ANSCode code;
JXL_RETURN_IF_ERROR(
@@ -28,7 +39,7 @@ int TestOneInput(const uint8_t* data, size_t size) {
const size_t maxreads = size * 8;
size_t numreads = 0;
int context = 0;
- while (DivCeil(br.TotalBitsConsumed(), kBitsPerByte) < size &&
+ while (jxl::DivCeil(br.TotalBitsConsumed(), jxl::kBitsPerByte) < size &&
numreads <= maxreads) {
int code = ansreader.ReadHybridUint(context, &br, context_map);
context = code % numContexts;
@@ -39,8 +50,9 @@ int TestOneInput(const uint8_t* data, size_t size) {
return 0;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- return jxl::TestOneInput(data, size);
+ return jpegxl::tools::TestOneInput(data, size);
}
diff --git a/tools/bisector b/tools/scripts/bisector
index 2552045..b6a82d0 100755
--- a/tools/bisector
+++ b/tools/scripts/bisector
@@ -1,4 +1,10 @@
#!/usr/bin/env python
+#
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
r"""General-purpose bisector
Prints a space-separated list of values to stdout:
diff --git a/tools/scripts/build_cleaner.py b/tools/scripts/build_cleaner.py
new file mode 100755
index 0000000..0185fc5
--- /dev/null
+++ b/tools/scripts/build_cleaner.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+
+"""build_cleaner.py: Update build files.
+
+This tool keeps certain parts of the build files up to date.
+"""
+
+import argparse
+import locale
+import os
+import re
+import subprocess
+import sys
+import tempfile
+
+
+HEAD = """# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# This file is generated, do not modify by manually.
+# Run `tools/scripts/build_cleaner.py --update` to regenerate it.
+"""
+
+
+def RepoFiles(src_dir):
+ """Return the list of files from the source git repository"""
+ git_bin = os.environ.get('GIT_BIN', 'git')
+ files = subprocess.check_output([git_bin, '-C', src_dir, 'ls-files'])
+ ret = files.decode(locale.getpreferredencoding()).splitlines()
+ ret.sort()
+ return ret
+
+
+def Check(condition, msg):
+ if not condition:
+ print(msg)
+ sys.exit(2)
+
+
+def ContainsFn(*parts):
+ return lambda path: any(part in path for part in parts)
+
+
+def HasPrefixFn(*prefixes):
+ return lambda path: any(path.startswith(prefix) for prefix in prefixes)
+
+
+def HasSuffixFn(*suffixes):
+ return lambda path: any(path.endswith(suffix) for suffix in suffixes)
+
+
+def Filter(src, fn):
+ yes_list = []
+ no_list = []
+ for item in src:
+ (yes_list if fn(item) else no_list).append(item)
+ return yes_list, no_list
+
+
+def SplitLibFiles(repo_files):
+ """Splits the library files into the different groups."""
+
+ srcs_base = 'lib/'
+ srcs, _ = Filter(repo_files, HasPrefixFn(srcs_base))
+ srcs = [path[len(srcs_base):] for path in srcs]
+ srcs, _ = Filter(srcs, HasSuffixFn('.cc', '.h', '.ui'))
+ srcs.sort()
+
+ # Let's keep Jpegli sources a bit separate for a while.
+ jpegli_srcs, srcs = Filter(srcs, HasPrefixFn('jpegli'))
+ # TODO(eustas): move to tools?
+ _, srcs = Filter(srcs, HasSuffixFn('gbench_main.cc'))
+
+ # First pick files scattered across directories.
+ tests, srcs = Filter(srcs, HasSuffixFn('_test.cc'))
+ jpegli_tests, jpegli_srcs = Filter(jpegli_srcs, HasSuffixFn('_test.cc'))
+ # TODO(eustas): move to separate list?
+ _, srcs = Filter(srcs, ContainsFn('testing.h'))
+ _, jpegli_srcs = Filter(jpegli_srcs, ContainsFn('testing.h'))
+ testlib_files, srcs = Filter(srcs, ContainsFn('test'))
+ jpegli_testlib_files, jpegli_srcs = Filter(jpegli_srcs, ContainsFn('test'))
+ jpegli_libjpeg_helper_files, jpegli_testlib_files = Filter(
+ jpegli_testlib_files, ContainsFn('libjpeg_test_util'))
+ gbench_sources, srcs = Filter(srcs, HasSuffixFn('_gbench.cc'))
+
+ extras_sources, srcs = Filter(srcs, HasPrefixFn('extras/'))
+ lib_srcs, srcs = Filter(srcs, HasPrefixFn('jxl/'))
+ public_headers, srcs = Filter(srcs, HasPrefixFn('include/jxl/'))
+ threads_sources, srcs = Filter(srcs, HasPrefixFn('threads/'))
+
+ Check(len(srcs) == 0, 'Orphan source files: ' + str(srcs))
+
+ base_sources, lib_srcs = Filter(lib_srcs, HasPrefixFn('jxl/base/'))
+
+ jpegli_wrapper_sources, jpegli_srcs = Filter(
+ jpegli_srcs, HasSuffixFn('libjpeg_wrapper.cc'))
+ jpegli_sources = jpegli_srcs
+
+ threads_public_headers, public_headers = Filter(
+ public_headers, ContainsFn('_parallel_runner'))
+
+ codec_names = ['apng', 'exr', 'gif', 'jpegli', 'jpg', 'jxl', 'npy', 'pgx',
+ 'pnm']
+ codecs = {}
+ for codec in codec_names:
+ codec_sources, extras_sources = Filter(extras_sources, HasPrefixFn(
+ f'extras/dec/{codec}', f'extras/enc/{codec}'))
+ codecs[f'codec_{codec}_sources'] = codec_sources
+
+ # TODO(eustas): move to separate folder?
+ extras_for_tools_sources, extras_sources = Filter(extras_sources, ContainsFn(
+ '/codec', '/hlg', '/metrics', '/packed_image_convert', '/render_hdr',
+ '/tone_mapping'))
+
+ # Source files only needed by the encoder or by tools (including decoding
+ # tools), but not by the decoder library.
+ # TODO(eustas): investigate the status of codec_in_out.h
+ # TODO(eustas): rename butteraugli_wrapper.cc to butteraugli.cc?
+ # TODO(eustas): is it possible to make butteraugli more standalone?
+ enc_sources, lib_srcs = Filter(lib_srcs, ContainsFn('/enc_', '/butteraugli',
+ 'jxl/encode.cc', 'jxl/encode_internal.h'
+ ))
+
+ # The remaining of the files are in the dec_library.
+ dec_jpeg_sources, dec_sources = Filter(lib_srcs, HasPrefixFn('jxl/jpeg/',
+ 'jxl/decode_to_jpeg.cc', 'jxl/decode_to_jpeg.h'))
+ dec_box_sources, dec_sources = Filter(dec_sources, HasPrefixFn(
+ 'jxl/box_content_decoder.cc', 'jxl/box_content_decoder.h'))
+ cms_sources, dec_sources = Filter(dec_sources, HasPrefixFn('jxl/cms/'))
+
+ # TODO(lode): further prune dec_srcs: only those files that the decoder
+ # absolutely needs, and or not only for encoding, should be listed here.
+
+ return codecs | {'base_sources': base_sources,
+ 'cms_sources': cms_sources,
+ 'dec_box_sources': dec_box_sources,
+ 'dec_jpeg_sources': dec_jpeg_sources,
+ 'dec_sources': dec_sources,
+ 'enc_sources': enc_sources,
+ 'extras_for_tools_sources': extras_for_tools_sources,
+ 'extras_sources': extras_sources,
+ 'gbench_sources': gbench_sources,
+ 'jpegli_sources': jpegli_sources,
+ 'jpegli_testlib_files': jpegli_testlib_files,
+ 'jpegli_libjpeg_helper_files': jpegli_libjpeg_helper_files,
+ 'jpegli_tests': jpegli_tests,
+ 'jpegli_wrapper_sources' : jpegli_wrapper_sources,
+ 'public_headers': public_headers,
+ 'testlib_files': testlib_files,
+ 'tests': tests,
+ 'threads_public_headers': threads_public_headers,
+ 'threads_sources': threads_sources,
+ }
+
+
+def MaybeUpdateFile(args, filename, new_text):
+ """Optionally replace file with new contents.
+
+ If args.update is set, it will update the file with the new contents,
+ otherwise it will return True when no changes were needed.
+ """
+ filepath = os.path.join(args.src_dir, filename)
+ with open(filepath, 'r') as f:
+ src_text = f.read()
+
+ if new_text == src_text:
+ return True
+
+ if args.update:
+ print('Updating %s' % filename)
+ with open(filepath, 'w') as f:
+ f.write(new_text)
+ return True
+ else:
+ prefix = os.path.basename(filename)
+ with tempfile.NamedTemporaryFile(mode='w', prefix=prefix) as new_file:
+ new_file.write(new_text)
+ new_file.flush()
+ subprocess.call(['diff', '-u', filepath, '--label', 'a/' + filename,
+ new_file.name, '--label', 'b/' + filename])
+ return False
+
+
+def FormatList(items, prefix, suffix):
+ return ''.join(f'{prefix}{item}{suffix}\n' for item in items)
+
+
+def FormatGniVar(name, var):
+ if type(var) is list:
+ contents = FormatList(var, ' "', '",')
+ return f'{name} = [\n{contents}]\n'
+ else: # TODO(eustas): do we need scalar strings?
+ return f'{name} = {var}\n'
+
+
+def FormatCMakeVar(name, var):
+ if type(var) is list:
+ contents = FormatList(var, ' ', '')
+ return f'set({name}\n{contents})\n'
+ else: # TODO(eustas): do we need scalar strings?
+ return f'set({name} {var})\n'
+
+
+def GetJpegLibVersion(src_dir):
+ with open(os.path.join(src_dir, 'CMakeLists.txt'), 'r') as f:
+ cmake_text = f.read()
+ m = re.search(r'set\(JPEGLI_LIBJPEG_LIBRARY_SOVERSION "([0-9]+)"',
+ cmake_text)
+ version = m.group(1)
+ if len(version) == 1:
+ version += "0"
+ return version
+
+
+def BuildCleaner(args):
+ repo_files = RepoFiles(args.src_dir)
+
+ with open(os.path.join(args.src_dir, 'lib/CMakeLists.txt'), 'r') as f:
+ cmake_text = f.read()
+ version = {'major_version': '', 'minor_version': '', 'patch_version': ''}
+ for var in version.keys():
+ cmake_var = f'JPEGXL_{var.upper()}'
+ # TODO(eustas): use `cmake -L`
+ # Regexp:
+ # set(_varname_ _capture_decimal_)
+ match = re.search(r'set\(' + cmake_var + r' ([0-9]+)\)', cmake_text)
+ version[var] = match.group(1)
+
+ version['jpegli_lib_version'] = GetJpegLibVersion(args.src_dir)
+
+ lists = SplitLibFiles(repo_files)
+
+ cmake_chunks = [HEAD]
+ cmake_parts = lists
+ for var in sorted(cmake_parts):
+ cmake_chunks.append(FormatCMakeVar(
+ 'JPEGXL_INTERNAL_' + var.upper(), cmake_parts[var]))
+
+ gni_chunks = [HEAD]
+ gni_parts = version | lists
+ for var in sorted(gni_parts):
+ gni_chunks.append(FormatGniVar('libjxl_' + var, gni_parts[var]))
+
+ okay = [
+ MaybeUpdateFile(args, 'lib/jxl_lists.cmake', '\n'.join(cmake_chunks)),
+ MaybeUpdateFile(args, 'lib/jxl_lists.bzl', '\n'.join(gni_chunks)),
+ ]
+ return all(okay)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--src-dir',
+ default=os.path.realpath(os.path.join( os.path.dirname(__file__), '../..')),
+ help='path to the build directory')
+ parser.add_argument('--update', default=False, action='store_true',
+ help='update the build files instead of only checking')
+ args = parser.parse_args()
+ Check(BuildCleaner(args), 'Build files need update.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/build_stats.py b/tools/scripts/build_stats.py
index b1dc1ea..63265e2 100755
--- a/tools/build_stats.py
+++ b/tools/scripts/build_stats.py
@@ -20,6 +20,7 @@ import collections
import itertools
import json
import os
+import platform
import re
import struct
import subprocess
@@ -29,6 +30,7 @@ import tempfile
# Ignore functions with stack size smaller than this value.
MIN_STACK_SIZE = 32
+IS_OSX = (platform.system() == 'Darwin')
Symbol = collections.namedtuple('Symbol', ['address', 'size', 'typ', 'name'])
@@ -55,7 +57,10 @@ RAM_SIZE = 'dbs'
# u - symbols imported from some other library
# a - absolute address symbols
-IGNORE_SYMBOLS = 'ua'
+# c - common symbol
+# i - indirect symbol
+# - - debugger symbol table entries
+IGNORE_SYMBOLS = 'uaci-'
SIMD_NAMESPACES = [
'N_SCALAR', 'N_WASM', 'N_NEON', 'N_PPC8', 'N_SSE4', 'N_AVX2', 'N_AVX3']
@@ -65,17 +70,30 @@ def LoadSymbols(filename):
ret = []
nmout = subprocess.check_output(['nm', '--format=posix', filename])
for line in nmout.decode('utf-8').splitlines():
- if line.rstrip().endswith(':'):
+ line = line.rstrip()
+ if len(line) == 0:
+ # OSX nm produces extra crlf at the end
+ continue
+ if line.endswith(':'):
# Ignore object names.
continue
+ line = re.sub(' +', ' ', line)
# symbol_name, symbol_type, (optional) address, (optional) size
symlist = line.rstrip().split(' ')
- assert 2 <= len(symlist) <= 4
+ col_count = len(symlist)
+ assert 2 <= col_count <= 4
ret.append(Symbol(
- int(symlist[2], 16) if len(symlist) > 2 else None,
- int(symlist[3], 16) if len(symlist) > 3 else None,
+ int(symlist[2], 16) if col_count > 2 else None,
+ int(symlist[3], 16) if col_count > 3 else None,
symlist[1],
symlist[0]))
+ if IS_OSX:
+ ret = sorted(ret, key=lambda sym: sym.address)
+ for i in range(len(ret) - 1):
+ size = ret[i + 1].address - ret[i].address
+ if size > (1 << 30):
+ continue
+ ret[i] = ret[i]._replace(size=size)
return ret
def LoadTargetCommand(target, build_dir):
@@ -145,8 +163,9 @@ def LoadStackSizes(filename, binutils=''):
section, which can be done by compiling with -fstack-size-section in clang.
"""
with tempfile.NamedTemporaryFile() as stack_sizes_sec:
+ objcopy = ['objcopy', 'gobjcopy'][IS_OSX]
subprocess.check_call(
- [binutils + 'objcopy', '-O', 'binary', '--only-section=.stack_sizes',
+ [binutils + objcopy, '-O', 'binary', '--only-section=.stack_sizes',
'--set-section-flags', '.stack_sizes=alloc', filename,
stack_sizes_sec.name])
stack_sizes = stack_sizes_sec.read()
@@ -157,10 +176,11 @@ def LoadStackSizes(filename, binutils=''):
# dynamic stack allocations are not included.
# Get the pointer format based on the ELF file.
+ objdump = ['objdump', 'gobjdump'][IS_OSX]
output = subprocess.check_output(
- [binutils + 'objdump', '-a', filename]).decode('utf-8')
+ [binutils + objdump, '-a', filename]).decode('utf-8')
elf_format = re.search('file format (.*)$', output, re.MULTILINE).group(1)
- if elf_format.startswith('elf64-little') or elf_format == 'elf64-x86-64':
+ if elf_format.startswith('elf64-little') or elf_format.endswith('-x86-64') or elf_format.endswith('-arm64'):
pointer_fmt = '<Q'
elif elf_format.startswith('elf32-little') or elf_format == 'elf32-i386':
pointer_fmt = '<I'
@@ -234,7 +254,7 @@ def PrintStats(stats):
print('%-32s %17s %17s' % ('Object name', 'Binary size', 'Static RAM size'))
for name, bin_size, ram_size in table:
print('%-32s %8d (%5.1f%%) %8d (%5.1f%%)' % (
- name, bin_size, 100. * bin_size / mx_bin_size,
+ name, bin_size, (100. * bin_size / mx_bin_size) if mx_bin_size else 0,
ram_size, (100. * ram_size / mx_ram_size) if mx_ram_size else 0))
print()
diff --git a/tools/check_author.py b/tools/scripts/check_author.py
index ae1c279..7e16859 100755
--- a/tools/check_author.py
+++ b/tools/scripts/check_author.py
@@ -73,6 +73,8 @@ def CheckAuthor(args):
print("User %s <%s> not found, please add yourself to the AUTHORS file" % (
args.name, args.email),
file=sys.stderr)
+ print("Hint: to override author in PR run:\n"
+ " git commit --amend --author=\"Your Name <ldap@corp.com>\" --no-edit")
sorted_alphabetically = IndividualsInAlphabeticOrder(authors_path)
if not sorted_alphabetically:
@@ -90,7 +92,7 @@ def main():
help='name of the commit author to check')
parser.add_argument(
'--source-dir',
- default=os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
+ default=os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))),
help='path to the source directory where the AUTHORS file is located')
parser.add_argument('--dry-run', default=False, action='store_true',
help='Don\'t return an exit code in case of failure')
diff --git a/tools/cjxl_bisect_bpp b/tools/scripts/cjxl_bisect_bpp
index d7a1066..13a908c 100755
--- a/tools/cjxl_bisect_bpp
+++ b/tools/scripts/cjxl_bisect_bpp
@@ -1,5 +1,10 @@
#!/bin/sh
#
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+#
# Bisects JPEG XL encoding quality parameter to reach a given
# target bits-per-pixel value.
# (To be used directly, or as a template for tailored processing.)
diff --git a/tools/cjxl_bisect_size b/tools/scripts/cjxl_bisect_size
index 9cd88ea..c0945d9 100755
--- a/tools/cjxl_bisect_size
+++ b/tools/scripts/cjxl_bisect_size
@@ -1,5 +1,10 @@
#!/bin/sh
#
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+#
# Bisects JPEG XL encoding quality parameter to reach a given
# target byte-size.
# (To be used directly, or as a template for tailored processing.)
diff --git a/tools/demo_progressive_saliency_encoding.py b/tools/scripts/demo_progressive_saliency_encoding.py
index 6eb5cad..6eb5cad 100755
--- a/tools/demo_progressive_saliency_encoding.py
+++ b/tools/scripts/demo_progressive_saliency_encoding.py
diff --git a/tools/scripts/jpegli_tools_test.sh b/tools/scripts/jpegli_tools_test.sh
new file mode 100644
index 0000000..96df3b0
--- /dev/null
+++ b/tools/scripts/jpegli_tools_test.sh
@@ -0,0 +1,287 @@
+#!/bin/bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# End-to-end roundtrip tests for cjpegli and djpegli tools, and other linux
+# tools linked with the jpegli library.
+
+set -eux
+
+MYDIR=$(dirname $(realpath "$0"))
+JPEGXL_TEST_DATA_PATH="${MYDIR}/../../testdata"
+
+# Temporary files cleanup hooks.
+CLEANUP_FILES=()
+cleanup() {
+ if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then
+ rm -rf "${CLEANUP_FILES[@]}"
+ fi
+}
+trap 'retcode=$?; { set +x; } 2>/dev/null; cleanup' INT TERM EXIT
+
+verify_ssimulacra2() {
+ local score="$("${ssimulacra2}" "${1}" "${2}")"
+ python3 -c "import sys; sys.exit(not ${score} >= ${3})"
+}
+
+verify_max_bpp() {
+ local infn="$1"
+ local jpgfn="$2"
+ local maxbpp="$3"
+ local size="$(wc -c "${jpgfn}" | cut -d' ' -f1)"
+ local pixels=$(( "$(identify "${infn}" | cut -d' ' -f3 | tr 'x' '*')" ))
+ python3 -c "import sys; sys.exit(not ${size} * 8 <= ${maxbpp} * ${pixels})"
+}
+
+# Test that jpeg files created with cjpegli can be decoded with normal djpeg.
+cjpegli_test() {
+ local infn="${JPEGXL_TEST_DATA_PATH}/$1"
+ local encargs="$2"
+ local minscore="$3"
+ local maxbpp="$4"
+ local jpgfn="$(mktemp -p "${tmpdir}")"
+ local outfn="$(mktemp -p "${tmpdir}").ppm"
+
+ "${cjpegli}" "${infn}" "${jpgfn}" $encargs
+ djpeg -outfile "${outfn}" "${jpgfn}"
+
+ verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}"
+ verify_max_bpp "${infn}" "${jpgfn}" "${maxbpp}"
+}
+
+# Test full cjpegli/djpegli roundtrip.
+cjpegli_djpegli_test() {
+ local infn="${JPEGXL_TEST_DATA_PATH}/$1"
+ local encargs="$2"
+ local minscore="$3"
+ local maxbpp="$4"
+ local jpgfn="$(mktemp -p "${tmpdir}")"
+ local outfn="$(mktemp -p "${tmpdir}").png"
+
+ "${cjpegli}" "${infn}" "${jpgfn}" $encargs
+ "${djpegli}" "${jpgfn}" "${outfn}"
+
+ verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}"
+ verify_max_bpp "${infn}" "${jpgfn}" "${maxbpp}"
+}
+
+# Test the --target_size command line argument of cjpegli.
+cjpegli_test_target_size() {
+ local infn="${JPEGXL_TEST_DATA_PATH}/$1"
+ local encargs="$2"
+ local target_size="$3"
+ local jpgfn="$(mktemp -p "$tmpdir")"
+
+ "${cjpegli}" "${infn}" "${jpgfn}" $encargs --target_size "${target_size}"
+ local size="$(wc -c "${jpgfn}" | cut -d' ' -f1)"
+ python3 -c "import sys; sys.exit(not ${target_size} * 0.996 <= ${size})"
+ python3 -c "import sys; sys.exit(not ${target_size} * 1.004 >= ${size})"
+}
+
+# Test that jpeg files created with cjpeg binary + jpegli library can be decoded
+# with normal libjpeg.
+cjpeg_test() {
+ local infn="${JPEGXL_TEST_DATA_PATH}/$1"
+ local encargs="$2"
+ local minscore="$3"
+ local maxbpp="$4"
+ local jpgfn="$(mktemp -p "$tmpdir")"
+ local outfn="$(mktemp -p "${tmpdir}").png"
+
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ cjpeg $encargs -outfile "${jpgfn}" "${infn}"
+ djpeg -outfile "${outfn}" "${jpgfn}"
+
+ verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}"
+ verify_max_bpp "${infn}" "${jpgfn}" "${maxbpp}"
+}
+
+# Test decoding of jpeg files with the djpegli binary.
+djpegli_test() {
+ local infn="${JPEGXL_TEST_DATA_PATH}/$1"
+ local encargs="$2"
+ local minscore="$3"
+ local jpgfn="$(mktemp -p "$tmpdir")"
+
+ cjpeg $encargs -outfile "${jpgfn}" "${infn}"
+
+ # Test that disabling output works.
+ "${djpegli}" "${jpgfn}" --disable_output
+ for ext in png pgm ppm pfm pnm baz; do
+ "${djpegli}" "${jpgfn}" /foo/bar.$ext --disable_output
+ done
+
+ # Test decoding to PNG, PPM, PNM, PFM
+ for ext in png ppm pnm pfm; do
+ local outfn="$(mktemp -p "${tmpdir}").${ext}"
+ "${djpegli}" "${jpgfn}" "${outfn}" --num_reps 2
+ verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}"
+ done
+
+ # Test decoding to PGM (for grayscale input)
+ if [[ "${infn: -6}" == ".g.png" ]]; then
+ local outfn="$(mktemp -p "${tmpdir}").pgm"
+ "${djpegli}" "${jpgfn}" "${outfn}" --quiet
+ verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}"
+ fi
+
+ # Test decoding to 16 bit
+ for ext in png pnm; do
+ local outfn8="$(mktemp -p "${tmpdir}").8.${ext}"
+ local outfn16="$(mktemp -p "${tmpdir}").16.${ext}"
+ "${djpegli}" "${jpgfn}" "${outfn8}"
+ "${djpegli}" "${jpgfn}" "${outfn16}" --bitdepth 16
+ local score8="$("${ssimulacra2}" "${infn}" "${outfn8}")"
+ local score16="$("${ssimulacra2}" "${infn}" "${outfn16}")"
+ python3 -c "import sys; sys.exit(not ${score16} > ${score8})"
+ done
+}
+
+# Test decoding of jpeg files with the djpeg binary + jpegli library.
+djpeg_test() {
+ local infn="${JPEGXL_TEST_DATA_PATH}/$1"
+ local encargs="$2"
+ local minscore="$3"
+ local jpgfn="$(mktemp -p "$tmpdir")"
+
+ cjpeg $encargs -outfile "${jpgfn}" "${infn}"
+
+ # Test default behaviour.
+ local outfn="$(mktemp -p "${tmpdir}").pnm"
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ djpeg -outfile "${outfn}" "${jpgfn}"
+ verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}"
+
+ # Test color quantization.
+ local outfn="$(mktemp -p "${tmpdir}").pnm"
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ djpeg -outfile "${outfn}" -colors 128 "${jpgfn}"
+ verify_ssimulacra2 "${infn}" "${outfn}" 48
+
+ local outfn="$(mktemp -p "${tmpdir}").pnm"
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ djpeg -outfile "${outfn}" -colors 128 -onepass -dither fs "${jpgfn}"
+ verify_ssimulacra2 "${infn}" "${outfn}" 30
+
+ local outfn="$(mktemp -p "${tmpdir}").pnm"
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ djpeg -outfile "${outfn}" -colors 128 -onepass -dither ordered "${jpgfn}"
+ verify_ssimulacra2 "${infn}" "${outfn}" 30
+
+ # Test -grayscale flag.
+ local outfn="$(mktemp -p "${tmpdir}").pgm"
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ djpeg -outfile "${outfn}" -grayscale "${jpgfn}"
+ local outfn2="$(mktemp -p "${tmpdir}").pgm"
+ convert "${infn}" -set colorspace Gray "${outfn2}"
+ # JPEG color conversion is in gamma-compressed space, so it will not match
+ # the correct grayscale version very well.
+ verify_ssimulacra2 "${outfn2}" "${outfn}" 60
+
+ # Test -rgb flag.
+ local outfn="$(mktemp -p "${tmpdir}").ppm"
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ djpeg -outfile "${outfn}" -rgb "${jpgfn}"
+ verify_ssimulacra2 "${infn}" "${outfn}" "${minscore}"
+
+ # Test -crop flag.
+ for geometry in 256x256+128+128 256x127+128+117; do
+ local outfn="$(mktemp -p "${tmpdir}").pnm"
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ djpeg -outfile "${outfn}" -crop "${geometry}" "${jpgfn}"
+ local outfn2="$(mktemp -p "${tmpdir}").pnm"
+ convert "${infn}" -crop "${geometry}" "${outfn2}"
+ verify_ssimulacra2 "${outfn2}" "${outfn}" "${minscore}"
+ done
+
+ # Test output scaling.
+ for scale in 1/4 3/8 1/2 5/8 9/8; do
+ local scalepct="$(python3 -c "print(100.0*${scale})")%"
+ local geometry=96x128+0+0
+ local outfn="$(mktemp -p "${tmpdir}").pnm"
+ LD_LIBRARY_PATH="${build_dir}/lib/jpegli:${LD_LIBRARY_PATH:-}" \
+ djpeg -outfile "${outfn}" -scale "${scale}" -crop "${geometry}" "${jpgfn}"
+ local outfn2="$(mktemp -p "${tmpdir}").pnm"
+ convert "${infn}" -scale "${scalepct}" -crop "${geometry}" "${outfn2}"
+ verify_ssimulacra2 "${outfn2}" "${outfn}" 80
+ done
+}
+
+main() {
+ local tmpdir=$(mktemp -d)
+ CLEANUP_FILES+=("${tmpdir}")
+
+ local build_dir="${1:-}"
+ if [[ -z "${build_dir}" ]]; then
+ build_dir=$(realpath "${MYDIR}/../../build")
+ fi
+
+ local cjpegli="${build_dir}/tools/cjpegli"
+ local djpegli="${build_dir}/tools/djpegli"
+ local ssimulacra2="${build_dir}/tools/ssimulacra2"
+ local rgb_in="jxl/flower/flower_small.rgb.png"
+ local gray_in="jxl/flower/flower_small.g.png"
+ local ppm_rgb="jxl/flower/flower_small.rgb.depth8.ppm"
+ local ppm_gray="jxl/flower/flower_small.g.depth8.pgm"
+
+ cjpegli_test "${rgb_in}" "" 88.5 1.7
+ cjpegli_test "${rgb_in}" "-q 80" 84 1.2
+ cjpegli_test "${rgb_in}" "-q 95" 91.5 2.4
+ cjpegli_test "${rgb_in}" "-d 0.5" 92 2.6
+ cjpegli_test "${rgb_in}" "--chroma_subsampling 420" 87 1.5
+ cjpegli_test "${rgb_in}" "--chroma_subsampling 440" 87 1.6
+ cjpegli_test "${rgb_in}" "--chroma_subsampling 422" 87 1.6
+ cjpegli_test "${rgb_in}" "--std_quant" 91 2.2
+ cjpegli_test "${rgb_in}" "--noadaptive_quantization" 88.5 1.85
+ cjpegli_test "${rgb_in}" "-p 1" 88.5 1.72
+ cjpegli_test "${rgb_in}" "-p 0" 88.5 1.75
+ cjpegli_test "${rgb_in}" "-p 0 --fixed_code" 88.5 1.8
+ cjpegli_test "${gray_in}" "" 92 1.4
+
+ cjpegli_test_target_size "${rgb_in}" "" 10000
+ cjpegli_test_target_size "${rgb_in}" "" 50000
+ cjpegli_test_target_size "${rgb_in}" "" 100000
+ cjpegli_test_target_size "${rgb_in}" "--chroma_subsampling 420" 20000
+ cjpegli_test_target_size "${rgb_in}" "--xyb" 20000
+ cjpegli_test_target_size "${rgb_in}" "-p 0 --fixed_code" 20000
+
+ cjpegli_test "jxl/flower/flower_small.rgb.depth8.ppm" "" 88.5 1.7
+ cjpegli_test "jxl/flower/flower_small.rgb.depth16.ppm" "" 89 1.7
+ cjpegli_test "jxl/flower/flower_small.g.depth8.pgm" "" 89 1.7
+ cjpegli_test "jxl/flower/flower_small.g.depth16.pgm" "" 89 1.7
+
+ cjpegli_djpegli_test "${rgb_in}" "" 89 1.7
+ cjpegli_djpegli_test "${rgb_in}" "--xyb" 87 1.5
+
+ djpegli_test "${ppm_rgb}" "-q 95" 92
+ djpegli_test "${ppm_rgb}" "-q 95 -sample 1x1" 93
+ djpegli_test "${ppm_gray}" "-q 95 -gray" 94
+
+ cjpeg_test "${ppm_rgb}" "" 89 1.9
+ cjpeg_test "${ppm_rgb}" "-optimize" 89 1.85
+ cjpeg_test "${ppm_rgb}" "-optimize -progressive" 89 1.8
+ cjpeg_test "${ppm_rgb}" "-sample 2x2" 87 1.65
+ cjpeg_test "${ppm_rgb}" "-sample 1x2" 88 1.75
+ cjpeg_test "${ppm_rgb}" "-sample 2x1" 88 1.75
+ cjpeg_test "${ppm_rgb}" "-grayscale" -50 1.45
+ cjpeg_test "${ppm_rgb}" "-rgb" 92 4.5
+ cjpeg_test "${ppm_rgb}" "-restart 1" 89 1.9
+ cjpeg_test "${ppm_rgb}" "-restart 1024B" 89 1.9
+ cjpeg_test "${ppm_rgb}" "-smooth 30" 88 1.75
+ cjpeg_test "${ppm_gray}" "-grayscale" 92 1.45
+ # The -q option works differently on v62 vs. v8 cjpeg binaries, so we have to
+ # have looser bounds than would be necessary if we sticked to a particular
+ # cjpeg version.
+ cjpeg_test "${ppm_rgb}" "-q 50" 76 0.95
+ cjpeg_test "${ppm_rgb}" "-q 80" 84 1.6
+ cjpeg_test "${ppm_rgb}" "-q 90" 89 2.35
+ cjpeg_test "${ppm_rgb}" "-q 100" 95 7.45
+
+ djpeg_test "${ppm_rgb}" "-q 95" 92
+ djpeg_test "${ppm_rgb}" "-q 95 -sample 1x1" 93
+ djpeg_test "${ppm_gray}" "-q 95 -gray" 94
+}
+
+main "$@"
diff --git a/tools/scripts/jxl-eval.sh b/tools/scripts/jxl-eval.sh
new file mode 100755
index 0000000..138aac8
--- /dev/null
+++ b/tools/scripts/jxl-eval.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -eu
+
+GSROOT="${GSROOT:-gs://jxl-quality}"
+URLROOT="${URLROOT:-https://storage.googleapis.com/jxl-quality}"
+BUILD_DIR="${BUILD_DIR:-./build}"
+BUILD_MODE="${BUILD_MODE:-opt}"
+DESC="${DESC:-exp}"
+
+build_libjxl() {
+ export BUILD_DIR="${BUILD_DIR}"
+ export SKIP_TEST=1
+ ./ci.sh "${BUILD_MODE}"
+}
+
+build_mozjpeg() {
+ if [[ ! -d "${HOME}/mozjpeg" ]]; then
+ (cd "${HOME}"
+ git clone https://github.com/mozilla/mozjpeg.git
+ )
+ fi
+ (cd "${HOME}/mozjpeg"
+ mkdir -p build
+ cmake -GNinja -B build
+ ninja -C build
+ )
+}
+
+download_corpus() {
+ local corpus="$1"
+ local localdir="${HOME}/corpora/${corpus}"
+ local remotedir="${GSROOT}/corpora/${corpus}"
+ if [[ ! -d "${localdir}" ]]; then
+ mkdir -p "${localdir}"
+ fi
+ gsutil -m rsync "${remotedir}" "${localdir}"
+}
+
+create_report() {
+ local corpus="$1"
+ local codec="$2"
+ shift 2
+ local rev="$(git rev-parse --short HEAD)"
+ local originals="${URLROOT}/corpora/${corpus}"
+ if git diff HEAD --quiet; then
+ local expid="${corpus}/${rev}/base"
+ else
+ local expid="${corpus}/${rev}/${DESC}"
+ fi
+ local output_dir="benchmark_results/${expid}"
+ local bucket="eval/${USER}/${expid}"
+ local indexhtml="index.$(echo ${codec} | tr ':' '_').html"
+ local url="${URLROOT}/${bucket}/${indexhtml}"
+ local use_decompressed="--save_decompressed --html_report_use_decompressed"
+ if [[ "${codec:0:4}" == "jpeg" ]]; then
+ use_decompressed="--nohtml_report_use_decompressed"
+ fi
+ (
+ cd "${BUILD_DIR}"
+ tools/benchmark_xl \
+ --output_dir "${output_dir}" \
+ --input "${HOME}/corpora/${corpus}/*.??g" \
+ --codec="${codec}" \
+ --save_compressed \
+ --write_html_report \
+ "${use_decompressed}" \
+ --originals_url="${originals}" \
+ $@
+ gsutil -m rsync "${output_dir}" "${GSROOT}/${bucket}"
+ echo "You can view evaluation results at:"
+ echo "${url}"
+ )
+}
+
+cmd_upload_corpus() {
+ local corpus="$1"
+ gsutil -m rsync "${HOME}/corpora/${corpus}" "${GSROOT}/corpora/${corpus}"
+}
+
+cmd_report() {
+ local corpus="$1"
+ local codec="$2"
+ if [[ "${codec}" == *","* ]]; then
+ echo "Multiple codecs are not allowed in html report"
+ exit 1
+ fi
+ download_corpus "${corpus}"
+ if [[ "${codec:0:4}" == "jpeg" ]]; then
+ build_mozjpeg
+ export LD_LIBRARY_PATH="${HOME}/mozjpeg/build:${LD_LIBRARY_PATH:-}"
+ fi
+ build_libjxl
+ create_report "$@"
+}
+
+main() {
+ local cmd="${1:-}"
+ if [[ -z "${cmd}" ]]; then
+ cat >&2 <<EOF
+Use: $0 CMD
+
+Where CMD is one of:
+ upload_corpus CORPUS
+ Upload the image corpus in $HOME/corpora/CORPUS to the cloud
+ report CORPUS CODEC
+ Build and run benchmark of codec CODEC on image corpus CORPUS and upload
+ the results to the cloud. If the codec is jpeg, the mozjpeg library will be
+ built and used through LD_LIBRARY_PATH
+EOF
+ echo "Usage $0 CMD"
+ exit 1
+ fi
+ cmd="cmd_${cmd}"
+ shift
+ set -x
+ "${cmd}" "$@"
+}
+
+main "$@"
diff --git a/tools/ossfuzz-build.sh b/tools/scripts/ossfuzz-build.sh
index b5fbb45..7ab45b6 100755
--- a/tools/ossfuzz-build.sh
+++ b/tools/scripts/ossfuzz-build.sh
@@ -20,6 +20,7 @@ main() {
build_args=(
-G Ninja
-DBUILD_TESTING=OFF
+ -DBUILD_SHARED_LIBS=OFF
-DJPEGXL_ENABLE_BENCHMARK=OFF
-DJPEGXL_ENABLE_DEVTOOLS=ON
-DJPEGXL_ENABLE_EXAMPLES=OFF
diff --git a/tools/progressive_saliency.conf b/tools/scripts/progressive_saliency.conf
index 987651a..987651a 100644
--- a/tools/progressive_saliency.conf
+++ b/tools/scripts/progressive_saliency.conf
diff --git a/tools/progressive_sizes.sh b/tools/scripts/progressive_sizes.sh
index a1e808d..08d3079 100755
--- a/tools/progressive_sizes.sh
+++ b/tools/scripts/progressive_sizes.sh
@@ -16,8 +16,8 @@ cleanup() {
trap cleanup EXIT
-CJXL=$(realpath $(dirname "$0"))/../build/tools/cjxl
-DJXL=$(realpath $(dirname "$0"))/../build/tools/djxl
+CJXL=$(realpath $(dirname "$0"))/../../build/tools/cjxl
+DJXL=$(realpath $(dirname "$0"))/../../build/tools/djxl
${CJXL} "$@" ${TMPDIR}/x.jxl &>/dev/null
S1=$(${DJXL} ${TMPDIR}/x.jxl --print_read_bytes -s 1 2>&1 | grep 'Decoded' | grep -o '[0-9]*')
diff --git a/tools/reference_zip.sh b/tools/scripts/reference_zip.sh
index 6a284b4..6a284b4 100755
--- a/tools/reference_zip.sh
+++ b/tools/scripts/reference_zip.sh
diff --git a/tools/roundtrip_test.sh b/tools/scripts/roundtrip_test.sh
index 46b7756..b3bb300 100644
--- a/tools/roundtrip_test.sh
+++ b/tools/scripts/roundtrip_test.sh
@@ -7,12 +7,10 @@
# End-to-end roundtrip tests for cjxl and djxl tools.
MYDIR=$(dirname $(realpath "$0"))
-JPEGXL_TEST_DATA_PATH="${MYDIR}/../testdata"
+JPEGXL_TEST_DATA_PATH="${MYDIR}/../../testdata"
set -eux
-EMULATOR=${EMULATOR:-}
-
# Temporary files cleanup hooks.
CLEANUP_FILES=()
cleanup() {
@@ -22,14 +20,20 @@ cleanup() {
}
trap 'retcode=$?; { set +x; } 2>/dev/null; cleanup' INT TERM EXIT
+roundtrip_lossless_pnm_test() {
+ local infn="${JPEGXL_TEST_DATA_PATH}/$1"
+ local jxlfn="$(mktemp -p "$tmpdir")"
+ local outfn="$(mktemp -p "$tmpdir").${infn: -3}"
+
+ "${encoder}" "${infn}" "${jxlfn}" -d 0 -e 1
+ "${decoder}" "${jxlfn}" "${outfn}"
+ diff "${infn}" "${outfn}"
+}
+
roundtrip_test() {
local infn="${JPEGXL_TEST_DATA_PATH}/$1"
local encargs="$2"
local maxdist="$3"
-
- local encoder="${EMULATOR} ${build_dir}/tools/cjxl"
- local decoder="${EMULATOR} ${build_dir}/tools/djxl"
- local comparator="${EMULATOR} ${build_dir}/tools/ssimulacra_main"
local jxlfn="$(mktemp -p "$tmpdir")"
"${encoder}" "${infn}" "${jxlfn}" $encargs
@@ -66,6 +70,28 @@ roundtrip_test() {
local dist="$("${comparator}" "${infn}" "${outfn}")"
python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist})"
+ # Test decoding to 16 bit png.
+ "${decoder}" "${jxlfn}" "${outfn}" --bits_per_sample 16
+ local dist="$("${comparator}" "${infn}" "${outfn}")"
+ python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist} + 0.0005)"
+
+ # Test decoding to pfm.
+ local outfn="$(mktemp -p "$tmpdir").pfm"
+ "${decoder}" "${jxlfn}" "${outfn}"
+ local dist="$("${comparator}" "${infn}" "${outfn}")"
+ python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist})"
+
+ # Test decoding to ppm.
+ local outfn="$(mktemp -p "$tmpdir").ppm"
+ "${decoder}" "${jxlfn}" "${outfn}"
+ local dist="$("${comparator}" "${infn}" "${outfn}")"
+ python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist})"
+
+ # Test decoding to 16 bit ppm.
+ "${decoder}" "${jxlfn}" "${outfn}" --bits_per_sample 16
+ local dist="$("${comparator}" "${infn}" "${outfn}")"
+ python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist} + 0.0005)"
+
# Test decoding to jpg.
outfn="$(mktemp -p "$tmpdir").jpg"
"${decoder}" "${jxlfn}" "${outfn}" --num_reps 2
@@ -83,9 +109,30 @@ main() {
build_dir=$(realpath "${MYDIR}/../../build")
fi
- roundtrip_test "jxl/flower/flower.png" "-e 1" 0.02
- roundtrip_test "jxl/flower/flower.png" "-e 1 -d 0.0" 0.0
+ local encoder="${build_dir}/tools/cjxl"
+ local decoder="${build_dir}/tools/djxl"
+ local comparator="${build_dir}/tools/ssimulacra_main"
+
+ roundtrip_test "jxl/flower/flower_small.rgb.png" "-e 1" 0.02
+ roundtrip_test "jxl/flower/flower_small.rgb.png" "-e 1 -d 0.0" 0.0
+ roundtrip_test "jxl/flower/flower_small.rgb.depth8.ppm" \
+ "-e 1 --streaming_input" 0.02
+ roundtrip_test "jxl/flower/flower_small.rgb.depth8.ppm" \
+ "-e 1 -d 0.0 --streaming_input" 0.0
+ roundtrip_test "jxl/flower/flower_small.rgb.depth8.ppm" \
+ "-e 1 --streaming_output" 0.02
+ roundtrip_test "jxl/flower/flower_small.rgb.depth8.ppm" \
+ "-e 1 -d 0.0 --streaming_input --streaming_output" 0.0
roundtrip_test "jxl/flower/flower_cropped.jpg" "-e 1" 0.0
+
+ roundtrip_lossless_pnm_test "jxl/flower/flower_small.rgb.depth1.ppm"
+ roundtrip_lossless_pnm_test "jxl/flower/flower_small.g.depth1.pgm"
+ for i in `seq 2 16`; do
+ roundtrip_lossless_pnm_test "jxl/flower/flower_small.rgb.depth$i.ppm"
+ roundtrip_lossless_pnm_test "jxl/flower/flower_small.g.depth$i.pgm"
+ roundtrip_lossless_pnm_test "jxl/flower/flower_small.ga.depth$i.pam"
+ roundtrip_lossless_pnm_test "jxl/flower/flower_small.rgba.depth$i.pam"
+ done
}
main "$@"
diff --git a/tools/scripts/test_cost-arm64-lowprecision.zip b/tools/scripts/test_cost-arm64-lowprecision.zip
new file mode 100644
index 0000000..92045e2
--- /dev/null
+++ b/tools/scripts/test_cost-arm64-lowprecision.zip
Binary files differ
diff --git a/tools/scripts/test_cost-arm64.zip b/tools/scripts/test_cost-arm64.zip
new file mode 100644
index 0000000..0d196ed
--- /dev/null
+++ b/tools/scripts/test_cost-arm64.zip
Binary files differ
diff --git a/tools/scripts/test_cost-armhf.zip b/tools/scripts/test_cost-armhf.zip
new file mode 100644
index 0000000..988c96d
--- /dev/null
+++ b/tools/scripts/test_cost-armhf.zip
Binary files differ
diff --git a/tools/scripts/test_cost-i386.zip b/tools/scripts/test_cost-i386.zip
new file mode 100644
index 0000000..718789f
--- /dev/null
+++ b/tools/scripts/test_cost-i386.zip
Binary files differ
diff --git a/tools/scripts/transform_sources_list.py b/tools/scripts/transform_sources_list.py
new file mode 100644
index 0000000..1194fe0
--- /dev/null
+++ b/tools/scripts/transform_sources_list.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+import sys
+
+def find_key(entries : list[str], key: str) -> int:
+ prefix = f"{key.lower()}: "
+ for i in range(len(entries)):
+ if entries[i].lower().startswith(prefix):
+ return i
+ return -1
+
+def set_value(entries: list[str], key: str, value: str):
+ new_line = f'{key}: {value}'
+ # TODO(eustas): deal with repeated items
+ idx = find_key(entries, key)
+ if idx < 0:
+ entries.append(new_line)
+ else:
+ entries[idx] = new_line
+
+def transform_deb_822(archs):
+ sources_path = "/etc/apt/sources.list.d/debian.sources"
+ with open(sources_path) as f:
+ lines = [line.rstrip() for line in f]
+ lines.append('')
+ entries = []
+ entry = []
+ for line in lines:
+ if len(line) == 0:
+ if len(entry) > 0:
+ entries.append(entry)
+ entry = []
+ else:
+ entry.append(line)
+
+ new_entries = []
+ for entry in entries:
+ types_key = find_key(entry, "Types")
+ if types_key < 0:
+ continue
+ if "types: deb" != entry[types_key].lower():
+ continue
+ deb_entry = entry[:]
+ for arch in archs:
+ deb_entry.append(f"Architectures-Add: {arch}")
+ new_entries.append(deb_entry)
+ deb_src_entry = deb_entry[:]
+ set_value(deb_src_entry, "Types", "deb-src")
+ new_entries.append(deb_src_entry)
+
+ new_lines = []
+ for entry in new_entries:
+ if len(new_lines) > 0:
+ new_lines.append("")
+ new_lines.extend(entry)
+
+ with open(sources_path, "w") as f:
+ f.write('\n'.join(new_lines))
+
+def main():
+ if len(sys.argv) != 2:
+ print(f"Usage: {sys.argv[1]} ARCHS")
+ sys.exit(1)
+ archs_str = sys.argv[1]
+ archs = archs_str.split(',')
+ if True:
+ transform_deb_822(archs)
+ else:
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/set_from_bytes_fuzzer.cc b/tools/set_from_bytes_fuzzer.cc
index 5eb9f75..0b95072 100644
--- a/tools/set_from_bytes_fuzzer.cc
+++ b/tools/set_from_bytes_fuzzer.cc
@@ -7,27 +7,29 @@
#include <stdint.h>
#include "lib/extras/codec.h"
+#include "lib/extras/size_constraints.h"
#include "lib/jxl/base/data_parallel.h"
#include "lib/jxl/base/span.h"
-#include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/codec_in_out.h"
+#include "tools/thread_pool_internal.h"
-namespace jxl {
+namespace {
int TestOneInput(const uint8_t* data, size_t size) {
- CodecInOut io;
- io.constraints.dec_max_xsize = 1u << 16;
- io.constraints.dec_max_ysize = 1u << 16;
- io.constraints.dec_max_pixels = 1u << 22;
- ThreadPoolInternal pool(0);
+ jxl::CodecInOut io;
+ jxl::SizeConstraints constraints;
+ constraints.dec_max_xsize = 1u << 16;
+ constraints.dec_max_ysize = 1u << 16;
+ constraints.dec_max_pixels = 1u << 22;
+ jpegxl::tools::ThreadPoolInternal pool(0);
- (void)SetFromBytes(Span<const uint8_t>(data, size), &io, &pool);
+ (void)jxl::SetFromBytes(jxl::Bytes(data, size), &io, &pool, &constraints);
return 0;
}
-} // namespace jxl
+} // namespace
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- return jxl::TestOneInput(data, size);
+ return TestOneInput(data, size);
}
diff --git a/tools/speed_stats.cc b/tools/speed_stats.cc
index cdef814..d378d09 100644
--- a/tools/speed_stats.cc
+++ b/tools/speed_stats.cc
@@ -5,6 +5,10 @@
#include "tools/speed_stats.h"
+#ifndef __STDC_FORMAT_MACROS
+#define __STDC_FORMAT_MACROS
+#endif
+
#include <inttypes.h>
#include <math.h>
#include <stddef.h>
@@ -54,21 +58,19 @@ bool SpeedStats::GetSummary(SpeedStats::Summary* s) {
s->central_tendency = pow(product, 1.0 / (elapsed_.size() - 1));
s->variability = 0.0;
s->type = " geomean:";
- return true;
+ if (isnormal(s->central_tendency)) return true;
}
// Else: median
std::sort(elapsed_.begin(), elapsed_.end());
s->central_tendency = elapsed_.data()[elapsed_.size() / 2];
- std::vector<double> deviations(elapsed_.size());
+ double stdev = 0;
for (size_t i = 0; i < elapsed_.size(); i++) {
- deviations[i] = fabs(elapsed_[i] - s->central_tendency);
+ double diff = elapsed_[i] - s->central_tendency;
+ stdev += diff * diff;
}
- std::nth_element(deviations.begin(),
- deviations.begin() + deviations.size() / 2,
- deviations.end());
- s->variability = deviations[deviations.size() / 2];
- s->type = "median: ";
+ s->variability = sqrt(stdev);
+ s->type = " median:";
return true;
}
@@ -84,8 +86,14 @@ std::string SummaryStat(double value, const char* unit,
const double value_min = value / s.max;
const double value_max = value / s.min;
- snprintf(stat_str, sizeof(stat_str), ",%s %.2f %s/s [%.2f, %.2f]", s.type,
- value_tendency, unit, value_min, value_max);
+ char variability[20] = {'\0'};
+ if (s.variability != 0.0) {
+ const double stdev = value / s.variability;
+ snprintf(variability, sizeof(variability), " (stdev %.3f)", stdev);
+ }
+
+ snprintf(stat_str, sizeof(stat_str), ",%s %.3f %s/s [%.2f, %.2f]%s", s.type,
+ value_tendency, unit, value_min, value_max, variability);
return stat_str;
}
@@ -99,16 +107,11 @@ bool SpeedStats::Print(size_t worker_threads) {
std::string mps_stats = SummaryStat(xsize_ * ysize_ * 1e-6, "MP", s);
std::string mbs_stats = SummaryStat(file_size_ * 1e-6, "MB", s);
- char variability[20] = {'\0'};
- if (s.variability != 0.0) {
- snprintf(variability, sizeof(variability), " (var %.2f)", s.variability);
- }
-
fprintf(stderr,
- "%" PRIu64 " x %" PRIu64 "%s%s%s, %" PRIu64 " reps, %" PRIu64
+ "%" PRIu64 " x %" PRIu64 "%s%s, %" PRIu64 " reps, %" PRIu64
" threads.\n",
static_cast<uint64_t>(xsize_), static_cast<uint64_t>(ysize_),
- mps_stats.c_str(), mbs_stats.c_str(), variability,
+ mps_stats.c_str(), mbs_stats.c_str(),
static_cast<uint64_t>(elapsed_.size()),
static_cast<uint64_t>(worker_threads));
return true;
diff --git a/tools/ssimulacra2.cc b/tools/ssimulacra2.cc
new file mode 100644
index 0000000..5ddaab3
--- /dev/null
+++ b/tools/ssimulacra2.cc
@@ -0,0 +1,492 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+SSIMULACRA 2
+Structural SIMilarity Unveiling Local And Compression Related Artifacts
+
+Perceptual metric developed by Jon Sneyers (Cloudinary) in July 2022,
+updated in April 2023.
+Design:
+- XYB color space (rescaled to a 0..1 range and with B-Y)
+- SSIM map (with correction: no double gamma correction)
+- 'blockiness/ringing' map (distorted has edges where original is smooth)
+- 'smoothing' map (distorted is smooth where original has edges)
+- error maps are computed at 6 scales (1:1 to 1:32) for each component (X,Y,B)
+- downscaling is done in linear RGB
+- for all 6*3*3=54 maps, two norms are computed: 1-norm (mean) and 4-norm
+- a weighted sum of these 54*2=108 norms leads to the final score
+- weights were tuned based on a large set of subjective scores
+ (CID22, TID2013, Kadid10k, KonFiG-IQA).
+*/
+
+#include "tools/ssimulacra2.h"
+
+#include <jxl/cms.h>
+#include <stdio.h>
+
+#include <cmath>
+
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/enc_xyb.h"
+#include "lib/jxl/gauss_blur.h"
+#include "lib/jxl/image_ops.h"
+
+namespace {
+
+using jxl::Image3F;
+using jxl::ImageF;
+
+static const float kC2 = 0.0009f;
+static const int kNumScales = 6;
+
+Image3F Downsample(const Image3F& in, size_t fx, size_t fy) {
+ const size_t out_xsize = (in.xsize() + fx - 1) / fx;
+ const size_t out_ysize = (in.ysize() + fy - 1) / fy;
+ Image3F out(out_xsize, out_ysize);
+ const float normalize = 1.0f / (fx * fy);
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t oy = 0; oy < out_ysize; ++oy) {
+ float* JXL_RESTRICT row_out = out.PlaneRow(c, oy);
+ for (size_t ox = 0; ox < out_xsize; ++ox) {
+ float sum = 0.0f;
+ for (size_t iy = 0; iy < fy; ++iy) {
+ for (size_t ix = 0; ix < fx; ++ix) {
+ const size_t x = std::min(ox * fx + ix, in.xsize() - 1);
+ const size_t y = std::min(oy * fy + iy, in.ysize() - 1);
+ sum += in.PlaneRow(c, y)[x];
+ }
+ }
+ row_out[ox] = sum * normalize;
+ }
+ }
+ }
+ return out;
+}
+
+void Multiply(const Image3F& a, const Image3F& b, Image3F* mul) {
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < a.ysize(); ++y) {
+ const float* JXL_RESTRICT in1 = a.PlaneRow(c, y);
+ const float* JXL_RESTRICT in2 = b.PlaneRow(c, y);
+ float* JXL_RESTRICT out = mul->PlaneRow(c, y);
+ for (size_t x = 0; x < a.xsize(); ++x) {
+ out[x] = in1[x] * in2[x];
+ }
+ }
+ }
+}
+
+// Temporary storage for Gaussian blur, reused for multiple images.
+class Blur {
+ public:
+ Blur(const size_t xsize, const size_t ysize)
+ : rg_(jxl::CreateRecursiveGaussian(1.5)), temp_(xsize, ysize) {}
+
+ void operator()(const ImageF& in, ImageF* JXL_RESTRICT out) {
+ jxl::ThreadPool* null_pool = nullptr;
+ FastGaussian(rg_, in, null_pool, &temp_, out);
+ }
+
+ Image3F operator()(const Image3F& in) {
+ Image3F out(in.xsize(), in.ysize());
+ operator()(in.Plane(0), &out.Plane(0));
+ operator()(in.Plane(1), &out.Plane(1));
+ operator()(in.Plane(2), &out.Plane(2));
+ return out;
+ }
+
+ // Allows reusing across scales.
+ void ShrinkTo(const size_t xsize, const size_t ysize) {
+ temp_.ShrinkTo(xsize, ysize);
+ }
+
+ private:
+ hwy::AlignedUniquePtr<jxl::RecursiveGaussian> rg_;
+ ImageF temp_;
+};
+
+double tothe4th(double x) {
+ x *= x;
+ x *= x;
+ return x;
+}
+void SSIMMap(const Image3F& m1, const Image3F& m2, const Image3F& s11,
+ const Image3F& s22, const Image3F& s12, double* plane_averages) {
+ const double onePerPixels = 1.0 / (m1.ysize() * m1.xsize());
+ for (size_t c = 0; c < 3; ++c) {
+ double sum1[2] = {0.0};
+ for (size_t y = 0; y < m1.ysize(); ++y) {
+ const float* JXL_RESTRICT row_m1 = m1.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_m2 = m2.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s11 = s11.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s22 = s22.PlaneRow(c, y);
+ const float* JXL_RESTRICT row_s12 = s12.PlaneRow(c, y);
+ for (size_t x = 0; x < m1.xsize(); ++x) {
+ float mu1 = row_m1[x];
+ float mu2 = row_m2[x];
+ float mu11 = mu1 * mu1;
+ float mu22 = mu2 * mu2;
+ float mu12 = mu1 * mu2;
+ /* Correction applied compared to the original SSIM formula, which has:
+
+ luma_err = 2 * mu1 * mu2 / (mu1^2 + mu2^2)
+ = 1 - (mu1 - mu2)^2 / (mu1^2 + mu2^2)
+
+ The denominator causes error in the darks (low mu1 and mu2) to weigh
+ more than error in the brights (high mu1 and mu2). This would make
+ sense if values correspond to linear luma. However, the actual values
+ are either gamma-compressed luma (which supposedly is already
+ perceptually uniform) or chroma (where weighing green more than red
+ or blue more than yellow does not make any sense at all). So it is
+ better to simply drop this denominator.
+ */
+ float num_m = 1.0 - (mu1 - mu2) * (mu1 - mu2);
+ float num_s = 2 * (row_s12[x] - mu12) + kC2;
+ float denom_s = (row_s11[x] - mu11) + (row_s22[x] - mu22) + kC2;
+
+ // Use 1 - SSIM' so it becomes an error score instead of a quality
+ // index. This makes it make sense to compute an L_4 norm.
+ double d = 1.0 - (num_m * num_s / denom_s);
+ d = std::max(d, 0.0);
+ sum1[0] += d;
+ sum1[1] += tothe4th(d);
+ }
+ }
+ plane_averages[c * 2] = onePerPixels * sum1[0];
+ plane_averages[c * 2 + 1] = sqrt(sqrt(onePerPixels * sum1[1]));
+ }
+}
+
+void EdgeDiffMap(const Image3F& img1, const Image3F& mu1, const Image3F& img2,
+ const Image3F& mu2, double* plane_averages) {
+ const double onePerPixels = 1.0 / (img1.ysize() * img1.xsize());
+ for (size_t c = 0; c < 3; ++c) {
+ double sum1[4] = {0.0};
+ for (size_t y = 0; y < img1.ysize(); ++y) {
+ const float* JXL_RESTRICT row1 = img1.PlaneRow(c, y);
+ const float* JXL_RESTRICT row2 = img2.PlaneRow(c, y);
+ const float* JXL_RESTRICT rowm1 = mu1.PlaneRow(c, y);
+ const float* JXL_RESTRICT rowm2 = mu2.PlaneRow(c, y);
+ for (size_t x = 0; x < img1.xsize(); ++x) {
+ double d1 = (1.0 + std::abs(row2[x] - rowm2[x])) /
+ (1.0 + std::abs(row1[x] - rowm1[x])) -
+ 1.0;
+
+ // d1 > 0: distorted has an edge where original is smooth
+ // (indicating ringing, color banding, blockiness, etc)
+ double artifact = std::max(d1, 0.0);
+ sum1[0] += artifact;
+ sum1[1] += tothe4th(artifact);
+
+ // d1 < 0: original has an edge where distorted is smooth
+ // (indicating smoothing, blurring, smearing, etc)
+ double detail_lost = std::max(-d1, 0.0);
+ sum1[2] += detail_lost;
+ sum1[3] += tothe4th(detail_lost);
+ }
+ }
+ plane_averages[c * 4] = onePerPixels * sum1[0];
+ plane_averages[c * 4 + 1] = sqrt(sqrt(onePerPixels * sum1[1]));
+ plane_averages[c * 4 + 2] = onePerPixels * sum1[2];
+ plane_averages[c * 4 + 3] = sqrt(sqrt(onePerPixels * sum1[3]));
+ }
+}
+
+/* Get all components in more or less 0..1 range
+ Range of Rec2020 with these adjustments:
+ X: 0.017223..0.998838
+ Y: 0.010000..0.855303
+ B: 0.048759..0.989551
+ Range of sRGB:
+ X: 0.204594..0.813402
+ Y: 0.010000..0.855308
+ B: 0.272295..0.938012
+ The maximum pixel-wise difference has to be <= 1 for the ssim formula to make
+ sense.
+*/
+void MakePositiveXYB(jxl::Image3F& img) {
+ for (size_t y = 0; y < img.ysize(); ++y) {
+ float* JXL_RESTRICT rowY = img.PlaneRow(1, y);
+ float* JXL_RESTRICT rowB = img.PlaneRow(2, y);
+ float* JXL_RESTRICT rowX = img.PlaneRow(0, y);
+ for (size_t x = 0; x < img.xsize(); ++x) {
+ rowB[x] = (rowB[x] - rowY[x]) + 0.55f;
+ rowX[x] = rowX[x] * 14.f + 0.42f;
+ rowY[x] += 0.01f;
+ }
+ }
+}
+
+void AlphaBlend(jxl::ImageBundle& img, float bg) {
+ for (size_t y = 0; y < img.ysize(); ++y) {
+ float* JXL_RESTRICT r = img.color()->PlaneRow(0, y);
+ float* JXL_RESTRICT g = img.color()->PlaneRow(1, y);
+ float* JXL_RESTRICT b = img.color()->PlaneRow(2, y);
+ const float* JXL_RESTRICT a = img.alpha()->Row(y);
+ for (size_t x = 0; x < img.xsize(); ++x) {
+ r[x] = a[x] * r[x] + (1.f - a[x]) * bg;
+ g[x] = a[x] * g[x] + (1.f - a[x]) * bg;
+ b[x] = a[x] * b[x] + (1.f - a[x]) * bg;
+ }
+ }
+}
+
+} // namespace
+
+/*
+The final score is based on a weighted sum of 108 sub-scores:
+- for 6 scales (1:1 to 1:32, downsampled in linear RGB)
+- for 3 components (X, Y, B-Y, rescaled to 0..1 range)
+- using 2 norms (the 1-norm and the 4-norm)
+- over 3 error maps:
+ - SSIM' (SSIM without the spurious gamma correction term)
+ - "ringing" (distorted edges where there are no orig edges)
+ - "blurring" (orig edges where there are no distorted edges)
+
+The weights were obtained by running Nelder-Mead simplex search,
+optimizing to minimize MSE for the CID22 training set and to
+maximize Kendall rank correlation (and with a lower weight,
+also Pearson correlation) with the CID22 training set and the
+TID2013, Kadid10k and KonFiG-IQA datasets.
+Validation was done on the CID22 validation set.
+
+Final results after tuning (Kendall | Spearman | Pearson):
+ CID22: 0.6903 | 0.8805 | 0.8583
+ TID2013: 0.6590 | 0.8445 | 0.8471
+ KADID-10k: 0.6175 | 0.8133 | 0.8030
+ KonFiG(F): 0.7668 | 0.9194 | 0.9136
+*/
+double Msssim::Score() const {
+ double ssim = 0.0;
+ constexpr double weight[108] = {0.0,
+ 0.0007376606707406586,
+ 0.0,
+ 0.0,
+ 0.0007793481682867309,
+ 0.0,
+ 0.0,
+ 0.0004371155730107379,
+ 0.0,
+ 1.1041726426657346,
+ 0.00066284834129271,
+ 0.00015231632783718752,
+ 0.0,
+ 0.0016406437456599754,
+ 0.0,
+ 1.8422455520539298,
+ 11.441172603757666,
+ 0.0,
+ 0.0007989109436015163,
+ 0.000176816438078653,
+ 0.0,
+ 1.8787594979546387,
+ 10.94906990605142,
+ 0.0,
+ 0.0007289346991508072,
+ 0.9677937080626833,
+ 0.0,
+ 0.00014003424285435884,
+ 0.9981766977854967,
+ 0.00031949755934435053,
+ 0.0004550992113792063,
+ 0.0,
+ 0.0,
+ 0.0013648766163243398,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 7.466890328078848,
+ 0.0,
+ 17.445833984131262,
+ 0.0006235601634041466,
+ 0.0,
+ 0.0,
+ 6.683678146179332,
+ 0.00037724407979611296,
+ 1.027889937768264,
+ 225.20515300849274,
+ 0.0,
+ 0.0,
+ 19.213238186143016,
+ 0.0011401524586618361,
+ 0.001237755635509985,
+ 176.39317598450694,
+ 0.0,
+ 0.0,
+ 24.43300999870476,
+ 0.28520802612117757,
+ 0.0004485436923833408,
+ 0.0,
+ 0.0,
+ 0.0,
+ 34.77906344483772,
+ 44.835625328877896,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0008680556573291698,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0005313191874358747,
+ 0.0,
+ 0.00016533814161379112,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0004179171803251336,
+ 0.0017290828234722833,
+ 0.0,
+ 0.0020827005846636437,
+ 0.0,
+ 0.0,
+ 8.826982764996862,
+ 23.19243343998926,
+ 0.0,
+ 95.1080498811086,
+ 0.9863978034400682,
+ 0.9834382792465353,
+ 0.0012286405048278493,
+ 171.2667255897307,
+ 0.9807858872435379,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0005130064588990679,
+ 0.0,
+ 0.00010854057858411537};
+
+ size_t i = 0;
+ char ch[] = "XYB";
+ const bool verbose = false;
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t scale = 0; scale < scales.size(); ++scale) {
+ for (size_t n = 0; n < 2; n++) {
+#ifdef SSIMULACRA2_OUTPUT_RAW_SCORES_FOR_WEIGHT_TUNING
+ printf("%.12f,%.12f,%.12f,", scales[scale].avg_ssim[c * 2 + n],
+ scales[scale].avg_edgediff[c * 4 + n],
+ scales[scale].avg_edgediff[c * 4 + 2 + n]);
+#endif
+ if (verbose) {
+ printf("%f from channel %c ssim, scale 1:%i, %" PRIuS
+ "-norm (weight %f)\n",
+ weight[i] * std::abs(scales[scale].avg_ssim[c * 2 + n]), ch[c],
+ 1 << scale, n * 3 + 1, weight[i]);
+ }
+ ssim += weight[i++] * std::abs(scales[scale].avg_ssim[c * 2 + n]);
+ if (verbose) {
+ printf("%f from channel %c ringing, scale 1:%i, %" PRIuS
+ "-norm (weight %f)\n",
+ weight[i] * std::abs(scales[scale].avg_edgediff[c * 4 + n]),
+ ch[c], 1 << scale, n * 3 + 1, weight[i]);
+ }
+ ssim += weight[i++] * std::abs(scales[scale].avg_edgediff[c * 4 + n]);
+ if (verbose) {
+ printf(
+ "%f from channel %c blur, scale 1:%i, %" PRIuS
+ "-norm (weight %f)\n",
+ weight[i] * std::abs(scales[scale].avg_edgediff[c * 4 + n + 2]),
+ ch[c], 1 << scale, n * 3 + 1, weight[i]);
+ }
+ ssim +=
+ weight[i++] * std::abs(scales[scale].avg_edgediff[c * 4 + n + 2]);
+ }
+ }
+ }
+
+ ssim = ssim * 0.9562382616834844;
+ ssim = 2.326765642916932 * ssim - 0.020884521182843837 * ssim * ssim +
+ 6.248496625763138e-05 * ssim * ssim * ssim;
+ if (ssim > 0) {
+ ssim = 100.0 - 10.0 * pow(ssim, 0.6276336467831387);
+ } else {
+ ssim = 100.0;
+ }
+ return ssim;
+}
+
+Msssim ComputeSSIMULACRA2(const jxl::ImageBundle& orig,
+ const jxl::ImageBundle& dist, float bg) {
+ Msssim msssim;
+
+ jxl::Image3F img1(orig.xsize(), orig.ysize());
+ jxl::Image3F img2(img1.xsize(), img1.ysize());
+
+ jxl::ImageBundle orig2 = orig.Copy();
+ jxl::ImageBundle dist2 = dist.Copy();
+
+ if (orig.HasAlpha()) AlphaBlend(orig2, bg);
+ if (dist.HasAlpha()) AlphaBlend(dist2, bg);
+ orig2.ClearExtraChannels();
+ dist2.ClearExtraChannels();
+
+ JXL_CHECK(orig2.TransformTo(jxl::ColorEncoding::LinearSRGB(orig2.IsGray()),
+ *JxlGetDefaultCms()));
+ JXL_CHECK(dist2.TransformTo(jxl::ColorEncoding::LinearSRGB(dist2.IsGray()),
+ *JxlGetDefaultCms()));
+
+ jxl::ToXYB(orig2, nullptr, &img1, *JxlGetDefaultCms(), nullptr);
+ jxl::ToXYB(dist2, nullptr, &img2, *JxlGetDefaultCms(), nullptr);
+ MakePositiveXYB(img1);
+ MakePositiveXYB(img2);
+
+ Image3F mul(img1.xsize(), img1.ysize());
+ Blur blur(img1.xsize(), img1.ysize());
+
+ for (int scale = 0; scale < kNumScales; scale++) {
+ if (img1.xsize() < 8 || img1.ysize() < 8) {
+ break;
+ }
+ if (scale) {
+ orig2.SetFromImage(Downsample(*orig2.color(), 2, 2),
+ jxl::ColorEncoding::LinearSRGB(orig2.IsGray()));
+ dist2.SetFromImage(Downsample(*dist2.color(), 2, 2),
+ jxl::ColorEncoding::LinearSRGB(dist2.IsGray()));
+ img1.ShrinkTo(orig2.xsize(), orig2.ysize());
+ img2.ShrinkTo(orig2.xsize(), orig2.ysize());
+ jxl::ToXYB(orig2, nullptr, &img1, *JxlGetDefaultCms(), nullptr);
+ jxl::ToXYB(dist2, nullptr, &img2, *JxlGetDefaultCms(), nullptr);
+ MakePositiveXYB(img1);
+ MakePositiveXYB(img2);
+ }
+ mul.ShrinkTo(img1.xsize(), img1.ysize());
+ blur.ShrinkTo(img1.xsize(), img1.ysize());
+
+ Multiply(img1, img1, &mul);
+ Image3F sigma1_sq = blur(mul);
+
+ Multiply(img2, img2, &mul);
+ Image3F sigma2_sq = blur(mul);
+
+ Multiply(img1, img2, &mul);
+ Image3F sigma12 = blur(mul);
+
+ Image3F mu1 = blur(img1);
+ Image3F mu2 = blur(img2);
+
+ MsssimScale sscale;
+ SSIMMap(mu1, mu2, sigma1_sq, sigma2_sq, sigma12, sscale.avg_ssim);
+ EdgeDiffMap(img1, mu1, img2, mu2, sscale.avg_edgediff);
+ msssim.scales.push_back(sscale);
+ }
+ return msssim;
+}
+
+Msssim ComputeSSIMULACRA2(const jxl::ImageBundle& orig,
+ const jxl::ImageBundle& distorted) {
+ return ComputeSSIMULACRA2(orig, distorted, 0.5f);
+}
diff --git a/tools/ssimulacra2.h b/tools/ssimulacra2.h
new file mode 100644
index 0000000..36d1193
--- /dev/null
+++ b/tools/ssimulacra2.h
@@ -0,0 +1,32 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_SSIMULACRA2_H_
+#define TOOLS_SSIMULACRA2_H_
+
+#include <vector>
+
+#include "lib/jxl/image_bundle.h"
+
+struct MsssimScale {
+ double avg_ssim[3 * 2];
+ double avg_edgediff[3 * 4];
+};
+
+struct Msssim {
+ std::vector<MsssimScale> scales;
+
+ double Score() const;
+};
+
+// Computes the SSIMULACRA 2 score between reference image 'orig' and
+// distorted image 'distorted'. In case of alpha transparency, assume
+// a gray background if intensity 'bg' (in range 0..1).
+Msssim ComputeSSIMULACRA2(const jxl::ImageBundle &orig,
+ const jxl::ImageBundle &distorted, float bg);
+Msssim ComputeSSIMULACRA2(const jxl::ImageBundle &orig,
+ const jxl::ImageBundle &distorted);
+
+#endif // TOOLS_SSIMULACRA2_H_
diff --git a/tools/ssimulacra2_main.cc b/tools/ssimulacra2_main.cc
new file mode 100644
index 0000000..758f188
--- /dev/null
+++ b/tools/ssimulacra2_main.cc
@@ -0,0 +1,83 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdio.h>
+
+#include "lib/extras/codec.h"
+// TODO(eustas): we should, but we can't?
+// #include "lib/jxl/base/span.h"
+#include "tools/file_io.h"
+#include "tools/ssimulacra2.h"
+
+int PrintUsage(char** argv) {
+ fprintf(stderr, "Usage: %s orig.png distorted.png\n", argv[0]);
+ fprintf(stderr,
+ "Returns a score in range -inf..100, which correlates to subjective "
+ "visual quality:\n");
+ fprintf(stderr,
+ " 30 = low quality (p10 worst output of mozjpeg -quality 30)\n");
+ fprintf(stderr,
+ " 50 = medium quality (average output of cjxl -q 40 or mozjpeg "
+ "-quality 40,\n");
+ fprintf(stderr,
+ " p10 output of cjxl -q 50 or mozjpeg "
+ "-quality 60)\n");
+ fprintf(stderr,
+ " 70 = high quality (average output of cjxl -q 70 or mozjpeg "
+ "-quality 70,\n");
+ fprintf(stderr,
+ " p10 output of cjxl -q 75 or mozjpeg "
+ "-quality 80)\n");
+ fprintf(stderr,
+ " 90 = very high quality (impossible to distinguish from "
+ "original at 1:1,\n");
+ fprintf(stderr,
+ " average output of cjxl -q 90 or "
+ "mozjpeg -quality 90)\n");
+ return 1;
+}
+
+int main(int argc, char** argv) {
+ if (argc != 3) return PrintUsage(argv);
+
+ jxl::CodecInOut io[2];
+ const char* purpose[] = {"original", "distorted"};
+ for (size_t i = 0; i < 2; ++i) {
+ std::vector<uint8_t> encoded;
+ if (!jpegxl::tools::ReadFile(argv[1 + i], &encoded)) {
+ fprintf(stderr, "Could not load %s image: %s\n", purpose[i], argv[1 + i]);
+ return 1;
+ }
+ if (!jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(),
+ &io[i])) {
+ fprintf(stderr, "Could not decode %s image: %s\n", purpose[i],
+ argv[1 + i]);
+ return 1;
+ }
+ if (io[i].xsize() < 8 || io[i].ysize() < 8) {
+ fprintf(stderr, "Minimum image size is 8x8 pixels\n");
+ return 1;
+ }
+ }
+ jxl::CodecInOut& io1 = io[0];
+ jxl::CodecInOut& io2 = io[1];
+
+ if (io1.xsize() != io2.xsize() || io1.ysize() != io2.ysize()) {
+ fprintf(stderr, "Image size mismatch\n");
+ return 1;
+ }
+
+ if (!io1.Main().HasAlpha()) {
+ Msssim msssim = ComputeSSIMULACRA2(io1.Main(), io2.Main());
+ printf("%.8f\n", msssim.Score());
+ } else {
+ // in case of alpha transparency: blend against dark and bright backgrounds
+ // and return the worst of both scores
+ Msssim msssim0 = ComputeSSIMULACRA2(io1.Main(), io2.Main(), 0.1f);
+ Msssim msssim1 = ComputeSSIMULACRA2(io1.Main(), io2.Main(), 0.9f);
+ printf("%.8f\n", std::min(msssim0.Score(), msssim1.Score()));
+ }
+ return 0;
+}
diff --git a/tools/ssimulacra_main.cc b/tools/ssimulacra_main.cc
index 5b48fe2..70538a5 100644
--- a/tools/ssimulacra_main.cc
+++ b/tools/ssimulacra_main.cc
@@ -6,8 +6,12 @@
#include <stdio.h>
#include "lib/extras/codec.h"
-#include "lib/jxl/color_management.h"
-#include "lib/jxl/enc_color_management.h"
+// TODO(eustas): we should, but we can't?
+// #include "lib/jxl/base/span.h"
+#include <jxl/cms.h>
+
+#include "lib/jxl/image_bundle.h"
+#include "tools/file_io.h"
#include "tools/ssimulacra.h"
namespace ssimulacra {
@@ -33,26 +37,31 @@ int Run(int argc, char** argv) {
}
if (argc < input_arg + 2) return PrintUsage(argv);
- jxl::CodecInOut io1;
- jxl::CodecInOut io2;
- JXL_CHECK(SetFromFile(argv[input_arg], jxl::extras::ColorHints(), &io1));
- JXL_CHECK(SetFromFile(argv[input_arg + 1], jxl::extras::ColorHints(), &io2));
- JXL_CHECK(io1.TransformTo(jxl::ColorEncoding::LinearSRGB(io1.Main().IsGray()),
- jxl::GetJxlCms()));
- JXL_CHECK(io2.TransformTo(jxl::ColorEncoding::LinearSRGB(io2.Main().IsGray()),
- jxl::GetJxlCms()));
-
- if (io1.xsize() != io2.xsize() || io1.ysize() != io2.ysize()) {
+ jxl::CodecInOut io[2];
+ for (size_t i = 0; i < 2; ++i) {
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(argv[input_arg + i], &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(),
+ &io[i]));
+ }
+ jxl::ImageBundle& ib1 = io[0].Main();
+ jxl::ImageBundle& ib2 = io[1].Main();
+ JXL_CHECK(ib1.TransformTo(jxl::ColorEncoding::LinearSRGB(ib1.IsGray()),
+ *JxlGetDefaultCms(), nullptr));
+ JXL_CHECK(ib2.TransformTo(jxl::ColorEncoding::LinearSRGB(ib2.IsGray()),
+ *JxlGetDefaultCms(), nullptr));
+ jxl::Image3F& img1 = *ib1.color();
+ jxl::Image3F& img2 = *ib2.color();
+ if (img1.xsize() != img2.xsize() || img1.ysize() != img2.ysize()) {
fprintf(stderr, "Image size mismatch\n");
return 1;
}
- if (io1.xsize() < 8 || io1.ysize() < 8) {
+ if (img1.xsize() < 8 || img1.ysize() < 8) {
fprintf(stderr, "Minimum image size is 8x8 pixels\n");
return 1;
}
- Ssimulacra ssimulacra =
- ComputeDiff(*io1.Main().color(), *io2.Main().color(), simple);
+ Ssimulacra ssimulacra = ComputeDiff(img1, img2, simple);
if (verbose) {
ssimulacra.PrintDetails();
diff --git a/tools/thread_pool_internal.h b/tools/thread_pool_internal.h
new file mode 100644
index 0000000..92a1176
--- /dev/null
+++ b/tools/thread_pool_internal.h
@@ -0,0 +1,47 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_THREAD_POOL_INTERNAL_H_
+#define TOOLS_THREAD_POOL_INTERNAL_H_
+
+#include <jxl/thread_parallel_runner_cxx.h>
+#include <stddef.h>
+
+#include <cmath>
+#include <thread> // NOLINT
+
+#include "lib/jxl/base/data_parallel.h"
+
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::ThreadPool;
+
+// Helper class to pass an internal ThreadPool-like object using threads.
+class ThreadPoolInternal {
+ public:
+ // Starts the given number of worker threads and blocks until they are ready.
+ // "num_worker_threads" defaults to one per hyperthread. If zero, all tasks
+ // run on the main thread.
+ explicit ThreadPoolInternal(
+ size_t num_threads = std::thread::hardware_concurrency()) {
+ runner_ =
+ JxlThreadParallelRunnerMake(/* memory_manager */ nullptr, num_threads);
+ pool_.reset(new ThreadPool(JxlThreadParallelRunner, runner_.get()));
+ }
+
+ ThreadPoolInternal(const ThreadPoolInternal&) = delete;
+ ThreadPoolInternal& operator&(const ThreadPoolInternal&) = delete;
+ ThreadPool* operator&() { return pool_.get(); }
+
+ private:
+ JxlThreadParallelRunnerPtr runner_;
+ std::unique_ptr<ThreadPool> pool_;
+};
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_THREAD_POOL_INTERNAL_H_
diff --git a/tools/transforms_fuzzer.cc b/tools/transforms_fuzzer.cc
index 1ef08b2..6d78a05 100644
--- a/tools/transforms_fuzzer.cc
+++ b/tools/transforms_fuzzer.cc
@@ -10,7 +10,21 @@
#include "lib/jxl/modular/encoding/encoding.h"
#include "lib/jxl/modular/transform/transform.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
+
+using ::jxl::BitReader;
+using ::jxl::BitReaderScopedCloser;
+using ::jxl::Bytes;
+using ::jxl::Channel;
+using ::jxl::GroupHeader;
+using ::jxl::Image;
+using ::jxl::ModularOptions;
+using ::jxl::pixel_type;
+using ::jxl::Rng;
+using ::jxl::Status;
+using ::jxl::Transform;
+using ::jxl::weighted::Header;
namespace {
void FillChannel(Channel& ch, Rng& rng) {
@@ -32,7 +46,7 @@ void AssertEq(T a, T b) {
int TestOneInput(const uint8_t* data, size_t size) {
static Status nevermind = true;
- BitReader reader(Span<const uint8_t>(data, size));
+ BitReader reader(Bytes(data, size));
BitReaderScopedCloser reader_closer(&reader, &nevermind);
Rng rng(reader.ReadFixedBits<56>());
@@ -50,8 +64,8 @@ int TestOneInput(const uint8_t* data, size_t size) {
size_t w_orig = static_cast<size_t>(reader.ReadFixedBits<16>());
size_t h_orig = static_cast<size_t>(reader.ReadFixedBits<16>());
- size_t w = DivCeil(w_orig, upsampling);
- size_t h = DivCeil(h_orig, upsampling);
+ size_t w = jxl::DivCeil(w_orig, upsampling);
+ size_t h = jxl::DivCeil(h_orig, upsampling);
if ((nb_chans == 2) || ((nb_chans + nb_extra) == 0) || (w * h == 0) ||
((w_orig * h_orig * (nb_chans + nb_extra)) > (1 << 23))) {
@@ -80,21 +94,22 @@ int TestOneInput(const uint8_t* data, size_t size) {
Channel& ch = image.channel[c];
ch.hshift = hshift[c];
ch.vshift = vshift[c];
- ch.shrink(DivCeil(w, 1 << hshift[c]), DivCeil(h, 1 << vshift[c]));
+ ch.shrink(jxl::DivCeil(w, 1 << hshift[c]), jxl::DivCeil(h, 1 << vshift[c]));
}
for (size_t ec = 0; ec < nb_extra; ec++) {
Channel& ch = image.channel[ec + nb_chans];
size_t ch_up = ec_upsampling[ec];
- int up_level = CeilLog2Nonzero(ch_up) - CeilLog2Nonzero(upsampling);
- ch.shrink(DivCeil(w_orig, ch_up), DivCeil(h_orig, ch_up));
+ int up_level =
+ jxl::CeilLog2Nonzero(ch_up) - jxl::CeilLog2Nonzero(upsampling);
+ ch.shrink(jxl::DivCeil(w_orig, ch_up), jxl::DivCeil(h_orig, ch_up));
ch.hshift = ch.vshift = up_level;
}
GroupHeader header;
- if (!Bundle::Read(&reader, &header)) return 0;
- weighted::Header w_header;
- if (!Bundle::Read(&reader, &w_header)) return 0;
+ if (!jxl::Bundle::Read(&reader, &header)) return 0;
+ Header w_header;
+ if (!jxl::Bundle::Read(&reader, &w_header)) return 0;
// TODO(eustas): give it a try?
if (!reader.AllReadsWithinBounds()) return 0;
@@ -122,16 +137,17 @@ int TestOneInput(const uint8_t* data, size_t size) {
const Channel& ch = image.channel[c];
AssertEq(ch.hshift, hshift[c]);
AssertEq(ch.vshift, vshift[c]);
- AssertEq(ch.w, DivCeil(w, 1 << hshift[c]));
- AssertEq(ch.h, DivCeil(h, 1 << vshift[c]));
+ AssertEq(ch.w, jxl::DivCeil(w, 1 << hshift[c]));
+ AssertEq(ch.h, jxl::DivCeil(h, 1 << vshift[c]));
}
for (size_t ec = 0; ec < nb_extra; ec++) {
const Channel& ch = image.channel[ec + nb_chans];
size_t ch_up = ec_upsampling[ec];
- int up_level = CeilLog2Nonzero(ch_up) - CeilLog2Nonzero(upsampling);
- AssertEq(ch.w, DivCeil(w_orig, ch_up));
- AssertEq(ch.h, DivCeil(h_orig, ch_up));
+ int up_level =
+ jxl::CeilLog2Nonzero(ch_up) - jxl::CeilLog2Nonzero(upsampling);
+ AssertEq(ch.w, jxl::DivCeil(w_orig, ch_up));
+ AssertEq(ch.h, jxl::DivCeil(h_orig, ch_up));
AssertEq(ch.hshift, up_level);
AssertEq(ch.vshift, up_level);
}
@@ -139,8 +155,9 @@ int TestOneInput(const uint8_t* data, size_t size) {
return 0;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- return jxl::TestOneInput(data, size);
+ return jpegxl::tools::TestOneInput(data, size);
}
diff --git a/tools/upscaling_coefficients/upscaler_demo.py b/tools/upscaling_coefficients/upscaler_demo.py
index 89f1320..e873bd1 100644
--- a/tools/upscaling_coefficients/upscaler_demo.py
+++ b/tools/upscaling_coefficients/upscaler_demo.py
@@ -37,14 +37,14 @@ def convolution(pixels, kernel):
`kernel`.
Args:
- pixels: A [heigth, width]- or [height, width, num_channels]-array
+ pixels: A [height, width]- or [height, width, num_channels]-array
representing an image.
kernel: A [upscaling_factor, upscaling_factor, kernel_size,
kernel_size]-array used for the convolution.
Returns:
- A [upscaling_factor*heigth, upscaling_factor*width]- or
+ A [upscaling_factor*height, upscaling_factor*width]- or
[upscaling_factor*height, upscaling_factor*width, num_channels]-array representing the
convoluted upscaled image.
"""
diff --git a/tools/viewer/CMakeLists.txt b/tools/viewer/CMakeLists.txt
index 7dbe5e3..2b25e26 100644
--- a/tools/viewer/CMakeLists.txt
+++ b/tools/viewer/CMakeLists.txt
@@ -3,9 +3,9 @@
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
-find_package(Qt5 QUIET COMPONENTS Widgets)
-if (NOT Qt5_FOUND)
- message(WARNING "Qt5 was not found. The directory viewer will not be built.")
+find_package(Qt6 QUIET COMPONENTS Widgets)
+if (NOT Qt6_FOUND)
+ message(WARNING "Qt6 was not found. The directory viewer will not be built.")
return()
endif ()
@@ -31,7 +31,7 @@ target_include_directories(viewer PRIVATE
"${PROJECT_SOURCE_DIR}"
)
target_link_libraries(viewer
- Qt5::Widgets
+ Qt6::Widgets
icc_detect
jxl
jxl_threads
diff --git a/tools/viewer/load_jxl.cc b/tools/viewer/load_jxl.cc
index 7fd35d8..b97a906 100644
--- a/tools/viewer/load_jxl.cc
+++ b/tools/viewer/load_jxl.cc
@@ -5,18 +5,21 @@
#include "tools/viewer/load_jxl.h"
+#include <jxl/decode.h>
+#include <jxl/decode_cxx.h>
+#include <jxl/thread_parallel_runner_cxx.h>
+#include <jxl/types.h>
#include <stdint.h>
#include <QElapsedTimer>
#include <QFile>
-#include "jxl/decode.h"
-#include "jxl/decode_cxx.h"
-#include "jxl/thread_parallel_runner_cxx.h"
-#include "jxl/types.h"
+#define CMS_NO_REGISTER_KEYWORD 1
#include "lcms2.h"
+#undef CMS_NO_REGISTER_KEYWORD
-namespace jxl {
+namespace jpegxl {
+namespace tools {
namespace {
@@ -97,12 +100,11 @@ QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile,
size_t icc_size;
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderGetICCProfileSize(
- dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size));
+ dec.get(), JXL_COLOR_PROFILE_TARGET_DATA, &icc_size));
std::vector<uint8_t> icc_profile(icc_size);
- EXPECT_EQ(JXL_DEC_SUCCESS,
- JxlDecoderGetColorAsICCProfile(
- dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA,
- icc_profile.data(), icc_profile.size()));
+ EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetColorAsICCProfile(
+ dec.get(), JXL_COLOR_PROFILE_TARGET_DATA,
+ icc_profile.data(), icc_profile.size()));
std::vector<float> float_pixels(pixel_count * 4);
EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec.get()));
@@ -135,40 +137,19 @@ QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile,
if (elapsed_ns != nullptr) *elapsed_ns = timer.nsecsElapsed();
QImage result(info.xsize, info.ysize,
-#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
info.alpha_premultiplied ? QImage::Format_RGBA64_Premultiplied
- : QImage::Format_RGBA64
-#else
- info.alpha_premultiplied ? QImage::Format_ARGB32_Premultiplied
- : QImage::Format_ARGB32
-#endif
- );
+ : QImage::Format_RGBA64);
for (int y = 0; y < result.height(); ++y) {
-#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
QRgba64* const row = reinterpret_cast<QRgba64*>(result.scanLine(y));
-#else
- QRgb* const row = reinterpret_cast<QRgb*>(result.scanLine(y));
-#endif
const uint16_t* const data = uint16_pixels.data() + result.width() * y * 4;
for (int x = 0; x < result.width(); ++x) {
-#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
row[x] = qRgba64(data[4 * x + 0], data[4 * x + 1], data[4 * x + 2],
- data[4 * x + 3])
-#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0)
- .toArgb32()
-#endif
- ;
-#else
- // Qt version older than 5.6 doesn't have a qRgba64.
- row[x] = qRgba(data[4 * x + 0] * (255.f / 65535) + .5f,
- data[4 * x + 1] * (255.f / 65535) + .5f,
- data[4 * x + 2] * (255.f / 65535) + .5f,
- data[4 * x + 3] * (255.f / 65535) + .5f);
-#endif
+ data[4 * x + 3]);
}
}
return result;
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/viewer/load_jxl.h b/tools/viewer/load_jxl.h
index 594f646..85dc1a9 100644
--- a/tools/viewer/load_jxl.h
+++ b/tools/viewer/load_jxl.h
@@ -10,11 +10,13 @@
#include <QImage>
#include <QString>
-namespace jxl {
+namespace jpegxl {
+namespace tools {
QImage loadJxlImage(const QString& filename, const QByteArray& targetIccProfile,
qint64* elapsed, bool* usedRequestedProfile = nullptr);
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_VIEWER_LOAD_JXL_H_
diff --git a/tools/viewer/main.cc b/tools/viewer/main.cc
index d677888..1e80be3 100644
--- a/tools/viewer/main.cc
+++ b/tools/viewer/main.cc
@@ -12,7 +12,7 @@ int main(int argc, char** argv) {
QStringList arguments = application.arguments();
arguments.removeFirst();
- jxl::ViewerWindow window;
+ jpegxl::tools::ViewerWindow window;
window.show();
if (!arguments.empty()) {
diff --git a/tools/viewer/viewer_window.cc b/tools/viewer/viewer_window.cc
index 530c2f0..6b5f912 100644
--- a/tools/viewer/viewer_window.cc
+++ b/tools/viewer/viewer_window.cc
@@ -15,7 +15,8 @@
#include "tools/icc_detect/icc_detect.h"
#include "tools/viewer/load_jxl.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
namespace {
@@ -50,7 +51,7 @@ void ViewerWindow::loadFilesAndDirectories(QStringList entries) {
filenames_.clear();
QSet<QString> visited;
for (const QString& entry : entries) {
- recursivelyAddSubEntries(entry, &visited, &filenames_);
+ recursivelyAddSubEntries(QFileInfo(entry), &visited, &filenames_);
}
const bool several = filenames_.size() > 1;
@@ -127,4 +128,5 @@ void ViewerWindow::refreshImage() {
}
}
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
diff --git a/tools/viewer/viewer_window.h b/tools/viewer/viewer_window.h
index 42de5bc..78aafb9 100644
--- a/tools/viewer/viewer_window.h
+++ b/tools/viewer/viewer_window.h
@@ -12,7 +12,8 @@
#include "tools/viewer/ui_viewer_window.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
class ViewerWindow : public QMainWindow {
Q_OBJECT
@@ -36,6 +37,7 @@ class ViewerWindow : public QMainWindow {
bool hasWarnedAboutMonitorProfile_ = false;
};
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
#endif // TOOLS_VIEWER_VIEWER_WINDOW_H_
diff --git a/tools/wasm_demo/CMakeLists.txt b/tools/wasm_demo/CMakeLists.txt
new file mode 100644
index 0000000..0549e76
--- /dev/null
+++ b/tools/wasm_demo/CMakeLists.txt
@@ -0,0 +1,64 @@
+if (NOT JPEGXL_ENABLE_TOOLS OR NOT EMSCRIPTEN)
+ return()
+endif()
+
+# WASM API facade.
+add_executable(jxl_decoder jxl_decoder.cc jxl_decompressor.cc no_png.cc)
+add_executable(jxl_decoder_for_test jxl_decoder.cc jxl_decompressor.cc no_png.cc)
+target_link_libraries(jxl_decoder jxl_extras-internal jxl_threads)
+target_link_libraries(jxl_decoder_for_test jxl_extras-internal jxl_threads)
+
+set(JXL_C_SYMBOLS
+ _free
+ _malloc
+)
+
+set(JXL_DECODER_SYMBOLS
+ _jxlCreateInstance
+ _jxlDestroyInstance
+ _jxlFlush
+ _jxlProcessInput
+)
+
+set(JXL_DECOMPRESSOR_SYMBOLS
+ _jxlDecompress
+ _jxlCleanup
+)
+
+set(JXL_MODULE_SYMBOLS ${JXL_C_SYMBOLS} ${JXL_DECODER_SYMBOLS} ${JXL_DECOMPRESSOR_SYMBOLS})
+
+list(JOIN JXL_MODULE_SYMBOLS ", " JXL_MODULE_EXPORTS)
+
+set(JXL_WASM_SITE_LINK_FLAGS " -O3 -s FILESYSTEM=0 --closure 1 -mnontrapping-fptoint")
+set(JXL_WASM_TEST_LINK_FLAGS " -O1 -s NODERAWFS=1 ")
+
+set(JXL_WASM_BASE_LINK_FLAGS "\
+ -s ALLOW_MEMORY_GROWTH=1 \
+ -s DISABLE_EXCEPTION_CATCHING=1 \
+ -s MODULARIZE=1 \
+ -s USE_PTHREADS=1 \
+ -s PTHREAD_POOL_SIZE=4 \
+")
+
+# libpng is used only by "decompressor"
+set(JXL_DECODER_LINK_FLAGS "${JXL_WASM_BASE_LINK_FLAGS} \
+ -s EXPORT_NAME=\"JxlDecoderModule\" \
+ -s \"EXPORTED_FUNCTIONS=[${JXL_MODULE_EXPORTS}]\" \
+")
+
+set_target_properties(jxl_decoder PROPERTIES LINK_FLAGS
+ "${JXL_DECODER_LINK_FLAGS} ${JXL_WASM_SITE_LINK_FLAGS}")
+
+set_target_properties(jxl_decoder_for_test PROPERTIES LINK_FLAGS
+ "${JXL_DECODER_LINK_FLAGS} ${JXL_WASM_TEST_LINK_FLAGS}")
+
+if (BUILD_TESTING)
+ add_test(
+ NAME test_wasm_jxl_decoder
+ COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR}
+ --no-experimental-fetch
+ ${CMAKE_CURRENT_SOURCE_DIR}/jxl_decoder_test.js
+ )
+ set_tests_properties(test_wasm_jxl_decoder PROPERTIES
+ ENVIRONMENT NODE_PATH=$<TARGET_FILE_DIR:jxl_decoder_for_test>)
+endif() # BUILD_TESTING
diff --git a/tools/wasm_demo/README.md b/tools/wasm_demo/README.md
new file mode 100644
index 0000000..804cd35
--- /dev/null
+++ b/tools/wasm_demo/README.md
@@ -0,0 +1,126 @@
+## WebAssembly demonstration
+
+This folder contains an example how to decode JPEG XL files on a web page using
+WASM engine.
+
+### One line demo
+
+The simplest way to get support of JXL images on the client side is simply to
+link one extra script (`<script src="service_worker.js">`) to the page.
+This script installs a `ServiceWorker` that:
+
+ - checks if the browser supports the JXL image format already
+ - if it is not, then advertise `image/jxl` as media format in image requests
+ - then, if the server responds with `image/jxl` content it gets decoded and
+ re-encoded to PNG on the fly
+
+Generally the message / data flow looks the following way:
+
+ - `Fetch API` receives a resource request from client page (e.g. when the HTML
+ engine discovers an `img` tag) and asks the `ServiceWorker` how to proceed
+ - the `ServiceWorker` alters the request and uses the `Fetch API`
+ to obtain data
+ - when data arrives, the `ServiceWorker` forwards it to the "client"
+ (the page) that initiated the resource request
+ - the client forwards the data to a worker (see `client_worker.js`) to avoid
+ processing in the "main loop" thread
+ - a worker does the actual decoding; to make it faster several additional
+ workers are spawned (to enable multi-threading in WASM module);
+ the decoded image is wrapped in non-compressed PNG format and sent back
+ to client
+ - the client relays image data to `ServiceWorker`
+ - the `ServiceWorker` passes data to `Fetch API` as a response to initial
+ resource request
+
+Despite the additional "hop" (client) in the flow, data is not copied every
+time but rather "transferred" between the participants.
+
+Demo page: `one_line_demo.html`. Extended demo, that also shows how long it
+took do decode images: `one_line_demo_with_console.html`.
+
+Page that shows "manual" decoding (and has benchmarking capabilities):
+`manual_decode_demo.html`.
+
+### Hosting
+
+To enable multi-threading some files should be served in a secure context (i.e.
+transferred over HTTPS) and executed in a "site-isolation" mode (controlled by
+COOP and COEP response headers).
+
+Unfortunately [GitHub Pages](https://pages.github.com/) does not allow setting
+response headers.
+
+[Netlify](https://www.netlify.com/) provides free, easy to setup and deploy
+platform for serving such demonstration sites. However, any other
+service provider / software that allows changing response headers could be
+employed as well.
+
+`netlify.toml` and `netlify/precompressed.ts` specify the serving rules.
+Namely, some requests get "upgraded" responses:
+
+ - if a request specifies that `brotli` compression is supported,
+ then precompressed entries are sent
+ - if a request specifies that `image/jxl` format is allowed,
+ then entries transcoded to JXL format are sent
+
+### How to build the demo
+
+`build_site.py` script takes care of JavaScript minification, template
+substitution and resource compression. Its arguments are:
+
+ - source path: site template directory (that contains this README file)
+ - binary path: build directory, that contains compiled WASM module
+ - output path
+
+To complete the site few more files are to be added to output directory:
+
+ - `image00.jpg`, `image01.png` demo images; will be shown if `ServiceWorker`
+ is not yet operable (fallback); to see those one could initiate
+ "hard page reload" (press Shift-(Ctrl|Cmd)-R)
+ - `image00.jpg.jxl`, `image01.png.jxl` demo images in JXL format
+ - `imageNN.jxl` images for "manual" decoding demo; NN is a number starting
+ form `00`
+ - `favicon.ico` is an optional site icon
+ - `index.html` is an optional site "home" page
+
+In the source code (`service_worker.js`) there are two compile-time constants
+that modify the behaviour of Service Worker:
+
+ - `FORCE_COP` flag allows rewriting responses to add COOP / COEP headers;
+ this is useful when it is difficult / impossible to setup response headers
+ otherwise (e.g. GitHub Pages)
+ - `FORCE_DECODING` flag activate JXL decoding when image response type has
+ `Content-Encoding` header set to `application/octet-stream`; this happens
+ when server does not know the JXL MIME-type
+
+One dependency that `build_site.py` requires is [uglifyjs](https://github.com/mishoo/UglifyJS), which can be installed with
+```
+npm install uglify-js -g
+```
+If you followed the [wasm build instructions](../../docs/building_wasm.md),
+assuming you are in the root level of the cloned libjxl repo a typical call to
+build the site would be
+```bash
+python3 ./tools/wasm_demo/build_site.py ./tools/wasm_demo/ ./build-wasm32/tools/wasm_demo/ /path/to/demo-site
+```
+Then you need to put your image files in the correct same place and are should be good to go.
+
+
+To summarize, using the wasm decoder together with a service workder amounts to adding
+```html
+<script src="service_worker.js"></script>
+```
+to your html and then putting the `service_worker.js` and `jxl_decoder.wasm` binary in directory where they can be read.
+
+
+It is not guaranteed, but somewhat fresh demo is hosted on
+`https://jxl-demo.netlify.app/`, e.g.:
+
+ - [one line demo](https://jxl-demo.netlify.app/one_line_demo_with_console.html)
+ - [one line demo with console](https://jxl-demo.netlify.app/one_line_demo.html)
+ - [manual decode demo](https://jxl-demo.netlify.app/manual_decode_demo.html?img=1&colorSpace=rec2100-pq&runBenchmark=30&wantSdr=false&displayNits=1500);
+ URL contains query parameters that control rendering and benchmarking options;
+ please note, that HDR canvas is often not enabled by default, it could be
+ enabled in some browsers via `about://flags/#enable-experimental-web-platform-features`
+ - [`service_worker.js`](https://jxl-demo.netlify.app/service_worker.js)
+ - [`jxl_decoder.wasm`](https://jxl-demo.netlify.app/jxl_decoder.wasm)
diff --git a/tools/wasm_demo/build_site.py b/tools/wasm_demo/build_site.py
new file mode 100644
index 0000000..f47fa97
--- /dev/null
+++ b/tools/wasm_demo/build_site.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+import shutil
+import subprocess
+import sys
+
+from pathlib import Path
+
+BROTLIFY = False
+ZOPFLIFY = False
+LEAN = True
+NETLIFY = False
+
+REMOVE_SHEBANG = ['jxl_decoder.js']
+EMBED_BIN = [
+ 'jxl_decoder.js',
+ 'jxl_decoder.worker.js'
+]
+EMBED_SRC = ['client_worker.js']
+TEMPLATES = ['service_worker.js']
+COPY_BIN = ['jxl_decoder.wasm'] + [] if LEAN else EMBED_BIN
+COPY_SRC = [
+ 'one_line_demo.html',
+ 'one_line_demo_with_console.html',
+ 'manual_decode_demo.html',
+] + [] if not NETLIFY else [
+ 'netlify.toml',
+ 'netlify'
+] + [] if LEAN else EMBED_SRC
+
+COMPRESS = COPY_BIN + COPY_SRC + TEMPLATES
+COMPRESSIBLE_EXT = ['.html', '.js', '.wasm']
+
+def escape_js(js):
+ return js.replace('\\', '\\\\').replace('\'', '\\\'')
+
+def remove_shebang(txt):
+ lines = txt.splitlines(True) # Keep line-breaks
+ if len(lines) > 0:
+ if lines[0].startswith('#!'):
+ lines = lines[1:]
+ return ''.join(lines)
+
+def compress(path):
+ name = path.name
+ compressible = any([name.endswith(ext) for ext in COMPRESSIBLE_EXT])
+ if not compressible:
+ print(f'Not compressing {name}')
+ return
+ print(f'Processing {name}')
+ orig_size = path.stat().st_size
+ if BROTLIFY:
+ cmd_brotli = ['brotli', '-Zfk', path.absolute()]
+ subprocess.run(cmd_brotli, check=True, stdout=sys.stdout, stderr=sys.stderr)
+ br_size = path.parent.joinpath(name + '.br').stat().st_size
+ print(f' Brotli: {orig_size} -> {br_size}')
+ if ZOPFLIFY:
+ cmd_zopfli = ['zopfli', path.absolute()]
+ subprocess.run(cmd_zopfli, check=True, stdout=sys.stdout, stderr=sys.stderr)
+ gz_size = path.parent.joinpath(name + '.gz').stat().st_size
+ print(f' Zopfli: {orig_size} -> {gz_size}')
+
+def check_util(name):
+ cmd = [name, '-h']
+ try:
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ except:
+ print(f"NOTE: {name} not installed")
+ return False
+ return True
+
+def check_utils():
+ global BROTLIFY
+ BROTLIFY = BROTLIFY and check_util('brotli')
+ global ZOPFLIFY
+ ZOPFLIFY = ZOPFLIFY and check_util('zopfli')
+ if not check_util('uglifyjs'):
+ print("FAIL: uglifyjs is required to build a site")
+ sys.exit()
+
+def uglify(text, name):
+ cmd = ['uglifyjs', '-m', '-c']
+ ugly_result = subprocess.run(
+ cmd, capture_output=True, check=True, input=text, text=True)
+ ugly_text = ugly_result.stdout.strip()
+ print(f'Uglify {name}: {len(text)} -> {len(ugly_text)}')
+ return ugly_text
+
+if __name__ == "__main__":
+ if len(sys.argv) != 4:
+ print(f"Usage: python3 {sys.argv[0]} SRC_DIR BINARY_DIR OUTPUT_DIR")
+ exit(-1)
+ source_path = Path(sys.argv[1]) # CMake build dir
+ binary_path = Path(sys.argv[2]) # Site template dir
+ output_path = Path(sys.argv[3]) # Site output
+
+ check_utils()
+
+ for name in REMOVE_SHEBANG:
+ path = binary_path.joinpath(name)
+ text = path.read_text().strip()
+ path.write_text(remove_shebang(text))
+ remove_shebang
+
+ substitutes = {}
+
+ for name in EMBED_BIN:
+ key = '$' + name + '$'
+ path = binary_path.joinpath(name)
+ value = escape_js(uglify(path.read_text().strip(), name))
+ substitutes[key] = value
+
+ for name in EMBED_SRC:
+ key = '$' + name + '$'
+ path = source_path.joinpath(name)
+ value = escape_js(uglify(path.read_text().strip(), name))
+ substitutes[key] = value
+
+ for name in TEMPLATES:
+ print(f'Processing template {name}')
+ path = source_path.joinpath(name)
+ text = path.read_text().strip()
+ for key, value in substitutes.items():
+ text = text.replace(key, value)
+ #text = uglify(text, name)
+ output_path.joinpath(name).write_text(text)
+
+ for name in COPY_SRC:
+ path = source_path.joinpath(name)
+ if path.is_dir():
+ shutil.copytree(path, output_path.joinpath(
+ name).absolute(), dirs_exist_ok=True)
+ else:
+ shutil.copy(path, output_path.absolute())
+
+ # TODO(eustas): uglify
+ for name in COPY_BIN:
+ shutil.copy(binary_path.joinpath(name), output_path.absolute())
+
+ for name in COMPRESS:
+ compress(output_path.joinpath(name))
diff --git a/tools/wasm_demo/client_worker.js b/tools/wasm_demo/client_worker.js
new file mode 100644
index 0000000..5751b38
--- /dev/null
+++ b/tools/wasm_demo/client_worker.js
@@ -0,0 +1,99 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+let decoder = null;
+
+// Serialize work; plus postpone processing until decoder is ready.
+let jobs = [];
+
+const processJobs = () => {
+ // Decoder not yet loaded.
+ if (!decoder) {
+ return;
+ }
+
+ while (true) {
+ let job = null;
+ // Currently we do not do progressive; process only "inputComplete" jobs.
+ for (let i = 0; i < jobs.length; ++i) {
+ if (!jobs[i].inputComplete) {
+ continue;
+ }
+ job = jobs[i];
+ jobs[i] = jobs[jobs.length - 1];
+ jobs.pop();
+ break;
+ }
+ if (!job) {
+ return;
+ }
+ console.log('CW job: ' + job.uid);
+ const input = job.input;
+ let totalInputLength = 0;
+ for (let i = 0; i < input.length; i++) {
+ totalInputLength += input[i].length;
+ }
+
+ // TODO(eustas): persist to reduce fragmentation?
+ const buffer = decoder._malloc(totalInputLength);
+ // TODO(eustas): check OOM
+ let offset = 0;
+ for (let i = 0; i < input.length; ++i) {
+ decoder.HEAP8.set(input[i], buffer + offset);
+ offset += input[i].length;
+ }
+ let t0 = Date.now();
+ // TODO(eustas): check result
+ const result = decoder._jxlDecompress(buffer, totalInputLength);
+ let t1 = Date.now();
+ const msg = 'Decoded ' + job.url + ' in ' + (t1 - t0) + 'ms';
+ // console.log(msg);
+ decoder._free(buffer);
+ const outputLength = decoder.HEAP32[result >> 2];
+ const outputAddr = decoder.HEAP32[(result + 4) >> 2];
+ const output = new Uint8Array(outputLength);
+ const outputSrc = new Uint8Array(decoder.HEAP8.buffer);
+ output.set(outputSrc.slice(outputAddr, outputAddr + outputLength));
+ decoder._jxlCleanup(result);
+ const response = {uid: job.uid, data: output, msg: msg};
+ postMessage(response, [output.buffer]);
+ }
+};
+
+onmessage = function(event) {
+ const data = event.data;
+ console.log('CW received: ' + data.op);
+ if (data.op === 'decodeJxl') {
+ let job = null;
+ for (let i = 0; i < jobs.length; ++i) {
+ if (jobs[i].uid === data.uid) {
+ job = jobs[i];
+ break;
+ }
+ }
+ if (!job) {
+ job = {uid: data.uid, input: [], inputComplete: false, url: data.url};
+ jobs.push(job);
+ }
+ if (data.data) {
+ job.input.push(data.data);
+ } else {
+ job.inputComplete = true;
+ }
+ processJobs();
+ }
+};
+
+const onLoadJxlModule = (instance) => {
+ decoder = instance;
+ processJobs();
+};
+
+importScripts('jxl_decoder.js');
+const config = {
+ mainScriptUrlOrBlob: 'https://jxl-demo.netlify.app/jxl_decoder.js',
+ INITIAL_MEMORY: 16 * 1024 * 1024,
+};
+JxlDecoderModule(config).then(onLoadJxlModule);
diff --git a/tools/jxl_emcc.cc b/tools/wasm_demo/jxl_decoder.cc
index c4c855a..633674c 100644
--- a/tools/jxl_emcc.cc
+++ b/tools/wasm_demo/jxl_decoder.cc
@@ -3,25 +3,25 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include "tools/wasm_demo/jxl_decoder.h"
+
+#include <jxl/decode.h>
+#include <jxl/decode_cxx.h>
+#include <jxl/thread_parallel_runner_cxx.h>
+
+#include <cstdio>
#include <cstring>
#include <memory>
#include <vector>
-#include "jxl/decode.h"
-#include "jxl/decode_cxx.h"
-#include "jxl/thread_parallel_runner_cxx.h"
-
-#if !defined(__wasm__)
-#include "lib/jxl/base/file_io.h"
-#endif
+extern "C" {
namespace {
-struct DecoderInstance {
- uint32_t width = 0;
- uint32_t height = 0;
- uint8_t* pixels = nullptr;
- uint32_t color_space = 0;
+struct DecoderInstancePrivate {
+ // Due to "Standard Layout" rules it is guaranteed that address of the entity
+ // and its first non-static member are the same.
+ DecoderInstance info;
size_t pixels_size = 0;
bool want_sdr;
@@ -35,26 +35,29 @@ struct DecoderInstance {
} // namespace
-extern "C" {
+DecoderInstance* jxlCreateInstance(bool want_sdr, uint32_t display_nits) {
+ DecoderInstancePrivate* self = new DecoderInstancePrivate();
-void* jxlCreateInstance(bool want_sdr, uint32_t display_nits) {
- DecoderInstance* instance = new DecoderInstance();
- instance->want_sdr = want_sdr;
- instance->display_nits = display_nits;
+ if (!self) {
+ return nullptr;
+ }
+
+ self->want_sdr = want_sdr;
+ self->display_nits = display_nits;
JxlDataType storageFormat = want_sdr ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16;
- instance->format = {4, storageFormat, JXL_NATIVE_ENDIAN, 0};
- instance->decoder = JxlDecoderMake(nullptr);
+ self->format = {4, storageFormat, JXL_NATIVE_ENDIAN, 0};
+ self->decoder = JxlDecoderMake(nullptr);
- JxlDecoder* dec = instance->decoder.get();
+ JxlDecoder* dec = self->decoder.get();
auto report_error = [&](uint32_t code, const char* text) {
fprintf(stderr, "%s\n", text);
- // instance->result = code;
- return instance;
+ delete self;
+ return reinterpret_cast<DecoderInstance*>(code);
};
- instance->thread_pool = JxlThreadParallelRunnerMake(nullptr, 4);
- void* runner = instance->thread_pool.get();
+ self->thread_pool = JxlThreadParallelRunnerMake(nullptr, 4);
+ void* runner = self->thread_pool.get();
auto status =
JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner);
@@ -74,33 +77,32 @@ void* jxlCreateInstance(bool want_sdr, uint32_t display_nits) {
if (JXL_DEC_SUCCESS != status) {
return report_error(3, "JxlDecoderSetProgressiveDetail failed");
}
- return instance;
+ return &self->info;
}
-void jxlDestroyInstance(void* opaque_instance) {
- if (opaque_instance == nullptr) return;
- DecoderInstance* instance =
- reinterpret_cast<DecoderInstance*>(opaque_instance);
+void jxlDestroyInstance(DecoderInstance* instance) {
+ if (instance == nullptr) return;
+ DecoderInstancePrivate* self =
+ reinterpret_cast<DecoderInstancePrivate*>(instance);
if (instance->pixels) {
free(instance->pixels);
}
- delete instance;
+ delete self;
}
-uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input,
+uint32_t jxlProcessInput(DecoderInstance* instance, const uint8_t* input,
size_t input_size) {
- if (opaque_instance == nullptr) return static_cast<uint32_t>(-1);
- DecoderInstance* instance =
- reinterpret_cast<DecoderInstance*>(opaque_instance);
- JxlDecoder* dec = instance->decoder.get();
+ if (instance == nullptr) return static_cast<uint32_t>(-1);
+ DecoderInstancePrivate* self =
+ reinterpret_cast<DecoderInstancePrivate*>(instance);
+ JxlDecoder* dec = self->decoder.get();
auto report_error = [&](int code, const char* text) {
fprintf(stderr, "%s\n", text);
- // instance->result = code;
return static_cast<uint32_t>(code);
};
- std::vector<uint8_t>& tail = instance->tail;
+ std::vector<uint8_t>& tail = self->tail;
if (!tail.empty()) {
tail.reserve(tail.size() + input_size);
tail.insert(tail.end(), input, input + input_size);
@@ -152,8 +154,8 @@ uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input,
}
instance->width = info.xsize;
instance->height = info.ysize;
- status = JxlDecoderImageOutBufferSize(dec, &instance->format,
- &instance->pixels_size);
+ status =
+ JxlDecoderImageOutBufferSize(dec, &self->format, &self->pixels_size);
if (status != JXL_DEC_SUCCESS) {
release_input();
return report_error(-6, "JxlDecoderImageOutBufferSize failed");
@@ -162,15 +164,14 @@ uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input,
release_input();
return report_error(-7, "Tried to realloc pixels");
}
- instance->pixels =
- reinterpret_cast<uint8_t*>(malloc(instance->pixels_size));
+ instance->pixels = reinterpret_cast<uint8_t*>(malloc(self->pixels_size));
} else if (JXL_DEC_NEED_IMAGE_OUT_BUFFER == status) {
- if (!instance->pixels) {
+ if (!self->info.pixels) {
release_input();
return report_error(-8, "Out buffer not allocated");
}
- status = JxlDecoderSetImageOutBuffer(
- dec, &instance->format, instance->pixels, instance->pixels_size);
+ status = JxlDecoderSetImageOutBuffer(dec, &self->format, instance->pixels,
+ self->pixels_size);
if (status != JXL_DEC_SUCCESS) {
release_input();
return report_error(-9, "JxlDecoderSetImageOutBuffer failed");
@@ -180,8 +181,8 @@ uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input,
color_encoding.color_space = JXL_COLOR_SPACE_RGB;
color_encoding.white_point = JXL_WHITE_POINT_D65;
color_encoding.primaries =
- instance->want_sdr ? JXL_PRIMARIES_SRGB : JXL_PRIMARIES_2100;
- color_encoding.transfer_function = instance->want_sdr
+ self->want_sdr ? JXL_PRIMARIES_SRGB : JXL_PRIMARIES_2100;
+ color_encoding.transfer_function = self->want_sdr
? JXL_TRANSFER_FUNCTION_SRGB
: JXL_TRANSFER_FUNCTION_PQ;
color_encoding.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL;
@@ -200,15 +201,15 @@ uint32_t jxlProcessInput(void* opaque_instance, const uint8_t* input,
return 0;
}
-uint32_t jxlFlush(void* opaque_instance) {
- if (opaque_instance == nullptr) return static_cast<uint32_t>(-1);
- DecoderInstance* instance =
- reinterpret_cast<DecoderInstance*>(opaque_instance);
- JxlDecoder* dec = instance->decoder.get();
+uint32_t jxlFlush(DecoderInstance* instance) {
+ if (instance == nullptr) return static_cast<uint32_t>(-1);
+ DecoderInstancePrivate* self =
+ reinterpret_cast<DecoderInstancePrivate*>(instance);
+ JxlDecoder* dec = self->decoder.get();
auto report_error = [&](int code, const char* text) {
fprintf(stderr, "%s\n", text);
- // instance->result = code;
+ // self->result = code;
return static_cast<uint32_t>(code);
};
@@ -224,20 +225,4 @@ uint32_t jxlFlush(void* opaque_instance) {
return 0;
}
-#if !defined(__wasm__)
-int main(int argc, char* argv[]) {
- std::vector<uint8_t> data;
- JXL_RETURN_IF_ERROR(jxl::ReadFile(argv[1], &data));
- fprintf(stderr, "File size: %d\n", (int)data.size());
-
- void* instance = jxlCreateInstance(true, 100);
- uint32_t status = jxlProcessInput(instance, data.data(), data.size());
- fprintf(stderr, "Process result: %d\n", status);
- jxlFlush(instance);
- status = jxlProcessInput(instance, nullptr, 0);
- fprintf(stderr, "Process result: %d\n", status);
- jxlDestroyInstance(instance);
-}
-#endif
-
} // extern "C"
diff --git a/tools/wasm_demo/jxl_decoder.h b/tools/wasm_demo/jxl_decoder.h
new file mode 100644
index 0000000..ad6d88e
--- /dev/null
+++ b/tools/wasm_demo/jxl_decoder.h
@@ -0,0 +1,48 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_WASM_DEMO_JXL_DECODER_H_
+#define TOOLS_WASM_DEMO_JXL_DECODER_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+extern "C" {
+
+typedef struct DecoderInstance {
+ uint32_t width = 0;
+ uint32_t height = 0;
+ uint8_t* pixels = nullptr;
+
+ // The rest is opaque.
+} DecoderInstance;
+
+/*
+ Returns (as uint32_t):
+ 0 - OOM
+ 1 - JxlDecoderSetParallelRunner failed
+ 2 - JxlDecoderSubscribeEvents failed
+ 3 - JxlDecoderSetProgressiveDetail failed
+ >=4 - OK
+ */
+DecoderInstance* jxlCreateInstance(bool want_sdr, uint32_t display_nits);
+
+void jxlDestroyInstance(DecoderInstance* instance);
+
+/*
+ Returns (as uint32_t):
+ 0 - OK (pixels are ready)
+ 1 - ready to flush
+ 2 - needs more input
+ >=3 - error
+ */
+uint32_t jxlProcessInput(DecoderInstance* instance, const uint8_t* input,
+ size_t input_size);
+
+uint32_t jxlFlush(DecoderInstance* instance);
+
+} // extern "C"
+
+#endif // TOOLS_WASM_DEMO_JXL_DECODER_H_
diff --git a/tools/wasm_demo/jxl_decoder_test.js b/tools/wasm_demo/jxl_decoder_test.js
new file mode 100644
index 0000000..22dfa07
--- /dev/null
+++ b/tools/wasm_demo/jxl_decoder_test.js
@@ -0,0 +1,140 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+function assertTrue(ok, msg) {
+ if (!ok) {
+ console.log('FAIL: ' + msg);
+ process.exit(1);
+ }
+}
+
+function runTest(testFn) {
+ console.log('Running ' + testFn.name);
+ testFn();
+ console.log('PASS');
+}
+
+let jxlModule;
+
+const isAddress = (v) => {
+ return (v >= 4) && ((v & (1 << 31)) === 0);
+};
+
+let splinesJxl = new Uint8Array([
+ 0xff, 0x0a, 0xf8, 0x19, 0x10, 0x09, 0xd8, 0x63, 0x10, 0x00, 0xbc, 0x00,
+ 0xa6, 0x19, 0x4a, 0xa3, 0x56, 0x8c, 0x94, 0x62, 0x24, 0x7d, 0x12, 0x72,
+ 0x87, 0x00, 0x00, 0xda, 0xd4, 0xc9, 0xc1, 0xe2, 0x9e, 0x02, 0xb9, 0x37,
+ 0x00, 0xfe, 0x07, 0x9a, 0x91, 0x08, 0xcd, 0xbf, 0xa1, 0xdc, 0x71, 0x36,
+ 0x62, 0xc8, 0x97, 0x31, 0xc4, 0x3e, 0x58, 0x02, 0xc1, 0x01, 0x00
+]);
+
+let crossJxl = new Uint8Array([
+ 0xff, 0x0a, 0x98, 0x10, 0x10, 0x50, 0x5c, 0x08, 0x08, 0x02, 0x01,
+ 0x00, 0x98, 0x00, 0x4b, 0x18, 0x8b, 0x15, 0x00, 0xd4, 0x92, 0x62,
+ 0xcc, 0x98, 0x91, 0x17, 0x08, 0x01, 0xe0, 0x92, 0xbc, 0x7e, 0xdf,
+ 0xbf, 0xff, 0x50, 0xc0, 0x64, 0x35, 0xb0, 0x40, 0x1e, 0x24, 0xa9,
+ 0xac, 0x38, 0xd9, 0x13, 0x1e, 0x85, 0x4a, 0x0d
+]);
+
+function testSdr() {
+ let decoder = jxlModule._jxlCreateInstance(
+ /* wantSdr */ true, /* displayNits */ 100);
+ assertTrue(isAddress(decoder), 'create decoder instance');
+ let encoded = splinesJxl;
+ let buffer = jxlModule._malloc(encoded.length);
+ jxlModule.HEAP8.set(encoded, buffer);
+
+ let result = jxlModule._jxlProcessInput(decoder, buffer, encoded.length);
+ assertTrue(result === 0, 'process input');
+
+ let w = jxlModule.HEAP32[decoder >> 2];
+ let h = jxlModule.HEAP32[(decoder + 4) >> 2];
+ let pixelData = jxlModule.HEAP32[(decoder + 8) >> 2];
+
+ assertTrue(pixelData, 'output allocated');
+ assertTrue(h === 320, 'output height');
+ assertTrue(w === 320, 'output width ');
+
+ jxlModule._jxlDestroyInstance(decoder);
+ jxlModule._free(buffer);
+}
+
+function testRegular() {
+ let decoder = jxlModule._jxlCreateInstance(
+ /* wantSdr */ false, /* displayNits */ 100);
+ assertTrue(isAddress(decoder), 'create decoder instance');
+ let encoded = splinesJxl;
+ let buffer = jxlModule._malloc(encoded.length);
+ jxlModule.HEAP8.set(encoded, buffer);
+
+ let result = jxlModule._jxlProcessInput(decoder, buffer, encoded.length);
+ assertTrue(result === 0, 'process input');
+
+ let w = jxlModule.HEAP32[decoder >> 2];
+ let h = jxlModule.HEAP32[(decoder + 4) >> 2];
+ let pixelData = jxlModule.HEAP32[(decoder + 8) >> 2];
+
+ assertTrue(pixelData, 'output allocated');
+ assertTrue(h === 320, 'output height');
+ assertTrue(w === 320, 'output width ');
+
+ jxlModule._jxlDestroyInstance(decoder);
+ jxlModule._free(buffer);
+}
+
+function testChunks() {
+ let decoder = jxlModule._jxlCreateInstance(
+ /* wantSdr */ false, /* displayNits */ 100);
+ assertTrue(isAddress(decoder), 'create decoder instance');
+ let encoded = splinesJxl;
+ let buffer = jxlModule._malloc(encoded.length);
+ jxlModule.HEAP8.set(encoded, buffer);
+
+ let part1_length = encoded.length >> 1;
+ let part2_length = encoded.length - part1_length;
+
+ let result = jxlModule._jxlProcessInput(decoder, buffer, part1_length);
+ assertTrue(result === 2, 'process first part');
+
+ result =
+ jxlModule._jxlProcessInput(decoder, buffer + part1_length, part2_length);
+ assertTrue(result === 0, 'process second part');
+
+ let w = jxlModule.HEAP32[decoder >> 2];
+ let h = jxlModule.HEAP32[(decoder + 4) >> 2];
+ let pixelData = jxlModule.HEAP32[(decoder + 8) >> 2];
+
+ assertTrue(pixelData, 'output allocated');
+ assertTrue(h === 320, 'output height');
+ assertTrue(w === 320, 'output width ');
+
+ jxlModule._jxlDestroyInstance(decoder);
+ jxlModule._free(buffer);
+}
+
+function testDecompress() {
+ let encoded = crossJxl;
+ let buffer = jxlModule._malloc(encoded.length);
+ jxlModule.HEAP8.set(encoded, buffer);
+
+ let output = jxlModule._jxlDecompress(buffer, encoded.length);
+ assertTrue(isAddress(output), 'decompress');
+
+ jxlModule._free(buffer);
+
+ let pngSize = jxlModule.HEAP32[output >> 2];
+ let px = 20 * 20;
+ assertTrue(pngSize >= 6 * px, 'png size');
+ assertTrue(pngSize <= 6 * px + 800, 'png size');
+
+ jxlModule._jxlCleanup(output);
+}
+
+require('jxl_decoder_for_test.js')().then(module => {
+ jxlModule = module;
+ let tests = [testSdr, testRegular, testChunks, testDecompress];
+ tests.forEach(runTest);
+ process.exit(0);
+});
diff --git a/tools/wasm_demo/jxl_decompressor.cc b/tools/wasm_demo/jxl_decompressor.cc
new file mode 100644
index 0000000..648e1ef
--- /dev/null
+++ b/tools/wasm_demo/jxl_decompressor.cc
@@ -0,0 +1,117 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/wasm_demo/jxl_decompressor.h"
+
+#include <jxl/thread_parallel_runner_cxx.h>
+
+#include <cstring>
+#include <memory>
+
+#include "lib/extras/dec/jxl.h"
+#include "tools/wasm_demo/no_png.h"
+
+extern "C" {
+
+namespace {
+
+struct DecompressorOutputPrivate {
+ // Due to "Standard Layout" rules it is guaranteed that address of the entity
+ // and its first non-static member are the same.
+ DecompressorOutput output;
+};
+
+void MaybeMakeCicp(const jxl::extras::PackedPixelFile& ppf,
+ std::vector<uint8_t>* cicp) {
+ cicp->clear();
+ const JxlColorEncoding& clr = ppf.color_encoding;
+ uint8_t color_primaries = 0;
+ uint8_t transfer_function = static_cast<uint8_t>(clr.transfer_function);
+
+ if (clr.color_space != JXL_COLOR_SPACE_RGB) {
+ return;
+ }
+ if (clr.primaries == JXL_PRIMARIES_P3) {
+ if (clr.white_point == JXL_WHITE_POINT_D65) {
+ color_primaries = 12;
+ } else if (clr.white_point == JXL_WHITE_POINT_DCI) {
+ color_primaries = 11;
+ } else {
+ return;
+ }
+ } else if (clr.primaries != JXL_PRIMARIES_CUSTOM &&
+ clr.white_point == JXL_WHITE_POINT_D65) {
+ color_primaries = static_cast<uint8_t>(clr.primaries);
+ } else {
+ return;
+ }
+ if (clr.transfer_function == JXL_TRANSFER_FUNCTION_UNKNOWN ||
+ clr.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) {
+ return;
+ }
+
+ cicp->resize(4);
+ cicp->at(0) = color_primaries; // Colour Primaries
+ cicp->at(1) = transfer_function; // Transfer Function
+ cicp->at(2) = 0; // Matrix Coefficients
+ cicp->at(3) = 1; // Video Full Range Flag
+}
+
+} // namespace
+
+DecompressorOutput* jxlDecompress(const uint8_t* input, size_t input_size) {
+ DecompressorOutputPrivate* self = new DecompressorOutputPrivate();
+
+ if (!self) {
+ return nullptr;
+ }
+
+ auto report_error = [&](uint32_t code, const char* text) {
+ fprintf(stderr, "%s\n", text);
+ delete self;
+ return reinterpret_cast<DecompressorOutput*>(code);
+ };
+
+ auto thread_pool = JxlThreadParallelRunnerMake(nullptr, 4);
+ void* runner = thread_pool.get();
+
+ jxl::extras::JXLDecompressParams dparams;
+ JxlPixelFormat format = {/* num_channels */ 3, JXL_TYPE_UINT16,
+ JXL_BIG_ENDIAN, /* align */ 0};
+ dparams.accepted_formats.push_back(format);
+ dparams.runner = JxlThreadParallelRunner;
+ dparams.runner_opaque = runner;
+ jxl::extras::PackedPixelFile ppf;
+
+ if (!jxl::extras::DecodeImageJXL(input, input_size, dparams, nullptr, &ppf)) {
+ return report_error(1, "failed to decode jxl");
+ }
+
+ // Just 1-st frame.
+ const auto& image = ppf.frames[0].color;
+ std::vector<uint8_t> cicp;
+ MaybeMakeCicp(ppf, &cicp);
+ self->output.data = WrapPixelsToPng(
+ image.xsize, image.ysize, (format.data_type == JXL_TYPE_UINT16) ? 16 : 8,
+ /* has_alpha */ false, reinterpret_cast<const uint8_t*>(image.pixels()),
+ ppf.icc, cicp, &self->output.size);
+ if (!self->output.data) {
+ return report_error(2, "failed to encode png");
+ }
+
+ return &self->output;
+}
+
+void jxlCleanup(DecompressorOutput* output) {
+ if (output == nullptr) return;
+ DecompressorOutputPrivate* self =
+ reinterpret_cast<DecompressorOutputPrivate*>(output);
+ if (self->output.data) {
+ free(self->output.data);
+ }
+ delete self;
+}
+
+} // extern "C"
diff --git a/tools/wasm_demo/jxl_decompressor.h b/tools/wasm_demo/jxl_decompressor.h
new file mode 100644
index 0000000..2ba16a0
--- /dev/null
+++ b/tools/wasm_demo/jxl_decompressor.h
@@ -0,0 +1,34 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_WASM_DEMO_JXL_DECOMPRESSOR_H_
+#define TOOLS_WASM_DEMO_JXL_DECOMPRESSOR_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+extern "C" {
+
+typedef struct DecompressorOutput {
+ uint32_t size = 0;
+ uint8_t* data = nullptr;
+
+ // The rest is opaque.
+} DecompressorOutput;
+
+/*
+ Returns (as uint32_t):
+ 0 - OOM
+ 1 - decoding JXL failed
+ 2 - encoding PNG failed
+ >=4 - OK
+ */
+DecompressorOutput* jxlDecompress(const uint8_t* input, size_t input_size);
+
+void jxlCleanup(DecompressorOutput* output);
+
+} // extern "C"
+
+#endif // TOOLS_WASM_DEMO_JXL_DECOMPRESSOR_H_
diff --git a/tools/wasm_demo/manual_decode_demo.html b/tools/wasm_demo/manual_decode_demo.html
new file mode 100644
index 0000000..e11aed6
--- /dev/null
+++ b/tools/wasm_demo/manual_decode_demo.html
@@ -0,0 +1,340 @@
+<html>
+<head>
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
+ <style>
+#log p {
+ margin: 0;
+}
+ </style>
+</head>
+<body>
+<div id="log" style="padding:2px; border: solid 1px #000; background-color: #ccc; margin:2px; height: 8em; font-family: monospace; overflow-y: auto; font-size: 8px;"></div>
+<script>
+// WASM module.
+let jxlModule = null;
+// Flag; if true, then HDR color space / 16 bit output is supported.
+let hdrCanvas = false;
+
+// Add message to "console".
+let addMessage = (text, color) => {
+ let log = document.getElementById('log');
+ let message = document.createElement('p');
+ message.style = 'color: ' + color + ';';
+ message.textContent = text;
+ log.append(message);
+ log.scrollTop = log.scrollHeight;
+}
+
+// Callback from WASM module when it becomes available.
+let onLoadJxlModule = (module) => {
+ jxlModule = module;
+ addMessage('WASM module loaded', 'black');
+ onJxlModuleReady();
+};
+
+// Check if multi-threading is supported (i.e. SharedArrayBuffer is allowed).
+let probeMutlithreading = () => {
+ try {
+ new SharedArrayBuffer();
+ return true;
+ } catch (ex) {
+ addMessage('Installing Service Worker, please wait...', 'orange');
+ return false;
+ }
+};
+
+// Check if HDR features are enabled.
+let probeHdr = () => {
+ addMessage('Probing HDR features', 'black');
+ try {
+ let tmpCanvas = document.createElement('canvas');
+ tmpCanvas.width = 1;
+ tmpCanvas.height = 1;
+ let ctx = tmpCanvas.getContext('2d', {colorSpace: 'rec2100-pq', pixelFormat: 'float16'});
+ // make it fail on firefox...
+ ctx.getContextAttributes();
+ addMessage('HDR canvas supported', 'green');
+ return true;
+ } catch (ex) {
+ addMessage(ex, 'red');
+ addMessage('Are Blink experiments enabled? about://flags/#enable-experimental-web-platform-features', 'blue');
+ return false;
+ }
+};
+
+// "main" method executed after page is loaded; all scripts are "synchronous" elements,
+// so it is guaranted that script elements are loaded and executed.
+let onDomContentLoaded = () => {
+ if (!probeMutlithreading()) return;
+ hdrCanvas = probeHdr();
+ JxlDecoderModule().then(onLoadJxlModule);
+};
+
+// Pass next chunk to decoder and interprets result.
+let processInput = (img, chunkLen) => {
+ let response = {
+ wantFlush: false,
+ copyPixels: false,
+ error: false,
+ }
+ do {
+ let t0 = performance.now();
+ let result = jxlModule._jxlProcessInput(img.decoder, img.buffer, chunkLen);
+ let t1 = performance.now();
+ let tProcessing = t1 - t0;
+ // addMessage('Processed chunk in ' + tProcessing + 'ms', 'blue');
+ img.totalProcessing += tProcessing;
+ // addMessage('Process result: ' + result, 'green');
+ if (result === 2) {
+ addMessage('Needs more input', 'gray');
+ } else if (result === 0) {
+ // addMessage('Image ready', 'gray');
+ response.wantFlush = false;
+ response.copyPixels = true;
+ } else if (result === 1) {
+ if (img.wantProgressive) {
+ addMessage('DC ready', 'gray');
+ response.wantFlush = true;
+ response.copyPixels = true;
+ } else {
+ // addMessage('Skipping DC flush', 'gray');
+ chunkLen = 0;
+ continue;
+ }
+ } else {
+ addMessage('Processing error', 'red');
+ img.broken = true;
+ response.error = true;
+ break;
+ }
+ break;
+ } while (true);
+ return response;
+}
+
+// Decode chunk and present results (dump to canvas).
+let processChunk = (img, chunkLen) => {
+ let result = processInput(img, chunkLen);
+ if (result.error) return;
+
+ if (result.wantFlush) {
+ let t2 = performance.now();
+ let flushResult = jxlModule._jxlFlush(img.decoder);
+ let t3 = performance.now();
+ let tFlushing = t3 - t2;
+ addMessage('Flush result: ' + flushResult, 'gray');
+ img.totalFlushing += tFlushing;
+ }
+
+ if (!result.copyPixels) return;
+
+ let w = jxlModule.HEAP32[img.decoder >> 2];
+ let h = jxlModule.HEAP32[(img.decoder + 4) >> 2];
+ let pixelData = jxlModule.HEAP32[(img.decoder + 8) >> 2];
+ if (!img.canvas) {
+ img.canvas = document.createElement('canvas');
+ img.canvas.width = w;
+ img.canvas.height = h;
+ img.canvas.style = 'width:100%';
+ // TODO(eustas): postpone until really flushed
+ document.body.appendChild(img.canvas);
+ let ctxOptions = {colorSpace: img.colorSpace, pixelFormat: 'float16'};
+ let pixelOptions = {colorSpace: img.colorSpace, storageFormat: 'uint16'};
+ if (img.wantSdr) {
+ ctxOptions = null;
+ pixelOptions = null;
+ }
+ img.canvasCtx = img.canvas.getContext('2d', ctxOptions);
+ img.pixels = img.canvasCtx.getImageData(0, 0, w, h, pixelOptions);
+ }
+
+ let src = null;
+ let start = pixelData;
+ if (img.wantSdr) {
+ src = new Uint8Array(jxlModule.HEAP8.buffer);
+ } else {
+ src = new Uint16Array(jxlModule.HEAP8.buffer);
+ start = start >> 1;
+ }
+ let end = start + w * h * 4;
+ img.pixels.data.set(src.slice(start, end));
+ img.canvasCtx.putImageData(img.pixels, 0, 0);
+};
+
+const BUF_LEN = 150 * 1024;
+
+// Image data cache for benchmarking.
+let fullImage = new Uint8Array(0);
+
+// Callback for fetch data.
+let onChunk = (img, chunk) => {
+ if (chunk.done) {
+ addMessage('Read finished | total processing: ' + img.totalProcessing.toFixed(1) + 'ms | total flushing ' + img.totalFlushing.toFixed(1) + 'ms', 'black');
+ cleanup(img);
+ img.onComplete(img);
+ return;
+ }
+ if (img.broken) return;
+
+ if (!img.decoder) {
+ let decoder = jxlModule._jxlCreateInstance(img.wantSdr, img.displayNits);
+ if (decoder < 4) {
+ img.broken = true;
+ cleanup(img);
+ addMessage('Failed to create decoder instance', 'red');
+ return;
+ }
+ img.decoder = decoder;
+ img.buffer = jxlModule._malloc(BUF_LEN);
+ }
+
+ // addMessage('Received chunk: ' + chunk.value.length, 'gray');
+ let newFullImage = new Uint8Array(fullImage.length + chunk.value.length);
+ newFullImage.set(fullImage);
+ newFullImage.set(chunk.value, fullImage.length);
+ fullImage = newFullImage;
+
+ let offset = 0;
+ while (offset < chunk.value.length) {
+ let delta = chunk.value.length - offset;
+ if (delta > BUF_LEN) delta = BUF_LEN;
+ jxlModule.HEAP8.set(chunk.value.slice(offset, offset + delta), img.buffer);
+ offset += delta;
+ processChunk(img, delta);
+ if (img.broken) {
+ return;
+ }
+ }
+
+ // Break the promise chain.
+ setTimeout(img.proceed, 0);
+};
+
+// Read next chunk; NB: used to break promise chain.
+let proceed = (img) => {
+ img.reader.read().then(img.onChunk, img.onReadError);
+};
+
+// Release (in-module) memory resources.
+let cleanup = (img) => {
+ if (img.decoder) {
+ jxlModule._jxlDestroyInstance(img.decoder);
+ img.decoder = 0;
+ }
+ if (img.buffer) {
+ jxlModule._free(img.buffer);
+ img.buffer = 0;
+ }
+};
+
+// Report error and cleanup.
+let onReadError = (img, error) => {
+ img.broken = true;
+ cleanup(img);
+ addMessage('Read failed: ' + error, 'red');
+};
+
+// On successful fetch start.
+let onResponse = (img, response) => {
+ if (!response.ok) {
+ addMessage('Fetch failed: ' + response.status + ' (' + response.statusText + ')');
+ return;
+ }
+ // Alas, not supported by fetch:
+ // let reader = response.body.getReader({mode: "byob"});
+ img.reader = response.body.getReader();
+
+ img.proceed();
+};
+
+// On image decoding completion.
+let onComplete = (img) => {
+ if (!img.runBenchmark) return;
+
+ let buffer = jxlModule._malloc(fullImage.length);
+ jxlModule.HEAP8.set(fullImage, buffer);
+ img.buffer = buffer;
+ let results = [];
+
+ for (let i = 0; i < img.runBenchmark; ++i) {
+ img.totalProcessing = 0;
+ img.decoder = jxlModule._jxlCreateInstance(img.wantSdr, img.displayNits);
+ processChunk(img, fullImage.length);
+ jxlModule._jxlDestroyInstance(img.decoder);
+ results.push(img.totalProcessing);
+ //addMessage('Decoding time: ' + img.totalProcessing + 'ms', 'black');
+ }
+
+ results.sort();
+ addMessage('Min decoding time: ' + results[0].toFixed(3) + 'ms', 'black');
+ addMessage('Median decoding time: ' + results[results.length >> 1].toFixed(3) + 'ms', 'black');
+ addMessage('Max decoding time: ' + results[results.length - 1].toFixed(3) + 'ms', 'black');
+
+ jxlModule._free(buffer);
+};
+
+// Fill cookie object template.
+let makeImg = () => {
+ return {
+ name: '',
+ colorSpace: 'rec2100-pq',
+ wantSdr: false,
+ displayNits: 100,
+ broken: false,
+ decoder: 0,
+ canvas: null,
+ canvasCtx: null,
+ pixels: null,
+ buffer: 0,
+ wantProgressive: false,
+ onlyDecode: false,
+ totalProcessing: 0,
+ totalFlushing: 0,
+ runBenchmark: 0,
+ onChunk: () => {},
+ onReadError: () => {},
+ proceed: () => {},
+ onComplete: () => {},
+ };
+}
+
+// Parse URL query and run image decoding / benchmarking.
+let onJxlModuleReady = () => {
+ let params = (new URL(document.location)).searchParams;
+ const images = ['image00.jxl', 'image01.jxl'];
+ let imgIdx = (params.get('img') | 0) % images.length;
+ let imgName = images[imgIdx];
+
+ let colorSpace = params.get('colorSpace') || 'srgb';
+ let wantSdr = params.get('wantSdr') == 'true';
+ let displayNits = parseInt(params.get('displayNits') || '0');
+ let runBenchmark = parseInt(params.get('runBenchmark') || '0');
+
+ if (!hdrCanvas) {
+ colorSpace = 'srgb-linear';
+ displayNits = displayNits || 100;
+ wantSdr = true;
+ }
+
+ addMessage('Color-space: "' + colorSpace + '", tone-map to SDR: ' + wantSdr + ', displayNits: ' + (displayNits || 'n/a'), 'black');
+
+ let img = makeImg();
+ img.name = imgName;
+ img.colorSpace = colorSpace;
+ img.wantSdr = wantSdr;
+ img.displayNits = displayNits;
+ img.onChunk = onChunk.bind(null, img);
+ img.onReadError = onReadError.bind(null, img);
+ img.proceed = proceed.bind(null, img);
+ img.onComplete = onComplete.bind(null, img);
+ img.runBenchmark = runBenchmark;
+
+ fetch(new Request(imgName, {cache: "no-store"})).then(onResponse.bind(null, img));
+};
+
+document.addEventListener('DOMContentLoaded', onDomContentLoaded);
+</script>
+
+<script src="jxl_decoder.js"></script>
+</body>
+</html>
diff --git a/tools/wasm_demo/netlify.toml b/tools/wasm_demo/netlify.toml
new file mode 100644
index 0000000..44d9d56
--- /dev/null
+++ b/tools/wasm_demo/netlify.toml
@@ -0,0 +1,19 @@
+# Copyright (c) the JPEG XL Project Authors. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# We use "edge functions" feature to substitute response with pre-compressed
+# entries whenever those are available and browser supports Brotli or Gzip
+# content-encoding.
+[[edge_functions]]
+path = "/*"
+function = "precompressed"
+
+# Request browser "site-isolation" enabled.
+# This allows using "SharedArrayBuffers" required for multi-threaded WASM.
+[[headers]]
+for = "/*"
+ [headers.values]
+ Cross-Origin-Opener-Policy = "same-origin"
+ Cross-Origin-Embedder-Policy = "require-corp"
diff --git a/tools/wasm_demo/netlify/edge-functions/precompressed.ts b/tools/wasm_demo/netlify/edge-functions/precompressed.ts
new file mode 100644
index 0000000..c169432
--- /dev/null
+++ b/tools/wasm_demo/netlify/edge-functions/precompressed.ts
@@ -0,0 +1,87 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import type {Context} from 'netlify:edge';
+
+// This lambda is executed whenever request URL matches.
+export default async (request: Request, context: Context) => {
+ // Measure time for debugging purpose.
+ let t0 = Date.now();
+ // Get resource path (i.e. ignore query parameters).
+ let url = request.url.split('?')[0];
+ // Pick request headers; fallback to empty string if header is not set.
+ let acceptEncodingHeader = request.headers.get('Accept-Encoding') || '';
+ let acceptHeader = request.headers.get('Accept') || '';
+ let etag = request.headers.get('If-None-Match') || '';
+ // Roughly parse encodings list; this ignores "quality"; no modern browsers
+ // use it -> don't care.
+ let splitter = /[,;]/;
+ let supportedEncodings =
+ acceptEncodingHeader.split(splitter).map(v => v.trimStart());
+ let supportsBr = supportedEncodings.includes('br');
+ let supportedMedia = acceptHeader.split(splitter).map(v => v.trimStart());
+ let supportsJxl = supportedMedia.includes('image/jxl');
+ // Dump basic request info (we care about).
+ context.log(
+ 'URL: ' + url + '; acceptEncodingHeader: ' + acceptEncodingHeader +
+ '; supportsBr: ' + supportsBr + '; supportsJxl: ' + supportsJxl +
+ '; etag: ' + etag);
+
+ // If browser does not support Brotli/Jxl - just process request normally.
+
+ if (!supportsBr && !supportsJxl) {
+ return;
+ }
+
+ // Jxl processing is higher priority, because images are (usually) transferred
+ // with 'identity' content encoding.
+ let isJxlWorkflow = supportsJxl;
+ let suffix = isJxlWorkflow ? '.jxl' : '.br';
+
+ // Request pre-compressed resource (with a suffix).
+ let response = await context.rewrite(url + suffix);
+ context.log('Response status: ' + response.status);
+ // First latency checkpoint (as we synchronously wait for resource fetch).
+ let t1 = Date.now();
+ // If pre-compressed resource does not exist - pass.
+ if (response.status == 404) {
+ return;
+ }
+ // Get resource ETag.
+ let responseEtag = response.headers.get('ETag') || '';
+ context.log('Response etag: ' + responseEtag);
+ // We rely on platform to check ETag; add debugging info just in case.
+ if (etag.length >= 4 && responseEtag === etag) {
+ console.log('Match; status: ' + response.status);
+ }
+ // Status 200 is regular "OK" - fetch resource; in such a case we need to
+ // craft response with the response contents.
+ // Status 3xx likely means "use cache"; pass response as is.
+ // Status 4xx is unlikely (404 has been already processed).
+ // Status 5xx is server error - nothing we could do around it.
+ if (response.status != 200) return response;
+ // Second time consuming operation - wait for resource contents.
+ let data = await response.arrayBuffer();
+ let fixedHeaders = new Headers(response.headers);
+
+ if (isJxlWorkflow) {
+ fixedHeaders.set('Content-Type', 'image/jxl');
+ } else { // is Brotli workflow
+ // Set "Content-Type" based on resource suffix;
+ // otherwise browser will complain.
+ let contentEncoding = 'text/html; charset=UTF-8';
+ if (url.endsWith('.js')) {
+ contentEncoding = 'application/javascript';
+ } else if (url.endsWith('.wasm')) {
+ contentEncoding = 'application/wasm';
+ }
+ fixedHeaders.set('Content-Type', contentEncoding);
+ // Inform browser that data stream is compressed.
+ fixedHeaders.set('Content-Encoding', 'br');
+ }
+ let t2 = Date.now();
+ console.log('Timing: ' + (t1 - t0) + ' ' + (t2 - t1));
+ return new Response(data, {headers: fixedHeaders});
+};
diff --git a/tools/wasm_demo/no_png.cc b/tools/wasm_demo/no_png.cc
new file mode 100644
index 0000000..01527d3
--- /dev/null
+++ b/tools/wasm_demo/no_png.cc
@@ -0,0 +1,220 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/wasm_demo/no_png.h"
+
+#include <array>
+#include <memory>
+
+extern "C" {
+
+namespace {
+
+static std::array<uint32_t, 256> makeCrc32Lut() {
+ std::array<uint32_t, 256> result;
+ for (uint32_t i = 0; i < 256; ++i) {
+ constexpr uint32_t poly = 0xEDB88320;
+ uint32_t v = i;
+ for (size_t i = 0; i < 8; ++i) {
+ uint32_t mask = ~((v & 1) - 1);
+ v = (v >> 1) ^ (poly & mask);
+ }
+ result[i] = v;
+ }
+ return result;
+}
+
+const std::array<uint32_t, 256> kCrc32Lut = makeCrc32Lut();
+
+const std::array<uint32_t, 8> kPngMagic = {137, 80, 78, 71, 13, 10, 26, 10};
+
+// No need to SIMDify it, only small blocks are actually checksummed.
+uint32_t CalculateCrc32(const uint8_t* start, const uint8_t* end) {
+ uint32_t result = ~0;
+ for (const uint8_t* data = start; data < end; ++data) {
+ result ^= *data;
+ result = (result >> 8) ^ kCrc32Lut[result & 0xFF];
+ }
+ return ~result;
+}
+
+void AdlerCopy(const uint8_t* src, uint8_t* dst, size_t length, uint32_t* s1,
+ uint32_t* s2) {
+ // TODO(eustas): SIMD-ify and use multithreading.
+
+ // Precondition: s1, s2 normalized; length <= 65535
+ uint32_t a = *s1;
+ uint32_t b = *s2;
+
+ for (size_t i = 0; i < length; ++i) {
+ const uint8_t v = src[i];
+ a += v;
+ b += a;
+ dst[i] = v;
+ }
+
+ // Postcondition: s1, s2 normalized.
+ *s1 = a % 65521;
+ *s2 = b % 65521;
+}
+
+constexpr size_t kMaxDeflateBlock = 65535;
+constexpr uint32_t kIhdrSize = 13;
+constexpr uint32_t kCicpSize = 4;
+
+void WriteU8(uint8_t*& dst, uint8_t value) { *(dst++) = value; }
+
+void WriteU16(uint8_t*& dst, uint16_t value) {
+ memcpy(dst, &value, 2);
+ dst += 2;
+}
+
+void WriteU32(uint8_t*& dst, uint32_t value) {
+ memcpy(dst, &value, 4);
+ dst += 4;
+}
+
+void WriteU32BE(uint8_t*& dst, uint32_t value) {
+ WriteU32(dst, __builtin_bswap32(value));
+}
+
+} // namespace
+
+uint8_t* WrapPixelsToPng(size_t width, size_t height, size_t bit_depth,
+ bool has_alpha, const uint8_t* input,
+ const std::vector<uint8_t>& icc,
+ const std::vector<uint8_t>& cicp,
+ uint32_t* output_size) {
+ size_t row_size = width * (bit_depth / 8) * (3 + has_alpha);
+ size_t data_size = height * (row_size + 1);
+ size_t num_deflate_blocks =
+ (data_size + kMaxDeflateBlock - 1) / kMaxDeflateBlock;
+ size_t idat_size = data_size + num_deflate_blocks * 5 + 6;
+ // 64k is enough for everyone
+ bool has_iccp = !icc.empty() && (icc.size() <= kMaxDeflateBlock);
+ size_t iccp_size = 3 + icc.size() + 5 + 6; // name + data + deflate-wrapping
+ bool has_cicp = (cicp.size() == kCicpSize);
+ size_t total_size = 0;
+ total_size += kPngMagic.size();
+ total_size += 12 + kIhdrSize;
+ total_size += has_cicp ? (kCicpSize + 12) : 0;
+ total_size += has_iccp ? (iccp_size + 12) : 0;
+ total_size += 12 + idat_size;
+ total_size += 12; // IEND
+
+ uint8_t* output = static_cast<uint8_t*>(malloc(total_size));
+ if (!output) {
+ return nullptr;
+ }
+ uint8_t* dst = output;
+ *output_size = total_size;
+
+ for (size_t i = 0; i < kPngMagic.size(); ++i) {
+ *(dst++) = kPngMagic[i];
+ }
+
+ // IHDR
+ WriteU32BE(dst, kIhdrSize);
+ uint8_t* chunk_start = dst;
+ WriteU32(dst, 0x52444849);
+ WriteU32BE(dst, width);
+ WriteU32BE(dst, height);
+ WriteU8(dst, bit_depth);
+ WriteU8(dst, has_alpha ? 6 : 2);
+ WriteU8(dst, 0); // compression: deflate
+ WriteU8(dst, 0); // filters: standard
+ WriteU8(dst, 0); // interlace: no
+ uint32_t crc32 = CalculateCrc32(chunk_start, dst);
+ WriteU32BE(dst, crc32);
+
+ if (has_cicp) {
+ // cICP
+ WriteU32BE(dst, kCicpSize);
+ uint8_t* chunk_start = dst;
+ WriteU32(dst, 0x50434963);
+ for (size_t i = 0; i < kCicpSize; ++i) {
+ WriteU8(dst, cicp[i]);
+ }
+ uint32_t crc32 = CalculateCrc32(chunk_start, dst);
+ WriteU32BE(dst, crc32);
+ }
+
+ if (has_iccp) {
+ // iCCP
+ WriteU32BE(dst, iccp_size);
+ uint8_t* chunk_start = dst;
+ WriteU32(dst, 0x50434369);
+ WriteU8(dst, '1'); // Profile name
+ WriteU8(dst, 0); // NUL terminator
+ WriteU8(dst, 0); // Compression method: deflate
+ WriteU8(dst, 0x08); // CM = 8 (deflate), CINFO = 0 (window size = 2**(0+8))
+ WriteU8(dst, 29); // FCHECK; (FCHECK + 256* CMF) % 31 = 0
+ uint32_t adler_s1 = 1;
+ uint32_t adler_s2 = 0;
+ WriteU8(dst, 1); // btype = 00 (uncompressed), last
+ uint16_t block_size = static_cast<uint16_t>(icc.size());
+ WriteU16(dst, block_size);
+ WriteU16(dst, ~block_size);
+ AdlerCopy(icc.data(), dst, block_size, &adler_s1, &adler_s2);
+ dst += block_size;
+ uint32_t adler = (adler_s2 << 8) | adler_s1;
+ WriteU32BE(dst, adler);
+ uint32_t crc32 = CalculateCrc32(chunk_start, dst);
+ WriteU32BE(dst, crc32);
+ }
+
+ // IDAT
+ WriteU32BE(dst, idat_size);
+ WriteU32(dst, 0x54414449);
+ size_t offset = 0;
+ size_t bytes_to_next_row = 0;
+ uint32_t adler_s1 = 1;
+ uint32_t adler_s2 = 0;
+ WriteU8(dst, 0x08); // CM = 8 (deflate), CINFO = 0 (window size = 2**(0+8))
+ WriteU8(dst, 29); // FCHECK; (FCHECK + 256* CMF) % 31 = 0
+ for (size_t i = 0; i < num_deflate_blocks; ++i) {
+ size_t block_size = data_size - offset;
+ if (block_size > kMaxDeflateBlock) {
+ block_size = kMaxDeflateBlock;
+ }
+ bool is_last = ((i + 1) == num_deflate_blocks);
+ WriteU8(dst, is_last); // btype = 00 (uncompressed)
+ offset += block_size;
+
+ WriteU16(dst, block_size);
+ WriteU16(dst, ~block_size);
+ while (block_size > 0) {
+ if (bytes_to_next_row == 0) {
+ WriteU8(dst, 0); // filter: raw
+ adler_s2 += adler_s1;
+ bytes_to_next_row = row_size;
+ block_size--;
+ continue;
+ }
+ size_t bytes_to_copy = std::min(block_size, bytes_to_next_row);
+ AdlerCopy(input, dst, bytes_to_copy, &adler_s1, &adler_s2);
+ dst += bytes_to_copy;
+ input += bytes_to_copy;
+ block_size -= bytes_to_copy;
+ bytes_to_next_row -= bytes_to_copy;
+ }
+ }
+ // Fake Adler works well in Chrome; so let's not waste CPU cycles.
+ uint32_t adler = 0; // (adler_s2 << 8) | adler_s1;
+ WriteU32BE(dst, adler);
+ WriteU32BE(dst, 0); // Fake CRC32
+
+ // IEND
+ WriteU32BE(dst, 0);
+ chunk_start = dst;
+ WriteU32(dst, 0x444E4549);
+ // TODO(eustas): this is fixed value; precalculate?
+ crc32 = CalculateCrc32(chunk_start, dst);
+ WriteU32BE(dst, crc32);
+
+ return output;
+}
+
+} // extern "C"
diff --git a/tools/wasm_demo/no_png.h b/tools/wasm_demo/no_png.h
new file mode 100644
index 0000000..1486c47
--- /dev/null
+++ b/tools/wasm_demo/no_png.h
@@ -0,0 +1,24 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef TOOLS_WASM_DEMO_NO_PNG_H_
+#define TOOLS_WASM_DEMO_NO_PNG_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <vector>
+
+extern "C" {
+
+uint8_t* WrapPixelsToPng(size_t width, size_t height, size_t bit_depth,
+ bool has_alpha, const uint8_t* input,
+ const std::vector<uint8_t>& icc,
+ const std::vector<uint8_t>& cicp,
+ uint32_t* output_size);
+
+} // extern "C"
+
+#endif // TOOLS_WASM_DEMO_NO_PNG_H_
diff --git a/tools/wasm_demo/one_line_demo.html b/tools/wasm_demo/one_line_demo.html
new file mode 100644
index 0000000..a2966ac
--- /dev/null
+++ b/tools/wasm_demo/one_line_demo.html
@@ -0,0 +1,20 @@
+<html>
+
+<head>
+ <link rel="icon" type="image/x-icon" href="favicon.ico" />
+ <script src="service_worker.js">
+/*
+ * Just load this script, et voila! It will install ServiceWorker to
+ * advertise image/jxl media type and decode responses.
+ * NB: if "addMessage" function is defined it will be used to report
+ * decoding times / problems.
+ */
+ </script>
+</head>
+
+<body>
+ <img src="image00.jxl" style="width:100%" />
+ <img src="image01.jxl" style="width:100%" />
+</body>
+
+</html>
diff --git a/tools/wasm_demo/one_line_demo_with_console.html b/tools/wasm_demo/one_line_demo_with_console.html
new file mode 100644
index 0000000..e2c52ae
--- /dev/null
+++ b/tools/wasm_demo/one_line_demo_with_console.html
@@ -0,0 +1,34 @@
+<html>
+
+<head>
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
+ <script src="service_worker.js"></script>
+ <style>
+ #log p {
+ margin: 0;
+ }
+ </style>
+</head>
+
+<body>
+ <div id="log" style="padding:2px; border: solid 1px #000; background-color: #ccc; margin:2px; height: 8em; font-family: monospace; overflow-y: auto; font-size: 8px;"></div>
+ <script>
+ let addMessage = (text, color) => {
+ let log = document.getElementById('log');
+ let message = document.createElement('p');
+ message.style = 'color: ' + color + ';';
+ message.textContent = text;
+ log.append(message);
+ log.scrollTop = log.scrollHeight;
+ }
+ </script>
+
+<!-- Use those with capable server
+ <img src="image00.jpg" style="width:100%" />
+ <img src="image01.png" style="width:100%" />
+-->
+ <img src="image00.jxl" style="width:100%" />
+ <img src="image01.jxl" style="width:100%" />
+</body>
+
+</html>
diff --git a/tools/wasm_demo/service_worker.js b/tools/wasm_demo/service_worker.js
new file mode 100644
index 0000000..531e5c2
--- /dev/null
+++ b/tools/wasm_demo/service_worker.js
@@ -0,0 +1,317 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+ * ServiceWorker script.
+ *
+ * Multi-threading in WASM is currently implemented by the means of
+ * SharedArrayBuffer. Due to infamous vulnerabilities this feature is disabled
+ * unless site is running in "cross-origin isolated" mode.
+ * If there is not enough control over the server (e.g. when pages are hosted as
+ * "github pages") ServiceWorker is used to upgrade responses with corresponding
+ * headers.
+ *
+ * This script could be executed in 2 environments: HTML page or ServiceWorker.
+ * The environment is detected by the type of "window" reference.
+ *
+ * When this script is executed from HTML page then ServiceWorker is registered.
+ * Page reload might be necessary in some situations. By default it is done via
+ * `window.location.reload()`. However this can be altered by setting a
+ * configuration object `window.serviceWorkerConfig`. It's `doReload` property
+ * should be a replacement callable.
+ *
+ * When this script is executed from ServiceWorker then standard lifecycle
+ * event dispatchers are setup along with `fetch` interceptor.
+ */
+
+(() => {
+ // Set COOP/COEP headers for document/script responses; use when this can not
+ // be done on server side (e.g. GitHub Pages).
+ const FORCE_COP = true;
+ // Interpret 'content-type: application/octet-stream' as JXL; use when server
+ // does not set appropriate content type (e.g. GitHub Pages).
+ const FORCE_DECODING = true;
+ // Embedded (baked-in) responses for faster turn-around.
+ const EMBEDDED = {
+ 'client_worker.js': '$client_worker.js$',
+ 'jxl_decoder.js': '$jxl_decoder.js$',
+ 'jxl_decoder.worker.js': '$jxl_decoder.worker.js$',
+ };
+
+ // Enable SharedArrayBuffer.
+ const setCopHeaders = (headers) => {
+ headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
+ headers.set('Cross-Origin-Opener-Policy', 'same-origin');
+ };
+
+ // Inflight object: {clientId, uid, timestamp, controller}
+ const inflight = [];
+
+ // Generate (very likely) unique string.
+ const makeUid = () => {
+ return Math.random().toString(36).substring(2) +
+ Math.random().toString(36).substring(2);
+ };
+
+ // Make list (non-recursively) of transferable entities.
+ const gatherTransferrables = (...args) => {
+ const result = [];
+ for (let i = 0; i < args.length; ++i) {
+ if (args[i] && args[i].buffer) {
+ result.push(args[i].buffer);
+ }
+ }
+ return result;
+ };
+
+ // Serve items that are embedded in this service worker.
+ const maybeProcessEmbeddedResources = (event) => {
+ const url = event.request.url;
+ // Shortcut for baked-in scripts.
+ for (const [key, value] of Object.entries(EMBEDDED)) {
+ if (url.endsWith(key)) {
+ const headers = new Headers();
+ headers.set('Content-Type', 'application/javascript');
+ setCopHeaders(headers);
+
+ event.respondWith(new Response(value, {
+ status: 200,
+ statusText: 'OK',
+ headers: headers,
+ }));
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // Decode JXL image response and serve it as a PNG image.
+ const wrapImageResponse = async (clientId, originalResponse) => {
+ // TODO(eustas): cache?
+ const client = await clients.get(clientId);
+ // Client is gone? Not our problem then.
+ if (!client) {
+ return originalResponse;
+ }
+
+ const inputStream = await originalResponse.body;
+ // Can't use "BYOB" for regular responses.
+ const reader = inputStream.getReader();
+
+ const inflightEntry = {
+ clientId: clientId,
+ uid: makeUid(),
+ timestamp: Date.now(),
+ inputStreamReader: reader,
+ outputStreamController: null
+ };
+ inflight.push(inflightEntry);
+
+ const outputStream = new ReadableStream({
+ start: (controller) => {
+ inflightEntry.outputStreamController = controller;
+ }
+ });
+
+ const onRead = (chunk) => {
+ const msg = {
+ op: 'decodeJxl',
+ uid: inflightEntry.uid,
+ url: originalResponse.url,
+ data: chunk.value || null
+ };
+ client.postMessage(msg, gatherTransferrables(msg.data));
+ if (!chunk.done) {
+ reader.read().then(onRead);
+ }
+ };
+ // const view = new SharedArrayBuffer(65536);
+ const view = new Uint8Array(65536);
+ reader.read(view).then(onRead);
+
+ let modifiedResponseHeaders = new Headers(originalResponse.headers);
+ modifiedResponseHeaders.delete('Content-Length');
+ modifiedResponseHeaders.set('Content-Type', 'image/png');
+ modifiedResponseHeaders.set('Server', 'ServiceWorker');
+ return new Response(outputStream, {headers: modifiedResponseHeaders});
+ };
+
+ // Check if response needs decoding; if so - do it.
+ const wrapImageRequest = async (clientId, request) => {
+ let modifiedRequestHeaders = new Headers(request.headers);
+ modifiedRequestHeaders.append('Accept', 'image/jxl');
+ let modifiedRequest =
+ new Request(request, {headers: modifiedRequestHeaders});
+ let originalResponse = await fetch(modifiedRequest);
+ let contentType = originalResponse.headers.get('Content-Type');
+
+ let isJxlResponse = (contentType === 'image/jxl');
+ if (FORCE_DECODING && contentType === 'application/octet-stream') {
+ isJxlResponse = true;
+ }
+ if (isJxlResponse) {
+ return wrapImageResponse(clientId, originalResponse);
+ }
+
+ return originalResponse;
+ };
+
+ const reportError = (err) => {
+ // console.error(err);
+ };
+
+ const upgradeResponse = (response) => {
+ if (response.status === 0) {
+ return response;
+ }
+
+ const newHeaders = new Headers(response.headers);
+ setCopHeaders(newHeaders);
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: newHeaders,
+ });
+ };
+
+ // Process fetch request; either bypass, or serve embedded resource,
+ // or upgrade.
+ const onFetch = async (event) => {
+ const clientId = event.clientId;
+ const request = event.request;
+
+ // Pass direct cached resource requests.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return;
+ }
+
+ // Serve backed resources.
+ if (maybeProcessEmbeddedResources(event)) {
+ return;
+ }
+
+ // Notify server we are JXL-capable.
+ if (request.destination === 'image') {
+ let accept = request.headers.get('Accept');
+ // Only if browser does not support JXL.
+ if (accept.indexOf('image/jxl') === -1) {
+ event.respondWith(wrapImageRequest(clientId, request));
+ }
+ return;
+ }
+
+ if (FORCE_COP) {
+ event.respondWith(
+ fetch(event.request).then(upgradeResponse).catch(reportError));
+ }
+ };
+
+ // Serve decoded bytes.
+ const onMessage = (event) => {
+ const data = event.data;
+ const uid = data.uid;
+ let inflightEntry = null;
+ for (let i = 0; i < inflight.length; ++i) {
+ if (inflight[i].uid === uid) {
+ inflightEntry = inflight[i];
+ break;
+ }
+ }
+ if (!inflightEntry) {
+ console.log('Ooops, not found: ' + uid);
+ return;
+ }
+ inflightEntry.outputStreamController.enqueue(data.data);
+ inflightEntry.outputStreamController.close();
+ };
+
+ // This method is "main" for service worker.
+ const serviceWorkerMain = () => {
+ // https://v8.dev/blog/wasm-code-caching
+ // > Every web site must perform at least one full compilation of a
+ // > WebAssembly module — use workers to hide that from your users.
+ // TODO(eustas): not 100% reliable, investigate why
+ self['JxlDecoderLeak'] =
+ WebAssembly.compileStreaming(fetch('jxl_decoder.wasm'));
+
+ // ServiceWorker lifecycle.
+ self.addEventListener('install', () => {
+ return self.skipWaiting();
+ });
+ self.addEventListener(
+ 'activate', (event) => event.waitUntil(self.clients.claim()));
+ self.addEventListener('message', onMessage);
+ // Intercept some requests.
+ self.addEventListener('fetch', onFetch);
+ };
+
+ // Service workers does not support multi-threading; that is why decoding is
+ // relayed back to "client" (document / window).
+ const prepareClient = () => {
+ const clientWorker = new Worker('client_worker.js');
+ clientWorker.onmessage = (event) => {
+ const data = event.data;
+ if (typeof addMessage !== 'undefined') {
+ if (data.msg) {
+ addMessage(data.msg, 'blue');
+ }
+ }
+ navigator.serviceWorker.controller.postMessage(
+ data, gatherTransferrables(data.data));
+ };
+
+ // Forward ServiceWorker requests to "Client" worker.
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ clientWorker.postMessage(
+ event.data, gatherTransferrables(event.data.data));
+ });
+ };
+
+ // Executed in HTML page environment.
+ const maybeRegisterServiceWorker = () => {
+ const config = {
+ log: console.log,
+ error: console.error,
+ requestReload: (msg) => window.location.reload(),
+ ...window.serviceWorkerConfig // add overrides
+ }
+
+ if (!window.isSecureContext) {
+ config.log('Secure context is required for this ServiceWorker.');
+ return;
+ }
+
+ const nav = navigator; // Explicitly capture navigator object.
+ const onServiceWorkerRegistrationSuccess = (registration) => {
+ config.log('Service Worker registered', registration.scope);
+ if (!registration.active || !nav.serviceWorker.controller) {
+ config.requestReload(
+ 'Reload to allow Service Worker process all requests');
+ }
+ };
+
+ const onServiceWorkerRegistrationFailure = (err) => {
+ config.error('Service Worker failed to register:', err);
+ };
+
+ navigator.serviceWorker.register(window.document.currentScript.src)
+ .then(
+ onServiceWorkerRegistrationSuccess,
+ onServiceWorkerRegistrationFailure);
+ };
+
+ const pageMain = () => {
+ maybeRegisterServiceWorker();
+ prepareClient();
+ };
+
+ // Detect environment and run corresponding "main" method.
+ if (typeof window === 'undefined') {
+ serviceWorkerMain();
+ } else {
+ pageMain();
+ }
+})();
diff --git a/tools/xyb_range.cc b/tools/xyb_range.cc
index 1ce4882..e2afd56 100644
--- a/tools/xyb_range.cc
+++ b/tools/xyb_range.cc
@@ -3,6 +3,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#include <jxl/cms.h>
#include <stdio.h>
#include <utility>
@@ -12,15 +13,20 @@
#include "lib/jxl/base/printf_macros.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/color_encoding_internal.h"
-#include "lib/jxl/color_management.h"
-#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_xyb.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
-namespace jxl {
+namespace jpegxl {
+namespace tools {
namespace {
+using ::jxl::CodecInOut;
+using ::jxl::ColorEncoding;
+using ::jxl::Image3F;
+using ::jxl::ImageBundle;
+using ::jxl::ThreadPool;
+
void PrintXybRange() {
Image3F linear(1u << 16, 257);
for (int b = 0; b < 256; ++b) {
@@ -43,7 +49,7 @@ void PrintXybRange() {
const ImageBundle& ib = io.Main();
ThreadPool* null_pool = nullptr;
Image3F opsin(ib.xsize(), ib.ysize());
- (void)ToXYB(ib, null_pool, &opsin, GetJxlCms());
+ (void)jxl::ToXYB(ib, null_pool, &opsin, *JxlGetDefaultCms());
for (size_t c = 0; c < 3; ++c) {
float minval = 1e10f;
float maxval = -1e10f;
@@ -75,6 +81,7 @@ void PrintXybRange() {
}
} // namespace
-} // namespace jxl
+} // namespace tools
+} // namespace jpegxl
-int main() { jxl::PrintXybRange(); }
+int main() { jpegxl::tools::PrintXybRange(); }