diff options
author | Job Bautista <jobbautista9@protonmail.com> | 2022-12-22 12:53:52 +0800 |
---|---|---|
committer | Job Bautista <jobbautista9@protonmail.com> | 2022-12-22 12:53:52 +0800 |
commit | 3d070292327405acdc89279cee1a54a233d05cc2 (patch) | |
tree | ad5c5df11e0c429b00216940dc1a7d6a6778a09a /media | |
parent | fc62ce007670111f3b7f76b27d22810a530d8973 (diff) | |
download | uxp-3d070292327405acdc89279cee1a54a233d05cc2.tar.gz |
Issue #2061 - Part 1: Update libjxl source to 0.7.0.
Diffstat (limited to 'media')
296 files changed, 15491 insertions, 12004 deletions
diff --git a/media/libjxl/src/.gitmodules b/media/libjxl/src/.gitmodules index b211bed3be..bd008a6129 100644 --- a/media/libjxl/src/.gitmodules +++ b/media/libjxl/src/.gitmodules @@ -22,9 +22,6 @@ [submodule "third_party/zlib"] path = third_party/zlib url = https://github.com/madler/zlib.git -[submodule "third_party/gflags"] - path = third_party/gflags - url = https://github.com/gflags/gflags [submodule "third_party/testdata"] - path = third_party/testdata + path = testdata url = https://github.com/libjxl/testdata diff --git a/media/libjxl/src/AUTHORS b/media/libjxl/src/AUTHORS index 401350f86a..c8522b8fc3 100644 --- a/media/libjxl/src/AUTHORS +++ b/media/libjxl/src/AUTHORS @@ -19,26 +19,35 @@ Google LLC <*@google.com> Alex Xu (Hello71) <alex_y_xu@yahoo.ca> Alexander Sago <cagelight@gmail.com> Andrius Lukas Narbutas <andrius4669@gmail.com> +Aous Naman <aous@unsw.edu.au> Artem Selishchev +Biswapriyo Nath <nathbappai@gmail.com> CanadianBaconBoi <beamconnor@gmail.com> Daniel Novomeský <dnovomesky@gmail.com> David Burnett <vargolsoft@gmail.com> Dirk Lemstra <dirk@lemstra.org> Don Olmstead <don.j.olmstead@gmail.com> +Even Rouault <even.rouault@spatialys.com> Heiko Becker <heirecka@exherbo.org> Jon Sneyers <jon@cloudinary.com> +Kai Hollberg <Schweinepriester@users.noreply.github.com> Kleis Auke Wolthuizen <github@kleisauke.nl> L. E. Segovia Leo Izen <leo.izen@gmail.com> Lovell Fuller +Maarten DB <anonymous.maarten@gmail.com> Marcin Konicki <ahwayakchih@gmail.com> Martin Strunz Mathieu Malaterre <mathieu.malaterre@gmail.com> +Mikk Leini <mikk.leini@krakul.eu> Misaki Kasumi <misakikasumi@outlook.com> Petr Diblík Pieter Wuille +roland-rollo Samuel Leong <wvvwvvvvwvvw@gmail.com> +Sandro <sandro.jaeckel@gmail.com> Stephan T. Lavavej <stl@nuwen.net> +Thomas Bonfort <thomas.bonfort@airbus.com> Vincent Torri <vincent.torri@gmail.com> xiota Yonatan Nebenzhal <yonatan.nebenzhl@gmail.com> diff --git a/media/libjxl/src/CHANGELOG.md b/media/libjxl/src/CHANGELOG.md index 9cfc3da86d..cf68400807 100644 --- a/media/libjxl/src/CHANGELOG.md +++ b/media/libjxl/src/CHANGELOG.md @@ -6,18 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +## [0.7] - 2022-07-21 + ### Added + - Export version information in headers. - decoder API: Ability to decode the content of metadata boxes: - `JXL_DEC_BOX`, `JXL_DEC_BOX_NEED_MORE_OUTPUT`, `JxlDecoderSetBoxBuffer`, + `JXL_DEC_BOX`, `JXL_DEC_BOX_NEED_MORE_OUTPUT`, `JxlDecoderSetBoxBuffer`, `JxlDecoderGetBoxType`, `JxlDecoderGetBoxSizeRaw` and - `JxlDecoderSetDecompressBoxes` - - decoder API: ability to mark the input is finished: `JxlDecoderCloseInput` - - encoder API: ability to add metadata boxes, added new functions - `JxlEncoderAddBox`, `JxlEncoderUseBoxes`, `JxlEncoderCloseBoxes` and - `JxlEncoderCloseFrames`. + `JxlDecoderSetDecompressBoxes`. + - decoder API: ability to mark the input is finished: `JxlDecoderCloseInput`. + - decoder API: ability to request updates on different progressive events using + `JxlDecoderSetProgressiveDetail`; currently supported events are + `kDC`, `kLastPasses` and `kPasses`. + - decoder API: ability to specify desired intensity target using + `JxlDecoderSetDesiredIntensityTarget` - decoder API: new function `JxlDecoderSetCoalesced` to allow decoding non-coalesced (unblended) frames, e.g. layers of a composite still image or the cropped frames of a recompressed GIF/APNG. + - decoder API: new function `JxlDecoderSetUnpremultiplyAlpha` to set + preference for getting an associated alpha channel with premultiplied or + unpremultiplied colors. - decoder API: field added to `JxlFrameHeader`: a `JxlLayerInfo` struct that contains crop dimensions and offsets and blending information for the non-coalesced case. @@ -26,37 +35,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - decoder API: new function `JxlDecoderSetMultithreadedImageOutCallback`, allowing output callbacks to receive more information about the number of threads on which they are running. - - encoder API: added ability to set several encoder options to frames using - `JxlEncoderFrameSettingsSetOption` + - decoder API: new function `JxlDecoderSkipCurrentFrame` to skip processing + the current frame after a progressive detail is reached. + - decoder API: new function `JxlDecoderGetIntendedDownsamplingRatio` to get + the intended downsampling ratio of progressive steps, based on the + information in the frame header. + - decoder API: new function `JxlDecoderSetRenderSpotcolors` to allow disabling + rendering of spot colors. + - decoder/encoder API: add two fields to `JXLBasicInfo`: `intrinsic_xsize` + and `intrinsic_ysize` to signal the intrinsic size. + - encoder API: ability to add metadata boxes, added new functions + `JxlEncoderAddBox`, `JxlEncoderUseBoxes`, `JxlEncoderCloseBoxes` and + `JxlEncoderCloseFrames`. + - encoder API: added ability to set several encoder options / extra fields to + frames using `JxlEncoderSetFrameName`, `JxlEncoderFrameSettingsSetOption`, + `JxlEncoderFrameSettingsSetFloatOption`. + - encoder API: added ability to check required codestream compatibility level + and force specified using `JxlEncoderGetRequiredCodestreamLevel` and + `JxlEncoderSetCodestreamLevel`. + - encoder API: added ability to force emitting box-based container format + using `JxlEncoderUseContainer`. + - encoder API: added ability to store JPEG metadata for lossless reconstruction + using `JxlEncoderStoreJPEGMetadata` - encoder API: new functions `JxlEncoderSetFrameHeader` and `JxlEncoderSetExtraChannelBlendInfo` to set animation and blending parameters of the frame, and `JxlEncoderInitFrameHeader` and `JxlEncoderInitBlendInfo` to initialize the structs to set. - - decoder/encoder API: add two fields to `JXLBasicInfo`: `intrinsic_xsize` - and `intrinsic_ysize` to signal the intrinsic size. - encoder API: ability to encode arbitrary extra channels: `JxlEncoderInitExtraChannelInfo`, `JxlEncoderSetExtraChannelInfo`, `JxlEncoderSetExtraChannelName` and `JxlEncoderSetExtraChannelBuffer`. + - encoder API: ability to plug custom CMS implementation using + `JxlEncoderSetCms(JxlEncoder* enc, JxlCmsInterface cms)` + - encoder API: added `JxlEncoderGetError` to retrieve last encoder error. ### Changed - decoder API: using `JxlDecoderCloseInput` at the end of all input is required when using JXL_DEC_BOX, and is now also encouraged in other cases, but not - required in those other cases for backwards compatiblity. + required in those other cases for backwards compatibility. - encoder API: `JxlEncoderCloseInput` now closes both frames and boxes input. +- CLI: `cjxl` and `djxl` have been reimplemented on the base of public decoder + and encoder API; dropped dependency on `gflags` for argument parsing. ### Deprecated -- encoder API: `JxlEncoderOptions`: use `JxlEncoderFrameSettings` instead +- decoder API: `JXL_DEC_EXTENSIONS` event: use `JXL_DEC_BASIC_INFO` +- decoder / encoder API: pixel types `JXL_TYPE_BOOLEAN` and `JXL_TYPE_UINT32`: + consider using `JXL_TYPE_UINT8` and `JXL_TYPE_FLOAT` correspondingly. +- decoder API: pixel format parameter for `JxlDecoderGetColorAsEncodedProfile` + and `JxlDecoderGetICCProfileSize`: pass `NULL`. +- decoder API: `JxlDecoderDefaultPixelFormat` +- encoder API: `JxlEncoderOptions`: use `JxlEncoderFrameSettings` instead. - encoder API: `JxlEncoderOptionsCreate`: use `JxlEncoderFrameSettingsCreate` - instead + instead. - encoder API: `JxlEncoderOptionsSetDistance`: use `JxlEncoderSetFrameDistance` - instead + instead. - encoder API: `JxlEncoderOptionsSetLossless`: use `JxlEncoderSetFrameLossless` - instead -- encoder API: `JxlEncoderOptionsSetEffort`: use `JxlEncoderFrameSettingsSetOption( - frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, effort)` instead. + instead. +- encoder API: `JxlEncoderOptionsSetEffort`: use + `JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, effort)` + instead. - encoder API: `JxlEncoderOptionsSetDecodingSpeed`: use - `JxlEncoderFrameSettingsSetOption(frame_settings, - JXL_ENC_FRAME_SETTING_DECODING_SPEED, tier)` instead. + `JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_DECODING_SPEED, tier)` + instead. - encoder API: deprecated `JXL_ENC_NOT_SUPPORTED`, the encoder returns `JXL_ENC_ERROR` instead and there is no need to handle `JXL_ENC_NOT_SUPPORTED`. diff --git a/media/libjxl/src/CMakeLists.txt b/media/libjxl/src/CMakeLists.txt index 50251566fe..533815d232 100644 --- a/media/libjxl/src/CMakeLists.txt +++ b/media/libjxl/src/CMakeLists.txt @@ -51,12 +51,12 @@ if(CHECK_PIE_SUPPORTED) endif() ### Project build options: -if(${CXX_FUZZERS_SUPPORTED}) +if(CXX_FUZZERS_SUPPORTED) # Enabled by default except on arm64, Windows and Apple builds. set(ENABLE_FUZZERS_DEFAULT true) endif() find_package(PkgConfig) -if(NOT APPLE AND NOT WIN32 AND NOT HAIKU AND ${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") +if(NOT APPLE AND NOT WIN32 AND NOT HAIKU AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") pkg_check_modules(TCMallocMinimalVersionCheck QUIET IMPORTED_TARGET libtcmalloc_minimal) if(TCMallocMinimalVersionCheck_FOUND AND @@ -72,18 +72,27 @@ if(NOT APPLE AND NOT WIN32 AND NOT HAIKU AND ${CMAKE_SYSTEM_PROCESSOR} MATCHES " endif() endif() +check_cxx_source_compiles( + "int main() { + #if !defined(HWY_DISABLED_TARGETS) + static_assert(false, \"HWY_DISABLED_TARGETS is not defined\"); + #endif + return 0; + }" + JXL_HWY_DISABLED_TARGETS_FORCED +) + set(WARNINGS_AS_ERRORS_DEFAULT false) if((SANITIZER STREQUAL "msan") OR JPEGXL_EMSCRIPTEN) set(BUNDLE_LIBPNG_DEFAULT YES) - set(BUNDLE_GFLAGS_DEFAULT YES) else() set(BUNDLE_LIBPNG_DEFAULT NO) - set(BUNDLE_GFLAGS_DEFAULT NO) endif() # Standard cmake naming for building shared libraries. -option(BUILD_SHARED_LIBS "Build shared libraries instead of static ones" ON) +get_property(SHARED_LIBS_SUPPORTED GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS) +option(BUILD_SHARED_LIBS "Build shared libraries instead of static ones" ${SHARED_LIBS_SUPPORTED}) set(JPEGXL_ENABLE_FUZZERS ${ENABLE_FUZZERS_DEFAULT} CACHE BOOL "Build JPEGXL fuzzer targets.") @@ -99,8 +108,6 @@ set(JPEGXL_ENABLE_BENCHMARK true CACHE BOOL "Build JPEGXL benchmark tools.") set(JPEGXL_ENABLE_EXAMPLES true CACHE BOOL "Build JPEGXL library usage examples.") -set(JPEGXL_BUNDLE_GFLAGS ${BUNDLE_GFLAGS_DEFAULT} CACHE BOOL - "Build gflags from source and link it statically.") set(JPEGXL_BUNDLE_LIBPNG ${BUNDLE_LIBPNG_DEFAULT} CACHE BOOL "Build libpng from source and link it statically.") set(JPEGXL_ENABLE_JNI true CACHE BOOL @@ -122,7 +129,9 @@ set(JPEGXL_ENABLE_PLUGINS false CACHE BOOL set(JPEGXL_ENABLE_COVERAGE false CACHE BOOL "Enable code coverage tracking for libjxl. This also enables debug and disables optimizations.") set(JPEGXL_ENABLE_PROFILER false CACHE BOOL - "Builds in support for profiling (printed by tools if extra flags given") + "Builds in support for profiling (printed by tools if extra flags given)") +set(JPEGXL_ENABLE_SIZELESS_VECTORS false CACHE BOOL + "Builds in support for SVE/RVV vectorization") set(JPEGXL_ENABLE_TRANSCODE_JPEG true CACHE BOOL "Builds in support for decoding transcoded JXL files back to JPEG,\ disabling it makes the decoder reject JXL_DEC_JPEG_RECONSTRUCTION events,\ @@ -149,20 +158,20 @@ set(JPEGXL_FORCE_SYSTEM_HWY false CACHE BOOL # Check minimum compiler versions. Older compilers are not supported and fail # with hard to understand errors. -if (NOT ${CMAKE_C_COMPILER_ID} STREQUAL ${CMAKE_CXX_COMPILER_ID}) +if (NOT CMAKE_C_COMPILER_ID STREQUAL CMAKE_CXX_COMPILER_ID) message(FATAL_ERROR "Different C/C++ compilers set: " "${CMAKE_C_COMPILER_ID} vs ${CMAKE_CXX_COMPILER_ID}") endif() -if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Android NDK's toolchain.cmake fakes the clang version in # CMAKE_CXX_COMPILER_VERSION with an incorrect number, so ignore this. - if (NOT ${CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION} MATCHES "clang" - AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 6) + if (NOT CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION MATCHES "clang" + AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5) message(FATAL_ERROR - "Minimum Clang version required is Clang 6, please update.") + "Minimum Clang version required is Clang 5, please update.") endif() -elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") - if (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 7) +elseif (CMAKE_CXX_COMPILER_ID MATCHES "GNU") + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7) message(FATAL_ERROR "Minimum GCC version required is 7, please update.") endif() @@ -234,7 +243,13 @@ if(JPEGXL_STATIC) endif() endif() # JPEGXL_STATIC -if ("${CXX_MACRO_PREFIX_MAP}") +if (JPEGXL_EMSCRIPTEN) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread") +endif() + +if (CXX_MACRO_PREFIX_MAP) add_compile_options(-fmacro-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.) endif() @@ -259,16 +274,22 @@ if(MSVC) add_definitions(-D_CRT_SECURE_NO_WARNINGS) endif() +# TODO(eustas): JXL currently compiles, but does not pass tests... +if (NOT JXL_HWY_DISABLED_TARGETS_FORCED AND NOT JPEGXL_ENABLE_SIZELESS_VECTORS) + add_definitions(-DHWY_DISABLED_TARGETS=\(HWY_SVE|HWY_SVE2|HWY_SVE_256|HWY_SVE2_128|HWY_RVV\)) + message("Warning: HWY_SVE, HWY_SVE2, HWY_SVE_256, HWY_SVE2_128 and HWY_RVV CPU targets are disabled") +endif() + # In CMake before 3.12 it is problematic to pass repeated flags like -Xclang. # For this reason we place them in CMAKE_CXX_FLAGS instead. # See https://gitlab.kitware.com/cmake/cmake/issues/15826 # Machine flags. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -funwind-tables") -if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Xclang -mrelax-all") endif() -if ("${CXX_CONSTRUCTOR_ALIASES_SUPPORTED}") +if (CXX_CONSTRUCTOR_ALIASES_SUPPORTED) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Xclang -mconstructor-aliases") endif() @@ -281,7 +302,7 @@ endif() # CPU flags - remove once we have NEON dynamic dispatch # TODO(janwas): this also matches M1, but only ARMv7 is intended/needed. -if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") +if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm") if(JPEGXL_FORCE_NEON) # GCC requires these flags, otherwise __ARM_NEON is undefined. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ @@ -307,8 +328,10 @@ endif () # !MSVC include(GNUInstallDirs) +# Separately build/configure testing frameworks and other third_party libraries +# to allow disabling tests in those libraries. +include(third_party/testing.cmake) add_subdirectory(third_party) - # Copy the JXL license file to the output build directory. configure_file("${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" ${PROJECT_BINARY_DIR}/LICENSE.jpeg-xl COPYONLY) @@ -318,7 +341,7 @@ enable_testing() include(CTest) # Specify default location of `testdata`: if(NOT DEFINED JPEGXL_TEST_DATA_PATH) - set(JPEGXL_TEST_DATA_PATH "${PROJECT_SOURCE_DIR}/third_party/testdata") + set(JPEGXL_TEST_DATA_PATH "${PROJECT_SOURCE_DIR}/testdata") endif() # Libraries. @@ -382,9 +405,9 @@ endif() # JPEGXL_ENABLE_DOXYGEN if(JPEGXL_ENABLE_MANPAGES) find_program(ASCIIDOC a2x) -if(NOT "${ASCIIDOC}" STREQUAL "ASCIIDOC-NOTFOUND") +if(ASCIIDOC) file(STRINGS "${ASCIIDOC}" ASCIIDOC_SHEBANG LIMIT_COUNT 1) -if(ASCIIDOC_SHEBANG MATCHES "/sh") +if(ASCIIDOC_SHEBANG MATCHES "/sh|/bash") set(ASCIIDOC_PY_FOUND ON) # Run the program directly and set ASCIIDOC as empty. set(ASCIIDOC_PY "${ASCIIDOC}") @@ -401,7 +424,7 @@ else() find_package(Python COMPONENTS Interpreter QUIET) if(NOT Python_Interpreter_FOUND) find_program(ASCIIDOC_PY python) - if(NOT ASCIIDOC_PY STREQUAL "ASCIIDOC_PY-NOTFOUND") + if(ASCIIDOC_PY) set(ASCIIDOC_PY_FOUND ON) endif() else() @@ -432,16 +455,16 @@ if (ASCIIDOC_PY_FOUND) endif() # ASCIIDOC_PY_FOUND else() message(WARNING "asciidoc was not found, the man pages will not be installed.") -endif() # ASCIIDOC != "ASCIIDOC-NOTFOUND" +endif() # ASCIIDOC endif() # JPEGXL_ENABLE_MANPAGES # Example usage code. -if (${JPEGXL_ENABLE_EXAMPLES}) +if (JPEGXL_ENABLE_EXAMPLES) include(examples/examples.cmake) endif () # Plugins for third-party software -if (${JPEGXL_ENABLE_PLUGINS}) +if (JPEGXL_ENABLE_PLUGINS) add_subdirectory(plugins) endif () diff --git a/media/libjxl/src/README.md b/media/libjxl/src/README.md index d0aa4b7dd2..b0f2e3b50e 100644 --- a/media/libjxl/src/README.md +++ b/media/libjxl/src/README.md @@ -1,7 +1,13 @@ # JPEG XL reference implementation -[![Build&Test](https://github.com/libjxl/libjxl/actions/workflows/build_test.yml/badge.svg)]( +[![Build/Test](https://github.com/libjxl/libjxl/actions/workflows/build_test.yml/badge.svg)]( https://github.com/libjxl/libjxl/actions/workflows/build_test.yml) +[![Build/Test Cross](https://github.com/libjxl/libjxl/actions/workflows/build_test_cross.yml/badge.svg)]( +https://github.com/libjxl/libjxl/actions/workflows/build_test_cross.yml) +[![Conformance](https://github.com/libjxl/libjxl/actions/workflows/conformance.yml/badge.svg)]( +https://github.com/libjxl/libjxl/actions/workflows/conformance.yml) +[![CIFuzz](https://github.com/libjxl/libjxl/actions/workflows/fuzz.yml/badge.svg)]( +https://github.com/libjxl/libjxl/actions/workflows/fuzz.yml) [![Releases](https://github.com/libjxl/libjxl/actions/workflows/release.yaml/badge.svg)]( https://github.com/libjxl/libjxl/actions/workflows/release.yaml) [![Doc](https://readthedocs.org/projects/libjxl/badge/?version=latest)]( @@ -63,7 +69,7 @@ Required dependencies for compiling the code, in a Debian/Ubuntu based distribution run: ```bash -sudo apt install cmake pkg-config libbrotli-dev libgflags-dev +sudo apt install cmake pkg-config libbrotli-dev ``` Optional dependencies for supporting other formats in the `cjxl`/`djxl` tools, @@ -168,7 +174,7 @@ format: Cloudinary and Google. * [JPEG XL Format Overview](doc/format_overview.md) * [Introductory paper](https://www.spiedigitallibrary.org/proceedings/Download?fullDOI=10.1117%2F12.2529237) (open-access) * [XL Overview](doc/xl_overview.md) - a brief introduction to the source code modules -* [JPEG XL white paper](http://ds.jpeg.org/whitepapers/jpeg-xl-whitepaper.pdf) +* [JPEG XL white paper](https://ds.jpeg.org/whitepapers/jpeg-xl-whitepaper.pdf) * [JPEG XL official website](https://jpeg.org/jpegxl) * [JPEG XL community website](https://jpegxl.info) diff --git a/media/libjxl/src/bash_test.sh b/media/libjxl/src/bash_test.sh index 6a628c9349..675026ab2e 100755 --- a/media/libjxl/src/bash_test.sh +++ b/media/libjxl/src/bash_test.sh @@ -103,6 +103,12 @@ test_printf_size_t() { ret=1 fi + if grep -n -E 'gmock\.h' \ + $(git ls-files | grep -E '(\.c|\.cc|\.cpp|\.h)$' | grep -v -F /test_utils.h); then + echo "Don't include gmock directly, instead include 'test_utils.h'. " >&2 + ret=1 + fi + local f for f in $(git ls-files | grep -E "\.cc$" | xargs grep 'PRI[udx]S' | cut -f 1 -d : | uniq); do @@ -116,7 +122,7 @@ test_printf_size_t() { fi done - for f in $(git ls-files | grep -E "\.h$" | grep -v -F printf_macros.h | + for f in $(git ls-files | grep -E "\.h$" | grep -v -E '(printf_macros\.h|test_utils\.h)' | xargs grep -n 'PRI[udx]S'); do # Having PRIuS / PRIdS in a header file means that printf_macros.h may # be included before a system header, in particular before gtest headers. diff --git a/media/libjxl/src/ci.sh b/media/libjxl/src/ci.sh index 49ac042596..45d5218d23 100755 --- a/media/libjxl/src/ci.sh +++ b/media/libjxl/src/ci.sh @@ -15,13 +15,14 @@ OS=`uname -s` MYDIR=$(dirname $(realpath "$0")) ### Environment parameters: -TEST_STACK_LIMIT="${TEST_STACK_LIMIT:-128}" +TEST_STACK_LIMIT="${TEST_STACK_LIMIT:-256}" CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-RelWithDebInfo} CMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH:-} CMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER:-} CMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER:-} CMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM:-} SKIP_TEST="${SKIP_TEST:-0}" +TEST_SELECTOR="${TEST_SELECTOR:-}" BUILD_TARGET="${BUILD_TARGET:-}" ENABLE_WASM_SIMD="${ENABLE_WASM_SIMD:-0}" if [[ -n "${BUILD_TARGET}" ]]; then @@ -41,27 +42,11 @@ FUZZER_MAX_TIME="${FUZZER_MAX_TIME:-0}" SANITIZER="none" -if [[ "${BUILD_TARGET}" == wasm* ]]; then - # Check that environment is setup for the WASM build target. - if [[ -z "${EMSCRIPTEN}" ]]; then - echo "'EMSCRIPTEN' is not defined. Use 'emconfigure' wrapper to setup WASM build environment" >&2 - return 1 - fi - # Remove the side-effect of "emconfigure" wrapper - it considers NodeJS environment. - unset EMMAKEN_JUST_CONFIGURE - EMS_TOOLCHAIN_FILE="${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake" - if [[ -f "${EMS_TOOLCHAIN_FILE}" ]]; then - CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE:-${EMS_TOOLCHAIN_FILE}} - else - echo "Warning: EMSCRIPTEN CMake module not found" >&2 - fi - CMAKE_CROSSCOMPILING_EMULATOR="${MYDIR}/js-wasm-wrapper.sh" -fi if [[ "${BUILD_TARGET%%-*}" == "x86_64" || "${BUILD_TARGET%%-*}" == "i686" ]]; then # Default to building all targets, even if compiler baseline is SSE4 - HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-HWY_SCALAR} + HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-HWY_EMU128} else HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-} fi @@ -350,7 +335,6 @@ cmake_configure() { -G Ninja -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" - -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" -DCMAKE_MODULE_LINKER_FLAGS="${CMAKE_MODULE_LINKER_FLAGS}" -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" @@ -392,11 +376,14 @@ cmake_configure() { # Only the first element of the target triplet. -DCMAKE_SYSTEM_PROCESSOR="${BUILD_TARGET%%-*}" -DCMAKE_SYSTEM_NAME="${system_name}" + -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" ) else - # sjpeg confuses WASM SIMD with SSE. args+=( + # sjpeg confuses WASM SIMD with SSE. -DSJPEG_ENABLE_SIMD=OFF + # Building shared libs is not very useful for WASM. + -DBUILD_SHARED_LIBS=OFF ) fi args+=( @@ -457,7 +444,11 @@ cmake_configure() { -DCMAKE_MAKE_PROGRAM="${CMAKE_MAKE_PROGRAM}" ) fi - cmake "${args[@]}" "$@" + if [[ "${BUILD_TARGET}" == wasm* ]]; then + emcmake cmake "${args[@]}" "$@" + else + cmake "${args[@]}" "$@" + fi } cmake_build_and_test() { @@ -485,7 +476,7 @@ cmake_build_and_test() { (cd "${BUILD_DIR}" export UBSAN_OPTIONS=print_stacktrace=1 [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" - ctest -j $(nproc --all || echo 1) --output-on-failure) + ctest -j $(nproc --all || echo 1) ${TEST_SELECTOR} --output-on-failure) fi } @@ -494,7 +485,7 @@ cmake_build_and_test() { # library. strip_dead_code() { # Emscripten does tree shaking without any extra flags. - if [[ "${CMAKE_TOOLCHAIN_FILE##*/}" == "Emscripten.cmake" ]]; then + if [[ "${BUILD_TARGET}" == wasm* ]]; then return 0 fi # -ffunction-sections, -fdata-sections and -Wl,--gc-sections effectively @@ -711,20 +702,24 @@ cmd_msan_install() { export CC="${CC:-clang}" export CXX="${CXX:-clang++}" detect_clang_version - local llvm_tag="llvmorg-${CLANG_VERSION}.0.0" - case "${CLANG_VERSION}" in - "6.0") - llvm_tag="llvmorg-6.0.1" - ;; - "7") - llvm_tag="llvmorg-7.0.1" - ;; - esac - local llvm_targz="${tmpdir}/${llvm_tag}.tar.gz" - curl -L --show-error -o "${llvm_targz}" \ - "https://github.com/llvm/llvm-project/archive/${llvm_tag}.tar.gz" - tar -C "${tmpdir}" -zxf "${llvm_targz}" - local llvm_root="${tmpdir}/llvm-project-${llvm_tag}" + # Allow overriding the LLVM checkout. + local llvm_root="${LLVM_ROOT:-}" + if [ -z "${llvm_root}" ]; then + local llvm_tag="llvmorg-${CLANG_VERSION}.0.0" + case "${CLANG_VERSION}" in + "6.0") + llvm_tag="llvmorg-6.0.1" + ;; + "7") + llvm_tag="llvmorg-7.0.1" + ;; + esac + local llvm_targz="${tmpdir}/${llvm_tag}.tar.gz" + curl -L --show-error -o "${llvm_targz}" \ + "https://github.com/llvm/llvm-project/archive/${llvm_tag}.tar.gz" + tar -C "${tmpdir}" -zxf "${llvm_targz}" + llvm_root="${tmpdir}/llvm-project-${llvm_tag}" + fi local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" rm -rf "${msan_prefix}" @@ -1020,11 +1015,11 @@ cmd_arm_benchmark() { ) local images=( - "third_party/testdata/third_party/imagecompression.info/flower_foveon.png" + "testdata/jxl/flower/flower.png" ) local jpg_images=( - "third_party/testdata/third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg" + "testdata/jxl/flower/flower.png.im_q85_420.jpg" ) if [[ "${SKIP_CPUSET:-}" == "1" ]]; then @@ -1219,10 +1214,10 @@ cmd_lint() { # We include in this linter all the changes including the uncommitted changes # to avoid printing changes already applied. set -x + # Ignoring the error that git-clang-format outputs. git -C "${MYDIR}" "${clang_format}" --binary "${clang_format}" \ - --style=file --diff "${MR_ANCESTOR_SHA}" -- >"${tmppatch}" + --style=file --diff "${MR_ANCESTOR_SHA}" -- >"${tmppatch}" || true { set +x; } 2>/dev/null - if grep -E '^--- ' "${tmppatch}">/dev/null; then if [[ -n "${LINT_OUTPUT:-}" ]]; then cp "${tmppatch}" "${LINT_OUTPUT}" @@ -1399,10 +1394,8 @@ cmd_bump_version() { fi fi - newver="${major}.${minor}" - if [[ "${patch}" != "0" ]]; then - newver="${newver}.${patch}" - fi + newver="${major}.${minor}.${patch}" + echo "Bumping version to ${newver} (${major}.${minor}.${patch})" sed -E \ -e "s/(set\\(JPEGXL_MAJOR_VERSION) [0-9]+\\)/\\1 ${major})/" \ @@ -1423,11 +1416,14 @@ cmd_bump_version() { # Check that the AUTHORS file contains the email of the committer. cmd_authors() { merge_request_commits - # TODO(deymo): Handle multiple commits and check that they are all the same - # author. - local email=$(git log --format='%ae' "${MR_HEAD_SHA}^!") - local name=$(git log --format='%an' "${MR_HEAD_SHA}^!") - "${MYDIR}"/tools/check_author.py "${email}" "${name}" + local emails + local names + readarray -t emails < <(git log --format='%ae' "${MR_HEAD_SHA}...${MR_ANCESTOR_SHA}") + readarray -t names < <(git log --format='%an' "${MR_HEAD_SHA}...${MR_ANCESTOR_SHA}") + for i in "${!names[@]}"; do + echo "Checking name '${names[$i]}' with email '${emails[$i]}' ..." + "${MYDIR}"/tools/check_author.py "${emails[$i]}" "${names[$i]}" + done } main() { @@ -1490,6 +1486,7 @@ You can pass some optional environment variables as well: - SKIP_TEST=1: Skip the test stage. - STORE_IMAGES=0: Makes the benchmark discard the computed images. - TEST_STACK_LIMIT: Stack size limit (ulimit -s) during tests, in KiB. + - TEST_SELECTOR: pass additional arguments to ctest, e.g. "-R .Resample.". - STACK_SIZE=1: Generate binaries with the .stack_sizes sections. These optional environment variables are forwarded to the cmake call as diff --git a/media/libjxl/src/cmake/FindBrotli.cmake b/media/libjxl/src/cmake/FindBrotli.cmake index 568356fe4c..5c6cb09870 100644 --- a/media/libjxl/src/cmake/FindBrotli.cmake +++ b/media/libjxl/src/cmake/FindBrotli.cmake @@ -26,7 +26,7 @@ foreach(brlib IN ITEMS ${brlibs}) ) if (${BRPREFIX}_LIBRARY AND NOT TARGET ${brlib}) - if(${CMAKE_VERSION} VERSION_LESS "3.13.5") + if(CMAKE_VERSION VERSION_LESS "3.13.5") add_library(${brlib} INTERFACE IMPORTED GLOBAL) set_property(TARGET ${brlib} PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${BROTLI_INCLUDE_DIR}) target_link_libraries(${brlib} INTERFACE ${${BRPREFIX}_LIBRARY}) diff --git a/media/libjxl/src/cmake/FindHWY.cmake b/media/libjxl/src/cmake/FindHWY.cmake index 267b4224ef..c1deb9b851 100644 --- a/media/libjxl/src/cmake/FindHWY.cmake +++ b/media/libjxl/src/cmake/FindHWY.cmake @@ -46,7 +46,7 @@ find_package_handle_standard_args(HWY if (HWY_LIBRARY AND NOT TARGET hwy) add_library(hwy INTERFACE IMPORTED GLOBAL) - if(${CMAKE_VERSION} VERSION_LESS "3.13.5") + if(CMAKE_VERSION VERSION_LESS "3.13.5") set_property(TARGET hwy PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${HWY_INCLUDE_DIR}) target_link_libraries(hwy INTERFACE ${HWY_LIBRARY}) set_property(TARGET hwy PROPERTY INTERFACE_COMPILE_OPTIONS ${PC_HWY_CFLAGS_OTHER}) diff --git a/media/libjxl/src/cmake/FindLCMS2.cmake b/media/libjxl/src/cmake/FindLCMS2.cmake index c36f7f9edd..0a7b54eb96 100644 --- a/media/libjxl/src/cmake/FindLCMS2.cmake +++ b/media/libjxl/src/cmake/FindLCMS2.cmake @@ -39,7 +39,7 @@ find_package_handle_standard_args(LCMS2 if (LCMS2_LIBRARY AND NOT TARGET lcms2) add_library(lcms2 INTERFACE IMPORTED GLOBAL) - if(${CMAKE_VERSION} VERSION_LESS "3.13.5") + if(CMAKE_VERSION VERSION_LESS "3.13.5") set_property(TARGET lcms2 PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${LCMS2_INCLUDE_DIR}) target_link_libraries(lcms2 INTERFACE ${LCMS2_LIBRARY}) set_property(TARGET lcms2 PROPERTY INTERFACE_COMPILE_OPTIONS ${PC_LCMS2_CFLAGS_OTHER}) diff --git a/media/libjxl/src/debian/changelog b/media/libjxl/src/debian/changelog index 1041baabf0..a63607e5e5 100644 --- a/media/libjxl/src/debian/changelog +++ b/media/libjxl/src/debian/changelog @@ -2,7 +2,7 @@ jpeg-xl (0.7) UNRELEASED; urgency=medium * Bump JPEG XL version to 0.7. - -- JPEG XL Maintainers <jpegxl@google.com> Fri, 10 Sep 2021 16:08:17 +0200 + -- JPEG XL Maintainers <jpegxl@google.com> Mon, 08 Aug 2022 14:43:58 +0000 jpeg-xl (0.6) unstable; urgency=medium diff --git a/media/libjxl/src/debian/control b/media/libjxl/src/debian/control index cab8e0e7f3..7a3c502e0e 100644 --- a/media/libjxl/src/debian/control +++ b/media/libjxl/src/debian/control @@ -9,7 +9,6 @@ Build-Depends: debhelper (>= 9), libbrotli-dev, libgdk-pixbuf-2.0-dev | libgdk-pixbuf2.0-dev, - libgflags-dev | libgflag-dev, libgif-dev, libgimp2.0-dev, libgmock-dev, diff --git a/media/libjxl/src/debian/copyright b/media/libjxl/src/debian/copyright index ca0c7922fc..20225a9209 100644 --- a/media/libjxl/src/debian/copyright +++ b/media/libjxl/src/debian/copyright @@ -38,27 +38,7 @@ License: BSD-3-clause (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -Files: third_party/testdata/third_party/imagecompression.info/* -Copyright: their respective owners. -License: License without any prohibitive copyright restrictions. - See https://imagecompression.info/test_images/ for details. - . - These Images are available without any prohibitive copyright restrictions. - . - These images are (c) there respective owners. You are granted full - redistribution and publication rights on these images provided: - . - 1. The origin of the pictures must not be misrepresented; you must not claim - that you took the original pictures. If you use, publish or redistribute them, - an acknowledgment would be appreciated but is not required. - 2. Altered versions must be plainly marked as such, and must not be - misinterpreted as being the originals. - 3. No payment is required for distribution of this material, it must be - available freely under the conditions stated here. That is, it is prohibited to - sell the material. - 4. This notice may not be removed or altered from any distribution. - -Files: third_party/testdata/third_party/pngsuite/* +Files: testdata/external/pngsuite/* Copyright: Willem van Schaik, 1996, 2011 License: PngSuite License See http://www.schaik.com/pngsuite/ for details. @@ -66,15 +46,15 @@ License: PngSuite License Permission to use, copy, modify and distribute these images for any purpose and without fee is hereby granted. -Files: third_party/testdata/raw.pixls/* +Files: testdata/external/raw.pixls/* Copyright: their respective owners listed in https://raw.pixls.us/ License: CC0-1.0 -Files: third_party/testdata/raw.pixls/* +Files: testdata/external/wesaturate/* Copyright: their respective owners listed in https://www.wesaturate.com/ License: CC0-1.0 -Files: third_party/testdata/third_party/wide-gamut-tests/ +Files: testdata/external/wide-gamut-tests/ Copyright: github.com/codelogic/wide-gamut-tests authors. License: Apache-2.0 diff --git a/media/libjxl/src/deps.sh b/media/libjxl/src/deps.sh index 907589b267..9aaabba2e0 100755 --- a/media/libjxl/src/deps.sh +++ b/media/libjxl/src/deps.sh @@ -14,8 +14,7 @@ MYDIR=$(dirname $(realpath "$0")) # Git revisions we use for the given submodules. Update these whenever you # update a git submodule. THIRD_PARTY_BROTLI="35ef5c554d888bef217d449346067de05e269b30" -THIRD_PARTY_GFLAGS="827c769e5fc98e0f2a34c47cef953cc6328abced" -THIRD_PARTY_HIGHWAY="f13e3b956eb226561ac79427893ec0afd66f91a8" +THIRD_PARTY_HIGHWAY="22e3d7276f4157d4a47586ba9fd91dd6303f441a" THIRD_PARTY_SKCMS="64374756e03700d649f897dbd98c95e78c30c7da" THIRD_PARTY_SJPEG="868ab558fad70fcbe8863ba4e85179eeb81cc840" THIRD_PARTY_ZLIB="cacf7f1d4e3d44d871b605da3b647f07d718623f" @@ -73,7 +72,6 @@ EOF # Sources downloaded from a tarball. download_github third_party/brotli google/brotli - download_github third_party/gflags gflags/gflags download_github third_party/highway google/highway download_github third_party/sjpeg webmproject/sjpeg download_github third_party/skcms \ diff --git a/media/libjxl/src/doc/developing_in_debian.md b/media/libjxl/src/doc/developing_in_debian.md index fd349fb675..a88b682ff3 100644 --- a/media/libjxl/src/doc/developing_in_debian.md +++ b/media/libjxl/src/doc/developing_in_debian.md @@ -12,7 +12,7 @@ Apart from the dependencies in `third_party`, some of the tools use external dependencies that need to be installed on your system first: ```bash -sudo apt install cmake clang doxygen g++ extra-cmake-modules libgflags-dev \ +sudo apt install cmake clang doxygen g++ extra-cmake-modules \ libgif-dev libjpeg-dev ninja-build libgoogle-perftools-dev ``` diff --git a/media/libjxl/src/doc/release.md b/media/libjxl/src/doc/release.md index b1b903bb27..70f1278a42 100644 --- a/media/libjxl/src/doc/release.md +++ b/media/libjxl/src/doc/release.md @@ -21,12 +21,15 @@ been merged to `main`, resulting in some errors being detected hours after the code is merged or even days after in the case of fuzzer-detected bugs. Release tags are cut from *release branches*. Each MAJOR.MINOR version has its -own release branch, for example releases `0.5`, `0.5.1`, `0.5.2`, ... would have -tags `v0.5`, `v0.5.1`, `v0.5.2`, ... on commits from the `v0.5.x` branch. -`v0.5.x` is a branch name, not a tag name, and doesn't represent a released +own release branch, for example releases `0.7.0`, `0.7.1`, `0.7.2`, ... would +have tags `v0.7.0`, `v0.7.1`, `v0.7.2`, ... on commits from the `v0.7.x` branch. +`v0.7.x` is a branch name, not a tag name, and doesn't represent a released version since semantic versioning requires that the PATCH is a non-negative number. Released tags don't each one have their own release branch, all releases -from the same MAJOR.MINOR version will share the same branch. +from the same MAJOR.MINOR version will share the same branch. The first commit +after the branch-off points between the main branch and the release branch +should be tagged with the suffix `-snapshot` and the name of the next +MAJOR.MINOR version, in order to get meaningful ouput for `git --describe`. The main purpose of the release branch is to stabilize the code before a release. This involves including fixes to existing bugs but **not** including @@ -36,7 +39,7 @@ branch into the release branch without including the new *features* from `main`. For this reason it is important to make small commits in `main` and separate bug fixes from new features. -After the initial minor release (`M.N`, for example `0.5.0` or just `0.5`) the +After the initial minor release (`MAJOR.MINOR.PATCH`, for example `0.5.0`) the release branch is used to continue to cherry-pick fixes to be included in a patch release, for example a version `0.5.1` release. Patch fixes are only meant to fix security bugs or other critical bugs that can't wait until the next major @@ -114,12 +117,12 @@ branch is created the code in `main` will only be included in the next major or minor release. Right after a release branch update the version targeting the next release. Artifacts from `main` should include the new (unreleased) version, so it is important to update it. For example, after the `v0.5.x` branch is -created from main, you should update the version on `main` to `0.6`. +created from main, you should update the version on `main` to `0.6.0`. To help update it, run this helper command (in a Debian-based system): ```bash -./ci.sh bump_version 0.6 +./ci.sh bump_version 0.6.0 ``` This will update the version in the following files: @@ -248,12 +251,10 @@ To publish a release open the [New Release page](https://github.com/libjxl/libjxl/releases/new) and follow these instructions: - * Set the "Tag version" as "v" plus the semantic version number. Omit the ".0" - when the PATCH version is 0, for example use "v0.5" or "v0.5.1" but not - "v0.5.0". + * Set the "Tag version" as "v" plus the semantic version number. - * Select the "Target" as your release branch. For a "v0.5" release tag you - would use the "v0.5.x" branch. + * Select the "Target" as your release branch. For example for a "v0.7.1" + release tag you should use the "v0.7.x" branch. * Use the version number as the release title. diff --git a/media/libjxl/src/doc/software_support.md b/media/libjxl/src/doc/software_support.md index b9be07ada3..9dddad7de7 100644 --- a/media/libjxl/src/doc/software_support.md +++ b/media/libjxl/src/doc/software_support.md @@ -24,6 +24,8 @@ Please add missing software to this list. - [ImageMagick](https://imagemagick.org/): supported since 7.0.10-54 - [libvips](https://libvips.github.io/libvips/): supported since 8.11 - [Imlib2](https://github.com/alistair7/imlib2-jxl) +- [FFmpeg](https://github.com/FFmpeg/FFmpeg/search?q=jpeg-xl&type=commits) +- [GDAL](https://gdal.org/drivers/raster/jpegxl.html): supported since 3.4.0 as a TIFF codec, and 3.6.0 as standalone format ## OS-level support / UI frameworks / file browser plugins @@ -35,10 +37,12 @@ Please add missing software to this list. - [Windows thumbnail handler](https://github.com/saschanaz/jxl-winthumb) - [OpenMandriva Lx (since 4.3 RC)](https://www.openmandriva.org/en/news/article/openmandriva-lx-4-3-rc-available-for-testing) - [KaOS (since 2021.06)](https://news.itsfoss.com/kaos-2021-06-release/) +- [EFL (since 1.27, no external plugin needed)](https://www.enlightenment.org) ## Image editors - [GIMP (since 2.99.8)](https://www.gimp.org/news/2021/10/20/gimp-2-99-8-released/); plugin for older versions available in libjxl repo +- [Krita](https://invent.kde.org/graphics/krita/-/commit/13e5d2e5b9f0eac5c8064b7767f0b62264a0797b) - Photoshop: no plugin available yet, no official support yet ## Image viewers @@ -46,9 +50,12 @@ Please add missing software to this list. - [XnView](https://www.xnview.com/en/) - [ImageGlass](https://imageglass.org/) - [IrfanView](https://www.irfanview.com/); supported since 4.59 - requires a [plugin](https://www.irfanview.com/plugins.htm) to be downloaded and enabled. -- Any viewer based on Qt, KDE, GDK-pixbuf, ImageMagick, libvips or imlib2 (see above) +- [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.12.1) +- Any viewer based on Qt, KDE, GDK-pixbuf, EFL, ImageMagick, libvips or imlib2 (see above) - Qt viewers: gwenview, digiKam, KolourPaint, KPhotoAlbum, LXImage-Qt, qimgv, qView, nomacs, VookiImageViewer, PhotoQt - GTK viewers: Eye of Gnome (eog), gThumb, Geeqie + - EFL viewers: entice, ephoto +- [Swayimg](https://github.com/artemsen/swayimg) ## Online tools diff --git a/media/libjxl/src/js-wasm-wrapper.sh b/media/libjxl/src/js-wasm-wrapper.sh deleted file mode 100755 index fb91c5578c..0000000000 --- a/media/libjxl/src/js-wasm-wrapper.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/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. - -# Continuous integration helper module. This module is meant to be called from -# the .gitlab-ci.yml file during the continuous integration build, as well as -# from the command line for developers. - -# This wrapper is used to enable WASM SIMD when running tests. -# Unfortunately, it is impossible to pass the option directly via the -# CMAKE_CROSSCOMPILING_EMULATOR variable. - -# Fallback to default v8 binary, if override is not set. -V8="${V8:-$(which v8)}" -SCRIPT="$1" -shift -"${V8}" --experimental-wasm-simd "${SCRIPT}" -- "$@" diff --git a/media/libjxl/src/lib/CMakeLists.txt b/media/libjxl/src/lib/CMakeLists.txt index 4bb4ebd35e..5c8e0bada5 100644 --- a/media/libjxl/src/lib/CMakeLists.txt +++ b/media/libjxl/src/lib/CMakeLists.txt @@ -52,7 +52,7 @@ set(JPEGXL_INTERNAL_FLAGS ) # Warning flags supported by clang. -if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") list(APPEND JPEGXL_INTERNAL_FLAGS -Wdeprecated-increment-bool # TODO(deymo): Add -Wextra-semi once we update third_party/highway. @@ -90,7 +90,7 @@ if (WIN32) -Wno-zero-as-null-pointer-constant ) - if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") list(APPEND JPEGXL_INTERNAL_FLAGS -Wno-used-but-marked-unused -Wno-unused-template @@ -110,7 +110,7 @@ else() # WIN32 -fmath-errno ) - if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") list(APPEND JPEGXL_INTERNAL_FLAGS -fnew-alignment=8 -fno-cxx-exceptions @@ -136,7 +136,9 @@ endif() #!MSVC include(jxl.cmake) # Other libraries outside the core jxl library. -include(jxl_extras.cmake) +if(JPEGXL_ENABLE_TOOLS) + include(jxl_extras.cmake) +endif() include(jxl_threads.cmake) # Install all the library headers from the source and the generated ones. There diff --git a/media/libjxl/src/lib/extras/codec.cc b/media/libjxl/src/lib/extras/codec.cc index 80ed7c867c..774b4ccb6e 100644 --- a/media/libjxl/src/lib/extras/codec.cc +++ b/media/libjxl/src/lib/extras/codec.cc @@ -52,7 +52,7 @@ Status SetFromBytes(const Span<const uint8_t> bytes, Status SetFromFile(const std::string& pathname, const extras::ColorHints& color_hints, CodecInOut* io, ThreadPool* pool, extras::Codec* orig_codec) { - PaddedBytes encoded; + std::vector<uint8_t> encoded; JXL_RETURN_IF_ERROR(ReadFile(pathname, &encoded)); JXL_RETURN_IF_ERROR(SetFromBytes(Span<const uint8_t>(encoded), color_hints, io, pool, orig_codec)); @@ -61,67 +61,66 @@ Status SetFromFile(const std::string& pathname, Status Encode(const CodecInOut& io, const extras::Codec codec, const ColorEncoding& c_desired, size_t bits_per_sample, - PaddedBytes* bytes, ThreadPool* pool) { + std::vector<uint8_t>* bytes, ThreadPool* pool) { JXL_CHECK(!io.Main().c_current().ICC().empty()); JXL_CHECK(!c_desired.ICC().empty()); io.CheckMetadata(); if (io.Main().IsJPEG()) { JXL_WARNING("Writing JPEG data as pixels"); } - - extras::PackedPixelFile ppf; - size_t num_channels = io.metadata.m.color_encoding.Channels(); JxlPixelFormat format = { - static_cast<uint32_t>(num_channels), - bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, - JXL_NATIVE_ENDIAN, 0}; - std::vector<uint8_t> bytes_vector; + 0, // num_channels is ignored by the converter + bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, JXL_BIG_ENDIAN, + 0}; const bool floating_point = bits_per_sample > 16; + std::unique_ptr<extras::Encoder> encoder; + std::ostringstream os; switch (codec) { case extras::Codec::kPNG: #if JPEGXL_ENABLE_APNG - return extras::EncodeImageAPNG(&io, c_desired, bits_per_sample, pool, - bytes); + encoder = extras::GetAPNGEncoder(); + break; #else return JXL_FAILURE("JPEG XL was built without (A)PNG support"); #endif case extras::Codec::kJPG: #if JPEGXL_ENABLE_JPEG - return EncodeImageJPG(&io, - io.use_sjpeg ? extras::JpegEncoder::kSJpeg - : extras::JpegEncoder::kLibJpeg, - io.jpeg_quality, YCbCrChromaSubsampling(), pool, - bytes); + format.data_type = JXL_TYPE_UINT8; + encoder = extras::GetJPEGEncoder(); + os << io.jpeg_quality; + encoder->SetOption("q", os.str()); + break; #else return JXL_FAILURE("JPEG XL was built without JPEG support"); #endif case extras::Codec::kPNM: - - // Choose native for PFM; PGM/PPM require big-endian (N/A for PBM) - format.endianness = floating_point ? JXL_NATIVE_ENDIAN : JXL_BIG_ENDIAN; - if (floating_point) { + if (io.Main().HasAlpha()) { + encoder = extras::GetPAMEncoder(); + } else if (io.Main().IsGray()) { + encoder = extras::GetPGMEncoder(); + } else if (!floating_point) { + encoder = extras::GetPPMEncoder(); + } else { format.data_type = JXL_TYPE_FLOAT; + format.endianness = JXL_NATIVE_ENDIAN; + encoder = extras::GetPFMEncoder(); } if (!c_desired.IsSRGB()) { JXL_WARNING( - "PNM encoder cannot store custom ICC profile; decoder\n" + "PNM encoder cannot store custom ICC profile; decoder " "will need hint key=color_space to get the same values"); } - JXL_RETURN_IF_ERROR(extras::ConvertCodecInOutToPackedPixelFile( - io, format, c_desired, pool, &ppf)); - JXL_RETURN_IF_ERROR(extras::EncodeImagePNM( - ppf, bits_per_sample, pool, /*frame_index=*/0, &bytes_vector)); - bytes->assign(bytes_vector.data(), - bytes_vector.data() + bytes_vector.size()); - return true; + break; case extras::Codec::kPGX: - return extras::EncodeImagePGX(&io, c_desired, bits_per_sample, pool, - bytes); + encoder = extras::GetPGXEncoder(); + break; case extras::Codec::kGIF: return JXL_FAILURE("Encoding to GIF is not implemented"); case extras::Codec::kEXR: #if JPEGXL_ENABLE_EXR - return extras::EncodeImageEXR(&io, c_desired, pool, bytes); + format.data_type = JXL_TYPE_FLOAT; + encoder = extras::GetEXREncoder(); + break; #else return JXL_FAILURE("JPEG XL was built without OpenEXR support"); #endif @@ -129,7 +128,19 @@ Status Encode(const CodecInOut& io, const extras::Codec codec, return JXL_FAILURE("Cannot encode using Codec::kUnknown"); } - return JXL_FAILURE("Invalid codec"); + if (!encoder) { + return JXL_FAILURE("Invalid codec."); + } + + extras::PackedPixelFile ppf; + JXL_RETURN_IF_ERROR( + ConvertCodecInOutToPackedPixelFile(io, format, c_desired, pool, &ppf)); + extras::EncodedImage encoded_image; + JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded_image, pool)); + JXL_ASSERT(encoded_image.bitstreams.size() == 1); + *bytes = encoded_image.bitstreams[0]; + + return true; } Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, @@ -163,7 +174,7 @@ Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, bits_per_sample = 16; } - PaddedBytes encoded; + std::vector<uint8_t> encoded; return Encode(io, codec, c_desired, bits_per_sample, &encoded, pool) && WriteFile(encoded, pathname); } diff --git a/media/libjxl/src/lib/extras/codec.h b/media/libjxl/src/lib/extras/codec.h index 88e96d308e..73fdc80be9 100644 --- a/media/libjxl/src/lib/extras/codec.h +++ b/media/libjxl/src/lib/extras/codec.h @@ -49,7 +49,7 @@ Status SetFromFile(const std::string& pathname, // color space to c_desired. Status Encode(const CodecInOut& io, extras::Codec codec, const ColorEncoding& c_desired, size_t bits_per_sample, - PaddedBytes* bytes, ThreadPool* pool = nullptr); + std::vector<uint8_t>* bytes, ThreadPool* pool = nullptr); // Deduces codec, calls Encode and writes to file. Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, diff --git a/media/libjxl/src/lib/extras/codec_test.cc b/media/libjxl/src/lib/extras/codec_test.cc index d7d9b86954..19cac39979 100644 --- a/media/libjxl/src/lib/extras/codec_test.cc +++ b/media/libjxl/src/lib/extras/codec_test.cc @@ -9,12 +9,15 @@ #include <stdio.h> #include <algorithm> +#include <sstream> +#include <string> #include <utility> #include <vector> -#include "gtest/gtest.h" #include "lib/extras/dec/pgx.h" #include "lib/extras/dec/pnm.h" +#include "lib/extras/enc/encode.h" +#include "lib/extras/packed_image_convert.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/base/random.h" #include "lib/jxl/base/thread_pool_internal.h" @@ -23,12 +26,20 @@ #include "lib/jxl/image.h" #include "lib/jxl/image_bundle.h" #include "lib/jxl/image_test_utils.h" +#include "lib/jxl/test_utils.h" #include "lib/jxl/testdata.h" namespace jxl { namespace extras { namespace { +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::NotNull; +using ::testing::SizeIs; + std::string ExtensionFromCodec(Codec codec, const bool is_gray, const bool has_alpha, const size_t bits_per_sample) { @@ -54,159 +65,233 @@ std::string ExtensionFromCodec(Codec codec, const bool is_gray, return std::string(); } -CodecInOut CreateTestImage(const size_t xsize, const size_t ysize, - const bool is_gray, const bool add_alpha, - const size_t bits_per_sample, - const ColorEncoding& c_native) { - Image3F image(xsize, ysize); - Rng rng(129); - if (is_gray) { - for (size_t y = 0; y < ysize; ++y) { - float* JXL_RESTRICT row0 = image.PlaneRow(0, y); - float* JXL_RESTRICT row1 = image.PlaneRow(1, y); - float* JXL_RESTRICT row2 = image.PlaneRow(2, y); - for (size_t x = 0; x < xsize; ++x) { - row0[x] = row1[x] = row2[x] = rng.UniformF(0.0f, 1.0f); +void VerifySameImage(const PackedImage& im0, size_t bits_per_sample0, + const PackedImage& im1, size_t bits_per_sample1, + bool lossless = true) { + ASSERT_EQ(im0.xsize, im1.xsize); + ASSERT_EQ(im0.ysize, im1.ysize); + ASSERT_EQ(im0.format.num_channels, im1.format.num_channels); + auto get_factor = [](JxlPixelFormat f, size_t bits) -> double { + return 1.0 / ((1u << std::min(test::GetPrecision(f.data_type), bits)) - 1); + }; + double factor0 = get_factor(im0.format, bits_per_sample0); + double factor1 = get_factor(im1.format, bits_per_sample1); + auto pixels0 = static_cast<const uint8_t*>(im0.pixels()); + auto pixels1 = static_cast<const uint8_t*>(im1.pixels()); + auto rgba0 = + test::ConvertToRGBA32(pixels0, im0.xsize, im0.ysize, im0.format, factor0); + auto rgba1 = + test::ConvertToRGBA32(pixels1, im1.xsize, im1.ysize, im1.format, factor1); + double tolerance = + lossless ? 0.5 * std::min(factor0, factor1) : 3.0f / 255.0f; + if (bits_per_sample0 == 32 || bits_per_sample1 == 32) { + tolerance = 0.5 * std::max(factor0, factor1); + } + for (size_t y = 0; y < im0.ysize; ++y) { + for (size_t x = 0; x < im0.xsize; ++x) { + for (size_t c = 0; c < im0.format.num_channels; ++c) { + size_t ix = (y * im0.xsize + x) * 4 + c; + double val0 = rgba0[ix]; + double val1 = rgba1[ix]; + ASSERT_NEAR(val1, val0, tolerance) + << "y = " << y << " x = " << x << " c = " << c; } } - } else { - RandomFillImage(&image, 0.0f, 1.0f); } - CodecInOut io; +} + +JxlColorEncoding CreateTestColorEncoding(bool is_gray) { + JxlColorEncoding c; + c.color_space = is_gray ? JXL_COLOR_SPACE_GRAY : JXL_COLOR_SPACE_RGB; + c.white_point = JXL_WHITE_POINT_D65; + c.primaries = JXL_PRIMARIES_P3; + c.rendering_intent = JXL_RENDERING_INTENT_RELATIVE; + c.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR; + // Roundtrip through internal color encoding to fill in primaries and white + // point CIE xy coordinates. + ColorEncoding c_internal; + JXL_CHECK(ConvertExternalToInternalColorEncoding(c, &c_internal)); + ConvertInternalToExternalColorEncoding(c_internal, &c); + return c; +} - if (bits_per_sample == 32) { - io.metadata.m.SetFloat32Samples(); +std::vector<uint8_t> GenerateICC(JxlColorEncoding color_encoding) { + ColorEncoding c; + JXL_CHECK(ConvertExternalToInternalColorEncoding(color_encoding, &c)); + JXL_CHECK(c.CreateICC()); + PaddedBytes icc = c.ICC(); + return std::vector<uint8_t>(icc.begin(), icc.end()); +} + +void StoreRandomValue(uint8_t* out, Rng* rng, JxlPixelFormat format, + size_t bits_per_sample) { + uint64_t max_val = (1ull << bits_per_sample) - 1; + if (format.data_type == JXL_TYPE_UINT8) { + *out = rng->UniformU(0, max_val); + } else if (format.data_type == JXL_TYPE_UINT16) { + uint32_t val = rng->UniformU(0, max_val); + if (format.endianness == JXL_BIG_ENDIAN) { + StoreBE16(val, out); + } else { + StoreLE16(val, out); + } } else { - io.metadata.m.SetUintSamples(bits_per_sample); - } - io.metadata.m.color_encoding = c_native; - io.SetFromImage(std::move(image), c_native); - if (add_alpha) { - ImageF alpha(xsize, ysize); - RandomFillImage(&alpha, 0.0f, 1.f); - io.metadata.m.SetAlphaBits(bits_per_sample <= 8 ? 8 : 16); - io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + ASSERT_EQ(format.data_type, JXL_TYPE_FLOAT); + float val = rng->UniformF(0.0, 1.0); + uint32_t uval; + memcpy(&uval, &val, 4); + if (format.endianness == JXL_BIG_ENDIAN) { + StoreBE32(uval, out); + } else { + StoreLE32(uval, out); + } } - return io; } -// Ensures reading a newly written file leads to the same image pixels. -void TestRoundTrip(Codec codec, const size_t xsize, const size_t ysize, - const bool is_gray, const bool add_alpha, - const size_t bits_per_sample, ThreadPool* pool) { - // JPEG encoding is not lossless. - if (codec == Codec::kJPG) return; - if (codec == Codec::kPNM && add_alpha) return; - // Our EXR codec always uses 16-bit premultiplied alpha, does not support - // grayscale, and somehow does not have sufficient precision for this test. - if (codec == Codec::kEXR) return; - printf("Codec %s bps:%" PRIuS " gr:%d al:%d\n", - ExtensionFromCodec(codec, is_gray, add_alpha, bits_per_sample).c_str(), - bits_per_sample, is_gray, add_alpha); - - ColorEncoding c_native; - c_native.SetColorSpace(is_gray ? ColorSpace::kGray : ColorSpace::kRGB); - // Note: this must not be wider than c_external, otherwise gamut clipping - // will cause large round-trip errors. - c_native.primaries = Primaries::kP3; - c_native.tf.SetTransferFunction(TransferFunction::kLinear); - JXL_CHECK(c_native.CreateICC()); - - // Generally store same color space to reduce round trip errors.. - ColorEncoding c_external = c_native; - // .. unless we have enough precision for some transforms. - if (bits_per_sample >= 16) { - c_external.white_point = WhitePoint::kE; - c_external.primaries = Primaries::k2100; - c_external.tf.SetTransferFunction(TransferFunction::kSRGB); +void FillPackedImage(size_t bits_per_sample, PackedImage* image) { + JxlPixelFormat format = image->format; + size_t bytes_per_channel = PackedImage::BitsPerChannel(format.data_type) / 8; + uint8_t* out = static_cast<uint8_t*>(image->pixels()); + size_t stride = image->xsize * format.num_channels * bytes_per_channel; + ASSERT_EQ(image->pixels_size, image->ysize * stride); + Rng rng(129); + for (size_t y = 0; y < image->ysize; ++y) { + for (size_t x = 0; x < image->xsize; ++x) { + for (size_t c = 0; c < format.num_channels; ++c) { + StoreRandomValue(out, &rng, format, bits_per_sample); + out += bytes_per_channel; + } + } } - JXL_CHECK(c_external.CreateICC()); +} - const CodecInOut io = CreateTestImage(xsize, ysize, is_gray, add_alpha, - bits_per_sample, c_native); - const ImageBundle& ib1 = io.Main(); +struct TestImageParams { + Codec codec; + size_t xsize; + size_t ysize; + size_t bits_per_sample; + bool is_gray; + bool add_alpha; + bool big_endian; + + bool ShouldTestRoundtrip() const { + if (codec == Codec::kPNG) { + return true; + } else if (codec == Codec::kPNM) { + // TODO(szabadka) Make PNM encoder endianness-aware. + return ((bits_per_sample <= 16 && big_endian) || + (bits_per_sample == 32 && !add_alpha && !big_endian)); + } else if (codec == Codec::kPGX) { + return ((bits_per_sample == 8 || bits_per_sample == 16) && is_gray && + !add_alpha); + } else if (codec == Codec::kEXR) { +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) + // OpenEXR 2.3 has a memory leak in IlmThread_2_3::ThreadPool + return false; +#else + return bits_per_sample == 32 && !is_gray; +#endif + } else if (codec == Codec::kJPG) { + return bits_per_sample == 8 && !add_alpha; + } else { + return false; + } + } - PaddedBytes encoded; - JXL_CHECK(Encode(io, codec, c_external, bits_per_sample, &encoded, pool)); + JxlPixelFormat PixelFormat() const { + JxlPixelFormat format; + format.num_channels = (is_gray ? 1 : 3) + (add_alpha ? 1 : 0); + format.data_type = (bits_per_sample == 32 ? JXL_TYPE_FLOAT + : bits_per_sample > 8 ? JXL_TYPE_UINT16 + : JXL_TYPE_UINT8); + format.endianness = big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN; + format.align = 0; + return format; + } - CodecInOut io2; - ColorHints color_hints; - // Only for PNM because PNG will warn about ignoring them. - if (codec == Codec::kPNM) { - color_hints.Add("color_space", Description(c_external)); + std::string DebugString() const { + std::ostringstream os; + os << "bps:" << bits_per_sample << " gr:" << is_gray << " al:" << add_alpha + << " be: " << big_endian; + return os.str(); } - JXL_CHECK(SetFromBytes(Span<const uint8_t>(encoded), color_hints, &io2, pool, - nullptr)); - ImageBundle& ib2 = io2.Main(); +}; + +void CreateTestImage(const TestImageParams& params, PackedPixelFile* ppf) { + ppf->info.xsize = params.xsize; + ppf->info.ysize = params.ysize; + ppf->info.bits_per_sample = params.bits_per_sample; + ppf->info.exponent_bits_per_sample = params.bits_per_sample == 32 ? 8 : 0; + ppf->info.num_color_channels = params.is_gray ? 1 : 3; + ppf->info.alpha_bits = params.add_alpha ? params.bits_per_sample : 0; + ppf->info.alpha_premultiplied = (params.codec == Codec::kEXR); + + JxlColorEncoding color_encoding = CreateTestColorEncoding(params.is_gray); + ppf->icc = GenerateICC(color_encoding); + ppf->color_encoding = color_encoding; + + PackedFrame frame(params.xsize, params.ysize, params.PixelFormat()); + FillPackedImage(params.bits_per_sample, &frame.color); + ppf->frames.emplace_back(std::move(frame)); +} - EXPECT_EQ(Description(c_external), - Description(io2.metadata.m.color_encoding)); +// Ensures reading a newly written file leads to the same image pixels. +void TestRoundTrip(const TestImageParams& params, ThreadPool* pool) { + if (!params.ShouldTestRoundtrip()) return; - // See c_external above - for low bits_per_sample the encoded space is - // already the same. - if (bits_per_sample < 16) { - EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current())); - } + std::string extension = ExtensionFromCodec( + params.codec, params.is_gray, params.add_alpha, params.bits_per_sample); + printf("Codec %s %s\n", extension.c_str(), params.DebugString().c_str()); - if (add_alpha) { - EXPECT_TRUE(SamePixels(ib1.alpha(), *ib2.alpha())); - } + PackedPixelFile ppf_in; + CreateTestImage(params, &ppf_in); - JXL_CHECK(ib2.TransformTo(ib1.c_current(), GetJxlCms(), pool)); + EncodedImage encoded; + auto encoder = Encoder::FromExtension(extension); + ASSERT_TRUE(encoder.get()); + ASSERT_TRUE(encoder->Encode(ppf_in, &encoded, pool)); + ASSERT_EQ(encoded.bitstreams.size(), 1); - double max_l1, max_rel; - // Round-trip tolerances must be higher than in external_image_test because - // codecs do not support unbounded ranges. -#if JPEGXL_ENABLE_SKCMS - if (bits_per_sample <= 12) { - max_l1 = 0.5; - max_rel = 6E-3; - } else { - max_l1 = 1E-3; - max_rel = 5E-4; - } -#else // JPEGXL_ENABLE_SKCMS - if (bits_per_sample <= 12) { - max_l1 = 0.5; - max_rel = 6E-3; - } else if (bits_per_sample == 16) { - max_l1 = 3E-3; - max_rel = 1E-4; - } else { -#ifdef __ARM_ARCH - // pow() implementation in arm is a bit less precise than in x86 and - // therefore we need a bigger error margin in this case. - max_l1 = 1E-7; - max_rel = 1E-4; -#else - max_l1 = 1E-7; - max_rel = 1E-5; -#endif + PackedPixelFile ppf_out; + ASSERT_TRUE(DecodeBytes(Span<const uint8_t>(encoded.bitstreams[0]), + ColorHints(), SizeConstraints(), &ppf_out)); + + if (params.codec != Codec::kPNM && params.codec != Codec::kPGX && + params.codec != Codec::kEXR) { + EXPECT_EQ(ppf_in.icc, ppf_out.icc); } -#endif // JPEGXL_ENABLE_SKCMS - VerifyRelativeError(ib1.color(), *ib2.color(), max_l1, max_rel); + ASSERT_EQ(ppf_out.frames.size(), 1); + VerifySameImage(ppf_in.frames[0].color, ppf_in.info.bits_per_sample, + ppf_out.frames[0].color, ppf_out.info.bits_per_sample, + /*lossless=*/params.codec != Codec::kJPG); } -#if 0 TEST(CodecTest, TestRoundTrip) { ThreadPoolInternal pool(12); - const size_t xsize = 7; - const size_t ysize = 4; + TestImageParams params; + params.xsize = 7; + params.ysize = 4; - for (Codec codec : Values<Codec>()) { - for (int bits_per_sample : {8, 10, 12, 16, 32}) { + for (Codec codec : AvailableCodecs()) { + for (int bits_per_sample : {4, 8, 10, 12, 16, 32}) { for (bool is_gray : {false, true}) { for (bool add_alpha : {false, true}) { - TestRoundTrip(codec, xsize, ysize, is_gray, add_alpha, - static_cast<size_t>(bits_per_sample), &pool); + for (bool big_endian : {false, true}) { + params.codec = codec; + params.bits_per_sample = static_cast<size_t>(bits_per_sample); + params.is_gray = is_gray; + params.add_alpha = add_alpha; + params.big_endian = big_endian; + TestRoundTrip(params, &pool); + } } } } } } -#endif CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool, const ColorHints& color_hints = ColorHints()) { @@ -217,7 +302,7 @@ CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool, const ImageBundle& ib1 = io.Main(); // Encode/Decode again to make sure Encode carries through all metadata. - PaddedBytes encoded; + std::vector<uint8_t> encoded; JXL_CHECK(Encode(io, Codec::kPNG, io.metadata.m.color_encoding, io.metadata.m.bit_depth.bits_per_sample, &encoded, pool)); @@ -256,11 +341,11 @@ CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool, TEST(CodecTest, TestMetadataSRGB) { ThreadPoolInternal pool(12); - const char* paths[] = {"third_party/raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png", - "third_party/raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png", - "third_party/raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png", - "third_party/raw.pixls/Nikon-D300-12bit_srgb8_dt.png", - "third_party/raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"}; + const char* paths[] = {"external/raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png", + "external/raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png", + "external/raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png", + "external/raw.pixls/Nikon-D300-12bit_srgb8_dt.png", + "external/raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"}; for (const char* relative_pathname : paths) { const CodecInOut io = DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool); @@ -285,9 +370,9 @@ TEST(CodecTest, TestMetadataLinear) { ThreadPoolInternal pool(12); const char* paths[3] = { - "third_party/raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png", - "third_party/raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png", - "third_party/raw.pixls/Nikon-D300-12bit_2020_g1_dt.png", + "external/raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png", + "external/raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png", + "external/raw.pixls/Nikon-D300-12bit_2020_g1_dt.png", }; const WhitePoint white_points[3] = {WhitePoint::kCustom, WhitePoint::kD65, WhitePoint::kD65}; @@ -317,8 +402,8 @@ TEST(CodecTest, TestMetadataICC) { ThreadPoolInternal pool(12); const char* paths[] = { - "third_party/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png", - "third_party/raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png", + "external/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png", + "external/raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png", }; for (const char* relative_pathname : paths) { const CodecInOut io = @@ -340,28 +425,28 @@ TEST(CodecTest, TestMetadataICC) { } } -TEST(CodecTest, Testthird_party/pngsuite) { +TEST(CodecTest, Testexternal/pngsuite) { ThreadPoolInternal pool(12); // Ensure we can load PNG with text, japanese UTF-8, compressed text. - (void)DecodeRoundtrip("third_party/pngsuite/ct1n0g04.png", Codec::kPNG, &pool); - (void)DecodeRoundtrip("third_party/pngsuite/ctjn0g04.png", Codec::kPNG, &pool); - (void)DecodeRoundtrip("third_party/pngsuite/ctzn0g04.png", Codec::kPNG, &pool); + (void)DecodeRoundtrip("external/pngsuite/ct1n0g04.png", Codec::kPNG, &pool); + (void)DecodeRoundtrip("external/pngsuite/ctjn0g04.png", Codec::kPNG, &pool); + (void)DecodeRoundtrip("external/pngsuite/ctzn0g04.png", Codec::kPNG, &pool); // Extract gAMA const CodecInOut b1 = - DecodeRoundtrip("third_party/pngsuite/g10n3p04.png", Codec::kPNG, &pool); + DecodeRoundtrip("external/pngsuite/g10n3p04.png", Codec::kPNG, &pool); EXPECT_TRUE(b1.metadata.color_encoding.tf.IsLinear()); // Extract cHRM const CodecInOut b_p = - DecodeRoundtrip("third_party/pngsuite/ccwn2c08.png", Codec::kPNG, &pool); + DecodeRoundtrip("external/pngsuite/ccwn2c08.png", Codec::kPNG, &pool); EXPECT_EQ(Primaries::kSRGB, b_p.metadata.color_encoding.primaries); EXPECT_EQ(WhitePoint::kD65, b_p.metadata.color_encoding.white_point); // Extract EXIF from (new-style) dedicated chunk const CodecInOut b_exif = - DecodeRoundtrip("third_party/pngsuite/exif2c08.png", Codec::kPNG, &pool); + DecodeRoundtrip("external/pngsuite/exif2c08.png", Codec::kPNG, &pool); EXPECT_EQ(978, b_exif.blobs.exif.size()); } #endif @@ -384,18 +469,88 @@ void VerifyWideGamutMetadata(const std::string& relative_pathname, TEST(CodecTest, TestWideGamut) { ThreadPoolInternal pool(12); - // VerifyWideGamutMetadata("third_party/wide-gamut-tests/P3-sRGB-color-bars.png", + // VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-bars.png", // Primaries::kP3, &pool); - VerifyWideGamutMetadata("third_party/wide-gamut-tests/P3-sRGB-color-ring.png", + VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-ring.png", Primaries::kP3, &pool); - // VerifyWideGamutMetadata("third_party/wide-gamut-tests/R2020-sRGB-color-bars.png", + // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-bars.png", // Primaries::k2100, &pool); - // VerifyWideGamutMetadata("third_party/wide-gamut-tests/R2020-sRGB-color-ring.png", + // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-ring.png", // Primaries::k2100, &pool); } TEST(CodecTest, TestPNM) { TestCodecPNM(); } +TEST(CodecTest, FormatNegotiation) { + const std::vector<JxlPixelFormat> accepted_formats = { + {/*num_channels=*/4, + /*data_type=*/JXL_TYPE_UINT16, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}, + {/*num_channels=*/3, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}, + {/*num_channels=*/3, + /*data_type=*/JXL_TYPE_UINT16, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}, + {/*num_channels=*/1, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}, + }; + + JxlBasicInfo info; + JxlEncoderInitBasicInfo(&info); + info.bits_per_sample = 12; + info.num_color_channels = 2; + + JxlPixelFormat format; + EXPECT_FALSE(SelectFormat(accepted_formats, info, &format)); + + info.num_color_channels = 3; + ASSERT_TRUE(SelectFormat(accepted_formats, info, &format)); + EXPECT_EQ(format.num_channels, info.num_color_channels); + // 16 is the smallest accepted format that can accommodate the 12-bit data. + EXPECT_EQ(format.data_type, JXL_TYPE_UINT16); +} + +TEST(CodecTest, EncodeToPNG) { + ThreadPool* const pool = nullptr; + + std::unique_ptr<Encoder> png_encoder = Encoder::FromExtension(".png"); + ASSERT_THAT(png_encoder, NotNull()); + + const PaddedBytes original_png = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + PackedPixelFile ppf; + ASSERT_TRUE(extras::DecodeBytes(Span<const uint8_t>(original_png), + ColorHints(), SizeConstraints(), &ppf)); + + const JxlPixelFormat& format = ppf.frames.front().color.format; + ASSERT_THAT( + png_encoder->AcceptedFormats(), + Contains(AllOf(Field(&JxlPixelFormat::num_channels, format.num_channels), + Field(&JxlPixelFormat::data_type, format.data_type), + Field(&JxlPixelFormat::endianness, format.endianness)))); + EncodedImage encoded_png; + ASSERT_TRUE(png_encoder->Encode(ppf, &encoded_png, pool)); + EXPECT_THAT(encoded_png.icc, IsEmpty()); + ASSERT_THAT(encoded_png.bitstreams, SizeIs(1)); + + PackedPixelFile decoded_ppf; + ASSERT_TRUE( + extras::DecodeBytes(Span<const uint8_t>(encoded_png.bitstreams.front()), + ColorHints(), SizeConstraints(), &decoded_ppf)); + + ASSERT_EQ(decoded_ppf.info.bits_per_sample, ppf.info.bits_per_sample); + ASSERT_EQ(decoded_ppf.frames.size(), 1); + VerifySameImage(ppf.frames[0].color, ppf.info.bits_per_sample, + decoded_ppf.frames[0].color, + decoded_ppf.info.bits_per_sample); +} + } // namespace } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/decode.cc b/media/libjxl/src/lib/extras/dec/decode.cc index 55ce0ba3f2..8712e03aac 100644 --- a/media/libjxl/src/lib/extras/dec/decode.cc +++ b/media/libjxl/src/lib/extras/dec/decode.cc @@ -31,6 +31,25 @@ constexpr size_t kMinBytes = 9; } // namespace +std::vector<Codec> AvailableCodecs() { + std::vector<Codec> out; +#if JPEGXL_ENABLE_APNG + out.push_back(Codec::kPNG); +#endif +#if JPEGXL_ENABLE_EXR + out.push_back(Codec::kEXR); +#endif +#if JPEGXL_ENABLE_GIF + out.push_back(Codec::kGIF); +#endif +#if JPEGXL_ENABLE_JPEG + out.push_back(Codec::kJPG); +#endif + out.push_back(Codec::kPGX); + out.push_back(Codec::kPNM); + return out; +} + Codec CodecFromExtension(std::string extension, size_t* JXL_RESTRICT bits_per_sample) { std::transform( diff --git a/media/libjxl/src/lib/extras/dec/decode.h b/media/libjxl/src/lib/extras/dec/decode.h index 32ec4c6368..7f0ff70aa8 100644 --- a/media/libjxl/src/lib/extras/dec/decode.h +++ b/media/libjxl/src/lib/extras/dec/decode.h @@ -12,15 +12,12 @@ #include <stdint.h> #include <string> +#include <vector> #include "lib/extras/dec/color_hints.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/status.h" #include "lib/jxl/codec_in_out.h" -#include "lib/jxl/field_encodings.h" // MakeBit namespace jxl { namespace extras { @@ -36,27 +33,14 @@ enum class Codec : uint32_t { kEXR }; -static inline constexpr uint64_t EnumBits(Codec /*unused*/) { - // Return only fully-supported codecs (kGIF is decode-only). - return MakeBit(Codec::kPNM) -#if JPEGXL_ENABLE_APNG - | MakeBit(Codec::kPNG) -#endif -#if JPEGXL_ENABLE_JPEG - | MakeBit(Codec::kJPG) -#endif -#if JPEGXL_ENABLE_EXR - | MakeBit(Codec::kEXR) -#endif - ; -} +std::vector<Codec> AvailableCodecs(); // If and only if extension is ".pfm", *bits_per_sample is updated to 32 so // that Encode() would encode to PFM instead of PPM. Codec CodecFromExtension(std::string extension, size_t* JXL_RESTRICT bits_per_sample = nullptr); -// Decodes "bytes" and sets io->metadata.m. +// Decodes "bytes" info *ppf. // color_space_hint may specify the color space, otherwise, defaults to sRGB. Status DecodeBytes(Span<const uint8_t> bytes, const ColorHints& color_hints, const SizeConstraints& constraints, diff --git a/media/libjxl/src/lib/extras/dec/jxl.cc b/media/libjxl/src/lib/extras/dec/jxl.cc new file mode 100644 index 0000000000..0e1035646d --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/jxl.cc @@ -0,0 +1,480 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/jxl.h" + +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/types.h" +#include "lib/extras/dec/color_description.h" +#include "lib/extras/enc/encode.h" +#include "lib/jxl/base/printf_macros.h" + +namespace jxl { +namespace extras { +namespace { + +struct BoxProcessor { + BoxProcessor(JxlDecoder* dec) : dec_(dec) { Reset(); } + + void InitializeOutput(std::vector<uint8_t>* out) { + box_data_ = out; + AddMoreOutput(); + } + + bool AddMoreOutput() { + Flush(); + static const size_t kBoxOutputChunkSize = 1 << 16; + box_data_->resize(box_data_->size() + kBoxOutputChunkSize); + next_out_ = box_data_->data() + total_size_; + avail_out_ = box_data_->size() - total_size_; + if (JXL_DEC_SUCCESS != + JxlDecoderSetBoxBuffer(dec_, next_out_, avail_out_)) { + fprintf(stderr, "JxlDecoderSetBoxBuffer failed\n"); + return false; + } + return true; + } + + void FinalizeOutput() { + if (box_data_ == nullptr) return; + Flush(); + box_data_->resize(total_size_); + Reset(); + } + + private: + JxlDecoder* dec_; + std::vector<uint8_t>* box_data_; + uint8_t* next_out_; + size_t avail_out_; + size_t total_size_; + + void Reset() { + box_data_ = nullptr; + next_out_ = nullptr; + avail_out_ = 0; + total_size_ = 0; + } + void Flush() { + if (box_data_ == nullptr) return; + size_t remaining = JxlDecoderReleaseBoxBuffer(dec_); + size_t bytes_written = avail_out_ - remaining; + next_out_ += bytes_written; + avail_out_ -= bytes_written; + total_size_ += bytes_written; + } +}; + +} // namespace + +bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, + const JXLDecompressParams& dparams, size_t* decoded_bytes, + PackedPixelFile* ppf, std::vector<uint8_t>* jpeg_bytes) { + auto decoder = JxlDecoderMake(/*memory_manager=*/nullptr); + JxlDecoder* dec = decoder.get(); + ppf->frames.clear(); + + if (dparams.runner_opaque != nullptr && + JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec, dparams.runner, + dparams.runner_opaque)) { + fprintf(stderr, "JxlEncoderSetParallelRunner failed\n"); + return false; + } + + JxlPixelFormat format; + std::vector<JxlPixelFormat> accepted_formats = dparams.accepted_formats; + if (accepted_formats.empty()) { + for (const uint32_t num_channels : {1, 2, 3, 4}) { + accepted_formats.push_back( + {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0}); + } + } + JxlColorEncoding color_encoding; + size_t num_color_channels = 0; + if (!dparams.color_space.empty()) { + if (!jxl::ParseDescription(dparams.color_space, &color_encoding)) { + fprintf(stderr, "Failed to parse color space %s.\n", + dparams.color_space.c_str()); + return false; + } + num_color_channels = + color_encoding.color_space == JXL_COLOR_SPACE_GRAY ? 1 : 3; + } + + bool can_reconstruct_jpeg = false; + std::vector<uint8_t> jpeg_data_chunk; + if (jpeg_bytes != nullptr) { + jpeg_data_chunk.resize(16384); + jpeg_bytes->resize(0); + } + + int events = (JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE); + + bool max_passes_defined = + (dparams.max_passes < std::numeric_limits<uint32_t>::max()); + if (max_passes_defined || dparams.max_downsampling > 1) { + events |= JXL_DEC_FRAME_PROGRESSION; + if (max_passes_defined) { + JxlDecoderSetProgressiveDetail(dec, JxlProgressiveDetail::kPasses); + } else { + JxlDecoderSetProgressiveDetail(dec, JxlProgressiveDetail::kLastPasses); + } + } + if (jpeg_bytes != nullptr) { + events |= JXL_DEC_JPEG_RECONSTRUCTION; + } else { + events |= (JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_PREVIEW_IMAGE | + JXL_DEC_BOX); + } + if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec, events)) { + fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); + return false; + } + if (jpeg_bytes == nullptr) { + if (JXL_DEC_SUCCESS != + JxlDecoderSetRenderSpotcolors(dec, dparams.render_spotcolors)) { + fprintf(stderr, "JxlDecoderSetRenderSpotColors failed\n"); + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetKeepOrientation(dec, dparams.keep_orientation)) { + fprintf(stderr, "JxlDecoderSetKeepOrientation failed\n"); + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetUnpremultiplyAlpha(dec, dparams.unpremultiply_alpha)) { + fprintf(stderr, "JxlDecoderSetUnpremultiplyAlpha failed\n"); + return false; + } + if (dparams.display_nits > 0 && + JXL_DEC_SUCCESS != + JxlDecoderSetDesiredIntensityTarget(dec, dparams.display_nits)) { + fprintf(stderr, "Decoder failed to set desired intensity target\n"); + return false; + } + if (JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(dec, JXL_TRUE)) { + fprintf(stderr, "JxlDecoderSetDecompressBoxes failed\n"); + return false; + } + } + if (JXL_DEC_SUCCESS != JxlDecoderSetInput(dec, bytes, bytes_size)) { + fprintf(stderr, "Decoder failed to set input\n"); + return false; + } + uint32_t progression_index = 0; + bool codestream_done = false; + BoxProcessor boxes(dec); + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + if (status == JXL_DEC_ERROR) { + fprintf(stderr, "Failed to decode image\n"); + return false; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + if (codestream_done) { + break; + } + if (dparams.allow_partial_input) { + if (JXL_DEC_SUCCESS != JxlDecoderFlushImage(dec)) { + fprintf(stderr, + "Input file is truncated and there is no preview " + "available yet.\n"); + return false; + } + break; + } + fprintf(stderr, + "Input file is truncated and allow_partial_input was disabled."); + return false; + } else if (status == JXL_DEC_BOX) { + boxes.FinalizeOutput(); + JxlBoxType box_type; + if (JXL_DEC_SUCCESS != JxlDecoderGetBoxType(dec, box_type, JXL_TRUE)) { + fprintf(stderr, "JxlDecoderGetBoxType failed\n"); + return false; + } + std::vector<uint8_t>* box_data = nullptr; + if (memcmp(box_type, "Exif", 4) == 0) { + box_data = &ppf->metadata.exif; + } else if (memcmp(box_type, "iptc", 4) == 0) { + box_data = &ppf->metadata.iptc; + } else if (memcmp(box_type, "jumb", 4) == 0) { + box_data = &ppf->metadata.jumbf; + } else if (memcmp(box_type, "xml ", 4) == 0) { + box_data = &ppf->metadata.xmp; + } + if (box_data) { + boxes.InitializeOutput(box_data); + } + } else if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) { + boxes.AddMoreOutput(); + } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { + can_reconstruct_jpeg = true; + // Decoding to JPEG. + if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec, + jpeg_data_chunk.data(), + jpeg_data_chunk.size())) { + fprintf(stderr, "Decoder failed to set JPEG Buffer\n"); + return false; + } + } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + // Decoded a chunk to JPEG. + size_t used_jpeg_output = + jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec); + jpeg_bytes->insert(jpeg_bytes->end(), jpeg_data_chunk.data(), + jpeg_data_chunk.data() + used_jpeg_output); + if (used_jpeg_output == 0) { + // Chunk is too small. + jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2); + } + if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec, + jpeg_data_chunk.data(), + jpeg_data_chunk.size())) { + fprintf(stderr, "Decoder failed to set JPEG Buffer\n"); + return false; + } + } else if (status == JXL_DEC_BASIC_INFO) { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec, &ppf->info)) { + fprintf(stderr, "JxlDecoderGetBasicInfo failed\n"); + return false; + } + if (num_color_channels != 0) { + // Mark the change in number of color channels due to the requested + // color space. + ppf->info.num_color_channels = num_color_channels; + } + // Select format according to accepted formats. + if (!jxl::extras::SelectFormat(accepted_formats, ppf->info, &format)) { + fprintf(stderr, "SelectFormat failed\n"); + return false; + } + bool have_alpha = (format.num_channels == 2 || format.num_channels == 4); + if (!have_alpha) { + // Mark in the basic info that alpha channel was dropped. + ppf->info.alpha_bits = 0; + } else if (dparams.unpremultiply_alpha) { + // Mark in the basic info that alpha was unpremultiplied. + ppf->info.alpha_premultiplied = false; + } + bool alpha_found = false; + for (uint32_t i = 0; i < ppf->info.num_extra_channels; ++i) { + JxlExtraChannelInfo eci; + if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &eci)) { + fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n"); + return false; + } + if (eci.type == JXL_CHANNEL_ALPHA && have_alpha && !alpha_found) { + // Skip the first alpha channels because it is already present in the + // interleaved image. + alpha_found = true; + continue; + } + std::string name(eci.name_length + 1, 0); + if (JXL_DEC_SUCCESS != + JxlDecoderGetExtraChannelName(dec, i, &name[0], name.size())) { + fprintf(stderr, "JxlDecoderGetExtraChannelName failed\n"); + return false; + } + name.resize(eci.name_length); + ppf->extra_channels_info.push_back({eci, i, name}); + } + } else if (status == JXL_DEC_COLOR_ENCODING) { + if (!dparams.color_space.empty()) { + if (ppf->info.uses_original_profile) { + fprintf(stderr, + "Warning: --color_space ignored because the image is " + "not XYB encoded.\n"); + } else { + if (JXL_DEC_SUCCESS != + JxlDecoderSetPreferredColorProfile(dec, &color_encoding)) { + fprintf(stderr, "Failed to set color space.\n"); + return false; + } + } + } + size_t icc_size = 0; + JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_DATA; + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) { + fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); + } + if (icc_size != 0) { + ppf->icc.resize(icc_size); + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsICCProfile(dec, nullptr, target, + ppf->icc.data(), icc_size)) { + fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); + return false; + } + } + if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsEncodedProfile( + dec, nullptr, target, &ppf->color_encoding)) { + ppf->color_encoding.color_space = JXL_COLOR_SPACE_UNKNOWN; + } + icc_size = 0; + target = JXL_COLOR_PROFILE_TARGET_ORIGINAL; + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) { + fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); + } + if (icc_size != 0) { + ppf->orig_icc.resize(icc_size); + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsICCProfile(dec, nullptr, target, + ppf->orig_icc.data(), icc_size)) { + fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); + return false; + } + } + } else if (status == JXL_DEC_FRAME) { + jxl::extras::PackedFrame frame(ppf->info.xsize, ppf->info.ysize, format); + if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &frame.frame_info)) { + fprintf(stderr, "JxlDecoderGetFrameHeader failed\n"); + return false; + } + frame.name.resize(frame.frame_info.name_length + 1, 0); + if (JXL_DEC_SUCCESS != + JxlDecoderGetFrameName(dec, &frame.name[0], frame.name.size())) { + fprintf(stderr, "JxlDecoderGetFrameName failed\n"); + return false; + } + frame.name.resize(frame.frame_info.name_length); + ppf->frames.emplace_back(std::move(frame)); + progression_index = 0; + } else if (status == JXL_DEC_FRAME_PROGRESSION) { + size_t downsampling = JxlDecoderGetIntendedDownsamplingRatio(dec); + if ((max_passes_defined && progression_index >= dparams.max_passes) || + (!max_passes_defined && downsampling <= dparams.max_downsampling)) { + if (JXL_DEC_SUCCESS != JxlDecoderFlushImage(dec)) { + fprintf(stderr, "JxlDecoderFlushImage failed\n"); + return false; + } + if (ppf->frames.back().frame_info.is_last) { + break; + } + if (JXL_DEC_SUCCESS != JxlDecoderSkipCurrentFrame(dec)) { + fprintf(stderr, "JxlDecoderSkipCurrentFrame failed\n"); + return false; + } + } + ++progression_index; + } else if (status == JXL_DEC_NEED_PREVIEW_OUT_BUFFER) { + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderPreviewOutBufferSize(dec, &format, &buffer_size)) { + fprintf(stderr, "JxlDecoderPreviewOutBufferSize failed\n"); + return false; + } + ppf->preview_frame = std::unique_ptr<jxl::extras::PackedFrame>( + new jxl::extras::PackedFrame(ppf->info.preview.xsize, + ppf->info.preview.ysize, format)); + if (buffer_size != ppf->preview_frame->color.pixels_size) { + fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n", + buffer_size, ppf->preview_frame->color.pixels_size); + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetPreviewOutBuffer( + dec, &format, ppf->preview_frame->color.pixels(), buffer_size)) { + fprintf(stderr, "JxlDecoderSetPreviewOutBuffer failed\n"); + return false; + } + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + if (jpeg_bytes != nullptr) { + break; + } + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)) { + fprintf(stderr, "JxlDecoderImageOutBufferSize failed\n"); + return false; + } + jxl::extras::PackedFrame& frame = ppf->frames.back(); + if (buffer_size != frame.color.pixels_size) { + fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n", + buffer_size, frame.color.pixels_size); + return false; + } + + if (dparams.use_image_callback) { + auto callback = [](void* opaque, size_t x, size_t y, size_t num_pixels, + const void* pixels) { + auto* ppf = reinterpret_cast<jxl::extras::PackedPixelFile*>(opaque); + jxl::extras::PackedImage& color = ppf->frames.back().color; + uint8_t* pixels_buffer = reinterpret_cast<uint8_t*>(color.pixels()); + size_t sample_size = color.pixel_stride(); + memcpy(pixels_buffer + (color.stride * y + sample_size * x), pixels, + num_pixels * sample_size); + }; + if (JXL_DEC_SUCCESS != + JxlDecoderSetImageOutCallback(dec, &format, callback, ppf)) { + fprintf(stderr, "JxlDecoderSetImageOutCallback failed\n"); + return false; + } + } else { + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec, &format, + frame.color.pixels(), + buffer_size)) { + fprintf(stderr, "JxlDecoderSetImageOutBuffer failed\n"); + return false; + } + } + JxlPixelFormat ec_format = format; + ec_format.num_channels = 1; + for (const auto& eci : ppf->extra_channels_info) { + frame.extra_channels.emplace_back(jxl::extras::PackedImage( + ppf->info.xsize, ppf->info.ysize, ec_format)); + auto& ec = frame.extra_channels.back(); + size_t buffer_size; + if (JXL_DEC_SUCCESS != JxlDecoderExtraChannelBufferSize( + dec, &ec_format, &buffer_size, eci.index)) { + fprintf(stderr, "JxlDecoderExtraChannelBufferSize failed\n"); + return false; + } + if (buffer_size != ec.pixels_size) { + fprintf(stderr, + "Invalid extra channel buffer size" + " %" PRIuS " %" PRIuS "\n", + buffer_size, ec.pixels_size); + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetExtraChannelBuffer(dec, &ec_format, ec.pixels(), + buffer_size, eci.index)) { + fprintf(stderr, "JxlDecoderSetExtraChannelBuffer failed\n"); + return false; + } + } + } else if (status == JXL_DEC_SUCCESS) { + // Decoding finished successfully. + break; + } else if (status == JXL_DEC_PREVIEW_IMAGE) { + // Nothing to do. + } else if (status == JXL_DEC_FULL_IMAGE) { + if (jpeg_bytes != nullptr || ppf->frames.back().frame_info.is_last) { + codestream_done = true; + } + } else { + fprintf(stderr, "Error: unexpected status: %d\n", + static_cast<int>(status)); + return false; + } + } + boxes.FinalizeOutput(); + if (jpeg_bytes != nullptr) { + if (!can_reconstruct_jpeg) return false; + size_t used_jpeg_output = + jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec); + jpeg_bytes->insert(jpeg_bytes->end(), jpeg_data_chunk.data(), + jpeg_data_chunk.data() + used_jpeg_output); + } + if (decoded_bytes) { + *decoded_bytes = bytes_size - JxlDecoderReleaseInput(dec); + } + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/jxl.h b/media/libjxl/src/lib/extras/dec/jxl.h new file mode 100644 index 0000000000..c462fa4b74 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/jxl.h @@ -0,0 +1,66 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_DEC_JXL_H_ +#define LIB_EXTRAS_DEC_JXL_H_ + +// Decodes JPEG XL images in memory. + +#include <stdint.h> + +#include <limits> +#include <string> +#include <vector> + +#include "jxl/parallel_runner.h" +#include "jxl/types.h" +#include "lib/extras/packed_image.h" + +namespace jxl { +namespace extras { + +struct JXLDecompressParams { + // If empty, little endian float formats will be accepted. + std::vector<JxlPixelFormat> accepted_formats; + + // Requested output color space description. + std::string color_space; + // If set, performs tone mapping to this intensity target luminance. + float display_nits = 0.0; + // Whether spot colors are rendered on the image. + bool render_spotcolors = true; + // Whether to keep or undo the orientation given in the header. + bool keep_orientation = false; + + // If runner_opaque is set, the decoder uses this parallel runner. + JxlParallelRunner runner; + void* runner_opaque = nullptr; + + // Whether truncated input should be treated as an error. + bool allow_partial_input = false; + + // How many passes to decode at most. By default, decode everything. + uint32_t max_passes = std::numeric_limits<uint32_t>::max(); + + // Alternatively, one can specify the maximum tolerable downscaling factor + // with respect to the full size of the image. By default, nothing less than + // the full size is requested. + size_t max_downsampling = 1; + + // Whether to use the image callback or the image buffer to get the output. + bool use_image_callback = true; + // Whether to unpremultiply colors for associated alpha channels. + bool unpremultiply_alpha = false; +}; + +bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, + const JXLDecompressParams& dparams, size_t* decoded_bytes, + PackedPixelFile* ppf, + std::vector<uint8_t>* jpeg_bytes = nullptr); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_JXL_H_ diff --git a/media/libjxl/src/lib/extras/dec/pnm.cc b/media/libjxl/src/lib/extras/dec/pnm.cc index 93a42be321..03aecef29c 100644 --- a/media/libjxl/src/lib/extras/dec/pnm.cc +++ b/media/libjxl/src/lib/extras/dec/pnm.cc @@ -366,13 +366,18 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes, ppf->frames.emplace_back(header.xsize, header.ysize, format); auto* frame = &ppf->frames.back(); - frame->color.bitdepth_from_format = false; - frame->color.flipped_y = header.bits_per_sample == 32; // PFMs are flipped size_t pnm_remaining_size = bytes.data() + bytes.size() - pos; if (pnm_remaining_size < frame->color.pixels_size) { return JXL_FAILURE("PNM file too small"); } - memcpy(frame->color.pixels(), pos, frame->color.pixels_size); + const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped + uint8_t* out = reinterpret_cast<uint8_t*>(frame->color.pixels()); + for (size_t y = 0; y < header.ysize; ++y) { + size_t y_in = flipped_y ? header.ysize - 1 - y : y; + const uint8_t* row_in = &pos[y_in * frame->color.stride]; + uint8_t* row_out = &out[y * frame->color.stride]; + memcpy(row_out, row_in, frame->color.stride); + } return true; } diff --git a/media/libjxl/src/lib/extras/enc/apng.cc b/media/libjxl/src/lib/extras/enc/apng.cc index 372b3b32ea..db6cf9ef4a 100644 --- a/media/libjxl/src/lib/extras/enc/apng.cc +++ b/media/libjxl/src/lib/extras/enc/apng.cc @@ -42,18 +42,9 @@ #include <string> #include <vector> -#include "jxl/encode.h" -#include "lib/jxl/base/compiler_specific.h" +#include "lib/extras/exif.h" +#include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/printf_macros.h" -#include "lib/jxl/color_encoding_internal.h" -#include "lib/jxl/dec_external_image.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_image_bundle.h" -#include "lib/jxl/exif.h" -#include "lib/jxl/frame_header.h" -#include "lib/jxl/headers.h" -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" #include "png.h" /* original (unpatched) libpng is ok */ namespace jxl { @@ -61,15 +52,44 @@ namespace extras { namespace { +class APNGEncoder : public Encoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 2, 3, 4}) { + for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { + formats.push_back(JxlPixelFormat{num_channels, data_type, + JXL_BIG_ENDIAN, /*align=*/0}); + } + } + return formats; + } + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + encoded_image->icc.clear(); + encoded_image->bitstreams.resize(1); + return EncodePackedPixelFileToAPNG(ppf, pool, + &encoded_image->bitstreams.front()); + } + + private: + Status EncodePackedPixelFileToAPNG(const PackedPixelFile& ppf, + ThreadPool* pool, + std::vector<uint8_t>* bytes) const; +}; + static void PngWrite(png_structp png_ptr, png_bytep data, png_size_t length) { - PaddedBytes* bytes = static_cast<PaddedBytes*>(png_get_io_ptr(png_ptr)); - bytes->append(data, data + length); + std::vector<uint8_t>* bytes = + static_cast<std::vector<uint8_t>*>(png_get_io_ptr(png_ptr)); + bytes->insert(bytes->end(), data, data + length); } // Stores XMP and EXIF/IPTC into key/value strings for PNG class BlobsWriterPNG { public: - static Status Encode(const Blobs& blobs, std::vector<std::string>* strings) { + static Status Encode(const PackedMetadata& blobs, + std::vector<std::string>* strings) { if (!blobs.exif.empty()) { // PNG viewers typically ignore Exif orientation but not all of them do // (and e.g. cjxl doesn't), so we overwrite the Exif orientation to the @@ -122,26 +142,112 @@ class BlobsWriterPNG { } }; -} // namespace +void MaybeAddCICP(JxlColorEncoding c_enc, png_structp png_ptr, + png_infop info_ptr) { + png_byte cicp_data[4] = {}; + png_unknown_chunk cicp_chunk; + if (c_enc.color_space != JXL_COLOR_SPACE_RGB) { + return; + } + if (c_enc.primaries == JXL_PRIMARIES_P3) { + if (c_enc.white_point == JXL_WHITE_POINT_D65) { + cicp_data[0] = 12; + } else if (c_enc.white_point == JXL_WHITE_POINT_DCI) { + cicp_data[0] = 11; + } else { + return; + } + } else if (c_enc.primaries != JXL_PRIMARIES_CUSTOM && + c_enc.white_point == JXL_WHITE_POINT_D65) { + cicp_data[0] = static_cast<png_byte>(c_enc.primaries); + } else { + return; + } + if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_UNKNOWN || + c_enc.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) { + return; + } + cicp_data[1] = static_cast<png_byte>(c_enc.transfer_function); + cicp_data[2] = 0; + cicp_data[3] = 1; + cicp_chunk.data = cicp_data; + cicp_chunk.size = sizeof(cicp_data); + cicp_chunk.location = PNG_HAVE_PLTE; + memcpy(cicp_chunk.name, "cICP", 5); + png_set_keep_unknown_chunks(png_ptr, 3, + reinterpret_cast<const png_byte*>("cICP"), 1); + png_set_unknown_chunks(png_ptr, info_ptr, &cicp_chunk, 1); +} -Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes) { - if (bits_per_sample > 8) { - bits_per_sample = 16; - } else if (bits_per_sample < 8) { - // PNG can also do 4, 2, and 1 bits per sample, but it isn't implemented - bits_per_sample = 8; +Status APNGEncoder::EncodePackedPixelFileToAPNG( + const PackedPixelFile& ppf, ThreadPool* pool, + std::vector<uint8_t>* bytes) const { + size_t xsize = ppf.info.xsize; + size_t ysize = ppf.info.ysize; + bool has_alpha = ppf.info.alpha_bits != 0; + bool is_gray = ppf.info.num_color_channels == 1; + size_t color_channels = ppf.info.num_color_channels; + size_t num_channels = color_channels + (has_alpha ? 1 : 0); + size_t num_samples = num_channels * xsize * ysize; + + if (!ppf.info.have_animation && ppf.frames.size() != 1) { + return JXL_FAILURE("Invalid number of frames"); } size_t count = 0; - bool have_anim = io->metadata.m.have_animation; size_t anim_chunks = 0; - int W = 0, H = 0; - for (size_t i = 0; i < io->frames.size(); i++) { - auto& frame = io->frames[i]; - if (!have_anim && i + 1 < io->frames.size()) continue; + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + + const PackedImage& color = frame.color; + const JxlPixelFormat format = color.format; + const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels()); + size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type); + size_t bytes_per_sample = data_bits_per_sample / 8; + size_t out_bytes_per_sample = bytes_per_sample > 1 ? 2 : 1; + size_t out_stride = xsize * num_channels * out_bytes_per_sample; + size_t out_size = ysize * out_stride; + std::vector<uint8_t> out(out_size); + + if (format.data_type == JXL_TYPE_UINT8) { + if (ppf.info.bits_per_sample < 8) { + float mul = 255.0 / ((1u << ppf.info.bits_per_sample) - 1); + for (size_t i = 0; i < num_samples; ++i) { + out[i] = static_cast<uint8_t>(in[i] * mul + 0.5); + } + } else { + memcpy(&out[0], in, out_size); + } + } else if (format.data_type == JXL_TYPE_UINT16) { + if (ppf.info.bits_per_sample < 16 || + format.endianness != JXL_BIG_ENDIAN) { + float mul = 65535.0 / ((1u << ppf.info.bits_per_sample) - 1); + const uint8_t* p_in = in; + uint8_t* p_out = out.data(); + for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) { + uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE16(p_in) + : LoadLE16(p_in)); + StoreBE16(static_cast<uint32_t>(val * mul + 0.5), p_out); + } + } else { + memcpy(&out[0], in, out_size); + } + } else if (format.data_type == JXL_TYPE_FLOAT) { + float mul = 65535.0; + const uint8_t* p_in = in; + uint8_t* p_out = out.data(); + for (size_t i = 0; i < num_samples; ++i, p_in += 4, p_out += 2) { + uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE32(p_in) + : LoadLE32(p_in)); + float fval; + memcpy(&fval, &val, 4); + StoreBE16(static_cast<uint32_t>(fval * mul + 0.5), p_out); + } + } else { + return JXL_FAILURE("Unsupported pixel data type"); + } + png_structp png_ptr; png_infop info_ptr; @@ -155,46 +261,24 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, png_set_write_fn(png_ptr, bytes, PngWrite, NULL); png_set_flush(png_ptr, 0); - ImageBundle ib = frame.Copy(); - const size_t alpha_bits = ib.HasAlpha() ? bits_per_sample : 0; - ImageMetadata metadata = io->metadata.m; - ImageBundle store(&metadata); - const ImageBundle* transformed; - JXL_RETURN_IF_ERROR(TransformIfNeeded(ib, c_desired, GetJxlCms(), pool, - &store, &transformed)); - size_t stride = ib.oriented_xsize() * - DivCeil(c_desired.Channels() * bits_per_sample + alpha_bits, - kBitsPerByte); - std::vector<uint8_t> raw_bytes(stride * ib.oriented_ysize()); - JXL_RETURN_IF_ERROR(ConvertToExternal( - *transformed, bits_per_sample, /*float_out=*/false, - c_desired.Channels() + (ib.HasAlpha() ? 1 : 0), JXL_BIG_ENDIAN, stride, - pool, raw_bytes.data(), raw_bytes.size(), - /*out_callback=*/{}, metadata.GetOrientation())); - - int width = ib.oriented_xsize(); - int height = ib.oriented_ysize(); - - png_byte color_type = - (c_desired.Channels() == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_GRAY); - if (ib.HasAlpha()) color_type |= PNG_COLOR_MASK_ALPHA; - png_byte bit_depth = bits_per_sample; + int width = xsize; + int height = ysize; + + png_byte color_type = (is_gray ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB); + if (has_alpha) color_type |= PNG_COLOR_MASK_ALPHA; + png_byte bit_depth = out_bytes_per_sample * 8; png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); if (count == 0) { - W = width; - H = height; - - // TODO(jon): instead of always setting an iCCP, could try to avoid that - // have to avoid warnings on the ICC profile becoming fatal - png_set_benign_errors(png_ptr, 1); - png_set_iCCP(png_ptr, info_ptr, "1", 0, c_desired.ICC().data(), - c_desired.ICC().size()); - + MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr); + if (!ppf.icc.empty()) { + png_set_benign_errors(png_ptr, 1); + png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(), ppf.icc.size()); + } std::vector<std::string> textstrings; - JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(io->blobs, &textstrings)); + JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings)); for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) { png_text text; text.key = const_cast<png_charp>(textstrings[kk].c_str()); @@ -211,27 +295,24 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, bytes->resize(pos); } - if (have_anim) { + if (ppf.info.have_animation) { if (count == 0) { png_byte adata[8]; - png_save_uint_32(adata, io->frames.size()); - png_save_uint_32(adata + 4, io->metadata.m.animation.num_loops); + png_save_uint_32(adata, ppf.frames.size()); + png_save_uint_32(adata + 4, ppf.info.animation.num_loops); png_byte actl[5] = "acTL"; png_write_chunk(png_ptr, actl, adata, 8); } png_byte fdata[26]; - JXL_ASSERT(W == width); - JXL_ASSERT(H == height); // TODO(jon): also make this work for the non-coalesced case png_save_uint_32(fdata, anim_chunks++); png_save_uint_32(fdata + 4, width); png_save_uint_32(fdata + 8, height); png_save_uint_32(fdata + 12, 0); png_save_uint_32(fdata + 16, 0); - png_save_uint_16( - fdata + 20, - frame.duration * io->metadata.m.animation.tps_denominator); - png_save_uint_16(fdata + 22, io->metadata.m.animation.tps_numerator); + png_save_uint_16(fdata + 20, frame.frame_info.duration * + ppf.info.animation.tps_denominator); + png_save_uint_16(fdata + 22, ppf.info.animation.tps_numerator); fdata[24] = 1; fdata[25] = 0; png_byte fctl[5] = "fcTL"; @@ -240,7 +321,7 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, std::vector<uint8_t*> rows(height); for (int y = 0; y < height; ++y) { - rows[y] = raw_bytes.data() + y * stride; + rows[y] = out.data() + y * out_stride; } png_write_flush(png_ptr); @@ -268,7 +349,9 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, } count++; - if (count == io->frames.size() || !have_anim) png_write_end(png_ptr, NULL); + if (count == ppf.frames.size() || !ppf.info.have_animation) { + png_write_end(png_ptr, NULL); + } png_destroy_write_struct(&png_ptr, &info_ptr); } @@ -276,5 +359,11 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, return true; } +} // namespace + +std::unique_ptr<Encoder> GetAPNGEncoder() { + return jxl::make_unique<APNGEncoder>(); +} + } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/apng.h b/media/libjxl/src/lib/extras/enc/apng.h index 7f0fb94d9b..2a2139c8fa 100644 --- a/media/libjxl/src/lib/extras/enc/apng.h +++ b/media/libjxl/src/lib/extras/enc/apng.h @@ -8,19 +8,14 @@ // Encodes APNG images in memory. -#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/codec_in_out.h" +#include <memory> + +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -// Encodes `io` into `bytes`. -Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes); +std::unique_ptr<Encoder> GetAPNGEncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/encode.cc b/media/libjxl/src/lib/extras/enc/encode.cc new file mode 100644 index 0000000000..dc593d2900 --- /dev/null +++ b/media/libjxl/src/lib/extras/enc/encode.cc @@ -0,0 +1,136 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/enc/encode.h" + +#include <locale> + +#if JPEGXL_ENABLE_APNG +#include "lib/extras/enc/apng.h" +#endif +#if JPEGXL_ENABLE_EXR +#include "lib/extras/enc/exr.h" +#endif +#if JPEGXL_ENABLE_JPEG +#include "lib/extras/enc/jpg.h" +#endif +#include "lib/extras/enc/npy.h" +#include "lib/extras/enc/pgx.h" +#include "lib/extras/enc/pnm.h" +#include "lib/jxl/base/printf_macros.h" + +namespace jxl { +namespace extras { + +Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) const { + if (info.xsize == 0 || info.ysize == 0) { + return JXL_FAILURE("Empty image"); + } + if (info.num_color_channels != 1 && info.num_color_channels != 3) { + return JXL_FAILURE("Invalid number of color channels"); + } + if (info.alpha_bits > 0 && info.alpha_bits != info.bits_per_sample) { + return JXL_FAILURE("Alpha bit depth does not match image bit depth"); + } + if (info.orientation != JXL_ORIENT_IDENTITY) { + return JXL_FAILURE("Orientation must be identity"); + } + return true; +} + +Status Encoder::VerifyPackedImage(const PackedImage& image, + const JxlBasicInfo& info) const { + if (image.pixels() == nullptr) { + return JXL_FAILURE("Invalid image."); + } + if (image.stride != image.xsize * image.pixel_stride()) { + return JXL_FAILURE("Invalid image stride."); + } + if (image.pixels_size != image.ysize * image.stride) { + return JXL_FAILURE("Invalid image size."); + } + size_t info_num_channels = + (info.num_color_channels + (info.alpha_bits > 0 ? 1 : 0)); + if (image.xsize != info.xsize || image.ysize != info.ysize || + image.format.num_channels != info_num_channels) { + return JXL_FAILURE("Frame size does not match image size"); + } + if (info.bits_per_sample > + PackedImage::BitsPerChannel(image.format.data_type)) { + return JXL_FAILURE("Bit depth does not fit pixel data type"); + } + return true; +} + +Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats, + const JxlBasicInfo& basic_info, JxlPixelFormat* format) { + const size_t original_bit_depth = basic_info.bits_per_sample; + size_t current_bit_depth = 0; + size_t num_alpha_channels = (basic_info.alpha_bits != 0 ? 1 : 0); + size_t num_channels = basic_info.num_color_channels + num_alpha_channels; + for (;;) { + for (const JxlPixelFormat& candidate : accepted_formats) { + if (candidate.num_channels != num_channels) continue; + const size_t candidate_bit_depth = + PackedImage::BitsPerChannel(candidate.data_type); + if ( + // Candidate bit depth is less than what we have and still enough + (original_bit_depth <= candidate_bit_depth && + candidate_bit_depth < current_bit_depth) || + // Or larger than the too-small bit depth we currently have + (current_bit_depth < candidate_bit_depth && + current_bit_depth < original_bit_depth)) { + *format = candidate; + current_bit_depth = candidate_bit_depth; + } + } + if (current_bit_depth == 0) { + if (num_channels > basic_info.num_color_channels) { + // Try dropping the alpha channel. + --num_channels; + continue; + } + return JXL_FAILURE("no appropriate format found"); + } + break; + } + if (current_bit_depth < original_bit_depth) { + JXL_WARNING("encoding %" PRIuS "-bit original to %" PRIuS " bits", + original_bit_depth, current_bit_depth); + } + return true; +} + +std::unique_ptr<Encoder> Encoder::FromExtension(std::string extension) { + std::transform( + extension.begin(), extension.end(), extension.begin(), + [](char c) { return std::tolower(c, std::locale::classic()); }); +#if JPEGXL_ENABLE_APNG + if (extension == ".png" || extension == ".apng") return GetAPNGEncoder(); +#endif + +#if JPEGXL_ENABLE_JPEG + if (extension == ".jpg") return GetJPEGEncoder(); + if (extension == ".jpeg") return GetJPEGEncoder(); +#endif + + if (extension == ".npy") return GetNumPyEncoder(); + + if (extension == ".pgx") return GetPGXEncoder(); + + if (extension == ".pam") return GetPAMEncoder(); + if (extension == ".pgm") return GetPGMEncoder(); + if (extension == ".ppm") return GetPPMEncoder(); + if (extension == ".pfm") return GetPFMEncoder(); + +#if JPEGXL_ENABLE_EXR + if (extension == ".exr") return GetEXREncoder(); +#endif + + return nullptr; +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/encode.h b/media/libjxl/src/lib/extras/enc/encode.h new file mode 100644 index 0000000000..92eec50b6f --- /dev/null +++ b/media/libjxl/src/lib/extras/enc/encode.h @@ -0,0 +1,77 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_ENC_ENCODE_H_ +#define LIB_EXTRAS_ENC_ENCODE_H_ + +// Facade for image encoders. + +#include <string> +#include <unordered_map> + +#include "lib/extras/dec/decode.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace extras { + +struct EncodedImage { + // One (if the format supports animations or the image has only one frame) or + // more sequential bitstreams. + std::vector<std::vector<uint8_t>> bitstreams; + + // For each extra channel one or more sequential bitstreams. + std::vector<std::vector<std::vector<uint8_t>>> extra_channel_bitstreams; + + std::vector<uint8_t> preview_bitstream; + + // If the format does not support embedding color profiles into the bitstreams + // above, it will be present here, to be written as a separate file. If it + // does support them, this field will be empty. + std::vector<uint8_t> icc; + + // Additional output for conformance testing, only filled in by NumPyEncoder. + std::vector<uint8_t> metadata; +}; + +class Encoder { + public: + static std::unique_ptr<Encoder> FromExtension(std::string extension); + + virtual ~Encoder() = default; + + virtual std::vector<JxlPixelFormat> AcceptedFormats() const = 0; + + // Any existing data in encoded_image is discarded. + virtual Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const = 0; + + void SetOption(std::string name, std::string value) { + options_[std::move(name)] = std::move(value); + } + + protected: + const std::unordered_map<std::string, std::string>& options() const { + return options_; + } + + Status VerifyBasicInfo(const JxlBasicInfo& info) const; + + Status VerifyPackedImage(const PackedImage& image, + const JxlBasicInfo& info) const; + + private: + std::unordered_map<std::string, std::string> options_; +}; + +// TODO(sboukortt): consider exposing this as part of the C API. +Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats, + const JxlBasicInfo& basic_info, JxlPixelFormat* format); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ENC_ENCODE_H_ diff --git a/media/libjxl/src/lib/extras/enc/exr.cc b/media/libjxl/src/lib/extras/enc/exr.cc index 3d88404888..05e05f96ce 100644 --- a/media/libjxl/src/lib/extras/enc/exr.cc +++ b/media/libjxl/src/lib/extras/enc/exr.cc @@ -12,11 +12,9 @@ #include <vector> -#include "lib/jxl/alpha.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_image_bundle.h" +#include "jxl/codestream_header.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/byte_order.h" namespace jxl { namespace extras { @@ -32,22 +30,10 @@ namespace Imath = IMATH_NAMESPACE; // to uint64_t. This alternative should work in all cases. using ExrInt64 = decltype(std::declval<OpenEXR::IStream>().tellg()); -size_t GetNumThreads(ThreadPool* pool) { - size_t exr_num_threads = 1; - JXL_CHECK(RunOnPool( - pool, 0, 1, - [&](size_t num_threads) { - exr_num_threads = num_threads; - return true; - }, - [&](uint32_t /* task */, size_t /*thread*/) {}, "DecodeImageEXRThreads")); - return exr_num_threads; -} - class InMemoryOStream : public OpenEXR::OStream { public: // `bytes` must outlive the InMemoryOStream. - explicit InMemoryOStream(PaddedBytes* const bytes) + explicit InMemoryOStream(std::vector<uint8_t>* const bytes) : OStream(/*fileName=*/""), bytes_(*bytes) {} void write(const char c[], const int n) override { @@ -67,42 +53,69 @@ class InMemoryOStream : public OpenEXR::OStream { } private: - PaddedBytes& bytes_; + std::vector<uint8_t>& bytes_; size_t pos_ = 0; }; -} // namespace +// Loads a Big-Endian float +float LoadBEFloat(const uint8_t* p) { + uint32_t u = LoadBE32(p); + float result; + memcpy(&result, &u, 4); + return result; +} + +// Loads a Little-Endian float +float LoadLEFloat(const uint8_t* p) { + uint32_t u = LoadLE32(p); + float result; + memcpy(&result, &u, 4); + return result; +} + +Status EncodeImageEXR(const PackedImage& image, const JxlBasicInfo& info, + const JxlColorEncoding& c_enc, ThreadPool* pool, + std::vector<uint8_t>* bytes) { + OpenEXR::setGlobalThreadCount(0); + + const size_t xsize = info.xsize; + const size_t ysize = info.ysize; + const bool has_alpha = info.alpha_bits > 0; + const bool alpha_is_premultiplied = info.alpha_premultiplied; + + if (info.num_color_channels != 3 || + c_enc.color_space != JXL_COLOR_SPACE_RGB || + c_enc.transfer_function != JXL_TRANSFER_FUNCTION_LINEAR) { + return JXL_FAILURE("Unsupported color encoding for OpenEXR output."); + } + + const size_t num_channels = 3 + (has_alpha ? 1 : 0); + const JxlPixelFormat format = image.format; + + if (format.data_type != JXL_TYPE_FLOAT) { + return JXL_FAILURE("Unsupported pixel format for OpenEXR output"); + } -Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, - ThreadPool* pool, PaddedBytes* bytes) { - // As in `DecodeImageEXR`, `pool` is only used for pixel conversion, not for - // actual OpenEXR I/O. - OpenEXR::setGlobalThreadCount(GetNumThreads(pool)); - - ColorEncoding c_linear = c_desired; - c_linear.tf.SetTransferFunction(TransferFunction::kLinear); - JXL_RETURN_IF_ERROR(c_linear.CreateICC()); - ImageMetadata metadata = io->metadata.m; - ImageBundle store(&metadata); - const ImageBundle* linear; - JXL_RETURN_IF_ERROR(TransformIfNeeded(io->Main(), c_linear, GetJxlCms(), pool, - &store, &linear)); - - const bool has_alpha = io->Main().HasAlpha(); - const bool alpha_is_premultiplied = io->Main().AlphaIsPremultiplied(); - - OpenEXR::Header header(io->xsize(), io->ysize()); - const PrimariesCIExy& primaries = c_linear.HasPrimaries() - ? c_linear.GetPrimaries() - : ColorEncoding::SRGB().GetPrimaries(); + const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels()); + size_t in_stride = num_channels * 4 * xsize; + + OpenEXR::Header header(xsize, ysize); OpenEXR::Chromaticities chromaticities; - chromaticities.red = Imath::V2f(primaries.r.x, primaries.r.y); - chromaticities.green = Imath::V2f(primaries.g.x, primaries.g.y); - chromaticities.blue = Imath::V2f(primaries.b.x, primaries.b.y); + chromaticities.red = + Imath::V2f(c_enc.primaries_red_xy[0], c_enc.primaries_red_xy[1]); + chromaticities.green = + Imath::V2f(c_enc.primaries_green_xy[0], c_enc.primaries_green_xy[1]); + chromaticities.blue = + Imath::V2f(c_enc.primaries_blue_xy[0], c_enc.primaries_blue_xy[1]); chromaticities.white = - Imath::V2f(c_linear.GetWhitePoint().x, c_linear.GetWhitePoint().y); + Imath::V2f(c_enc.white_point_xy[0], c_enc.white_point_xy[1]); OpenEXR::addChromaticities(header, chromaticities); - OpenEXR::addWhiteLuminance(header, io->metadata.m.IntensityTarget()); + OpenEXR::addWhiteLuminance(header, 255.0f); + + auto loadFloat = + format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat : LoadLEFloat; + auto loadAlpha = + has_alpha ? loadFloat : [](const uint8_t* p) -> float { return 1.0f; }; // Ensure that the destructor of RgbaOutputFile has run before we look at the // size of `bytes`. @@ -112,50 +125,32 @@ Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, os, header, has_alpha ? OpenEXR::WRITE_RGBA : OpenEXR::WRITE_RGB); // How many rows to write at once. Again, the OpenEXR documentation // recommends writing the whole image in one call. - const int y_chunk_size = io->ysize(); - std::vector<OpenEXR::Rgba> output_rows(io->xsize() * y_chunk_size); + const int y_chunk_size = ysize; + std::vector<OpenEXR::Rgba> output_rows(xsize * y_chunk_size); - for (size_t start_y = 0; start_y < io->ysize(); start_y += y_chunk_size) { + for (size_t start_y = 0; start_y < ysize; start_y += y_chunk_size) { // Inclusive. - const size_t end_y = - std::min(start_y + y_chunk_size - 1, io->ysize() - 1); - output.setFrameBuffer(output_rows.data() - start_y * io->xsize(), - /*xStride=*/1, /*yStride=*/io->xsize()); - JXL_RETURN_IF_ERROR(RunOnPool( - pool, start_y, end_y + 1, ThreadPool::NoInit, - [&](const uint32_t y, size_t /* thread */) { - const float* const JXL_RESTRICT input_rows[] = { - linear->color().ConstPlaneRow(0, y), - linear->color().ConstPlaneRow(1, y), - linear->color().ConstPlaneRow(2, y), - }; - OpenEXR::Rgba* const JXL_RESTRICT row_data = - &output_rows[(y - start_y) * io->xsize()]; - if (has_alpha) { - const float* const JXL_RESTRICT alpha_row = - io->Main().alpha().ConstRow(y); - if (alpha_is_premultiplied) { - for (size_t x = 0; x < io->xsize(); ++x) { - row_data[x] = - OpenEXR::Rgba(input_rows[0][x], input_rows[1][x], - input_rows[2][x], alpha_row[x]); - } - } else { - for (size_t x = 0; x < io->xsize(); ++x) { - row_data[x] = OpenEXR::Rgba(alpha_row[x] * input_rows[0][x], - alpha_row[x] * input_rows[1][x], - alpha_row[x] * input_rows[2][x], - alpha_row[x]); - } - } - } else { - for (size_t x = 0; x < io->xsize(); ++x) { - row_data[x] = OpenEXR::Rgba(input_rows[0][x], input_rows[1][x], - input_rows[2][x], 1.f); - } - } - }, - "EncodeImageEXR")); + const size_t end_y = std::min(start_y + y_chunk_size - 1, ysize - 1); + output.setFrameBuffer(output_rows.data() - start_y * xsize, + /*xStride=*/1, /*yStride=*/xsize); + for (size_t y = start_y; y <= end_y; ++y) { + const uint8_t* in_row = &in[(y - start_y) * in_stride]; + OpenEXR::Rgba* const JXL_RESTRICT row_data = + &output_rows[(y - start_y) * xsize]; + for (size_t x = 0; x < xsize; ++x) { + const uint8_t* in_pixel = &in_row[4 * num_channels * x]; + float r = loadFloat(&in_pixel[0]); + float g = loadFloat(&in_pixel[4]); + float b = loadFloat(&in_pixel[8]); + const float alpha = loadAlpha(&in_pixel[12]); + if (!alpha_is_premultiplied) { + r *= alpha; + g *= alpha; + b *= alpha; + } + row_data[x] = OpenEXR::Rgba(r, g, b, alpha); + } + } output.writePixels(/*numScanLines=*/end_y - start_y + 1); } } @@ -163,5 +158,43 @@ Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, return true; } +class EXREncoder : public Encoder { + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 2, 3, 4}) { + for (const JxlDataType data_type : {JXL_TYPE_FLOAT, JXL_TYPE_FLOAT16}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/data_type, + /*endianness=*/endianness, + /*align=*/0}); + } + } + } + return formats; + } + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + encoded_image->icc.clear(); + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImageEXR(frame.color, ppf.info, + ppf.color_encoding, pool, + &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr<Encoder> GetEXREncoder() { + return jxl::make_unique<EXREncoder>(); +} + } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/exr.h b/media/libjxl/src/lib/extras/enc/exr.h index 0423bcbadb..1baaa0272f 100644 --- a/media/libjxl/src/lib/extras/enc/exr.h +++ b/media/libjxl/src/lib/extras/enc/exr.h @@ -8,21 +8,14 @@ // Encodes OpenEXR images in memory. -#include "lib/extras/packed_image.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/codec_in_out.h" -#include "lib/jxl/color_encoding_internal.h" +#include <memory> + +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -// Transforms from io->c_current to `c_desired` (with the transfer function set -// to linear as that is the OpenEXR convention) and encodes into `bytes`. -Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, - ThreadPool* pool, PaddedBytes* bytes); +std::unique_ptr<Encoder> GetEXREncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/jpg.cc b/media/libjxl/src/lib/extras/enc/jpg.cc index 83b2895756..93a39dd2e4 100644 --- a/media/libjxl/src/lib/extras/enc/jpg.cc +++ b/media/libjxl/src/lib/extras/enc/jpg.cc @@ -12,19 +12,12 @@ #include <algorithm> #include <iterator> #include <numeric> +#include <sstream> #include <utility> #include <vector> -#include "lib/jxl/base/compiler_specific.h" +#include "lib/extras/exif.h" #include "lib/jxl/base/status.h" -#include "lib/jxl/color_encoding_internal.h" -#include "lib/jxl/common.h" -#include "lib/jxl/dec_external_image.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_image_bundle.h" -#include "lib/jxl/exif.h" -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" #include "lib/jxl/sanitizers.h" #if JPEGXL_ENABLE_SJPEG #include "sjpeg.h" @@ -44,8 +37,21 @@ constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, 0x66, 0x00, 0x00}; constexpr int kExifMarker = JPEG_APP0 + 1; +enum class JpegEncoder { + kLibJpeg, + kSJpeg, +}; + +bool IsSRGBEncoding(const JxlColorEncoding& c) { + return ((c.color_space == JXL_COLOR_SPACE_RGB || + c.color_space == JXL_COLOR_SPACE_GRAY) && + c.primaries == JXL_PRIMARIES_SRGB && + c.white_point == JXL_WHITE_POINT_D65 && + c.transfer_function == JXL_TRANSFER_FUNCTION_SRGB); +} + void WriteICCProfile(jpeg_compress_struct* const cinfo, - const PaddedBytes& icc) { + const std::vector<uint8_t>& icc) { constexpr size_t kMaxIccBytesInMarker = kMaxBytesInMarker - sizeof kICCSignature - 2; const int num_markers = @@ -80,25 +86,34 @@ void WriteExif(jpeg_compress_struct* const cinfo, } } -Status SetChromaSubsampling(const YCbCrChromaSubsampling& chroma_subsampling, +Status SetChromaSubsampling(const std::string& subsampling, jpeg_compress_struct* const cinfo) { - for (size_t i = 0; i < 3; i++) { - cinfo->comp_info[i].h_samp_factor = - 1 << (chroma_subsampling.MaxHShift() - - chroma_subsampling.HShift(i < 2 ? i ^ 1 : i)); - cinfo->comp_info[i].v_samp_factor = - 1 << (chroma_subsampling.MaxVShift() - - chroma_subsampling.VShift(i < 2 ? i ^ 1 : i)); + const std::pair<const char*, + std::pair<std::array<uint8_t, 3>, std::array<uint8_t, 3>>> + options[] = {{"444", {{{1, 1, 1}}, {{1, 1, 1}}}}, + {"420", {{{2, 1, 1}}, {{2, 1, 1}}}}, + {"422", {{{2, 1, 1}}, {{1, 1, 1}}}}, + {"440", {{{1, 1, 1}}, {{2, 1, 1}}}}}; + for (const auto& option : options) { + if (subsampling == option.first) { + for (size_t i = 0; i < 3; i++) { + cinfo->comp_info[i].h_samp_factor = option.second.first[i]; + cinfo->comp_info[i].v_samp_factor = option.second.second[i]; + } + return true; + } } - return true; + return false; } -} // namespace - -Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, - size_t quality, - const YCbCrChromaSubsampling& chroma_subsampling, - PaddedBytes* bytes) { +Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info, + const std::vector<uint8_t>& icc, + std::vector<uint8_t> exif, size_t quality, + const std::string& chroma_subsampling, + std::vector<uint8_t>* bytes) { + if (BITS_IN_JSAMPLE != 8 || sizeof(JSAMPLE) != 1) { + return JXL_FAILURE("Only 8 bit JSAMPLE is supported."); + } jpeg_compress_struct cinfo; // cinfo is initialized by libjpeg, which we are not instrumenting with // msan. @@ -109,15 +124,10 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, unsigned char* buffer = nullptr; unsigned long size = 0; jpeg_mem_dest(&cinfo, &buffer, &size); - cinfo.image_width = ib->oriented_xsize(); - cinfo.image_height = ib->oriented_ysize(); - if (ib->IsGray()) { - cinfo.input_components = 1; - cinfo.in_color_space = JCS_GRAYSCALE; - } else { - cinfo.input_components = 3; - cinfo.in_color_space = JCS_RGB; - } + cinfo.image_width = image.xsize; + cinfo.image_height = image.ysize; + cinfo.input_components = info.num_color_channels; + cinfo.in_color_space = info.num_color_channels == 1 ? JCS_GRAYSCALE : JCS_RGB; jpeg_set_defaults(&cinfo); cinfo.optimize_coding = TRUE; if (cinfo.input_components == 3) { @@ -125,27 +135,21 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, } jpeg_set_quality(&cinfo, quality, TRUE); jpeg_start_compress(&cinfo, TRUE); - if (!ib->IsSRGB()) { - WriteICCProfile(&cinfo, ib->c_current().ICC()); + if (!icc.empty()) { + WriteICCProfile(&cinfo, icc); } - if (!io->blobs.exif.empty()) { - std::vector<uint8_t> exif = io->blobs.exif; + if (!exif.empty()) { ResetExifOrientation(exif); WriteExif(&cinfo, exif); } if (cinfo.input_components > 3 || cinfo.input_components < 0) return JXL_FAILURE("invalid numbers of components"); - size_t stride = - ib->oriented_xsize() * cinfo.input_components * sizeof(JSAMPLE); - PaddedBytes raw_bytes(stride * ib->oriented_ysize()); - JXL_RETURN_IF_ERROR(ConvertToExternal( - *ib, BITS_IN_JSAMPLE, /*float_out=*/false, cinfo.input_components, - JXL_BIG_ENDIAN, stride, nullptr, raw_bytes.data(), raw_bytes.size(), - /*out_callback=*/{}, ib->metadata()->GetOrientation())); - - for (size_t y = 0; y < ib->oriented_ysize(); ++y) { - JSAMPROW row[] = {raw_bytes.data() + y * stride}; + std::vector<uint8_t> raw_bytes(image.pixels_size); + memcpy(&raw_bytes[0], reinterpret_cast<const uint8_t*>(image.pixels()), + image.pixels_size); + for (size_t y = 0; y < info.ysize; ++y) { + JSAMPROW row[] = {raw_bytes.data() + y * image.stride}; jpeg_write_scanlines(&cinfo, row, 1); } @@ -160,41 +164,34 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, return true; } -Status EncodeWithSJpeg(const ImageBundle* ib, const CodecInOut* io, - size_t quality, - const YCbCrChromaSubsampling& chroma_subsampling, - PaddedBytes* bytes) { +Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info, + const std::vector<uint8_t>& icc, + std::vector<uint8_t> exif, size_t quality, + const std::string& chroma_subsampling, + std::vector<uint8_t>* bytes) { #if !JPEGXL_ENABLE_SJPEG return JXL_FAILURE("JPEG XL was built without sjpeg support"); #else sjpeg::EncoderParam param(quality); - if (!ib->IsSRGB()) { - param.iccp.assign(ib->metadata()->color_encoding.ICC().begin(), - ib->metadata()->color_encoding.ICC().end()); + if (!icc.empty()) { + param.iccp.assign(icc.begin(), icc.end()); } - std::vector<uint8_t> exif = io->blobs.exif; if (!exif.empty()) { ResetExifOrientation(exif); param.exif.assign(exif.begin(), exif.end()); } - if (chroma_subsampling.Is444()) { + if (chroma_subsampling == "444") { param.yuv_mode = SJPEG_YUV_444; - } else if (chroma_subsampling.Is420()) { + } else if (chroma_subsampling == "420") { param.yuv_mode = SJPEG_YUV_SHARP; } else { return JXL_FAILURE("sjpeg does not support this chroma subsampling mode"); } - size_t stride = ib->oriented_xsize() * 3; - PaddedBytes rgb(ib->xsize() * ib->ysize() * 3); - JXL_RETURN_IF_ERROR( - ConvertToExternal(*ib, 8, /*float_out=*/false, 3, JXL_BIG_ENDIAN, stride, - nullptr, rgb.data(), rgb.size(), - /*out_callback=*/{}, ib->metadata()->GetOrientation())); - + size_t stride = info.xsize * 3; + const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels()); std::string output; - JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib->oriented_xsize(), - ib->oriented_ysize(), stride, param, - &output)); + JXL_RETURN_IF_ERROR( + sjpeg::Encode(pixels, image.xsize, image.ysize, stride, param, &output)); bytes->assign( reinterpret_cast<const uint8_t*>(output.data()), reinterpret_cast<const uint8_t*>(output.data() + output.size())); @@ -202,31 +199,30 @@ Status EncodeWithSJpeg(const ImageBundle* ib, const CodecInOut* io, #endif } -Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, - YCbCrChromaSubsampling chroma_subsampling, - ThreadPool* pool, PaddedBytes* bytes) { - if (io->Main().HasAlpha()) { +Status EncodeImageJPG(const PackedImage& image, const JxlBasicInfo& info, + const std::vector<uint8_t>& icc, + std::vector<uint8_t> exif, JpegEncoder encoder, + size_t quality, const std::string& chroma_subsampling, + ThreadPool* pool, std::vector<uint8_t>* bytes) { + if (image.format.data_type != JXL_TYPE_UINT8) { + return JXL_FAILURE("Unsupported pixel data type"); + } + if (info.alpha_bits > 0) { return JXL_FAILURE("alpha is not supported"); } if (quality > 100) { return JXL_FAILURE("please specify a 0-100 JPEG quality"); } - const ImageBundle* ib; - ImageMetadata metadata = io->metadata.m; - ImageBundle ib_store(&metadata); - JXL_RETURN_IF_ERROR(TransformIfNeeded(io->Main(), - io->metadata.m.color_encoding, - GetJxlCms(), pool, &ib_store, &ib)); - switch (encoder) { case JpegEncoder::kLibJpeg: - JXL_RETURN_IF_ERROR( - EncodeWithLibJpeg(ib, io, quality, chroma_subsampling, bytes)); + JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(image, info, icc, std::move(exif), + quality, chroma_subsampling, + bytes)); break; case JpegEncoder::kSJpeg: - JXL_RETURN_IF_ERROR( - EncodeWithSJpeg(ib, io, quality, chroma_subsampling, bytes)); + JXL_RETURN_IF_ERROR(EncodeWithSJpeg(image, info, icc, std::move(exif), + quality, chroma_subsampling, bytes)); break; default: return JXL_FAILURE("tried to use an unknown JPEG encoder"); @@ -235,5 +231,68 @@ Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, return true; } +class JPEGEncoder : public Encoder { + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 3}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/endianness, + /*align=*/0}); + } + } + return formats; + } + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + const auto& options = this->options(); + int quality = 100; + auto it_quality = options.find("q"); + if (it_quality != options.end()) { + std::istringstream is(it_quality->second); + JXL_RETURN_IF_ERROR(static_cast<bool>(is >> quality)); + } + std::string chroma_subsampling = "444"; + auto it_chroma_subsampling = options.find("chroma_subsampling"); + if (it_chroma_subsampling != options.end()) { + chroma_subsampling = it_chroma_subsampling->second; + } + JpegEncoder jpeg_encoder = JpegEncoder::kLibJpeg; + auto it_encoder = options.find("jpeg_encoder"); + if (it_encoder != options.end()) { + if (it_encoder->second == "libjpeg") { + jpeg_encoder = JpegEncoder::kLibJpeg; + } else if (it_encoder->second == "sjpeg") { + jpeg_encoder = JpegEncoder::kSJpeg; + } else { + return JXL_FAILURE("unknown jpeg encoder \"%s\"", + it_encoder->second.c_str()); + } + } + std::vector<uint8_t> icc; + if (!IsSRGBEncoding(ppf.color_encoding)) { + icc = ppf.icc; + } + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImageJPG( + frame.color, ppf.info, icc, ppf.metadata.exif, jpeg_encoder, quality, + chroma_subsampling, pool, &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr<Encoder> GetJPEGEncoder() { + return jxl::make_unique<JPEGEncoder>(); +} + } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/jpg.h b/media/libjxl/src/lib/extras/enc/jpg.h index ccea1415a8..20b37cd168 100644 --- a/media/libjxl/src/lib/extras/enc/jpg.h +++ b/media/libjxl/src/lib/extras/enc/jpg.h @@ -8,27 +8,14 @@ // Encodes JPG pixels and metadata in memory. -#include <stdint.h> +#include <memory> -#include "lib/extras/codec.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/codec_in_out.h" +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -enum class JpegEncoder { - kLibJpeg, - kSJpeg, -}; - -// Encodes into `bytes`. -Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, - YCbCrChromaSubsampling chroma_subsampling, - ThreadPool* pool, PaddedBytes* bytes); +std::unique_ptr<Encoder> GetJPEGEncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/npy.cc b/media/libjxl/src/lib/extras/enc/npy.cc new file mode 100644 index 0000000000..1428e6427d --- /dev/null +++ b/media/libjxl/src/lib/extras/enc/npy.cc @@ -0,0 +1,322 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/enc/npy.h" + +#include <stdio.h> + +#include <sstream> +#include <string> +#include <vector> + +#include "jxl/types.h" +#include "lib/extras/packed_image.h" + +namespace jxl { +namespace extras { +namespace { + +// JSON value writing + +class JSONField { + public: + virtual ~JSONField() = default; + virtual void Write(std::ostream& o, uint32_t indent) const = 0; + + protected: + JSONField() = default; +}; + +class JSONValue : public JSONField { + public: + template <typename T> + explicit JSONValue(const T& value) : value_(std::to_string(value)) {} + + explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {} + + explicit JSONValue(bool value) : value_(value ? "true" : "false") {} + + void Write(std::ostream& o, uint32_t indent) const override { o << value_; } + + private: + std::string value_; +}; + +class JSONDict : public JSONField { + public: + JSONDict() = default; + + template <typename T> + T* AddEmpty(const std::string& key) { + static_assert(std::is_convertible<T*, JSONField*>::value, + "T must be a JSONField"); + T* ret = new T(); + values_.emplace_back( + key, std::unique_ptr<JSONField>(static_cast<JSONField*>(ret))); + return ret; + } + + template <typename T> + void Add(const std::string& key, const T& value) { + values_.emplace_back(key, std::unique_ptr<JSONField>(new JSONValue(value))); + } + + void Write(std::ostream& o, uint32_t indent) const override { + std::string indent_str(indent, ' '); + o << "{"; + bool is_first = true; + for (const auto& key_value : values_) { + if (!is_first) { + o << ","; + } + is_first = false; + o << std::endl << indent_str << " \"" << key_value.first << "\": "; + key_value.second->Write(o, indent + 2); + } + if (!values_.empty()) { + o << std::endl << indent_str; + } + o << "}"; + } + + private: + // Dictionary with order. + std::vector<std::pair<std::string, std::unique_ptr<JSONField>>> values_; +}; + +class JSONArray : public JSONField { + public: + JSONArray() = default; + + template <typename T> + T* AddEmpty() { + static_assert(std::is_convertible<T*, JSONField*>::value, + "T must be a JSONField"); + T* ret = new T(); + values_.emplace_back(ret); + return ret; + } + + template <typename T> + void Add(const T& value) { + values_.emplace_back(new JSONValue(value)); + } + + void Write(std::ostream& o, uint32_t indent) const override { + std::string indent_str(indent, ' '); + o << "["; + bool is_first = true; + for (const auto& value : values_) { + if (!is_first) { + o << ","; + } + is_first = false; + o << std::endl << indent_str << " "; + value->Write(o, indent + 2); + } + if (!values_.empty()) { + o << std::endl << indent_str; + } + o << "]"; + } + + private: + std::vector<std::unique_ptr<JSONField>> values_; +}; + +void GenerateMetadata(const PackedPixelFile& ppf, std::vector<uint8_t>* out) { + JSONDict meta; + // Same order as in 18181-3 CD. + + // Frames. + auto* meta_frames = meta.AddEmpty<JSONArray>("frames"); + for (size_t i = 0; i < ppf.frames.size(); i++) { + auto* frame_i = meta_frames->AddEmpty<JSONDict>(); + if (ppf.info.have_animation) { + frame_i->Add("duration", + JSONValue(ppf.frames[i].frame_info.duration * 1.0f * + ppf.info.animation.tps_denominator / + ppf.info.animation.tps_numerator)); + } + + frame_i->Add("name", JSONValue(ppf.frames[i].name)); + + if (ppf.info.animation.have_timecodes) { + frame_i->Add("timecode", JSONValue(ppf.frames[i].frame_info.timecode)); + } + } + +#define METADATA(FIELD) meta.Add(#FIELD, ppf.info.FIELD) + + METADATA(intensity_target); + METADATA(min_nits); + METADATA(relative_to_max_display); + METADATA(linear_below); + + if (ppf.info.have_preview) { + meta.AddEmpty<JSONDict>("preview"); + // TODO(veluca): can we have duration/name/timecode here? + } + + { + auto ectype = meta.AddEmpty<JSONArray>("extra_channel_type"); + auto bps = meta.AddEmpty<JSONArray>("bits_per_sample"); + auto ebps = meta.AddEmpty<JSONArray>("exp_bits_per_sample"); + bps->Add(ppf.info.bits_per_sample); + ebps->Add(ppf.info.exponent_bits_per_sample); + for (size_t i = 0; i < ppf.extra_channels_info.size(); i++) { + switch (ppf.extra_channels_info[i].ec_info.type) { + case JXL_CHANNEL_ALPHA: { + ectype->Add(std::string("Alpha")); + break; + } + case JXL_CHANNEL_DEPTH: { + ectype->Add(std::string("Depth")); + break; + } + case JXL_CHANNEL_SPOT_COLOR: { + ectype->Add(std::string("SpotColor")); + break; + } + case JXL_CHANNEL_SELECTION_MASK: { + ectype->Add(std::string("SelectionMask")); + break; + } + case JXL_CHANNEL_BLACK: { + ectype->Add(std::string("Black")); + break; + } + case JXL_CHANNEL_CFA: { + ectype->Add(std::string("CFA")); + break; + } + case JXL_CHANNEL_THERMAL: { + ectype->Add(std::string("Thermal")); + break; + } + default: { + ectype->Add(std::string("UNKNOWN")); + break; + } + } + bps->Add(ppf.extra_channels_info[i].ec_info.bits_per_sample); + ebps->Add(ppf.extra_channels_info[i].ec_info.exponent_bits_per_sample); + } + } + + std::ostringstream os; + meta.Write(os, 0); + out->resize(os.str().size()); + memcpy(out->data(), os.str().data(), os.str().size()); +} + +void Append(std::vector<uint8_t>* out, const void* data, size_t size) { + size_t pos = out->size(); + out->resize(pos + size); + memcpy(out->data() + pos, data, size); +} + +void WriteNPYHeader(size_t xsize, size_t ysize, uint32_t num_channels, + size_t num_frames, std::vector<uint8_t>* out) { + const uint8_t header[] = "\x93NUMPY\x01\x00"; + Append(out, header, 8); + std::stringstream ss; + ss << "{'descr': '<f4', 'fortran_order': False, 'shape': (" << num_frames + << ", " << ysize << ", " << xsize << ", " << num_channels << "), }\n"; + // 16-bit little endian header length. + uint8_t header_len[2] = {static_cast<uint8_t>(ss.str().size() % 256), + static_cast<uint8_t>(ss.str().size() / 256)}; + Append(out, header_len, 2); + Append(out, ss.str().data(), ss.str().size()); +} + +bool WriteFrameToNPYArray(size_t xsize, size_t ysize, const PackedFrame& frame, + std::vector<uint8_t>* out) { + const auto& color = frame.color; + if (color.xsize != xsize || color.ysize != ysize) { + return false; + } + for (const auto& ec : frame.extra_channels) { + if (ec.xsize != xsize || ec.ysize != ysize) { + return false; + } + } + // interleave the samples from color and extra channels + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + { + size_t sample_size = color.pixel_stride(); + size_t offset = y * color.stride + x * sample_size; + uint8_t* pixels = reinterpret_cast<uint8_t*>(color.pixels()); + JXL_ASSERT(offset + sample_size <= color.pixels_size); + Append(out, pixels + offset, sample_size); + } + for (const auto& ec : frame.extra_channels) { + size_t sample_size = ec.pixel_stride(); + size_t offset = y * ec.stride + x * sample_size; + uint8_t* pixels = reinterpret_cast<uint8_t*>(ec.pixels()); + JXL_ASSERT(offset + sample_size <= ec.pixels_size); + Append(out, pixels + offset, sample_size); + } + } + } + return true; +} + +// Writes a PackedPixelFile as a numpy 4D ndarray in binary format. +bool WriteNPYArray(const PackedPixelFile& ppf, std::vector<uint8_t>* out) { + size_t xsize = ppf.info.xsize; + size_t ysize = ppf.info.ysize; + WriteNPYHeader(xsize, ysize, + ppf.info.num_color_channels + ppf.extra_channels_info.size(), + ppf.frames.size(), out); + for (const auto& frame : ppf.frames) { + if (!WriteFrameToNPYArray(xsize, ysize, frame, out)) { + return false; + } + } + return true; +} + +class NumPyEncoder : public Encoder { + public: + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + GenerateMetadata(ppf, &encoded_image->metadata); + encoded_image->bitstreams.emplace_back(); + if (!WriteNPYArray(ppf, &encoded_image->bitstreams.back())) { + return false; + } + if (ppf.preview_frame) { + size_t xsize = ppf.info.preview.xsize; + size_t ysize = ppf.info.preview.ysize; + WriteNPYHeader(xsize, ysize, ppf.info.num_color_channels, 1, + &encoded_image->preview_bitstream); + if (!WriteFrameToNPYArray(xsize, ysize, *ppf.preview_frame, + &encoded_image->preview_bitstream)) { + return false; + } + } + return true; + } + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 3}) { + formats.push_back(JxlPixelFormat{num_channels, JXL_TYPE_FLOAT, + JXL_LITTLE_ENDIAN, /*align=*/0}); + } + return formats; + } +}; + +} // namespace + +std::unique_ptr<Encoder> GetNumPyEncoder() { + return jxl::make_unique<NumPyEncoder>(); +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/npy.h b/media/libjxl/src/lib/extras/enc/npy.h new file mode 100644 index 0000000000..3ee6208ec2 --- /dev/null +++ b/media/libjxl/src/lib/extras/enc/npy.h @@ -0,0 +1,23 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_ENC_NPY_H_ +#define LIB_EXTRAS_ENC_NPY_H_ + +// Encodes pixels to numpy array, used for conformance testing. + +#include <memory> + +#include "lib/extras/enc/encode.h" + +namespace jxl { +namespace extras { + +std::unique_ptr<Encoder> GetNumPyEncoder(); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ENC_NPY_H_ diff --git a/media/libjxl/src/lib/extras/enc/pgx.cc b/media/libjxl/src/lib/extras/enc/pgx.cc index cc333a62ac..ef204ad1c6 100644 --- a/media/libjxl/src/lib/extras/enc/pgx.cc +++ b/media/libjxl/src/lib/extras/enc/pgx.cc @@ -8,18 +8,10 @@ #include <stdio.h> #include <string.h> -#include "lib/jxl/base/bits.h" -#include "lib/jxl/base/compiler_specific.h" -#include "lib/jxl/base/file_io.h" +#include "jxl/codestream_header.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/printf_macros.h" -#include "lib/jxl/color_management.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/fields.h" // AllDefault -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" namespace jxl { namespace extras { @@ -27,60 +19,63 @@ namespace { constexpr size_t kMaxHeaderSize = 200; -Status EncodeHeader(const ImageBundle& ib, const size_t bits_per_sample, - char* header, int* JXL_RESTRICT chars_written) { - if (ib.HasAlpha()) return JXL_FAILURE("PGX: can't store alpha"); - if (!ib.IsGray()) return JXL_FAILURE("PGX: must be grayscale"); +Status EncodeHeader(const JxlBasicInfo& info, char* header, + int* chars_written) { + if (info.alpha_bits > 0) { + return JXL_FAILURE("PGX: can't store alpha"); + } + if (info.num_color_channels != 1) { + return JXL_FAILURE("PGX: must be grayscale"); + } // TODO(lode): verify other bit depths: for other bit depths such as 1 or 4 // bits, have a test case to verify it works correctly. For bits > 16, we may // need to change the way external_image works. - if (bits_per_sample != 8 && bits_per_sample != 16) { + if (info.bits_per_sample != 8 && info.bits_per_sample != 16) { return JXL_FAILURE("PGX: bits other than 8 or 16 not yet supported"); } // Use ML (Big Endian), LM may not be well supported by all decoders. - *chars_written = snprintf(header, kMaxHeaderSize, - "PG ML + %" PRIuS " %" PRIuS " %" PRIuS "\n", - bits_per_sample, ib.xsize(), ib.ysize()); + *chars_written = snprintf(header, kMaxHeaderSize, "PG ML + %u %u %u\n", + info.bits_per_sample, info.xsize, info.ysize); JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < kMaxHeaderSize); return true; } -} // namespace +Status EncodeImagePGX(const PackedFrame& frame, const JxlBasicInfo& info, + std::vector<uint8_t>* bytes) { + char header[kMaxHeaderSize]; + int header_size = 0; + JXL_RETURN_IF_ERROR(EncodeHeader(info, header, &header_size)); -Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes) { - if (!Bundle::AllDefault(io->metadata.m)) { - JXL_WARNING("PGX encoder ignoring metadata - use a different codec"); - } - if (!c_desired.IsSRGB()) { - JXL_WARNING( - "PGX encoder cannot store custom ICC profile; decoder\n" - "will need hint key=color_space to get the same values"); + const PackedImage& color = frame.color; + const JxlPixelFormat format = color.format; + const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels()); + size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type); + size_t bytes_per_sample = data_bits_per_sample / kBitsPerByte; + size_t num_samples = info.xsize * info.ysize; + + if (info.bits_per_sample != data_bits_per_sample) { + return JXL_FAILURE("Bit depth does not match pixel data type"); } - ImageBundle ib = io->Main().Copy(); - - ImageMetadata metadata = io->metadata.m; - ImageBundle store(&metadata); - const ImageBundle* transformed; - JXL_RETURN_IF_ERROR(TransformIfNeeded(ib, c_desired, GetJxlCms(), pool, - &store, &transformed)); - PaddedBytes pixels(ib.xsize() * ib.ysize() * - (bits_per_sample / kBitsPerByte)); - size_t stride = ib.xsize() * (bits_per_sample / kBitsPerByte); - JXL_RETURN_IF_ERROR( - ConvertToExternal(*transformed, bits_per_sample, - /*float_out=*/false, - /*num_channels=*/1, JXL_BIG_ENDIAN, stride, pool, - pixels.data(), pixels.size(), - /*out_callback=*/{}, metadata.GetOrientation())); + std::vector<uint8_t> pixels(num_samples * bytes_per_sample); - char header[kMaxHeaderSize]; - int header_size = 0; - JXL_RETURN_IF_ERROR(EncodeHeader(ib, bits_per_sample, header, &header_size)); + if (format.data_type == JXL_TYPE_UINT8) { + memcpy(&pixels[0], in, num_samples * bytes_per_sample); + } else if (format.data_type == JXL_TYPE_UINT16) { + if (format.endianness != JXL_BIG_ENDIAN) { + const uint8_t* p_in = in; + uint8_t* p_out = pixels.data(); + for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) { + StoreBE16(LoadLE16(p_in), p_out); + } + } else { + memcpy(&pixels[0], in, num_samples * bytes_per_sample); + } + } else { + return JXL_FAILURE("Unsupported pixel data type"); + } bytes->resize(static_cast<size_t>(header_size) + pixels.size()); memcpy(bytes->data(), header, static_cast<size_t>(header_size)); @@ -89,5 +84,41 @@ Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, return true; } +class PGXEncoder : public Encoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/1, + /*data_type=*/data_type, + /*endianness=*/endianness, + /*align=*/0}); + } + } + return formats; + } + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + encoded_image->icc.assign(ppf.icc.begin(), ppf.icc.end()); + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR( + EncodeImagePGX(frame, ppf.info, &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr<Encoder> GetPGXEncoder() { + return jxl::make_unique<PGXEncoder>(); +} + } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/pgx.h b/media/libjxl/src/lib/extras/enc/pgx.h index 6f14e20668..f24e391b09 100644 --- a/media/libjxl/src/lib/extras/enc/pgx.h +++ b/media/libjxl/src/lib/extras/enc/pgx.h @@ -11,21 +11,12 @@ #include <stddef.h> #include <stdint.h> -#include "lib/extras/packed_image.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/codec_in_out.h" -#include "lib/jxl/color_encoding_internal.h" +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -// Transforms from io->c_current to `c_desired` and encodes into `bytes`. -Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes); +std::unique_ptr<Encoder> GetPGXEncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/pnm.cc b/media/libjxl/src/lib/extras/enc/pnm.cc index 9cedda09d5..9b5f6cbc95 100644 --- a/media/libjxl/src/lib/extras/enc/pnm.cc +++ b/media/libjxl/src/lib/extras/enc/pnm.cc @@ -32,22 +32,19 @@ namespace { constexpr size_t kMaxHeaderSize = 200; -Status EncodeHeader(const PackedPixelFile& ppf, const size_t bits_per_sample, - const bool little_endian, char* header, - int* JXL_RESTRICT chars_written) { - bool is_gray = ppf.info.num_color_channels <= 2; - size_t oriented_xsize = - ppf.info.orientation <= 4 ? ppf.info.xsize : ppf.info.ysize; - size_t oriented_ysize = - ppf.info.orientation <= 4 ? ppf.info.ysize : ppf.info.xsize; - if (ppf.info.alpha_bits > 0) { // PAM +Status EncodeHeader(const PackedImage& image, size_t bits_per_sample, + bool little_endian, char* header, int* chars_written) { + size_t num_channels = image.format.num_channels; + bool is_gray = num_channels <= 2; + bool has_alpha = num_channels == 2 || num_channels == 4; + if (has_alpha) { // PAM if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits"); const uint32_t max_val = (1U << bits_per_sample) - 1; *chars_written = snprintf(header, kMaxHeaderSize, "P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS "\nDEPTH %u\nMAXVAL %u\nTUPLTYPE %s\nENDHDR\n", - oriented_xsize, oriented_ysize, is_gray ? 2 : 4, max_val, + image.xsize, image.ysize, is_gray ? 2 : 4, max_val, is_gray ? "GRAYSCALE_ALPHA" : "RGB_ALPHA"); JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < kMaxHeaderSize); @@ -56,77 +53,164 @@ Status EncodeHeader(const PackedPixelFile& ppf, const size_t bits_per_sample, const double scale = little_endian ? -1.0 : 1.0; *chars_written = snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n", - type, oriented_xsize, oriented_ysize, scale); + type, image.xsize, image.ysize, scale); JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < kMaxHeaderSize); } else { // PGM/PPM + if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits"); const uint32_t max_val = (1U << bits_per_sample) - 1; - if (max_val >= 65536) return JXL_FAILURE("PNM cannot have > 16 bits"); const char type = is_gray ? '5' : '6'; *chars_written = snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n", - type, oriented_xsize, oriented_ysize, max_val); + type, image.xsize, image.ysize, max_val); JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < kMaxHeaderSize); } return true; } -Span<const uint8_t> MakeSpan(const char* str) { - return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), - strlen(str)); +Status EncodeImagePNM(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) { + // Choose native for PFM; PGM/PPM require big-endian + bool is_little_endian = bits_per_sample > 16 && IsLittleEndian(); + char header[kMaxHeaderSize]; + int header_size = 0; + JXL_RETURN_IF_ERROR(EncodeHeader(image, bits_per_sample, is_little_endian, + header, &header_size)); + bytes->resize(static_cast<size_t>(header_size) + image.pixels_size); + memcpy(bytes->data(), header, static_cast<size_t>(header_size)); + const bool flipped_y = bits_per_sample == 32; // PFMs are flipped + const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels()); + uint8_t* out = bytes->data() + header_size; + for (size_t y = 0; y < image.ysize; ++y) { + size_t y_out = flipped_y ? image.ysize - 1 - y : y; + const uint8_t* row_in = &in[y * image.stride]; + uint8_t* row_out = &out[y_out * image.stride]; + memcpy(row_out, row_in, image.stride); + } + return true; } -// Flip the image vertically for loading/saving PFM files which have the -// scanlines inverted. -void VerticallyFlipImage(float* const float_image, const size_t xsize, - const size_t ysize, const size_t num_channels) { - for (size_t y = 0; y < ysize / 2; y++) { - float* first_row = &float_image[y * num_channels * xsize]; - float* other_row = &float_image[(ysize - y - 1) * num_channels * xsize]; - for (size_t c = 0; c < num_channels; c++) { - for (size_t x = 0; x < xsize; ++x) { - float tmp = first_row[x * num_channels + c]; - first_row[x * num_channels + c] = other_row[x * num_channels + c]; - other_row[x * num_channels + c] = tmp; +class PNMEncoder : public Encoder { + public: + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() || + !ppf.metadata.jumbf.empty() || !ppf.metadata.xmp.empty()) { + JXL_WARNING("PNM encoder ignoring metadata - use a different codec"); + } + encoded_image->icc = ppf.icc; + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.color, ppf.info.bits_per_sample, + &encoded_image->bitstreams.back())); + } + for (size_t i = 0; i < ppf.extra_channels_info.size(); ++i) { + const auto& ec_info = ppf.extra_channels_info[i].ec_info; + encoded_image->extra_channel_bitstreams.emplace_back(); + auto& ec_bitstreams = encoded_image->extra_channel_bitstreams.back(); + for (const auto& frame : ppf.frames) { + ec_bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.extra_channels[i], + ec_info.bits_per_sample, + &ec_bitstreams.back())); } } + return true; } -} +}; -} // namespace +class PPMEncoder : public PNMEncoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 2, 3, 4}) { + for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/data_type, + /*endianness=*/endianness, + /*align=*/0}); + } + } + } + return formats; + } +}; -Status EncodeImagePNM(const PackedPixelFile& ppf, size_t bits_per_sample, - ThreadPool* pool, size_t frame_index, - std::vector<uint8_t>* bytes) { - const bool floating_point = bits_per_sample > 16; - // Choose native for PFM; PGM/PPM require big-endian - const JxlEndianness endianness = - floating_point ? JXL_NATIVE_ENDIAN : JXL_BIG_ENDIAN; - if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() || - !ppf.metadata.jumbf.empty() || !ppf.metadata.xmp.empty()) { - JXL_WARNING("PNM encoder ignoring metadata - use a different codec"); +class PFMEncoder : public PNMEncoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 3}) { + for (const JxlDataType data_type : {JXL_TYPE_FLOAT16, JXL_TYPE_FLOAT}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/data_type, + /*endianness=*/endianness, + /*align=*/0}); + } + } + } + return formats; } +}; - char header[kMaxHeaderSize]; - int header_size = 0; - bool is_little_endian = endianness == JXL_LITTLE_ENDIAN || - (endianness == JXL_NATIVE_ENDIAN && IsLittleEndian()); - JXL_RETURN_IF_ERROR(EncodeHeader(ppf, bits_per_sample, is_little_endian, - header, &header_size)); - bytes->resize(static_cast<size_t>(header_size) + - ppf.frames[frame_index].color.pixels_size); - memcpy(bytes->data(), header, static_cast<size_t>(header_size)); - memcpy(bytes->data() + header_size, ppf.frames[frame_index].color.pixels(), - ppf.frames[frame_index].color.pixels_size); - if (floating_point) { - VerticallyFlipImage(reinterpret_cast<float*>(bytes->data() + header_size), - ppf.frames[frame_index].color.xsize, - ppf.frames[frame_index].color.ysize, - ppf.info.num_color_channels); +class PGMEncoder : public PPMEncoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats = PPMEncoder::AcceptedFormats(); + for (auto it = formats.begin(); it != formats.end();) { + if (it->num_channels > 2) { + it = formats.erase(it); + } else { + ++it; + } + } + return formats; } +}; - return true; +class PAMEncoder : public PPMEncoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats = PPMEncoder::AcceptedFormats(); + for (auto it = formats.begin(); it != formats.end();) { + if (it->num_channels != 2 && it->num_channels != 4) { + it = formats.erase(it); + } else { + ++it; + } + } + return formats; + } +}; + +Span<const uint8_t> MakeSpan(const char* str) { + return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), + strlen(str)); +} + +} // namespace + +std::unique_ptr<Encoder> GetPPMEncoder() { + return jxl::make_unique<PPMEncoder>(); +} + +std::unique_ptr<Encoder> GetPFMEncoder() { + return jxl::make_unique<PFMEncoder>(); +} + +std::unique_ptr<Encoder> GetPGMEncoder() { + return jxl::make_unique<PGMEncoder>(); +} + +std::unique_ptr<Encoder> GetPAMEncoder() { + return jxl::make_unique<PAMEncoder>(); } } // namespace extras diff --git a/media/libjxl/src/lib/extras/enc/pnm.h b/media/libjxl/src/lib/extras/enc/pnm.h index ecf0526a1a..403208cecd 100644 --- a/media/libjxl/src/lib/extras/enc/pnm.h +++ b/media/libjxl/src/lib/extras/enc/pnm.h @@ -10,21 +10,17 @@ // TODO(janwas): workaround for incorrect Win64 codegen (cause unknown) #include <hwy/highway.h> +#include <memory> -#include "lib/extras/packed_image.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/codec_in_out.h" -#include "lib/jxl/color_encoding_internal.h" +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -// Transforms from io->c_current to `c_desired` and encodes into `bytes`. -Status EncodeImagePNM(const PackedPixelFile& ppf, size_t bits_per_sample, - ThreadPool* pool, size_t frame_index, - std::vector<uint8_t>* bytes); +std::unique_ptr<Encoder> GetPAMEncoder(); +std::unique_ptr<Encoder> GetPGMEncoder(); +std::unique_ptr<Encoder> GetPPMEncoder(); +std::unique_ptr<Encoder> GetPFMEncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/exif.cc b/media/libjxl/src/lib/extras/exif.cc new file mode 100644 index 0000000000..7d926558c3 --- /dev/null +++ b/media/libjxl/src/lib/extras/exif.cc @@ -0,0 +1,55 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/exif.h" + +#include "lib/jxl/base/byte_order.h" + +namespace jxl { + +constexpr uint16_t kExifOrientationTag = 274; + +void ResetExifOrientation(std::vector<uint8_t>& exif) { + if (exif.size() < 12) return; // not enough bytes for a valid exif blob + bool bigendian; + uint8_t* t = exif.data(); + if (LoadLE32(t) == 0x2A004D4D) { + bigendian = true; + } else if (LoadLE32(t) == 0x002A4949) { + bigendian = false; + } else { + return; // not a valid tiff header + } + t += 4; + uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t)); + if (exif.size() < 12 + offset + 2 || offset < 8) return; + t += offset - 4; + uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + while (nb_tags > 0) { + if (t + 12 >= exif.data() + exif.size()) return; + uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + if (tag == kExifOrientationTag) { + uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t)); + t += 4; + if (type == 3 && count == 1) { + if (bigendian) { + StoreBE16(1, t); + } else { + StoreLE16(1, t); + } + } + return; + } else { + t += 10; + nb_tags--; + } + } +} + +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/exif.h b/media/libjxl/src/lib/extras/exif.h new file mode 100644 index 0000000000..f22b2ccef5 --- /dev/null +++ b/media/libjxl/src/lib/extras/exif.h @@ -0,0 +1,20 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_EXIF_H_ +#define LIB_EXTRAS_EXIF_H_ + +#include <stdint.h> + +#include <vector> + +namespace jxl { + +// Sets the Exif orientation to the identity, to avoid repeated orientation +void ResetExifOrientation(std::vector<uint8_t>& exif); + +} // namespace jxl + +#endif // LIB_EXTRAS_EXIF_H_ diff --git a/media/libjxl/src/lib/extras/packed_image.h b/media/libjxl/src/lib/extras/packed_image.h index 59dbfe57c4..1296472101 100644 --- a/media/libjxl/src/lib/extras/packed_image.h +++ b/media/libjxl/src/lib/extras/packed_image.h @@ -22,7 +22,6 @@ #include "jxl/codestream_header.h" #include "jxl/encode.h" #include "jxl/types.h" -#include "lib/jxl/base/status.h" #include "lib/jxl/common.h" namespace jxl { @@ -33,26 +32,6 @@ class PackedImage { public: PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format) : PackedImage(xsize, ysize, format, CalcStride(format, xsize)) {} - PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format, - size_t stride) - : xsize(xsize), - ysize(ysize), - stride(stride), - format(format), - pixels_size(ysize * stride), - pixels_(malloc(std::max<size_t>(1, pixels_size)), free) {} - // Construct the image using the passed pixel buffer. The buffer is owned by - // this object and released with free(). - PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format, - void* pixels, size_t pixels_size) - : xsize(xsize), - ysize(ysize), - stride(CalcStride(format, xsize)), - format(format), - pixels_size(pixels_size), - pixels_(pixels, free) { - JXL_ASSERT(pixels_size >= stride * ysize); - } // The interleaved pixels as defined in the storage format. void* pixels() const { return pixels_.get(); } @@ -61,15 +40,6 @@ class PackedImage { size_t xsize; size_t ysize; - // Whether the y coordinate is flipped (y=0 is the last row). - bool flipped_y = false; - - // Whether the range is determined by format or by JxlBasicInfo - // e.g. if format is UINT16 and JxlBasicInfo bits_per_sample is 10, - // then if bitdepth_from_format == true, the range is 0..65535 - // while if bitdepth_from_format == false, the range is 0..1023. - bool bitdepth_from_format = true; - // The number of bytes per row. size_t stride; @@ -77,6 +47,11 @@ class PackedImage { JxlPixelFormat format; size_t pixels_size; + size_t pixel_stride() const { + return (BitsPerChannel(format.data_type) * format.num_channels / + jxl::kBitsPerByte); + } + static size_t BitsPerChannel(JxlDataType data_type) { switch (data_type) { case JXL_TYPE_UINT8: @@ -93,6 +68,15 @@ class PackedImage { } private: + PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format, + size_t stride) + : xsize(xsize), + ysize(ysize), + stride(stride), + format(format), + pixels_size(ysize * stride), + pixels_(malloc(std::max<size_t>(1, pixels_size)), free) {} + static size_t CalcStride(const JxlPixelFormat& format, size_t xsize) { size_t stride = xsize * (BitsPerChannel(format.data_type) * format.num_channels / jxl::kBitsPerByte); @@ -140,20 +124,20 @@ class PackedPixelFile { // The extra channel metadata information. struct PackedExtraChannel { - PackedExtraChannel(const JxlExtraChannelInfo& ec_info, - const std::string& name) - : ec_info(ec_info), name(name) {} - JxlExtraChannelInfo ec_info; + size_t index; std::string name; }; std::vector<PackedExtraChannel> extra_channels_info; - // Color information. If the icc is empty, the JxlColorEncoding should be used - // instead. + // Color information of the decoded pixels. + // If the icc is empty, the JxlColorEncoding should be used instead. std::vector<uint8_t> icc; JxlColorEncoding color_encoding = {}; + // The icc profile of the original image. + std::vector<uint8_t> orig_icc; + std::unique_ptr<PackedFrame> preview_frame; std::vector<PackedFrame> frames; PackedMetadata metadata; diff --git a/media/libjxl/src/lib/extras/packed_image_convert.cc b/media/libjxl/src/lib/extras/packed_image_convert.cc index 65336a70a1..dcdd12a673 100644 --- a/media/libjxl/src/lib/extras/packed_image_convert.cc +++ b/media/libjxl/src/lib/extras/packed_image_convert.cc @@ -20,6 +20,60 @@ namespace jxl { namespace extras { +Status ConvertPackedFrameToImageBundle(const JxlBasicInfo& info, + const PackedFrame& frame, + const CodecInOut& io, ThreadPool* pool, + ImageBundle* bundle) { + JXL_ASSERT(frame.color.pixels() != nullptr); + const bool float_in = frame.color.format.data_type == JXL_TYPE_FLOAT16 || + frame.color.format.data_type == JXL_TYPE_FLOAT; + size_t frame_bits_per_sample = + float_in ? PackedImage::BitsPerChannel(frame.color.format.data_type) + : info.bits_per_sample; + JXL_ASSERT(frame_bits_per_sample != 0); + // It is ok for the frame.color.format.num_channels to not match the + // number of channels on the image. + JXL_ASSERT(1 <= frame.color.format.num_channels && + frame.color.format.num_channels <= 4); + + const Span<const uint8_t> span( + static_cast<const uint8_t*>(frame.color.pixels()), + frame.color.pixels_size); + JXL_ASSERT(Rect(frame.frame_info.layer_info.crop_x0, + frame.frame_info.layer_info.crop_y0, + frame.frame_info.layer_info.xsize, + frame.frame_info.layer_info.ysize) + .IsInside(Rect(0, 0, info.xsize, info.ysize))); + if (info.have_animation) { + bundle->duration = frame.frame_info.duration; + bundle->blend = frame.frame_info.layer_info.blend_info.blendmode > 0; + bundle->use_for_next_frame = + frame.frame_info.layer_info.save_as_reference > 0; + bundle->origin.x0 = frame.frame_info.layer_info.crop_x0; + bundle->origin.y0 = frame.frame_info.layer_info.crop_y0; + } + bundle->name = frame.name; // frame.frame_info.name_length is ignored here. + JXL_ASSERT(io.metadata.m.color_encoding.IsGray() == + (frame.color.format.num_channels <= 2)); + + JXL_RETURN_IF_ERROR(ConvertFromExternal( + span, frame.color.xsize, frame.color.ysize, io.metadata.m.color_encoding, + frame.color.format.num_channels, + /*alpha_is_premultiplied=*/info.alpha_premultiplied, + frame_bits_per_sample, frame.color.format.endianness, pool, bundle, + /*float_in=*/float_in, /*align=*/0)); + + bundle->extra_channels().resize(io.metadata.m.extra_channel_info.size()); + for (size_t i = 0; i < frame.extra_channels.size(); i++) { + const auto& ppf_ec = frame.extra_channels[i]; + bundle->extra_channels()[i] = ImageF(ppf_ec.xsize, ppf_ec.ysize); + JXL_CHECK(BufferToImageF(ppf_ec.format, ppf_ec.xsize, ppf_ec.ysize, + ppf_ec.pixels(), ppf_ec.pixels_size, pool, + &bundle->extra_channels()[i])); + } + return true; +} + Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, ThreadPool* pool, CodecInOut* io) { const bool has_alpha = ppf.info.alpha_bits != 0; @@ -63,7 +117,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, PaddedBytes icc; icc.append(ppf.icc); if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) { - fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB"); + fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB\n"); io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray); } else { if (io->metadata.m.color_encoding.IsGray() != is_gray) { @@ -105,61 +159,24 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, io->metadata.m.extra_channel_info.push_back(std::move(out)); } + // Convert the preview + if (ppf.preview_frame) { + size_t preview_xsize = ppf.preview_frame->color.xsize; + size_t preview_ysize = ppf.preview_frame->color.ysize; + io->metadata.m.have_preview = true; + JXL_RETURN_IF_ERROR( + io->metadata.m.preview_size.Set(preview_xsize, preview_ysize)); + JXL_RETURN_IF_ERROR(ConvertPackedFrameToImageBundle( + ppf.info, *ppf.preview_frame, *io, pool, &io->preview_frame)); + } + // Convert the pixels io->dec_pixels = 0; io->frames.clear(); for (const auto& frame : ppf.frames) { - JXL_ASSERT(frame.color.pixels() != nullptr); - size_t frame_bits_per_sample = - (frame.color.bitdepth_from_format - ? frame.color.BitsPerChannel(frame.color.format.data_type) - : ppf.info.bits_per_sample); - JXL_ASSERT(frame_bits_per_sample != 0); - // It is ok for the frame.color.format.num_channels to not match the - // number of channels on the image. - JXL_ASSERT(1 <= frame.color.format.num_channels && - frame.color.format.num_channels <= 4); - - const Span<const uint8_t> span( - static_cast<const uint8_t*>(frame.color.pixels()), - frame.color.pixels_size); - Rect frame_rect = Rect(frame.frame_info.layer_info.crop_x0, - frame.frame_info.layer_info.crop_y0, - frame.frame_info.layer_info.xsize, - frame.frame_info.layer_info.ysize); - JXL_ASSERT(frame_rect.IsInside(Rect(0, 0, ppf.info.xsize, ppf.info.ysize))); ImageBundle bundle(&io->metadata.m); - if (ppf.info.have_animation) { - bundle.duration = frame.frame_info.duration; - bundle.blend = frame.frame_info.layer_info.blend_info.blendmode > 0; - bundle.use_for_next_frame = - frame.frame_info.layer_info.save_as_reference > 0; - bundle.origin.x0 = frame.frame_info.layer_info.crop_x0; - bundle.origin.y0 = frame.frame_info.layer_info.crop_y0; - } - bundle.name = frame.name; // frame.frame_info.name_length is ignored here. - JXL_ASSERT(io->metadata.m.color_encoding.IsGray() == - (frame.color.format.num_channels <= 2)); - - const bool float_in = frame.color.format.data_type == JXL_TYPE_FLOAT16 || - frame.color.format.data_type == JXL_TYPE_FLOAT; - JXL_RETURN_IF_ERROR(ConvertFromExternal( - span, frame.color.xsize, frame.color.ysize, - io->metadata.m.color_encoding, frame.color.format.num_channels, - /*alpha_is_premultiplied=*/ppf.info.alpha_premultiplied, - frame_bits_per_sample, frame.color.format.endianness, - /*flipped_y=*/frame.color.flipped_y, pool, &bundle, - /*float_in=*/float_in, /*align=*/0)); - - bundle.extra_channels().resize(io->metadata.m.extra_channel_info.size()); - for (size_t i = 0; i < frame.extra_channels.size(); i++) { - const auto& ppf_ec = frame.extra_channels[i]; - bundle.extra_channels()[i] = ImageF(ppf_ec.xsize, ppf_ec.ysize); - JXL_CHECK(BufferToImageF(ppf_ec.format, ppf_ec.xsize, ppf_ec.ysize, - ppf_ec.pixels(), ppf_ec.pixels_size, pool, - &bundle.extra_channels()[i])); - } - + JXL_RETURN_IF_ERROR( + ConvertPackedFrameToImageBundle(ppf.info, frame, *io, pool, &bundle)); io->frames.push_back(std::move(bundle)); io->dec_pixels += frame.color.xsize * frame.color.ysize; } @@ -222,9 +239,7 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, // Convert the color encoding ppf->icc.assign(c_desired.ICC().begin(), c_desired.ICC().end()); - if (ppf->icc.empty()) { - ConvertInternalToExternalColorEncoding(c_desired, &ppf->color_encoding); - } + ConvertInternalToExternalColorEncoding(c_desired, &ppf->color_encoding); // Convert the extra blobs ppf->metadata.exif = io.blobs.exif; @@ -236,8 +251,7 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, // Convert the pixels ppf->frames.clear(); for (const auto& frame : io.frames) { - size_t frame_bits_per_sample = frame.metadata()->bit_depth.bits_per_sample; - JXL_ASSERT(frame_bits_per_sample != 0); + JXL_ASSERT(frame.metadata()->bit_depth.bits_per_sample != 0); // It is ok for the frame.color().kNumPlanes to not match the // number of channels on the image. const uint32_t num_channels = @@ -249,11 +263,9 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, PackedFrame packed_frame(frame.oriented_xsize(), frame.oriented_ysize(), format); - packed_frame.color.bitdepth_from_format = float_out; const size_t bits_per_sample = - packed_frame.color.bitdepth_from_format - ? packed_frame.color.BitsPerChannel(pixel_format.data_type) - : ppf->info.bits_per_sample; + float_out ? packed_frame.color.BitsPerChannel(pixel_format.data_type) + : ppf->info.bits_per_sample; packed_frame.name = frame.name; packed_frame.frame_info.name_length = frame.name.size(); // Color transform diff --git a/media/libjxl/src/lib/extras/render_hdr.cc b/media/libjxl/src/lib/extras/render_hdr.cc new file mode 100644 index 0000000000..b247699cd3 --- /dev/null +++ b/media/libjxl/src/lib/extras/render_hdr.cc @@ -0,0 +1,60 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/render_hdr.h" + +#include "lib/extras/hlg.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/enc_color_management.h" + +namespace jxl { + +Status RenderHDR(CodecInOut* io, float display_nits, ThreadPool* pool) { + const ColorEncoding& original_color_encoding = io->metadata.m.color_encoding; + if (!(original_color_encoding.tf.IsPQ() || + original_color_encoding.tf.IsHLG())) { + // Nothing to do. + return true; + } + + if (original_color_encoding.tf.IsPQ()) { + JXL_RETURN_IF_ERROR(ToneMapTo({0, display_nits}, io, pool)); + JXL_RETURN_IF_ERROR(GamutMap(io, /*preserve_saturation=*/0.1, pool)); + } else { + const float intensity_target = io->metadata.m.IntensityTarget(); + const float gamma_hlg_to_display = GetHlgGamma(display_nits); + // If the image is already in display space, we need to account for the + // already-applied OOTF. + const float gamma_display_to_display = + gamma_hlg_to_display / GetHlgGamma(intensity_target); + // Ensures that conversions to linear in HlgOOTF below will not themselves + // include the OOTF. + io->metadata.m.SetIntensityTarget(300); + + bool need_gamut_mapping = false; + for (ImageBundle& ib : io->frames) { + const float gamma = ib.c_current().tf.IsHLG() ? gamma_hlg_to_display + : gamma_display_to_display; + if (gamma < 1) need_gamut_mapping = true; + JXL_RETURN_IF_ERROR(HlgOOTF(&ib, gamma, pool)); + } + io->metadata.m.SetIntensityTarget(display_nits); + + if (need_gamut_mapping) { + JXL_RETURN_IF_ERROR(GamutMap(io, /*preserve_saturation=*/0.1, pool)); + } + } + + ColorEncoding rec2020_pq; + rec2020_pq.SetColorSpace(ColorSpace::kRGB); + rec2020_pq.white_point = WhitePoint::kD65; + rec2020_pq.primaries = Primaries::k2100; + rec2020_pq.tf.SetTransferFunction(TransferFunction::kPQ); + JXL_RETURN_IF_ERROR(rec2020_pq.CreateICC()); + io->metadata.m.color_encoding = rec2020_pq; + return io->TransformTo(rec2020_pq, GetJxlCms(), pool); +} + +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/render_hdr.h b/media/libjxl/src/lib/extras/render_hdr.h new file mode 100644 index 0000000000..95127e074b --- /dev/null +++ b/media/libjxl/src/lib/extras/render_hdr.h @@ -0,0 +1,27 @@ +// Copyright (c) the JPEG XL Project 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 LIB_EXTRAS_RENDER_HDR_H_ +#define LIB_EXTRAS_RENDER_HDR_H_ + +#include "lib/jxl/codec_in_out.h" + +namespace jxl { + +// If `io` has an original color space using PQ or HLG, this renders it +// appropriately for a display with a peak luminance of `display_nits` and +// converts the result to a Rec. 2020 / PQ image. Otherwise, leaves the image as +// is. +// PQ images are tone-mapped using the method described in Rep. ITU-R BT.2408-5 +// annex 5, while HLG images are rendered using the HLG OOTF with a gamma +// appropriate for the given target luminance. +// With a sufficiently bright SDR display, converting the output of this +// function to an SDR colorspace may look decent. +Status RenderHDR(CodecInOut* io, float display_nits, + ThreadPool* pool = nullptr); + +} // namespace jxl + +#endif // LIB_EXTRAS_RENDER_HDR_H_ diff --git a/media/libjxl/src/lib/extras/tone_mapping.cc b/media/libjxl/src/lib/extras/tone_mapping.cc index ac4306f8ed..1ed1b29119 100644 --- a/media/libjxl/src/lib/extras/tone_mapping.cc +++ b/media/libjxl/src/lib/extras/tone_mapping.cc @@ -10,13 +10,15 @@ #include <hwy/foreach_target.h> #include <hwy/highway.h> +#include "lib/jxl/dec_tone_mapping-inl.h" #include "lib/jxl/enc_color_management.h" -#include "lib/jxl/transfer_functions-inl.h" HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +static constexpr float rec2020_luminances[3] = {0.2627f, 0.6780f, 0.0593f}; + Status ToneMapFrame(const std::pair<float, float> display_nits, ImageBundle* const ib, ThreadPool* const pool) { // Perform tone mapping as described in Report ITU-R BT.2390-8, section 5.4 @@ -34,42 +36,12 @@ Status ToneMapFrame(const std::pair<float, float> display_nits, JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC()); JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool)); - const auto eotf_inv = [&df](const V luminance) -> V { - return TF_PQ().EncodedFromDisplay(df, luminance * Set(df, 1. / 10000)); - }; - - const V pq_mastering_min = - eotf_inv(Set(df, ib->metadata()->tone_mapping.min_nits)); - const V pq_mastering_max = - eotf_inv(Set(df, ib->metadata()->tone_mapping.intensity_target)); - const V pq_mastering_range = pq_mastering_max - pq_mastering_min; - const V inv_pq_mastering_range = - Set(df, 1) / (pq_mastering_max - pq_mastering_min); - const V min_lum = (eotf_inv(Set(df, display_nits.first)) - pq_mastering_min) * - inv_pq_mastering_range; - const V max_lum = - (eotf_inv(Set(df, display_nits.second)) - pq_mastering_min) * - inv_pq_mastering_range; - const V ks = MulAdd(Set(df, 1.5f), max_lum, Set(df, -0.5f)); - const V b = min_lum; - - const V inv_one_minus_ks = Set(df, 1) / Max(Set(df, 1e-6f), Set(df, 1) - ks); - const auto T = [ks, inv_one_minus_ks](const V a) { - return (a - ks) * inv_one_minus_ks; - }; - const auto P = [&T, &df, ks, max_lum](const V b) { - const V t_b = T(b); - const V t_b_2 = t_b * t_b; - const V t_b_3 = t_b_2 * t_b; - return MulAdd( - MulAdd(Set(df, 2), t_b_3, MulAdd(Set(df, -3), t_b_2, Set(df, 1))), ks, - MulAdd(t_b_3 + MulAdd(Set(df, -2), t_b_2, t_b), Set(df, 1) - ks, - MulAdd(Set(df, -2), t_b_3, Set(df, 3) * t_b_2) * max_lum)); - }; - - const V inv_max_display_nits = Set(df, 1 / display_nits.second); + Rec2408ToneMapper<decltype(df)> tone_mapper( + {ib->metadata()->tone_mapping.min_nits, + ib->metadata()->IntensityTarget()}, + display_nits, rec2020_luminances); - JXL_RETURN_IF_ERROR(RunOnPool( + return RunOnPool( pool, 0, ib->ysize(), ThreadPool::NoInit, [&](const uint32_t y, size_t /* thread */) { float* const JXL_RESTRICT row_r = ib->color()->PlaneRow(0, y); @@ -79,43 +51,13 @@ Status ToneMapFrame(const std::pair<float, float> display_nits, V red = Load(df, row_r + x); V green = Load(df, row_g + x); V blue = Load(df, row_b + x); - const V luminance = Set(df, ib->metadata()->IntensityTarget()) * - (MulAdd(Set(df, 0.2627f), red, - MulAdd(Set(df, 0.6780f), green, - Set(df, 0.0593f) * blue))); - const V normalized_pq = - Min(Set(df, 1.f), (eotf_inv(luminance) - pq_mastering_min) * - inv_pq_mastering_range); - const V e2 = - IfThenElse(normalized_pq < ks, normalized_pq, P(normalized_pq)); - const V one_minus_e2 = Set(df, 1) - e2; - const V one_minus_e2_2 = one_minus_e2 * one_minus_e2; - const V one_minus_e2_4 = one_minus_e2_2 * one_minus_e2_2; - const V e3 = MulAdd(b, one_minus_e2_4, e2); - const V e4 = MulAdd(e3, pq_mastering_range, pq_mastering_min); - const V new_luminance = - Min(Set(df, display_nits.second), - ZeroIfNegative(Set(df, 10000) * - TF_PQ().DisplayFromEncoded(df, e4))); - - const V ratio = new_luminance / luminance; - const V normalizer = - Set(df, ib->metadata()->IntensityTarget()) * inv_max_display_nits; - - for (V* const val : {&red, &green, &blue}) { - *val = IfThenElse(luminance <= Set(df, 1e-6f), new_luminance, - *val * ratio) * - normalizer; - } - + tone_mapper.ToneMap(&red, &green, &blue); Store(red, df, row_r + x); Store(green, df, row_g + x); Store(blue, df, row_b + x); } }, - "ToneMap")); - - return true; + "ToneMap"); } Status GamutMapFrame(ImageBundle* const ib, float preserve_saturation, @@ -141,44 +83,8 @@ Status GamutMapFrame(ImageBundle* const ib, float preserve_saturation, V red = Load(df, row_r + x); V green = Load(df, row_g + x); V blue = Load(df, row_b + x); - const V luminance = - MulAdd(Set(df, 0.2627f), red, - MulAdd(Set(df, 0.6780f), green, Set(df, 0.0593f) * blue)); - - // Desaturate out-of-gamut pixels. This is done by mixing each pixel - // with just enough gray of the target luminance to make all - // components non-negative. - // - For saturation preservation, if a component is still larger than - // 1 then the pixel is normalized to have a maximum component of 1. - // That will reduce its luminance. - // - For luminance preservation, getting all components below 1 is - // done by mixing in yet more gray. That will desaturate it further. - V gray_mix_saturation = Zero(df); - V gray_mix_luminance = Zero(df); - for (const V val : {red, green, blue}) { - const V inv_val_minus_gray = Set(df, 1) / (val - luminance); - gray_mix_saturation = - IfThenElse(val >= luminance, gray_mix_saturation, - Max(gray_mix_saturation, val * inv_val_minus_gray)); - gray_mix_luminance = - Max(gray_mix_luminance, - IfThenElse(val <= luminance, gray_mix_saturation, - (val - Set(df, 1)) * inv_val_minus_gray)); - } - const V gray_mix = - Clamp(Set(df, preserve_saturation) * - (gray_mix_saturation - gray_mix_luminance) + - gray_mix_luminance, - Zero(df), Set(df, 1)); - for (V* const val : {&red, &green, &blue}) { - *val = MulAdd(gray_mix, luminance - *val, *val); - } - const V normalizer = - Set(df, 1) / Max(Set(df, 1), Max(red, Max(green, blue))); - for (V* const val : {&red, &green, &blue}) { - *val = *val * normalizer; - } - + GamutMap(&red, &green, &blue, rec2020_luminances, + preserve_saturation); Store(red, df, row_r + x); Store(green, df, row_g + x); Store(blue, df, row_b + x); diff --git a/media/libjxl/src/lib/extras/tone_mapping_gbench.cc b/media/libjxl/src/lib/extras/tone_mapping_gbench.cc index b156992813..2f97b88667 100644 --- a/media/libjxl/src/lib/extras/tone_mapping_gbench.cc +++ b/media/libjxl/src/lib/extras/tone_mapping_gbench.cc @@ -13,8 +13,7 @@ namespace jxl { static void BM_ToneMapping(benchmark::State& state) { CodecInOut image; - const PaddedBytes image_bytes = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes image_bytes = ReadTestData("jxl/flower/flower.png"); JXL_CHECK(SetFromBytes(Span<const uint8_t>(image_bytes), &image)); // Convert to linear Rec. 2020 so that `ToneMapTo` doesn't have to and we diff --git a/media/libjxl/src/lib/include/jxl/cms_interface.h b/media/libjxl/src/lib/include/jxl/cms_interface.h index 50ee406865..fb852eeb1f 100644 --- a/media/libjxl/src/lib/include/jxl/cms_interface.h +++ b/media/libjxl/src/lib/include/jxl/cms_interface.h @@ -19,7 +19,7 @@ #define JXL_CMS_INTERFACE_H_ #include "jxl/color_encoding.h" -#include "jxl/memory_manager.h" +#include "jxl/types.h" #if defined(__cplusplus) || defined(c_plusplus) extern "C" { diff --git a/media/libjxl/src/lib/include/jxl/codestream_header.h b/media/libjxl/src/lib/include/jxl/codestream_header.h index 536b5a3aa6..d126577870 100644 --- a/media/libjxl/src/lib/include/jxl/codestream_header.h +++ b/media/libjxl/src/lib/include/jxl/codestream_header.h @@ -18,7 +18,6 @@ #include <stddef.h> #include <stdint.h> -#include "jxl/color_encoding.h" #include "jxl/types.h" #if defined(__cplusplus) || defined(c_plusplus) @@ -175,10 +174,14 @@ typedef struct { * (linear if outputting to floating point, nonlinear with standard sRGB * transfer function if outputting to unsigned integers) but will not convert * it to to the original color profile. The decoder also does not convert to - * the target display color profile, but instead will always indicate which - * color profile the returned pixel data is encoded in when using @see - * JXL_COLOR_PROFILE_TARGET_DATA so that a CMS can be used to convert the - * data. + * the target display color profile. To convert the pixel data produced by + * the decoder to the original color profile, one of the JxlDecoderGetColor* + * functions needs to be called with @ref JXL_COLOR_PROFILE_TARGET_DATA to get + * the color profile of the decoder output, and then an external CMS can be + * used for conversion. + * Note that for lossy compression, this should be set to false for most use + * cases, and if needed, the image should be converted to the original color + * profile after decoding, as described above. */ JXL_BOOL uses_original_profile; diff --git a/media/libjxl/src/lib/include/jxl/color_encoding.h b/media/libjxl/src/lib/include/jxl/color_encoding.h index 2815c53bb5..b16f6a01ee 100644 --- a/media/libjxl/src/lib/include/jxl/color_encoding.h +++ b/media/libjxl/src/lib/include/jxl/color_encoding.h @@ -16,8 +16,6 @@ #include <stdint.h> -#include "jxl/types.h" - #if defined(__cplusplus) || defined(c_plusplus) extern "C" { #endif @@ -90,7 +88,7 @@ typedef enum { JXL_TRANSFER_FUNCTION_LINEAR = 8, /** As specified in IEC 61966-2-1 sRGB */ JXL_TRANSFER_FUNCTION_SRGB = 13, - /** As specified in SMPTE ST 428-1 */ + /** As specified in SMPTE ST 2084 */ JXL_TRANSFER_FUNCTION_PQ = 16, /** As specified in SMPTE ST 428-1 */ JXL_TRANSFER_FUNCTION_DCI = 17, diff --git a/media/libjxl/src/lib/include/jxl/decode.h b/media/libjxl/src/lib/include/jxl/decode.h index 9b9d2f0e30..66820bfcd9 100644 --- a/media/libjxl/src/lib/include/jxl/decode.h +++ b/media/libjxl/src/lib/include/jxl/decode.h @@ -16,13 +16,13 @@ #include <stddef.h> #include <stdint.h> -#include "jxl/cms_interface.h" #include "jxl/codestream_header.h" #include "jxl/color_encoding.h" #include "jxl/jxl_export.h" #include "jxl/memory_manager.h" #include "jxl/parallel_runner.h" #include "jxl/types.h" +#include "jxl/version.h" #if defined(__cplusplus) || defined(c_plusplus) extern "C" { @@ -37,14 +37,14 @@ extern "C" { */ JXL_EXPORT uint32_t JxlDecoderVersion(void); -/** The result of JxlSignatureCheck. +/** The result of @ref JxlSignatureCheck. */ typedef enum { /** Not enough bytes were passed to determine if a valid signature was found. */ JXL_SIG_NOT_ENOUGH_BYTES = 0, - /** No valid JPEGXL header was found. */ + /** No valid JPEG XL header was found. */ JXL_SIG_INVALID = 1, /** A valid JPEG XL codestream signature was found, that is a JPEG XL image @@ -66,61 +66,72 @@ typedef enum { * @p size doesn't need to be a full image, only the beginning of the file. * * @return a flag indicating if a JPEG XL signature was found and what type. - * - JXL_SIG_NOT_ENOUGH_BYTES not enough bytes were passed to determine - * if a valid signature is there. - * - JXL_SIG_INVALID: no valid signature found for JPEG XL decoding. - * - JXL_SIG_CODESTREAM a valid JPEG XL codestream signature was found. - * - JXL_SIG_CONTAINER a valid JPEG XL container signature was found. + * - @ref JXL_SIG_NOT_ENOUGH_BYTES if not enough bytes were passed to + * determine if a valid signature is there. + * - @ref JXL_SIG_INVALID if no valid signature found for JPEG XL decoding. + * - @ref JXL_SIG_CODESTREAM if a valid JPEG XL codestream signature was + * found. + * - @ref JXL_SIG_CONTAINER if a valid JPEG XL container signature was found. */ JXL_EXPORT JxlSignature JxlSignatureCheck(const uint8_t* buf, size_t len); /** - * Opaque structure that holds the JPEGXL decoder. + * Opaque structure that holds the JPEG XL decoder. * - * Allocated and initialized with JxlDecoderCreate(). - * Cleaned up and deallocated with JxlDecoderDestroy(). + * Allocated and initialized with @ref JxlDecoderCreate(). + * Cleaned up and deallocated with @ref JxlDecoderDestroy(). */ typedef struct JxlDecoderStruct JxlDecoder; /** - * Creates an instance of JxlDecoder and initializes it. + * Creates an instance of @ref JxlDecoder and initializes it. * * @p memory_manager will be used for all the library dynamic allocations made * from this instance. The parameter may be NULL, in which case the default - * allocator will be used. See jpegxl/memory_manager.h for details. + * allocator will be used. See jxl/memory_manager.h for details. * * @param memory_manager custom allocator function. It may be NULL. The memory * manager will be copied internally. * @return @c NULL if the instance can not be allocated or initialized - * @return pointer to initialized JxlDecoder otherwise + * @return pointer to initialized @ref JxlDecoder otherwise */ JXL_EXPORT JxlDecoder* JxlDecoderCreate(const JxlMemoryManager* memory_manager); /** - * Re-initializes a JxlDecoder instance, so it can be re-used for decoding + * Re-initializes a @ref JxlDecoder instance, so it can be re-used for decoding * another image. All state and settings are reset as if the object was - * newly created with JxlDecoderCreate, but the memory manager is kept. + * newly created with @ref JxlDecoderCreate, but the memory manager is kept. * * @param dec instance to be re-initialized. */ JXL_EXPORT void JxlDecoderReset(JxlDecoder* dec); /** - * Deinitializes and frees JxlDecoder instance. + * Deinitializes and frees @ref JxlDecoder instance. * * @param dec instance to be cleaned up and deallocated. */ JXL_EXPORT void JxlDecoderDestroy(JxlDecoder* dec); /** - * Return value for JxlDecoderProcessInput. - * The values from JXL_DEC_BASIC_INFO onwards are optional informative + * Return value for @ref JxlDecoderProcessInput. + * The values from @ref JXL_DEC_BASIC_INFO onwards are optional informative * events that can be subscribed to, they are never returned if they - * have not been registered with JxlDecoderSubscribeEvents. + * have not been registered with @ref JxlDecoderSubscribeEvents. */ typedef enum { /** Function call finished successfully, or decoding is finished and there is * nothing more to be done. + * + * Note that @ref JxlDecoderProcessInput will return JXL_DEC_SUCCESS if all + * events that were registered with @ref JxlDecoderSubscribeEvents were + * processed, even before the end of the JPEG XL codestream. + * + * In this case, the return value @ref JxlDecoderReleaseInput will be the same + * as it was at the last signaled event. E.g. if JXL_DEC_FULL_IMAGE was + * subscribed to, then all bytes from the end of the JPEG XL codestream + * (including possible boxes needed for jpeg reconstruction) will be returned + * as unprocessed. */ JXL_DEC_SUCCESS = 0, @@ -129,219 +140,296 @@ typedef enum { */ JXL_DEC_ERROR = 1, - /** The decoder needs more input bytes to continue. Before the next - * JxlDecoderProcessInput call, more input data must be set, by calling - * JxlDecoderReleaseInput (if input was set previously) and then calling - * JxlDecoderSetInput. JxlDecoderReleaseInput returns how many bytes are - * not yet processed, before a next call to JxlDecoderProcessInput all - * unprocessed bytes must be provided again (the address need not match, but - * the contents must), and more bytes must be concatenated after the + /** The decoder needs more input bytes to continue. Before the next @ref + * JxlDecoderProcessInput call, more input data must be set, by calling @ref + * JxlDecoderReleaseInput (if input was set previously) and then calling @ref + * JxlDecoderSetInput. @ref JxlDecoderReleaseInput returns how many bytes + * are not yet processed, before a next call to @ref JxlDecoderProcessInput + * all unprocessed bytes must be provided again (the address need not match, + * but the contents must), and more bytes must be concatenated after the * unprocessed bytes. + * In most cases, @ref JxlDecoderReleaseInput will return no unprocessed bytes + * at this event, the only exceptions are if the previously set input ended + * within (a) the raw codestream signature, (b) the signature box, (c) a box + * header, or (d) the first 4 bytes of a brob, ftyp, or jxlp box. In any of + * these cases the number of unprocessed bytes is less than 20. */ JXL_DEC_NEED_MORE_INPUT = 2, /** The decoder is able to decode a preview image and requests setting a - * preview output buffer using JxlDecoderSetPreviewOutBuffer. This occurs if - * JXL_DEC_PREVIEW_IMAGE is requested and it is possible to decode a preview - * image from the codestream and the preview out buffer was not yet set. There - * is maximum one preview image in a codestream. + * preview output buffer using @ref JxlDecoderSetPreviewOutBuffer. This occurs + * if @ref JXL_DEC_PREVIEW_IMAGE is requested and it is possible to decode a + * preview image from the codestream and the preview out buffer was not yet + * set. There is maximum one preview image in a codestream. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the frame header (including ToC) of the preview frame as + * unprocessed. */ JXL_DEC_NEED_PREVIEW_OUT_BUFFER = 3, /** The decoder is able to decode a DC image and requests setting a DC output - * buffer using JxlDecoderSetDCOutBuffer. This occurs if JXL_DEC_DC_IMAGE is - * requested and it is possible to decode a DC image from the codestream and - * the DC out buffer was not yet set. This event re-occurs for new frames - * if there are multiple animation frames. - * DEPRECATED: the DC feature in this form will be removed. For progressive - * rendering, JxlDecoderFlushImage should be used. + * buffer using @ref JxlDecoderSetDCOutBuffer. This occurs if @ref + * JXL_DEC_DC_IMAGE is requested and it is possible to decode a DC image from + * the codestream and the DC out buffer was not yet set. This event re-occurs + * for new frames if there are multiple animation frames. + * @deprecated The DC feature in this form will be removed. For progressive + * rendering, @ref JxlDecoderFlushImage should be used. */ JXL_DEC_NEED_DC_OUT_BUFFER = 4, /** The decoder requests an output buffer to store the full resolution image, - * which can be set with JxlDecoderSetImageOutBuffer or with - * JxlDecoderSetImageOutCallback. This event re-occurs for new frames if there - * are multiple animation frames and requires setting an output again. + * which can be set with @ref JxlDecoderSetImageOutBuffer or with @ref + * JxlDecoderSetImageOutCallback. This event re-occurs for new frames if + * there are multiple animation frames and requires setting an output again. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the frame header (including ToC) as unprocessed. */ JXL_DEC_NEED_IMAGE_OUT_BUFFER = 5, /** The JPEG reconstruction buffer is too small for reconstructed JPEG - * codestream to fit. JxlDecoderSetJPEGBuffer must be called again to make - * room for remaining bytes. This event may occur multiple times after - * JXL_DEC_JPEG_RECONSTRUCTION. + * codestream to fit. @ref JxlDecoderSetJPEGBuffer must be called again to + * make room for remaining bytes. This event may occur multiple times + * after @ref JXL_DEC_JPEG_RECONSTRUCTION. */ JXL_DEC_JPEG_NEED_MORE_OUTPUT = 6, - /** The box contents output buffer is too small. JxlDecoderSetBoxBuffer must - * be called again to make room for remaining bytes. This event may occur - * multiple times after JXL_DEC_BOX. + /** The box contents output buffer is too small. @ref JxlDecoderSetBoxBuffer + * must be called again to make room for remaining bytes. This event may occur + * multiple times after @ref JXL_DEC_BOX. */ JXL_DEC_BOX_NEED_MORE_OUTPUT = 7, - /** Informative event by JxlDecoderProcessInput: basic information such as - * image dimensions and extra channels. This event occurs max once per image. + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": Basic information such as image dimensions and + * extra channels. This event occurs max once per image. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the basic info as unprocessed (including the last byte of basic info + * if it did not end on a byte boundary). */ JXL_DEC_BASIC_INFO = 0x40, - /** Informative event by JxlDecoderProcessInput: user extensions of the - * codestream header. This event occurs max once per image and always later - * than JXL_DEC_BASIC_INFO and earlier than any pixel data. + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": User extensions of the codestream header. This + * event occurs max once per image and always later than @ref + * JXL_DEC_BASIC_INFO and earlier than any pixel data. + * + * @deprecated The decoder no longer returns this, the header extensions, + * if any, are available at the JXL_DEC_BASIC_INFO event. */ JXL_DEC_EXTENSIONS = 0x80, - /** Informative event by JxlDecoderProcessInput: color encoding or ICC - * profile from the codestream header. This event occurs max once per image - * and always later than JXL_DEC_BASIC_INFO and earlier than any pixel - * data. + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": Color encoding or ICC profile from the + * codestream header. This event occurs max once per image and always later + * than @ref JXL_DEC_BASIC_INFO and earlier than any pixel data. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the image header (which is the start of the first frame) as + * unprocessed. */ JXL_DEC_COLOR_ENCODING = 0x100, - /** Informative event by JxlDecoderProcessInput: Preview image, a small - * frame, decoded. This event can only happen if the image has a preview - * frame encoded. This event occurs max once for the codestream and always - * later than JXL_DEC_COLOR_ENCODING and before JXL_DEC_FRAME. + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": Preview image, a small frame, decoded. This + * event can only happen if the image has a preview frame encoded. This event + * occurs max once for the codestream and always later than @ref + * JXL_DEC_COLOR_ENCODING and before @ref JXL_DEC_FRAME. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the preview frame as unprocessed. */ JXL_DEC_PREVIEW_IMAGE = 0x200, - /** Informative event by JxlDecoderProcessInput: Beginning of a frame. + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": Beginning of a frame. @ref * JxlDecoderGetFrameHeader can be used at this point. A note on frames: * a JPEG XL image can have internal frames that are not intended to be * displayed (e.g. used for compositing a final frame), but this only returns - * displayed frames, unless JxlDecoderSetCoalescing was set to JXL_FALSE: in - * that case, the individual layers are returned, without blending. Note that - * even when coalescing is disabled, only frames of type kRegularFrame are - * returned; frames of type kReferenceOnly and kLfFrame are always for + * displayed frames, unless @ref JxlDecoderSetCoalescing was set to JXL_FALSE: + * in that case, the individual layers are returned, without blending. Note + * that even when coalescing is disabled, only frames of type kRegularFrame + * are returned; frames of type kReferenceOnly and kLfFrame are always for * internal purposes only and cannot be accessed. A displayed frame either has * an animation duration or is the only or last frame in the image. This event - * occurs max once per displayed frame, always later than - * JXL_DEC_COLOR_ENCODING, and always earlier than any pixel data. While JPEG - * XL supports encoding a single frame as the composition of multiple internal - * sub-frames also called frames, this event is not indicated for the internal - * frames. + * occurs max once per displayed frame, always later than @ref + * JXL_DEC_COLOR_ENCODING, and always earlier than any pixel data. While + * JPEG XL supports encoding a single frame as the composition of multiple + * internal sub-frames also called frames, this event is not indicated for the + * internal frames. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the frame header (including ToC) as unprocessed. */ JXL_DEC_FRAME = 0x400, - /** Informative event by JxlDecoderProcessInput: DC image, 8x8 sub-sampled - * frame, decoded. It is not guaranteed that the decoder will always return DC - * separately, but when it does it will do so before outputting the full - * frame. JxlDecoderSetDCOutBuffer must be used after getting the basic - * image information to be able to get the DC pixels, if not this return - * status only indicates we're past this point in the codestream. This event - * occurs max once per frame and always later than JXL_DEC_FRAME_HEADER - * and other header events and earlier than full resolution pixel data. - * DEPRECATED: the DC feature in this form will be removed. For progressive - * rendering, JxlDecoderFlushImage should be used. + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": DC image, 8x8 sub-sampled frame, decoded. It is + * not guaranteed that the decoder will always return DC separately, but when + * it does it will do so before outputting the full frame. @ref + * JxlDecoderSetDCOutBuffer must be used after getting the basic image + * information to be able to get the DC pixels, if not this return status only + * indicates we're past this point in the codestream. This event occurs max + * once per frame and always later than @ref JXL_DEC_FRAME and other header + * events and earlier than full resolution pixel data. + * + * @deprecated The DC feature in this form will be removed. For progressive + * rendering, @ref JxlDecoderFlushImage should be used. */ JXL_DEC_DC_IMAGE = 0x800, - /** Informative event by JxlDecoderProcessInput: full frame (or layer, in case - * coalescing is disabled) is decoded. JxlDecoderSetImageOutBuffer must be - * used after getting the basic image information to be able to get the image - * pixels, if not this return status only indicates we're past this point in - * the codestream. This event occurs max once per frame and always later than + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": full frame (or layer, in case coalescing is + * disabled) is decoded. @ref JxlDecoderSetImageOutBuffer must be used after + * getting the basic image information to be able to get the image pixels, if + * not this return status only indicates we're past this point in the + * codestream. This event occurs max once per frame and always later than @ref * JXL_DEC_DC_IMAGE. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the frame (or if @ref JXL_DEC_JPEG_RECONSTRUCTION is subscribed to, + * from the end of the last box that is needed for jpeg reconstruction) as + * unprocessed. */ JXL_DEC_FULL_IMAGE = 0x1000, - /** Informative event by JxlDecoderProcessInput: JPEG reconstruction data - * decoded. JxlDecoderSetJPEGBuffer may be used to set a JPEG - * reconstruction buffer after getting the JPEG reconstruction data. If a JPEG - * reconstruction buffer is set a byte stream identical to the JPEG codestream - * used to encode the image will be written to the JPEG reconstruction buffer - * instead of pixels to the image out buffer. This event occurs max once per - * image and always before JXL_DEC_FULL_IMAGE. + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": JPEG reconstruction data decoded. @ref + * JxlDecoderSetJPEGBuffer may be used to set a JPEG reconstruction buffer + * after getting the JPEG reconstruction data. If a JPEG reconstruction buffer + * is set a byte stream identical to the JPEG codestream used to encode the + * image will be written to the JPEG reconstruction buffer instead of pixels + * to the image out buffer. This event occurs max once per image and always + * before @ref JXL_DEC_FULL_IMAGE. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the 'jbrd' box as unprocessed. */ JXL_DEC_JPEG_RECONSTRUCTION = 0x2000, - /** Informative event by JxlDecoderProcessInput: The header of a box of the - * container format (BMFF) is decoded. The following API functions related to - * boxes can be used after this event: - * @see JxlDecoderSetBoxBuffer and JxlDecoderReleaseBoxBuffer: set and release - * a buffer to get the box data. - * @see JxlDecoderGetBoxType get the 4-character box typename. - * @see JxlDecoderGetBoxSizeRaw get the size of the box as it appears in the - * container file, not decompressed. - * @see JxlDecoderSetDecompressBoxes to configure whether to get the box - * data decompressed, or possibly compressed. + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": The header of a box of the container format + * (BMFF) is decoded. The following API functions related to boxes can be used + * after this event: + * - @ref JxlDecoderSetBoxBuffer and @ref JxlDecoderReleaseBoxBuffer + * "JxlDecoderReleaseBoxBuffer": set and release a buffer to get the box + * data. + * - @ref JxlDecoderGetBoxType get the 4-character box typename. + * - @ref JxlDecoderGetBoxSizeRaw get the size of the box as it appears in + * the container file, not decompressed. + * - @ref JxlDecoderSetDecompressBoxes to configure whether to get the box + * data decompressed, or possibly compressed. * * Boxes can be compressed. This is so when their box type is * "brob". In that case, they have an underlying decompressed box - * type and decompressed data. JxlDecoderSetDecompressBoxes allows + * type and decompressed data. @ref JxlDecoderSetDecompressBoxes allows * configuring which data to get. Decompressing requires - * Brotli. JxlDecoderGetBoxType has a flag to get the compressed box + * Brotli. @ref JxlDecoderGetBoxType has a flag to get the compressed box * type, which can be "brob", or the decompressed box type. If a box * is not compressed (its compressed type is not "brob"), then * the output decompressed box type and data is independent of what * setting is configured. * - * The buffer set with JxlDecoderSetBoxBuffer must be set again for each next - * box to be obtained, or can be left unset to skip outputting this box. - * The output buffer contains the full box data when the next JXL_DEC_BOX - * event or JXL_DEC_SUCCESS occurs. JXL_DEC_BOX occurs for all boxes, - * including non-metadata boxes such as the signature box or codestream boxes. - * To check whether the box is a metadata type for respectively EXIF, XMP or - * JUMBF, use JxlDecoderGetBoxType and check for types "Exif", "xml " and - * "jumb" respectively. + * The buffer set with @ref JxlDecoderSetBoxBuffer must be set again for each + * next box to be obtained, or can be left unset to skip outputting this box. + * The output buffer contains the full box data when the next @ref JXL_DEC_BOX + * event or @ref JXL_DEC_SUCCESS occurs. @ref JXL_DEC_BOX occurs for all + * boxes, including non-metadata boxes such as the signature box or codestream + * boxes. To check whether the box is a metadata type for respectively EXIF, + * XMP or JUMBF, use @ref JxlDecoderGetBoxType and check for types "Exif", + * "xml " and "jumb" respectively. + * + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * start of the box header as unprocessed. */ JXL_DEC_BOX = 0x4000, - /** Informative event by JxlDecoderProcessInput: a progressive step in - * decoding the frame is reached. When calling @ref JxlDecoderFlushImage at - * this point, the flushed image will correspond exactly to this point in - * decoding, and not yet contain partial results (such as partially more fine - * detail) of a next step. By default, this event will trigger maximum once - * per frame, when a 8x8th resolution (DC) image is ready (the image data is - * still returned at full resolution, giving upscaled DC). Use @ref + /** Informative event by @ref JxlDecoderProcessInput + * "JxlDecoderProcessInput": a progressive step in decoding the frame is + * reached. When calling @ref JxlDecoderFlushImage at this point, the flushed + * image will correspond exactly to this point in decoding, and not yet + * contain partial results (such as partially more fine detail) of a next + * step. By default, this event will trigger maximum once per frame, when a + * 8x8th resolution (DC) image is ready (the image data is still returned at + * full resolution, giving upscaled DC). Use @ref * JxlDecoderSetProgressiveDetail to configure more fine-grainedness. The * event is not guaranteed to trigger, not all images have progressive steps * or DC encoded. + * In this case, @ref JxlDecoderReleaseInput will return all bytes from the + * end of the section that was needed to produce this progressive event as + * unprocessed. */ JXL_DEC_FRAME_PROGRESSION = 0x8000, } JxlDecoderStatus; /** Rewinds decoder to the beginning. The same input must be given again from * the beginning of the file and the decoder will emit events from the beginning - * again. When rewinding (as opposed to JxlDecoderReset), the decoder can keep - * state about the image, which it can use to skip to a requested frame more - * efficiently with JxlDecoderSkipFrames. Settings such as parallel runner or - * subscribed events are kept. After rewind, JxlDecoderSubscribeEvents can be - * used again, and it is feasible to leave out events that were already handled - * before, such as JXL_DEC_BASIC_INFO and JXL_DEC_COLOR_ENCODING, since they - * will provide the same information as before. + * again. When rewinding (as opposed to @ref JxlDecoderReset), the decoder can + * keep state about the image, which it can use to skip to a requested frame + * more efficiently with @ref JxlDecoderSkipFrames. Settings such as parallel + * runner or subscribed events are kept. After rewind, @ref + * JxlDecoderSubscribeEvents can be used again, and it is feasible to leave out + * events that were already handled before, such as @ref JXL_DEC_BASIC_INFO + * and @ref JXL_DEC_COLOR_ENCODING, since they will provide the same information + * as before. + * The difference to @ref JxlDecoderReset is that some state is kept, namely + * settings set by a call to + * - @ref JxlDecoderSetCoalescing, + * - @ref JxlDecoderSetDesiredIntensityTarget, + * - @ref JxlDecoderSetDecompressBoxes, + * - @ref JxlDecoderSetKeepOrientation, + * - @ref JxlDecoderSetUnpremultiplyAlpha, + * - @ref JxlDecoderSetParallelRunner, + * - @ref JxlDecoderSetRenderSpotcolors, and + * - @ref JxlDecoderSubscribeEvents. + * * @param dec decoder object */ JXL_EXPORT void JxlDecoderRewind(JxlDecoder* dec); /** Makes the decoder skip the next `amount` frames. It still needs to process * the input, but will not output the frame events. It can be more efficient - * when skipping frames, and even more so when using this after + * when skipping frames, and even more so when using this after @ref * JxlDecoderRewind. If the decoder is already processing a frame (could - * have emitted JXL_DEC_FRAME but not yet JXL_DEC_FULL_IMAGE), it starts - * skipping from the next frame. If the amount is larger than the amount of - * frames remaining in the image, all remaining frames are skipped. Calling this - * function multiple times adds the amount to skip to the already existing + * have emitted @ref JXL_DEC_FRAME but not yet @ref JXL_DEC_FULL_IMAGE), it + * starts skipping from the next frame. If the amount is larger than the amount + * of frames remaining in the image, all remaining frames are skipped. Calling + * this function multiple times adds the amount to skip to the already existing * amount. - * A frame here is defined as a frame that without skipping emits events such as - * JXL_DEC_FRAME and JXL_FULL_IMAGE, frames that are internal to the file format - * but are not rendered as part of an animation, or are not the final still - * frame of a still image, are not counted. + * + * A frame here is defined as a frame that without skipping emits events such + * as @ref JXL_DEC_FRAME and @ref JXL_DEC_FULL_IMAGE, frames that are internal + * to the file format but are not rendered as part of an animation, or are not + * the final still frame of a still image, are not counted. + * * @param dec decoder object * @param amount the amount of frames to skip */ JXL_EXPORT void JxlDecoderSkipFrames(JxlDecoder* dec, size_t amount); /** + * Skips processing the current frame. Can be called after frame processing + * already started, signaled by a @ref JXL_DEC_NEED_IMAGE_OUT_BUFFER event, + * but before the corrsponding @ref JXL_DEC_FULL_IMAGE event. The next signaled + * event will be another @ref JXL_DEC_FRAME, or @ref JXL_DEC_SUCCESS if there + * are no more frames. If pixel data is required from the already processed part + * of the frame, @ref JxlDecoderFlushImage must be called before this. + * + * @param dec decoder object + * @return @ref JXL_DEC_SUCCESS if there is a frame to skip, and @ref + * JXL_DEC_ERROR if the function was not called during frame processing. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderSkipCurrentFrame(JxlDecoder* dec); + +/** * Get the default pixel format for this decoder. * * Requires that the decoder can produce JxlBasicInfo. * - * @param dec JxlDecoder to query when creating the recommended pixel format. + * @param dec @ref JxlDecoder to query when creating the recommended pixel + * format. * @param format JxlPixelFormat to populate with the recommended settings for - * the data loaded into this decoder. - * @return JXL_DEC_SUCCESS if no error, JXL_DEC_NEED_MORE_INPUT if the - * basic info isn't yet available, and JXL_DEC_ERROR otherwise. + * the data loaded into this decoder. + * @return @ref JXL_DEC_SUCCESS if no error, @ref JXL_DEC_NEED_MORE_INPUT if the + * basic info isn't yet available, and @ref JXL_DEC_ERROR otherwise. + * + * DEPRECATED: this function will be removed in the future. */ -JXL_EXPORT JxlDecoderStatus +JXL_EXPORT JXL_DEPRECATED JxlDecoderStatus JxlDecoderDefaultPixelFormat(const JxlDecoder* dec, JxlPixelFormat* format); /** @@ -350,11 +438,11 @@ JxlDecoderDefaultPixelFormat(const JxlDecoder* dec, JxlPixelFormat* format); * * @param dec decoder object * @param parallel_runner function pointer to runner for multithreading. It may - * be NULL to use the default, single-threaded, runner. A multithreaded - * runner should be set to reach fast performance. + * be NULL to use the default, single-threaded, runner. A multithreaded + * runner should be set to reach fast performance. * @param parallel_runner_opaque opaque pointer for parallel_runner. - * @return JXL_DEC_SUCCESS if the runner was set, JXL_DEC_ERROR - * otherwise (the previous runner remains set). + * @return @ref JXL_DEC_SUCCESS if the runner was set, @ref JXL_DEC_ERROR + * otherwise (the previous runner remains set). */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetParallelRunner(JxlDecoder* dec, JxlParallelRunner parallel_runner, @@ -362,14 +450,14 @@ JxlDecoderSetParallelRunner(JxlDecoder* dec, JxlParallelRunner parallel_runner, /** * Returns a hint indicating how many more bytes the decoder is expected to - * need to make JxlDecoderGetBasicInfo available after the next + * need to make @ref JxlDecoderGetBasicInfo available after the next @ref * JxlDecoderProcessInput call. This is a suggested large enough value for - * the amount of bytes to provide in the next JxlDecoderSetInput call, but it is - * not guaranteed to be an upper bound nor a lower bound. This number does not - * include bytes that have already been released from the input. - * Can be used before the first JxlDecoderProcessInput call, and is correct - * the first time in most cases. If not, JxlDecoderSizeHintBasicInfo can be - * called again to get an updated hint. + * the amount of bytes to provide in the next @ref JxlDecoderSetInput call, but + * it is not guaranteed to be an upper bound nor a lower bound. This number does + * not include bytes that have already been released from the input. Can be used + * before the first @ref JxlDecoderProcessInput call, and is correct the first + * time in most cases. If not, @ref JxlDecoderSizeHintBasicInfo can be called + * again to get an updated hint. * * @param dec decoder object * @return the size hint in bytes if the basic info is not yet fully decoded. @@ -377,17 +465,17 @@ JxlDecoderSetParallelRunner(JxlDecoder* dec, JxlParallelRunner parallel_runner, */ JXL_EXPORT size_t JxlDecoderSizeHintBasicInfo(const JxlDecoder* dec); -/** Select for which informative events (JXL_DEC_BASIC_INFO, etc...) the +/** Select for which informative events, i.e. @ref JXL_DEC_BASIC_INFO, etc., the * decoder should return with a status. It is not required to subscribe to any * events, data can still be requested from the decoder as soon as it available. * By default, the decoder is subscribed to no events (events_wanted == 0), and * the decoder will then only return when it cannot continue because it needs * more input data or more output buffer. This function may only be be called - * before using JxlDecoderProcessInput + * before using @ref JxlDecoderProcessInput. * * @param dec decoder object * @param events_wanted bitfield of desired events. - * @return JXL_DEC_SUCCESS if no error, JXL_DEC_ERROR otherwise. + * @return @ref JXL_DEC_SUCCESS if no error, @ref JXL_DEC_ERROR otherwise. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSubscribeEvents(JxlDecoder* dec, int events_wanted); @@ -397,45 +485,62 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderSubscribeEvents(JxlDecoder* dec, * indicating that the decoder must perform a rotation and/or * mirroring to the encoded image data. * - * *) If skip_reorientation is JXL_FALSE (the default): the decoder - * will apply the transformation from the orientation setting, hence - * rendering the image according to its specified intent. When - * producing a JxlBasicInfo, the decoder will always set the - * orientation field to JXL_ORIENT_IDENTITY (matching the returned - * pixel data) and also align xsize and ysize so that they correspond - * to the width and the height of the returned pixel data. - * - * *) If skip_reorientation is JXL_TRUE: the decoder will skip - * applying the transformation from the orientation setting, returning - * the image in the as-in-bitstream pixeldata orientation. - * This may be faster to decode since the decoder doesn't have to apply the - * transformation, but can cause wrong display of the image if the orientation - * tag is not correctly taken into account by the user. + * - If skip_reorientation is JXL_FALSE (the default): the decoder + * will apply the transformation from the orientation setting, hence + * rendering the image according to its specified intent. When + * producing a JxlBasicInfo, the decoder will always set the + * orientation field to JXL_ORIENT_IDENTITY (matching the returned + * pixel data) and also align xsize and ysize so that they correspond + * to the width and the height of the returned pixel data. + * - If skip_reorientation is JXL_TRUE: the decoder will skip + * applying the transformation from the orientation setting, returning + * the image in the as-in-bitstream pixeldata orientation. + * This may be faster to decode since the decoder doesn't have to apply the + * transformation, but can cause wrong display of the image if the + * orientation tag is not correctly taken into account by the user. * * By default, this option is disabled, and the returned pixel data is * re-oriented according to the image's Orientation setting. * * This function must be called at the beginning, before decoding is performed. * - * @see JxlBasicInfo for the orientation field, and @see JxlOrientation for the + * @see JxlBasicInfo for the orientation field, and @ref JxlOrientation for the * possible values. * * @param dec decoder object * @param skip_reorientation JXL_TRUE to enable, JXL_FALSE to disable. - * @return JXL_DEC_SUCCESS if no error, JXL_DEC_ERROR otherwise. + * @return @ref JXL_DEC_SUCCESS if no error, @ref JXL_DEC_ERROR otherwise. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetKeepOrientation(JxlDecoder* dec, JXL_BOOL skip_reorientation); +/** + * Enables or disables preserving of associated alpha channels. If + * unpremul_alpha is set to JXL_FALSE then for associated alpha channel, the + * pixel data is returned with premultiplied colors. If it is set to JXL_TRUE, + * The colors will be unpremultiplied based on the alpha channel. This function + * has no effect if the image does not have an associated alpha channel. + * + * By default, this option is disabled, and the returned pixel data "as is". + * + * This function must be called at the beginning, before decoding is performed. + * + * @param dec decoder object + * @param unpremul_alpha JXL_TRUE to enable, JXL_FALSE to disable. + * @return @ref JXL_DEC_SUCCESS if no error, @ref JXL_DEC_ERROR otherwise. + */ +JXL_EXPORT JxlDecoderStatus +JxlDecoderSetUnpremultiplyAlpha(JxlDecoder* dec, JXL_BOOL unpremul_alpha); + /** Enables or disables rendering spot colors. By default, spot colors * are rendered, which is OK for viewing the decoded image. If render_spotcolors * is JXL_FALSE, then spot colors are not rendered, and have to be retrieved - * separately using JxlDecoderSetExtraChannelBuffer. This is useful for e.g. - * printing applications. + * separately using @ref JxlDecoderSetExtraChannelBuffer. This is useful for + * e.g. printing applications. * * @param dec decoder object * @param render_spotcolors JXL_TRUE to enable (default), JXL_FALSE to disable. - * @return JXL_DEC_SUCCESS if no error, JXL_DEC_ERROR otherwise. + * @return @ref JXL_DEC_SUCCESS if no error, @ref JXL_DEC_ERROR otherwise. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetRenderSpotcolors(JxlDecoder* dec, JXL_BOOL render_spotcolors); @@ -450,106 +555,111 @@ JxlDecoderSetRenderSpotcolors(JxlDecoder* dec, JXL_BOOL render_spotcolors); * * @param dec decoder object * @param coalescing JXL_TRUE to enable coalescing (default), JXL_FALSE to - * disable it. - * @return JXL_DEC_SUCCESS if no error, JXL_DEC_ERROR otherwise. + * disable it. + * @return @ref JXL_DEC_SUCCESS if no error, @ref JXL_DEC_ERROR otherwise. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetCoalescing(JxlDecoder* dec, JXL_BOOL coalescing); /** * Decodes JPEG XL file using the available bytes. Requires input has been - * set with JxlDecoderSetInput. After JxlDecoderProcessInput, input can - * optionally be released with JxlDecoderReleaseInput and then set again to - * next bytes in the stream. JxlDecoderReleaseInput returns how many bytes are - * not yet processed, before a next call to JxlDecoderProcessInput all - * unprocessed bytes must be provided again (the address need not match, but the - * contents must), and more bytes may be concatenated after the unprocessed - * bytes. + * set with @ref JxlDecoderSetInput. After @ref JxlDecoderProcessInput, input + * can optionally be released with @ref JxlDecoderReleaseInput and then set + * again to next bytes in the stream. @ref JxlDecoderReleaseInput returns how + * many bytes are not yet processed, before a next call to @ref + * JxlDecoderProcessInput all unprocessed bytes must be provided again (the + * address need not match, but the contents must), and more bytes may be + * concatenated after the unprocessed bytes. * * The returned status indicates whether the decoder needs more input bytes, or * more output buffer for a certain type of output data. No matter what the - * returned status is (other than JXL_DEC_ERROR), new information, such as - * JxlDecoderGetBasicInfo, may have become available after this call. When - * the return value is not JXL_DEC_ERROR or JXL_DEC_SUCCESS, the decoding - * requires more JxlDecoderProcessInput calls to continue. + * returned status is (other than @ref JXL_DEC_ERROR), new information, such + * as @ref JxlDecoderGetBasicInfo, may have become available after this call. + * When the return value is not @ref JXL_DEC_ERROR or @ref JXL_DEC_SUCCESS, the + * decoding requires more @ref JxlDecoderProcessInput calls to continue. * * @param dec decoder object - * @return JXL_DEC_SUCCESS when decoding finished and all events handled. If you - * still have more unprocessed input data anyway, then you can still continue - * by using JxlDecoderSetInput and calling JxlDecoderProcessInput again, similar - * to handling JXL_DEC_NEED_MORE_INPUT. JXL_DEC_SUCCESS can occur instead of - * JXL_DEC_NEED_MORE_INPUT when, for example, the input data ended right at - * the boundary of a box of the container format, all essential codestream boxes - * were already decoded, but extra metadata boxes are still present in the next - * data. JxlDecoderProcessInput cannot return success if all codestream boxes - * have not been seen yet. - * @return JXL_DEC_ERROR when decoding failed, e.g. invalid codestream. - * TODO(lode) document the input data mechanism - * @return JXL_DEC_NEED_MORE_INPUT when more input data is necessary. - * @return JXL_DEC_BASIC_INFO when basic info such as image dimensions is - * available and this informative event is subscribed to. - * @return JXL_DEC_EXTENSIONS when JPEG XL codestream user extensions are - * available and this informative event is subscribed to. - * @return JXL_DEC_COLOR_ENCODING when color profile information is - * available and this informative event is subscribed to. - * @return JXL_DEC_PREVIEW_IMAGE when preview pixel information is available and - * output in the preview buffer. - * @return JXL_DEC_DC_IMAGE when DC pixel information (8x8 downscaled version - * of the image) is available and output is in the DC buffer. - * @return JXL_DEC_FULL_IMAGE when all pixel information at highest detail is - * available and has been output in the pixel buffer. + * @return @ref JXL_DEC_SUCCESS when decoding finished and all events handled. + * If you still have more unprocessed input data anyway, then you can still + * continue by using @ref JxlDecoderSetInput and calling @ref + * JxlDecoderProcessInput again, similar to handling @ref + * JXL_DEC_NEED_MORE_INPUT. @ref JXL_DEC_SUCCESS can occur instead of @ref + * JXL_DEC_NEED_MORE_INPUT when, for example, the input data ended right at + * the boundary of a box of the container format, all essential codestream + * boxes were already decoded, but extra metadata boxes are still present in + * the next data. @ref JxlDecoderProcessInput cannot return success if all + * codestream boxes have not been seen yet. + * @return @ref JXL_DEC_ERROR when decoding failed, e.g. invalid codestream. + * TODO(lode): document the input data mechanism + * @return @ref JXL_DEC_NEED_MORE_INPUT when more input data is necessary. + * @return @ref JXL_DEC_BASIC_INFO when basic info such as image dimensions is + * available and this informative event is subscribed to. + * @return @ref JXL_DEC_COLOR_ENCODING when color profile information is + * available and this informative event is subscribed to. + * @return @ref JXL_DEC_PREVIEW_IMAGE when preview pixel information is + * available and output in the preview buffer. + * @return @ref JXL_DEC_DC_IMAGE when DC pixel information (8x8 downscaled + * version of the image) is available and output is in the DC buffer. + * @return @ref JXL_DEC_FULL_IMAGE when all pixel information at highest detail + * is available and has been output in the pixel buffer. */ JXL_EXPORT JxlDecoderStatus JxlDecoderProcessInput(JxlDecoder* dec); /** - * Sets input data for JxlDecoderProcessInput. The data is owned by the caller - * and may be used by the decoder until JxlDecoderReleaseInput is called or - * the decoder is destroyed or reset so must be kept alive until then. - * Cannot be called if JxlDecoderSetInput was already called and - * JxlDecoderReleaseInput was not yet called, and cannot be called after + * Sets input data for @ref JxlDecoderProcessInput. The data is owned by the + * caller and may be used by the decoder until @ref JxlDecoderReleaseInput is + * called or the decoder is destroyed or reset so must be kept alive until then. + * Cannot be called if @ref JxlDecoderSetInput was already called and @ref + * JxlDecoderReleaseInput was not yet called, and cannot be called after @ref * JxlDecoderCloseInput indicating the end of input was called. + * * @param dec decoder object * @param data pointer to next bytes to read from * @param size amount of bytes available starting from data - * @return JXL_DEC_ERROR if input was already set without releasing or - * JxlDecoderCloseInput was already called, JXL_DEC_SUCCESS otherwise. + * @return @ref JXL_DEC_ERROR if input was already set without releasing or @ref + * JxlDecoderCloseInput was already called, @ref JXL_DEC_SUCCESS otherwise. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetInput(JxlDecoder* dec, const uint8_t* data, size_t size); /** - * Releases input which was provided with JxlDecoderSetInput. Between - * JxlDecoderProcessInput and JxlDecoderReleaseInput, the user may not alter - * the data in the buffer. Calling JxlDecoderReleaseInput is required whenever - * any input is already set and new input needs to be added with - * JxlDecoderSetInput, but is not required before JxlDecoderDestroy or - * JxlDecoderReset. Calling JxlDecoderReleaseInput when no input is set is + * Releases input which was provided with @ref JxlDecoderSetInput. Between @ref + * JxlDecoderProcessInput and @ref JxlDecoderReleaseInput, the user may not + * alter the data in the buffer. Calling @ref JxlDecoderReleaseInput is required + * whenever any input is already set and new input needs to be added with @ref + * JxlDecoderSetInput, but is not required before @ref JxlDecoderDestroy or @ref + * JxlDecoderReset. Calling @ref JxlDecoderReleaseInput when no input is set is * not an error and returns 0. + * * @param dec decoder object - * @return the amount of bytes the decoder has not yet processed that are - * still remaining in the data set by JxlDecoderSetInput, or 0 if no input is - * set or JxlDecoderReleaseInput was already called. For a next call to - * JxlDecoderProcessInput, the buffer must start with these unprocessed bytes. - * This value doesn't provide information about how many bytes the decoder - * truly processed internally or how large the original JPEG XL codestream or - * file are. + * @return The amount of bytes the decoder has not yet processed that are still + * remaining in the data set by @ref JxlDecoderSetInput, or 0 if no input is + * set or @ref JxlDecoderReleaseInput was already called. For a next call + * to @ref JxlDecoderProcessInput, the buffer must start with these + * unprocessed bytes. From this value it is possible to infer the position + * of certain JPEG XL codestream elements (e.g. end of headers, frame + * start/end). See the documentation of individual values of @ref + * JxlDecoderStatus for more information. */ JXL_EXPORT size_t JxlDecoderReleaseInput(JxlDecoder* dec); /** - * Marks the input as finished, indicates that no more JxlDecoderSetInput will - * be called. This function allows the decoder to determine correctly if it + * Marks the input as finished, indicates that no more @ref JxlDecoderSetInput + * will be called. This function allows the decoder to determine correctly if it * should return success, need more input or error in certain cases. For * backwards compatibility with a previous version of the API, using this - * function is optional when not using the JXL_DEC_BOX event (the decoder is - * able to determine the end of the image frames without marking the end), but - * using this function is required when using JXL_DEC_BOX for getting metadata - * box contents. This function does not replace JxlDecoderReleaseInput, that - * function should still be called if its return value is needed. - * JxlDecoderCloseInput should be called as soon as all known input bytes are - * set (e.g. at the beginning when not streaming but setting all input at once), - * before the final JxlDecoderProcessInput calls. + * function is optional when not using the @ref JXL_DEC_BOX event (the decoder + * is able to determine the end of the image frames without marking the end), + * but using this function is required when using @ref JXL_DEC_BOX for getting + * metadata box contents. This function does not replace @ref + * JxlDecoderReleaseInput, that function should still be called if its return + * value is needed. + * + * @ref JxlDecoderCloseInput should be called as soon as all known input bytes + * are set (e.g. at the beginning when not streaming but setting all input + * at once), before the final @ref JxlDecoderProcessInput calls. + * * @param dec decoder object */ JXL_EXPORT void JxlDecoderCloseInput(JxlDecoder* dec); @@ -560,10 +670,10 @@ JXL_EXPORT void JxlDecoderCloseInput(JxlDecoder* dec); * * @param dec decoder object * @param info struct to copy the information into, or NULL to only check - * whether the information is available through the return value. - * @return JXL_DEC_SUCCESS if the value is available, - * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case - * of other error conditions. + * whether the information is available through the return value. + * @return @ref JXL_DEC_SUCCESS if the value is available, @ref + * JXL_DEC_NEED_MORE_INPUT if not yet available, @ref JXL_DEC_ERROR + * in case of other error conditions. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetBasicInfo(const JxlDecoder* dec, JxlBasicInfo* info); @@ -575,10 +685,10 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderGetBasicInfo(const JxlDecoder* dec, * @param dec decoder object * @param index index of the extra channel to query. * @param info struct to copy the information into, or NULL to only check - * whether the information is available through the return value. - * @return JXL_DEC_SUCCESS if the value is available, - * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case - * of other error conditions. + * whether the information is available through the return value. + * @return @ref JXL_DEC_SUCCESS if the value is available, @ref + * JXL_DEC_NEED_MORE_INPUT if not yet available, @ref JXL_DEC_ERROR + * in case of other error conditions. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetExtraChannelInfo( const JxlDecoder* dec, size_t index, JxlExtraChannelInfo* info); @@ -593,9 +703,9 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderGetExtraChannelInfo( * @param index index of the extra channel to query. * @param name buffer to copy the name into * @param size size of the name buffer in bytes - * @return JXL_DEC_SUCCESS if the value is available, - * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case - * of other error conditions. + * @return @ref JXL_DEC_SUCCESS if the value is available, @ref + * JXL_DEC_NEED_MORE_INPUT if not yet available, @ref JXL_DEC_ERROR + * in case of other error conditions. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetExtraChannelName(const JxlDecoder* dec, size_t index, @@ -604,11 +714,11 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderGetExtraChannelName(const JxlDecoder* dec, /** Defines which color profile to get: the profile from the codestream * metadata header, which represents the color profile of the original image, - * or the color profile from the pixel data received by the decoder. Both are - * the same if the basic has uses_original_profile set. + * or the color profile from the pixel data produced by the decoder. Both are + * the same if the JxlBasicInfo has uses_original_profile set. */ typedef enum { - /** Get the color profile of the original image from the metadata.. + /** Get the color profile of the original image from the metadata. */ JXL_COLOR_PROFILE_TARGET_ORIGINAL = 0, @@ -621,174 +731,182 @@ typedef enum { * This is an alternative to an ICC Profile, which can represent a more limited * amount of color spaces, but represents them exactly through enum values. * - * It is often possible to use JxlDecoderGetColorAsICCProfile as an + * It is often possible to use @ref JxlDecoderGetColorAsICCProfile as an * alternative anyway. The following scenarios are possible: - * - The JPEG XL image has an attached ICC Profile, in that case, the encoded - * structured data is not available, this function will return an error - * status. JxlDecoderGetColorAsICCProfile should be called instead. - * - The JPEG XL image has an encoded structured color profile, and it - * represents an RGB or grayscale color space. This function will return it. - * You can still use JxlDecoderGetColorAsICCProfile as well as an - * alternative if desired, though depending on which RGB color space is - * represented, the ICC profile may be a close approximation. It is also not - * always feasible to deduce from an ICC profile which named color space it - * exactly represents, if any, as it can represent any arbitrary space. - * - The JPEG XL image has an encoded structured color profile, and it indicates - * an unknown or xyb color space. In that case, - * JxlDecoderGetColorAsICCProfile is not available. - * - * When rendering an image on a system that supports ICC profiles, + * - The JPEG XL image has an attached ICC Profile, in that case, the encoded + * structured data is not available, this function will return an error + * status. @ref JxlDecoderGetColorAsICCProfile should be called instead. + * - The JPEG XL image has an encoded structured color profile, and it + * represents an RGB or grayscale color space. This function will return it. + * You can still use @ref JxlDecoderGetColorAsICCProfile as well as an + * alternative if desired, though depending on which RGB color space is + * represented, the ICC profile may be a close approximation. It is also not + * always feasible to deduce from an ICC profile which named color space it + * exactly represents, if any, as it can represent any arbitrary space. + * - The JPEG XL image has an encoded structured color profile, and it + * indicates an unknown or xyb color space. In that case, @ref + * JxlDecoderGetColorAsICCProfile is not available. + * + * When rendering an image on a system that supports ICC profiles, @ref * JxlDecoderGetColorAsICCProfile should be used first. When rendering * for a specific color space, possibly indicated in the JPEG XL - * image, JxlDecoderGetColorAsEncodedProfile should be used first. + * image, @ref JxlDecoderGetColorAsEncodedProfile should be used first. * * @param dec decoder object - * @param format pixel format to output the data to. Only used for - * JXL_COLOR_PROFILE_TARGET_DATA, may be nullptr otherwise. + * @param unused_format deprecated, can be NULL * @param target whether to get the original color profile from the metadata * or the color profile of the decoded pixels. * @param color_encoding struct to copy the information into, or NULL to only - * check whether the information is available through the return value. - * @return JXL_DEC_SUCCESS if the data is available and returned, - * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case - * the encoded structured color profile does not exist in the codestream. + * check whether the information is available through the return value. + * @return @ref JXL_DEC_SUCCESS if the data is available and returned, @ref + * JXL_DEC_NEED_MORE_INPUT if not yet available, @ref JXL_DEC_ERROR in + * case the encoded structured color profile does not exist in the + * codestream. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetColorAsEncodedProfile( - const JxlDecoder* dec, const JxlPixelFormat* format, + const JxlDecoder* dec, const JxlPixelFormat* unused_format, JxlColorProfileTarget target, JxlColorEncoding* color_encoding); /** - * Outputs the size in bytes of the ICC profile returned by + * Outputs the size in bytes of the ICC profile returned by @ref * JxlDecoderGetColorAsICCProfile, if available, or indicates there is none * available. In most cases, the image will have an ICC profile available, but - * if it does not, JxlDecoderGetColorAsEncodedProfile must be used instead. + * if it does not, @ref JxlDecoderGetColorAsEncodedProfile must be used instead. + * * @see JxlDecoderGetColorAsEncodedProfile for more information. The ICC * profile is either the exact ICC profile attached to the codestream metadata, * or a close approximation generated from JPEG XL encoded structured data, * depending of what is encoded in the codestream. * * @param dec decoder object - * @param format pixel format to output the data to. Only used for - * JXL_COLOR_PROFILE_TARGET_DATA, may be nullptr otherwise. + * @param unused_format deprecated, can be NULL * @param target whether to get the original color profile from the metadata * or the color profile of the decoded pixels. * @param size variable to output the size into, or NULL to only check the - * return status. - * @return JXL_DEC_SUCCESS if the ICC profile is available, - * JXL_DEC_NEED_MORE_INPUT if the decoder has not yet received enough - * input data to determine whether an ICC profile is available or what its - * size is, JXL_DEC_ERROR in case the ICC profile is not available and - * cannot be generated. + * return status. + * @return @ref JXL_DEC_SUCCESS if the ICC profile is available, @ref + * JXL_DEC_NEED_MORE_INPUT if the decoder has not yet received enough + * input data to determine whether an ICC profile is available or what its + * size is, @ref JXL_DEC_ERROR in case the ICC profile is not available and + * cannot be generated. */ -JXL_EXPORT JxlDecoderStatus -JxlDecoderGetICCProfileSize(const JxlDecoder* dec, const JxlPixelFormat* format, - JxlColorProfileTarget target, size_t* size); +JXL_EXPORT JxlDecoderStatus JxlDecoderGetICCProfileSize( + const JxlDecoder* dec, const JxlPixelFormat* unused_format, + JxlColorProfileTarget target, size_t* size); /** - * Outputs ICC profile if available. The profile is only available if + * Outputs ICC profile if available. The profile is only available if @ref * JxlDecoderGetICCProfileSize returns success. The output buffer must have - * at least as many bytes as given by JxlDecoderGetICCProfileSize. + * at least as many bytes as given by @ref JxlDecoderGetICCProfileSize. * * @param dec decoder object - * @param format pixel format to output the data to. Only used for - * JXL_COLOR_PROFILE_TARGET_DATA, may be nullptr otherwise. + * @param unused_format deprecated, can be NULL * @param target whether to get the original color profile from the metadata * or the color profile of the decoded pixels. * @param icc_profile buffer to copy the ICC profile into * @param size size of the icc_profile buffer in bytes - * @return JXL_DEC_SUCCESS if the profile was successfully returned is - * available, JXL_DEC_NEED_MORE_INPUT if not yet available, - * JXL_DEC_ERROR if the profile doesn't exist or the output size is not - * large enough. + * @return @ref JXL_DEC_SUCCESS if the profile was successfully returned is + * available, @ref JXL_DEC_NEED_MORE_INPUT if not yet available, @ref + * JXL_DEC_ERROR if the profile doesn't exist or the output size is not + * large enough. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetColorAsICCProfile( - const JxlDecoder* dec, const JxlPixelFormat* format, + const JxlDecoder* dec, const JxlPixelFormat* unused_format, JxlColorProfileTarget target, uint8_t* icc_profile, size_t size); -/** Sets the color profile to use for JXL_COLOR_PROFILE_TARGET_DATA for the +/** Sets the color profile to use for @ref JXL_COLOR_PROFILE_TARGET_DATA for the * special case when the decoder has a choice. This only has effect for a JXL - * image where uses_original_profile is false, and the original color profile is - * encoded as an ICC color profile rather than a JxlColorEncoding with known - * enum values. In most other cases (uses uses_original_profile is true, or the - * color profile is already given as a JxlColorEncoding), this setting is - * ignored and the decoder uses a profile related to the image. - * No matter what, the JXL_COLOR_PROFILE_TARGET_DATA must still be queried to - * know the actual data format of the decoded pixels after decoding. - * - * The intended use case of this function is for cases where you are using - * a color management system to parse the original ICC color profile - * (JXL_COLOR_PROFILE_TARGET_ORIGINAL), from this you know that the ICC - * profile represents one of the color profiles supported by JxlColorEncoding - * (such as sRGB, PQ or HLG): in that case it is beneficial (but not necessary) - * to use JxlDecoderSetPreferredColorProfile to match the parsed profile. The - * JXL decoder has no color management system built in, but can convert XYB - * color to any of the ones supported by JxlColorEncoding. - * - * Can only be set after the JXL_DEC_COLOR_ENCODING event occurred and before - * any other event occurred, and can affect the result of - * JXL_COLOR_PROFILE_TARGET_DATA (but not of JXL_COLOR_PROFILE_TARGET_ORIGINAL), - * so should be used after getting JXL_COLOR_PROFILE_TARGET_ORIGINAL but before - * getting JXL_COLOR_PROFILE_TARGET_DATA. The color_encoding must be grayscale - * if num_color_channels from the basic info is 1, RGB if num_color_channels - * from the basic info is 3. - * - * If JxlDecoderSetPreferredColorProfile is not used, then for images for which - * uses_original_profile is false and with ICC color profile, the decoder will - * choose linear sRGB for color images, linear grayscale for grayscale images. - * This function only sets a preference, since for other images the decoder has - * no choice what color profile to use, it is determined by the image. + * image where uses_original_profile is false. If uses_original_profile is true, + * this setting is ignored and the decoder uses a profile related to the image. + * No matter what, the @ref JXL_COLOR_PROFILE_TARGET_DATA must still be queried + * to know the actual data format of the decoded pixels after decoding. + * + * The JXL decoder has no color management system built in, but can convert XYB + * color to any of the ones supported by JxlColorEncoding. Note that if the + * requested color encoding has a narrower gamut, or the white points differ, + * then the resulting image can have significant color distortion. + * + * Can only be set after the @ref JXL_DEC_COLOR_ENCODING event occurred and + * before any other event occurred, and can affect the result of @ref + * JXL_COLOR_PROFILE_TARGET_DATA (but not of @ref + * JXL_COLOR_PROFILE_TARGET_ORIGINAL), so should be used after getting @ref + * JXL_COLOR_PROFILE_TARGET_ORIGINAL but before getting @ref + * JXL_COLOR_PROFILE_TARGET_DATA. The color_encoding must be grayscale if + * num_color_channels from the basic info is 1, RGB if num_color_channels from + * the basic info is 3. + * + * If @ref JxlDecoderSetPreferredColorProfile is not used, then for images for + * which uses_original_profile is false and with ICC color profile, the decoder + * will choose linear sRGB for color images, linear grayscale for grayscale + * images. This function only sets a preference, since for other images the + * decoder has no choice what color profile to use, it is determined by the + * image. * * @param dec decoder object * @param color_encoding the default color encoding to set - * @return JXL_DEC_SUCCESS if the preference was set successfully, JXL_DEC_ERROR - * otherwise. + * @return @ref JXL_DEC_SUCCESS if the preference was set successfully, @ref + * JXL_DEC_ERROR otherwise. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetPreferredColorProfile( JxlDecoder* dec, const JxlColorEncoding* color_encoding); +/** Requests that the decoder perform tone mapping to the peak display luminance + * passed as @c desired_intensity_target, if appropriate. + * @note This is provided for convenience and the exact tone mapping that is + * performed is not meant to be considered authoritative in any way. It may + * change from version to version. + * @param dec decoder object + * @param desired_intensity_target the intended target peak luminance + * @return @ref JXL_DEC_SUCCESS if the preference was set successfully, @ref + * JXL_DEC_ERROR otherwise. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderSetDesiredIntensityTarget( + JxlDecoder* dec, float desired_intensity_target); + /** * Returns the minimum size in bytes of the preview image output pixel buffer - * for the given format. This is the buffer for JxlDecoderSetPreviewOutBuffer. - * Requires the preview header information is available in the decoder. + * for the given format. This is the buffer for @ref + * JxlDecoderSetPreviewOutBuffer. Requires the preview header information is + * available in the decoder. * * @param dec decoder object * @param format format of pixels * @param size output value, buffer size in bytes - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * information not available yet. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * information not available yet. */ JXL_EXPORT JxlDecoderStatus JxlDecoderPreviewOutBufferSize( const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size); /** * Sets the buffer to write the small resolution preview image - * to. The size of the buffer must be at least as large as given by - * JxlDecoderPreviewOutBufferSize. The buffer follows the format described by - * JxlPixelFormat. The preview image dimensions are given by the + * to. The size of the buffer must be at least as large as given by @ref + * JxlDecoderPreviewOutBufferSize. The buffer follows the format described + * by JxlPixelFormat. The preview image dimensions are given by the * JxlPreviewHeader. The buffer is owned by the caller. * * @param dec decoder object * @param format format of pixels. Object owned by user and its contents are - * copied internally. + * copied internally. * @param buffer buffer type to output the pixel data to * @param size size of buffer in bytes - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * size too small. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * size too small. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetPreviewOutBuffer( JxlDecoder* dec, const JxlPixelFormat* format, void* buffer, size_t size); /** * Outputs the information from the frame, such as duration when have_animation. - * This function can be called when JXL_DEC_FRAME occurred for the current + * This function can be called when @ref JXL_DEC_FRAME occurred for the current * frame, even when have_animation in the JxlBasicInfo is JXL_FALSE. * * @param dec decoder object * @param header struct to copy the information into, or NULL to only check - * whether the information is available through the return value. - * @return JXL_DEC_SUCCESS if the value is available, - * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case - * of other error conditions. + * whether the information is available through the return value. + * @return @ref JXL_DEC_SUCCESS if the value is available, @ref + * JXL_DEC_NEED_MORE_INPUT if not yet available, @ref JXL_DEC_ERROR in + * case of other error conditions. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetFrameHeader(const JxlDecoder* dec, JxlFrameHeader* header); @@ -801,16 +919,16 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderGetFrameHeader(const JxlDecoder* dec, * @param name buffer to copy the name into * @param size size of the name buffer in bytes, including zero termination * character, so this must be at least JxlFrameHeader.name_length + 1. - * @return JXL_DEC_SUCCESS if the value is available, - * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case - * of other error conditions. + * @return @ref JXL_DEC_SUCCESS if the value is available, @ref + * JXL_DEC_NEED_MORE_INPUT if not yet available, @ref JXL_DEC_ERROR in + * case of other error conditions. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetFrameName(const JxlDecoder* dec, char* name, size_t size); /** * Outputs the blend information for the current frame for a specific extra - * channel. This function can be called when JXL_DEC_FRAME occurred for the + * channel. This function can be called when @ref JXL_DEC_FRAME occurred for the * current frame, even when have_animation in the JxlBasicInfo is JXL_FALSE. * This information is only useful if coalescing is disabled; otherwise the * decoder will have performed blending already. @@ -818,162 +936,156 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderGetFrameName(const JxlDecoder* dec, * @param dec decoder object * @param index the index of the extra channel * @param blend_info struct to copy the information into - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetExtraChannelBlendInfo( const JxlDecoder* dec, size_t index, JxlBlendInfo* blend_info); /** * Returns the minimum size in bytes of the DC image output buffer - * for the given format. This is the buffer for JxlDecoderSetDCOutBuffer. + * for the given format. This is the buffer for @ref JxlDecoderSetDCOutBuffer. * Requires the basic image information is available in the decoder. * * @param dec decoder object * @param format format of pixels * @param size output value, buffer size in bytes - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * information not available yet. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * information not available yet. * - * DEPRECATED: the DC feature in this form will be removed. Use - * JxlDecoderFlushImage for progressive rendering. + * @deprecated The DC feature in this form will be removed. Use @ref + * JxlDecoderFlushImage for progressive rendering. */ JXL_EXPORT JXL_DEPRECATED JxlDecoderStatus JxlDecoderDCOutBufferSize( const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size); /** * Sets the buffer to write the lower resolution (8x8 sub-sampled) DC image - * to. The size of the buffer must be at least as large as given by + * to. The size of the buffer must be at least as large as given by @ref * JxlDecoderDCOutBufferSize. The buffer follows the format described by * JxlPixelFormat. The DC image has dimensions ceil(xsize / 8) * ceil(ysize / * 8). The buffer is owned by the caller. * * @param dec decoder object * @param format format of pixels. Object owned by user and its contents are - * copied internally. + * copied internally. * @param buffer buffer type to output the pixel data to * @param size size of buffer in bytes - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * size too small. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * size too small. * - * DEPRECATED: the DC feature in this form will be removed. Use - * JxlDecoderFlushImage for progressive rendering. + * @deprecated The DC feature in this form will be removed. Use @ref + * JxlDecoderFlushImage for progressive rendering. */ JXL_EXPORT JXL_DEPRECATED JxlDecoderStatus JxlDecoderSetDCOutBuffer( JxlDecoder* dec, const JxlPixelFormat* format, void* buffer, size_t size); /** * Returns the minimum size in bytes of the image output pixel buffer for the - * given format. This is the buffer for JxlDecoderSetImageOutBuffer. Requires - * that the basic image information is available in the decoder in the case of - * coalescing enabled (default). In case coalescing is disabled, this can only - * be called after the JXL_DEC_FRAME event occurs. In that case, it will return - * the size required to store the possibly cropped frame (which can be larger or - * smaller than the image dimensions). + * given format. This is the buffer for @ref JxlDecoderSetImageOutBuffer. + * Requires that the basic image information is available in the decoder in the + * case of coalescing enabled (default). In case coalescing is disabled, this + * can only be called after the @ref JXL_DEC_FRAME event occurs. In that case, + * it will return the size required to store the possibly cropped frame (which + * can be larger or smaller than the image dimensions). * * @param dec decoder object * @param format format of the pixels. * @param size output value, buffer size in bytes - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * information not available yet. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * information not available yet. */ JXL_EXPORT JxlDecoderStatus JxlDecoderImageOutBufferSize( const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size); /** * Sets the buffer to write the full resolution image to. This can be set when - * the JXL_DEC_FRAME event occurs, must be set when the - * JXL_DEC_NEED_IMAGE_OUT_BUFFER event occurs, and applies only for the current - * frame. The size of the buffer must be at least as large as given by - * JxlDecoderImageOutBufferSize. The buffer follows the format described by - * JxlPixelFormat. The buffer is owned by the caller. + * the @ref JXL_DEC_FRAME event occurs, must be set when the @ref + * JXL_DEC_NEED_IMAGE_OUT_BUFFER event occurs, and applies only for the + * current frame. The size of the buffer must be at least as large as given + * by @ref JxlDecoderImageOutBufferSize. The buffer follows the format described + * by JxlPixelFormat. The buffer is owned by the caller. * * @param dec decoder object * @param format format of the pixels. Object owned by user and its contents - * are copied internally. + * are copied internally. * @param buffer buffer type to output the pixel data to * @param size size of buffer in bytes - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * size too small. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * size too small. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetImageOutBuffer( JxlDecoder* dec, const JxlPixelFormat* format, void* buffer, size_t size); /** - * Function type for JxlDecoderSetImageOutCallback. - * @see JxlDecoderSetImageOutCallback for usage. + * Function type for @ref JxlDecoderSetImageOutCallback. * * The callback may be called simultaneously by different threads when using a * threaded parallel runner, on different pixels. * - * @param opaque optional user data, as given to JxlDecoderSetImageOutCallback. + * @param opaque optional user data, as given to @ref + * JxlDecoderSetImageOutCallback. * @param x horizontal position of leftmost pixel of the pixel data. * @param y vertical position of the pixel data. * @param num_pixels amount of pixels included in the pixel data, horizontally. - * This is not the same as xsize of the full image, it may be smaller. - * @param pixels pixel data as a horizontal stripe, in the format passed to - * JxlDecoderSetImageOutCallback. The memory is not owned by the user, and is - * only valid during the time the callback is running. + * This is not the same as xsize of the full image, it may be smaller. + * @param pixels pixel data as a horizontal stripe, in the format passed to @ref + * JxlDecoderSetImageOutCallback. The memory is not owned by the user, and + * is only valid during the time the callback is running. */ typedef void (*JxlImageOutCallback)(void* opaque, size_t x, size_t y, size_t num_pixels, const void* pixels); /** - * Initialization callback for @c JxlDecoderSetMultithreadedImageOutCallback. - * - * @see JxlDecoderSetMultithreadedImageOutCallback + * Initialization callback for @ref JxlDecoderSetMultithreadedImageOutCallback. * - * @param init_opaque optional user data, as given to - * @c JxlDecoderSetMultithreadedImageOutCallback. + * @param init_opaque optional user data, as given to @ref + * JxlDecoderSetMultithreadedImageOutCallback. * @param num_threads maximum number of threads that will call the @c run - * callback concurrently. + * callback concurrently. * @param num_pixels_per_thread maximum number of pixels that will be passed in - * one call to @c run. + * one call to @c run. * @return a pointer to data that will be passed to the @c run callback, or - * @c NULL if initialization failed. + * @c NULL if initialization failed. */ typedef void* (*JxlImageOutInitCallback)(void* init_opaque, size_t num_threads, size_t num_pixels_per_thread); /** - * Worker callback for @c JxlDecoderSetMultithreadedImageOutCallback. - * - * @see JxlDecoderSetMultithreadedImageOutCallback + * Worker callback for @ref JxlDecoderSetMultithreadedImageOutCallback. * * @param run_opaque user data returned by the @c init callback. * @param thread_id number in `[0, num_threads)` identifying the thread of the - * current invocation of the callback. + * current invocation of the callback. * @param x horizontal position of the first (leftmost) pixel of the pixel data. * @param y vertical position of the pixel data. * @param num_pixels number of pixels in the pixel data. May be less than the - * full @c xsize of the image, and will be at most equal to the @c - * num_pixels_per_thread that was passed to @c init. - * @param pixels pixel data as a horizontal stripe, in the format passed to @c - * JxlDecoderSetMultithreadedImageOutCallback. The data pointed to remains owned - * by the caller and is only guaranteed to outlive the current callback - * invocation. + * full @c xsize of the image, and will be at most equal to the @c + * num_pixels_per_thread that was passed to @c init. + * @param pixels pixel data as a horizontal stripe, in the format passed to @ref + * JxlDecoderSetMultithreadedImageOutCallback. The data pointed to + * remains owned by the caller and is only guaranteed to outlive the current + * callback invocation. */ typedef void (*JxlImageOutRunCallback)(void* run_opaque, size_t thread_id, size_t x, size_t y, size_t num_pixels, const void* pixels); /** - * Destruction callback for @c JxlDecoderSetMultithreadedImageOutCallback, + * Destruction callback for @ref JxlDecoderSetMultithreadedImageOutCallback, * called after all invocations of the @c run callback to perform any * appropriate clean-up of the @c run_opaque data returned by @c init. * - * @see JxlDecoderSetMultithreadedImageOutCallback - * * @param run_opaque user data returned by the @c init callback. */ typedef void (*JxlImageOutDestroyCallback)(void* run_opaque); /** - * Sets pixel output callback. This is an alternative to - * JxlDecoderSetImageOutBuffer. This can be set when the JXL_DEC_FRAME event - * occurs, must be set when the JXL_DEC_NEED_IMAGE_OUT_BUFFER event occurs, and - * applies only for the current frame. Only one of JxlDecoderSetImageOutBuffer - * or JxlDecoderSetImageOutCallback may be used for the same frame, not both at - * the same time. + * Sets pixel output callback. This is an alternative to @ref + * JxlDecoderSetImageOutBuffer. This can be set when the @ref JXL_DEC_FRAME + * event occurs, must be set when the @ref JXL_DEC_NEED_IMAGE_OUT_BUFFER event + * occurs, and applies only for the current frame. Only one of @ref + * JxlDecoderSetImageOutBuffer or @ref JxlDecoderSetImageOutCallback may be used + * for the same frame, not both at the same time. * * The callback will be called multiple times, to receive the image * data in small chunks. The callback receives a horizontal stripe of pixel @@ -984,53 +1096,53 @@ typedef void (*JxlImageOutDestroyCallback)(void* run_opaque); * simultaneously by different threads when using a threaded parallel runner, on * different pixels. * - * If JxlDecoderFlushImage is not used, then each pixel will be visited exactly - * once by the different callback calls, during processing with one or more - * JxlDecoderProcessInput calls. These pixels are decoded to full detail, they - * are not part of a lower resolution or lower quality progressive pass, but the - * final pass. - * - * If JxlDecoderFlushImage is used, then in addition each pixel will be visited - * zero or one times during the blocking JxlDecoderFlushImage call. Pixels - * visited as a result of JxlDecoderFlushImage may represent a lower resolution - * or lower quality intermediate progressive pass of the image. Any visited - * pixel will be of a quality at least as good or better than previous visits of - * this pixel. A pixel may be visited zero times if it cannot be decoded yet - * or if it was already decoded to full precision (this behavior is not - * guaranteed). + * If @ref JxlDecoderFlushImage is not used, then each pixel will be visited + * exactly once by the different callback calls, during processing with one or + * more @ref JxlDecoderProcessInput calls. These pixels are decoded to full + * detail, they are not part of a lower resolution or lower quality progressive + * pass, but the final pass. + * + * If @ref JxlDecoderFlushImage is used, then in addition each pixel will be + * visited zero or one times during the blocking @ref JxlDecoderFlushImage call. + * Pixels visited as a result of @ref JxlDecoderFlushImage may represent a lower + * resolution or lower quality intermediate progressive pass of the image. Any + * visited pixel will be of a quality at least as good or better than previous + * visits of this pixel. A pixel may be visited zero times if it cannot be + * decoded yet or if it was already decoded to full precision (this behavior is + * not guaranteed). * * @param dec decoder object * @param format format of the pixels. Object owned by user; its contents are - * copied internally. + * copied internally. * @param callback the callback function receiving partial scanlines of pixel - * data. + * data. * @param opaque optional user data, which will be passed on to the callback, - * may be NULL. - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * JxlDecoderSetImageOutBuffer already set. + * may be NULL. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such + * as @ref JxlDecoderSetImageOutBuffer already set. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetImageOutCallback(JxlDecoder* dec, const JxlPixelFormat* format, JxlImageOutCallback callback, void* opaque); -/** Similar to @c JxlDecoderSetImageOutCallback except that the callback is +/** Similar to @ref JxlDecoderSetImageOutCallback except that the callback is * allowed an initialization phase during which it is informed of how many * threads will call it concurrently, and those calls are further informed of * which thread they are occurring in. * * @param dec decoder object * @param format format of the pixels. Object owned by user; its contents are - * copied internally. + * copied internally. * @param init_callback initialization callback. * @param run_callback the callback function receiving partial scanlines of - * pixel data. + * pixel data. * @param destroy_callback clean-up callback invoked after all calls to @c - * run_callback. May be NULL if no clean-up is necessary. + * run_callback. May be NULL if no clean-up is necessary. * @param init_opaque optional user data passed to @c init_callback, may be NULL - * (unlike the return value from @c init_callback which may only be NULL if - * initialization failed). - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as @c - * JxlDecoderSetImageOutBuffer having already been called. + * (unlike the return value from @c init_callback which may only be NULL if + * initialization failed). + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such + * as @ref JxlDecoderSetImageOutBuffer having already been called. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetMultithreadedImageOutCallback( JxlDecoder* dec, const JxlPixelFormat* format, @@ -1039,18 +1151,18 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderSetMultithreadedImageOutCallback( /** * Returns the minimum size in bytes of an extra channel pixel buffer for the - * given format. This is the buffer for JxlDecoderSetExtraChannelBuffer. + * given format. This is the buffer for @ref JxlDecoderSetExtraChannelBuffer. * Requires the basic image information is available in the decoder. * * @param dec decoder object * @param format format of the pixels. The num_channels value is ignored and is - * always treated to be 1. + * always treated to be 1. * @param size output value, buffer size in bytes - * @param index which extra channel to get, matching the index used in @see - * JxlDecoderGetExtraChannelInfo. Must be smaller than num_extra_channels in the - * associated JxlBasicInfo. - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * information not available yet or invalid index. + * @param index which extra channel to get, matching the index used in @ref + * JxlDecoderGetExtraChannelInfo. Must be smaller than num_extra_channels in + * the associated JxlBasicInfo. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * information not available yet or invalid index. */ JXL_EXPORT JxlDecoderStatus JxlDecoderExtraChannelBufferSize( const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size, @@ -1058,32 +1170,33 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderExtraChannelBufferSize( /** * Sets the buffer to write an extra channel to. This can be set when - * the JXL_DEC_FRAME or JXL_DEC_NEED_IMAGE_OUT_BUFFER event occurs, and applies - * only for the current frame. The size of the buffer must be at least as large - * as given by JxlDecoderExtraChannelBufferSize. The buffer follows the format - * described by JxlPixelFormat, but where num_channels is 1. The buffer is owned - * by the caller. The amount of extra channels is given by the - * num_extra_channels field in the associated JxlBasicInfo, and the information - * of individual extra channels can be queried with @see + * the @ref JXL_DEC_FRAME or @ref JXL_DEC_NEED_IMAGE_OUT_BUFFER event occurs, + * and applies only for the current frame. The size of the buffer must be at + * least as large as given by @ref JxlDecoderExtraChannelBufferSize. The buffer + * follows the format described by JxlPixelFormat, but where num_channels is 1. + * The buffer is owned by the caller. The amount of extra channels is given by + * the num_extra_channels field in the associated JxlBasicInfo, and the + * information of individual extra channels can be queried with @ref * JxlDecoderGetExtraChannelInfo. To get multiple extra channels, this function * must be called multiple times, once for each wanted index. Not all images * have extra channels. The alpha channel is an extra channel and can be gotten - * as part of the color channels when using an RGBA pixel buffer with - * JxlDecoderSetImageOutBuffer, but additionally also can be gotten separately - * as extra channel. The color channels themselves cannot be gotten this way. + * as part of the color channels when using an RGBA pixel buffer with @ref + * JxlDecoderSetImageOutBuffer, but additionally also can be gotten + * separately as extra channel. The color channels themselves cannot be gotten + * this way. * * * @param dec decoder object * @param format format of the pixels. Object owned by user and its contents - * are copied internally. The num_channels value is ignored and is always - * treated to be 1. + * are copied internally. The num_channels value is ignored and is always + * treated to be 1. * @param buffer buffer type to output the pixel data to * @param size size of buffer in bytes - * @param index which extra channel to get, matching the index used in @see - * JxlDecoderGetExtraChannelInfo. Must be smaller than num_extra_channels in the - * associated JxlBasicInfo. - * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as - * size too small or invalid index. + * @param index which extra channel to get, matching the index used in @ref + * JxlDecoderGetExtraChannelInfo. Must be smaller than num_extra_channels in + * the associated JxlBasicInfo. + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * size too small or invalid index. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetExtraChannelBuffer(JxlDecoder* dec, const JxlPixelFormat* format, @@ -1092,79 +1205,82 @@ JxlDecoderSetExtraChannelBuffer(JxlDecoder* dec, const JxlPixelFormat* format, /** * Sets output buffer for reconstructed JPEG codestream. * - * The data is owned by the caller and may be used by the decoder until - * JxlDecoderReleaseJPEGBuffer is called or the decoder is destroyed or reset so - * must be kept alive until then. + * The data is owned by the caller and may be used by the decoder until @ref + * JxlDecoderReleaseJPEGBuffer is called or the decoder is destroyed or + * reset so must be kept alive until then. * - * If a JPEG buffer was set before and released with - * JxlDecoderReleaseJPEGBuffer, bytes that the decoder has already output should - * not be included, only the remaining bytes output must be set. + * If a JPEG buffer was set before and released with @ref + * JxlDecoderReleaseJPEGBuffer, bytes that the decoder has already output + * should not be included, only the remaining bytes output must be set. * * @param dec decoder object * @param data pointer to next bytes to write to * @param size amount of bytes available starting from data - * @return JXL_DEC_ERROR if output buffer was already set and - * JxlDecoderReleaseJPEGBuffer was not called on it, JXL_DEC_SUCCESS otherwise + * @return @ref JXL_DEC_ERROR if output buffer was already set and @ref + * JxlDecoderReleaseJPEGBuffer was not called on it, @ref JXL_DEC_SUCCESS + * otherwise */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetJPEGBuffer(JxlDecoder* dec, uint8_t* data, size_t size); /** - * Releases buffer which was provided with JxlDecoderSetJPEGBuffer. + * Releases buffer which was provided with @ref JxlDecoderSetJPEGBuffer. * - * Calling JxlDecoderReleaseJPEGBuffer is required whenever - * a buffer is already set and a new buffer needs to be added with - * JxlDecoderSetJPEGBuffer, but is not required before JxlDecoderDestroy or - * JxlDecoderReset. + * Calling @ref JxlDecoderReleaseJPEGBuffer is required whenever + * a buffer is already set and a new buffer needs to be added with @ref + * JxlDecoderSetJPEGBuffer, but is not required before @ref + * JxlDecoderDestroy or @ref JxlDecoderReset. * - * Calling JxlDecoderReleaseJPEGBuffer when no buffer is set is + * Calling @ref JxlDecoderReleaseJPEGBuffer when no buffer is set is * not an error and returns 0. * * @param dec decoder object * @return the amount of bytes the decoder has not yet written to of the data - * set by JxlDecoderSetJPEGBuffer, or 0 if no buffer is set or - * JxlDecoderReleaseJPEGBuffer was already called. + * set by @ref JxlDecoderSetJPEGBuffer, or 0 if no buffer is set or @ref + * JxlDecoderReleaseJPEGBuffer was already called. */ JXL_EXPORT size_t JxlDecoderReleaseJPEGBuffer(JxlDecoder* dec); /** * Sets output buffer for box output codestream. * - * The data is owned by the caller and may be used by the decoder until - * JxlDecoderReleaseBoxBuffer is called or the decoder is destroyed or reset so - * must be kept alive until then. + * The data is owned by the caller and may be used by the decoder until @ref + * JxlDecoderReleaseBoxBuffer is called or the decoder is destroyed or + * reset so must be kept alive until then. * - * If for the current box a box buffer was set before and released with - * JxlDecoderReleaseBoxBuffer, bytes that the decoder has already output should - * not be included, only the remaining bytes output must be set. + * If for the current box a box buffer was set before and released with @ref + * JxlDecoderReleaseBoxBuffer, bytes that the decoder has already output + * should not be included, only the remaining bytes output must be set. * - * The JxlDecoderReleaseBoxBuffer must be used at the next JXL_DEC_BOX event - * or final JXL_DEC_SUCCESS event to compute the size of the output box bytes. + * The @ref JxlDecoderReleaseBoxBuffer must be used at the next @ref JXL_DEC_BOX + * event or final @ref JXL_DEC_SUCCESS event to compute the size of the output + * box bytes. * * @param dec decoder object * @param data pointer to next bytes to write to * @param size amount of bytes available starting from data - * @return JXL_DEC_ERROR if output buffer was already set and - * JxlDecoderReleaseBoxBuffer was not called on it, JXL_DEC_SUCCESS otherwise + * @return @ref JXL_DEC_ERROR if output buffer was already set and @ref + * JxlDecoderReleaseBoxBuffer was not called on it, @ref JXL_DEC_SUCCESS + * otherwise */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetBoxBuffer(JxlDecoder* dec, uint8_t* data, size_t size); /** - * Releases buffer which was provided with JxlDecoderSetBoxBuffer. + * Releases buffer which was provided with @ref JxlDecoderSetBoxBuffer. * - * Calling JxlDecoderReleaseBoxBuffer is required whenever - * a buffer is already set and a new buffer needs to be added with - * JxlDecoderSetBoxBuffer, but is not required before JxlDecoderDestroy or - * JxlDecoderReset. + * Calling @ref JxlDecoderReleaseBoxBuffer is required whenever + * a buffer is already set and a new buffer needs to be added with @ref + * JxlDecoderSetBoxBuffer, but is not required before @ref + * JxlDecoderDestroy or @ref JxlDecoderReset. * - * Calling JxlDecoderReleaseBoxBuffer when no buffer is set is + * Calling @ref JxlDecoderReleaseBoxBuffer when no buffer is set is * not an error and returns 0. * * @param dec decoder object * @return the amount of bytes the decoder has not yet written to of the data - * set by JxlDecoderSetBoxBuffer, or 0 if no buffer is set or - * JxlDecoderReleaseBoxBuffer was already called. + * set by @ref JxlDecoderSetBoxBuffer, or 0 if no buffer is set or @ref + * JxlDecoderReleaseBoxBuffer was already called. */ JXL_EXPORT size_t JxlDecoderReleaseBoxBuffer(JxlDecoder* dec); @@ -1177,66 +1293,68 @@ JXL_EXPORT size_t JxlDecoderReleaseBoxBuffer(JxlDecoder* dec); * finished. * * The default mode is raw. This setting can only be changed before decoding, or - * directly after a JXL_DEC_BOX event, and is remembered until the decoder is - * reset or destroyed. + * directly after a @ref JXL_DEC_BOX event, and is remembered until the decoder + * is reset or destroyed. * * Enabling decompressed mode requires Brotli support from the library. * * @param dec decoder object * @param decompress JXL_TRUE to transparently decompress, JXL_FALSE to get - * boxes in raw mode. - * @return JXL_DEC_ERROR if decompressed mode is set and Brotli is not - * available, JXL_DEC_SUCCESS otherwise. + * boxes in raw mode. + * @return @ref JXL_DEC_ERROR if decompressed mode is set and Brotli is not + * available, @ref JXL_DEC_SUCCESS otherwise. */ JXL_EXPORT JxlDecoderStatus JxlDecoderSetDecompressBoxes(JxlDecoder* dec, JXL_BOOL decompress); /** - * Outputs the type of the current box, after a JXL_DEC_BOX event occured, as 4 - * characters without null termination character. In case of a compressed "brob" - * box, this will return "brob" if the decompressed argument is JXL_FALSE, or - * the underlying box type if the decompressed argument is JXL_TRUE. + * Outputs the type of the current box, after a @ref JXL_DEC_BOX event occured, + * as 4 characters without null termination character. In case of a compressed + * "brob" box, this will return "brob" if the decompressed argument is + * JXL_FALSE, or the underlying box type if the decompressed argument is + * JXL_TRUE. * * The following box types are currently described in ISO/IEC 18181-2: - * - "Exif": a box with EXIF metadata. Starts with a 4-byte tiff header - * offset (big-endian uint32) that indicates the start of the actual EXIF data - * (which starts with a tiff header). Usually the offset will be zero and the - * EXIF data starts immediately after the offset field. The Exif orientation - * should be ignored by applications; the JPEG XL codestream orientation takes - * precedence and libjxl will by default apply the correct orientation - * automatically (see JxlDecoderSetKeepOrientation). - * - "xml ": a box with XML data, in particular XMP metadata. - * - "jumb": a JUMBF superbox (JPEG Universal Metadata Box Format, ISO/IEC - * 19566-5). - * - "JXL ": mandatory signature box, must come first, 12 bytes long including - * the box header - * - "ftyp": a second mandatory signature box, must come second, 20 bytes long - * including the box header - * - "jxll": a JXL level box. This indicates if the codestream is level 5 or - * level 10 compatible. If not present, it is level 5. Level 10 allows more - * features such as very high image resolution and bit-depths above 16 bits - * per channel. Added automatically by the encoder when - * JxlEncoderSetCodestreamLevel is used - * - "jxlc": a box with the image codestream, in case the codestream is not - * split across multiple boxes. The codestream contains the JPEG XL image - * itself, including the basic info such as image dimensions, ICC color - * profile, and all the pixel data of all the image frames. - * - "jxlp": a codestream box in case it is split across multiple boxes. - * The contents are the same as in case of a jxlc box, when concatenated. - * - "brob": a Brotli-compressed box, which otherwise represents an existing - * type of box such as Exif or "xml ". When JxlDecoderSetDecompressBoxes is - * set to JXL_TRUE, these boxes will be transparently decompressed by the - * decoder. - * - "jxli": frame index box, can list the keyframes in case of a JXL animation, - * allowing the decoder to jump to individual frames more efficiently. - * - "jbrd": JPEG reconstruction box, contains the information required to - * byte-for-byte losslessly recontruct a JPEG-1 image. The JPEG DCT - * coefficients (pixel content) themselves as well as the ICC profile are - * encoded in the JXL codestream (jxlc or jxlp) itself. EXIF, XMP and JUMBF - * metadata is encoded in the corresponding boxes. The jbrd box itself - * contains information such as the remaining app markers of the JPEG-1 file - * and everything else required to fit the information together into the - * exact original JPEG file. + * - "Exif": a box with EXIF metadata. Starts with a 4-byte tiff header offset + * (big-endian uint32) that indicates the start of the actual EXIF data + * (which starts with a tiff header). Usually the offset will be zero and the + * EXIF data starts immediately after the offset field. The Exif orientation + * should be ignored by applications; the JPEG XL codestream orientation + * takes precedence and libjxl will by default apply the correct orientation + * automatically (see @ref JxlDecoderSetKeepOrientation). + * - "xml ": a box with XML data, in particular XMP metadata. + * - "jumb": a JUMBF superbox (JPEG Universal Metadata Box Format, ISO/IEC + * 19566-5). + * - "JXL ": mandatory signature box, must come first, 12 bytes long including + * the box header + * - "ftyp": a second mandatory signature box, must come second, 20 bytes long + * including the box header + * - "jxll": a JXL level box. This indicates if the codestream is level 5 or + * level 10 compatible. If not present, it is level 5. Level 10 allows more + * features such as very high image resolution and bit-depths above 16 bits + * per channel. Added automatically by the encoder when + * JxlEncoderSetCodestreamLevel is used + * - "jxlc": a box with the image codestream, in case the codestream is not + * split across multiple boxes. The codestream contains the JPEG XL image + * itself, including the basic info such as image dimensions, ICC color + * profile, and all the pixel data of all the image frames. + * - "jxlp": a codestream box in case it is split across multiple boxes. + * The contents are the same as in case of a jxlc box, when concatenated. + * - "brob": a Brotli-compressed box, which otherwise represents an existing + * type of box such as Exif or "xml ". When @ref JxlDecoderSetDecompressBoxes + * is set to JXL_TRUE, these boxes will be transparently decompressed by the + * decoder. + * - "jxli": frame index box, can list the keyframes in case of a JPEG XL + * animation allowing the decoder to jump to individual frames more + * efficiently. + * - "jbrd": JPEG reconstruction box, contains the information required to + * byte-for-byte losslessly recontruct a JPEG-1 image. The JPEG DCT + * coefficients (pixel content) themselves as well as the ICC profile are + * encoded in the JXL codestream (jxlc or jxlp) itself. EXIF, XMP and JUMBF + * metadata is encoded in the corresponding boxes. The jbrd box itself + * contains information such as the remaining app markers of the JPEG-1 file + * and everything else required to fit the information together into the + * exact original JPEG file. * * Other application-specific boxes can exist. Their typename should not begin * with "jxl" or "JXL" or conflict with other existing typenames. @@ -1250,62 +1368,73 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderSetDecompressBoxes(JxlDecoder* dec, * @param dec decoder object * @param type buffer to copy the type into * @param decompressed which box type to get: JXL_FALSE to get the raw box type, - * which can be "brob", JXL_TRUE, get the underlying box type. - * @return JXL_DEC_SUCCESS if the value is available, JXL_DEC_ERROR if not, for - * example the JXL file does not use the container format. + * which can be "brob", JXL_TRUE, get the underlying box type. + * @return @ref JXL_DEC_SUCCESS if the value is available, @ref JXL_DEC_ERROR if + * not, for example the JXL file does not use the container format. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetBoxType(JxlDecoder* dec, JxlBoxType type, JXL_BOOL decompressed); /** - * Returns the size of a box as it appears in the container file, after the + * Returns the size of a box as it appears in the container file, after the @ref * JXL_DEC_BOX event. For a non-compressed box, this is the size of the * contents, excluding the 4 bytes indicating the box type. For a compressed * "brob" box, this is the size of the compressed box contents plus the * additional 4 byte indicating the underlying box type, but excluding the 4 * bytes indicating "brob". This function gives the size of the data that will * be written in the output buffer when getting boxes in the default raw - * compressed mode. When JxlDecoderSetDecompressBoxes is enabled, the return - * value of function does not change, and the decompressed size is not known - * before it has already been decompressed and output. + * compressed mode. When @ref JxlDecoderSetDecompressBoxes is enabled, the + * return value of function does not change, and the decompressed size is not + * known before it has already been decompressed and output. * * @param dec decoder object * @param size raw size of the box in bytes - * @return JXL_DEC_ERROR if no box size is available, JXL_DEC_SUCCESS otherwise. + * @return @ref JXL_DEC_ERROR if no box size is available, @ref JXL_DEC_SUCCESS + * otherwise. */ JXL_EXPORT JxlDecoderStatus JxlDecoderGetBoxSizeRaw(const JxlDecoder* dec, uint64_t* size); /** - * Configures at which progressive steps in frame decoding the @ref - * JXL_DEC_FRAME_PROGRESSION event occurs. By default, this is 0. The detail - * values mean: 0 = only trigger for the DC image, the 8x8th lower resolution - * image. 1 = also trigger when a full pass of groups is ready. Higher values - * indicate more steps but are not yet implemented. Higher values always include - * the events of lower values as well. + * Configures at which progressive steps in frame decoding these @ref + * JXL_DEC_FRAME_PROGRESSION event occurs. The default value for the level + * of detail if this function is never called is `kDC`. + * + * @param dec decoder object + * @param detail at which level of detail to trigger @ref + * JXL_DEC_FRAME_PROGRESSION + * @return @ref JXL_DEC_SUCCESS on success, @ref JXL_DEC_ERROR on error, such as + * an invalid value for the progressive detail. + */ +JXL_EXPORT JxlDecoderStatus +JxlDecoderSetProgressiveDetail(JxlDecoder* dec, JxlProgressiveDetail detail); + +/** + * Returns the intended downsampling ratio for the progressive frame produced + * by @ref JxlDecoderFlushImage after the latest @ref JXL_DEC_FRAME_PROGRESSION + * event. * * @param dec decoder object - * @param detail at which level of detail to trigger JXL_DEC_FRAME_PROGRESSION + * @return The intended downsampling ratio, can be 1, 2, 4 or 8. */ -JXL_EXPORT void JxlDecoderSetProgressiveDetail(JxlDecoder* dec, - uint32_t detail); +JXL_EXPORT size_t JxlDecoderGetIntendedDownsamplingRatio(JxlDecoder* dec); /** * Outputs progressive step towards the decoded image so far when only partial - * input was received. If the flush was successful, the buffer set with + * input was received. If the flush was successful, the buffer set with @ref * JxlDecoderSetImageOutBuffer will contain partial image data. * - * Can be called when JxlDecoderProcessInput returns JXL_DEC_NEED_MORE_INPUT, - * after the JXL_DEC_FRAME event already occurred and before the - * JXL_DEC_FULL_IMAGE event occurred for a frame. + * Can be called when @ref JxlDecoderProcessInput returns @ref + * JXL_DEC_NEED_MORE_INPUT, after the @ref JXL_DEC_FRAME event already occurred + * and before the @ref JXL_DEC_FULL_IMAGE event occurred for a frame. * * @param dec decoder object - * @return JXL_DEC_SUCCESS if image data was flushed to the output buffer, or - * JXL_DEC_ERROR when no flush was done, e.g. if not enough image data was - * available yet even for flush, or no output buffer was set yet. An error is - * not fatal, it only indicates no flushed image is available now, regular, - * decoding can still be performed. + * @return @ref JXL_DEC_SUCCESS if image data was flushed to the output buffer, + * or @ref JXL_DEC_ERROR when no flush was done, e.g. if not enough image + * data was available yet even for flush, or no output buffer was set yet. + * This error is not fatal, it only indicates no flushed image is available + * right now. Regular decoding can still be performed. */ JXL_EXPORT JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec); diff --git a/media/libjxl/src/lib/include/jxl/encode.h b/media/libjxl/src/lib/include/jxl/encode.h index eb0d6c8370..4813e3b7cf 100644 --- a/media/libjxl/src/lib/include/jxl/encode.h +++ b/media/libjxl/src/lib/include/jxl/encode.h @@ -13,8 +13,8 @@ #ifndef JXL_ENCODE_H_ #define JXL_ENCODE_H_ +#include "jxl/cms_interface.h" #include "jxl/codestream_header.h" -#include "jxl/decode.h" #include "jxl/jxl_export.h" #include "jxl/memory_manager.h" #include "jxl/parallel_runner.h" @@ -73,15 +73,60 @@ typedef enum { /** DEPRECATED: the encoder does not return this status and there is no need * to handle or expect it. + * Instead, JXL_ENC_ERROR is returned with error condition + * JXL_ENC_ERR_NOT_SUPPORTED. */ JXL_ENC_NOT_SUPPORTED = 3, } JxlEncoderStatus; /** - * Id of encoder options for a frame. This includes options such as the - * image quality and compression speed for this frame. This does not include - * non-frame related encoder options such as for boxes. + * Error conditions: + * API usage errors have the 0x80 bit set to 1 + * Other errors have the 0x80 bit set to 0 + */ +typedef enum { + /** No error + */ + JXL_ENC_ERR_OK = 0, + + /** Generic encoder error due to unspecified cause + */ + JXL_ENC_ERR_GENERIC = 1, + + /** Out of memory + * TODO(jon): actually catch this and return this error + */ + JXL_ENC_ERR_OOM = 2, + + /** JPEG bitstream reconstruction data could not be + * represented (e.g. too much tail data) + */ + JXL_ENC_ERR_JBRD = 3, + + /** Input is invalid (e.g. corrupt JPEG file or ICC profile) + */ + JXL_ENC_ERR_BAD_INPUT = 4, + + /** The encoder doesn't (yet) support this. Either no version of libjxl + * supports this, and the API is used incorrectly, or the libjxl version + * should have been checked before trying to do this. + */ + JXL_ENC_ERR_NOT_SUPPORTED = 0x80, + + /** The encoder API is used in an incorrect way. + * In this case, a debug build of libjxl should output a specific error + * message. (if not, please open an issue about it) + */ + JXL_ENC_ERR_API_USAGE = 0x81, + +} JxlEncoderError; + +/** + * Id of encoder options for a frame. This includes options such as setting + * encoding effort/speed or overriding the use of certain coding tools, for this + * frame. This does not include non-frame related encoder options such as for + * boxes. */ typedef enum { /** Sets encoder effort/speed level without affecting decoding speed. Valid @@ -236,9 +281,12 @@ typedef enum { */ JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM = 24, - /** Color space for modular encoding: -1=default, 0-35=reverse color transform + /** Reversible color transform for modular encoding: -1=default, 0-41=RCT * index, e.g. index 0 = none, index 6 = YCoCg. - * The default behavior is to try several, depending on the speed setting. + * If this option is set to a non-default value, the RCT will be globally + * applied to the whole frame. + * The default behavior is to try several RCTs locally per modular group, + * depending on the speed and distance setting. */ JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE = 25, @@ -355,6 +403,15 @@ JxlEncoderSetParallelRunner(JxlEncoder* enc, JxlParallelRunner parallel_runner, void* parallel_runner_opaque); /** + * Get the (last) error code in case JXL_ENC_ERROR was returned. + * + * @param enc encoder object. + * @return the JxlEncoderError that caused the (last) JXL_ENC_ERROR to be + * returned. + */ +JXL_EXPORT JxlEncoderError JxlEncoderGetError(JxlEncoder* enc); + +/** * Encodes JPEG XL file using the available bytes. @p *avail_out indicates how * many output bytes are available, and @p *next_out points to the input bytes. * *avail_out will be decremented by the amount of bytes that have been @@ -519,12 +576,12 @@ JxlEncoderAddJPEGFrame(const JxlEncoderFrameSettings* frame_settings, * If the image has alpha, and alpha is not passed here, it will implicitly be * set to all-opaque (an alpha value of 1.0 everywhere). * - * The color profile of the pixels depends on the value of uses_original_profile - * in the JxlBasicInfo. If true, the pixels are assumed to be encoded in the - * original profile that is set with JxlEncoderSetColorEncoding or - * JxlEncoderSetICCProfile. If false, the pixels are assumed to be nonlinear - * sRGB for integer data types (JXL_TYPE_UINT8, JXL_TYPE_UINT16), and linear - * sRGB for floating point data types (JXL_TYPE_FLOAT16, JXL_TYPE_FLOAT). + * The pixels are assumed to be encoded in the original profile that is set with + * JxlEncoderSetColorEncoding or JxlEncoderSetICCProfile. If none of these + * functions were used, the pixels are assumed to be nonlinear sRGB for integer + * data types (JXL_TYPE_UINT8, JXL_TYPE_UINT16), and linear sRGB for floating + * point data types (JXL_TYPE_FLOAT16, JXL_TYPE_FLOAT). + * * Sample values in floating-point pixel formats are allowed to be outside the * nominal range, e.g. to represent out-of-sRGB-gamut colors in the * uses_original_profile=false case. They are however not allowed to be NaN or @@ -715,6 +772,7 @@ JXL_EXPORT void JxlEncoderCloseInput(JxlEncoder* enc); * is an alternative to JxlEncoderSetICCProfile and only one of these two must * be used. This one sets the color encoding as a @ref JxlColorEncoding, while * the other sets it as ICC binary data. + * Must be called after JxlEncoderSetBasicInfo. * * @param enc encoder object. * @param color color encoding. Object owned by the caller and its contents are @@ -730,6 +788,7 @@ JxlEncoderSetColorEncoding(JxlEncoder* enc, const JxlColorEncoding* color); * ICC color profile. This is an alternative to JxlEncoderSetColorEncoding and * only one of these two must be used. This one sets the color encoding as ICC * binary data, while the other defines it as a @ref JxlColorEncoding. + * Must be called after JxlEncoderSetBasicInfo. * * @param enc encoder object. * @param icc_profile bytes of the original ICC profile @@ -852,7 +911,25 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelName(JxlEncoder* enc, */ JXL_EXPORT JxlEncoderStatus JxlEncoderFrameSettingsSetOption( JxlEncoderFrameSettings* frame_settings, JxlEncoderFrameSettingId option, - int32_t value); + int64_t value); + +/** + * Sets a frame-specific option of float type to the encoder options. + * The JxlEncoderFrameSettingId argument determines which option is set. + * + * @param frame_settings set of options and metadata for this frame. Also + * includes reference to the encoder object. + * @param option ID of the option to set. + * @param value Float value to set for this option. + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR in + * case of an error, such as invalid or unknown option id, or invalid integer + * value for the given option. If an error is returned, the state of the + * JxlEncoderFrameSettings object is still valid and is the same as before this + * function was called. + */ +JXL_EXPORT JxlEncoderStatus JxlEncoderFrameSettingsSetFloatOption( + JxlEncoderFrameSettings* frame_settings, JxlEncoderFrameSettingId option, + float value); /** Forces the encoder to use the box-based container format (BMFF) even * when not necessary. @@ -893,8 +970,8 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderStoreJPEGMetadata(JxlEncoder* enc, JXL_BOOL store_jpeg_metadata); /** Sets the feature level of the JPEG XL codestream. Valid values are 5 and - * 10. Keeping the default value of 5 is recommended for compatibility with all - * decoders. + * 10, or -1 (to choose automatically). Using the minimum required level, or + * level 5 in most cases, is recommended for compatibility with all decoders. * * Level 5: for end-user image delivery, this level is the most widely * supported level by image decoders and the recommended level to use unless a @@ -910,16 +987,19 @@ JxlEncoderStoreJPEGMetadata(JxlEncoder* enc, JXL_BOOL store_jpeg_metadata); * 5 limitations, allows CMYK color and up to 32 bits per color channel, but * may be less widely supported. * - * The default value is 5. To use level 10 features, the setting must be - * explicitly set to 10, the encoder will not automatically enable it. If - * incompatible parameters such as too high image resolution for the current - * level are set, the encoder will return an error. For internal coding tools, - * the encoder will only use those compatible with the level setting. + * The default value is -1. This means the encoder will automatically choose + * between level 5 and level 10 based on what information is inside the @ref + * JxlBasicInfo structure. Do note that some level 10 features, particularly + * those used by animated JPEG XL codestreams, might require level 10, even + * though the @ref JxlBasicInfo only suggests level 5. In this case, the level + * must be explicitly set to 10, otherwise the encoder will return an error. + * The encoder will restrict internal encoding choices to those compatible with + * the level setting. * * This setting can only be set at the beginning, before encoding starts. * * @param enc encoder object. - * @param level the level value to set, must be 5 or 10. + * @param level the level value to set, must be -1, 5, or 10. * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR * otherwise. */ diff --git a/media/libjxl/src/lib/include/jxl/types.h b/media/libjxl/src/lib/include/jxl/types.h index ef804edcf1..722780fe8d 100644 --- a/media/libjxl/src/lib/include/jxl/types.h +++ b/media/libjxl/src/lib/include/jxl/types.h @@ -115,6 +115,33 @@ typedef struct { */ typedef char JxlBoxType[4]; +/** Types of progressive detail. + * Setting a progressive detail with value N implies all progressive details + * with smaller or equal value. Currently only the following level of + * progressive detail is implemented: + * - kDC (which implies kFrames) + * - kLastPasses (which implies kDC and kFrames) + * - kPasses (which implies kLastPasses, kDC and kFrames) + */ +typedef enum { + // after completed kRegularFrames + kFrames = 0, + // after completed DC (1:8) + kDC = 1, + // after completed AC passes that are the last pass for their resolution + // target. + kLastPasses = 2, + // after completed AC passes that are not the last pass for their resolution + // target. + kPasses = 3, + // during DC frame when lower resolution are completed (1:32, 1:16) + kDCProgressive = 4, + // after completed groups + kDCGroups = 5, + // after completed groups + kGroups = 6, +} JxlProgressiveDetail; + #if defined(__cplusplus) || defined(c_plusplus) } #endif diff --git a/media/libjxl/src/lib/jxl.cmake b/media/libjxl/src/lib/jxl.cmake index 81fa04f98c..72c07f48e5 100644 --- a/media/libjxl/src/lib/jxl.cmake +++ b/media/libjxl/src/lib/jxl.cmake @@ -62,8 +62,12 @@ set(JPEGXL_INTERNAL_SOURCES_DEC jxl/compressed_dc.cc jxl/compressed_dc.h jxl/convolve-inl.h - jxl/convolve.cc jxl/convolve.h + jxl/convolve_separable5.cc + jxl/convolve_separable7.cc + jxl/convolve_slow.cc + jxl/convolve_symmetric3.cc + jxl/convolve_symmetric5.cc jxl/dct-inl.h jxl/dct_block-inl.h jxl/dct_scales.cc @@ -90,9 +94,9 @@ set(JPEGXL_INTERNAL_SOURCES_DEC jxl/dec_modular.h jxl/dec_noise.cc jxl/dec_noise.h - jxl/dec_params.h jxl/dec_patch_dictionary.cc jxl/dec_patch_dictionary.h + jxl/dec_tone_mapping-inl.h jxl/dec_transforms-inl.h jxl/dec_xyb-inl.h jxl/dec_xyb.cc @@ -106,7 +110,6 @@ set(JPEGXL_INTERNAL_SOURCES_DEC jxl/entropy_coder.h jxl/epf.cc jxl/epf.h - jxl/exif.cc jxl/exif.h jxl/fast_dct-inl.h jxl/fast_dct.cc @@ -198,6 +201,8 @@ set(JPEGXL_INTERNAL_SOURCES_DEC jxl/render_pipeline/stage_chroma_upsampling.h jxl/render_pipeline/stage_epf.cc jxl/render_pipeline/stage_epf.h + jxl/render_pipeline/stage_from_linear.cc + jxl/render_pipeline/stage_from_linear.h jxl/render_pipeline/stage_gaborish.cc jxl/render_pipeline/stage_gaborish.h jxl/render_pipeline/stage_noise.cc @@ -208,6 +213,10 @@ set(JPEGXL_INTERNAL_SOURCES_DEC jxl/render_pipeline/stage_splines.h jxl/render_pipeline/stage_spot.cc jxl/render_pipeline/stage_spot.h + jxl/render_pipeline/stage_to_linear.cc + jxl/render_pipeline/stage_to_linear.h + jxl/render_pipeline/stage_tone_mapping.cc + jxl/render_pipeline/stage_tone_mapping.h jxl/render_pipeline/stage_upsampling.cc jxl/render_pipeline/stage_upsampling.h jxl/render_pipeline/stage_write.cc @@ -235,8 +244,6 @@ set(JPEGXL_INTERNAL_SOURCES_ENC jxl/butteraugli/butteraugli.cc jxl/butteraugli/butteraugli.h jxl/butteraugli_wrapper.cc - jxl/dec_file.cc - jxl/dec_file.h jxl/enc_ac_strategy.cc jxl/enc_ac_strategy.h jxl/enc_adaptive_quantization.cc @@ -272,7 +279,6 @@ set(JPEGXL_INTERNAL_SOURCES_ENC jxl/enc_entropy_coder.h jxl/enc_external_image.cc jxl/enc_external_image.h - jxl/enc_fast_heuristics.cc jxl/enc_file.cc jxl/enc_file.h jxl/enc_frame.cc @@ -400,10 +406,10 @@ target_compile_options(jxl_dec-obj PRIVATE ${JPEGXL_INTERNAL_FLAGS}) target_compile_options(jxl_dec-obj PUBLIC ${JPEGXL_COVERAGE_FLAGS}) set_property(TARGET jxl_dec-obj PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(jxl_dec-obj PUBLIC - ${PROJECT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/include - $<TARGET_PROPERTY:hwy,INTERFACE_INCLUDE_DIRECTORIES> - $<TARGET_PROPERTY:brotlicommon-static,INTERFACE_INCLUDE_DIRECTORIES> + "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>" + "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>" + "$<BUILD_INTERFACE:$<TARGET_PROPERTY:hwy,INTERFACE_INCLUDE_DIRECTORIES>>" + "$<BUILD_INTERFACE:$<TARGET_PROPERTY:brotlicommon-static,INTERFACE_INCLUDE_DIRECTORIES>>" ) target_compile_definitions(jxl_dec-obj PUBLIC ${OBJ_COMPILE_DEFINITIONS} @@ -441,6 +447,9 @@ else () ) endif () +# Generate version.h +configure_file("jxl/version.h.in" "include/jxl/version.h") + # Headers for exporting/importing public headers include(GenerateExportHeader) set_target_properties(jxl_dec-obj PROPERTIES @@ -470,9 +479,9 @@ add_library(jxl_dec-static STATIC target_link_libraries(jxl_dec-static PUBLIC ${JPEGXL_COVERAGE_FLAGS} ${JPEGXL_DEC_INTERNAL_LIBS}) target_include_directories(jxl_dec-static PUBLIC - "${PROJECT_SOURCE_DIR}" - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_BINARY_DIR}/include") + "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>" + "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>" + "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>") # The list of objects in the static and shared libraries. set(JPEGXL_INTERNAL_OBJECTS @@ -491,9 +500,9 @@ add_library(jxl-static STATIC ${JPEGXL_INTERNAL_OBJECTS}) target_link_libraries(jxl-static PUBLIC ${JPEGXL_COVERAGE_FLAGS} ${JPEGXL_INTERNAL_LIBS}) target_include_directories(jxl-static PUBLIC - "${PROJECT_SOURCE_DIR}" - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_BINARY_DIR}/include") + "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>" + "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>" + "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>") # JXL_EXPORT is defined to "__declspec(dllimport)" automatically by CMake # in Windows builds when including headers from the C API and compiling from @@ -506,7 +515,7 @@ target_compile_definitions(jxl_dec-static INTERFACE -DJXL_EXPORT=) # TODO(deymo): Move TCMalloc linkage to the tools/ directory since the library # shouldn't do any allocs anyway. -if(${JPEGXL_ENABLE_TCMALLOC}) +if(JPEGXL_ENABLE_TCMALLOC) pkg_check_modules(TCMallocMinimal REQUIRED IMPORTED_TARGET libtcmalloc_minimal) # tcmalloc 2.8 has concurrency issues that makes it sometimes return nullptr @@ -526,15 +535,14 @@ endif() # JPEGXL_ENABLE_TCMALLOC # Install the static library too, but as jxl.a file without the -static except # in Windows. -if (NOT WIN32) +if (NOT WIN32 OR MINGW) set_target_properties(jxl-static PROPERTIES OUTPUT_NAME "jxl") set_target_properties(jxl_dec-static PROPERTIES OUTPUT_NAME "jxl_dec") endif() install(TARGETS jxl-static DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(TARGETS jxl_dec-static DESTINATION ${CMAKE_INSTALL_LIBDIR}) -if (((NOT DEFINED "${TARGET_SUPPORTS_SHARED_LIBS}") OR - TARGET_SUPPORTS_SHARED_LIBS) AND NOT JPEGXL_STATIC AND BUILD_SHARED_LIBS) +if (BUILD_SHARED_LIBS) # Public shared library. add_library(jxl SHARED ${JPEGXL_INTERNAL_OBJECTS}) @@ -543,8 +551,8 @@ target_link_libraries(jxl PUBLIC ${JPEGXL_COVERAGE_FLAGS}) target_link_libraries(jxl PRIVATE ${JPEGXL_INTERNAL_SHARED_LIBS}) # Shared library include path contains only the "include/" paths. target_include_directories(jxl PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_BINARY_DIR}/include") + "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>" + "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>") set_target_properties(jxl PROPERTIES VERSION ${JPEGXL_LIBRARY_VERSION} SOVERSION ${JPEGXL_LIBRARY_SOVERSION} @@ -591,7 +599,7 @@ foreach(target IN ITEMS jxl jxl_dec) # This hides the default visibility symbols from static libraries bundled into # the shared library. In particular this prevents exposing symbols from hwy # and skcms in the shared library. - if(${LINKER_SUPPORT_EXCLUDE_LIBS}) + if(LINKER_SUPPORT_EXCLUDE_LIBS) set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS " ${LINKER_EXCLUDE_LIBS_FLAG}") endif() @@ -607,8 +615,7 @@ install(TARGETS jxl else() add_library(jxl ALIAS jxl-static) add_library(jxl_dec ALIAS jxl_dec-static) -endif() # TARGET_SUPPORTS_SHARED_LIBS AND NOT JPEGXL_STATIC AND - # BUILD_SHARED_LIBS +endif() # BUILD_SHARED_LIBS # Add a pkg-config file for libjxl. set(JPEGXL_LIBRARY_REQUIRES @@ -616,6 +623,20 @@ set(JPEGXL_LIBRARY_REQUIRES if(NOT JPEGXL_ENABLE_SKCMS) set(JPEGXL_LIBRARY_REQUIRES "${JPEGXL_LIBRARY_REQUIRES} lcms2") endif() + +# Allow adding prefix if CMAKE_INSTALL_INCLUDEDIR not absolute. +if(IS_ABSOLUTE "${CMAKE_INSTALL_INCLUDEDIR}") + set(PKGCONFIG_TARGET_INCLUDES "${CMAKE_INSTALL_INCLUDEDIR}") +else() + set(PKGCONFIG_TARGET_INCLUDES "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") +endif() +# Allow adding prefix if CMAKE_INSTALL_LIBDIR not absolute. +if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(PKGCONFIG_TARGET_LIBS "${CMAKE_INSTALL_LIBDIR}") +else() + set(PKGCONFIG_TARGET_LIBS "\${exec_prefix}/${CMAKE_INSTALL_LIBDIR}") +endif() + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/jxl/libjxl.pc.in" "libjxl.pc" @ONLY) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libjxl.pc" diff --git a/media/libjxl/src/lib/jxl/alpha.cc b/media/libjxl/src/lib/jxl/alpha.cc index 77ac9021d7..f0ab39ac08 100644 --- a/media/libjxl/src/lib/jxl/alpha.cc +++ b/media/libjxl/src/lib/jxl/alpha.cc @@ -108,4 +108,13 @@ void UnpremultiplyAlpha(float* JXL_RESTRICT r, float* JXL_RESTRICT g, } } +void UnpremultiplyAlpha(float* JXL_RESTRICT rgba, size_t num_pixels) { + for (size_t x = 0, ix = 0; x < num_pixels; ++x, ix += 4) { + const float multiplier = 1.f / std::max(kSmallAlpha, rgba[ix + 3]); + rgba[ix] *= multiplier; + rgba[ix + 1] *= multiplier; + rgba[ix + 2] *= multiplier; + } +} + } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/alpha.h b/media/libjxl/src/lib/jxl/alpha.h index efb76c800f..f49790b582 100644 --- a/media/libjxl/src/lib/jxl/alpha.h +++ b/media/libjxl/src/lib/jxl/alpha.h @@ -60,6 +60,7 @@ void PremultiplyAlpha(float* JXL_RESTRICT r, float* JXL_RESTRICT g, void UnpremultiplyAlpha(float* JXL_RESTRICT r, float* JXL_RESTRICT g, float* JXL_RESTRICT b, const float* JXL_RESTRICT a, size_t num_pixels); +void UnpremultiplyAlpha(float* JXL_RESTRICT rgba, size_t num_pixels); } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/alpha_test.cc b/media/libjxl/src/lib/jxl/alpha_test.cc index d90bbd37d9..c643fbdc54 100644 --- a/media/libjxl/src/lib/jxl/alpha_test.cc +++ b/media/libjxl/src/lib/jxl/alpha_test.cc @@ -5,8 +5,7 @@ #include "lib/jxl/alpha.h" -#include "gmock/gmock.h" -#include "gtest/gtest.h" +#include "lib/jxl/test_utils.h" namespace jxl { namespace { diff --git a/media/libjxl/src/lib/jxl/ans_common.cc b/media/libjxl/src/lib/jxl/ans_common.cc index cc0d58b446..32a658fb43 100644 --- a/media/libjxl/src/lib/jxl/ans_common.cc +++ b/media/libjxl/src/lib/jxl/ans_common.cc @@ -12,11 +12,11 @@ namespace jxl { -std::vector<int> CreateFlatHistogram(int length, int total_count) { +std::vector<int32_t> CreateFlatHistogram(int length, int total_count) { JXL_ASSERT(length > 0); JXL_ASSERT(length <= total_count); const int count = total_count / length; - std::vector<int> result(length, count); + std::vector<int32_t> result(length, count); const int rem_counts = total_count % length; for (int i = 0; i < rem_counts; ++i) { ++result[i]; @@ -48,7 +48,7 @@ std::vector<int> CreateFlatHistogram(int length, int total_count) { // underfull nor overfull, and represents exactly two symbols. The overfull // entry might be either overfull or underfull, and is pushed into the // corresponding stack. -void InitAliasTable(std::vector<int> distribution, uint32_t range, +void InitAliasTable(std::vector<int32_t> distribution, uint32_t range, size_t log_alpha_size, AliasTable::Entry* JXL_RESTRICT a) { while (!distribution.empty() && distribution.back() == 0) { distribution.pop_back(); diff --git a/media/libjxl/src/lib/jxl/ans_common.h b/media/libjxl/src/lib/jxl/ans_common.h index 12ce1eff36..fb5058e310 100644 --- a/media/libjxl/src/lib/jxl/ans_common.h +++ b/media/libjxl/src/lib/jxl/ans_common.h @@ -32,7 +32,7 @@ static JXL_INLINE uint32_t GetPopulationCountPrecision(uint32_t logcount, // Returns a histogram where the counts are positive, differ by at most 1, // and add up to total_count. The bigger counts (if any) are at the beginning // of the histogram. -std::vector<int> CreateFlatHistogram(int length, int total_count); +std::vector<int32_t> CreateFlatHistogram(int length, int total_count); // An alias table implements a mapping from the [0, ANS_TAB_SIZE) range into // the [0, ANS_MAX_ALPHABET_SIZE) range, satisfying the following conditions: @@ -135,7 +135,7 @@ struct AliasTable { }; // Computes an alias table for a given distribution. -void InitAliasTable(std::vector<int> distribution, uint32_t range, +void InitAliasTable(std::vector<int32_t> distribution, uint32_t range, size_t log_alpha_size, AliasTable::Entry* JXL_RESTRICT a); } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/ans_test.cc b/media/libjxl/src/lib/jxl/ans_test.cc index 3d7eaf59cc..ca9883d372 100644 --- a/media/libjxl/src/lib/jxl/ans_test.cc +++ b/media/libjxl/src/lib/jxl/ans_test.cc @@ -112,7 +112,7 @@ void RoundtripRandomUnbalancedStream(int alphabet_size) { constexpr int kPrecision = 1 << 10; Rng rng(0); for (size_t i = 0; i < kReps; i++) { - std::vector<int> distributions[kNumHistograms]; + std::vector<int> distributions[kNumHistograms] = {}; for (int j = 0; j < kNumHistograms; j++) { distributions[j].resize(kPrecision); int symbol = 0; diff --git a/media/libjxl/src/lib/jxl/aux_out.cc b/media/libjxl/src/lib/jxl/aux_out.cc index 5624bf492c..d8ee9460f6 100644 --- a/media/libjxl/src/lib/jxl/aux_out.cc +++ b/media/libjxl/src/lib/jxl/aux_out.cc @@ -25,6 +25,12 @@ void AuxOut::Print(size_t num_inputs) const { printf("Average butteraugli iters: %10.2f\n", num_butteraugli_iters * 1.0 / num_inputs); + if (min_quant_rescale != 1.0 || max_quant_rescale != 1.0) { + printf("quant rescale range: %f .. %f\n", min_quant_rescale, + max_quant_rescale); + printf("bitrate error range: %.3f%% .. %.3f%%\n", + 100.0f * min_bitrate_error, 100.0f * max_bitrate_error); + } for (size_t i = 0; i < layers.size(); ++i) { if (layers[i].total_bits != 0) { diff --git a/media/libjxl/src/lib/jxl/aux_out.h b/media/libjxl/src/lib/jxl/aux_out.h index 23d498652f..7076603125 100644 --- a/media/libjxl/src/lib/jxl/aux_out.h +++ b/media/libjxl/src/lib/jxl/aux_out.h @@ -37,72 +37,54 @@ namespace jxl { enum { kLayerHeader = 0, kLayerTOC, + kLayerDictionary, + kLayerSplines, kLayerNoise, kLayerQuant, - kLayerDequantTables, - kLayerOrder, + kLayerModularTree, + kLayerModularGlobal, kLayerDC, + kLayerModularDcGroup, kLayerControlFields, + kLayerOrder, kLayerAC, kLayerACTokens, - kLayerDictionary, - kLayerDots, - kLayerSplines, - kLayerLossless, - kLayerModularGlobal, - kLayerModularDcGroup, kLayerModularAcGroup, - kLayerModularTree, - kLayerAlpha, - kLayerDepth, - kLayerExtraChannels, kNumImageLayers }; static inline const char* LayerName(size_t layer) { switch (layer) { case kLayerHeader: - return "headers"; + return "Headers"; case kLayerTOC: return "TOC"; + case kLayerDictionary: + return "Patches"; + case kLayerSplines: + return "Splines"; case kLayerNoise: - return "noise"; + return "Noise"; case kLayerQuant: - return "quantizer"; - case kLayerDequantTables: - return "quant tables"; - case kLayerOrder: - return "order"; + return "Quantizer"; + case kLayerModularTree: + return "ModularTree"; + case kLayerModularGlobal: + return "ModularGlobal"; case kLayerDC: return "DC"; + case kLayerModularDcGroup: + return "ModularDcGroup"; case kLayerControlFields: return "ControlFields"; + case kLayerOrder: + return "CoeffOrder"; case kLayerAC: - return "AC"; + return "ACHistograms"; case kLayerACTokens: return "ACTokens"; - case kLayerDictionary: - return "dictionary"; - case kLayerDots: - return "dots"; - case kLayerSplines: - return "splines"; - case kLayerLossless: - return "lossless"; - case kLayerModularGlobal: - return "modularGlobal"; - case kLayerModularDcGroup: - return "modularDcGroup"; case kLayerModularAcGroup: - return "modularAcGroup"; - case kLayerModularTree: - return "modularTree"; - case kLayerAlpha: - return "alpha"; - case kLayerDepth: - return "depth"; - case kLayerExtraChannels: - return "extra channels"; + return "ModularAcGroup"; default: JXL_ABORT("Invalid layer %d\n", static_cast<int>(layer)); } @@ -167,10 +149,22 @@ struct AuxOut { dc_pred_usage[i] += victim.dc_pred_usage[i]; dc_pred_usage_xb[i] += victim.dc_pred_usage_xb[i]; } + max_quant_rescale = std::max(max_quant_rescale, victim.max_quant_rescale); + min_quant_rescale = std::min(min_quant_rescale, victim.min_quant_rescale); + max_bitrate_error = std::max(max_bitrate_error, victim.max_bitrate_error); + min_bitrate_error = std::min(min_bitrate_error, victim.min_bitrate_error); } void Print(size_t num_inputs) const; + size_t TotalBits() const { + size_t total = 0; + for (const auto& layer : layers) { + total += layer.total_bits; + } + return total; + } + template <typename T> void DumpImage(const char* label, const Image3<T>& image) const { if (!dump_image) return; @@ -285,6 +279,11 @@ struct AuxOut { int num_butteraugli_iters = 0; + float max_quant_rescale = 1.0f; + float min_quant_rescale = 1.0f; + float min_bitrate_error = 0.0f; + float max_bitrate_error = 0.0f; + // If not empty, additional debugging information (e.g. debug images) is // saved in files with this prefix. std::string debug_prefix; diff --git a/media/libjxl/src/lib/jxl/base/cache_aligned.cc b/media/libjxl/src/lib/jxl/base/cache_aligned.cc index 411d414521..9a9cc585a1 100644 --- a/media/libjxl/src/lib/jxl/base/cache_aligned.cc +++ b/media/libjxl/src/lib/jxl/base/cache_aligned.cc @@ -71,8 +71,9 @@ void* CacheAligned::Allocate(const size_t payload_size, size_t offset) { // To avoid wasting space, the header resides at the end of `unused`, // which therefore cannot be empty (offset == 0). if (offset == 0) { - offset = kAlignment; // = round_up(sizeof(AllocationHeader), kAlignment) - static_assert(sizeof(AllocationHeader) <= kAlignment, "Else: round up"); + // SVE/RVV vectors can be large, so we cannot rely on them (including the + // padding at the end of AllocationHeader) to fit in kAlignment. + offset = hwy::RoundUpTo(sizeof(AllocationHeader), kAlignment); } #if JXL_USE_MMAP diff --git a/media/libjxl/src/lib/jxl/base/compiler_specific.h b/media/libjxl/src/lib/jxl/base/compiler_specific.h index 24bbf74754..7aa8b99151 100644 --- a/media/libjxl/src/lib/jxl/base/compiler_specific.h +++ b/media/libjxl/src/lib/jxl/base/compiler_specific.h @@ -10,6 +10,8 @@ #include <stdint.h> +#include "lib/jxl/base/sanitizer_definitions.h" + // #if is shorter and safer than #ifdef. *_VERSION are zero if not detected, // otherwise 100 * major + minor version. Note that other packages check for // #ifdef COMPILER_MSVC, so we cannot use that same name. @@ -75,6 +77,16 @@ #define JXL_MAYBE_UNUSED __attribute__((unused)) #endif +// MSAN execution won't hurt if some code it not inlined, but this can greatly +// improve compilation time. Unfortunately this macro can not be used just +// everywhere - inside header files it leads to "multiple definition" error; +// though it would be better not to have JXL_INLINE in header overall. +#if JXL_MEMORY_SANITIZER || JXL_ADDRESS_SANITIZER || JXL_THREAD_SANITIZER +#define JXL_MAYBE_INLINE JXL_MAYBE_UNUSED +#else +#define JXL_MAYBE_INLINE JXL_INLINE +#endif + #if JXL_COMPILER_MSVC // Unsupported, __assume is not the same. #define JXL_LIKELY(expr) expr diff --git a/media/libjxl/src/lib/jxl/base/data_parallel.h b/media/libjxl/src/lib/jxl/base/data_parallel.h index 2fc03de6e2..666925aea4 100644 --- a/media/libjxl/src/lib/jxl/base/data_parallel.h +++ b/media/libjxl/src/lib/jxl/base/data_parallel.h @@ -31,6 +31,9 @@ class ThreadPool { ThreadPool(const ThreadPool&) = delete; ThreadPool& operator&(const ThreadPool&) = delete; + JxlParallelRunner runner() const { return runner_; } + void* runner_opaque() const { return runner_opaque_; } + // Runs init_func(num_threads) followed by data_func(task, thread) on worker // thread(s) for every task in [begin, end). init_func() must return a Status // indicating whether the initialization succeeded. diff --git a/media/libjxl/src/lib/jxl/base/file_io.h b/media/libjxl/src/lib/jxl/base/file_io.h index cb42eae834..8c7777c945 100644 --- a/media/libjxl/src/lib/jxl/base/file_io.h +++ b/media/libjxl/src/lib/jxl/base/file_io.h @@ -77,7 +77,8 @@ template <typename ContainerType> static inline Status ReadFile(const std::string& pathname, ContainerType* JXL_RESTRICT bytes) { FileWrapper f(pathname, "rb"); - if (f == nullptr) return JXL_FAILURE("Failed to open file for reading"); + if (f == nullptr) + return JXL_FAILURE("Failed to open file for reading: %s", pathname.c_str()); // Get size of file in bytes const int64_t size = f.size(); diff --git a/media/libjxl/src/lib/jxl/base/random.h b/media/libjxl/src/lib/jxl/base/random.h index c8b426b1cb..663b88c95d 100644 --- a/media/libjxl/src/lib/jxl/base/random.h +++ b/media/libjxl/src/lib/jxl/base/random.h @@ -20,7 +20,8 @@ namespace jxl { struct Rng { explicit Rng(size_t seed) - : s{0x94D049BB133111EBull, 0xBF58476D1CE4E5B9ull + seed} {} + : s{static_cast<uint64_t>(0x94D049BB133111EBull), + static_cast<uint64_t>(0xBF58476D1CE4E5B9ull) + seed} {} // Xorshift128+ adapted from xorshift128+-inl.h uint64_t operator()() { diff --git a/media/libjxl/src/lib/jxl/blending_test.cc b/media/libjxl/src/lib/jxl/blending_test.cc index cede9ce664..e032b99a4e 100644 --- a/media/libjxl/src/lib/jxl/blending_test.cc +++ b/media/libjxl/src/lib/jxl/blending_test.cc @@ -3,11 +3,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -#include "gmock/gmock.h" -#include "gtest/gtest.h" #include "lib/extras/codec.h" -#include "lib/jxl/dec_file.h" #include "lib/jxl/image_test_utils.h" +#include "lib/jxl/test_utils.h" #include "lib/jxl/testdata.h" namespace jxl { @@ -20,9 +18,8 @@ TEST(BlendingTest, Crops) { const PaddedBytes compressed = ReadTestData("jxl/blending/cropped_traffic_light.jxl"); - DecompressParams dparams; CodecInOut decoded; - ASSERT_TRUE(DecodeFile(dparams, compressed, &decoded, pool)); + ASSERT_TRUE(test::DecodeFile({}, compressed, &decoded, pool)); ASSERT_THAT(decoded.frames, SizeIs(4)); int i = 0; diff --git a/media/libjxl/src/lib/jxl/butteraugli/butteraugli.cc b/media/libjxl/src/lib/jxl/butteraugli/butteraugli.cc index c08dd0536c..ee1a53013f 100644 --- a/media/libjxl/src/lib/jxl/butteraugli/butteraugli.cc +++ b/media/libjxl/src/lib/jxl/butteraugli/butteraugli.cc @@ -272,7 +272,20 @@ namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Abs; +using hwy::HWY_NAMESPACE::Div; +using hwy::HWY_NAMESPACE::Gt; +using hwy::HWY_NAMESPACE::IfThenElse; +using hwy::HWY_NAMESPACE::IfThenElseZero; +using hwy::HWY_NAMESPACE::Lt; +using hwy::HWY_NAMESPACE::Max; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::MulSub; +using hwy::HWY_NAMESPACE::Neg; +using hwy::HWY_NAMESPACE::Sub; using hwy::HWY_NAMESPACE::Vec; +using hwy::HWY_NAMESPACE::ZeroIfNegative; template <class D, class V> HWY_INLINE V MaximumClamp(D d, V v, double kMaxVal) { @@ -280,24 +293,26 @@ HWY_INLINE V MaximumClamp(D d, V v, double kMaxVal) { const V mul = Set(d, kMul); const V maxval = Set(d, kMaxVal); // If greater than maxval or less than -maxval, replace with if_*. - const V if_pos = MulAdd(v - maxval, mul, maxval); - const V if_neg = MulSub(v + maxval, mul, maxval); - const V pos_or_v = IfThenElse(v >= maxval, if_pos, v); - return IfThenElse(v < Neg(maxval), if_neg, pos_or_v); + const V if_pos = MulAdd(Sub(v, maxval), mul, maxval); + const V if_neg = MulSub(Add(v, maxval), mul, maxval); + const V pos_or_v = IfThenElse(Ge(v, maxval), if_pos, v); + return IfThenElse(Lt(v, Neg(maxval)), if_neg, pos_or_v); } // Make area around zero less important (remove it). template <class D, class V> HWY_INLINE V RemoveRangeAroundZero(const D d, const double kw, const V x) { const auto w = Set(d, kw); - return IfThenElse(x > w, x - w, IfThenElseZero(x < Neg(w), x + w)); + return IfThenElse(Gt(x, w), Sub(x, w), + IfThenElseZero(Lt(x, Neg(w)), Add(x, w))); } // Make area around zero more important (2x it until the limit). template <class D, class V> HWY_INLINE V AmplifyRangeAroundZero(const D d, const double kw, const V x) { const auto w = Set(d, kw); - return IfThenElse(x > w, x + w, IfThenElse(x < Neg(w), x - w, x + x)); + return IfThenElse(Gt(x, w), Add(x, w), + IfThenElse(Lt(x, Neg(w)), Sub(x, w), Add(x, x))); } // XybLowFreqToVals converts from low-frequency XYB space to the 'vals' space. @@ -316,9 +331,9 @@ HWY_INLINE void XybLowFreqToVals(const D d, const V& x, const V& y, const V bmul = Set(d, bmuli); const V y_to_b_mul = Set(d, y_to_b_muli); const V b = MulAdd(y_to_b_mul, y, b_arg); - *valb = b * bmul; - *valx = x * xmul; - *valy = y * ymul; + *valb = Mul(b, bmul); + *valx = Mul(x, xmul); + *valy = Mul(y, ymul); } void SuppressXByY(const ImageF& in_x, const ImageF& in_y, const double yw, @@ -341,8 +356,9 @@ void SuppressXByY(const ImageF& in_x, const ImageF& in_y, const double yw, for (size_t x = 0; x < xsize; x += Lanes(d)) { const auto vx = Load(d, row_x + x); const auto vy = Load(d, row_y + x); - const auto scaler = MulAdd(ywv / MulAdd(vy, vy, ywv), one_minus_s, sv); - Store(scaler * vx, d, row_out + x); + const auto scaler = + MulAdd(Div(ywv, MulAdd(vy, vy, ywv)), one_minus_s, sv); + Store(Mul(scaler, vx), d, row_out + x); } } } @@ -372,7 +388,7 @@ static void SeparateFrequencies(size_t xsize, size_t ysize, const float* BUTTERAUGLI_RESTRICT row_lf = ps.lf.ConstPlaneRow(i, y); float* BUTTERAUGLI_RESTRICT row_mf = ps.mf.PlaneRow(i, y); for (size_t x = 0; x < xsize; x += Lanes(d)) { - const auto mf = Load(d, row_xyb + x) - Load(d, row_lf + x); + const auto mf = Sub(Load(d, row_xyb + x), Load(d, row_lf + x)); Store(mf, d, row_mf + x); } } @@ -397,7 +413,7 @@ static void SeparateFrequencies(size_t xsize, size_t ysize, float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[0].Row(y); for (size_t x = 0; x < xsize; x += Lanes(d)) { auto mf = Load(d, row_mf + x); - auto hf = Load(d, row_hf + x) - mf; + auto hf = Sub(Load(d, row_hf + x), mf); mf = RemoveRangeAroundZero(d, kRemoveMfRange, mf); Store(mf, d, row_mf + x); Store(hf, d, row_hf + x); @@ -409,7 +425,7 @@ static void SeparateFrequencies(size_t xsize, size_t ysize, float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[1].Row(y); for (size_t x = 0; x < xsize; x += Lanes(d)) { auto mf = Load(d, row_mf + x); - auto hf = Load(d, row_hf + x) - mf; + auto hf = Sub(Load(d, row_hf + x), mf); mf = AmplifyRangeAroundZero(d, kAddMfRange, mf); Store(mf, d, row_mf + x); @@ -452,7 +468,7 @@ static void SeparateFrequencies(size_t xsize, size_t ysize, float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[0].Row(y); for (size_t x = 0; x < xsize; x += Lanes(d)) { auto hf = Load(d, row_hf + x); - auto uhf = Load(d, row_uhf + x) - hf; + auto uhf = Sub(Load(d, row_uhf + x), hf); hf = RemoveRangeAroundZero(d, kRemoveHfRange, hf); uhf = RemoveRangeAroundZero(d, kRemoveUhfRange, uhf); Store(hf, d, row_hf + x); @@ -467,12 +483,12 @@ static void SeparateFrequencies(size_t xsize, size_t ysize, auto hf = Load(d, row_hf + x); hf = MaximumClamp(d, hf, kMaxclampHf); - auto uhf = Load(d, row_uhf + x) - hf; + auto uhf = Sub(Load(d, row_uhf + x), hf); uhf = MaximumClamp(d, uhf, kMaxclampUhf); - uhf *= Set(d, kMulYUhf); + uhf = Mul(uhf, Set(d, kMulYUhf)); Store(uhf, d, row_uhf + x); - hf *= Set(d, kMulYHf); + hf = Mul(hf, Set(d, kMulYHf)); hf = AmplifyRangeAroundZero(d, kAddHfRange, hf); Store(hf, d, row_hf + x); } @@ -500,6 +516,25 @@ static void SeparateFrequencies(size_t xsize, size_t ysize, } } +namespace { +template <typename V> +BUTTERAUGLI_INLINE V Sum(V a, V b, V c, V d) { + return Add(Add(a, b), Add(c, d)); +} +template <typename V> +BUTTERAUGLI_INLINE V Sum(V a, V b, V c, V d, V e) { + return Sum(a, b, c, Add(d, e)); +} +template <typename V> +BUTTERAUGLI_INLINE V Sum(V a, V b, V c, V d, V e, V f, V g) { + return Sum(a, b, c, Sum(d, e, f, g)); +} +template <typename V> +BUTTERAUGLI_INLINE V Sum(V a, V b, V c, V d, V e, V f, V g, V h, V i) { + return Add(Add(Sum(a, b, c, d), Sum(e, f, g, h)), i); +} +} // namespace + template <class D> Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, const float* BUTTERAUGLI_RESTRICT d, const intptr_t xs) { @@ -508,52 +543,52 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, const auto center = LoadU(df, d); // x grows, y constant - const auto sum_yconst = LoadU(df, d - 4) + LoadU(df, d - 2) + center + - LoadU(df, d + 2) + LoadU(df, d + 4); + const auto sum_yconst = Sum(LoadU(df, d - 4), LoadU(df, d - 2), center, + LoadU(df, d + 2), LoadU(df, d + 4)); // Will return this, sum of all line kernels - auto retval = sum_yconst * sum_yconst; + auto retval = Mul(sum_yconst, sum_yconst); { // y grows, x constant - auto sum = LoadU(df, d - xs3 - xs) + LoadU(df, d - xs - xs) + center + - LoadU(df, d + xs + xs) + LoadU(df, d + xs3 + xs); + auto sum = Sum(LoadU(df, d - xs3 - xs), LoadU(df, d - xs - xs), center, + LoadU(df, d + xs + xs), LoadU(df, d + xs3 + xs)); retval = MulAdd(sum, sum, retval); } { // both grow - auto sum = LoadU(df, d - xs3 - 3) + LoadU(df, d - xs - xs - 2) + center + - LoadU(df, d + xs + xs + 2) + LoadU(df, d + xs3 + 3); + auto sum = Sum(LoadU(df, d - xs3 - 3), LoadU(df, d - xs - xs - 2), center, + LoadU(df, d + xs + xs + 2), LoadU(df, d + xs3 + 3)); retval = MulAdd(sum, sum, retval); } { // y grows, x shrinks - auto sum = LoadU(df, d - xs3 + 3) + LoadU(df, d - xs - xs + 2) + center + - LoadU(df, d + xs + xs - 2) + LoadU(df, d + xs3 - 3); + auto sum = Sum(LoadU(df, d - xs3 + 3), LoadU(df, d - xs - xs + 2), center, + LoadU(df, d + xs + xs - 2), LoadU(df, d + xs3 - 3)); retval = MulAdd(sum, sum, retval); } { // y grows -4 to 4, x shrinks 1 -> -1 - auto sum = LoadU(df, d - xs3 - xs + 1) + LoadU(df, d - xs - xs + 1) + - center + LoadU(df, d + xs + xs - 1) + - LoadU(df, d + xs3 + xs - 1); + auto sum = + Sum(LoadU(df, d - xs3 - xs + 1), LoadU(df, d - xs - xs + 1), center, + LoadU(df, d + xs + xs - 1), LoadU(df, d + xs3 + xs - 1)); retval = MulAdd(sum, sum, retval); } { // y grows -4 to 4, x grows -1 -> 1 - auto sum = LoadU(df, d - xs3 - xs - 1) + LoadU(df, d - xs - xs - 1) + - center + LoadU(df, d + xs + xs + 1) + - LoadU(df, d + xs3 + xs + 1); + auto sum = + Sum(LoadU(df, d - xs3 - xs - 1), LoadU(df, d - xs - xs - 1), center, + LoadU(df, d + xs + xs + 1), LoadU(df, d + xs3 + xs + 1)); retval = MulAdd(sum, sum, retval); } { // x grows -4 to 4, y grows -1 to 1 - auto sum = LoadU(df, d - 4 - xs) + LoadU(df, d - 2 - xs) + center + - LoadU(df, d + 2 + xs) + LoadU(df, d + 4 + xs); + auto sum = Sum(LoadU(df, d - 4 - xs), LoadU(df, d - 2 - xs), center, + LoadU(df, d + 2 + xs), LoadU(df, d + 4 + xs)); retval = MulAdd(sum, sum, retval); } { // x grows -4 to 4, y shrinks 1 to -1 - auto sum = LoadU(df, d - 4 + xs) + LoadU(df, d - 2 + xs) + center + - LoadU(df, d + 2 - xs) + LoadU(df, d + 4 - xs); + auto sum = Sum(LoadU(df, d - 4 + xs), LoadU(df, d - 2 + xs), center, + LoadU(df, d + 2 - xs), LoadU(df, d + 4 - xs)); retval = MulAdd(sum, sum, retval); } { @@ -566,8 +601,8 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, 6_____*___ 7______*__ 8_________ */ - auto sum = LoadU(df, d - xs3 - 2) + LoadU(df, d - xs - xs - 1) + center + - LoadU(df, d + xs + xs + 1) + LoadU(df, d + xs3 + 2); + auto sum = Sum(LoadU(df, d - xs3 - 2), LoadU(df, d - xs - xs - 1), center, + LoadU(df, d + xs + xs + 1), LoadU(df, d + xs3 + 2)); retval = MulAdd(sum, sum, retval); } { @@ -580,8 +615,8 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, 6___*_____ 7__*______ 8_________ */ - auto sum = LoadU(df, d - xs3 + 2) + LoadU(df, d - xs - xs + 1) + center + - LoadU(df, d + xs + xs - 1) + LoadU(df, d + xs3 - 2); + auto sum = Sum(LoadU(df, d - xs3 + 2), LoadU(df, d - xs - xs + 1), center, + LoadU(df, d + xs + xs - 1), LoadU(df, d + xs3 - 2)); retval = MulAdd(sum, sum, retval); } { @@ -594,8 +629,8 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, 6_______*_ 7_________ 8_________ */ - auto sum = LoadU(df, d - xs - xs - 3) + LoadU(df, d - xs - 2) + center + - LoadU(df, d + xs + 2) + LoadU(df, d + xs + xs + 3); + auto sum = Sum(LoadU(df, d - xs - xs - 3), LoadU(df, d - xs - 2), center, + LoadU(df, d + xs + 2), LoadU(df, d + xs + xs + 3)); retval = MulAdd(sum, sum, retval); } { @@ -608,8 +643,8 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, 6_*_______ 7_________ 8_________ */ - auto sum = LoadU(df, d - xs - xs + 3) + LoadU(df, d - xs + 2) + center + - LoadU(df, d + xs - 2) + LoadU(df, d + xs + xs - 3); + auto sum = Sum(LoadU(df, d - xs - xs + 3), LoadU(df, d - xs + 2), center, + LoadU(df, d + xs - 2), LoadU(df, d + xs + xs - 3)); retval = MulAdd(sum, sum, retval); } { @@ -623,8 +658,8 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, 7_________ 8_________ */ - auto sum = LoadU(df, d + xs + xs - 4) + LoadU(df, d + xs - 2) + center + - LoadU(df, d - xs + 2) + LoadU(df, d - xs - xs + 4); + auto sum = Sum(LoadU(df, d + xs + xs - 4), LoadU(df, d + xs - 2), center, + LoadU(df, d - xs + 2), LoadU(df, d - xs - xs + 4)); retval = MulAdd(sum, sum, retval); } { @@ -637,8 +672,8 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, 6________* 7_________ 8_________ */ - auto sum = LoadU(df, d - xs - xs - 4) + LoadU(df, d - xs - 2) + center + - LoadU(df, d + xs + 2) + LoadU(df, d + xs + xs + 4); + auto sum = Sum(LoadU(df, d - xs - xs - 4), LoadU(df, d - xs - 2), center, + LoadU(df, d + xs + 2), LoadU(df, d + xs + xs + 4)); retval = MulAdd(sum, sum, retval); } { @@ -651,9 +686,9 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, 6_____*___ 7_________ 8______*__ */ - auto sum = LoadU(df, d - xs3 - xs - 2) + LoadU(df, d - xs - xs - 1) + - center + LoadU(df, d + xs + xs + 1) + - LoadU(df, d + xs3 + xs + 2); + auto sum = + Sum(LoadU(df, d - xs3 - xs - 2), LoadU(df, d - xs - xs - 1), center, + LoadU(df, d + xs + xs + 1), LoadU(df, d + xs3 + xs + 2)); retval = MulAdd(sum, sum, retval); } { @@ -666,9 +701,9 @@ Vec<D> MaltaUnit(MaltaTagLF /*tag*/, const D df, 6___*_____ 7_________ 8__*______ */ - auto sum = LoadU(df, d - xs3 - xs + 2) + LoadU(df, d - xs - xs + 1) + - center + LoadU(df, d + xs + xs - 1) + - LoadU(df, d + xs3 + xs - 2); + auto sum = + Sum(LoadU(df, d - xs3 - xs + 2), LoadU(df, d - xs - xs + 1), center, + LoadU(df, d + xs + xs - 1), LoadU(df, d + xs3 + xs - 2)); retval = MulAdd(sum, sum, retval); } return retval; @@ -682,65 +717,65 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, const auto center = LoadU(df, d); // x grows, y constant - const auto sum_yconst = LoadU(df, d - 4) + LoadU(df, d - 3) + - LoadU(df, d - 2) + LoadU(df, d - 1) + center + - LoadU(df, d + 1) + LoadU(df, d + 2) + - LoadU(df, d + 3) + LoadU(df, d + 4); + const auto sum_yconst = + Sum(LoadU(df, d - 4), LoadU(df, d - 3), LoadU(df, d - 2), + LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d + 2), + LoadU(df, d + 3), LoadU(df, d + 4)); // Will return this, sum of all line kernels - auto retval = sum_yconst * sum_yconst; + auto retval = Mul(sum_yconst, sum_yconst); { // y grows, x constant - auto sum = LoadU(df, d - xs3 - xs) + LoadU(df, d - xs3) + - LoadU(df, d - xs - xs) + LoadU(df, d - xs) + center + - LoadU(df, d + xs) + LoadU(df, d + xs + xs) + LoadU(df, d + xs3) + - LoadU(df, d + xs3 + xs); + auto sum = Sum(LoadU(df, d - xs3 - xs), LoadU(df, d - xs3), + LoadU(df, d - xs - xs), LoadU(df, d - xs), center, + LoadU(df, d + xs), LoadU(df, d + xs + xs), + LoadU(df, d + xs3), LoadU(df, d + xs3 + xs)); retval = MulAdd(sum, sum, retval); } { // both grow - auto sum = LoadU(df, d - xs3 - 3) + LoadU(df, d - xs - xs - 2) + - LoadU(df, d - xs - 1) + center + LoadU(df, d + xs + 1) + - LoadU(df, d + xs + xs + 2) + LoadU(df, d + xs3 + 3); + auto sum = Sum(LoadU(df, d - xs3 - 3), LoadU(df, d - xs - xs - 2), + LoadU(df, d - xs - 1), center, LoadU(df, d + xs + 1), + LoadU(df, d + xs + xs + 2), LoadU(df, d + xs3 + 3)); retval = MulAdd(sum, sum, retval); } { // y grows, x shrinks - auto sum = LoadU(df, d - xs3 + 3) + LoadU(df, d - xs - xs + 2) + - LoadU(df, d - xs + 1) + center + LoadU(df, d + xs - 1) + - LoadU(df, d + xs + xs - 2) + LoadU(df, d + xs3 - 3); + auto sum = Sum(LoadU(df, d - xs3 + 3), LoadU(df, d - xs - xs + 2), + LoadU(df, d - xs + 1), center, LoadU(df, d + xs - 1), + LoadU(df, d + xs + xs - 2), LoadU(df, d + xs3 - 3)); retval = MulAdd(sum, sum, retval); } { // y grows -4 to 4, x shrinks 1 -> -1 - auto sum = LoadU(df, d - xs3 - xs + 1) + LoadU(df, d - xs3 + 1) + - LoadU(df, d - xs - xs + 1) + LoadU(df, d - xs) + center + - LoadU(df, d + xs) + LoadU(df, d + xs + xs - 1) + - LoadU(df, d + xs3 - 1) + LoadU(df, d + xs3 + xs - 1); + auto sum = Sum(LoadU(df, d - xs3 - xs + 1), LoadU(df, d - xs3 + 1), + LoadU(df, d - xs - xs + 1), LoadU(df, d - xs), center, + LoadU(df, d + xs), LoadU(df, d + xs + xs - 1), + LoadU(df, d + xs3 - 1), LoadU(df, d + xs3 + xs - 1)); retval = MulAdd(sum, sum, retval); } { // y grows -4 to 4, x grows -1 -> 1 - auto sum = LoadU(df, d - xs3 - xs - 1) + LoadU(df, d - xs3 - 1) + - LoadU(df, d - xs - xs - 1) + LoadU(df, d - xs) + center + - LoadU(df, d + xs) + LoadU(df, d + xs + xs + 1) + - LoadU(df, d + xs3 + 1) + LoadU(df, d + xs3 + xs + 1); + auto sum = Sum(LoadU(df, d - xs3 - xs - 1), LoadU(df, d - xs3 - 1), + LoadU(df, d - xs - xs - 1), LoadU(df, d - xs), center, + LoadU(df, d + xs), LoadU(df, d + xs + xs + 1), + LoadU(df, d + xs3 + 1), LoadU(df, d + xs3 + xs + 1)); retval = MulAdd(sum, sum, retval); } { // x grows -4 to 4, y grows -1 to 1 - auto sum = LoadU(df, d - 4 - xs) + LoadU(df, d - 3 - xs) + - LoadU(df, d - 2 - xs) + LoadU(df, d - 1) + center + - LoadU(df, d + 1) + LoadU(df, d + 2 + xs) + - LoadU(df, d + 3 + xs) + LoadU(df, d + 4 + xs); + auto sum = + Sum(LoadU(df, d - 4 - xs), LoadU(df, d - 3 - xs), LoadU(df, d - 2 - xs), + LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d + 2 + xs), + LoadU(df, d + 3 + xs), LoadU(df, d + 4 + xs)); retval = MulAdd(sum, sum, retval); } { // x grows -4 to 4, y shrinks 1 to -1 - auto sum = LoadU(df, d - 4 + xs) + LoadU(df, d - 3 + xs) + - LoadU(df, d - 2 + xs) + LoadU(df, d - 1) + center + - LoadU(df, d + 1) + LoadU(df, d + 2 - xs) + - LoadU(df, d + 3 - xs) + LoadU(df, d + 4 - xs); + auto sum = + Sum(LoadU(df, d - 4 + xs), LoadU(df, d - 3 + xs), LoadU(df, d - 2 + xs), + LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d + 2 - xs), + LoadU(df, d + 3 - xs), LoadU(df, d + 4 - xs)); retval = MulAdd(sum, sum, retval); } { @@ -753,9 +788,9 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, 6_____*___ 7______*__ 8_________ */ - auto sum = LoadU(df, d - xs3 - 2) + LoadU(df, d - xs - xs - 1) + - LoadU(df, d - xs - 1) + center + LoadU(df, d + xs + 1) + - LoadU(df, d + xs + xs + 1) + LoadU(df, d + xs3 + 2); + auto sum = Sum(LoadU(df, d - xs3 - 2), LoadU(df, d - xs - xs - 1), + LoadU(df, d - xs - 1), center, LoadU(df, d + xs + 1), + LoadU(df, d + xs + xs + 1), LoadU(df, d + xs3 + 2)); retval = MulAdd(sum, sum, retval); } { @@ -768,9 +803,9 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, 6___*_____ 7__*______ 8_________ */ - auto sum = LoadU(df, d - xs3 + 2) + LoadU(df, d - xs - xs + 1) + - LoadU(df, d - xs + 1) + center + LoadU(df, d + xs - 1) + - LoadU(df, d + xs + xs - 1) + LoadU(df, d + xs3 - 2); + auto sum = Sum(LoadU(df, d - xs3 + 2), LoadU(df, d - xs - xs + 1), + LoadU(df, d - xs + 1), center, LoadU(df, d + xs - 1), + LoadU(df, d + xs + xs - 1), LoadU(df, d + xs3 - 2)); retval = MulAdd(sum, sum, retval); } { @@ -783,9 +818,9 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, 6_______*_ 7_________ 8_________ */ - auto sum = LoadU(df, d - xs - xs - 3) + LoadU(df, d - xs - 2) + - LoadU(df, d - xs - 1) + center + LoadU(df, d + xs + 1) + - LoadU(df, d + xs + 2) + LoadU(df, d + xs + xs + 3); + auto sum = Sum(LoadU(df, d - xs - xs - 3), LoadU(df, d - xs - 2), + LoadU(df, d - xs - 1), center, LoadU(df, d + xs + 1), + LoadU(df, d + xs + 2), LoadU(df, d + xs + xs + 3)); retval = MulAdd(sum, sum, retval); } { @@ -798,9 +833,9 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, 6_*_______ 7_________ 8_________ */ - auto sum = LoadU(df, d - xs - xs + 3) + LoadU(df, d - xs + 2) + - LoadU(df, d - xs + 1) + center + LoadU(df, d + xs - 1) + - LoadU(df, d + xs - 2) + LoadU(df, d + xs + xs - 3); + auto sum = Sum(LoadU(df, d - xs - xs + 3), LoadU(df, d - xs + 2), + LoadU(df, d - xs + 1), center, LoadU(df, d + xs - 1), + LoadU(df, d + xs - 2), LoadU(df, d + xs + xs - 3)); retval = MulAdd(sum, sum, retval); } { @@ -814,10 +849,10 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, 7_________ 8_________ */ - auto sum = LoadU(df, d + xs - 4) + LoadU(df, d + xs - 3) + - LoadU(df, d + xs - 2) + LoadU(df, d - 1) + center + - LoadU(df, d + 1) + LoadU(df, d - xs + 2) + - LoadU(df, d - xs + 3) + LoadU(df, d - xs + 4); + auto sum = + Sum(LoadU(df, d + xs - 4), LoadU(df, d + xs - 3), LoadU(df, d + xs - 2), + LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d - xs + 2), + LoadU(df, d - xs + 3), LoadU(df, d - xs + 4)); retval = MulAdd(sum, sum, retval); } { @@ -830,10 +865,10 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, 6_________ 7_________ 8_________ */ - auto sum = LoadU(df, d - xs - 4) + LoadU(df, d - xs - 3) + - LoadU(df, d - xs - 2) + LoadU(df, d - 1) + center + - LoadU(df, d + 1) + LoadU(df, d + xs + 2) + - LoadU(df, d + xs + 3) + LoadU(df, d + xs + 4); + auto sum = + Sum(LoadU(df, d - xs - 4), LoadU(df, d - xs - 3), LoadU(df, d - xs - 2), + LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d + xs + 2), + LoadU(df, d + xs + 3), LoadU(df, d + xs + 4)); retval = MulAdd(sum, sum, retval); } { @@ -846,10 +881,10 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, 6_____*___ 7_____*___ 8_____*___ */ - auto sum = LoadU(df, d - xs3 - xs - 1) + LoadU(df, d - xs3 - 1) + - LoadU(df, d - xs - xs - 1) + LoadU(df, d - xs) + center + - LoadU(df, d + xs) + LoadU(df, d + xs + xs + 1) + - LoadU(df, d + xs3 + 1) + LoadU(df, d + xs3 + xs + 1); + auto sum = Sum(LoadU(df, d - xs3 - xs - 1), LoadU(df, d - xs3 - 1), + LoadU(df, d - xs - xs - 1), LoadU(df, d - xs), center, + LoadU(df, d + xs), LoadU(df, d + xs + xs + 1), + LoadU(df, d + xs3 + 1), LoadU(df, d + xs3 + xs + 1)); retval = MulAdd(sum, sum, retval); } { @@ -862,10 +897,10 @@ Vec<D> MaltaUnit(MaltaTag /*tag*/, const D df, 6___*_____ 7___*_____ 8___*_____ */ - auto sum = LoadU(df, d - xs3 - xs + 1) + LoadU(df, d - xs3 + 1) + - LoadU(df, d - xs - xs + 1) + LoadU(df, d - xs) + center + - LoadU(df, d + xs) + LoadU(df, d + xs + xs - 1) + - LoadU(df, d + xs3 - 1) + LoadU(df, d + xs3 + xs - 1); + auto sum = Sum(LoadU(df, d - xs3 - xs + 1), LoadU(df, d - xs3 + 1), + LoadU(df, d - xs - xs + 1), LoadU(df, d - xs), center, + LoadU(df, d + xs), LoadU(df, d + xs + xs - 1), + LoadU(df, d + xs3 - 1), LoadU(df, d + xs3 + xs - 1)); retval = MulAdd(sum, sum, retval); } return retval; @@ -989,7 +1024,7 @@ static void MaltaDiffMapT(const Tag tag, const ImageF& lum0, const ImageF& lum1, } for (; x0 + Lanes(df) + 4 <= xsize_; x0 += Lanes(df)) { auto diff = Load(df, row_diff + x0); - diff += MaltaUnit(Tag(), df, row_in + x0, stride); + diff = Add(diff, MaltaUnit(Tag(), df, row_in + x0, stride)); Store(diff, df, row_diff + x0); } @@ -1250,8 +1285,8 @@ static void L2Diff(const ImageF& i0, const ImageF& i1, const float w, float* BUTTERAUGLI_RESTRICT row_diff = diffmap->PlaneRow(c, y); for (size_t x = 0; x < i0.xsize(); x += Lanes(d)) { - const auto diff = Load(d, row0 + x) - Load(d, row1 + x); - const auto diff2 = diff * diff; + const auto diff = Sub(Load(d, row0 + x), Load(d, row1 + x)); + const auto diff2 = Mul(diff, diff); const auto prev = Load(d, row_diff + x); Store(MulAdd(diff2, weight, prev), d, row_diff + x); } @@ -1272,9 +1307,9 @@ static void SetL2Diff(const ImageF& i0, const ImageF& i1, const float w, float* BUTTERAUGLI_RESTRICT row_diff = diffmap->PlaneRow(c, y); for (size_t x = 0; x < i0.xsize(); x += Lanes(d)) { - const auto diff = Load(d, row0 + x) - Load(d, row1 + x); - const auto diff2 = diff * diff; - Store(diff2 * weight, d, row_diff + x); + const auto diff = Sub(Load(d, row0 + x), Load(d, row1 + x)); + const auto diff2 = Mul(diff, diff); + Store(Mul(diff2, weight), d, row_diff + x); } } } @@ -1302,22 +1337,22 @@ static void L2DiffAsymmetric(const ImageF& i0, const ImageF& i1, float w_0gt1, const auto val1 = Load(d, row1 + x); // Primary symmetric quadratic objective. - const auto diff = val0 - val1; - auto total = MulAdd(diff * diff, vw_0gt1, Load(d, row_diff + x)); + const auto diff = Sub(val0, val1); + auto total = MulAdd(Mul(diff, diff), vw_0gt1, Load(d, row_diff + x)); // Secondary half-open quadratic objectives. const auto fabs0 = Abs(val0); - const auto too_small = Set(d, 0.4) * fabs0; + const auto too_small = Mul(Set(d, 0.4), fabs0); const auto too_big = fabs0; - const auto if_neg = - IfThenElse(val1 > Neg(too_small), val1 + too_small, - IfThenElseZero(val1 < Neg(too_big), Neg(val1) - too_big)); + const auto if_neg = IfThenElse( + Gt(val1, Neg(too_small)), Add(val1, too_small), + IfThenElseZero(Lt(val1, Neg(too_big)), Sub(Neg(val1), too_big))); const auto if_pos = - IfThenElse(val1 < too_small, too_small - val1, - IfThenElseZero(val1 > too_big, val1 - too_big)); - const auto v = IfThenElse(val0 < Zero(d), if_neg, if_pos); - total += vw_0lt1 * v * v; + IfThenElse(Lt(val1, too_small), Sub(too_small, val1), + IfThenElseZero(Gt(val1, too_big), Sub(val1, too_big))); + const auto v = IfThenElse(Lt(val0, Zero(d)), if_neg, if_pos); + total = MulAdd(vw_0lt1, Mul(v, v), total); Store(total, d, row_diff + x); } } @@ -1334,7 +1369,7 @@ V Gamma(const DF df, V v) { // clamping here. v = ZeroIfNegative(v); - const auto biased = v + Set(df, 9.9710635769299145); + const auto biased = Add(v, Set(df, 9.9710635769299145)); const auto log = FastLog2f(df, biased); // We could fold this into a custom Log2 polynomial, but there would be // relatively little gain. @@ -1373,9 +1408,9 @@ BUTTERAUGLI_INLINE void OpsinAbsorbance(const DF df, const V& in0, const V& in1, const V mix10 = Set(df, mixi10); const V mix11 = Set(df, mixi11); - *out0 = mix0 * in0 + mix1 * in1 + mix2 * in2 + mix3; - *out1 = mix4 * in0 + mix5 * in1 + mix6 * in2 + mix7; - *out2 = mix8 * in0 + mix9 * in1 + mix10 * in2 + mix11; + *out0 = MulAdd(mix0, in0, MulAdd(mix1, in1, MulAdd(mix2, in2, mix3))); + *out1 = MulAdd(mix4, in0, MulAdd(mix5, in1, MulAdd(mix6, in2, mix7))); + *out2 = MulAdd(mix8, in0, MulAdd(mix9, in1, MulAdd(mix10, in2, mix11))); if (Clamp) { *out0 = Max(*out0, mix3); @@ -1419,16 +1454,16 @@ Image3F OpsinDynamicsImage(const Image3F& rgb, const ButteraugliParams& params, auto pre_mixed1 = Undefined(df); auto pre_mixed2 = Undefined(df); OpsinAbsorbance<true>( - df, Load(df, row_blurred_r + x) * intensity_target_multiplier, - Load(df, row_blurred_g + x) * intensity_target_multiplier, - Load(df, row_blurred_b + x) * intensity_target_multiplier, + df, Mul(Load(df, row_blurred_r + x), intensity_target_multiplier), + Mul(Load(df, row_blurred_g + x), intensity_target_multiplier), + Mul(Load(df, row_blurred_b + x), intensity_target_multiplier), &pre_mixed0, &pre_mixed1, &pre_mixed2); pre_mixed0 = Max(pre_mixed0, min); pre_mixed1 = Max(pre_mixed1, min); pre_mixed2 = Max(pre_mixed2, min); - sensitivity0 = Gamma(df, pre_mixed0) / pre_mixed0; - sensitivity1 = Gamma(df, pre_mixed1) / pre_mixed1; - sensitivity2 = Gamma(df, pre_mixed2) / pre_mixed2; + sensitivity0 = Div(Gamma(df, pre_mixed0), pre_mixed0); + sensitivity1 = Div(Gamma(df, pre_mixed1), pre_mixed1); + sensitivity2 = Div(Gamma(df, pre_mixed2), pre_mixed2); sensitivity0 = Max(sensitivity0, min); sensitivity1 = Max(sensitivity1, min); sensitivity2 = Max(sensitivity2, min); @@ -1436,14 +1471,14 @@ Image3F OpsinDynamicsImage(const Image3F& rgb, const ButteraugliParams& params, auto cur_mixed0 = Undefined(df); auto cur_mixed1 = Undefined(df); auto cur_mixed2 = Undefined(df); - OpsinAbsorbance<false>(df, - Load(df, row_r + x) * intensity_target_multiplier, - Load(df, row_g + x) * intensity_target_multiplier, - Load(df, row_b + x) * intensity_target_multiplier, - &cur_mixed0, &cur_mixed1, &cur_mixed2); - cur_mixed0 *= sensitivity0; - cur_mixed1 *= sensitivity1; - cur_mixed2 *= sensitivity2; + OpsinAbsorbance<false>( + df, Mul(Load(df, row_r + x), intensity_target_multiplier), + Mul(Load(df, row_g + x), intensity_target_multiplier), + Mul(Load(df, row_b + x), intensity_target_multiplier), &cur_mixed0, + &cur_mixed1, &cur_mixed2); + cur_mixed0 = Mul(cur_mixed0, sensitivity0); + cur_mixed1 = Mul(cur_mixed1, sensitivity1); + cur_mixed2 = Mul(cur_mixed2, sensitivity2); // This is a kludge. The negative values should be zeroed away before // blurring. Ideally there would be no negative values in the first place. const auto min01 = Set(df, 1.7557483643287353f); @@ -1452,8 +1487,8 @@ Image3F OpsinDynamicsImage(const Image3F& rgb, const ButteraugliParams& params, cur_mixed1 = Max(cur_mixed1, min01); cur_mixed2 = Max(cur_mixed2, min2); - Store(cur_mixed0 - cur_mixed1, df, row_out_x + x); - Store(cur_mixed0 + cur_mixed1, df, row_out_y + x); + Store(Sub(cur_mixed0, cur_mixed1), df, row_out_x + x); + Store(Add(cur_mixed0, cur_mixed1), df, row_out_y + x); Store(cur_mixed2, df, row_out_b + x); } } diff --git a/media/libjxl/src/lib/jxl/codec_in_out.h b/media/libjxl/src/lib/jxl/codec_in_out.h index c1bff66366..23f0a4afeb 100644 --- a/media/libjxl/src/lib/jxl/codec_in_out.h +++ b/media/libjxl/src/lib/jxl/codec_in_out.h @@ -13,6 +13,7 @@ #include <utility> #include <vector> +#include "lib/jxl/alpha.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/common.h" #include "lib/jxl/frame_header.h" @@ -133,18 +134,57 @@ class CodecInOut { } return true; } - // Calls PremultiplyAlpha for each ImageBundle (preview/frames). - void PremultiplyAlpha() { + // Performs "PremultiplyAlpha" for each ImageBundle (preview/frames). + bool PremultiplyAlpha() { + const auto doPremultiplyAlpha = [](ImageBundle& bundle) { + if (!bundle.HasAlpha()) return; + if (!bundle.HasColor()) return; + auto* color = bundle.color(); + const auto* alpha = bundle.alpha(); + JXL_CHECK(color->ysize() == alpha->ysize()); + JXL_CHECK(color->xsize() == alpha->xsize()); + for (size_t y = 0; y < color->ysize(); y++) { + ::jxl::PremultiplyAlpha(color->PlaneRow(0, y), color->PlaneRow(1, y), + color->PlaneRow(2, y), alpha->Row(y), + color->xsize()); + } + }; ExtraChannelInfo* eci = metadata.m.Find(ExtraChannel::kAlpha); - if (eci == nullptr || eci->alpha_associated) return; // nothing to do + if (eci == nullptr || eci->alpha_associated) return false; if (metadata.m.have_preview) { - preview_frame.PremultiplyAlpha(); + doPremultiplyAlpha(preview_frame); } for (ImageBundle& ib : frames) { - ib.PremultiplyAlpha(); + doPremultiplyAlpha(ib); } eci->alpha_associated = true; - return; + return true; + } + + bool UnpremultiplyAlpha() { + const auto doUnpremultiplyAlpha = [](ImageBundle& bundle) { + if (!bundle.HasAlpha()) return; + if (!bundle.HasColor()) return; + auto* color = bundle.color(); + const auto* alpha = bundle.alpha(); + JXL_CHECK(color->ysize() == alpha->ysize()); + JXL_CHECK(color->xsize() == alpha->xsize()); + for (size_t y = 0; y < color->ysize(); y++) { + ::jxl::UnpremultiplyAlpha(color->PlaneRow(0, y), color->PlaneRow(1, y), + color->PlaneRow(2, y), alpha->Row(y), + color->xsize()); + } + }; + ExtraChannelInfo* eci = metadata.m.Find(ExtraChannel::kAlpha); + if (eci == nullptr || !eci->alpha_associated) return false; + if (metadata.m.have_preview) { + doUnpremultiplyAlpha(preview_frame); + } + for (ImageBundle& ib : frames) { + doUnpremultiplyAlpha(ib); + } + eci->alpha_associated = false; + return true; } // -- DECODER INPUT: @@ -170,7 +210,6 @@ class CodecInOut { std::vector<ImageBundle> frames; // size=1 if !metadata.have_animation - bool use_sjpeg = false; // If the image should be written to a JPEG, use this quality for encoding. size_t jpeg_quality; }; diff --git a/media/libjxl/src/lib/jxl/color_encoding_internal.h b/media/libjxl/src/lib/jxl/color_encoding_internal.h index 2a8ea07456..d9e0448feb 100644 --- a/media/libjxl/src/lib/jxl/color_encoding_internal.h +++ b/media/libjxl/src/lib/jxl/color_encoding_internal.h @@ -88,7 +88,7 @@ static inline constexpr uint64_t EnumBits(Primaries /*unused*/) { } // Values from CICP TransferCharacteristics -enum TransferFunction : uint32_t { +enum class TransferFunction : uint32_t { k709 = 1, kUnknown = 2, kLinear = 8, diff --git a/media/libjxl/src/lib/jxl/color_management.cc b/media/libjxl/src/lib/jxl/color_management.cc index 5d83db2db1..521a75adf6 100644 --- a/media/libjxl/src/lib/jxl/color_management.cc +++ b/media/libjxl/src/lib/jxl/color_management.cc @@ -468,7 +468,8 @@ Status MaybeCreateProfile(const ColorEncoding& c, CreateICCCurvParaTag({2.6, 1.0, 0.0, 1.0, 0.0}, 3, &tags)); break; default: - JXL_ABORT("Unknown TF %d", c.tf.GetTransferFunction()); + JXL_ABORT("Unknown TF %u", + static_cast<unsigned int>(c.tf.GetTransferFunction())); } } FinalizeICCTag(&tags, &tag_offset, &tag_size); diff --git a/media/libjxl/src/lib/jxl/color_management_test.cc b/media/libjxl/src/lib/jxl/color_management_test.cc index b38fac4650..99382ca64b 100644 --- a/media/libjxl/src/lib/jxl/color_management_test.cc +++ b/media/libjxl/src/lib/jxl/color_management_test.cc @@ -13,8 +13,6 @@ #include <string> #include <utility> -#include "gmock/gmock.h" -#include "gtest/gtest.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/file_io.h" diff --git a/media/libjxl/src/lib/jxl/compressed_dc.cc b/media/libjxl/src/lib/jxl/compressed_dc.cc index 7fae7bb446..3b2c323991 100644 --- a/media/libjxl/src/lib/jxl/compressed_dc.cc +++ b/media/libjxl/src/lib/jxl/compressed_dc.cc @@ -46,8 +46,16 @@ using D = HWY_FULL(float); using DScalar = HWY_CAPPED(float, 1); // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Abs; +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Div; +using hwy::HWY_NAMESPACE::Max; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; using hwy::HWY_NAMESPACE::Rebind; +using hwy::HWY_NAMESPACE::Sub; using hwy::HWY_NAMESPACE::Vec; +using hwy::HWY_NAMESPACE::ZeroIfNegative; // TODO(veluca): optimize constants. const float w1 = 0.20345139757231578f; @@ -88,12 +96,12 @@ JXL_INLINE void ComputePixelChannel(const D d, const float dc_factor, const auto w_side = Set(d, w1); const auto w_corner = Set(d, w2); - const auto corner = tl + tr + bl + br; - const auto side = ml + mr + tc + bc; - *sm = corner * w_corner + side * w_side + *mc * w_center; + const auto corner = Add(Add(tl, tr), Add(bl, br)); + const auto side = Add(Add(ml, mr), Add(tc, bc)); + *sm = MulAdd(corner, w_corner, MulAdd(side, w_side, Mul(*mc, w_center))); const auto dc_quant = Set(d, dc_factor); - *gap = MaxWorkaround(*gap, Abs((*mc - *sm) / dc_quant)); + *gap = MaxWorkaround(*gap, Abs(Div(Sub(*mc, *sm), dc_quant))); } template <typename D> @@ -120,11 +128,11 @@ JXL_INLINE void ComputePixel( auto factor = MulAdd(Set(d, -4.0f), gap, Set(d, 3.0f)); factor = ZeroIfNegative(factor); - auto out = MulAdd(sm_x - mc_x, factor, mc_x); + auto out = MulAdd(Sub(sm_x, mc_x), factor, mc_x); Store(out, d, out_rows[0] + x); - out = MulAdd(sm_y - mc_y, factor, mc_y); + out = MulAdd(Sub(sm_y, mc_y), factor, mc_y); Store(out, d, out_rows[1] + x); - out = MulAdd(sm_b - mc_b, factor, mc_b); + out = MulAdd(Sub(sm_b, mc_b), factor, mc_b); Store(out, d, out_rows[2] + x); } @@ -222,9 +230,9 @@ void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in, const auto in_q_x = Load(di, quant_row_x + x); const auto in_q_y = Load(di, quant_row_y + x); const auto in_q_b = Load(di, quant_row_b + x); - const auto in_x = ConvertTo(df, in_q_x) * fac_x; - const auto in_y = ConvertTo(df, in_q_y) * fac_y; - const auto in_b = ConvertTo(df, in_q_b) * fac_b; + const auto in_x = Mul(ConvertTo(df, in_q_x), fac_x); + const auto in_y = Mul(ConvertTo(df, in_q_y), fac_y); + const auto in_b = Mul(ConvertTo(df, in_q_b), fac_b); Store(in_y, df, dec_row_y + x); Store(MulAdd(in_y, cfl_fac_x, in_x), df, dec_row_x + x); Store(MulAdd(in_y, cfl_fac_b, in_b), df, dec_row_b + x); @@ -243,7 +251,7 @@ void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in, float* row = rect.PlaneRow(dc, c, y); for (size_t x = 0; x < rect.xsize(); x += Lanes(di)) { const auto in_q = Load(di, quant_row + x); - const auto in = ConvertTo(df, in_q) * fac; + const auto in = Mul(ConvertTo(df, in_q), fac); Store(in, df, row + x); } } diff --git a/media/libjxl/src/lib/jxl/convolve-inl.h b/media/libjxl/src/lib/jxl/convolve-inl.h index 724163d4c7..054c9c6f0d 100644 --- a/media/libjxl/src/lib/jxl/convolve-inl.h +++ b/media/libjxl/src/lib/jxl/convolve-inl.h @@ -12,7 +12,10 @@ #include <hwy/highway.h> +#include "lib/jxl/base/profiler.h" #include "lib/jxl/base/status.h" +#include "lib/jxl/image_ops.h" + HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { @@ -23,6 +26,7 @@ using hwy::HWY_NAMESPACE::Broadcast; #if HWY_TARGET != HWY_SCALAR using hwy::HWY_NAMESPACE::CombineShiftRightBytes; #endif +using hwy::HWY_NAMESPACE::TableLookupLanes; using hwy::HWY_NAMESPACE::Vec; // Synthesizes left/right neighbors from a vector of center pixels. @@ -110,6 +114,180 @@ class Neighbors { } }; +#if HWY_TARGET != HWY_SCALAR + +// Returns indices for SetTableIndices such that TableLookupLanes on the +// rightmost unaligned vector (rightmost sample in its most-significant lane) +// returns the mirrored values, with the mirror outside the last valid sample. +static inline const int32_t* MirrorLanes(const size_t mod) { + const HWY_CAPPED(float, 16) d; + constexpr size_t kN = MaxLanes(d); + + // For mod = `image width mod 16` 0..15: + // last full vec mirrored (mem order) loadedVec mirrorVec idxVec + // 0123456789abcdef| fedcba9876543210 fed..210 012..def 012..def + // 0123456789abcdef|0 0fedcba98765432 0fe..321 234..f00 123..eff + // 0123456789abcdef|01 10fedcba987654 10f..432 456..110 234..ffe + // 0123456789abcdef|012 210fedcba9876 210..543 67..2210 34..ffed + // 0123456789abcdef|0123 3210fedcba98 321..654 8..33210 4..ffedc + // 0123456789abcdef|01234 43210fedcba + // 0123456789abcdef|012345 543210fedc + // 0123456789abcdef|0123456 6543210fe + // 0123456789abcdef|01234567 76543210 + // 0123456789abcdef|012345678 8765432 + // 0123456789abcdef|0123456789 987654 + // 0123456789abcdef|0123456789A A9876 + // 0123456789abcdef|0123456789AB BA98 + // 0123456789abcdef|0123456789ABC CBA + // 0123456789abcdef|0123456789ABCD DC + // 0123456789abcdef|0123456789ABCDE E EDC..10f EED..210 ffe..321 +#if HWY_CAP_GE512 + HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, // + 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; +#elif HWY_CAP_GE256 + HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = { + 1, 2, 3, 4, 5, 6, 7, 7, // + 6, 5, 4, 3, 2, 1, 0}; +#else // 128-bit + HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = {1, 2, 3, 3, // + 2, 1, 0}; +#endif + return idx_lanes + kN - 1 - mod; +} + +#endif // HWY_TARGET != HWY_SCALAR + +// Single entry point for convolution. +// "Strategy" (Direct*/Separable*) decides kernel size and how to evaluate it. +template <class Strategy> +class ConvolveT { + static constexpr int64_t kRadius = Strategy::kRadius; + using Simd = HWY_CAPPED(float, 16); + + public: + static size_t MinWidth() { +#if HWY_TARGET == HWY_SCALAR + // First/Last use mirrored loads of up to +/- kRadius. + return 2 * kRadius; +#else + return Lanes(Simd()) + kRadius; +#endif + } + + // "Image" is ImageF or Image3F. + template <class Image, class Weights> + static void Run(const Image& in, const Rect& rect, const Weights& weights, + ThreadPool* pool, Image* out) { + PROFILER_ZONE("ConvolveT::Run"); + JXL_CHECK(SameSize(rect, *out)); + JXL_CHECK(rect.xsize() >= MinWidth()); + + static_assert(int64_t(kRadius) <= 3, + "Must handle [0, kRadius) and >= kRadius"); + switch (rect.xsize() % Lanes(Simd())) { + case 0: + return RunRows<0>(in, rect, weights, pool, out); + case 1: + return RunRows<1>(in, rect, weights, pool, out); + case 2: + return RunRows<2>(in, rect, weights, pool, out); + default: + return RunRows<3>(in, rect, weights, pool, out); + } + } + + private: + template <size_t kSizeModN, class WrapRow, class Weights> + static JXL_INLINE void RunRow(const float* JXL_RESTRICT in, + const size_t xsize, const int64_t stride, + const WrapRow& wrap_row, const Weights& weights, + float* JXL_RESTRICT out) { + Strategy::template ConvolveRow<kSizeModN>(in, xsize, stride, wrap_row, + weights, out); + } + + template <size_t kSizeModN, class Weights> + static JXL_INLINE void RunBorderRows(const ImageF& in, const Rect& rect, + const int64_t ybegin, const int64_t yend, + const Weights& weights, ImageF* out) { + const int64_t stride = in.PixelsPerRow(); + const WrapRowMirror wrap_row(in, rect.ysize()); + for (int64_t y = ybegin; y < yend; ++y) { + RunRow<kSizeModN>(rect.ConstRow(in, y), rect.xsize(), stride, wrap_row, + weights, out->Row(y)); + } + } + + // Image3F. + template <size_t kSizeModN, class Weights> + static JXL_INLINE void RunBorderRows(const Image3F& in, const Rect& rect, + const int64_t ybegin, const int64_t yend, + const Weights& weights, Image3F* out) { + const int64_t stride = in.PixelsPerRow(); + for (int64_t y = ybegin; y < yend; ++y) { + for (size_t c = 0; c < 3; ++c) { + const WrapRowMirror wrap_row(in.Plane(c), rect.ysize()); + RunRow<kSizeModN>(rect.ConstPlaneRow(in, c, y), rect.xsize(), stride, + wrap_row, weights, out->PlaneRow(c, y)); + } + } + } + + template <size_t kSizeModN, class Weights> + static JXL_INLINE void RunInteriorRows(const ImageF& in, const Rect& rect, + const int64_t ybegin, + const int64_t yend, + const Weights& weights, + ThreadPool* pool, ImageF* out) { + const int64_t stride = in.PixelsPerRow(); + JXL_CHECK(RunOnPool( + pool, ybegin, yend, ThreadPool::NoInit, + [&](const uint32_t y, size_t /*thread*/) HWY_ATTR { + RunRow<kSizeModN>(rect.ConstRow(in, y), rect.xsize(), stride, + WrapRowUnchanged(), weights, out->Row(y)); + }, + "Convolve")); + } + + // Image3F. + template <size_t kSizeModN, class Weights> + static JXL_INLINE void RunInteriorRows(const Image3F& in, const Rect& rect, + const int64_t ybegin, + const int64_t yend, + const Weights& weights, + ThreadPool* pool, Image3F* out) { + const int64_t stride = in.PixelsPerRow(); + JXL_CHECK(RunOnPool( + pool, ybegin, yend, ThreadPool::NoInit, + [&](const uint32_t y, size_t /*thread*/) HWY_ATTR { + for (size_t c = 0; c < 3; ++c) { + RunRow<kSizeModN>(rect.ConstPlaneRow(in, c, y), rect.xsize(), + stride, WrapRowUnchanged(), weights, + out->PlaneRow(c, y)); + } + }, + "Convolve3")); + } + + template <size_t kSizeModN, class Image, class Weights> + static JXL_INLINE void RunRows(const Image& in, const Rect& rect, + const Weights& weights, ThreadPool* pool, + Image* out) { + const int64_t ysize = rect.ysize(); + RunBorderRows<kSizeModN>(in, rect, 0, std::min(int64_t(kRadius), ysize), + weights, out); + if (ysize > 2 * int64_t(kRadius)) { + RunInteriorRows<kSizeModN>(in, rect, int64_t(kRadius), + ysize - int64_t(kRadius), weights, pool, out); + } + if (ysize > int64_t(kRadius)) { + RunBorderRows<kSizeModN>(in, rect, ysize - int64_t(kRadius), ysize, + weights, out); + } + } +}; + } // namespace // NOLINTNEXTLINE(google-readability-namespace-comments) } // namespace HWY_NAMESPACE diff --git a/media/libjxl/src/lib/jxl/convolve.cc b/media/libjxl/src/lib/jxl/convolve.cc deleted file mode 100644 index b3e81f0ee7..0000000000 --- a/media/libjxl/src/lib/jxl/convolve.cc +++ /dev/null @@ -1,1332 +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 "lib/jxl/convolve.h" - -#undef HWY_TARGET_INCLUDE -#define HWY_TARGET_INCLUDE "lib/jxl/convolve.cc" -#include <hwy/foreach_target.h> -#include <hwy/highway.h> - -#include "lib/jxl/common.h" // RoundUpTo -#include "lib/jxl/convolve-inl.h" -#include "lib/jxl/image_ops.h" -HWY_BEFORE_NAMESPACE(); -namespace jxl { -namespace HWY_NAMESPACE { - -// These templates are not found via ADL. -using hwy::HWY_NAMESPACE::Vec; - -// Weighted sum of 1x5 pixels around ix, iy with [wx2 wx1 wx0 wx1 wx2]. -template <class WrapY> -static float WeightedSumBorder(const ImageF& in, const WrapY wrap_y, - const int64_t ix, const int64_t iy, - const size_t xsize, const size_t ysize, - const float wx0, const float wx1, - const float wx2) { - const WrapMirror wrap_x; - const float* JXL_RESTRICT row = in.ConstRow(wrap_y(iy, ysize)); - const float in_m2 = row[wrap_x(ix - 2, xsize)]; - const float in_p2 = row[wrap_x(ix + 2, xsize)]; - const float in_m1 = row[wrap_x(ix - 1, xsize)]; - const float in_p1 = row[wrap_x(ix + 1, xsize)]; - const float in_00 = row[ix]; - const float sum_2 = wx2 * (in_m2 + in_p2); - const float sum_1 = wx1 * (in_m1 + in_p1); - const float sum_0 = wx0 * in_00; - return sum_2 + sum_1 + sum_0; -} - -template <class WrapY, class V> -static V WeightedSum(const ImageF& in, const WrapY wrap_y, const size_t ix, - const int64_t iy, const size_t ysize, const V wx0, - const V wx1, const V wx2) { - const HWY_FULL(float) d; - const float* JXL_RESTRICT center = in.ConstRow(wrap_y(iy, ysize)) + ix; - const auto in_m2 = LoadU(d, center - 2); - const auto in_p2 = LoadU(d, center + 2); - const auto in_m1 = LoadU(d, center - 1); - const auto in_p1 = LoadU(d, center + 1); - const auto in_00 = Load(d, center); - const auto sum_2 = wx2 * (in_m2 + in_p2); - const auto sum_1 = wx1 * (in_m1 + in_p1); - const auto sum_0 = wx0 * in_00; - return sum_2 + sum_1 + sum_0; -} - -// Produces result for one pixel -template <class WrapY> -float Symmetric5Border(const ImageF& in, const Rect& rect, const int64_t ix, - const int64_t iy, const WeightsSymmetric5& weights) { - const float w0 = weights.c[0]; - const float w1 = weights.r[0]; - const float w2 = weights.R[0]; - const float w4 = weights.d[0]; - const float w5 = weights.L[0]; - const float w8 = weights.D[0]; - - const size_t xsize = rect.xsize(); - const size_t ysize = rect.ysize(); - const WrapY wrap_y; - // Unrolled loop over all 5 rows of the kernel. - float sum0 = WeightedSumBorder(in, wrap_y, ix, iy, xsize, ysize, w0, w1, w2); - - sum0 += WeightedSumBorder(in, wrap_y, ix, iy - 2, xsize, ysize, w2, w5, w8); - float sum1 = - WeightedSumBorder(in, wrap_y, ix, iy + 2, xsize, ysize, w2, w5, w8); - - sum0 += WeightedSumBorder(in, wrap_y, ix, iy - 1, xsize, ysize, w1, w4, w5); - sum1 += WeightedSumBorder(in, wrap_y, ix, iy + 1, xsize, ysize, w1, w4, w5); - - return sum0 + sum1; -} - -// Produces result for one vector's worth of pixels -template <class WrapY> -static void Symmetric5Interior(const ImageF& in, const Rect& rect, - const int64_t ix, const int64_t iy, - const WeightsSymmetric5& weights, - float* JXL_RESTRICT row_out) { - const HWY_FULL(float) d; - - const auto w0 = LoadDup128(d, weights.c); - const auto w1 = LoadDup128(d, weights.r); - const auto w2 = LoadDup128(d, weights.R); - const auto w4 = LoadDup128(d, weights.d); - const auto w5 = LoadDup128(d, weights.L); - const auto w8 = LoadDup128(d, weights.D); - - const size_t ysize = rect.ysize(); - const WrapY wrap_y; - // Unrolled loop over all 5 rows of the kernel. - auto sum0 = WeightedSum(in, wrap_y, ix, iy, ysize, w0, w1, w2); - - sum0 += WeightedSum(in, wrap_y, ix, iy - 2, ysize, w2, w5, w8); - auto sum1 = WeightedSum(in, wrap_y, ix, iy + 2, ysize, w2, w5, w8); - - sum0 += WeightedSum(in, wrap_y, ix, iy - 1, ysize, w1, w4, w5); - sum1 += WeightedSum(in, wrap_y, ix, iy + 1, ysize, w1, w4, w5); - - Store(sum0 + sum1, d, row_out + ix); -} - -template <class WrapY> -static void Symmetric5Row(const ImageF& in, const Rect& rect, const int64_t iy, - const WeightsSymmetric5& weights, - float* JXL_RESTRICT row_out) { - const int64_t kRadius = 2; - const size_t xsize = rect.xsize(); - - size_t ix = 0; - const HWY_FULL(float) d; - const size_t N = Lanes(d); - const size_t aligned_x = RoundUpTo(kRadius, N); - for (; ix < std::min(aligned_x, xsize); ++ix) { - row_out[ix] = Symmetric5Border<WrapY>(in, rect, ix, iy, weights); - } - for (; ix + N + kRadius <= xsize; ix += N) { - Symmetric5Interior<WrapY>(in, rect, ix, iy, weights, row_out); - } - for (; ix < xsize; ++ix) { - row_out[ix] = Symmetric5Border<WrapY>(in, rect, ix, iy, weights); - } -} - -static JXL_NOINLINE void Symmetric5BorderRow(const ImageF& in, const Rect& rect, - const int64_t iy, - const WeightsSymmetric5& weights, - float* JXL_RESTRICT row_out) { - return Symmetric5Row<WrapMirror>(in, rect, iy, weights, row_out); -} - -#if HWY_TARGET != HWY_SCALAR - -// Returns indices for SetTableIndices such that TableLookupLanes on the -// rightmost unaligned vector (rightmost sample in its most-significant lane) -// returns the mirrored values, with the mirror outside the last valid sample. -static inline const int32_t* MirrorLanes(const size_t mod) { - const HWY_CAPPED(float, 16) d; - constexpr size_t kN = MaxLanes(d); - - // For mod = `image width mod 16` 0..15: - // last full vec mirrored (mem order) loadedVec mirrorVec idxVec - // 0123456789abcdef| fedcba9876543210 fed..210 012..def 012..def - // 0123456789abcdef|0 0fedcba98765432 0fe..321 234..f00 123..eff - // 0123456789abcdef|01 10fedcba987654 10f..432 456..110 234..ffe - // 0123456789abcdef|012 210fedcba9876 210..543 67..2210 34..ffed - // 0123456789abcdef|0123 3210fedcba98 321..654 8..33210 4..ffedc - // 0123456789abcdef|01234 43210fedcba - // 0123456789abcdef|012345 543210fedc - // 0123456789abcdef|0123456 6543210fe - // 0123456789abcdef|01234567 76543210 - // 0123456789abcdef|012345678 8765432 - // 0123456789abcdef|0123456789 987654 - // 0123456789abcdef|0123456789A A9876 - // 0123456789abcdef|0123456789AB BA98 - // 0123456789abcdef|0123456789ABC CBA - // 0123456789abcdef|0123456789ABCD DC - // 0123456789abcdef|0123456789ABCDE E EDC..10f EED..210 ffe..321 -#if HWY_CAP_GE512 - HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = { - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, // - 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; -#elif HWY_CAP_GE256 - HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = { - 1, 2, 3, 4, 5, 6, 7, 7, // - 6, 5, 4, 3, 2, 1, 0}; -#else // 128-bit - HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = {1, 2, 3, 3, // - 2, 1, 0}; -#endif - return idx_lanes + kN - 1 - mod; -} - -#endif // HWY_TARGET != HWY_SCALAR - -namespace strategy { - -struct StrategyBase { - using D = HWY_CAPPED(float, 16); - using V = Vec<D>; -}; - -// 3x3 convolution by symmetric kernel with a single scan through the input. -class Symmetric3 : public StrategyBase { - public: - static constexpr int64_t kRadius = 1; - - // Only accesses pixels in [0, xsize). - template <size_t kSizeModN, class WrapRow> - static JXL_INLINE void ConvolveRow(const float* const JXL_RESTRICT row_m, - const size_t xsize, const int64_t stride, - const WrapRow& wrap_row, - const WeightsSymmetric3& weights, - float* const JXL_RESTRICT row_out) { - const D d; - // t, m, b = top, middle, bottom row; - const float* const JXL_RESTRICT row_t = wrap_row(row_m - stride, stride); - const float* const JXL_RESTRICT row_b = wrap_row(row_m + stride, stride); - - // Must load in advance - compiler doesn't understand LoadDup128 and - // schedules them too late. - const V w0 = LoadDup128(d, weights.c); - const V w1 = LoadDup128(d, weights.r); - const V w2 = LoadDup128(d, weights.d); - - // l, c, r = left, center, right. Leftmost vector: need FirstL1. - { - const V tc = LoadU(d, row_t + 0); - const V mc = LoadU(d, row_m + 0); - const V bc = LoadU(d, row_b + 0); - const V tl = Neighbors::FirstL1(tc); - const V tr = LoadU(d, row_t + 0 + 1); - const V ml = Neighbors::FirstL1(mc); - const V mr = LoadU(d, row_m + 0 + 1); - const V bl = Neighbors::FirstL1(bc); - const V br = LoadU(d, row_b + 0 + 1); - const V conv = - WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); - Store(conv, d, row_out + 0); - } - - // Loop as long as we can load enough new values: - const size_t N = Lanes(d); - size_t x = N; - for (; x + N + kRadius <= xsize; x += N) { - const auto conv = ConvolveValid(row_t, row_m, row_b, x, w0, w1, w2); - Store(conv, d, row_out + x); - } - - // For final (partial) vector: - const V tc = LoadU(d, row_t + x); - const V mc = LoadU(d, row_m + x); - const V bc = LoadU(d, row_b + x); - - V tr, mr, br; -#if HWY_TARGET == HWY_SCALAR - tr = tc; // Single-lane => mirrored right neighbor = center value. - mr = mc; - br = bc; -#else - if (kSizeModN == 0) { - // The above loop didn't handle the last vector because it needs an - // additional right neighbor (generated via mirroring). - auto mirror = SetTableIndices(d, MirrorLanes(N - 1)); - tr = TableLookupLanes(tc, mirror); - mr = TableLookupLanes(mc, mirror); - br = TableLookupLanes(bc, mirror); - } else { - auto mirror = SetTableIndices(d, MirrorLanes((xsize % N) - 1)); - // Loads last valid value into uppermost lane and mirrors. - tr = TableLookupLanes(LoadU(d, row_t + xsize - N), mirror); - mr = TableLookupLanes(LoadU(d, row_m + xsize - N), mirror); - br = TableLookupLanes(LoadU(d, row_b + xsize - N), mirror); - } -#endif - - const V tl = LoadU(d, row_t + x - 1); - const V ml = LoadU(d, row_m + x - 1); - const V bl = LoadU(d, row_b + x - 1); - const V conv = WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); - Store(conv, d, row_out + x); - } - - private: - // Returns sum{x_i * w_i}. - template <class V> - static JXL_INLINE V WeightedSum(const V tl, const V tc, const V tr, - const V ml, const V mc, const V mr, - const V bl, const V bc, const V br, - const V w0, const V w1, const V w2) { - const V sum_tb = tc + bc; - - // Faster than 5 mul + 4 FMA. - const V mul0 = mc * w0; - const V sum_lr = ml + mr; - - const V x1 = sum_tb + sum_lr; - const V mul1 = MulAdd(x1, w1, mul0); - - const V sum_t2 = tl + tr; - const V sum_b2 = bl + br; - const V x2 = sum_t2 + sum_b2; - const V mul2 = MulAdd(x2, w2, mul1); - return mul2; - } - - static JXL_INLINE V ConvolveValid(const float* JXL_RESTRICT row_t, - const float* JXL_RESTRICT row_m, - const float* JXL_RESTRICT row_b, - const int64_t x, const V w0, const V w1, - const V w2) { - const D d; - const V tc = LoadU(d, row_t + x); - const V mc = LoadU(d, row_m + x); - const V bc = LoadU(d, row_b + x); - const V tl = LoadU(d, row_t + x - 1); - const V tr = LoadU(d, row_t + x + 1); - const V ml = LoadU(d, row_m + x - 1); - const V mr = LoadU(d, row_m + x + 1); - const V bl = LoadU(d, row_b + x - 1); - const V br = LoadU(d, row_b + x + 1); - return WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); - } -}; - -// 5x5 convolution by separable kernel with a single scan through the input. -// This is more cache-efficient than separate horizontal/vertical passes, and -// possibly faster (given enough registers) than tiling and/or transposing. -// -// Overview: imagine a 5x5 window around a central pixel. First convolve the -// rows by multiplying the pixels with the corresponding weights from -// WeightsSeparable5.horz[abs(x_offset) * 4]. Then multiply each of these -// intermediate results by the corresponding vertical weight, i.e. -// vert[abs(y_offset) * 4]. Finally, store the sum of these values as the -// convolution result at the position of the central pixel in the output. -// -// Each of these operations uses SIMD vectors. The central pixel and most -// importantly the output are aligned, so neighnoring pixels (e.g. x_offset=1) -// require unaligned loads. Because weights are supplied in identical groups of -// 4, we can use LoadDup128 to load them (slightly faster). -// -// Uses mirrored boundary handling. Until x >= kRadius, the horizontal -// convolution uses Neighbors class to shuffle vectors as if each of its lanes -// had been loaded from the mirrored offset. Similarly, the last full vector to -// write uses mirroring. In the case of scalar vectors, Neighbors is not usable -// and the value is loaded directly. Otherwise, the number of valid pixels -// modulo the vector size enables a small optimization: for smaller offsets, -// a non-mirrored load is sufficient. -class Separable5 : public StrategyBase { - public: - static constexpr int64_t kRadius = 2; - - template <size_t kSizeModN, class WrapRow> - static JXL_INLINE void ConvolveRow(const float* const JXL_RESTRICT row_m, - const size_t xsize, const int64_t stride, - const WrapRow& wrap_row, - const WeightsSeparable5& weights, - float* const JXL_RESTRICT row_out) { - const D d; - const int64_t neg_stride = -stride; // allows LEA addressing. - const float* const JXL_RESTRICT row_t2 = - wrap_row(row_m + 2 * neg_stride, stride); - const float* const JXL_RESTRICT row_t1 = - wrap_row(row_m + 1 * neg_stride, stride); - const float* const JXL_RESTRICT row_b1 = - wrap_row(row_m + 1 * stride, stride); - const float* const JXL_RESTRICT row_b2 = - wrap_row(row_m + 2 * stride, stride); - - const V wh0 = LoadDup128(d, weights.horz + 0 * 4); - const V wh1 = LoadDup128(d, weights.horz + 1 * 4); - const V wh2 = LoadDup128(d, weights.horz + 2 * 4); - const V wv0 = LoadDup128(d, weights.vert + 0 * 4); - const V wv1 = LoadDup128(d, weights.vert + 1 * 4); - const V wv2 = LoadDup128(d, weights.vert + 2 * 4); - - size_t x = 0; - - // More than one iteration for scalars. - for (; x < kRadius; x += Lanes(d)) { - const V conv0 = HorzConvolveFirst(row_m, x, xsize, wh0, wh1, wh2) * wv0; - - const V conv1t = HorzConvolveFirst(row_t1, x, xsize, wh0, wh1, wh2); - const V conv1b = HorzConvolveFirst(row_b1, x, xsize, wh0, wh1, wh2); - const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); - - const V conv2t = HorzConvolveFirst(row_t2, x, xsize, wh0, wh1, wh2); - const V conv2b = HorzConvolveFirst(row_b2, x, xsize, wh0, wh1, wh2); - const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); - Store(conv2, d, row_out + x); - } - - // Main loop: load inputs without padding - for (; x + Lanes(d) + kRadius <= xsize; x += Lanes(d)) { - const V conv0 = HorzConvolve(row_m + x, wh0, wh1, wh2) * wv0; - - const V conv1t = HorzConvolve(row_t1 + x, wh0, wh1, wh2); - const V conv1b = HorzConvolve(row_b1 + x, wh0, wh1, wh2); - const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); - - const V conv2t = HorzConvolve(row_t2 + x, wh0, wh1, wh2); - const V conv2b = HorzConvolve(row_b2 + x, wh0, wh1, wh2); - const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); - Store(conv2, d, row_out + x); - } - - // Last full vector to write (the above loop handled mod >= kRadius) -#if HWY_TARGET == HWY_SCALAR - while (x < xsize) { -#else - if (kSizeModN < kRadius) { -#endif - const V conv0 = - HorzConvolveLast<kSizeModN>(row_m, x, xsize, wh0, wh1, wh2) * wv0; - - const V conv1t = - HorzConvolveLast<kSizeModN>(row_t1, x, xsize, wh0, wh1, wh2); - const V conv1b = - HorzConvolveLast<kSizeModN>(row_b1, x, xsize, wh0, wh1, wh2); - const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); - - const V conv2t = - HorzConvolveLast<kSizeModN>(row_t2, x, xsize, wh0, wh1, wh2); - const V conv2b = - HorzConvolveLast<kSizeModN>(row_b2, x, xsize, wh0, wh1, wh2); - const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); - Store(conv2, d, row_out + x); - x += Lanes(d); - } - - // If mod = 0, the above vector was the last. - if (kSizeModN != 0) { - for (; x < xsize; ++x) { - float mul = 0.0f; - for (int64_t dy = -kRadius; dy <= kRadius; ++dy) { - const float wy = weights.vert[std::abs(dy) * 4]; - const float* clamped_row = wrap_row(row_m + dy * stride, stride); - for (int64_t dx = -kRadius; dx <= kRadius; ++dx) { - const float wx = weights.horz[std::abs(dx) * 4]; - const int64_t clamped_x = Mirror(x + dx, xsize); - mul += clamped_row[clamped_x] * wx * wy; - } - } - row_out[x] = mul; - } - } - } - - private: - // Same as HorzConvolve for the first/last vector in a row. - static JXL_INLINE V HorzConvolveFirst(const float* const JXL_RESTRICT row, - const int64_t x, const int64_t xsize, - const V wh0, const V wh1, const V wh2) { - const D d; - const V c = LoadU(d, row + x); - const V mul0 = c * wh0; - -#if HWY_TARGET == HWY_SCALAR - const V l1 = LoadU(d, row + Mirror(x - 1, xsize)); - const V l2 = LoadU(d, row + Mirror(x - 2, xsize)); -#else - (void)xsize; - const V l1 = Neighbors::FirstL1(c); - const V l2 = Neighbors::FirstL2(c); -#endif - - const V r1 = LoadU(d, row + x + 1); - const V r2 = LoadU(d, row + x + 2); - - const V mul1 = MulAdd(l1 + r1, wh1, mul0); - const V mul2 = MulAdd(l2 + r2, wh2, mul1); - return mul2; - } - - template <size_t kSizeModN> - static JXL_INLINE V HorzConvolveLast(const float* const JXL_RESTRICT row, - const int64_t x, const int64_t xsize, - const V wh0, const V wh1, const V wh2) { - const D d; - const V c = LoadU(d, row + x); - const V mul0 = c * wh0; - - const V l1 = LoadU(d, row + x - 1); - const V l2 = LoadU(d, row + x - 2); - - V r1, r2; -#if HWY_TARGET == HWY_SCALAR - r1 = LoadU(d, row + Mirror(x + 1, xsize)); - r2 = LoadU(d, row + Mirror(x + 2, xsize)); -#else - const size_t N = Lanes(d); - if (kSizeModN == 0) { - r2 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 2))); - r1 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 1))); - } else { // == 1 - const auto last = LoadU(d, row + xsize - N); - r2 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); - r1 = last; - } -#endif - - // Sum of pixels with Manhattan distance i, multiplied by weights[i]. - const V sum1 = l1 + r1; - const V mul1 = MulAdd(sum1, wh1, mul0); - const V sum2 = l2 + r2; - const V mul2 = MulAdd(sum2, wh2, mul1); - return mul2; - } - - // Requires kRadius valid pixels before/after pos. - static JXL_INLINE V HorzConvolve(const float* const JXL_RESTRICT pos, - const V wh0, const V wh1, const V wh2) { - const D d; - const V c = LoadU(d, pos); - const V mul0 = c * wh0; - - // Loading anew is faster than combining vectors. - const V l1 = LoadU(d, pos - 1); - const V r1 = LoadU(d, pos + 1); - const V l2 = LoadU(d, pos - 2); - const V r2 = LoadU(d, pos + 2); - // Sum of pixels with Manhattan distance i, multiplied by weights[i]. - const V sum1 = l1 + r1; - const V mul1 = MulAdd(sum1, wh1, mul0); - const V sum2 = l2 + r2; - const V mul2 = MulAdd(sum2, wh2, mul1); - return mul2; - } -}; // namespace strategy - -// 7x7 convolution by separable kernel with a single scan through the input. -// Extended version of Separable5, see documentation there. -class Separable7 : public StrategyBase { - public: - static constexpr int64_t kRadius = 3; - - template <size_t kSizeModN, class WrapRow> - static JXL_INLINE void ConvolveRow(const float* const JXL_RESTRICT row_m, - const size_t xsize, const int64_t stride, - const WrapRow& wrap_row, - const WeightsSeparable7& weights, - float* const JXL_RESTRICT row_out) { - const D d; - const int64_t neg_stride = -stride; // allows LEA addressing. - const float* const JXL_RESTRICT row_t3 = - wrap_row(row_m + 3 * neg_stride, stride); - const float* const JXL_RESTRICT row_t2 = - wrap_row(row_m + 2 * neg_stride, stride); - const float* const JXL_RESTRICT row_t1 = - wrap_row(row_m + 1 * neg_stride, stride); - const float* const JXL_RESTRICT row_b1 = - wrap_row(row_m + 1 * stride, stride); - const float* const JXL_RESTRICT row_b2 = - wrap_row(row_m + 2 * stride, stride); - const float* const JXL_RESTRICT row_b3 = - wrap_row(row_m + 3 * stride, stride); - - const V wh0 = LoadDup128(d, weights.horz + 0 * 4); - const V wh1 = LoadDup128(d, weights.horz + 1 * 4); - const V wh2 = LoadDup128(d, weights.horz + 2 * 4); - const V wh3 = LoadDup128(d, weights.horz + 3 * 4); - const V wv0 = LoadDup128(d, weights.vert + 0 * 4); - const V wv1 = LoadDup128(d, weights.vert + 1 * 4); - const V wv2 = LoadDup128(d, weights.vert + 2 * 4); - const V wv3 = LoadDup128(d, weights.vert + 3 * 4); - - size_t x = 0; - - // More than one iteration for scalars. - for (; x < kRadius; x += Lanes(d)) { - const V conv0 = - HorzConvolveFirst(row_m, x, xsize, wh0, wh1, wh2, wh3) * wv0; - - const V conv1t = HorzConvolveFirst(row_t1, x, xsize, wh0, wh1, wh2, wh3); - const V conv1b = HorzConvolveFirst(row_b1, x, xsize, wh0, wh1, wh2, wh3); - const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); - - const V conv2t = HorzConvolveFirst(row_t2, x, xsize, wh0, wh1, wh2, wh3); - const V conv2b = HorzConvolveFirst(row_b2, x, xsize, wh0, wh1, wh2, wh3); - const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); - - const V conv3t = HorzConvolveFirst(row_t3, x, xsize, wh0, wh1, wh2, wh3); - const V conv3b = HorzConvolveFirst(row_b3, x, xsize, wh0, wh1, wh2, wh3); - const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); - - Store(conv3, d, row_out + x); - } - - // Main loop: load inputs without padding - for (; x + Lanes(d) + kRadius <= xsize; x += Lanes(d)) { - const V conv0 = HorzConvolve(row_m + x, wh0, wh1, wh2, wh3) * wv0; - - const V conv1t = HorzConvolve(row_t1 + x, wh0, wh1, wh2, wh3); - const V conv1b = HorzConvolve(row_b1 + x, wh0, wh1, wh2, wh3); - const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); - - const V conv2t = HorzConvolve(row_t2 + x, wh0, wh1, wh2, wh3); - const V conv2b = HorzConvolve(row_b2 + x, wh0, wh1, wh2, wh3); - const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); - - const V conv3t = HorzConvolve(row_t3 + x, wh0, wh1, wh2, wh3); - const V conv3b = HorzConvolve(row_b3 + x, wh0, wh1, wh2, wh3); - const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); - - Store(conv3, d, row_out + x); - } - - // Last full vector to write (the above loop handled mod >= kRadius) -#if HWY_TARGET == HWY_SCALAR - while (x < xsize) { -#else - if (kSizeModN < kRadius) { -#endif - const V conv0 = - HorzConvolveLast<kSizeModN>(row_m, x, xsize, wh0, wh1, wh2, wh3) * - wv0; - - const V conv1t = - HorzConvolveLast<kSizeModN>(row_t1, x, xsize, wh0, wh1, wh2, wh3); - const V conv1b = - HorzConvolveLast<kSizeModN>(row_b1, x, xsize, wh0, wh1, wh2, wh3); - const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); - - const V conv2t = - HorzConvolveLast<kSizeModN>(row_t2, x, xsize, wh0, wh1, wh2, wh3); - const V conv2b = - HorzConvolveLast<kSizeModN>(row_b2, x, xsize, wh0, wh1, wh2, wh3); - const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); - - const V conv3t = - HorzConvolveLast<kSizeModN>(row_t3, x, xsize, wh0, wh1, wh2, wh3); - const V conv3b = - HorzConvolveLast<kSizeModN>(row_b3, x, xsize, wh0, wh1, wh2, wh3); - const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); - - Store(conv3, d, row_out + x); - x += Lanes(d); - } - - // If mod = 0, the above vector was the last. - if (kSizeModN != 0) { - for (; x < xsize; ++x) { - float mul = 0.0f; - for (int64_t dy = -kRadius; dy <= kRadius; ++dy) { - const float wy = weights.vert[std::abs(dy) * 4]; - const float* clamped_row = wrap_row(row_m + dy * stride, stride); - for (int64_t dx = -kRadius; dx <= kRadius; ++dx) { - const float wx = weights.horz[std::abs(dx) * 4]; - const int64_t clamped_x = Mirror(x + dx, xsize); - mul += clamped_row[clamped_x] * wx * wy; - } - } - row_out[x] = mul; - } - } - } - - private: - // Same as HorzConvolve for the first/last vector in a row. - static JXL_INLINE V HorzConvolveFirst(const float* const JXL_RESTRICT row, - const int64_t x, const int64_t xsize, - const V wh0, const V wh1, const V wh2, - const V wh3) { - const D d; - const V c = LoadU(d, row + x); - const V mul0 = c * wh0; - -#if HWY_TARGET == HWY_SCALAR - const V l1 = LoadU(d, row + Mirror(x - 1, xsize)); - const V l2 = LoadU(d, row + Mirror(x - 2, xsize)); - const V l3 = LoadU(d, row + Mirror(x - 3, xsize)); -#else - (void)xsize; - const V l1 = Neighbors::FirstL1(c); - const V l2 = Neighbors::FirstL2(c); - const V l3 = Neighbors::FirstL3(c); -#endif - - const V r1 = LoadU(d, row + x + 1); - const V r2 = LoadU(d, row + x + 2); - const V r3 = LoadU(d, row + x + 3); - - const V mul1 = MulAdd(l1 + r1, wh1, mul0); - const V mul2 = MulAdd(l2 + r2, wh2, mul1); - const V mul3 = MulAdd(l3 + r3, wh3, mul2); - return mul3; - } - - template <size_t kSizeModN> - static JXL_INLINE V HorzConvolveLast(const float* const JXL_RESTRICT row, - const int64_t x, const int64_t xsize, - const V wh0, const V wh1, const V wh2, - const V wh3) { - const D d; - const V c = LoadU(d, row + x); - const V mul0 = c * wh0; - - const V l1 = LoadU(d, row + x - 1); - const V l2 = LoadU(d, row + x - 2); - const V l3 = LoadU(d, row + x - 3); - - V r1, r2, r3; -#if HWY_TARGET == HWY_SCALAR - r1 = LoadU(d, row + Mirror(x + 1, xsize)); - r2 = LoadU(d, row + Mirror(x + 2, xsize)); - r3 = LoadU(d, row + Mirror(x + 3, xsize)); -#else - const size_t N = Lanes(d); - if (kSizeModN == 0) { - r3 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 3))); - r2 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 2))); - r1 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 1))); - } else if (kSizeModN == 1) { - const auto last = LoadU(d, row + xsize - N); - r3 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 2))); - r2 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); - r1 = last; - } else /* kSizeModN >= 2 */ { - const auto last = LoadU(d, row + xsize - N); - r3 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); - r2 = last; - r1 = LoadU(d, row + x + 1); - } -#endif - - // Sum of pixels with Manhattan distance i, multiplied by weights[i]. - const V sum1 = l1 + r1; - const V mul1 = MulAdd(sum1, wh1, mul0); - const V sum2 = l2 + r2; - const V mul2 = MulAdd(sum2, wh2, mul1); - const V sum3 = l3 + r3; - const V mul3 = MulAdd(sum3, wh3, mul2); - return mul3; - } - - // Returns one vector of horizontal convolution results; lane i is the result - // for pixel pos + i. This is the fast path for interior pixels, i.e. kRadius - // valid pixels before/after pos. - static JXL_INLINE V HorzConvolve(const float* const JXL_RESTRICT pos, - const V wh0, const V wh1, const V wh2, - const V wh3) { - const D d; - const V c = LoadU(d, pos); - const V mul0 = c * wh0; - - // TODO(janwas): better to Combine - const V l1 = LoadU(d, pos - 1); - const V r1 = LoadU(d, pos + 1); - const V l2 = LoadU(d, pos - 2); - const V r2 = LoadU(d, pos + 2); - const V l3 = LoadU(d, pos - 3); - const V r3 = LoadU(d, pos + 3); - // Sum of pixels with Manhattan distance i, multiplied by weights[i]. - const V sum1 = l1 + r1; - const V mul1 = MulAdd(sum1, wh1, mul0); - const V sum2 = l2 + r2; - const V mul2 = MulAdd(sum2, wh2, mul1); - const V sum3 = l3 + r3; - const V mul3 = MulAdd(sum3, wh3, mul2); - return mul3; - } -}; // namespace HWY_NAMESPACE - -} // namespace strategy - -// Single entry point for convolution. -// "Strategy" (Direct*/Separable*) decides kernel size and how to evaluate it. -template <class Strategy> -class ConvolveT { - static constexpr int64_t kRadius = Strategy::kRadius; - using Simd = HWY_CAPPED(float, 16); - - public: - static size_t MinWidth() { -#if HWY_TARGET == HWY_SCALAR - // First/Last use mirrored loads of up to +/- kRadius. - return 2 * kRadius; -#else - return Lanes(Simd()) + kRadius; -#endif - } - - // "Image" is ImageF or Image3F. - template <class Image, class Weights> - static void Run(const Image& in, const Rect& rect, const Weights& weights, - ThreadPool* pool, Image* out) { - PROFILER_ZONE("ConvolveT::Run"); - JXL_CHECK(SameSize(rect, *out)); - JXL_CHECK(rect.xsize() >= MinWidth()); - - static_assert(int64_t(kRadius) <= 3, - "Must handle [0, kRadius) and >= kRadius"); - switch (rect.xsize() % Lanes(Simd())) { - case 0: - return RunRows<0>(in, rect, weights, pool, out); - case 1: - return RunRows<1>(in, rect, weights, pool, out); - case 2: - return RunRows<2>(in, rect, weights, pool, out); - default: - return RunRows<3>(in, rect, weights, pool, out); - } - } - - private: - template <size_t kSizeModN, class WrapRow, class Weights> - static JXL_INLINE void RunRow(const float* JXL_RESTRICT in, - const size_t xsize, const int64_t stride, - const WrapRow& wrap_row, const Weights& weights, - float* JXL_RESTRICT out) { - Strategy::template ConvolveRow<kSizeModN>(in, xsize, stride, wrap_row, - weights, out); - } - - template <size_t kSizeModN, class Weights> - static JXL_INLINE void RunBorderRows(const ImageF& in, const Rect& rect, - const int64_t ybegin, const int64_t yend, - const Weights& weights, ImageF* out) { - const int64_t stride = in.PixelsPerRow(); - const WrapRowMirror wrap_row(in, rect.ysize()); - for (int64_t y = ybegin; y < yend; ++y) { - RunRow<kSizeModN>(rect.ConstRow(in, y), rect.xsize(), stride, wrap_row, - weights, out->Row(y)); - } - } - - // Image3F. - template <size_t kSizeModN, class Weights> - static JXL_INLINE void RunBorderRows(const Image3F& in, const Rect& rect, - const int64_t ybegin, const int64_t yend, - const Weights& weights, Image3F* out) { - const int64_t stride = in.PixelsPerRow(); - for (int64_t y = ybegin; y < yend; ++y) { - for (size_t c = 0; c < 3; ++c) { - const WrapRowMirror wrap_row(in.Plane(c), rect.ysize()); - RunRow<kSizeModN>(rect.ConstPlaneRow(in, c, y), rect.xsize(), stride, - wrap_row, weights, out->PlaneRow(c, y)); - } - } - } - - template <size_t kSizeModN, class Weights> - static JXL_INLINE void RunInteriorRows(const ImageF& in, const Rect& rect, - const int64_t ybegin, - const int64_t yend, - const Weights& weights, - ThreadPool* pool, ImageF* out) { - const int64_t stride = in.PixelsPerRow(); - JXL_CHECK(RunOnPool( - pool, ybegin, yend, ThreadPool::NoInit, - [&](const uint32_t y, size_t /*thread*/) HWY_ATTR { - RunRow<kSizeModN>(rect.ConstRow(in, y), rect.xsize(), stride, - WrapRowUnchanged(), weights, out->Row(y)); - }, - "Convolve")); - } - - // Image3F. - template <size_t kSizeModN, class Weights> - static JXL_INLINE void RunInteriorRows(const Image3F& in, const Rect& rect, - const int64_t ybegin, - const int64_t yend, - const Weights& weights, - ThreadPool* pool, Image3F* out) { - const int64_t stride = in.PixelsPerRow(); - JXL_CHECK(RunOnPool( - pool, ybegin, yend, ThreadPool::NoInit, - [&](const uint32_t y, size_t /*thread*/) HWY_ATTR { - for (size_t c = 0; c < 3; ++c) { - RunRow<kSizeModN>(rect.ConstPlaneRow(in, c, y), rect.xsize(), - stride, WrapRowUnchanged(), weights, - out->PlaneRow(c, y)); - } - }, - "Convolve3")); - } - - template <size_t kSizeModN, class Image, class Weights> - static JXL_INLINE void RunRows(const Image& in, const Rect& rect, - const Weights& weights, ThreadPool* pool, - Image* out) { - const int64_t ysize = rect.ysize(); - RunBorderRows<kSizeModN>(in, rect, 0, std::min(int64_t(kRadius), ysize), - weights, out); - if (ysize > 2 * int64_t(kRadius)) { - RunInteriorRows<kSizeModN>(in, rect, int64_t(kRadius), - ysize - int64_t(kRadius), weights, pool, out); - } - if (ysize > int64_t(kRadius)) { - RunBorderRows<kSizeModN>(in, rect, ysize - int64_t(kRadius), ysize, - weights, out); - } - } -}; - -void Symmetric3(const ImageF& in, const Rect& rect, - const WeightsSymmetric3& weights, ThreadPool* pool, - ImageF* out) { - using Conv = ConvolveT<strategy::Symmetric3>; - if (rect.xsize() >= Conv::MinWidth()) { - return Conv::Run(in, rect, weights, pool, out); - } - - return SlowSymmetric3(in, rect, weights, pool, out); -} - -// Symmetric5 is implemented above without ConvolveT. - -void Separable5(const ImageF& in, const Rect& rect, - const WeightsSeparable5& weights, ThreadPool* pool, - ImageF* out) { - using Conv = ConvolveT<strategy::Separable5>; - if (rect.xsize() >= Conv::MinWidth()) { - return Conv::Run(in, rect, weights, pool, out); - } - - return SlowSeparable5(in, rect, weights, pool, out); -} -void Separable5_3(const Image3F& in, const Rect& rect, - const WeightsSeparable5& weights, ThreadPool* pool, - Image3F* out) { - using Conv = ConvolveT<strategy::Separable5>; - if (rect.xsize() >= Conv::MinWidth()) { - return Conv::Run(in, rect, weights, pool, out); - } - - return SlowSeparable5(in, rect, weights, pool, out); -} - -void Separable7(const ImageF& in, const Rect& rect, - const WeightsSeparable7& weights, ThreadPool* pool, - ImageF* out) { - using Conv = ConvolveT<strategy::Separable7>; - if (rect.xsize() >= Conv::MinWidth()) { - return Conv::Run(in, rect, weights, pool, out); - } - - return SlowSeparable7(in, rect, weights, pool, out); -} -void Separable7_3(const Image3F& in, const Rect& rect, - const WeightsSeparable7& weights, ThreadPool* pool, - Image3F* out) { - using Conv = ConvolveT<strategy::Separable7>; - if (rect.xsize() >= Conv::MinWidth()) { - return Conv::Run(in, rect, weights, pool, out); - } - - return SlowSeparable7(in, rect, weights, pool, out); -} - -// Semi-vectorized (interior pixels Fonly); called directly like slow::, unlike -// the fully vectorized strategies below. -void Symmetric5(const ImageF& in, const Rect& rect, - const WeightsSymmetric5& weights, ThreadPool* pool, - ImageF* JXL_RESTRICT out) { - PROFILER_FUNC; - - const size_t ysize = rect.ysize(); - JXL_CHECK(RunOnPool( - pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, - [&](const uint32_t task, size_t /*thread*/) { - const int64_t iy = task; - - if (iy < 2 || iy >= static_cast<ssize_t>(ysize) - 2) { - Symmetric5BorderRow(in, rect, iy, weights, out->Row(iy)); - } else { - Symmetric5Row<WrapUnchanged>(in, rect, iy, weights, out->Row(iy)); - } - }, - "Symmetric5x5Convolution")); -} - -void Symmetric5_3(const Image3F& in, const Rect& rect, - const WeightsSymmetric5& weights, ThreadPool* pool, - Image3F* JXL_RESTRICT out) { - PROFILER_FUNC; - - const size_t ysize = rect.ysize(); - JXL_CHECK(RunOnPool( - pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, - [&](const uint32_t task, size_t /*thread*/) { - const size_t iy = task; - - if (iy < 2 || iy >= ysize - 2) { - for (size_t c = 0; c < 3; ++c) { - Symmetric5BorderRow(in.Plane(c), rect, iy, weights, - out->PlaneRow(c, iy)); - } - } else { - for (size_t c = 0; c < 3; ++c) { - Symmetric5Row<WrapUnchanged>(in.Plane(c), rect, iy, weights, - out->PlaneRow(c, iy)); - } - } - }, - "Symmetric5x5Convolution3")); -} - -// NOLINTNEXTLINE(google-readability-namespace-comments) -} // namespace HWY_NAMESPACE -} // namespace jxl -HWY_AFTER_NAMESPACE(); - -#if HWY_ONCE -namespace jxl { - -HWY_EXPORT(Symmetric3); -void Symmetric3(const ImageF& in, const Rect& rect, - const WeightsSymmetric3& weights, ThreadPool* pool, - ImageF* out) { - return HWY_DYNAMIC_DISPATCH(Symmetric3)(in, rect, weights, pool, out); -} - -HWY_EXPORT(Symmetric5); -void Symmetric5(const ImageF& in, const Rect& rect, - const WeightsSymmetric5& weights, ThreadPool* pool, - ImageF* JXL_RESTRICT out) { - return HWY_DYNAMIC_DISPATCH(Symmetric5)(in, rect, weights, pool, out); -} - -HWY_EXPORT(Symmetric5_3); -void Symmetric5_3(const Image3F& in, const Rect& rect, - const WeightsSymmetric5& weights, ThreadPool* pool, - Image3F* JXL_RESTRICT out) { - return HWY_DYNAMIC_DISPATCH(Symmetric5_3)(in, rect, weights, pool, out); -} - -HWY_EXPORT(Separable5); -void Separable5(const ImageF& in, const Rect& rect, - const WeightsSeparable5& weights, ThreadPool* pool, - ImageF* out) { - return HWY_DYNAMIC_DISPATCH(Separable5)(in, rect, weights, pool, out); -} - -HWY_EXPORT(Separable5_3); -void Separable5_3(const Image3F& in, const Rect& rect, - const WeightsSeparable5& weights, ThreadPool* pool, - Image3F* out) { - return HWY_DYNAMIC_DISPATCH(Separable5_3)(in, rect, weights, pool, out); -} - -HWY_EXPORT(Separable7); -void Separable7(const ImageF& in, const Rect& rect, - const WeightsSeparable7& weights, ThreadPool* pool, - ImageF* out) { - return HWY_DYNAMIC_DISPATCH(Separable7)(in, rect, weights, pool, out); -} - -HWY_EXPORT(Separable7_3); -void Separable7_3(const Image3F& in, const Rect& rect, - const WeightsSeparable7& weights, ThreadPool* pool, - Image3F* out) { - return HWY_DYNAMIC_DISPATCH(Separable7_3)(in, rect, weights, pool, out); -} - -//------------------------------------------------------------------------------ -// Kernels - -// Concentrates energy in low-frequency components (e.g. for antialiasing). -const WeightsSymmetric3& WeightsSymmetric3Lowpass() { - // Computed by research/convolve_weights.py's cubic spline approximations of - // prolate spheroidal wave functions. - constexpr float w0 = 0.36208932f; - constexpr float w1 = 0.12820096f; - constexpr float w2 = 0.03127668f; - static constexpr WeightsSymmetric3 weights = { - {HWY_REP4(w0)}, {HWY_REP4(w1)}, {HWY_REP4(w2)}}; - return weights; -} - -const WeightsSeparable5& WeightsSeparable5Lowpass() { - constexpr float w0 = 0.41714928f; - constexpr float w1 = 0.25539268f; - constexpr float w2 = 0.03603267f; - static constexpr WeightsSeparable5 weights = { - {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, - {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; - return weights; -} - -const WeightsSymmetric5& WeightsSymmetric5Lowpass() { - static constexpr WeightsSymmetric5 weights = { - {HWY_REP4(0.1740135f)}, {HWY_REP4(0.1065369f)}, {HWY_REP4(0.0150310f)}, - {HWY_REP4(0.0652254f)}, {HWY_REP4(0.0012984f)}, {HWY_REP4(0.0092025f)}}; - return weights; -} - -const WeightsSeparable5& WeightsSeparable5Gaussian1() { - constexpr float w0 = 0.38774f; - constexpr float w1 = 0.24477f; - constexpr float w2 = 0.06136f; - static constexpr WeightsSeparable5 weights = { - {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, - {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; - return weights; -} - -const WeightsSeparable5& WeightsSeparable5Gaussian2() { - constexpr float w0 = 0.250301f; - constexpr float w1 = 0.221461f; - constexpr float w2 = 0.153388f; - static constexpr WeightsSeparable5 weights = { - {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, - {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; - return weights; -} - -//------------------------------------------------------------------------------ -// Slow - -namespace { - -template <class WrapX, class WrapY> -float SlowSymmetric3Pixel(const ImageF& in, const int64_t ix, const int64_t iy, - const int64_t xsize, const int64_t ysize, - const WeightsSymmetric3& weights) { - float sum = 0.0f; - - // ix: image; kx: kernel - for (int64_t ky = -1; ky <= 1; ky++) { - const int64_t y = WrapY()(iy + ky, ysize); - const float* JXL_RESTRICT row_in = in.ConstRow(static_cast<size_t>(y)); - - const float wc = ky == 0 ? weights.c[0] : weights.r[0]; - const float wlr = ky == 0 ? weights.r[0] : weights.d[0]; - - const int64_t xm1 = WrapX()(ix - 1, xsize); - const int64_t xp1 = WrapX()(ix + 1, xsize); - sum += row_in[ix] * wc + (row_in[xm1] + row_in[xp1]) * wlr; - } - return sum; -} - -template <class WrapY> -void SlowSymmetric3Row(const ImageF& in, const int64_t iy, const int64_t xsize, - const int64_t ysize, const WeightsSymmetric3& weights, - float* JXL_RESTRICT row_out) { - row_out[0] = - SlowSymmetric3Pixel<WrapMirror, WrapY>(in, 0, iy, xsize, ysize, weights); - for (int64_t ix = 1; ix < xsize - 1; ix++) { - row_out[ix] = SlowSymmetric3Pixel<WrapUnchanged, WrapY>(in, ix, iy, xsize, - ysize, weights); - } - { - const int64_t ix = xsize - 1; - row_out[ix] = SlowSymmetric3Pixel<WrapMirror, WrapY>(in, ix, iy, xsize, - ysize, weights); - } -} - -} // namespace - -void SlowSymmetric3(const ImageF& in, const Rect& rect, - const WeightsSymmetric3& weights, ThreadPool* pool, - ImageF* JXL_RESTRICT out) { - PROFILER_FUNC; - - const int64_t xsize = static_cast<int64_t>(rect.xsize()); - const int64_t ysize = static_cast<int64_t>(rect.ysize()); - const int64_t kRadius = 1; - - JXL_CHECK(RunOnPool( - pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, - [&](const uint32_t task, size_t /*thread*/) { - const int64_t iy = task; - float* JXL_RESTRICT out_row = out->Row(static_cast<size_t>(iy)); - - if (iy < kRadius || iy >= ysize - kRadius) { - SlowSymmetric3Row<WrapMirror>(in, iy, xsize, ysize, weights, out_row); - } else { - SlowSymmetric3Row<WrapUnchanged>(in, iy, xsize, ysize, weights, - out_row); - } - }, - "SlowSymmetric3")); -} - -void SlowSymmetric3(const Image3F& in, const Rect& rect, - const WeightsSymmetric3& weights, ThreadPool* pool, - Image3F* JXL_RESTRICT out) { - PROFILER_FUNC; - - const int64_t xsize = static_cast<int64_t>(rect.xsize()); - const int64_t ysize = static_cast<int64_t>(rect.ysize()); - const int64_t kRadius = 1; - - JXL_CHECK(RunOnPool( - pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, - [&](const uint32_t task, size_t /*thread*/) { - const int64_t iy = task; - const size_t oy = static_cast<size_t>(iy); - - if (iy < kRadius || iy >= ysize - kRadius) { - for (size_t c = 0; c < 3; ++c) { - SlowSymmetric3Row<WrapMirror>(in.Plane(c), iy, xsize, ysize, - weights, out->PlaneRow(c, oy)); - } - } else { - for (size_t c = 0; c < 3; ++c) { - SlowSymmetric3Row<WrapUnchanged>(in.Plane(c), iy, xsize, ysize, - weights, out->PlaneRow(c, oy)); - } - } - }, - "SlowSymmetric3")); -} - -namespace { - -// Separable kernels, any radius. -float SlowSeparablePixel(const ImageF& in, const Rect& rect, const int64_t x, - const int64_t y, const int64_t radius, - const float* JXL_RESTRICT horz_weights, - const float* JXL_RESTRICT vert_weights) { - const size_t xsize = rect.xsize(); - const size_t ysize = rect.ysize(); - const WrapMirror wrap; - - float mul = 0.0f; - for (int dy = -radius; dy <= radius; ++dy) { - const float wy = vert_weights[std::abs(dy) * 4]; - const size_t sy = wrap(y + dy, ysize); - JXL_CHECK(sy < ysize); - const float* const JXL_RESTRICT row = rect.ConstRow(in, sy); - for (int dx = -radius; dx <= radius; ++dx) { - const float wx = horz_weights[std::abs(dx) * 4]; - const size_t sx = wrap(x + dx, xsize); - JXL_CHECK(sx < xsize); - mul += row[sx] * wx * wy; - } - } - return mul; -} - -} // namespace - -void SlowSeparable5(const ImageF& in, const Rect& rect, - const WeightsSeparable5& weights, ThreadPool* pool, - ImageF* out) { - PROFILER_FUNC; - const float* horz_weights = &weights.horz[0]; - const float* vert_weights = &weights.vert[0]; - - const size_t ysize = rect.ysize(); - JXL_CHECK(RunOnPool( - pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, - [&](const uint32_t task, size_t /*thread*/) { - const int64_t y = task; - - float* const JXL_RESTRICT row_out = out->Row(y); - for (size_t x = 0; x < rect.xsize(); ++x) { - row_out[x] = SlowSeparablePixel(in, rect, x, y, /*radius=*/2, - horz_weights, vert_weights); - } - }, - "SlowSeparable5")); -} - -void SlowSeparable5(const Image3F& in, const Rect& rect, - const WeightsSeparable5& weights, ThreadPool* pool, - Image3F* out) { - for (size_t c = 0; c < 3; ++c) { - SlowSeparable5(in.Plane(c), rect, weights, pool, &out->Plane(c)); - } -} - -void SlowSeparable7(const ImageF& in, const Rect& rect, - const WeightsSeparable7& weights, ThreadPool* pool, - ImageF* out) { - PROFILER_FUNC; - const float* horz_weights = &weights.horz[0]; - const float* vert_weights = &weights.vert[0]; - - const size_t ysize = rect.ysize(); - JXL_CHECK(RunOnPool( - pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, - [&](const uint32_t task, size_t /*thread*/) { - const int64_t y = task; - - float* const JXL_RESTRICT row_out = out->Row(y); - for (size_t x = 0; x < rect.xsize(); ++x) { - row_out[x] = SlowSeparablePixel(in, rect, x, y, /*radius=*/3, - horz_weights, vert_weights); - } - }, - "SlowSeparable7")); -} - -void SlowSeparable7(const Image3F& in, const Rect& rect, - const WeightsSeparable7& weights, ThreadPool* pool, - Image3F* out) { - for (size_t c = 0; c < 3; ++c) { - SlowSeparable7(in.Plane(c), rect, weights, pool, &out->Plane(c)); - } -} - -void SlowLaplacian5(const ImageF& in, const Rect& rect, ThreadPool* pool, - ImageF* out) { - PROFILER_FUNC; - JXL_CHECK(SameSize(rect, *out)); - - const size_t xsize = rect.xsize(); - const size_t ysize = rect.ysize(); - const WrapMirror wrap; - - JXL_CHECK(RunOnPool( - pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, - [&](const uint32_t task, size_t /*thread*/) { - const int64_t y = task; - - const float* const JXL_RESTRICT row_t = - rect.ConstRow(in, wrap(y - 2, ysize)); - const float* const JXL_RESTRICT row_m = rect.ConstRow(in, y); - const float* const JXL_RESTRICT row_b = - rect.ConstRow(in, wrap(y + 2, ysize)); - float* const JXL_RESTRICT row_out = out->Row(y); - - for (int64_t x = 0; static_cast<size_t>(x) < xsize; ++x) { - const int64_t xm2 = wrap(x - 2, xsize); - const int64_t xp2 = wrap(x + 2, xsize); - float r = 0.0f; - r += /* */ 1.0f * row_t[x]; - r += 1.0f * row_m[xm2] - 4.0f * row_m[x] + 1.0f * row_m[xp2]; - r += /* */ 1.0f * row_b[x]; - row_out[x] = r; - } - }, - "SlowLaplacian5")); -} - -void SlowLaplacian5(const Image3F& in, const Rect& rect, ThreadPool* pool, - Image3F* out) { - for (size_t c = 0; c < 3; ++c) { - SlowLaplacian5(in.Plane(c), rect, pool, &out->Plane(c)); - } -} - -} // namespace jxl -#endif // HWY_ONCE diff --git a/media/libjxl/src/lib/jxl/convolve.h b/media/libjxl/src/lib/jxl/convolve.h index c2e2ae42fb..2fcd2d0980 100644 --- a/media/libjxl/src/lib/jxl/convolve.h +++ b/media/libjxl/src/lib/jxl/convolve.h @@ -75,28 +75,14 @@ const WeightsSymmetric5& WeightsSymmetric5Lowpass(); void SlowSymmetric3(const ImageF& in, const Rect& rect, const WeightsSymmetric3& weights, ThreadPool* pool, ImageF* JXL_RESTRICT out); -void SlowSymmetric3(const Image3F& in, const Rect& rect, - const WeightsSymmetric3& weights, ThreadPool* pool, - Image3F* JXL_RESTRICT out); void SlowSeparable5(const ImageF& in, const Rect& rect, const WeightsSeparable5& weights, ThreadPool* pool, ImageF* out); -void SlowSeparable5(const Image3F& in, const Rect& rect, - const WeightsSeparable5& weights, ThreadPool* pool, - Image3F* out); void SlowSeparable7(const ImageF& in, const Rect& rect, const WeightsSeparable7& weights, ThreadPool* pool, ImageF* out); -void SlowSeparable7(const Image3F& in, const Rect& rect, - const WeightsSeparable7& weights, ThreadPool* pool, - Image3F* out); - -void SlowLaplacian5(const ImageF& in, const Rect& rect, ThreadPool* pool, - ImageF* out); -void SlowLaplacian5(const Image3F& in, const Rect& rect, ThreadPool* pool, - Image3F* out); void Symmetric3(const ImageF& in, const Rect& rect, const WeightsSymmetric3& weights, ThreadPool* pool, @@ -106,26 +92,14 @@ void Symmetric5(const ImageF& in, const Rect& rect, const WeightsSymmetric5& weights, ThreadPool* pool, ImageF* JXL_RESTRICT out); -void Symmetric5_3(const Image3F& in, const Rect& rect, - const WeightsSymmetric5& weights, ThreadPool* pool, - Image3F* JXL_RESTRICT out); - void Separable5(const ImageF& in, const Rect& rect, const WeightsSeparable5& weights, ThreadPool* pool, ImageF* out); -void Separable5_3(const Image3F& in, const Rect& rect, - const WeightsSeparable5& weights, ThreadPool* pool, - Image3F* out); - void Separable7(const ImageF& in, const Rect& rect, const WeightsSeparable7& weights, ThreadPool* pool, ImageF* out); -void Separable7_3(const Image3F& in, const Rect& rect, - const WeightsSeparable7& weights, ThreadPool* pool, - Image3F* out); - } // namespace jxl #endif // LIB_JXL_CONVOLVE_H_ diff --git a/media/libjxl/src/lib/jxl/convolve_separable5.cc b/media/libjxl/src/lib/jxl/convolve_separable5.cc new file mode 100644 index 0000000000..b26ff54bbc --- /dev/null +++ b/media/libjxl/src/lib/jxl/convolve_separable5.cc @@ -0,0 +1,261 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/convolve.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/convolve_separable5.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/jxl/convolve-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::Vec; + +// 5x5 convolution by separable kernel with a single scan through the input. +// This is more cache-efficient than separate horizontal/vertical passes, and +// possibly faster (given enough registers) than tiling and/or transposing. +// +// Overview: imagine a 5x5 window around a central pixel. First convolve the +// rows by multiplying the pixels with the corresponding weights from +// WeightsSeparable5.horz[abs(x_offset) * 4]. Then multiply each of these +// intermediate results by the corresponding vertical weight, i.e. +// vert[abs(y_offset) * 4]. Finally, store the sum of these values as the +// convolution result at the position of the central pixel in the output. +// +// Each of these operations uses SIMD vectors. The central pixel and most +// importantly the output are aligned, so neighnoring pixels (e.g. x_offset=1) +// require unaligned loads. Because weights are supplied in identical groups of +// 4, we can use LoadDup128 to load them (slightly faster). +// +// Uses mirrored boundary handling. Until x >= kRadius, the horizontal +// convolution uses Neighbors class to shuffle vectors as if each of its lanes +// had been loaded from the mirrored offset. Similarly, the last full vector to +// write uses mirroring. In the case of scalar vectors, Neighbors is not usable +// and the value is loaded directly. Otherwise, the number of valid pixels +// modulo the vector size enables a small optimization: for smaller offsets, +// a non-mirrored load is sufficient. +class Separable5Strategy { + using D = HWY_CAPPED(float, 16); + using V = Vec<D>; + + public: + static constexpr int64_t kRadius = 2; + + template <size_t kSizeModN, class WrapRow> + static JXL_MAYBE_INLINE void ConvolveRow( + const float* const JXL_RESTRICT row_m, const size_t xsize, + const int64_t stride, const WrapRow& wrap_row, + const WeightsSeparable5& weights, float* const JXL_RESTRICT row_out) { + const D d; + const int64_t neg_stride = -stride; // allows LEA addressing. + const float* const JXL_RESTRICT row_t2 = + wrap_row(row_m + 2 * neg_stride, stride); + const float* const JXL_RESTRICT row_t1 = + wrap_row(row_m + 1 * neg_stride, stride); + const float* const JXL_RESTRICT row_b1 = + wrap_row(row_m + 1 * stride, stride); + const float* const JXL_RESTRICT row_b2 = + wrap_row(row_m + 2 * stride, stride); + + const V wh0 = LoadDup128(d, weights.horz + 0 * 4); + const V wh1 = LoadDup128(d, weights.horz + 1 * 4); + const V wh2 = LoadDup128(d, weights.horz + 2 * 4); + const V wv0 = LoadDup128(d, weights.vert + 0 * 4); + const V wv1 = LoadDup128(d, weights.vert + 1 * 4); + const V wv2 = LoadDup128(d, weights.vert + 2 * 4); + + size_t x = 0; + + // More than one iteration for scalars. + for (; x < kRadius; x += Lanes(d)) { + const V conv0 = + Mul(HorzConvolveFirst(row_m, x, xsize, wh0, wh1, wh2), wv0); + + const V conv1t = HorzConvolveFirst(row_t1, x, xsize, wh0, wh1, wh2); + const V conv1b = HorzConvolveFirst(row_b1, x, xsize, wh0, wh1, wh2); + const V conv1 = MulAdd(Add(conv1t, conv1b), wv1, conv0); + + const V conv2t = HorzConvolveFirst(row_t2, x, xsize, wh0, wh1, wh2); + const V conv2b = HorzConvolveFirst(row_b2, x, xsize, wh0, wh1, wh2); + const V conv2 = MulAdd(Add(conv2t, conv2b), wv2, conv1); + Store(conv2, d, row_out + x); + } + + // Main loop: load inputs without padding + for (; x + Lanes(d) + kRadius <= xsize; x += Lanes(d)) { + const V conv0 = Mul(HorzConvolve(row_m + x, wh0, wh1, wh2), wv0); + + const V conv1t = HorzConvolve(row_t1 + x, wh0, wh1, wh2); + const V conv1b = HorzConvolve(row_b1 + x, wh0, wh1, wh2); + const V conv1 = MulAdd(Add(conv1t, conv1b), wv1, conv0); + + const V conv2t = HorzConvolve(row_t2 + x, wh0, wh1, wh2); + const V conv2b = HorzConvolve(row_b2 + x, wh0, wh1, wh2); + const V conv2 = MulAdd(Add(conv2t, conv2b), wv2, conv1); + Store(conv2, d, row_out + x); + } + + // Last full vector to write (the above loop handled mod >= kRadius) +#if HWY_TARGET == HWY_SCALAR + while (x < xsize) { +#else + if (kSizeModN < kRadius) { +#endif + const V conv0 = + Mul(HorzConvolveLast<kSizeModN>(row_m, x, xsize, wh0, wh1, wh2), wv0); + + const V conv1t = + HorzConvolveLast<kSizeModN>(row_t1, x, xsize, wh0, wh1, wh2); + const V conv1b = + HorzConvolveLast<kSizeModN>(row_b1, x, xsize, wh0, wh1, wh2); + const V conv1 = MulAdd(Add(conv1t, conv1b), wv1, conv0); + + const V conv2t = + HorzConvolveLast<kSizeModN>(row_t2, x, xsize, wh0, wh1, wh2); + const V conv2b = + HorzConvolveLast<kSizeModN>(row_b2, x, xsize, wh0, wh1, wh2); + const V conv2 = MulAdd(Add(conv2t, conv2b), wv2, conv1); + Store(conv2, d, row_out + x); + x += Lanes(d); + } + + // If mod = 0, the above vector was the last. + if (kSizeModN != 0) { + for (; x < xsize; ++x) { + float mul = 0.0f; + for (int64_t dy = -kRadius; dy <= kRadius; ++dy) { + const float wy = weights.vert[std::abs(dy) * 4]; + const float* clamped_row = wrap_row(row_m + dy * stride, stride); + for (int64_t dx = -kRadius; dx <= kRadius; ++dx) { + const float wx = weights.horz[std::abs(dx) * 4]; + const int64_t clamped_x = Mirror(x + dx, xsize); + mul += clamped_row[clamped_x] * wx * wy; + } + } + row_out[x] = mul; + } + } + } + + private: + // Same as HorzConvolve for the first/last vector in a row. + static JXL_MAYBE_INLINE V HorzConvolveFirst( + const float* const JXL_RESTRICT row, const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = Mul(c, wh0); + +#if HWY_TARGET == HWY_SCALAR + const V l1 = LoadU(d, row + Mirror(x - 1, xsize)); + const V l2 = LoadU(d, row + Mirror(x - 2, xsize)); +#else + (void)xsize; + const V l1 = Neighbors::FirstL1(c); + const V l2 = Neighbors::FirstL2(c); +#endif + + const V r1 = LoadU(d, row + x + 1); + const V r2 = LoadU(d, row + x + 2); + + const V mul1 = MulAdd(Add(l1, r1), wh1, mul0); + const V mul2 = MulAdd(Add(l2, r2), wh2, mul1); + return mul2; + } + + template <size_t kSizeModN> + static JXL_MAYBE_INLINE V + HorzConvolveLast(const float* const JXL_RESTRICT row, const int64_t x, + const int64_t xsize, const V wh0, const V wh1, const V wh2) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = Mul(c, wh0); + + const V l1 = LoadU(d, row + x - 1); + const V l2 = LoadU(d, row + x - 2); + + V r1, r2; +#if HWY_TARGET == HWY_SCALAR + r1 = LoadU(d, row + Mirror(x + 1, xsize)); + r2 = LoadU(d, row + Mirror(x + 2, xsize)); +#else + const size_t N = Lanes(d); + if (kSizeModN == 0) { + r2 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 2))); + r1 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 1))); + } else { // == 1 + const auto last = LoadU(d, row + xsize - N); + r2 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); + r1 = last; + } +#endif + + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = Add(l1, r1); + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = Add(l2, r2); + const V mul2 = MulAdd(sum2, wh2, mul1); + return mul2; + } + + // Requires kRadius valid pixels before/after pos. + static JXL_MAYBE_INLINE V HorzConvolve(const float* const JXL_RESTRICT pos, + const V wh0, const V wh1, + const V wh2) { + const D d; + const V c = LoadU(d, pos); + const V mul0 = Mul(c, wh0); + + // Loading anew is faster than combining vectors. + const V l1 = LoadU(d, pos - 1); + const V r1 = LoadU(d, pos + 1); + const V l2 = LoadU(d, pos - 2); + const V r2 = LoadU(d, pos + 2); + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = Add(l1, r1); + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = Add(l2, r2); + const V mul2 = MulAdd(sum2, wh2, mul1); + return mul2; + } +}; + +void Separable5(const ImageF& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + ImageF* out) { + using Conv = ConvolveT<Separable5Strategy>; + if (rect.xsize() >= Conv::MinWidth()) { + return Conv::Run(in, rect, weights, pool, out); + } + + return SlowSeparable5(in, rect, weights, pool, out); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(Separable5); +void Separable5(const ImageF& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + ImageF* out) { + return HWY_DYNAMIC_DISPATCH(Separable5)(in, rect, weights, pool, out); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/media/libjxl/src/lib/jxl/convolve_separable7.cc b/media/libjxl/src/lib/jxl/convolve_separable7.cc new file mode 100644 index 0000000000..086dfd22b5 --- /dev/null +++ b/media/libjxl/src/lib/jxl/convolve_separable7.cc @@ -0,0 +1,285 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/convolve.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/convolve_separable7.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/jxl/convolve-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::Vec; + +// 7x7 convolution by separable kernel with a single scan through the input. +// Extended version of Separable5, see documentation there. +class Separable7Strategy { + using D = HWY_CAPPED(float, 16); + using V = Vec<D>; + + public: + static constexpr int64_t kRadius = 3; + + template <size_t kSizeModN, class WrapRow> + static JXL_MAYBE_INLINE void ConvolveRow( + const float* const JXL_RESTRICT row_m, const size_t xsize, + const int64_t stride, const WrapRow& wrap_row, + const WeightsSeparable7& weights, float* const JXL_RESTRICT row_out) { + const D d; + const int64_t neg_stride = -stride; // allows LEA addressing. + const float* const JXL_RESTRICT row_t3 = + wrap_row(row_m + 3 * neg_stride, stride); + const float* const JXL_RESTRICT row_t2 = + wrap_row(row_m + 2 * neg_stride, stride); + const float* const JXL_RESTRICT row_t1 = + wrap_row(row_m + 1 * neg_stride, stride); + const float* const JXL_RESTRICT row_b1 = + wrap_row(row_m + 1 * stride, stride); + const float* const JXL_RESTRICT row_b2 = + wrap_row(row_m + 2 * stride, stride); + const float* const JXL_RESTRICT row_b3 = + wrap_row(row_m + 3 * stride, stride); + + const V wh0 = LoadDup128(d, weights.horz + 0 * 4); + const V wh1 = LoadDup128(d, weights.horz + 1 * 4); + const V wh2 = LoadDup128(d, weights.horz + 2 * 4); + const V wh3 = LoadDup128(d, weights.horz + 3 * 4); + const V wv0 = LoadDup128(d, weights.vert + 0 * 4); + const V wv1 = LoadDup128(d, weights.vert + 1 * 4); + const V wv2 = LoadDup128(d, weights.vert + 2 * 4); + const V wv3 = LoadDup128(d, weights.vert + 3 * 4); + + size_t x = 0; + + // More than one iteration for scalars. + for (; x < kRadius; x += Lanes(d)) { + const V conv0 = + Mul(HorzConvolveFirst(row_m, x, xsize, wh0, wh1, wh2, wh3), wv0); + + const V conv1t = HorzConvolveFirst(row_t1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1b = HorzConvolveFirst(row_b1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(Add(conv1t, conv1b), wv1, conv0); + + const V conv2t = HorzConvolveFirst(row_t2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2b = HorzConvolveFirst(row_b2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(Add(conv2t, conv2b), wv2, conv1); + + const V conv3t = HorzConvolveFirst(row_t3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3b = HorzConvolveFirst(row_b3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(Add(conv3t, conv3b), wv3, conv2); + + Store(conv3, d, row_out + x); + } + + // Main loop: load inputs without padding + for (; x + Lanes(d) + kRadius <= xsize; x += Lanes(d)) { + const V conv0 = Mul(HorzConvolve(row_m + x, wh0, wh1, wh2, wh3), wv0); + + const V conv1t = HorzConvolve(row_t1 + x, wh0, wh1, wh2, wh3); + const V conv1b = HorzConvolve(row_b1 + x, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(Add(conv1t, conv1b), wv1, conv0); + + const V conv2t = HorzConvolve(row_t2 + x, wh0, wh1, wh2, wh3); + const V conv2b = HorzConvolve(row_b2 + x, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(Add(conv2t, conv2b), wv2, conv1); + + const V conv3t = HorzConvolve(row_t3 + x, wh0, wh1, wh2, wh3); + const V conv3b = HorzConvolve(row_b3 + x, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(Add(conv3t, conv3b), wv3, conv2); + + Store(conv3, d, row_out + x); + } + + // Last full vector to write (the above loop handled mod >= kRadius) +#if HWY_TARGET == HWY_SCALAR + while (x < xsize) { +#else + if (kSizeModN < kRadius) { +#endif + const V conv0 = + Mul(HorzConvolveLast<kSizeModN>(row_m, x, xsize, wh0, wh1, wh2, wh3), + wv0); + + const V conv1t = + HorzConvolveLast<kSizeModN>(row_t1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1b = + HorzConvolveLast<kSizeModN>(row_b1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(Add(conv1t, conv1b), wv1, conv0); + + const V conv2t = + HorzConvolveLast<kSizeModN>(row_t2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2b = + HorzConvolveLast<kSizeModN>(row_b2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(Add(conv2t, conv2b), wv2, conv1); + + const V conv3t = + HorzConvolveLast<kSizeModN>(row_t3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3b = + HorzConvolveLast<kSizeModN>(row_b3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(Add(conv3t, conv3b), wv3, conv2); + + Store(conv3, d, row_out + x); + x += Lanes(d); + } + + // If mod = 0, the above vector was the last. + if (kSizeModN != 0) { + for (; x < xsize; ++x) { + float mul = 0.0f; + for (int64_t dy = -kRadius; dy <= kRadius; ++dy) { + const float wy = weights.vert[std::abs(dy) * 4]; + const float* clamped_row = wrap_row(row_m + dy * stride, stride); + for (int64_t dx = -kRadius; dx <= kRadius; ++dx) { + const float wx = weights.horz[std::abs(dx) * 4]; + const int64_t clamped_x = Mirror(x + dx, xsize); + mul += clamped_row[clamped_x] * wx * wy; + } + } + row_out[x] = mul; + } + } + } + + private: + // Same as HorzConvolve for the first/last vector in a row. + static JXL_MAYBE_INLINE V HorzConvolveFirst( + const float* const JXL_RESTRICT row, const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2, const V wh3) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = Mul(c, wh0); + +#if HWY_TARGET == HWY_SCALAR + const V l1 = LoadU(d, row + Mirror(x - 1, xsize)); + const V l2 = LoadU(d, row + Mirror(x - 2, xsize)); + const V l3 = LoadU(d, row + Mirror(x - 3, xsize)); +#else + (void)xsize; + const V l1 = Neighbors::FirstL1(c); + const V l2 = Neighbors::FirstL2(c); + const V l3 = Neighbors::FirstL3(c); +#endif + + const V r1 = LoadU(d, row + x + 1); + const V r2 = LoadU(d, row + x + 2); + const V r3 = LoadU(d, row + x + 3); + + const V mul1 = MulAdd(Add(l1, r1), wh1, mul0); + const V mul2 = MulAdd(Add(l2, r2), wh2, mul1); + const V mul3 = MulAdd(Add(l3, r3), wh3, mul2); + return mul3; + } + + template <size_t kSizeModN> + static JXL_MAYBE_INLINE V HorzConvolveLast( + const float* const JXL_RESTRICT row, const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2, const V wh3) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = Mul(c, wh0); + + const V l1 = LoadU(d, row + x - 1); + const V l2 = LoadU(d, row + x - 2); + const V l3 = LoadU(d, row + x - 3); + + V r1, r2, r3; +#if HWY_TARGET == HWY_SCALAR + r1 = LoadU(d, row + Mirror(x + 1, xsize)); + r2 = LoadU(d, row + Mirror(x + 2, xsize)); + r3 = LoadU(d, row + Mirror(x + 3, xsize)); +#else + const size_t N = Lanes(d); + if (kSizeModN == 0) { + r3 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 3))); + r2 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 2))); + r1 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 1))); + } else if (kSizeModN == 1) { + const auto last = LoadU(d, row + xsize - N); + r3 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 2))); + r2 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); + r1 = last; + } else /* kSizeModN >= 2 */ { + const auto last = LoadU(d, row + xsize - N); + r3 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); + r2 = last; + r1 = LoadU(d, row + x + 1); + } +#endif + + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = Add(l1, r1); + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = Add(l2, r2); + const V mul2 = MulAdd(sum2, wh2, mul1); + const V sum3 = Add(l3, r3); + const V mul3 = MulAdd(sum3, wh3, mul2); + return mul3; + } + + // Returns one vector of horizontal convolution results; lane i is the result + // for pixel pos + i. This is the fast path for interior pixels, i.e. kRadius + // valid pixels before/after pos. + static JXL_MAYBE_INLINE V HorzConvolve(const float* const JXL_RESTRICT pos, + const V wh0, const V wh1, const V wh2, + const V wh3) { + const D d; + const V c = LoadU(d, pos); + const V mul0 = Mul(c, wh0); + + // TODO(janwas): better to Combine + const V l1 = LoadU(d, pos - 1); + const V r1 = LoadU(d, pos + 1); + const V l2 = LoadU(d, pos - 2); + const V r2 = LoadU(d, pos + 2); + const V l3 = LoadU(d, pos - 3); + const V r3 = LoadU(d, pos + 3); + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = Add(l1, r1); + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = Add(l2, r2); + const V mul2 = MulAdd(sum2, wh2, mul1); + const V sum3 = Add(l3, r3); + const V mul3 = MulAdd(sum3, wh3, mul2); + return mul3; + } +}; + +void Separable7(const ImageF& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + ImageF* out) { + using Conv = ConvolveT<Separable7Strategy>; + if (rect.xsize() >= Conv::MinWidth()) { + return Conv::Run(in, rect, weights, pool, out); + } + + return SlowSeparable7(in, rect, weights, pool, out); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(Separable7); +void Separable7(const ImageF& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + ImageF* out) { + return HWY_DYNAMIC_DISPATCH(Separable7)(in, rect, weights, pool, out); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/media/libjxl/src/lib/jxl/convolve_slow.cc b/media/libjxl/src/lib/jxl/convolve_slow.cc new file mode 100644 index 0000000000..fffe5f74c8 --- /dev/null +++ b/media/libjxl/src/lib/jxl/convolve_slow.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 "lib/jxl/convolve.h" + +#include "lib/jxl/convolve-inl.h" + +namespace jxl { + +//------------------------------------------------------------------------------ +// Kernels + +// 4 instances of a given literal value, useful as input to LoadDup128. +#define JXL_REP4(literal) literal, literal, literal, literal + +// Concentrates energy in low-frequency components (e.g. for antialiasing). +const WeightsSymmetric3& WeightsSymmetric3Lowpass() { + // Computed by research/convolve_weights.py's cubic spline approximations of + // prolate spheroidal wave functions. + constexpr float w0 = 0.36208932f; + constexpr float w1 = 0.12820096f; + constexpr float w2 = 0.03127668f; + static constexpr WeightsSymmetric3 weights = { + {JXL_REP4(w0)}, {JXL_REP4(w1)}, {JXL_REP4(w2)}}; + return weights; +} + +const WeightsSeparable5& WeightsSeparable5Lowpass() { + constexpr float w0 = 0.41714928f; + constexpr float w1 = 0.25539268f; + constexpr float w2 = 0.03603267f; + static constexpr WeightsSeparable5 weights = { + {JXL_REP4(w0), JXL_REP4(w1), JXL_REP4(w2)}, + {JXL_REP4(w0), JXL_REP4(w1), JXL_REP4(w2)}}; + return weights; +} + +const WeightsSymmetric5& WeightsSymmetric5Lowpass() { + static constexpr WeightsSymmetric5 weights = { + {JXL_REP4(0.1740135f)}, {JXL_REP4(0.1065369f)}, {JXL_REP4(0.0150310f)}, + {JXL_REP4(0.0652254f)}, {JXL_REP4(0.0012984f)}, {JXL_REP4(0.0092025f)}}; + return weights; +} + +const WeightsSeparable5& WeightsSeparable5Gaussian1() { + constexpr float w0 = 0.38774f; + constexpr float w1 = 0.24477f; + constexpr float w2 = 0.06136f; + static constexpr WeightsSeparable5 weights = { + {JXL_REP4(w0), JXL_REP4(w1), JXL_REP4(w2)}, + {JXL_REP4(w0), JXL_REP4(w1), JXL_REP4(w2)}}; + return weights; +} + +const WeightsSeparable5& WeightsSeparable5Gaussian2() { + constexpr float w0 = 0.250301f; + constexpr float w1 = 0.221461f; + constexpr float w2 = 0.153388f; + static constexpr WeightsSeparable5 weights = { + {JXL_REP4(w0), JXL_REP4(w1), JXL_REP4(w2)}, + {JXL_REP4(w0), JXL_REP4(w1), JXL_REP4(w2)}}; + return weights; +} + +#undef JXL_REP4 + +//------------------------------------------------------------------------------ +// Slow + +namespace { + +template <class WrapX, class WrapY> +float SlowSymmetric3Pixel(const ImageF& in, const int64_t ix, const int64_t iy, + const int64_t xsize, const int64_t ysize, + const WeightsSymmetric3& weights) { + float sum = 0.0f; + + // ix: image; kx: kernel + for (int64_t ky = -1; ky <= 1; ky++) { + const int64_t y = WrapY()(iy + ky, ysize); + const float* JXL_RESTRICT row_in = in.ConstRow(static_cast<size_t>(y)); + + const float wc = ky == 0 ? weights.c[0] : weights.r[0]; + const float wlr = ky == 0 ? weights.r[0] : weights.d[0]; + + const int64_t xm1 = WrapX()(ix - 1, xsize); + const int64_t xp1 = WrapX()(ix + 1, xsize); + sum += row_in[ix] * wc + (row_in[xm1] + row_in[xp1]) * wlr; + } + return sum; +} + +template <class WrapY> +void SlowSymmetric3Row(const ImageF& in, const int64_t iy, const int64_t xsize, + const int64_t ysize, const WeightsSymmetric3& weights, + float* JXL_RESTRICT row_out) { + row_out[0] = + SlowSymmetric3Pixel<WrapMirror, WrapY>(in, 0, iy, xsize, ysize, weights); + for (int64_t ix = 1; ix < xsize - 1; ix++) { + row_out[ix] = SlowSymmetric3Pixel<WrapUnchanged, WrapY>(in, ix, iy, xsize, + ysize, weights); + } + { + const int64_t ix = xsize - 1; + row_out[ix] = SlowSymmetric3Pixel<WrapMirror, WrapY>(in, ix, iy, xsize, + ysize, weights); + } +} + +} // namespace + +void SlowSymmetric3(const ImageF& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + ImageF* JXL_RESTRICT out) { + PROFILER_FUNC; + + const int64_t xsize = static_cast<int64_t>(rect.xsize()); + const int64_t ysize = static_cast<int64_t>(rect.ysize()); + const int64_t kRadius = 1; + + JXL_CHECK(RunOnPool( + pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, + [&](const uint32_t task, size_t /*thread*/) { + const int64_t iy = task; + float* JXL_RESTRICT out_row = out->Row(static_cast<size_t>(iy)); + + if (iy < kRadius || iy >= ysize - kRadius) { + SlowSymmetric3Row<WrapMirror>(in, iy, xsize, ysize, weights, out_row); + } else { + SlowSymmetric3Row<WrapUnchanged>(in, iy, xsize, ysize, weights, + out_row); + } + }, + "SlowSymmetric3")); +} + +namespace { + +// Separable kernels, any radius. +float SlowSeparablePixel(const ImageF& in, const Rect& rect, const int64_t x, + const int64_t y, const int64_t radius, + const float* JXL_RESTRICT horz_weights, + const float* JXL_RESTRICT vert_weights) { + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + const WrapMirror wrap; + + float mul = 0.0f; + for (int dy = -radius; dy <= radius; ++dy) { + const float wy = vert_weights[std::abs(dy) * 4]; + const size_t sy = wrap(y + dy, ysize); + JXL_CHECK(sy < ysize); + const float* const JXL_RESTRICT row = rect.ConstRow(in, sy); + for (int dx = -radius; dx <= radius; ++dx) { + const float wx = horz_weights[std::abs(dx) * 4]; + const size_t sx = wrap(x + dx, xsize); + JXL_CHECK(sx < xsize); + mul += row[sx] * wx * wy; + } + } + return mul; +} + +} // namespace + +void SlowSeparable5(const ImageF& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + ImageF* out) { + PROFILER_FUNC; + const float* horz_weights = &weights.horz[0]; + const float* vert_weights = &weights.vert[0]; + + const size_t ysize = rect.ysize(); + JXL_CHECK(RunOnPool( + pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, + [&](const uint32_t task, size_t /*thread*/) { + const int64_t y = task; + + float* const JXL_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < rect.xsize(); ++x) { + row_out[x] = SlowSeparablePixel(in, rect, x, y, /*radius=*/2, + horz_weights, vert_weights); + } + }, + "SlowSeparable5")); +} + +void SlowSeparable7(const ImageF& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + ImageF* out) { + PROFILER_FUNC; + const float* horz_weights = &weights.horz[0]; + const float* vert_weights = &weights.vert[0]; + + const size_t ysize = rect.ysize(); + JXL_CHECK(RunOnPool( + pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, + [&](const uint32_t task, size_t /*thread*/) { + const int64_t y = task; + + float* const JXL_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < rect.xsize(); ++x) { + row_out[x] = SlowSeparablePixel(in, rect, x, y, /*radius=*/3, + horz_weights, vert_weights); + } + }, + "SlowSeparable7")); +} + +} // namespace jxl diff --git a/media/libjxl/src/lib/jxl/convolve_symmetric3.cc b/media/libjxl/src/lib/jxl/convolve_symmetric3.cc new file mode 100644 index 0000000000..06b59dfb60 --- /dev/null +++ b/media/libjxl/src/lib/jxl/convolve_symmetric3.cc @@ -0,0 +1,194 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/convolve.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/convolve_symmetric3.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/jxl/convolve-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::Vec; + +template <class WrapY, class V> +static V WeightedSum(const ImageF& in, const WrapY wrap_y, const size_t ix, + const int64_t iy, const size_t ysize, const V wx0, + const V wx1, const V wx2) { + const HWY_FULL(float) d; + const float* JXL_RESTRICT center = in.ConstRow(wrap_y(iy, ysize)) + ix; + const auto in_m2 = LoadU(d, center - 2); + const auto in_p2 = LoadU(d, center + 2); + const auto in_m1 = LoadU(d, center - 1); + const auto in_p1 = LoadU(d, center + 1); + const auto in_00 = Load(d, center); + const auto sum_2 = Mul(wx2, Add(in_m2, in_p2)); + const auto sum_1 = Mul(wx1, Add(in_m1, in_p1)); + const auto sum_0 = Mul(wx0, in_00); + return Add(sum_2, Add(sum_1, sum_0)); +} + +// 3x3 convolution by symmetric kernel with a single scan through the input. +class Symmetric3Strategy { + using D = HWY_CAPPED(float, 16); + using V = Vec<D>; + + public: + static constexpr int64_t kRadius = 1; + + // Only accesses pixels in [0, xsize). + template <size_t kSizeModN, class WrapRow> + static JXL_MAYBE_INLINE void ConvolveRow( + const float* const JXL_RESTRICT row_m, const size_t xsize, + const int64_t stride, const WrapRow& wrap_row, + const WeightsSymmetric3& weights, float* const JXL_RESTRICT row_out) { + const D d; + // t, m, b = top, middle, bottom row; + const float* const JXL_RESTRICT row_t = wrap_row(row_m - stride, stride); + const float* const JXL_RESTRICT row_b = wrap_row(row_m + stride, stride); + + // Must load in advance - compiler doesn't understand LoadDup128 and + // schedules them too late. + const V w0 = LoadDup128(d, weights.c); + const V w1 = LoadDup128(d, weights.r); + const V w2 = LoadDup128(d, weights.d); + + // l, c, r = left, center, right. Leftmost vector: need FirstL1. + { + const V tc = LoadU(d, row_t + 0); + const V mc = LoadU(d, row_m + 0); + const V bc = LoadU(d, row_b + 0); + const V tl = Neighbors::FirstL1(tc); + const V tr = LoadU(d, row_t + 0 + 1); + const V ml = Neighbors::FirstL1(mc); + const V mr = LoadU(d, row_m + 0 + 1); + const V bl = Neighbors::FirstL1(bc); + const V br = LoadU(d, row_b + 0 + 1); + const V conv = + WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + Store(conv, d, row_out + 0); + } + + // Loop as long as we can load enough new values: + const size_t N = Lanes(d); + size_t x = N; + for (; x + N + kRadius <= xsize; x += N) { + const auto conv = ConvolveValid(row_t, row_m, row_b, x, w0, w1, w2); + Store(conv, d, row_out + x); + } + + // For final (partial) vector: + const V tc = LoadU(d, row_t + x); + const V mc = LoadU(d, row_m + x); + const V bc = LoadU(d, row_b + x); + + V tr, mr, br; +#if HWY_TARGET == HWY_SCALAR + tr = tc; // Single-lane => mirrored right neighbor = center value. + mr = mc; + br = bc; +#else + if (kSizeModN == 0) { + // The above loop didn't handle the last vector because it needs an + // additional right neighbor (generated via mirroring). + auto mirror = SetTableIndices(d, MirrorLanes(N - 1)); + tr = TableLookupLanes(tc, mirror); + mr = TableLookupLanes(mc, mirror); + br = TableLookupLanes(bc, mirror); + } else { + auto mirror = SetTableIndices(d, MirrorLanes((xsize % N) - 1)); + // Loads last valid value into uppermost lane and mirrors. + tr = TableLookupLanes(LoadU(d, row_t + xsize - N), mirror); + mr = TableLookupLanes(LoadU(d, row_m + xsize - N), mirror); + br = TableLookupLanes(LoadU(d, row_b + xsize - N), mirror); + } +#endif + + const V tl = LoadU(d, row_t + x - 1); + const V ml = LoadU(d, row_m + x - 1); + const V bl = LoadU(d, row_b + x - 1); + const V conv = WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + Store(conv, d, row_out + x); + } + + private: + // Returns sum{x_i * w_i}. + template <class V> + static JXL_MAYBE_INLINE V WeightedSum(const V tl, const V tc, const V tr, + const V ml, const V mc, const V mr, + const V bl, const V bc, const V br, + const V w0, const V w1, const V w2) { + const V sum_tb = Add(tc, bc); + + // Faster than 5 mul + 4 FMA. + const V mul0 = Mul(mc, w0); + const V sum_lr = Add(ml, mr); + + const V x1 = Add(sum_tb, sum_lr); + const V mul1 = MulAdd(x1, w1, mul0); + + const V sum_t2 = Add(tl, tr); + const V sum_b2 = Add(bl, br); + const V x2 = Add(sum_t2, sum_b2); + const V mul2 = MulAdd(x2, w2, mul1); + return mul2; + } + + static JXL_MAYBE_INLINE V ConvolveValid(const float* JXL_RESTRICT row_t, + const float* JXL_RESTRICT row_m, + const float* JXL_RESTRICT row_b, + const int64_t x, const V w0, + const V w1, const V w2) { + const D d; + const V tc = LoadU(d, row_t + x); + const V mc = LoadU(d, row_m + x); + const V bc = LoadU(d, row_b + x); + const V tl = LoadU(d, row_t + x - 1); + const V tr = LoadU(d, row_t + x + 1); + const V ml = LoadU(d, row_m + x - 1); + const V mr = LoadU(d, row_m + x + 1); + const V bl = LoadU(d, row_b + x - 1); + const V br = LoadU(d, row_b + x + 1); + return WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + } +}; + +void Symmetric3(const ImageF& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + ImageF* out) { + using Conv = ConvolveT<Symmetric3Strategy>; + if (rect.xsize() >= Conv::MinWidth()) { + return Conv::Run(in, rect, weights, pool, out); + } + + return SlowSymmetric3(in, rect, weights, pool, out); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(Symmetric3); +void Symmetric3(const ImageF& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + ImageF* out) { + return HWY_DYNAMIC_DISPATCH(Symmetric3)(in, rect, weights, pool, out); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/media/libjxl/src/lib/jxl/convolve_symmetric5.cc b/media/libjxl/src/lib/jxl/convolve_symmetric5.cc new file mode 100644 index 0000000000..55a16899c3 --- /dev/null +++ b/media/libjxl/src/lib/jxl/convolve_symmetric5.cc @@ -0,0 +1,185 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/convolve.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/convolve_symmetric5.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/jxl/common.h" // RoundUpTo +#include "lib/jxl/convolve-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::Vec; + +// Weighted sum of 1x5 pixels around ix, iy with [wx2 wx1 wx0 wx1 wx2]. +template <class WrapY> +static float WeightedSumBorder(const ImageF& in, const WrapY wrap_y, + const int64_t ix, const int64_t iy, + const size_t xsize, const size_t ysize, + const float wx0, const float wx1, + const float wx2) { + const WrapMirror wrap_x; + const float* JXL_RESTRICT row = in.ConstRow(wrap_y(iy, ysize)); + const float in_m2 = row[wrap_x(ix - 2, xsize)]; + const float in_p2 = row[wrap_x(ix + 2, xsize)]; + const float in_m1 = row[wrap_x(ix - 1, xsize)]; + const float in_p1 = row[wrap_x(ix + 1, xsize)]; + const float in_00 = row[ix]; + const float sum_2 = wx2 * (in_m2 + in_p2); + const float sum_1 = wx1 * (in_m1 + in_p1); + const float sum_0 = wx0 * in_00; + return sum_2 + sum_1 + sum_0; +} + +template <class WrapY, class V> +static V WeightedSum(const ImageF& in, const WrapY wrap_y, const size_t ix, + const int64_t iy, const size_t ysize, const V wx0, + const V wx1, const V wx2) { + const HWY_FULL(float) d; + const float* JXL_RESTRICT center = in.ConstRow(wrap_y(iy, ysize)) + ix; + const auto in_m2 = LoadU(d, center - 2); + const auto in_p2 = LoadU(d, center + 2); + const auto in_m1 = LoadU(d, center - 1); + const auto in_p1 = LoadU(d, center + 1); + const auto in_00 = Load(d, center); + const auto sum_2 = Mul(wx2, Add(in_m2, in_p2)); + const auto sum_1 = Mul(wx1, Add(in_m1, in_p1)); + const auto sum_0 = Mul(wx0, in_00); + return Add(sum_2, Add(sum_1, sum_0)); +} + +// Produces result for one pixel +template <class WrapY> +float Symmetric5Border(const ImageF& in, const Rect& rect, const int64_t ix, + const int64_t iy, const WeightsSymmetric5& weights) { + const float w0 = weights.c[0]; + const float w1 = weights.r[0]; + const float w2 = weights.R[0]; + const float w4 = weights.d[0]; + const float w5 = weights.L[0]; + const float w8 = weights.D[0]; + + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + const WrapY wrap_y; + // Unrolled loop over all 5 rows of the kernel. + float sum0 = WeightedSumBorder(in, wrap_y, ix, iy, xsize, ysize, w0, w1, w2); + + sum0 += WeightedSumBorder(in, wrap_y, ix, iy - 2, xsize, ysize, w2, w5, w8); + float sum1 = + WeightedSumBorder(in, wrap_y, ix, iy + 2, xsize, ysize, w2, w5, w8); + + sum0 += WeightedSumBorder(in, wrap_y, ix, iy - 1, xsize, ysize, w1, w4, w5); + sum1 += WeightedSumBorder(in, wrap_y, ix, iy + 1, xsize, ysize, w1, w4, w5); + + return sum0 + sum1; +} + +// Produces result for one vector's worth of pixels +template <class WrapY> +static void Symmetric5Interior(const ImageF& in, const Rect& rect, + const int64_t ix, const int64_t iy, + const WeightsSymmetric5& weights, + float* JXL_RESTRICT row_out) { + const HWY_FULL(float) d; + + const auto w0 = LoadDup128(d, weights.c); + const auto w1 = LoadDup128(d, weights.r); + const auto w2 = LoadDup128(d, weights.R); + const auto w4 = LoadDup128(d, weights.d); + const auto w5 = LoadDup128(d, weights.L); + const auto w8 = LoadDup128(d, weights.D); + + const size_t ysize = rect.ysize(); + const WrapY wrap_y; + // Unrolled loop over all 5 rows of the kernel. + auto sum0 = WeightedSum(in, wrap_y, ix, iy, ysize, w0, w1, w2); + + sum0 = Add(sum0, WeightedSum(in, wrap_y, ix, iy - 2, ysize, w2, w5, w8)); + auto sum1 = WeightedSum(in, wrap_y, ix, iy + 2, ysize, w2, w5, w8); + + sum0 = Add(sum0, WeightedSum(in, wrap_y, ix, iy - 1, ysize, w1, w4, w5)); + sum1 = Add(sum1, WeightedSum(in, wrap_y, ix, iy + 1, ysize, w1, w4, w5)); + + Store(Add(sum0, sum1), d, row_out + ix); +} + +template <class WrapY> +static void Symmetric5Row(const ImageF& in, const Rect& rect, const int64_t iy, + const WeightsSymmetric5& weights, + float* JXL_RESTRICT row_out) { + const int64_t kRadius = 2; + const size_t xsize = rect.xsize(); + + size_t ix = 0; + const HWY_FULL(float) d; + const size_t N = Lanes(d); + const size_t aligned_x = RoundUpTo(kRadius, N); + for (; ix < std::min(aligned_x, xsize); ++ix) { + row_out[ix] = Symmetric5Border<WrapY>(in, rect, ix, iy, weights); + } + for (; ix + N + kRadius <= xsize; ix += N) { + Symmetric5Interior<WrapY>(in, rect, ix, iy, weights, row_out); + } + for (; ix < xsize; ++ix) { + row_out[ix] = Symmetric5Border<WrapY>(in, rect, ix, iy, weights); + } +} + +static JXL_NOINLINE void Symmetric5BorderRow(const ImageF& in, const Rect& rect, + const int64_t iy, + const WeightsSymmetric5& weights, + float* JXL_RESTRICT row_out) { + return Symmetric5Row<WrapMirror>(in, rect, iy, weights, row_out); +} + +// Semi-vectorized (interior pixels Fonly); called directly like slow::, unlike +// the fully vectorized strategies below. +void Symmetric5(const ImageF& in, const Rect& rect, + const WeightsSymmetric5& weights, ThreadPool* pool, + ImageF* JXL_RESTRICT out) { + PROFILER_FUNC; + + const size_t ysize = rect.ysize(); + JXL_CHECK(RunOnPool( + pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, + [&](const uint32_t task, size_t /*thread*/) { + const int64_t iy = task; + + if (iy < 2 || iy >= static_cast<ssize_t>(ysize) - 2) { + Symmetric5BorderRow(in, rect, iy, weights, out->Row(iy)); + } else { + Symmetric5Row<WrapUnchanged>(in, rect, iy, weights, out->Row(iy)); + } + }, + "Symmetric5x5Convolution")); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(Symmetric5); +void Symmetric5(const ImageF& in, const Rect& rect, + const WeightsSymmetric5& weights, ThreadPool* pool, + ImageF* JXL_RESTRICT out) { + return HWY_DYNAMIC_DISPATCH(Symmetric5)(in, rect, weights, pool, out); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/media/libjxl/src/lib/jxl/convolve_test.cc b/media/libjxl/src/lib/jxl/convolve_test.cc index b31eb2f973..2d75c31ea8 100644 --- a/media/libjxl/src/lib/jxl/convolve_test.cc +++ b/media/libjxl/src/lib/jxl/convolve_test.cc @@ -148,9 +148,9 @@ void TestConvolve() { ThreadPoolInternal pool3(3); for (size_t ysize = kConvolveMaxRadius; ysize < 16; ++ysize) { JXL_DEBUG(JXL_DEBUG_CONVOLVE, - "%" PRIuS " x %" PRIuS - " (target %d)===============================", - xsize, ysize, HWY_TARGET); + "%" PRIuS " x %" PRIuS " (target %" PRIx64 + ")===============================", + xsize, ysize, static_cast<int64_t>(HWY_TARGET)); JXL_DEBUG(JXL_DEBUG_CONVOLVE, "Sym3------------------"); VerifySymmetric3(xsize, ysize, null_pool, &rng); diff --git a/media/libjxl/src/lib/jxl/data_parallel_test.cc b/media/libjxl/src/lib/jxl/data_parallel_test.cc index 79aee0f3f7..dd6ea625fb 100644 --- a/media/libjxl/src/lib/jxl/data_parallel_test.cc +++ b/media/libjxl/src/lib/jxl/data_parallel_test.cc @@ -51,7 +51,7 @@ int TestInit(void* jpegxl_opaque, size_t num_threads) { return 0; } } // namespace -TEST_F(DataParallelTest, RunnerCalledParamenters) { +TEST_F(DataParallelTest, RunnerCalledParameters) { EXPECT_TRUE(pool_.Run( 1234, 5678, [](size_t /* num_threads */) { return true; }, [](uint32_t /* task */, size_t /* thread */) { return; })); diff --git a/media/libjxl/src/lib/jxl/dct-inl.h b/media/libjxl/src/lib/jxl/dct-inl.h index 5ccb9341df..532606075e 100644 --- a/media/libjxl/src/lib/jxl/dct-inl.h +++ b/media/libjxl/src/lib/jxl/dct-inl.h @@ -24,6 +24,13 @@ namespace jxl { namespace HWY_NAMESPACE { namespace { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::NegMulAdd; +using hwy::HWY_NAMESPACE::Sub; + template <size_t SZ> struct FVImpl { using type = HWY_CAPPED(float, SZ); @@ -48,7 +55,7 @@ struct CoeffBundle { for (size_t i = 0; i < N; i++) { auto in1 = Load(FV<SZ>(), ain1 + i * SZ); auto in2 = Load(FV<SZ>(), ain2 + (N - i - 1) * SZ); - Store(in1 + in2, FV<SZ>(), aout + i * SZ); + Store(Add(in1, in2), FV<SZ>(), aout + i * SZ); } } static void SubReverse(const float* JXL_RESTRICT ain1, @@ -57,7 +64,7 @@ struct CoeffBundle { for (size_t i = 0; i < N; i++) { auto in1 = Load(FV<SZ>(), ain1 + i * SZ); auto in2 = Load(FV<SZ>(), ain2 + (N - i - 1) * SZ); - Store(in1 - in2, FV<SZ>(), aout + i * SZ); + Store(Sub(in1, in2), FV<SZ>(), aout + i * SZ); } } static void B(float* JXL_RESTRICT coeff) { @@ -68,18 +75,18 @@ struct CoeffBundle { for (size_t i = 1; i + 1 < N; i++) { auto in1 = Load(FV<SZ>(), coeff + i * SZ); auto in2 = Load(FV<SZ>(), coeff + (i + 1) * SZ); - Store(in1 + in2, FV<SZ>(), coeff + i * SZ); + Store(Add(in1, in2), FV<SZ>(), coeff + i * SZ); } } static void BTranspose(float* JXL_RESTRICT coeff) { for (size_t i = N - 1; i > 0; i--) { auto in1 = Load(FV<SZ>(), coeff + i * SZ); auto in2 = Load(FV<SZ>(), coeff + (i - 1) * SZ); - Store(in1 + in2, FV<SZ>(), coeff + i * SZ); + Store(Add(in1, in2), FV<SZ>(), coeff + i * SZ); } auto sqrt2 = Set(FV<SZ>(), kSqrt2); auto in1 = Load(FV<SZ>(), coeff); - Store(in1 * sqrt2, FV<SZ>(), coeff); + Store(Mul(in1, sqrt2), FV<SZ>(), coeff); } // Ideally optimized away by compiler (except the multiply). static void InverseEvenOdd(const float* JXL_RESTRICT ain, @@ -110,7 +117,7 @@ struct CoeffBundle { for (size_t i = 0; i < N / 2; i++) { auto in1 = Load(FV<SZ>(), coeff + (N / 2 + i) * SZ); auto mul = Set(FV<SZ>(), WcMultipliers<N>::kMultipliers[i]); - Store(in1 * mul, FV<SZ>(), coeff + (N / 2 + i) * SZ); + Store(Mul(in1, mul), FV<SZ>(), coeff + (N / 2 + i) * SZ); } } static void MultiplyAndAdd(const float* JXL_RESTRICT coeff, @@ -137,7 +144,7 @@ struct CoeffBundle { const Block& out, size_t off) { auto mul = Set(FV<SZ>(), 1.0f / N); for (size_t i = 0; i < N; i++) { - out.StorePart(FV<SZ>(), mul * Load(FV<SZ>(), coeff + i * SZ), i, off); + out.StorePart(FV<SZ>(), Mul(mul, Load(FV<SZ>(), coeff + i * SZ)), i, off); } } }; @@ -155,8 +162,8 @@ struct DCT1DImpl<2, SZ> { JXL_INLINE void operator()(float* JXL_RESTRICT mem) { auto in1 = Load(FV<SZ>(), mem); auto in2 = Load(FV<SZ>(), mem + SZ); - Store(in1 + in2, FV<SZ>(), mem); - Store(in1 - in2, FV<SZ>(), mem + SZ); + Store(Add(in1, in2), FV<SZ>(), mem); + Store(Sub(in1, in2), FV<SZ>(), mem + SZ); } }; @@ -194,8 +201,8 @@ struct IDCT1DImpl<2, SZ> { JXL_DASSERT(to_stride >= SZ); auto in1 = LoadU(FV<SZ>(), from); auto in2 = LoadU(FV<SZ>(), from + from_stride); - StoreU(in1 + in2, FV<SZ>(), to); - StoreU(in1 - in2, FV<SZ>(), to + to_stride); + StoreU(Add(in1, in2), FV<SZ>(), to); + StoreU(Sub(in1, in2), FV<SZ>(), to + to_stride); } }; diff --git a/media/libjxl/src/lib/jxl/dec_ans.cc b/media/libjxl/src/lib/jxl/dec_ans.cc index a64493237e..c9145472e0 100644 --- a/media/libjxl/src/lib/jxl/dec_ans.cc +++ b/media/libjxl/src/lib/jxl/dec_ans.cc @@ -48,7 +48,7 @@ inline int DecodeVarLenUint16(BitReader* input) { return 0; } -Status ReadHistogram(int precision_bits, std::vector<int>* counts, +Status ReadHistogram(int precision_bits, std::vector<int32_t>* counts, BitReader* input) { int simple_code = input->ReadBits(1); if (simple_code == 1) { @@ -228,7 +228,7 @@ Status DecodeANSCodes(const size_t num_histograms, AliasTable::Entry* alias_tables = reinterpret_cast<AliasTable::Entry*>(result->alias_tables.get()); for (size_t c = 0; c < num_histograms; ++c) { - std::vector<int> counts; + std::vector<int32_t> counts; if (!ReadHistogram(ANS_LOG_TAB_SIZE, &counts, in)) { return JXL_FAILURE("Invalid histogram bitstream."); } diff --git a/media/libjxl/src/lib/jxl/dec_cache.cc b/media/libjxl/src/lib/jxl/dec_cache.cc index eb11297014..b819b5104a 100644 --- a/media/libjxl/src/lib/jxl/dec_cache.cc +++ b/media/libjxl/src/lib/jxl/dec_cache.cc @@ -9,11 +9,14 @@ #include "lib/jxl/render_pipeline/stage_blending.h" #include "lib/jxl/render_pipeline/stage_chroma_upsampling.h" #include "lib/jxl/render_pipeline/stage_epf.h" +#include "lib/jxl/render_pipeline/stage_from_linear.h" #include "lib/jxl/render_pipeline/stage_gaborish.h" #include "lib/jxl/render_pipeline/stage_noise.h" #include "lib/jxl/render_pipeline/stage_patches.h" #include "lib/jxl/render_pipeline/stage_splines.h" #include "lib/jxl/render_pipeline/stage_spot.h" +#include "lib/jxl/render_pipeline/stage_to_linear.h" +#include "lib/jxl/render_pipeline/stage_tone_mapping.h" #include "lib/jxl/render_pipeline/stage_upsampling.h" #include "lib/jxl/render_pipeline/stage_write.h" #include "lib/jxl/render_pipeline/stage_xyb.h" @@ -89,7 +92,9 @@ Status PassesDecoderState::PreparePipeline(ImageBundle* decoded, } if ((frame_header.flags & FrameHeader::kPatches) != 0) { - builder.AddStage(GetPatchesStage(&shared->image_features.patches)); + builder.AddStage( + GetPatchesStage(&shared->image_features.patches, + 3 + shared->metadata->m.num_extra_channels)); } if ((frame_header.flags & FrameHeader::kSplines) != 0) { builder.AddStage(GetSplineStage(&shared->image_features.splines)); @@ -150,19 +155,29 @@ Status PassesDecoderState::PreparePipeline(ImageBundle* decoded, height, rgb_output_is_rgba, has_alpha, alpha_c)); } else { + bool linear = false; if (frame_header.color_transform == ColorTransform::kYCbCr) { builder.AddStage(GetYCbCrStage()); } else if (frame_header.color_transform == ColorTransform::kXYB) { - builder.AddStage(GetXYBStage(output_encoding_info)); + builder.AddStage(GetXYBStage(output_encoding_info.opsin_params)); + linear = true; } // Nothing to do for kNone. if (options.coalescing && NeedsBlending(this)) { + if (linear) { + builder.AddStage(GetFromLinearStage(output_encoding_info)); + linear = false; + } builder.AddStage( GetBlendingStage(this, output_encoding_info.color_encoding)); } if (options.coalescing && frame_header.CanBeReferenced() && !frame_header.save_before_color_transform) { + if (linear) { + builder.AddStage(GetFromLinearStage(output_encoding_info)); + linear = false; + } builder.AddStage(GetWriteToImageBundleStage( &frame_storage_for_referencing, output_encoding_info.color_encoding)); } @@ -180,10 +195,30 @@ Status PassesDecoderState::PreparePipeline(ImageBundle* decoded, } } + auto tone_mapping_stage = GetToneMappingStage(output_encoding_info); + if (tone_mapping_stage) { + if (!linear) { + auto to_linear_stage = GetToLinearStage(output_encoding_info); + if (!to_linear_stage) { + return JXL_FAILURE( + "attempting to perform tone mapping on colorspace not " + "convertible to linear"); + } + builder.AddStage(std::move(to_linear_stage)); + linear = true; + } + builder.AddStage(std::move(tone_mapping_stage)); + } + + if (linear) { + builder.AddStage(GetFromLinearStage(output_encoding_info)); + linear = false; + } + if (pixel_callback.IsPresent()) { - builder.AddStage(GetWriteToPixelCallbackStage(pixel_callback, width, - height, rgb_output_is_rgba, - has_alpha, alpha_c)); + builder.AddStage(GetWriteToPixelCallbackStage( + pixel_callback, width, height, rgb_output_is_rgba, has_alpha, + unpremul_alpha, alpha_c)); } else if (rgb_output) { builder.AddStage(GetWriteToU8Stage(rgb_output, rgb_stride, height, rgb_output_is_rgba, has_alpha, diff --git a/media/libjxl/src/lib/jxl/dec_cache.h b/media/libjxl/src/lib/jxl/dec_cache.h index 7f1b15315b..7105ba8ba8 100644 --- a/media/libjxl/src/lib/jxl/dec_cache.h +++ b/media/libjxl/src/lib/jxl/dec_cache.h @@ -8,6 +8,7 @@ #include <stdint.h> +#include <atomic> #include <hwy/base.h> // HWY_ALIGN_MAX #include "jxl/decode.h" @@ -89,6 +90,9 @@ struct PassesDecoderState { // If true, rgb_output or callback output is RGBA using 4 instead of 3 bytes // per pixel. bool rgb_output_is_rgba; + // If true, the RGBA output will be unpremultiplied before writing to the + // output callback (the output buffer case is handled in ConvertToExternal). + bool unpremul_alpha; // Callback for line-by-line output. PixelCallback pixel_callback; @@ -134,7 +138,9 @@ struct PassesDecoderState { rgb_output = nullptr; rgb_output_is_rgba = false; + unpremul_alpha = false; fast_xyb_srgb8_conversion = false; + pixel_callback = PixelCallback(); used_acs = 0; upsampler8x = GetUpsamplingStage(shared->metadata->transform_data, 0, 3); diff --git a/media/libjxl/src/lib/jxl/dec_context_map.cc b/media/libjxl/src/lib/jxl/dec_context_map.cc index f7fc3d27a4..93c59f773b 100644 --- a/media/libjxl/src/lib/jxl/dec_context_map.cc +++ b/media/libjxl/src/lib/jxl/dec_context_map.cc @@ -37,8 +37,8 @@ void InverseMoveToFrontTransform(uint8_t* v, int v_len) { } } -bool VerifyContextMap(const std::vector<uint8_t>& context_map, - const size_t num_htrees) { +Status VerifyContextMap(const std::vector<uint8_t>& context_map, + const size_t num_htrees) { std::vector<bool> have_htree(num_htrees); size_t num_found = 0; for (const uint8_t htree : context_map) { @@ -58,8 +58,8 @@ bool VerifyContextMap(const std::vector<uint8_t>& context_map, } // namespace -bool DecodeContextMap(std::vector<uint8_t>* context_map, size_t* num_htrees, - BitReader* input) { +Status DecodeContextMap(std::vector<uint8_t>* context_map, size_t* num_htrees, + BitReader* input) { bool is_simple = input->ReadFixedBits<1>(); if (is_simple) { int bits_per_entry = input->ReadFixedBits<2>(); diff --git a/media/libjxl/src/lib/jxl/dec_context_map.h b/media/libjxl/src/lib/jxl/dec_context_map.h index 1db2317827..95b8a0ca92 100644 --- a/media/libjxl/src/lib/jxl/dec_context_map.h +++ b/media/libjxl/src/lib/jxl/dec_context_map.h @@ -22,8 +22,8 @@ constexpr size_t kMaxClusters = 256; // context_map->size() must be the number of possible context ids. // Sets *num_htrees to the number of different histogram ids in // *context_map. -bool DecodeContextMap(std::vector<uint8_t>* context_map, size_t* num_htrees, - BitReader* input); +Status DecodeContextMap(std::vector<uint8_t>* context_map, size_t* num_htrees, + BitReader* input); } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/dec_external_image.cc b/media/libjxl/src/lib/jxl/dec_external_image.cc index daead15768..abf3ed4337 100644 --- a/media/libjxl/src/lib/jxl/dec_external_image.cc +++ b/media/libjxl/src/lib/jxl/dec_external_image.cc @@ -32,6 +32,10 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Clamp; +using hwy::HWY_NAMESPACE::NearestInt; + // TODO(jon): check if this can be replaced by a FloatToU16 function void FloatToU32(const float* in, uint32_t* out, size_t num, float mul, size_t bits_per_sample) { @@ -50,7 +54,7 @@ void FloatToU32(const float* in, uint32_t* out, size_t num, float mul, auto v = Load(d, in + x); // Clamp turns NaN to 'min'. v = Clamp(v, Zero(d), one); - auto i = NearestInt(v * scale); + auto i = NearestInt(Mul(v, scale)); Store(BitCast(du, i), du, out + x); } @@ -451,14 +455,15 @@ Status ConvertToExternal(const jxl::ImageBundle& ib, size_t bits_per_sample, JxlEndianness endianness, size_t stride, jxl::ThreadPool* pool, void* out_image, size_t out_size, const PixelCallback& out_callback, - jxl::Orientation undo_orientation) { + jxl::Orientation undo_orientation, + bool unpremul_alpha) { bool want_alpha = num_channels == 2 || num_channels == 4; size_t color_channels = num_channels <= 2 ? 1 : 3; const Image3F* color = &ib.color(); // Undo premultiplied alpha. Image3F unpremul; - if (ib.AlphaIsPremultiplied() && ib.HasAlpha()) { + if (ib.AlphaIsPremultiplied() && ib.HasAlpha() && unpremul_alpha) { unpremul = Image3F(color->xsize(), color->ysize()); CopyImageTo(*color, &unpremul); for (size_t y = 0; y < unpremul.ysize(); y++) { diff --git a/media/libjxl/src/lib/jxl/dec_external_image.h b/media/libjxl/src/lib/jxl/dec_external_image.h index 134bfa6b65..9b3b8bf66a 100644 --- a/media/libjxl/src/lib/jxl/dec_external_image.h +++ b/media/libjxl/src/lib/jxl/dec_external_image.h @@ -38,7 +38,8 @@ Status ConvertToExternal(const jxl::ImageBundle& ib, size_t bits_per_sample, JxlEndianness endianness, size_t stride_out, jxl::ThreadPool* thread_pool, void* out_image, size_t out_size, const PixelCallback& out_callback, - jxl::Orientation undo_orientation); + jxl::Orientation undo_orientation, + bool unpremul_alpha = false); // Converts single-channel image to interleaved void* pixel buffer with the // given format, with a single channel. diff --git a/media/libjxl/src/lib/jxl/dec_file.cc b/media/libjxl/src/lib/jxl/dec_file.cc deleted file mode 100644 index 4ff1a7d797..0000000000 --- a/media/libjxl/src/lib/jxl/dec_file.cc +++ /dev/null @@ -1,177 +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 "lib/jxl/dec_file.h" - -#include <stddef.h> - -#include <utility> -#include <vector> - -#include "jxl/decode.h" -#include "lib/jxl/base/compiler_specific.h" -#include "lib/jxl/base/override.h" -#include "lib/jxl/base/profiler.h" -#include "lib/jxl/color_management.h" -#include "lib/jxl/common.h" -#include "lib/jxl/dec_bit_reader.h" -#include "lib/jxl/dec_frame.h" -#include "lib/jxl/frame_header.h" -#include "lib/jxl/headers.h" -#include "lib/jxl/icc_codec.h" -#include "lib/jxl/image_bundle.h" -#include "lib/jxl/jpeg/dec_jpeg_data_writer.h" - -namespace jxl { -namespace { - -Status DecodeHeaders(BitReader* reader, CodecInOut* io) { - JXL_RETURN_IF_ERROR(ReadSizeHeader(reader, &io->metadata.size)); - - JXL_RETURN_IF_ERROR(ReadImageMetadata(reader, &io->metadata.m)); - - io->metadata.transform_data.nonserialized_xyb_encoded = - io->metadata.m.xyb_encoded; - JXL_RETURN_IF_ERROR(Bundle::Read(reader, &io->metadata.transform_data)); - - return true; -} - -} // namespace - -// To avoid the complexity of file I/O and buffering, we assume the bitstream -// is loaded (or for large images/sequences: mapped into) memory. -Status DecodeFile(const DecompressParams& dparams, - const Span<const uint8_t> file, CodecInOut* JXL_RESTRICT io, - ThreadPool* pool) { - PROFILER_ZONE("DecodeFile uninstrumented"); - - // Marker - JxlSignature signature = JxlSignatureCheck(file.data(), file.size()); - if (signature == JXL_SIG_NOT_ENOUGH_BYTES || signature == JXL_SIG_INVALID) { - return JXL_FAILURE("File does not start with known JPEG XL signature"); - } - - std::unique_ptr<jpeg::JPEGData> jpeg_data = nullptr; - if (dparams.keep_dct) { - if (io->Main().jpeg_data == nullptr) { - return JXL_FAILURE("Caller must set jpeg_data"); - } - jpeg_data = std::move(io->Main().jpeg_data); - } - - Status ret = true; - { - BitReader reader(file); - BitReaderScopedCloser reader_closer(&reader, &ret); - if (reader.ReadFixedBits<16>() != 0x0AFF) { - // We don't have a naked codestream. Make a quick & dirty attempt to find - // the codestream. - // TODO(jon): get rid of this whole function - const unsigned char* begin = file.data(); - const unsigned char* end = file.data() + file.size() - 4; - while (begin < end) { - if (!memcmp(begin, "jxlc", 4)) break; - begin++; - } - if (begin >= end) return JXL_FAILURE("Couldn't find jxl codestream"); - reader.SkipBits(8 * (begin - file.data() + 2)); - unsigned int firstbytes = reader.ReadFixedBits<16>(); - if (firstbytes != 0x0AFF) - return JXL_FAILURE("Codestream didn't start with FF0A but with %X", - firstbytes); - } - - { - JXL_RETURN_IF_ERROR(DecodeHeaders(&reader, io)); - size_t xsize = io->metadata.xsize(); - size_t ysize = io->metadata.ysize(); - JXL_RETURN_IF_ERROR(VerifyDimensions(&io->constraints, xsize, ysize)); - } - - if (io->metadata.m.color_encoding.WantICC()) { - PaddedBytes icc; - JXL_RETURN_IF_ERROR(ReadICC(&reader, &icc)); - JXL_RETURN_IF_ERROR(io->metadata.m.color_encoding.SetICC(std::move(icc))); - } - // Set ICC profile in jpeg_data. - if (jpeg_data) { - Status res = jpeg::SetJPEGDataFromICC(io->metadata.m.color_encoding.ICC(), - jpeg_data.get()); - if (!res) { - return res; - } - } - - PassesDecoderState dec_state; - JXL_RETURN_IF_ERROR(dec_state.output_encoding_info.Set( - io->metadata, - ColorEncoding::LinearSRGB(io->metadata.m.color_encoding.IsGray()))); - - if (io->metadata.m.have_preview) { - JXL_RETURN_IF_ERROR(reader.JumpToByteBoundary()); - JXL_RETURN_IF_ERROR(DecodeFrame(dparams, &dec_state, pool, &reader, - &io->preview_frame, io->metadata, - &io->constraints, /*is_preview=*/true)); - } - - // Only necessary if no ICC and no preview. - JXL_RETURN_IF_ERROR(reader.JumpToByteBoundary()); - if (io->metadata.m.have_animation && dparams.keep_dct) { - return JXL_FAILURE("Cannot decode to JPEG an animation"); - } - - io->frames.clear(); - Status dec_ok(false); - do { - io->frames.emplace_back(&io->metadata.m); - if (jpeg_data) { - io->frames.back().jpeg_data = std::move(jpeg_data); - } - // Skip frames that are not displayed. - bool found_displayed_frame = true; - do { - dec_ok = - DecodeFrame(dparams, &dec_state, pool, &reader, &io->frames.back(), - io->metadata, &io->constraints); - if (!dparams.allow_partial_files) { - JXL_RETURN_IF_ERROR(dec_ok); - } else if (!dec_ok) { - io->frames.pop_back(); - found_displayed_frame = false; - break; - } - } while (dec_state.shared->frame_header.frame_type != - FrameType::kRegularFrame && - dec_state.shared->frame_header.frame_type != - FrameType::kSkipProgressive); - if (found_displayed_frame) { - // if found_displayed_frame is true io->frames shouldn't be empty - // because we added a frame before the loop. - JXL_ASSERT(!io->frames.empty()); - io->dec_pixels += io->frames.back().xsize() * io->frames.back().ysize(); - } - } while (!dec_state.shared->frame_header.is_last && dec_ok); - - if (io->frames.empty()) return JXL_FAILURE("Not enough data."); - - if (dparams.check_decompressed_size && !dparams.allow_partial_files && - dparams.max_downsampling == 1) { - if (reader.TotalBitsConsumed() != file.size() * kBitsPerByte) { - return JXL_FAILURE("DecodeFile reader position not at EOF."); - } - } - // Suppress errors when decoding partial files with DC frames. - if (!reader.AllReadsWithinBounds() && dparams.allow_partial_files) { - reader_closer.CloseAndSuppressError(); - } - - io->CheckMetadata(); - // reader is closed here. - } - return ret; -} - -} // namespace jxl diff --git a/media/libjxl/src/lib/jxl/dec_file.h b/media/libjxl/src/lib/jxl/dec_file.h deleted file mode 100644 index cd04d5d4c7..0000000000 --- a/media/libjxl/src/lib/jxl/dec_file.h +++ /dev/null @@ -1,48 +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. - -#ifndef LIB_JXL_DEC_FILE_H_ -#define LIB_JXL_DEC_FILE_H_ - -// Top-level interface for JXL decoding. - -#include <stdint.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/codec_in_out.h" -#include "lib/jxl/dec_params.h" - -namespace jxl { - -// Decodes the preview image, if present, and stores it in `preview`. -// Must be the first frame in the file. Does nothing if there is no preview -// frame present according to the metadata. -Status DecodePreview(const DecompressParams& dparams, - const CodecMetadata& metadata, - BitReader* JXL_RESTRICT reader, ThreadPool* pool, - ImageBundle* JXL_RESTRICT preview, uint64_t* dec_pixels, - const SizeConstraints* constraints); - -// Implementation detail: currently decodes to linear sRGB. The contract is: -// `io` appears 'identical' (modulo compression artifacts) to the encoder input -// in a color-aware viewer. Note that `io->metadata.m.color_encoding` -// identifies the color space that was passed to the encoder; clients that want -// that same encoding must call `io->TransformTo` afterwards. -Status DecodeFile(const DecompressParams& params, - const Span<const uint8_t> file, CodecInOut* io, - ThreadPool* pool = nullptr); - -static inline Status DecodeFile(const DecompressParams& params, - const PaddedBytes& file, CodecInOut* io, - ThreadPool* pool = nullptr) { - return DecodeFile(params, Span<const uint8_t>(file), io, pool); -} - -} // namespace jxl - -#endif // LIB_JXL_DEC_FILE_H_ diff --git a/media/libjxl/src/lib/jxl/dec_frame.cc b/media/libjxl/src/lib/jxl/dec_frame.cc index d5509aa79c..c90bb4720a 100644 --- a/media/libjxl/src/lib/jxl/dec_frame.cc +++ b/media/libjxl/src/lib/jxl/dec_frame.cc @@ -15,6 +15,7 @@ #include <utility> #include <vector> +#include "jxl/types.h" #include "lib/jxl/ac_context.h" #include "lib/jxl/ac_strategy.h" #include "lib/jxl/ans_params.h" @@ -35,7 +36,6 @@ #include "lib/jxl/dec_cache.h" #include "lib/jxl/dec_group.h" #include "lib/jxl/dec_modular.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/dec_patch_dictionary.h" #include "lib/jxl/dec_xyb.h" #include "lib/jxl/epf.h" @@ -77,186 +77,72 @@ Status DecodeGlobalDCInfo(BitReader* reader, bool is_jpeg, } } // namespace -Status DecodeFrameHeader(BitReader* JXL_RESTRICT reader, - FrameHeader* JXL_RESTRICT frame_header) { - JXL_ASSERT(frame_header->nonserialized_metadata != nullptr); - JXL_RETURN_IF_ERROR(ReadFrameHeader(reader, frame_header)); - return true; -} - -static BitReader* GetReaderForSection( - size_t num_groups, size_t num_passes, size_t group_codes_begin, - const std::vector<uint64_t>& group_offsets, - const std::vector<uint32_t>& group_sizes, BitReader* JXL_RESTRICT reader, - BitReader* JXL_RESTRICT store, size_t index) { - if (num_groups == 1 && num_passes == 1) return reader; - const size_t group_offset = group_codes_begin + group_offsets[index]; - const size_t next_group_offset = - group_codes_begin + group_offsets[index] + group_sizes[index]; - // The order of these variables must be: - // group_codes_begin <= group_offset <= next_group_offset <= file.size() - JXL_DASSERT(group_codes_begin <= group_offset); - JXL_DASSERT(group_offset <= next_group_offset); - JXL_DASSERT(next_group_offset <= reader->TotalBytes()); - const size_t group_size = next_group_offset - group_offset; - const size_t remaining_size = reader->TotalBytes() - group_offset; - const size_t size = std::min(group_size + 8, remaining_size); - *store = - BitReader(Span<const uint8_t>(reader->FirstByte() + group_offset, size)); - return store; -} - -Status DecodeFrame(const DecompressParams& dparams, - PassesDecoderState* dec_state, ThreadPool* JXL_RESTRICT pool, - BitReader* JXL_RESTRICT reader, ImageBundle* decoded, - const CodecMetadata& metadata, - const SizeConstraints* constraints, bool is_preview) { - PROFILER_ZONE("DecodeFrame uninstrumented"); - +Status DecodeFrame(PassesDecoderState* dec_state, ThreadPool* JXL_RESTRICT pool, + const uint8_t* next_in, size_t avail_in, + ImageBundle* decoded, const CodecMetadata& metadata, + bool use_slow_rendering_pipeline) { FrameDecoder frame_decoder(dec_state, metadata, pool, - dparams.use_slow_render_pipeline); - - frame_decoder.SetFrameSizeLimits(constraints); - - JXL_RETURN_IF_ERROR(frame_decoder.InitFrame( - reader, decoded, is_preview, dparams.allow_partial_files, - dparams.allow_partial_files && dparams.allow_more_progressive_steps, - true)); - - // Handling of progressive decoding. - const FrameHeader& frame_header = frame_decoder.GetFrameHeader(); - { - size_t max_passes = dparams.max_passes; - size_t max_downsampling = std::max( - dparams.max_downsampling >> (frame_header.dc_level * 3), size_t(1)); - // TODO(veluca): deal with downsamplings >= 8. - if (max_downsampling >= 8) { - max_passes = 0; - } else { - for (uint32_t i = 0; i < frame_header.passes.num_downsample; ++i) { - if (max_downsampling >= frame_header.passes.downsample[i] && - max_passes > frame_header.passes.last_pass[i]) { - max_passes = frame_header.passes.last_pass[i] + 1; - } - } - } - // Do not use downsampling for kReferenceOnly frames. - if (frame_header.frame_type == FrameType::kReferenceOnly) { - max_passes = frame_header.passes.num_passes; - } - max_passes = std::min<size_t>(max_passes, frame_header.passes.num_passes); - frame_decoder.SetMaxPasses(max_passes); - } - frame_decoder.SetRenderSpotcolors(dparams.render_spotcolors); - frame_decoder.SetCoalescing(dparams.coalescing); + use_slow_rendering_pipeline); - size_t processed_bytes = reader->TotalBitsConsumed() / kBitsPerByte; + BitReader reader(Span<const uint8_t>(next_in, avail_in)); + JXL_RETURN_IF_ERROR(frame_decoder.InitFrame(&reader, decoded, + /*is_preview=*/false, + /*output_needed=*/true)); + JXL_RETURN_IF_ERROR(reader.AllReadsWithinBounds()); + size_t header_bytes = reader.TotalBitsConsumed() / kBitsPerByte; + JXL_RETURN_IF_ERROR(reader.Close()); + size_t processed_bytes = header_bytes; Status close_ok = true; std::vector<std::unique_ptr<BitReader>> section_readers; { std::vector<std::unique_ptr<BitReaderScopedCloser>> section_closers; std::vector<FrameDecoder::SectionInfo> section_info; std::vector<FrameDecoder::SectionStatus> section_status; - size_t bytes_to_skip = 0; - for (size_t i = 0; i < frame_decoder.NumSections(); i++) { - size_t b = frame_decoder.SectionOffsets()[i]; - size_t e = b + frame_decoder.SectionSizes()[i]; - bytes_to_skip += e - b; - size_t pos = reader->TotalBitsConsumed() / kBitsPerByte; - if (pos + (dparams.allow_more_progressive_steps && - (i == 0 || - frame_header.encoding == FrameEncoding::kModular) - ? b - : e) <= - reader->TotalBytes() || - (i == 0 && dparams.allow_more_progressive_steps)) { - auto br = make_unique<BitReader>(Span<const uint8_t>( - reader->FirstByte() + b + pos, - (pos + b > reader->TotalBytes() - ? 0 - : std::min(reader->TotalBytes() - pos - b, e - b)))); - section_info.emplace_back(FrameDecoder::SectionInfo{br.get(), i}); - section_closers.emplace_back( - make_unique<BitReaderScopedCloser>(br.get(), &close_ok)); - section_readers.emplace_back(std::move(br)); - } else if (!dparams.allow_partial_files) { - return JXL_FAILURE("Premature end of stream."); - } + size_t pos = header_bytes; + for (auto toc_entry : frame_decoder.Toc()) { + JXL_RETURN_IF_ERROR(pos + toc_entry.size <= avail_in); + auto br = make_unique<BitReader>( + Span<const uint8_t>(next_in + pos, toc_entry.size)); + section_info.emplace_back( + FrameDecoder::SectionInfo{br.get(), toc_entry.id}); + section_closers.emplace_back( + make_unique<BitReaderScopedCloser>(br.get(), &close_ok)); + section_readers.emplace_back(std::move(br)); + pos += toc_entry.size; } - // Skip over the to-be-decoded sections. - reader->SkipBits(kBitsPerByte * bytes_to_skip); section_status.resize(section_info.size()); - JXL_RETURN_IF_ERROR(frame_decoder.ProcessSections( section_info.data(), section_info.size(), section_status.data())); - for (size_t i = 0; i < section_status.size(); i++) { - auto s = section_status[i]; - if (s == FrameDecoder::kDone) { - processed_bytes += frame_decoder.SectionSizes()[i]; - continue; - } - if (dparams.allow_more_progressive_steps && s == FrameDecoder::kPartial) { - continue; - } - if (dparams.max_downsampling > 1 && s == FrameDecoder::kSkipped) { - continue; - } - return JXL_FAILURE("Invalid section %" PRIuS " status: %d", - section_info[i].id, s); + JXL_RETURN_IF_ERROR(section_status[i] == FrameDecoder::kDone); + processed_bytes += frame_decoder.Toc()[i].size; } } - JXL_RETURN_IF_ERROR(close_ok); - JXL_RETURN_IF_ERROR(frame_decoder.FinalizeFrame()); decoded->SetDecodedBytes(processed_bytes); return true; } Status FrameDecoder::InitFrame(BitReader* JXL_RESTRICT br, ImageBundle* decoded, - bool is_preview, bool allow_partial_frames, - bool allow_partial_dc_global, - bool output_needed) { + bool is_preview, bool output_needed) { PROFILER_FUNC; decoded_ = decoded; JXL_ASSERT(is_finalized_); - allow_partial_frames_ = allow_partial_frames; - allow_partial_dc_global_ = allow_partial_dc_global; - // Reset the dequantization matrices to their default values. dec_state_->shared_storage.matrices = DequantMatrices(); frame_header_.nonserialized_is_preview = is_preview; - size_t pos = br->TotalBitsConsumed() / kBitsPerByte; - Status have_frameheader = - br->TotalBytes() > pos && DecodeFrameHeader(br, &frame_header_); - JXL_RETURN_IF_ERROR(have_frameheader || allow_partial_frames); - if (!have_frameheader) { - if (dec_state_->shared_storage.dc_frames[0].xsize() > 0) { - // If we have a (partial) DC frame available, but we don't have the next - // frame header (so allow_partial_frames is true), then we'll assume the - // next frame uses that DC frame (which may not be true, e.g. there might - // first be a ReferenceOnly patch frame, but it's reasonable to assume - // that the DC frame is a good progressive preview) - frame_header_.flags |= FrameHeader::kUseDcFrame; - frame_header_.encoding = FrameEncoding::kVarDCT; - frame_header_.dc_level = 0; - } else - return JXL_FAILURE("Couldn't read frame header"); - } + JXL_ASSERT(frame_header_.nonserialized_metadata != nullptr); + JXL_RETURN_IF_ERROR(ReadFrameHeader(br, &frame_header_)); frame_dim_ = frame_header_.ToFrameDimensions(); + JXL_DEBUG_V(2, "FrameHeader: %s", frame_header_.DebugString().c_str()); const size_t num_passes = frame_header_.passes.num_passes; - const size_t xsize = frame_dim_.xsize; - const size_t ysize = frame_dim_.ysize; const size_t num_groups = frame_dim_.num_groups; - // Check validity of frame dimensions. - JXL_RETURN_IF_ERROR(VerifyDimensions(constraints_, xsize, ysize)); - // If the previous frame was not a kRegularFrame, `decoded` may have different // dimensions; must reset to avoid errors. decoded->RemoveColor(); @@ -275,20 +161,31 @@ Status FrameDecoder::InitFrame(BitReader* JXL_RESTRICT br, ImageBundle* decoded, } // Read TOC. - uint64_t groups_total_size; const bool has_ac_global = true; const size_t toc_entries = NumTocEntries(num_groups, frame_dim_.num_dc_groups, num_passes, has_ac_global); - JXL_RETURN_IF_ERROR(ReadGroupOffsets(toc_entries, br, §ion_offsets_, - §ion_sizes_, &groups_total_size) || - allow_partial_frames); + std::vector<uint32_t> sizes; + std::vector<coeff_order_t> permutation; + JXL_RETURN_IF_ERROR(ReadToc(toc_entries, br, &sizes, &permutation)); + bool have_permutation = !permutation.empty(); + toc_.resize(toc_entries); + section_sizes_sum_ = 0; + for (size_t i = 0; i < toc_entries; ++i) { + toc_[i].size = sizes[i]; + size_t index = have_permutation ? permutation[i] : i; + toc_[index].id = i; + if (section_sizes_sum_ + toc_[i].size < section_sizes_sum_) { + return JXL_FAILURE("group offset overflow"); + } + section_sizes_sum_ += toc_[i].size; + } JXL_DASSERT((br->TotalBitsConsumed() % kBitsPerByte) == 0); const size_t group_codes_begin = br->TotalBitsConsumed() / kBitsPerByte; - JXL_DASSERT(!section_offsets_.empty()); + JXL_DASSERT(!toc_.empty()); // Overflow check. - if (group_codes_begin + groups_total_size < group_codes_begin) { + if (group_codes_begin + section_sizes_sum_ < group_codes_begin) { return JXL_FAILURE("Invalid group codes"); } @@ -347,9 +244,7 @@ Status FrameDecoder::InitFrame(BitReader* JXL_RESTRICT br, ImageBundle* decoded, decoded_passes_per_ac_group_.clear(); decoded_passes_per_ac_group_.resize(frame_dim_.num_groups, 0); processed_section_.clear(); - processed_section_.resize(section_offsets_.size()); - max_passes_ = frame_header_.passes.num_passes; - num_renders_ = 0; + processed_section_.resize(toc_.size()); allocated_ = false; return true; } @@ -382,14 +277,11 @@ Status FrameDecoder::ProcessDCGlobal(BitReader* br) { if (shared.frame_header.flags & FrameHeader::kNoise) { JXL_RETURN_IF_ERROR(DecodeNoise(br, &shared.image_features.noise_params)); } - if (!allow_partial_dc_global_ || - br->TotalBitsConsumed() < br->TotalBytes() * kBitsPerByte) { - JXL_RETURN_IF_ERROR(dec_state_->shared_storage.matrices.DecodeDC(br)); + JXL_RETURN_IF_ERROR(dec_state_->shared_storage.matrices.DecodeDC(br)); - if (frame_header_.encoding == FrameEncoding::kVarDCT) { - JXL_RETURN_IF_ERROR( - jxl::DecodeGlobalDCInfo(br, decoded_->IsJPEG(), dec_state_, pool_)); - } + if (frame_header_.encoding == FrameEncoding::kVarDCT) { + JXL_RETURN_IF_ERROR( + jxl::DecodeGlobalDCInfo(br, decoded_->IsJPEG(), dec_state_, pool_)); } // Splines' draw cache uses the color correlation map. if (shared.frame_header.flags & FrameHeader::kSplines) { @@ -398,7 +290,7 @@ Status FrameDecoder::ProcessDCGlobal(BitReader* br) { dec_state_->shared->cmap)); } Status dec_status = modular_frame_decoder_.DecodeGlobalInfo( - br, frame_header_, allow_partial_dc_global_); + br, frame_header_, /*allow_truncated_group=*/false); if (dec_status.IsFatalError()) return dec_status; if (dec_status) { decoded_dc_global_ = true; @@ -420,7 +312,8 @@ Status FrameDecoder::ProcessDCGroup(size_t dc_group_id, BitReader* br) { frame_dim_.dc_group_dim, frame_dim_.dc_group_dim); JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeGroup( mrect, br, 3, 1000, ModularStreamId::ModularDC(dc_group_id), - /*zerofill=*/false, nullptr, nullptr, nullptr, allow_partial_frames_)); + /*zerofill=*/false, nullptr, nullptr, + /*allow_truncated=*/false)); if (frame_header_.encoding == FrameEncoding::kVarDCT) { JXL_RETURN_IF_ERROR( modular_frame_decoder_.DecodeAcMetadata(dc_group_id, br, dec_state_)); @@ -562,6 +455,11 @@ Status FrameDecoder::ProcessACGroup(size_t ac_group_id, const size_t gy = ac_group_id / frame_dim_.xsize_groups; const size_t x = gx * group_dim; const size_t y = gy * group_dim; + JXL_DEBUG_V(3, + "Processing AC group %" PRIuS "(%" PRIuS ",%" PRIuS + ") group_dim: %" PRIuS " decoded passes: %u new passes: %" PRIuS, + ac_group_id, gx, gy, group_dim, + decoded_passes_per_ac_group_[ac_group_id], num_passes); RenderPipelineInput render_pipeline_input = dec_state_->render_pipeline->GetInputBuffers(ac_group_id, thread); @@ -580,23 +478,28 @@ Status FrameDecoder::ProcessACGroup(size_t ac_group_id, // don't limit to image dimensions here (is done in DecodeGroup) const Rect mrect(x, y, group_dim, group_dim); - for (size_t i = 0; i < frame_header_.passes.num_passes; i++) { + bool modular_ready = false; + size_t pass0 = decoded_passes_per_ac_group_[ac_group_id]; + size_t pass1 = + force_draw ? frame_header_.passes.num_passes : pass0 + num_passes; + for (size_t i = pass0; i < pass1; ++i) { int minShift, maxShift; frame_header_.passes.GetDownsamplingBracket(i, minShift, maxShift); - if (i >= decoded_passes_per_ac_group_[ac_group_id] && - i < decoded_passes_per_ac_group_[ac_group_id] + num_passes) { + bool modular_pass_ready = true; + if (i < pass0 + num_passes) { JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeGroup( - mrect, br[i - decoded_passes_per_ac_group_[ac_group_id]], minShift, - maxShift, ModularStreamId::ModularAC(ac_group_id, i), - /*zerofill=*/false, dec_state_, &render_pipeline_input, decoded_, - allow_partial_frames_)); - } else if (i >= decoded_passes_per_ac_group_[ac_group_id] + num_passes && - force_draw) { + mrect, br[i - pass0], minShift, maxShift, + ModularStreamId::ModularAC(ac_group_id, i), + /*zerofill=*/false, dec_state_, &render_pipeline_input, + /*allow_truncated=*/false, &modular_pass_ready)); + } else { JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeGroup( mrect, nullptr, minShift, maxShift, ModularStreamId::ModularAC(ac_group_id, i), /*zerofill=*/true, - dec_state_, &render_pipeline_input, decoded_, allow_partial_frames_)); + dec_state_, &render_pipeline_input, + /*allow_truncated=*/false, &modular_pass_ready)); } + if (modular_pass_ready) modular_ready = true; } decoded_passes_per_ac_group_[ac_group_id] += num_passes; @@ -627,19 +530,21 @@ Status FrameDecoder::ProcessACGroup(size_t ac_group_id, } } - if (!modular_frame_decoder_.UsesFullImage() && !decoded_->IsJPEG() && - should_run_pipeline) { - render_pipeline_input.Done(); + if (!modular_frame_decoder_.UsesFullImage() && !decoded_->IsJPEG()) { + if (should_run_pipeline && modular_ready) { + render_pipeline_input.Done(); + } else if (force_draw) { + return JXL_FAILURE("Modular group decoding failed."); + } } return true; } void FrameDecoder::MarkSections(const SectionInfo* sections, size_t num, SectionStatus* section_status) { - num_sections_done_ = num; + num_sections_done_ += num; for (size_t i = 0; i < num; i++) { - if (section_status[i] == SectionStatus::kSkipped || - section_status[i] == SectionStatus::kPartial) { + if (section_status[i] != SectionStatus::kDone) { processed_section_[sections[i].id] = false; num_sections_done_--; } @@ -656,7 +561,9 @@ Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num, std::vector<std::vector<size_t>> ac_group_sec( frame_dim_.num_groups, std::vector<size_t>(frame_header_.passes.num_passes, num)); - std::vector<size_t> num_ac_passes(frame_dim_.num_groups); + // This keeps track of the number of ac passes we want to process during this + // call of ProcessSections. + std::vector<size_t> desired_num_ac_passes(frame_dim_.num_groups); bool single_section = frame_dim_.num_groups == 1 && frame_header_.passes.num_passes == 1; if (single_section) { @@ -666,7 +573,7 @@ Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num, processed_section_[0] = true; ac_group_sec[0].resize(1); dc_global_sec = ac_global_sec = dc_group_sec[0] = ac_group_sec[0][0] = 0; - num_ac_passes[0] = 1; + desired_num_ac_passes[0] = 1; } else { section_status[0] = SectionStatus::kDuplicate; } @@ -691,9 +598,6 @@ Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num, if (acp >= frame_header_.passes.num_passes) { return JXL_FAILURE("Invalid section ID"); } - if (acp >= max_passes_) { - continue; - } ac_group_sec[acg][acp] = i; } processed_section_[sections[i].id] = true; @@ -701,12 +605,14 @@ Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num, // Count number of new passes per group. for (size_t g = 0; g < ac_group_sec.size(); g++) { size_t j = 0; - for (; j + decoded_passes_per_ac_group_[g] < max_passes_; j++) { + for (; j + decoded_passes_per_ac_group_[g] < + frame_header_.passes.num_passes; + j++) { if (ac_group_sec[g][j + decoded_passes_per_ac_group_[g]] == num) { break; } } - num_ac_passes[g] = j; + desired_num_ac_passes[g] = j; } } if (dc_global_sec != num) { @@ -747,33 +653,9 @@ Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num, dec_state_->PreparePipeline(decoded_, pipeline_options)); FinalizeDC(); JXL_RETURN_IF_ERROR(AllocateOutput()); - if (pause_at_progressive_ && !single_section) { - bool can_return_dc = true; - if (single_section) { - // If there's only one group and one pass, there is no separate section - // for DC and the entire full resolution image is available at once. - can_return_dc = false; - } - if (!decoded_->metadata()->extra_channel_info.empty()) { - // If extra channels are encoded with modular without squeeze, they - // don't support DC. If the are encoded with squeeze, DC works in theory - // but the implementation may not yet correctly support this for Flush. - // Therefore, can't correctly pause for a progressive step if there is - // an extra channel (including alpha channel) - can_return_dc = false; - } - if (frame_header_.encoding != FrameEncoding::kVarDCT) { - // DC is not guaranteed to be available in modular mode and may be a - // black image. If squeeze is used, it may be available depending on the - // current implementation. - // TODO(lode): do return DC if it's known that flushing at this point - // will produce a valid 1/8th downscaled image with modular encoding. - can_return_dc = false; - } - if (can_return_dc) { - MarkSections(sections, num, section_status); - return true; - } + if (progressive_detail_ >= JxlProgressiveDetail::kDC) { + MarkSections(sections, num, section_status); + return true; } } @@ -782,13 +664,22 @@ Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num, section_status[ac_global_sec] = SectionStatus::kDone; } + if (progressive_detail_ >= JxlProgressiveDetail::kLastPasses) { + // Mark that we only want the next progression pass. + size_t target_complete_passes = NextNumPassesToPause(); + for (size_t i = 0; i < ac_group_sec.size(); i++) { + desired_num_ac_passes[i] = + std::min(desired_num_ac_passes[i], + target_complete_passes - decoded_passes_per_ac_group_[i]); + } + } + if (decoded_ac_global_) { // Mark all the AC groups that we received as not complete yet. for (size_t i = 0; i < ac_group_sec.size(); i++) { - if (num_ac_passes[i] == 0 && !modular_frame_decoder_.UsesFullImage()) { - continue; + if (desired_num_ac_passes[i] != 0) { + dec_state_->render_pipeline->ClearDone(i); } - dec_state_->render_pipeline->ClearDone(i); } JXL_RETURN_IF_ERROR(RunOnPool( @@ -797,24 +688,25 @@ Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num, return PrepareStorage(num_threads, decoded_passes_per_ac_group_.size()); }, - [this, &ac_group_sec, &num_ac_passes, &num, §ions, §ion_status, - &has_error](size_t g, size_t thread) { - if (num_ac_passes[g] == 0) { // no new AC pass, nothing to do. + [this, &ac_group_sec, &desired_num_ac_passes, &num, §ions, + §ion_status, &has_error](size_t g, size_t thread) { + if (desired_num_ac_passes[g] == 0) { + // no new AC pass, nothing to do return; } (void)num; size_t first_pass = decoded_passes_per_ac_group_[g]; BitReader* JXL_RESTRICT readers[kMaxNumPasses]; - for (size_t i = 0; i < num_ac_passes[g]; i++) { + for (size_t i = 0; i < desired_num_ac_passes[g]; i++) { JXL_ASSERT(ac_group_sec[g][first_pass + i] != num); readers[i] = sections[ac_group_sec[g][first_pass + i]].br; } - if (!ProcessACGroup(g, readers, num_ac_passes[g], + if (!ProcessACGroup(g, readers, desired_num_ac_passes[g], GetStorageLocation(thread, g), /*force_draw=*/false, /*dc_only=*/false)) { has_error = true; } else { - for (size_t i = 0; i < num_ac_passes[g]; i++) { + for (size_t i = 0; i < desired_num_ac_passes[g]; i++) { section_status[ac_group_sec[g][first_pass + i]] = SectionStatus::kDone; } @@ -857,9 +749,9 @@ Status FrameDecoder::Flush() { // We don't have all AC yet: force a draw of all the missing areas. // Mark all sections as not complete. for (size_t i = 0; i < decoded_passes_per_ac_group_.size(); i++) { - if (decoded_passes_per_ac_group_[i] == frame_header_.passes.num_passes) - continue; - dec_state_->render_pipeline->ClearDone(i); + if (decoded_passes_per_ac_group_[i] < frame_header_.passes.num_passes) { + dec_state_->render_pipeline->ClearDone(i); + } } std::atomic<bool> has_error{false}; JXL_RETURN_IF_ERROR(RunOnPool( @@ -887,10 +779,9 @@ Status FrameDecoder::Flush() { } // undo global modular transforms and copy int pixel buffers to float ones - JXL_RETURN_IF_ERROR(modular_frame_decoder_.FinalizeDecoding( - dec_state_, pool_, decoded_, is_finalized_)); + JXL_RETURN_IF_ERROR(modular_frame_decoder_.FinalizeDecoding(dec_state_, pool_, + is_finalized_)); - num_renders_++; return true; } @@ -913,7 +804,7 @@ bool FrameDecoder::HasEverything() const { if (!have_dc_group) return false; } for (auto& nb_passes : decoded_passes_per_ac_group_) { - if (nb_passes < max_passes_) return false; + if (nb_passes < frame_header_.passes.num_passes) return false; } return true; } @@ -967,18 +858,9 @@ Status FrameDecoder::FinalizeFrame() { return true; } if (!finalized_dc_) { - // We don't have all of DC: EPF might not behave correctly (and is not - // particularly useful anyway on upsampling results), so we disable it. - dec_state_->shared_storage.frame_header.loop_filter.epf_iters = 0; - } - if (!HasEverything() && !allow_partial_frames_) { - return JXL_FAILURE( - "FinalizeFrame called before the frame was fully decoded"); - } - - if (!finalized_dc_) { - JXL_ASSERT(allow_partial_frames_); - JXL_RETURN_IF_ERROR(AllocateOutput()); + // We don't have all of DC, and render pipeline is not created yet, so we + // can not call Flush() yet. + return JXL_FAILURE("FinalizeFrame called before DC was fully decoded"); } JXL_RETURN_IF_ERROR(Flush()); diff --git a/media/libjxl/src/lib/jxl/dec_frame.h b/media/libjxl/src/lib/jxl/dec_frame.h index ac52405336..62c61c0fa9 100644 --- a/media/libjxl/src/lib/jxl/dec_frame.h +++ b/media/libjxl/src/lib/jxl/dec_frame.h @@ -9,6 +9,7 @@ #include <stdint.h> #include "jxl/decode.h" +#include "jxl/types.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/span.h" @@ -19,31 +20,20 @@ #include "lib/jxl/dec_bit_reader.h" #include "lib/jxl/dec_cache.h" #include "lib/jxl/dec_modular.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/frame_header.h" #include "lib/jxl/headers.h" #include "lib/jxl/image_bundle.h" namespace jxl { -// TODO(veluca): remove DecodeFrameHeader once the API migrates to FrameDecoder. - -// `frame_header` must have nonserialized_metadata and -// nonserialized_is_preview set. -Status DecodeFrameHeader(BitReader* JXL_RESTRICT reader, - FrameHeader* JXL_RESTRICT frame_header); - // Decodes a frame. Groups may be processed in parallel by `pool`. -// See DecodeFile for explanation of c_decoded. -// `io` is only used for reading maximum image size. Also updates -// `dec_state` with the new frame header. // `metadata` is the metadata that applies to all frames of the codestream // `decoded->metadata` must already be set and must match metadata.m. -Status DecodeFrame(const DecompressParams& dparams, - PassesDecoderState* dec_state, ThreadPool* JXL_RESTRICT pool, - BitReader* JXL_RESTRICT reader, ImageBundle* decoded, - const CodecMetadata& metadata, - const SizeConstraints* constraints, bool is_preview = false); +// Used in the encoder to model decoder behaviour, and in tests. +Status DecodeFrame(PassesDecoderState* dec_state, ThreadPool* JXL_RESTRICT pool, + const uint8_t* next_in, size_t avail_in, + ImageBundle* decoded, const CodecMetadata& metadata, + bool use_slow_rendering_pipeline = false); // TODO(veluca): implement "forced drawing". class FrameDecoder { @@ -56,28 +46,25 @@ class FrameDecoder { frame_header_(&metadata), use_slow_rendering_pipeline_(use_slow_rendering_pipeline) {} - // `constraints` must outlive the FrameDecoder if not null, or stay alive - // until the next call to SetFrameSizeLimits. - void SetFrameSizeLimits(const SizeConstraints* constraints) { - constraints_ = constraints; - } void SetRenderSpotcolors(bool rsc) { render_spotcolors_ = rsc; } void SetCoalescing(bool c) { coalescing_ = c; } // Read FrameHeader and table of contents from the given BitReader. // Also checks frame dimensions for their limits, and sets the output // image buffer. - // TODO(veluca): remove the `allow_partial_frames` flag - this should be moved - // on callers. Status InitFrame(BitReader* JXL_RESTRICT br, ImageBundle* decoded, - bool is_preview, bool allow_partial_frames, - bool allow_partial_dc_global, bool output_needed); + bool is_preview, bool output_needed); struct SectionInfo { BitReader* JXL_RESTRICT br; size_t id; }; + struct TocEntry { + size_t size; + size_t id; + }; + enum SectionStatus { // Processed correctly. kDone = 0, @@ -118,22 +105,20 @@ class FrameDecoder { // soon as the frame header is known. static int SavedAs(const FrameHeader& header); - // Returns offset of this section after the end of the TOC. The end of the TOC - // is the byte position of the bit reader after InitFrame was called. - const std::vector<uint64_t>& SectionOffsets() const { - return section_offsets_; - } - const std::vector<uint32_t>& SectionSizes() const { return section_sizes_; } - size_t NumSections() const { return section_sizes_.size(); } + uint64_t SumSectionSizes() const { return section_sizes_sum_; } + const std::vector<TocEntry>& Toc() const { return toc_; } - // TODO(veluca): remove once we remove --downsampling flag. - void SetMaxPasses(size_t max_passes) { max_passes_ = max_passes; } const FrameHeader& GetFrameHeader() const { return frame_header_; } // Returns whether a DC image has been decoded, accessible at low resolution // at passes.shared_storage.dc_storage bool HasDecodedDC() const { return finalized_dc_; } - bool HasDecodedAll() const { return NumSections() == num_sections_done_; } + bool HasDecodedAll() const { return toc_.size() == num_sections_done_; } + + size_t NumCompletePasses() const { + return *std::min_element(decoded_passes_per_ac_group_.begin(), + decoded_passes_per_ac_group_.end()); + }; // If enabled, ProcessSections will stop and return true when the DC // sections have been processed, instead of starting the AC sections. This @@ -141,7 +126,59 @@ class FrameDecoder { // 1/8th*1/8th resolution image). The return value of true then does not mean // all sections have been processed, use HasDecodedDC and HasDecodedAll // to check the true finished state. - void SetPauseAtProgressive() { pause_at_progressive_ = true; } + // Returns the progressive detail that will be effective for the frame. + JxlProgressiveDetail SetPauseAtProgressive(JxlProgressiveDetail prog_detail) { + bool single_section = + frame_dim_.num_groups == 1 && frame_header_.passes.num_passes == 1; + if (frame_header_.frame_type != kSkipProgressive && + // If there's only one group and one pass, there is no separate section + // for DC and the entire full resolution image is available at once. + !single_section && + // If extra channels are encoded with modular without squeeze, they + // don't support DC. If the are encoded with squeeze, DC works in theory + // but the implementation may not yet correctly support this for Flush. + // Therefore, can't correctly pause for a progressive step if there is + // an extra channel (including alpha channel) + // TOOD(firsching): Check if this is still the case. + decoded_->metadata()->extra_channel_info.empty() && + // DC is not guaranteed to be available in modular mode and may be a + // black image. If squeeze is used, it may be available depending on the + // current implementation. + // TODO(lode): do return DC if it's known that flushing at this point + // will produce a valid 1/8th downscaled image with modular encoding. + frame_header_.encoding == FrameEncoding::kVarDCT) { + progressive_detail_ = prog_detail; + } else { + progressive_detail_ = JxlProgressiveDetail::kFrames; + } + if (progressive_detail_ >= JxlProgressiveDetail::kPasses) { + for (size_t i = 1; i < frame_header_.passes.num_passes; ++i) { + passes_to_pause_.push_back(i); + } + } else if (progressive_detail_ >= JxlProgressiveDetail::kLastPasses) { + for (size_t i = 0; i < frame_header_.passes.num_downsample; ++i) { + passes_to_pause_.push_back(frame_header_.passes.last_pass[i] + 1); + } + // The format does not guarantee that these values are sorted. + std::sort(passes_to_pause_.begin(), passes_to_pause_.end()); + } + return progressive_detail_; + } + + size_t NextNumPassesToPause() const { + auto it = std::upper_bound(passes_to_pause_.begin(), passes_to_pause_.end(), + NumCompletePasses()); + return (it != passes_to_pause_.end() ? *it + : std::numeric_limits<size_t>::max()); + } + + void MaybeSetUnpremultiplyAlpha(bool unpremul_alpha) { + const jxl::ExtraChannelInfo* alpha = + decoded_->metadata()->Find(jxl::ExtraChannel::kAlpha); + if (alpha && alpha->alpha_associated && unpremul_alpha) { + dec_state_->unpremul_alpha = true; + } + } // Sets the buffer to which uint8 sRGB pixels will be decoded. This is not // supported for all images. If it succeeds, HasRGBBuffer() will return true. @@ -156,7 +193,9 @@ class FrameDecoder { // orientation. When outputting to the ImageBundle, no orientation is undone. void MaybeSetRGB8OutputBuffer(uint8_t* rgb_output, size_t stride, bool is_rgba, bool undo_orientation) const { - if (!CanDoLowMemoryPath(undo_orientation)) return; + if (!CanDoLowMemoryPath(undo_orientation) || dec_state_->unpremul_alpha) { + return; + } dec_state_->rgb_output = rgb_output; dec_state_->rgb_output_is_rgba = is_rgba; dec_state_->rgb_stride = stride; @@ -165,6 +204,8 @@ class FrameDecoder { if (decoded_->metadata()->xyb_encoded && dec_state_->output_encoding_info.color_encoding.IsSRGB() && dec_state_->output_encoding_info.all_default_opsin && + dec_state_->output_encoding_info.desired_intensity_target == + dec_state_->output_encoding_info.orig_intensity_target && HasFastXYBTosRGB8() && frame_header_.needs_color_transform()) { dec_state_->fast_xyb_srgb8_conversion = true; } @@ -183,7 +224,7 @@ class FrameDecoder { // results in not setting the buffer if the image has a non-identity EXIF // orientation. When outputting to the ImageBundle, no orientation is undone. void MaybeSetFloatCallback(const PixelCallback& pixel_callback, bool is_rgba, - bool undo_orientation) const { + bool unpremul_alpha, bool undo_orientation) const { if (!CanDoLowMemoryPath(undo_orientation)) return; dec_state_->pixel_callback = pixel_callback; dec_state_->rgb_output_is_rgba = is_rgba; @@ -221,11 +262,12 @@ class FrameDecoder { group_dec_caches_.resize(storage_size); } use_task_id_ = num_threads > num_tasks; + bool use_group_ids = (modular_frame_decoder_.UsesFullImage() && + (frame_header_.encoding == FrameEncoding::kVarDCT || + (frame_header_.flags & FrameHeader::kNoise))); if (dec_state_->render_pipeline) { JXL_RETURN_IF_ERROR(dec_state_->render_pipeline->PrepareForThreads( - storage_size, - /*use_group_ids=*/modular_frame_decoder_.UsesFullImage() && - frame_header_.encoding == FrameEncoding::kVarDCT)); + storage_size, use_group_ids)); } return true; } @@ -248,16 +290,13 @@ class FrameDecoder { PassesDecoderState* dec_state_; ThreadPool* pool_; - std::vector<uint64_t> section_offsets_; - std::vector<uint32_t> section_sizes_; - size_t max_passes_; + std::vector<TocEntry> toc_; + uint64_t section_sizes_sum_; // TODO(veluca): figure out the duplication between these and dec_state_. FrameHeader frame_header_; FrameDimensions frame_dim_; ImageBundle* decoded_; ModularFrameDecoder modular_frame_decoder_; - bool allow_partial_frames_; - bool allow_partial_dc_global_; bool render_spotcolors_ = true; bool coalescing_ = true; @@ -270,14 +309,10 @@ class FrameDecoder { bool finalized_dc_ = true; size_t num_sections_done_ = 0; bool is_finalized_ = true; - size_t num_renders_ = 0; bool allocated_ = false; std::vector<GroupDecCache> group_dec_caches_; - // Frame size limits. - const SizeConstraints* constraints_ = nullptr; - // Whether or not the task id should be used for storage indexing, instead of // the thread id. bool use_task_id_ = false; @@ -285,7 +320,10 @@ class FrameDecoder { // Testing setting: whether or not to use the slow rendering pipeline. bool use_slow_rendering_pipeline_; - bool pause_at_progressive_ = false; + JxlProgressiveDetail progressive_detail_ = kFrames; + // Number of completed passes where section decoding should pause. + // Used for progressive details at least kLastPasses. + std::vector<int> passes_to_pause_; }; } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/dec_group.cc b/media/libjxl/src/lib/jxl/dec_group.cc index 628c932e7c..689bc81779 100644 --- a/media/libjxl/src/lib/jxl/dec_group.cc +++ b/media/libjxl/src/lib/jxl/dec_group.cc @@ -97,10 +97,11 @@ void DequantLane(Vec<D> scaled_dequant_x, Vec<D> scaled_dequant_y, size_t k, Vec<D> x_cc_mul, Vec<D> b_cc_mul, const float* JXL_RESTRICT biases, ACPtr qblock[3], float* JXL_RESTRICT block) { - const auto x_mul = Load(d, dequant_matrices + k) * scaled_dequant_x; - const auto y_mul = Load(d, dequant_matrices + size + k) * scaled_dequant_y; + const auto x_mul = Mul(Load(d, dequant_matrices + k), scaled_dequant_x); + const auto y_mul = + Mul(Load(d, dequant_matrices + size + k), scaled_dequant_y); const auto b_mul = - Load(d, dequant_matrices + 2 * size + k) * scaled_dequant_b; + Mul(Load(d, dequant_matrices + 2 * size + k), scaled_dequant_b); Vec<DI> quantized_x_int; Vec<DI> quantized_y_int; @@ -117,11 +118,11 @@ void DequantLane(Vec<D> scaled_dequant_x, Vec<D> scaled_dequant_y, } const auto dequant_x_cc = - AdjustQuantBias(di, 0, quantized_x_int, biases) * x_mul; + Mul(AdjustQuantBias(di, 0, quantized_x_int, biases), x_mul); const auto dequant_y = - AdjustQuantBias(di, 1, quantized_y_int, biases) * y_mul; + Mul(AdjustQuantBias(di, 1, quantized_y_int, biases), y_mul); const auto dequant_b_cc = - AdjustQuantBias(di, 2, quantized_b_int, biases) * b_mul; + Mul(AdjustQuantBias(di, 2, quantized_b_int, biases), b_mul); const auto dequant_x = MulAdd(x_cc_mul, dequant_y, dequant_x_cc); const auto dequant_b = MulAdd(b_cc_mul, dequant_y, dequant_b_cc); @@ -241,8 +242,10 @@ Status DecodeGroupImpl(GetBlock* JXL_RESTRICT get_block, r[i] = Rect(block_rect.x0() >> hshift[i], block_rect.y0() >> vshift[i], block_rect.xsize() >> hshift[i], block_rect.ysize() >> vshift[i]); - JXL_ASSERT(r[i].IsInside({0, 0, dec_state->shared->dc->Plane(i).xsize(), - dec_state->shared->dc->Plane(i).ysize()})); + if (!r[i].IsInside({0, 0, dec_state->shared->dc->Plane(i).xsize(), + dec_state->shared->dc->Plane(i).ysize()})) { + return JXL_FAILURE("Frame dimensions are too big for the image."); + } } for (size_t by = 0; by < ysize_blocks; ++by) { @@ -383,11 +386,11 @@ Status DecodeGroupImpl(GetBlock* JXL_RESTRICT get_block, auto in = Load(di, transposed_dct + i); auto in_y = Load(di, transposed_dct_y + i); auto qt = Load(di, scaled_qtable + c * size + i); - auto coeff_scale = - ShiftRight<kCFLFixedPointPrecision>(qt * scale + round); + auto coeff_scale = ShiftRight<kCFLFixedPointPrecision>( + Add(Mul(qt, scale), round)); auto cfl_factor = ShiftRight<kCFLFixedPointPrecision>( - in_y * coeff_scale + round); - StoreU(DemoteTo(di16, in + cfl_factor), di16, jpeg_pos + i); + Add(Mul(in_y, coeff_scale), round)); + StoreU(DemoteTo(di16, Add(in, cfl_factor)), di16, jpeg_pos + i); } } jpeg_pos[0] = diff --git a/media/libjxl/src/lib/jxl/dec_group.h b/media/libjxl/src/lib/jxl/dec_group.h index 56542e9519..e50d22d2f8 100644 --- a/media/libjxl/src/lib/jxl/dec_group.h +++ b/media/libjxl/src/lib/jxl/dec_group.h @@ -21,7 +21,6 @@ #include "lib/jxl/dec_ans.h" #include "lib/jxl/dec_bit_reader.h" #include "lib/jxl/dec_cache.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/frame_header.h" #include "lib/jxl/image.h" #include "lib/jxl/quantizer.h" diff --git a/media/libjxl/src/lib/jxl/dec_modular.cc b/media/libjxl/src/lib/jxl/dec_modular.cc index b6601c12d5..bf85eaa05c 100644 --- a/media/libjxl/src/lib/jxl/dec_modular.cc +++ b/media/libjxl/src/lib/jxl/dec_modular.cc @@ -7,6 +7,8 @@ #include <stdint.h> +#include <atomic> +#include <sstream> #include <vector> #include "lib/jxl/frame_header.h" @@ -32,6 +34,8 @@ namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; using hwy::HWY_NAMESPACE::Rebind; void MultiplySum(const size_t xsize, @@ -42,8 +46,8 @@ void MultiplySum(const size_t xsize, const Rebind<pixel_type, HWY_FULL(float)> di; // assumes pixel_type <= float const auto factor_v = Set(df, factor); for (size_t x = 0; x < xsize; x += Lanes(di)) { - const auto in = Load(di, row_in + x) + Load(di, row_in_Y + x); - const auto out = ConvertTo(df, in) * factor_v; + const auto in = Add(Load(di, row_in + x), Load(di, row_in_Y + x)); + const auto out = Mul(ConvertTo(df, in), factor_v); Store(out, df, row_out + x); } } @@ -58,7 +62,7 @@ void RgbFromSingle(const size_t xsize, const auto factor_v = Set(df, factor); for (size_t x = 0; x < xsize; x += Lanes(di)) { const auto in = Load(di, row_in + x); - const auto out = ConvertTo(df, in) * factor_v; + const auto out = Mul(ConvertTo(df, in), factor_v); Store(out, df, out_r + x); Store(out, df, out_g + x); Store(out, df, out_b + x); @@ -74,7 +78,7 @@ void SingleFromSingle(const size_t xsize, const auto factor_v = Set(df, factor); for (size_t x = 0; x < xsize; x += Lanes(di)) { const auto in = Load(di, row_in + x); - const auto out = ConvertTo(df, in) * factor_v; + const auto out = Mul(ConvertTo(df, in), factor_v); Store(out, df, row_out + x); } } @@ -149,6 +153,28 @@ void int_to_float(const pixel_type* const JXL_RESTRICT row_in, } } +std::string ModularStreamId::DebugString() const { + std::ostringstream os; + os << (kind == kGlobalData ? "ModularGlobal" + : kind == kVarDCTDC ? "VarDCTDC" + : kind == kModularDC ? "ModularDC" + : kind == kACMetadata ? "ACMeta" + : kind == kQuantTable ? "QuantTable" + : kind == kModularAC ? "ModularAC" + : ""); + if (kind == kVarDCTDC || kind == kModularDC || kind == kACMetadata || + kind == kModularAC) { + os << " group " << group_id; + } + if (kind == kModularAC) { + os << " pass " << pass_id; + } + if (kind == kQuantTable) { + os << " " << quant_table_id; + } + return os.str(); +} + Status ModularFrameDecoder::DecodeGlobalInfo(BitReader* reader, const FrameHeader& frame_header, bool allow_truncated_group) { @@ -218,6 +244,8 @@ Status ModularFrameDecoder::DecodeGlobalInfo(BitReader* reader, all_same_shift = false; } + JXL_DEBUG_V(6, "DecodeGlobalInfo: full_image (w/o transforms) %s", + gi.DebugString().c_str()); ModularOptions options; options.max_chan_size = frame_dim.group_dim; options.group_dim = frame_dim.group_dim; @@ -248,12 +276,15 @@ Status ModularFrameDecoder::DecodeGlobalInfo(BitReader* reader, } } full_image = std::move(gi); + JXL_DEBUG_V(6, "DecodeGlobalInfo: full_image (with transforms) %s", + full_image.DebugString().c_str()); return dec_status; } void ModularFrameDecoder::MaybeDropFullImage() { if (full_image.transform.empty() && !have_something && all_same_shift) { use_full_image = false; + JXL_DEBUG_V(6, "Dropping full image"); for (auto& ch : full_image.channel) { // keep metadata on channels around, but dealloc their planes ch.plane = Plane<pixel_type>(); @@ -264,8 +295,11 @@ void ModularFrameDecoder::MaybeDropFullImage() { Status ModularFrameDecoder::DecodeGroup( const Rect& rect, BitReader* reader, int minShift, int maxShift, const ModularStreamId& stream, bool zerofill, PassesDecoderState* dec_state, - RenderPipelineInput* render_pipeline_input, ImageBundle* output, - bool allow_truncated) { + RenderPipelineInput* render_pipeline_input, bool allow_truncated, + bool* should_run_pipeline) { + JXL_DEBUG_V(6, "Decoding %s with rect %s and shift bracket %d..%d %s", + stream.DebugString().c_str(), Description(rect).c_str(), minShift, + maxShift, zerofill ? "using zerofill" : ""); JXL_DASSERT(stream.kind == ModularStreamId::kModularDC || stream.kind == ModularStreamId::kModularAC); const size_t xsize = rect.xsize(); @@ -302,7 +336,19 @@ Status ModularFrameDecoder::DecodeGroup( if (zerofill && use_full_image) return true; // Return early if there's nothing to decode. Otherwise there might be // problems later (in ModularImageToDecodedRect). - if (gi.channel.empty()) return true; + if (gi.channel.empty()) { + if (dec_state && should_run_pipeline) { + const auto& frame_header = dec_state->shared->frame_header; + const auto* metadata = frame_header.nonserialized_metadata; + if (do_color || metadata->m.num_extra_channels > 0) { + // Signal to FrameDecoder that we do not have some of the required input + // for the render pipeline. + *should_run_pipeline = false; + } + } + JXL_DEBUG_V(6, "Nothing to decode, returning early."); + return true; + } ModularOptions options; if (!zerofill) { auto status = ModularGenericDecompress( @@ -407,11 +453,11 @@ Status ModularFrameDecoder::DecodeAcMetadata(size_t group_id, BitReader* reader, uint32_t local_used_acs = 0; for (size_t iy = 0; iy < r.ysize(); iy++) { size_t y = r.y0() + iy; - int* row_qf = r.Row(&dec_state->shared_storage.raw_quant_field, iy); + int32_t* row_qf = r.Row(&dec_state->shared_storage.raw_quant_field, iy); uint8_t* row_epf = r.Row(&dec_state->shared_storage.epf_sharpness, iy); - int* row_in_1 = image.channel[2].plane.Row(0); - int* row_in_2 = image.channel[2].plane.Row(1); - int* row_in_3 = image.channel[3].plane.Row(iy); + int32_t* row_in_1 = image.channel[2].plane.Row(0); + int32_t* row_in_2 = image.channel[2].plane.Row(1); + int32_t* row_in_3 = image.channel[3].plane.Row(iy); for (size_t ix = 0; ix < r.xsize(); ix++) { size_t x = r.x0() + ix; int sharpness = row_in_3[ix]; @@ -448,8 +494,8 @@ Status ModularFrameDecoder::DecodeAcMetadata(size_t group_id, BitReader* reader, } JXL_RETURN_IF_ERROR( ac_strategy.SetNoBoundsCheck(x, y, AcStrategy::Type(row_in_1[num]))); - row_qf[ix] = - 1 + std::max(0, std::min(Quantizer::kQuantMax - 1, row_in_2[num])); + row_qf[ix] = 1 + std::max<int32_t>(0, std::min(Quantizer::kQuantMax - 1, + row_in_2[num])); num++; } } @@ -605,7 +651,13 @@ Status ModularFrameDecoder::ModularImageToDecodedRect( DivCeil(modular_rect.xsize(), 1 << ch_in.hshift), DivCeil(modular_rect.ysize(), 1 << ch_in.vshift)); mr = mr.Crop(ch_in.plane); - + if (r.ysize() != mr.ysize() || r.xsize() != mr.xsize()) { + return JXL_FAILURE("Dimension mismatch: trying to fit a %" PRIuS + "x%" PRIuS + " modular channel into " + "a %" PRIuS "x%" PRIuS " rect", + mr.xsize(), mr.ysize(), r.xsize(), r.ysize()); + } for (size_t y = 0; y < r.ysize(); ++y) { float* const JXL_RESTRICT row_out = r.Row(render_pipeline_input.GetBuffer(3 + ec).first, y); @@ -627,31 +679,35 @@ Status ModularFrameDecoder::ModularImageToDecodedRect( Status ModularFrameDecoder::FinalizeDecoding(PassesDecoderState* dec_state, jxl::ThreadPool* pool, - ImageBundle* output, bool inplace) { if (!use_full_image) return true; Image gi = (inplace ? std::move(full_image) : full_image.clone()); size_t xsize = gi.w; size_t ysize = gi.h; + JXL_DEBUG_V(3, "Finalizing decoding for modular image: %s", + gi.DebugString().c_str()); + // Don't use threads if total image size is smaller than a group if (xsize * ysize < frame_dim.group_dim * frame_dim.group_dim) pool = nullptr; // Undo the global transforms gi.undo_transforms(global_header.wp_header, pool); - for (auto t : global_transform) { - JXL_RETURN_IF_ERROR(t.Inverse(gi, global_header.wp_header)); - } + JXL_DASSERT(global_transform.empty()); if (gi.error) return JXL_FAILURE("Undoing transforms failed"); + for (size_t i = 0; i < dec_state->shared->frame_dim.num_groups; i++) { + dec_state->render_pipeline->ClearDone(i); + } std::atomic<bool> has_error{false}; JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, dec_state->shared->frame_dim.num_groups, [&](size_t num_threads) { - return dec_state->render_pipeline->PrepareForThreads( - num_threads, - /*use_group_ids=*/dec_state->shared->frame_header.encoding == - FrameEncoding::kVarDCT); + const auto& frame_header = dec_state->shared->frame_header; + bool use_group_ids = (frame_header.encoding == FrameEncoding::kVarDCT || + (frame_header.flags & FrameHeader::kNoise)); + return dec_state->render_pipeline->PrepareForThreads(num_threads, + use_group_ids); }, [&](const uint32_t group, size_t thread_id) { RenderPipelineInput input = @@ -701,7 +757,7 @@ Status ModularFrameDecoder::DecodeQuantTable( encoding->qraw.qtable->resize(required_size_x * required_size_y * 3); for (size_t c = 0; c < 3; c++) { for (size_t y = 0; y < required_size_y; y++) { - int* JXL_RESTRICT row = image.channel[c].Row(y); + int32_t* JXL_RESTRICT row = image.channel[c].Row(y); for (size_t x = 0; x < required_size_x; x++) { (*encoding->qraw.qtable)[c * required_size_x * required_size_y + y * required_size_x + x] = row[x]; diff --git a/media/libjxl/src/lib/jxl/dec_modular.h b/media/libjxl/src/lib/jxl/dec_modular.h index 026a2c9e2a..ec94b46482 100644 --- a/media/libjxl/src/lib/jxl/dec_modular.h +++ b/media/libjxl/src/lib/jxl/dec_modular.h @@ -8,15 +8,15 @@ #include <stddef.h> +#include <string> + #include "lib/jxl/aux_out_fwd.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/status.h" #include "lib/jxl/dec_bit_reader.h" #include "lib/jxl/dec_cache.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/frame_header.h" #include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" #include "lib/jxl/modular/encoding/encoding.h" #include "lib/jxl/modular/modular_image.h" @@ -82,6 +82,7 @@ struct ModularStreamId { static size_t Num(const FrameDimensions& frame_dim, size_t passes) { return ModularAC(0, passes).ID(frame_dim); } + std::string DebugString() const; }; class ModularFrameDecoder { @@ -93,7 +94,7 @@ class ModularFrameDecoder { int maxShift, const ModularStreamId& stream, bool zerofill, PassesDecoderState* dec_state, RenderPipelineInput* render_pipeline_input, - ImageBundle* output, bool allow_truncated); + bool allow_truncated, bool* should_run_pipeline = nullptr); // Decodes a VarDCT DC group (`group_id`) from the given `reader`. Status DecodeVarDCTDC(size_t group_id, BitReader* reader, PassesDecoderState* dec_state); @@ -111,7 +112,7 @@ class ModularFrameDecoder { // if it is false, it can be called multiple times (e.g. for progressive // steps) Status FinalizeDecoding(PassesDecoderState* dec_state, jxl::ThreadPool* pool, - ImageBundle* output, bool inplace); + bool inplace); bool have_dc() const { return have_something; } void MaybeDropFullImage(); bool UsesFullImage() const { return use_full_image; } diff --git a/media/libjxl/src/lib/jxl/dec_noise.cc b/media/libjxl/src/lib/jxl/dec_noise.cc index d232e48c03..f48398b5cf 100644 --- a/media/libjxl/src/lib/jxl/dec_noise.cc +++ b/media/libjxl/src/lib/jxl/dec_noise.cc @@ -29,6 +29,7 @@ namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Or; using hwy::HWY_NAMESPACE::ShiftRight; using hwy::HWY_NAMESPACE::Vec; @@ -46,7 +47,7 @@ void BitsToFloat(const uint32_t* JXL_RESTRICT random_bits, const auto bits = Load(du, random_bits); // 1.0 + 23 random mantissa bits = [1, 2) - const auto rand12 = BitCast(df, ShiftRight<9>(bits) | Set(du, 0x3F800000)); + const auto rand12 = BitCast(df, Or(ShiftRight<9>(bits), Set(du, 0x3F800000))); Store(rand12, df, floats); } @@ -58,7 +59,7 @@ void RandomImage(Xorshift128Plus* rng, const Rect& rect, // May exceed the vector size, hence we have two loops over x below. constexpr size_t kFloatsPerBatch = Xorshift128Plus::N * sizeof(uint64_t) / sizeof(float); - HWY_ALIGN uint64_t batch[Xorshift128Plus::N]; + HWY_ALIGN uint64_t batch[Xorshift128Plus::N] = {}; const HWY_FULL(float) df; const size_t N = Lanes(df); diff --git a/media/libjxl/src/lib/jxl/dec_params.h b/media/libjxl/src/lib/jxl/dec_params.h deleted file mode 100644 index a9a4b105ce..0000000000 --- a/media/libjxl/src/lib/jxl/dec_params.h +++ /dev/null @@ -1,52 +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. - -#ifndef LIB_JXL_DEC_PARAMS_H_ -#define LIB_JXL_DEC_PARAMS_H_ - -// Parameters and flags that govern JXL decompression. - -#include <stddef.h> -#include <stdint.h> - -#include <limits> - -#include "lib/jxl/base/override.h" - -namespace jxl { - -struct DecompressParams { - // If true, checks at the end of decoding that all of the compressed data - // was consumed by the decoder. - bool check_decompressed_size = true; - - // If true, skip dequant and iDCT and decode to JPEG (only if possible) - bool keep_dct = false; - // If true, render spot colors (otherwise only returned as extra channels) - bool render_spotcolors = true; - // If true, coalesce frames (otherwise return unblended frames) - bool coalescing = true; - - // How many passes to decode at most. By default, decode everything. - uint32_t max_passes = std::numeric_limits<uint32_t>::max(); - // Alternatively, one can specify the maximum tolerable downscaling factor - // with respect to the full size of the image. By default, nothing less than - // the full size is requested. - size_t max_downsampling = 1; - - // Try to decode as much as possible of a truncated codestream, but only whole - // sections at a time. - bool allow_partial_files = false; - // Allow even more progression. - bool allow_more_progressive_steps = false; - - // Internal test-only setting: whether or not to use the slow rendering - // pipeline. - bool use_slow_render_pipeline = false; -}; - -} // namespace jxl - -#endif // LIB_JXL_DEC_PARAMS_H_ diff --git a/media/libjxl/src/lib/jxl/dec_patch_dictionary.cc b/media/libjxl/src/lib/jxl/dec_patch_dictionary.cc index 194a932f36..4f8720922f 100644 --- a/media/libjxl/src/lib/jxl/dec_patch_dictionary.cc +++ b/media/libjxl/src/lib/jxl/dec_patch_dictionary.cc @@ -35,8 +35,6 @@ namespace jxl { -constexpr size_t kMaxPatches = 1 << 24; - Status PatchDictionary::Decode(BitReader* br, size_t xsize, size_t ysize, bool* uses_extra_channels) { positions_.clear(); @@ -52,10 +50,16 @@ Status PatchDictionary::Decode(BitReader* br, size_t xsize, size_t ysize, }; size_t num_ref_patch = read_num(kNumRefPatchContext); - // TODO(veluca): does this make sense? - if (num_ref_patch > kMaxPatches) { + // Limit max memory usage of patches to about 66 bytes per pixel (assuming 8 + // bytes per size_t) + const size_t num_pixels = xsize * ysize; + const size_t max_ref_patches = 1024 + num_pixels / 4; + const size_t max_patches = max_ref_patches * 4; + const size_t max_blending_infos = max_patches * 4; + if (num_ref_patch > max_ref_patches) { return JXL_FAILURE("Too many patches in dictionary"); } + size_t num_ec = shared_->metadata->m.num_extra_channels; size_t total_patches = 0; size_t next_size = 1; @@ -84,17 +88,21 @@ Status PatchDictionary::Decode(BitReader* br, size_t xsize, size_t ysize, } size_t id_count = read_num(kPatchCountContext) + 1; total_patches += id_count; - if (total_patches > kMaxPatches) { + if (total_patches > max_patches) { return JXL_FAILURE("Too many patches in dictionary"); } if (next_size < total_patches) { next_size *= 2; - next_size = std::min<size_t>(next_size, kMaxPatches); + next_size = std::min<size_t>(next_size, max_patches); + } + if (next_size * (num_ec + 1) > max_blending_infos) { + return JXL_FAILURE("Too many patches in dictionary"); } positions_.reserve(next_size); + blendings_.reserve(next_size * (num_ec + 1)); for (size_t i = 0; i < id_count; i++) { PatchPosition pos; - pos.ref_pos = ref_pos; + pos.ref_pos_idx = ref_positions_.size(); if (i == 0) { pos.x = read_num(kPatchPositionContext); pos.y = read_num(kPatchPositionContext); @@ -114,8 +122,7 @@ Status PatchDictionary::Decode(BitReader* br, size_t xsize, size_t ysize, " > %" PRIuS, pos.y, ref_pos.ysize, ysize); } - for (size_t j = 0; j < shared_->metadata->m.extra_channel_info.size() + 1; - j++) { + for (size_t j = 0; j < num_ec + 1; j++) { uint32_t blend_mode = read_num(kPatchBlendModeContext); if (blend_mode >= uint32_t(PatchBlendMode::kNumBlendModes)) { return JXL_FAILURE("Invalid patch blend mode: %u", blend_mode); @@ -146,10 +153,11 @@ Status PatchDictionary::Decode(BitReader* br, size_t xsize, size_t ysize, } else { info.clamp = false; } - pos.blending.push_back(info); + blendings_.push_back(info); } positions_.push_back(std::move(pos)); } + ref_positions_.emplace_back(std::move(ref_pos)); } positions_.shrink_to_fit(); @@ -157,80 +165,182 @@ Status PatchDictionary::Decode(BitReader* br, size_t xsize, size_t ysize, return JXL_FAILURE("ANS checksum failure."); } - ComputePatchCache(); + ComputePatchTree(); return true; } int PatchDictionary::GetReferences() const { int result = 0; - for (size_t i = 0; i < positions_.size(); ++i) { - result |= (1 << static_cast<int>(positions_[i].ref_pos.ref)); + for (size_t i = 0; i < ref_positions_.size(); ++i) { + result |= (1 << static_cast<int>(ref_positions_[i].ref)); } return result; } -void PatchDictionary::ComputePatchCache() { - patch_starts_.clear(); - sorted_patches_.clear(); - if (positions_.empty()) return; - std::vector<std::pair<size_t, size_t>> sorted_patches_y; - for (size_t i = 0; i < positions_.size(); i++) { - const PatchPosition& pos = positions_[i]; - for (size_t y = pos.y; y < pos.y + pos.ref_pos.ysize; y++) { - sorted_patches_y.emplace_back(y, i); - } +namespace { +struct PatchInterval { + size_t idx; + size_t y0, y1; +}; +} // namespace + +void PatchDictionary::ComputePatchTree() { + patch_tree_.clear(); + num_patches_.clear(); + sorted_patches_y0_.clear(); + sorted_patches_y1_.clear(); + if (positions_.empty()) { + return; } - // The relative order of patches that affect the same pixels is preserved. - // This is important for patches that have a blend mode different from kAdd. - std::sort(sorted_patches_y.begin(), sorted_patches_y.end()); - patch_starts_.resize(sorted_patches_y.back().first + 2, - sorted_patches_y.size()); - sorted_patches_.resize(sorted_patches_y.size()); - for (size_t i = 0; i < sorted_patches_y.size(); i++) { - sorted_patches_[i] = sorted_patches_y[i].second; - patch_starts_[sorted_patches_y[i].first] = - std::min(patch_starts_[sorted_patches_y[i].first], i); + // Create a y-interval for each patch. + std::vector<PatchInterval> intervals(positions_.size()); + for (size_t i = 0; i < positions_.size(); ++i) { + const auto& pos = positions_[i]; + intervals[i].idx = i; + intervals[i].y0 = pos.y; + intervals[i].y1 = pos.y + ref_positions_[pos.ref_pos_idx].ysize; + } + auto sort_by_y0 = [&intervals](size_t start, size_t end) { + std::sort(intervals.data() + start, intervals.data() + end, + [](const PatchInterval& i0, const PatchInterval& i1) { + return i0.y0 < i1.y0; + }); + }; + auto sort_by_y1 = [&intervals](size_t start, size_t end) { + std::sort(intervals.data() + start, intervals.data() + end, + [](const PatchInterval& i0, const PatchInterval& i1) { + return i0.y1 < i1.y1; + }); + }; + // Count the number of patches for each row. + sort_by_y1(0, intervals.size()); + num_patches_.resize(intervals.back().y1); + for (auto iv : intervals) { + for (size_t y = iv.y0; y < iv.y1; ++y) num_patches_[y]++; + } + PatchTreeNode root; + root.start = 0; + root.num = intervals.size(); + patch_tree_.push_back(root); + size_t next = 0; + while (next < patch_tree_.size()) { + auto& node = patch_tree_[next]; + size_t start = node.start; + size_t end = node.start + node.num; + // Choose the y_center for this node to be the median of interval starts. + sort_by_y0(start, end); + size_t middle_idx = start + node.num / 2; + node.y_center = intervals[middle_idx].y0; + // Divide the intervals in [start, end) into three groups: + // * those completely to the right of y_center: [right_start, end) + // * those overlapping y_center: [left_end, right_start) + // * those completely to the left of y_center: [start, left_end) + size_t right_start = middle_idx; + while (right_start < end && intervals[right_start].y0 == node.y_center) { + ++right_start; + } + sort_by_y1(start, right_start); + size_t left_end = right_start; + while (left_end > start && intervals[left_end - 1].y1 > node.y_center) { + --left_end; + } + // Fill in sorted_patches_y0_ and sorted_patches_y1_ for the current node. + node.num = right_start - left_end; + node.start = sorted_patches_y0_.size(); + for (ssize_t i = static_cast<ssize_t>(right_start) - 1; + i >= static_cast<ssize_t>(left_end); --i) { + sorted_patches_y1_.push_back({intervals[i].y1, intervals[i].idx}); + } + sort_by_y0(left_end, right_start); + for (size_t i = left_end; i < right_start; ++i) { + sorted_patches_y0_.push_back({intervals[i].y0, intervals[i].idx}); + } + // Create the left and right nodes (if not empty). + node.left_child = node.right_child = -1; + if (left_end > start) { + PatchTreeNode left; + left.start = start; + left.num = left_end - left.start; + patch_tree_[next].left_child = patch_tree_.size(); + patch_tree_.push_back(left); + } + if (right_start < end) { + PatchTreeNode right; + right.start = right_start; + right.num = end - right.start; + patch_tree_[next].right_child = patch_tree_.size(); + patch_tree_.push_back(right); + } + ++next; } - for (size_t i = patch_starts_.size() - 1; i > 0; i--) { - patch_starts_[i - 1] = std::min(patch_starts_[i], patch_starts_[i - 1]); +} + +std::vector<size_t> PatchDictionary::GetPatchesForRow(size_t y) const { + std::vector<size_t> result; + if (y < num_patches_.size() && num_patches_[y] > 0) { + result.reserve(num_patches_[y]); + for (ssize_t tree_idx = 0; tree_idx != -1;) { + JXL_DASSERT(tree_idx < (ssize_t)patch_tree_.size()); + const auto& node = patch_tree_[tree_idx]; + if (y <= node.y_center) { + for (size_t i = 0; i < node.num; ++i) { + const auto& p = sorted_patches_y0_[node.start + i]; + if (y < p.first) break; + result.push_back(p.second); + } + tree_idx = y < node.y_center ? node.left_child : -1; + } else { + for (size_t i = 0; i < node.num; ++i) { + const auto& p = sorted_patches_y1_[node.start + i]; + if (y >= p.first) break; + result.push_back(p.second); + } + tree_idx = node.right_child; + } + } + // Ensure that he relative order of patches that affect the same pixels is + // preserved. This is important for patches that have a blend mode + // different from kAdd. + std::sort(result.begin(), result.end()); } + return result; } // Adds patches to a segment of `xsize` pixels, starting at `inout`, assumed // to be located at position (x0, y) in the frame. void PatchDictionary::AddOneRow(float* const* inout, size_t y, size_t x0, size_t xsize) const { - if (patch_starts_.empty()) return; size_t num_ec = shared_->metadata->m.num_extra_channels; std::vector<const float*> fg_ptrs(3 + num_ec); - if (y + 1 >= patch_starts_.size()) return; - for (size_t id = patch_starts_[y]; id < patch_starts_[y + 1]; id++) { - const PatchPosition& pos = positions_[sorted_patches_[id]]; + for (size_t pos_idx : GetPatchesForRow(y)) { + const size_t blending_idx = pos_idx * (num_ec + 1); + const PatchPosition& pos = positions_[pos_idx]; + const PatchReferencePosition& ref_pos = ref_positions_[pos.ref_pos_idx]; size_t by = pos.y; size_t bx = pos.x; - size_t patch_xsize = pos.ref_pos.xsize; + size_t patch_xsize = ref_pos.xsize; JXL_DASSERT(y >= by); - JXL_DASSERT(y < by + pos.ref_pos.ysize); + JXL_DASSERT(y < by + ref_pos.ysize); size_t iy = y - by; - size_t ref = pos.ref_pos.ref; + size_t ref = ref_pos.ref; if (bx >= x0 + xsize) continue; if (bx + patch_xsize < x0) continue; size_t patch_x0 = std::max(bx, x0); size_t patch_x1 = std::min(bx + patch_xsize, x0 + xsize); for (size_t c = 0; c < 3; c++) { fg_ptrs[c] = shared_->reference_frames[ref].frame->color()->ConstPlaneRow( - c, pos.ref_pos.y0 + iy) + - pos.ref_pos.x0 + x0 - bx; + c, ref_pos.y0 + iy) + + ref_pos.x0 + x0 - bx; } for (size_t i = 0; i < num_ec; i++) { fg_ptrs[3 + i] = shared_->reference_frames[ref].frame->extra_channels()[i].ConstRow( - pos.ref_pos.y0 + iy) + - pos.ref_pos.x0 + x0 - bx; + ref_pos.y0 + iy) + + ref_pos.x0 + x0 - bx; } PerformBlending(inout, fg_ptrs.data(), inout, patch_x0 - x0, - patch_x1 - patch_x0, pos.blending[0], - pos.blending.data() + 1, + patch_x1 - patch_x0, blendings_[blending_idx], + blendings_.data() + blending_idx + 1, shared_->metadata->m.extra_channel_info); } } diff --git a/media/libjxl/src/lib/jxl/dec_patch_dictionary.h b/media/libjxl/src/lib/jxl/dec_patch_dictionary.h index 5ffdb8b6db..a950e83e85 100644 --- a/media/libjxl/src/lib/jxl/dec_patch_dictionary.h +++ b/media/libjxl/src/lib/jxl/dec_patch_dictionary.h @@ -78,25 +78,12 @@ struct PatchBlending { // Position and size of the patch in the reference frame. struct PatchReferencePosition { size_t ref, x0, y0, xsize, ysize; - bool operator<(const PatchReferencePosition& oth) const { - return std::make_tuple(ref, x0, y0, xsize, ysize) < - std::make_tuple(oth.ref, oth.x0, oth.y0, oth.xsize, oth.ysize); - } - bool operator==(const PatchReferencePosition& oth) const { - return !(*this < oth) && !(oth < *this); - } }; struct PatchPosition { // Position of top-left corner of the patch in the image. size_t x, y; - // Different blend mode for color and extra channels. - std::vector<PatchBlending> blending; - PatchReferencePosition ref_pos; - bool operator<(const PatchPosition& oth) const { - return std::make_tuple(ref_pos, x, y) < - std::make_tuple(oth.ref_pos, oth.x, oth.y); - } + size_t ref_pos_idx; }; struct PassesSharedState; @@ -119,7 +106,7 @@ class PatchDictionary { void Clear() { positions_.clear(); - ComputePatchCache(); + ComputePatchTree(); } // Adds patches to a segment of `xsize` pixels, starting at `inout`, assumed @@ -130,25 +117,33 @@ class PatchDictionary { // bit mask: bits 0-3 indicate reference frame 0-3. int GetReferences() const; + std::vector<size_t> GetPatchesForRow(size_t y) const; + private: friend class PatchDictionaryEncoder; const PassesSharedState* shared_; std::vector<PatchPosition> positions_; - - // Patch occurrences sorted by y. - std::vector<size_t> sorted_patches_; - // Index of the first patch for each y value. - std::vector<size_t> patch_starts_; - - // Patch IDs in position [patch_starts_[y], patch_start_[y+1]) of - // sorted_patches_ are all the patches that intersect the horizontal line at - // y. - // The relative order of patches that affect the same pixels is the same - - // important when applying patches is noncommutative. - - // Compute patches_by_y_ after updating positions_. - void ComputePatchCache(); + std::vector<PatchReferencePosition> ref_positions_; + std::vector<PatchBlending> blendings_; + + // Interval tree on the y coordinates of the patches. + struct PatchTreeNode { + ssize_t left_child; + ssize_t right_child; + size_t y_center; + // Range of patches in sorted_patches_y0_ and sorted_patches_y1_ that + // contain the row y_center. + size_t start; + size_t num; + }; + std::vector<PatchTreeNode> patch_tree_; + // Number of patches for each row. + std::vector<size_t> num_patches_; + std::vector<std::pair<size_t, size_t>> sorted_patches_y0_; + std::vector<std::pair<size_t, size_t>> sorted_patches_y1_; + + void ComputePatchTree(); }; } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/dec_tone_mapping-inl.h b/media/libjxl/src/lib/jxl/dec_tone_mapping-inl.h new file mode 100644 index 0000000000..a3260372cf --- /dev/null +++ b/media/libjxl/src/lib/jxl/dec_tone_mapping-inl.h @@ -0,0 +1,232 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#if defined(LIB_JXL_DEC_TONE_MAPPING_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_DEC_TONE_MAPPING_INL_H_ +#undef LIB_JXL_DEC_TONE_MAPPING_INL_H_ +#else +#define LIB_JXL_DEC_TONE_MAPPING_INL_H_ +#endif + +#include <hwy/highway.h> + +#include "lib/jxl/transfer_functions-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Clamp; +using hwy::HWY_NAMESPACE::Max; +using hwy::HWY_NAMESPACE::ZeroIfNegative; + +template <typename D> +class Rec2408ToneMapper { + private: + using V = hwy::HWY_NAMESPACE::Vec<D>; + + public: + explicit Rec2408ToneMapper(std::pair<float, float> source_range, + std::pair<float, float> target_range, + const float primaries_luminances[3]) + : source_range_(source_range), + target_range_(target_range), + red_Y_(primaries_luminances[0]), + green_Y_(primaries_luminances[1]), + blue_Y_(primaries_luminances[2]) {} + + void ToneMap(V* red, V* green, V* blue) const { + const V luminance = Mul(Set(df_, source_range_.second), + (MulAdd(Set(df_, red_Y_), *red, + MulAdd(Set(df_, green_Y_), *green, + Mul(Set(df_, blue_Y_), *blue))))); + const V pq_mastering_min = Set(df_, pq_mastering_min_); + const V inv_pq_mastering_range = Set(df_, inv_pq_mastering_range_); + const V normalized_pq = Min( + Set(df_, 1.f), + Mul(Sub(InvEOTF(luminance), pq_mastering_min), inv_pq_mastering_range)); + const V ks = Set(df_, ks_); + const V e2 = + IfThenElse(Lt(normalized_pq, ks), normalized_pq, P(normalized_pq)); + const V one_minus_e2 = Sub(Set(df_, 1), e2); + const V one_minus_e2_2 = Mul(one_minus_e2, one_minus_e2); + const V one_minus_e2_4 = Mul(one_minus_e2_2, one_minus_e2_2); + const V b = Set(df_, min_lum_); + const V e3 = MulAdd(b, one_minus_e2_4, e2); + const V pq_mastering_range = Set(df_, pq_mastering_range_); + const V e4 = MulAdd(e3, pq_mastering_range, pq_mastering_min); + const V new_luminance = + Min(Set(df_, target_range_.second), + ZeroIfNegative( + Mul(Set(df_, 10000), TF_PQ().DisplayFromEncoded(df_, e4)))); + + const V ratio = Div(new_luminance, luminance); + + const V normalizer = Set(df_, normalizer_); + for (V* const val : {red, green, blue}) { + *val = Mul(IfThenElse(Le(luminance, Set(df_, 1e-6f)), new_luminance, + Mul(*val, ratio)), + normalizer); + } + } + + private: + V InvEOTF(const V luminance) const { + return TF_PQ().EncodedFromDisplay(df_, + Mul(luminance, Set(df_, 1. / 10000))); + } + float InvEOTF(const float luminance) const { + return TF_PQ().EncodedFromDisplay(luminance / 10000.0f); + } + V T(const V a) const { + const V ks = Set(df_, ks_); + const V inv_one_minus_ks = Set(df_, inv_one_minus_ks_); + return Mul(Sub(a, ks), inv_one_minus_ks); + } + V P(const V b) const { + const V t_b = T(b); + const V t_b_2 = Mul(t_b, t_b); + const V t_b_3 = Mul(t_b_2, t_b); + const V ks = Set(df_, ks_); + const V max_lum = Set(df_, max_lum_); + return MulAdd( + MulAdd(Set(df_, 2), t_b_3, MulAdd(Set(df_, -3), t_b_2, Set(df_, 1))), + ks, + MulAdd(Add(t_b_3, MulAdd(Set(df_, -2), t_b_2, t_b)), + Sub(Set(df_, 1), ks), + MulAdd(Set(df_, -2), t_b_3, + Mul(Mul(Set(df_, 3), t_b_2), max_lum)))); + } + + D df_; + const std::pair<float, float> source_range_; + const std::pair<float, float> target_range_; + const float red_Y_; + const float green_Y_; + const float blue_Y_; + + const float pq_mastering_min_ = InvEOTF(source_range_.first); + const float pq_mastering_max_ = InvEOTF(source_range_.second); + const float pq_mastering_range_ = pq_mastering_max_ - pq_mastering_min_; + const float inv_pq_mastering_range_ = 1.0f / pq_mastering_range_; + // TODO(eustas): divide instead of inverse-multiply? + const float min_lum_ = (InvEOTF(target_range_.first) - pq_mastering_min_) * + inv_pq_mastering_range_; + // TODO(eustas): divide instead of inverse-multiply? + const float max_lum_ = (InvEOTF(target_range_.second) - pq_mastering_min_) * + inv_pq_mastering_range_; + const float ks_ = 1.5f * max_lum_ - 0.5f; + const float b_ = min_lum_; + + const float inv_one_minus_ks_ = 1.0f / std::max(1e-6f, 1.0f - ks_); + + const float normalizer_ = source_range_.second / target_range_.second; +}; + +class HlgOOTF { + public: + explicit HlgOOTF(float source_luminance, float target_luminance, + const float primaries_luminances[3]) + : HlgOOTF(/*gamma=*/std::pow( + 1.111f, std::log2(target_luminance / source_luminance)), + primaries_luminances) {} + + static HlgOOTF FromSceneLight(float display_luminance, + const float primaries_luminances[3]) { + return HlgOOTF(/*gamma=*/1.2f * + std::pow(1.111f, std::log2(display_luminance / 1000.f)), + primaries_luminances); + } + + static HlgOOTF ToSceneLight(float display_luminance, + const float primaries_luminances[3]) { + return HlgOOTF( + /*gamma=*/(1 / 1.2f) * + std::pow(1.111f, -std::log2(display_luminance / 1000.f)), + primaries_luminances); + } + + template <typename V> + void Apply(V* red, V* green, V* blue) const { + hwy::HWY_NAMESPACE::DFromV<V> df; + if (!apply_ootf_) return; + const V luminance = + MulAdd(Set(df, red_Y_), *red, + MulAdd(Set(df, green_Y_), *green, Mul(Set(df, blue_Y_), *blue))); + const V ratio = + Min(FastPowf(df, luminance, Set(df, exponent_)), Set(df, 1e9)); + *red = Mul(*red, ratio); + *green = Mul(*green, ratio); + *blue = Mul(*blue, ratio); + } + + bool WarrantsGamutMapping() const { return apply_ootf_ && exponent_ < 0; } + + private: + explicit HlgOOTF(float gamma, const float luminances[3]) + : exponent_(gamma - 1), + red_Y_(luminances[0]), + green_Y_(luminances[1]), + blue_Y_(luminances[2]) {} + const float exponent_; + const bool apply_ootf_ = exponent_ < -0.01f || 0.01f < exponent_; + const float red_Y_; + const float green_Y_; + const float blue_Y_; +}; + +template <typename V> +void GamutMap(V* red, V* green, V* blue, const float primaries_luminances[3], + float preserve_saturation = 0.1f) { + hwy::HWY_NAMESPACE::DFromV<V> df; + const V luminance = + MulAdd(Set(df, primaries_luminances[0]), *red, + MulAdd(Set(df, primaries_luminances[1]), *green, + Mul(Set(df, primaries_luminances[2]), *blue))); + + // Desaturate out-of-gamut pixels. This is done by mixing each pixel + // with just enough gray of the target luminance to make all + // components non-negative. + // - For saturation preservation, if a component is still larger than + // 1 then the pixel is normalized to have a maximum component of 1. + // That will reduce its luminance. + // - For luminance preservation, getting all components below 1 is + // done by mixing in yet more gray. That will desaturate it further. + V gray_mix_saturation = Zero(df); + V gray_mix_luminance = Zero(df); + for (const V* ch : {red, green, blue}) { + const V& val = *ch; + const V inv_val_minus_gray = Div(Set(df, 1), (Sub(val, luminance))); + gray_mix_saturation = + IfThenElse(Ge(val, luminance), gray_mix_saturation, + Max(gray_mix_saturation, Mul(val, inv_val_minus_gray))); + gray_mix_luminance = + Max(gray_mix_luminance, + IfThenElse(Le(val, luminance), gray_mix_saturation, + Mul(Sub(val, Set(df, 1)), inv_val_minus_gray))); + } + const V gray_mix = Clamp( + MulAdd(Set(df, preserve_saturation), + Sub(gray_mix_saturation, gray_mix_luminance), gray_mix_luminance), + Zero(df), Set(df, 1)); + for (V* const val : {red, green, blue}) { + *val = MulAdd(gray_mix, Sub(luminance, *val), *val); + } + const V normalizer = + Div(Set(df, 1), Max(Set(df, 1), Max(*red, Max(*green, *blue)))); + for (V* const val : {red, green, blue}) { + *val = Mul(*val, normalizer); + } +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_DEC_TONE_MAPPING_INL_H_ diff --git a/media/libjxl/src/lib/jxl/dec_transforms-inl.h b/media/libjxl/src/lib/jxl/dec_transforms-inl.h index 37f3cb7a4d..075619b3b9 100644 --- a/media/libjxl/src/lib/jxl/dec_transforms-inl.h +++ b/media/libjxl/src/lib/jxl/dec_transforms-inl.h @@ -23,6 +23,9 @@ namespace jxl { namespace HWY_NAMESPACE { namespace { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::MulAdd; + // Computes the lowest-frequency LF_ROWSxLF_COLS-sized square in output, which // is a DCT_ROWS*DCT_COLS-sized DCT block, by doing a ROWS*COLS DCT on the // input block. diff --git a/media/libjxl/src/lib/jxl/dec_xyb-inl.h b/media/libjxl/src/lib/jxl/dec_xyb-inl.h index 344b4cfe6c..a4f24cd123 100644 --- a/media/libjxl/src/lib/jxl/dec_xyb-inl.h +++ b/media/libjxl/src/lib/jxl/dec_xyb-inl.h @@ -21,7 +21,11 @@ namespace HWY_NAMESPACE { namespace { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; using hwy::HWY_NAMESPACE::Broadcast; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::Sub; // Inverts the pixel-wise RGB->XYB conversion in OpsinDynamicsImage() (including // the gamma mixing and simple gamma). Avoids clamping to [0, 1] - out of (sRGB) @@ -48,18 +52,18 @@ HWY_INLINE HWY_MAYBE_UNUSED void XybToRgb(D d, const V opsin_x, const V opsin_y, #endif // Color space: XYB -> RGB - auto gamma_r = opsin_y + opsin_x; - auto gamma_g = opsin_y - opsin_x; + auto gamma_r = Add(opsin_y, opsin_x); + auto gamma_g = Sub(opsin_y, opsin_x); auto gamma_b = opsin_b; - gamma_r -= Set(d, opsin_params.opsin_biases_cbrt[0]); - gamma_g -= Set(d, opsin_params.opsin_biases_cbrt[1]); - gamma_b -= Set(d, opsin_params.opsin_biases_cbrt[2]); + gamma_r = Sub(gamma_r, Set(d, opsin_params.opsin_biases_cbrt[0])); + gamma_g = Sub(gamma_g, Set(d, opsin_params.opsin_biases_cbrt[1])); + gamma_b = Sub(gamma_b, Set(d, opsin_params.opsin_biases_cbrt[2])); // Undo gamma compression: linear = gamma^3 for efficiency. - const auto gamma_r2 = gamma_r * gamma_r; - const auto gamma_g2 = gamma_g * gamma_g; - const auto gamma_b2 = gamma_b * gamma_b; + const auto gamma_r2 = Mul(gamma_r, gamma_r); + const auto gamma_g2 = Mul(gamma_g, gamma_g); + const auto gamma_b2 = Mul(gamma_b, gamma_b); const auto mixed_r = MulAdd(gamma_r2, gamma_r, neg_bias_r); const auto mixed_g = MulAdd(gamma_g2, gamma_g, neg_bias_g); const auto mixed_b = MulAdd(gamma_b2, gamma_b, neg_bias_b); @@ -67,9 +71,10 @@ HWY_INLINE HWY_MAYBE_UNUSED void XybToRgb(D d, const V opsin_x, const V opsin_y, const float* HWY_RESTRICT inverse_matrix = opsin_params.inverse_opsin_matrix; // Unmix (multiply by 3x3 inverse_matrix) - *linear_r = LoadDup128(d, &inverse_matrix[0 * 4]) * mixed_r; - *linear_g = LoadDup128(d, &inverse_matrix[3 * 4]) * mixed_r; - *linear_b = LoadDup128(d, &inverse_matrix[6 * 4]) * mixed_r; + // TODO(eustas): ref would be more readable than pointer + *linear_r = Mul(LoadDup128(d, &inverse_matrix[0 * 4]), mixed_r); + *linear_g = Mul(LoadDup128(d, &inverse_matrix[3 * 4]), mixed_r); + *linear_b = Mul(LoadDup128(d, &inverse_matrix[6 * 4]), mixed_r); *linear_r = MulAdd(LoadDup128(d, &inverse_matrix[1 * 4]), mixed_g, *linear_r); *linear_g = MulAdd(LoadDup128(d, &inverse_matrix[4 * 4]), mixed_g, *linear_g); *linear_b = MulAdd(LoadDup128(d, &inverse_matrix[7 * 4]), mixed_g, *linear_b); diff --git a/media/libjxl/src/lib/jxl/dec_xyb.cc b/media/libjxl/src/lib/jxl/dec_xyb.cc index 313abd6a81..ef4088f101 100644 --- a/media/libjxl/src/lib/jxl/dec_xyb.cc +++ b/media/libjxl/src/lib/jxl/dec_xyb.cc @@ -28,6 +28,7 @@ namespace HWY_NAMESPACE { // These templates are not found via ADL. using hwy::HWY_NAMESPACE::Broadcast; +using hwy::HWY_NAMESPACE::MulAdd; void OpsinToLinearInplace(Image3F* JXL_RESTRICT inout, ThreadPool* pool, const OpsinParams& opsin_params) { @@ -135,12 +136,12 @@ void YcbcrToRgb(const Image3F& ycbcr, Image3F* rgb, const Rect& rect) { float* g_row = rect.PlaneRow(rgb, 1, y); float* b_row = rect.PlaneRow(rgb, 2, y); for (size_t x = 0; x < xsize; x += S) { - const auto y_vec = Load(df, y_row + x) + c128; + const auto y_vec = Add(Load(df, y_row + x), c128); const auto cb_vec = Load(df, cb_row + x); const auto cr_vec = Load(df, cr_row + x); - const auto r_vec = crcr * cr_vec + y_vec; - const auto g_vec = cgcr * cr_vec + cgcb * cb_vec + y_vec; - const auto b_vec = cbcb * cb_vec + y_vec; + const auto r_vec = MulAdd(crcr, cr_vec, y_vec); + const auto g_vec = MulAdd(cgcr, cr_vec, MulAdd(cgcb, cb_vec, y_vec)); + const auto b_vec = MulAdd(cbcb, cb_vec, y_vec); Store(r_vec, df, r_row + x); Store(g_vec, df, g_row + x); Store(b_vec, df, b_row + x); @@ -196,86 +197,31 @@ void OpsinParams::Init(float intensity_target) { } } -Status OutputEncodingInfo::Set(const CodecMetadata& metadata, - const ColorEncoding& default_enc) { - const auto& im = metadata.transform_data.opsin_inverse_matrix; - float inverse_matrix[9]; - memcpy(inverse_matrix, im.inverse_matrix, sizeof(inverse_matrix)); - intensity_target = metadata.m.IntensityTarget(); - if (metadata.m.xyb_encoded) { - const auto& orig_color_encoding = metadata.m.color_encoding; - color_encoding = default_enc; - // Figure out if we can output to this color encoding. - do { - if (!orig_color_encoding.HaveFields()) break; - // TODO(veluca): keep in sync with dec_reconstruct.cc - if (!orig_color_encoding.tf.IsPQ() && !orig_color_encoding.tf.IsSRGB() && - !orig_color_encoding.tf.IsGamma() && - !orig_color_encoding.tf.IsLinear() && - !orig_color_encoding.tf.IsHLG() && !orig_color_encoding.tf.IsDCI() && - !orig_color_encoding.tf.Is709()) { - break; - } - if (orig_color_encoding.tf.IsGamma()) { - inverse_gamma = orig_color_encoding.tf.GetGamma(); - } - if (orig_color_encoding.tf.IsDCI()) { - inverse_gamma = 1.0f / 2.6f; - } - if (orig_color_encoding.IsGray() && - orig_color_encoding.white_point != WhitePoint::kD65) { - // TODO(veluca): figure out what should happen here. - break; - } - - if ((orig_color_encoding.primaries != Primaries::kSRGB || - orig_color_encoding.white_point != WhitePoint::kD65) && - !orig_color_encoding.IsGray()) { - all_default_opsin = false; - float srgb_to_xyzd50[9]; - const auto& srgb = ColorEncoding::SRGB(/*is_gray=*/false); - JXL_CHECK(PrimariesToXYZD50( - srgb.GetPrimaries().r.x, srgb.GetPrimaries().r.y, - srgb.GetPrimaries().g.x, srgb.GetPrimaries().g.y, - srgb.GetPrimaries().b.x, srgb.GetPrimaries().b.y, - srgb.GetWhitePoint().x, srgb.GetWhitePoint().y, srgb_to_xyzd50)); - float original_to_xyz[3][3]; - JXL_RETURN_IF_ERROR(PrimariesToXYZ( - orig_color_encoding.GetPrimaries().r.x, - orig_color_encoding.GetPrimaries().r.y, - orig_color_encoding.GetPrimaries().g.x, - orig_color_encoding.GetPrimaries().g.y, - orig_color_encoding.GetPrimaries().b.x, - orig_color_encoding.GetPrimaries().b.y, - orig_color_encoding.GetWhitePoint().x, - orig_color_encoding.GetWhitePoint().y, &original_to_xyz[0][0])); - memcpy(luminances, original_to_xyz[1], sizeof luminances); - float adapt_to_d50[9]; - JXL_RETURN_IF_ERROR(AdaptToXYZD50(orig_color_encoding.GetWhitePoint().x, - orig_color_encoding.GetWhitePoint().y, - adapt_to_d50)); - float xyzd50_to_original[9]; - MatMul(adapt_to_d50, &original_to_xyz[0][0], 3, 3, 3, - xyzd50_to_original); - JXL_RETURN_IF_ERROR(Inv3x3Matrix(xyzd50_to_original)); - float srgb_to_original[9]; - MatMul(xyzd50_to_original, srgb_to_xyzd50, 3, 3, 3, srgb_to_original); - MatMul(srgb_to_original, im.inverse_matrix, 3, 3, 3, inverse_matrix); - } - color_encoding = orig_color_encoding; - color_encoding_is_original = true; - if (color_encoding.tf.IsPQ()) { - intensity_target = 10000; - } - } while (false); - } else { - color_encoding = metadata.m.color_encoding; +bool CanOutputToColorEncoding(const ColorEncoding& c_desired) { + if (!c_desired.HaveFields()) { + return false; } - if (std::abs(intensity_target - 255.0) > 0.1f || !im.all_default) { - all_default_opsin = false; + // TODO(veluca): keep in sync with dec_reconstruct.cc + if (!c_desired.tf.IsPQ() && !c_desired.tf.IsSRGB() && + !c_desired.tf.IsGamma() && !c_desired.tf.IsLinear() && + !c_desired.tf.IsHLG() && !c_desired.tf.IsDCI() && !c_desired.tf.Is709()) { + return false; } - InitSIMDInverseMatrix(inverse_matrix, opsin_params.inverse_opsin_matrix, - intensity_target); + if (c_desired.IsGray() && c_desired.white_point != WhitePoint::kD65) { + // TODO(veluca): figure out what should happen here. + return false; + } + return true; +} + +Status OutputEncodingInfo::SetFromMetadata(const CodecMetadata& metadata) { + orig_color_encoding = metadata.m.color_encoding; + orig_intensity_target = metadata.m.IntensityTarget(); + desired_intensity_target = orig_intensity_target; + const auto& im = metadata.transform_data.opsin_inverse_matrix; + memcpy(orig_inverse_matrix, im.inverse_matrix, sizeof(orig_inverse_matrix)); + default_transform = im.all_default; + xyb_encoded = metadata.m.xyb_encoded; std::copy(std::begin(im.opsin_biases), std::end(im.opsin_biases), opsin_params.opsin_biases); for (int i = 0; i < 3; ++i) { @@ -284,6 +230,92 @@ Status OutputEncodingInfo::Set(const CodecMetadata& metadata, opsin_params.opsin_biases_cbrt[3] = opsin_params.opsin_biases[3] = 1; std::copy(std::begin(im.quant_biases), std::end(im.quant_biases), opsin_params.quant_biases); + bool orig_ok = CanOutputToColorEncoding(orig_color_encoding); + bool orig_grey = orig_color_encoding.IsGray(); + return SetColorEncoding(!xyb_encoded || orig_ok + ? orig_color_encoding + : ColorEncoding::LinearSRGB(orig_grey)); +} + +Status OutputEncodingInfo::MaybeSetColorEncoding( + const ColorEncoding& c_desired) { + if (!xyb_encoded || !CanOutputToColorEncoding(c_desired)) { + return false; + } + return SetColorEncoding(c_desired); +} + +Status OutputEncodingInfo::SetColorEncoding(const ColorEncoding& c_desired) { + color_encoding = c_desired; + color_encoding_is_original = orig_color_encoding.SameColorEncoding(c_desired); + + // Compute the opsin inverse matrix and luminances based on primaries and + // white point. + float inverse_matrix[9]; + bool inverse_matrix_is_default = default_transform; + memcpy(inverse_matrix, orig_inverse_matrix, sizeof(inverse_matrix)); + constexpr float kSRGBLuminances[3] = {0.2126, 0.7152, 0.0722}; + memcpy(luminances, kSRGBLuminances, sizeof(luminances)); + if ((c_desired.primaries != Primaries::kSRGB || + c_desired.white_point != WhitePoint::kD65) && + !c_desired.IsGray()) { + float srgb_to_xyzd50[9]; + const auto& srgb = ColorEncoding::SRGB(/*is_gray=*/false); + JXL_CHECK(PrimariesToXYZD50( + srgb.GetPrimaries().r.x, srgb.GetPrimaries().r.y, + srgb.GetPrimaries().g.x, srgb.GetPrimaries().g.y, + srgb.GetPrimaries().b.x, srgb.GetPrimaries().b.y, + srgb.GetWhitePoint().x, srgb.GetWhitePoint().y, srgb_to_xyzd50)); + float original_to_xyz[3][3]; + JXL_RETURN_IF_ERROR(PrimariesToXYZ( + c_desired.GetPrimaries().r.x, c_desired.GetPrimaries().r.y, + c_desired.GetPrimaries().g.x, c_desired.GetPrimaries().g.y, + c_desired.GetPrimaries().b.x, c_desired.GetPrimaries().b.y, + c_desired.GetWhitePoint().x, c_desired.GetWhitePoint().y, + &original_to_xyz[0][0])); + memcpy(luminances, original_to_xyz[1], sizeof luminances); + if (xyb_encoded) { + float adapt_to_d50[9]; + JXL_RETURN_IF_ERROR(AdaptToXYZD50(c_desired.GetWhitePoint().x, + c_desired.GetWhitePoint().y, + adapt_to_d50)); + float xyzd50_to_original[9]; + MatMul(adapt_to_d50, &original_to_xyz[0][0], 3, 3, 3, xyzd50_to_original); + JXL_RETURN_IF_ERROR(Inv3x3Matrix(xyzd50_to_original)); + float srgb_to_original[9]; + MatMul(xyzd50_to_original, srgb_to_xyzd50, 3, 3, 3, srgb_to_original); + MatMul(srgb_to_original, orig_inverse_matrix, 3, 3, 3, inverse_matrix); + inverse_matrix_is_default = false; + } + } + + if (c_desired.IsGray()) { + float tmp_inv_matrix[9]; + memcpy(tmp_inv_matrix, inverse_matrix, sizeof(inverse_matrix)); + float srgb_to_luma[9]; + memcpy(&srgb_to_luma[0], luminances, sizeof(luminances)); + memcpy(&srgb_to_luma[3], luminances, sizeof(luminances)); + memcpy(&srgb_to_luma[6], luminances, sizeof(luminances)); + MatMul(srgb_to_luma, tmp_inv_matrix, 3, 3, 3, inverse_matrix); + } + + // The internal XYB color space uses absolute luminance, so we scale back the + // opsin inverse matrix to relative luminance where 1.0 corresponds to the + // original intensity target, or to absolute luminance for PQ, where 1.0 + // corresponds to 10000 nits. + if (xyb_encoded) { + float intensity_target = + (c_desired.tf.IsPQ() ? 10000 : orig_intensity_target); + InitSIMDInverseMatrix(inverse_matrix, opsin_params.inverse_opsin_matrix, + intensity_target); + all_default_opsin = (std::abs(intensity_target - 255.0) <= 0.1f && + inverse_matrix_is_default); + } + + // Set the inverse gamma based on color space transfer function. + inverse_gamma = (c_desired.tf.IsGamma() ? c_desired.tf.GetGamma() + : c_desired.tf.IsDCI() ? 1.0f / 2.6f + : 1.0); return true; } diff --git a/media/libjxl/src/lib/jxl/dec_xyb.h b/media/libjxl/src/lib/jxl/dec_xyb.h index 21a46fcac9..ebaae9a176 100644 --- a/media/libjxl/src/lib/jxl/dec_xyb.h +++ b/media/libjxl/src/lib/jxl/dec_xyb.h @@ -29,23 +29,39 @@ struct OpsinParams { }; struct OutputEncodingInfo { + // + // Fields depending only on image metadata + // + ColorEncoding orig_color_encoding; + // Used for the HLG OOTF and PQ tone mapping. + float orig_intensity_target; + // Opsin inverse matrix taken from the metadata. + float orig_inverse_matrix[9]; + bool default_transform; + bool xyb_encoded; + // + // Fields depending on output color encoding + // ColorEncoding color_encoding; - // Used for Gamma and DCI transfer functions. - float inverse_gamma; + bool color_encoding_is_original; // Contains an opsin matrix that converts to the primaries of the output // encoding. OpsinParams opsin_params; - // default_enc is used for xyb encoded image with ICC profile, in other - // cases it has no effect. Use linear sRGB or grayscale if ICC profile is - // not matched (not parsed or no matching ColorEncoding exists) - Status Set(const CodecMetadata& metadata, const ColorEncoding& default_enc); - bool all_default_opsin = true; - bool color_encoding_is_original = false; - // Luminances of color_encoding's primaries, used for the HLG inverse OOTF. + bool all_default_opsin; + // Used for Gamma and DCI transfer functions. + float inverse_gamma; + // Luminances of color_encoding's primaries, used for the HLG inverse OOTF and + // for PQ tone mapping. // Default to sRGB's. - float luminances[3] = {0.2126, 0.7152, 0.0722}; - // Also used for the HLG inverse OOTF. - float intensity_target; + float luminances[3]; + // Used for the HLG inverse OOTF and PQ tone mapping. + float desired_intensity_target; + + Status SetFromMetadata(const CodecMetadata& metadata); + Status MaybeSetColorEncoding(const ColorEncoding& c_desired); + + private: + Status SetColorEncoding(const ColorEncoding& c_desired); }; // Converts `inout` (not padded) from opsin to linear sRGB in-place. Called from diff --git a/media/libjxl/src/lib/jxl/decode.cc b/media/libjxl/src/lib/jxl/decode.cc index e934f7ea0e..1a0facce12 100644 --- a/media/libjxl/src/lib/jxl/decode.cc +++ b/media/libjxl/src/lib/jxl/decode.cc @@ -5,6 +5,7 @@ #include "jxl/decode.h" +#include "jxl/types.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/span.h" #include "lib/jxl/base/status.h" @@ -14,6 +15,7 @@ #include "lib/jxl/dec_modular.h" #include "lib/jxl/decode_to_jpeg.h" #include "lib/jxl/fields.h" +#include "lib/jxl/frame_header.h" #include "lib/jxl/headers.h" #include "lib/jxl/icc_codec.h" #include "lib/jxl/image_bundle.h" @@ -32,16 +34,6 @@ bool OutOfBounds(size_t a, size_t b, size_t size) { return false; } -// Checks if a + b + c > size, taking possible integer overflow into account. -bool OutOfBounds(size_t a, size_t b, size_t c, size_t size) { - size_t pos = a + b; - if (pos < b) return true; // overflow happened - pos += c; - if (pos < c) return true; // overflow happened - if (pos > size) return true; - return false; -} - bool SumOverflows(size_t a, size_t b, size_t c) { size_t sum = a + b; if (sum < b) return true; @@ -165,8 +157,7 @@ enum class DecoderStage : uint32_t { }; enum class FrameStage : uint32_t { - kHeader, // Must parse frame header. dec->frame_start must be set up - // correctly already. + kHeader, // Must parse frame header. kTOC, // Must parse TOC kFull, // Must parse full pixels kFullOutput, // Must output full pixels @@ -189,105 +180,6 @@ enum class JpegReconStage : uint32_t { kFinished, // JPEG reconstruction fully handled }; -// Manages the sections for the FrameDecoder based on input bytes received. -struct Sections { - // sections_begin = position in the frame where the sections begin, after - // the frame header and TOC, so sections_begin = sum of frame header size and - // TOC size. - Sections(jxl::FrameDecoder* frame_dec, size_t frame_size, - size_t sections_begin) - : frame_dec_(frame_dec), - frame_size_(frame_size), - sections_begin_(sections_begin) {} - - Sections(const Sections&) = delete; - Sections& operator=(const Sections&) = delete; - Sections(Sections&&) = delete; - Sections& operator=(Sections&&) = delete; - - ~Sections() { - // Avoid memory leaks if the JXL decoder quits early and doesn't end up - // calling CloseInput(). - CloseInput(); - } - - // frame_dec_ must have been Inited already, but not yet done ProcessSections. - JxlDecoderStatus Init() { - section_received.resize(frame_dec_->NumSections(), 0); - - const auto& offsets = frame_dec_->SectionOffsets(); - const auto& sizes = frame_dec_->SectionSizes(); - - // Ensure none of the sums of section offset and size overflow. - for (size_t i = 0; i < frame_dec_->NumSections(); i++) { - if (OutOfBounds(sections_begin_, offsets[i], sizes[i], frame_size_)) { - return JXL_API_ERROR("section out of bounds"); - } - } - - return JXL_DEC_SUCCESS; - } - - // Sets the input data for the frame. The frame pointer must point to the - // beginning of the frame, size is the amount of bytes gotten so far and - // should increase with next calls until the full frame is loaded. - // TODO(lode): allow caller to provide only later chunks of memory when - // earlier sections are fully processed already. - void SetInput(const uint8_t* frame, size_t size) { - const auto& offsets = frame_dec_->SectionOffsets(); - const auto& sizes = frame_dec_->SectionSizes(); - - for (size_t i = 0; i < frame_dec_->NumSections(); i++) { - if (section_received[i]) continue; - if (!OutOfBounds(sections_begin_, offsets[i], sizes[i], size)) { - section_received[i] = 1; - section_info.emplace_back(jxl::FrameDecoder::SectionInfo{nullptr, i}); - section_status.emplace_back(); - } - } - // Reset all the bitreaders, because the address of the frame pointer may - // change, even if it always represents the same frame start. - for (size_t i = 0; i < section_info.size(); i++) { - size_t id = section_info[i].id; - JXL_ASSERT(section_info[i].br == nullptr); - section_info[i].br = new jxl::BitReader(jxl::Span<const uint8_t>( - frame + sections_begin_ + offsets[id], sizes[id])); - } - } - - JxlDecoderStatus CloseInput() { - bool out_of_bounds = false; - for (size_t i = 0; i < section_info.size(); i++) { - if (!section_info[i].br) continue; - if (!section_info[i].br->AllReadsWithinBounds()) { - // Mark out of bounds section, but keep closing and deleting the next - // ones as well. - out_of_bounds = true; - } - JXL_ASSERT(section_info[i].br->Close()); - delete section_info[i].br; - section_info[i].br = nullptr; - } - if (out_of_bounds) { - // If any bit reader indicates out of bounds, it's an error, not just - // needing more input, since we ensure only bit readers containing - // a complete section are provided to the FrameDecoder. - return JXL_API_ERROR("frame out of bounds"); - } - return JXL_DEC_SUCCESS; - } - - // Not managed by us. - jxl::FrameDecoder* frame_dec_; - - size_t frame_size_; - size_t sections_begin_; - - std::vector<jxl::FrameDecoder::SectionInfo> section_info; - std::vector<jxl::FrameDecoder::SectionStatus> section_status; - std::vector<char> section_received; -}; - /* Given list of frame references to storage slots, and storage slots in which this frame is saved, computes which frames are required to decode the frame at the @@ -374,6 +266,50 @@ struct ExtraChannelOutput { } // namespace +namespace jxl { + +typedef struct JxlDecoderFrameIndexBoxEntryStruct { + // OFFi: offset of start byte of this frame compared to start + // byte of previous frame from this index in the JPEG XL codestream. For the + // first frame, this is the offset from the first byte of the JPEG XL + // codestream. + uint64_t OFFi; + // Ti: duration in ticks between the start of this frame and + // the start of the next frame in the index. If this is the last frame in the + // index, this is the duration in ticks between the start of this frame and + // the end of the stream. A tick lasts TNUM / TDEN seconds. + uint32_t Ti; + // Fi: amount of frames the next frame in the index occurs + // after this frame. If this is the last frame in the index, this is the + // amount of frames after this frame in the remainder of the stream. Only + // frames that are presented by the decoder are counted for this purpose, this + // excludes frames that are not intended for display but for compositing with + // other frames, such as frames that aren't the last frame with a duration of + // 0 ticks. + uint32_t Fi; +} JxlDecoderFrameIndexBoxEntry; + +typedef struct JxlDecoderFrameIndexBoxStruct { + int64_t NF() const { return entries.size(); } + int32_t TNUM = 1; + int32_t TDEN = 1000; + + std::vector<JxlDecoderFrameIndexBoxEntry> entries; + + // That way we can ensure that every index box will have the first frame. + // If the API user decides to mark it as an indexed frame, we call + // the AddFrame again, this time with requested. + void AddFrame(uint64_t OFFi, uint32_t Ti, uint32_t Fi) { + JxlDecoderFrameIndexBoxEntry e; + e.OFFi = OFFi; + e.Ti = Ti; + e.Fi = Fi; + entries.push_back(e); + } +} JxlDecoderFrameIndexBox; + +} // namespace jxl + // NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) struct JxlDecoderStruct { JxlDecoderStruct() = default; @@ -385,22 +321,23 @@ struct JxlDecoderStruct { // Status of progression, internal. bool got_signature; - bool first_codestream_seen; // Indicates we know that we've seen the last codestream box: either this // was a jxlc box, or a jxlp box that has its index indicated as last by // having its most significant bit set, or no boxes are used at all. This // does not indicate the full codestream has already been seen, only the // last box of it has been initiated. bool last_codestream_seen; + bool got_codestream_signature; bool got_basic_info; - size_t header_except_icc_bits = 0; // To skip everything before ICC. + bool got_transform_data; // To skip everything before ICC. bool got_all_headers; // Codestream metadata headers. bool post_headers; // Already decoding pixels. jxl::ICCReader icc_reader; - + jxl::JxlDecoderFrameIndexBox frame_index_box; // This means either we actually got the preview image, or determined we // cannot get it or there is none. bool got_preview_image; + bool preview_frame; // Position of next_in in the original file including box format if present // (as opposed to position in the codestream) @@ -410,6 +347,7 @@ struct JxlDecoderStruct { size_t box_contents_end; size_t box_contents_size; size_t box_size; + size_t header_size; // Either a final box that runs until EOF, or the case of no container format // at all. bool box_contents_unbounded; @@ -436,8 +374,10 @@ struct JxlDecoderStruct { // Settings bool keep_orientation; + bool unpremul_alpha; bool render_spotcolors; bool coalescing; + float desired_intensity_target; // Bitfield, for which informative events (JXL_DEC_BASIC_INFO, etc...) the // decoder returns a status. By default, do not return for any of the events, @@ -451,11 +391,19 @@ struct JxlDecoderStruct { bool have_container; size_t box_count; + // The level of progressive detail in frame decoding. + JxlProgressiveDetail prog_detail = kDC; + // The progressive detail of the current frame. + JxlProgressiveDetail frame_prog_detail; + // The intended downsampling ratio for the current progression step. + size_t downsampling_target; + // Whether the preview out buffer was set. It is possible for the buffer to // be nullptr and buffer_set to be true, indicating it was deliberately // set to nullptr. bool preview_out_buffer_set; // Idem for the image buffer. + // Set to true if either an image out buffer or an image out callback was set. bool image_out_buffer_set; // Owned by the caller, buffers for DC image and full resolution images @@ -482,13 +430,15 @@ struct JxlDecoderStruct { std::vector<ExtraChannelOutput> extra_channel_output; jxl::CodecMetadata metadata; + // Same as metadata.m, except for the color_encoding, which is set to the + // output encoding. + jxl::ImageMetadata image_metadata; std::unique_ptr<jxl::ImageBundle> ib; - // ColorEncoding to use for xyb encoded image with ICC profile. - jxl::ColorEncoding default_enc; std::unique_ptr<jxl::PassesDecoderState> passes_state; std::unique_ptr<jxl::FrameDecoder> frame_dec; - std::unique_ptr<Sections> sections; + size_t next_section; + std::vector<char> section_processed; // The FrameDecoder is initialized, and not yet finalized bool frame_dec_in_progress; @@ -497,11 +447,9 @@ struct JxlDecoderStruct { // that is, the displayed frame. std::unique_ptr<jxl::FrameHeader> frame_header; - // Start of the current frame being processed, as offset from the beginning of - // the codestream. - size_t frame_start; - size_t frame_size; + size_t remaining_frame_size; FrameStage frame_stage; + bool dc_frame_progression_done; // The currently processed frame is the last of the current composite still, // and so must be returned as pixels bool is_last_of_still; @@ -537,17 +485,21 @@ struct JxlDecoderStruct { // vector, it must be treated as a required frame. std::vector<char> frame_required; - // Codestream input data is stored here, when the decoder takes in and stores - // the user input bytes. If the decoder does not do that (e.g. in one-shot - // case), this field is unused. - // TODO(lode): avoid needing this field once the C++ decoder doesn't need - // all bytes at once, to save memory. Find alternative to std::vector doubling - // strategy to prevent some memory usage. + // Codestream input data is copied here temporarily when the decoder needs + // more input bytes to process the next part of the stream. We copy the input + // data in order to be able to release it all through the API it when + // returning JXL_DEC_NEED_MORE_INPUT. std::vector<uint8_t> codestream_copy; - // Position in the actual codestream, which codestream_copy.begin() points to. - // Non-zero once earlier parts of the codestream vector have been erased. - // TODO(lode): use this variable to allow pruning codestream_copy + // Number of bytes at the end of codestream_copy that were not yet consumed + // by calling AdvanceInput(). + size_t codestream_unconsumed; + // Position in the codestream_copy vector that the decoder already finished + // processing. It can be greater than the current size of codestream_copy in + // case where the decoder skips some parts of the frame that were not yet + // provided. size_t codestream_pos; + // Number of bits after codestream_pos that were already processed. + size_t codestream_bits_ahead; BoxStage box_stage; @@ -582,11 +534,87 @@ struct JxlDecoderStruct { bool input_closed; void AdvanceInput(size_t size) { + JXL_DASSERT(avail_in >= size); next_in += size; avail_in -= size; file_pos += size; } + size_t AvailableCodestream() const { + size_t avail_codestream = avail_in; + if (!box_contents_unbounded) { + avail_codestream = + std::min<size_t>(avail_codestream, box_contents_end - file_pos); + } + return avail_codestream; + } + + void AdvanceCodestream(size_t size) { + size_t avail_codestream = AvailableCodestream(); + if (codestream_copy.empty()) { + if (size <= avail_codestream) { + AdvanceInput(size); + } else { + codestream_pos = size - avail_codestream; + AdvanceInput(avail_codestream); + } + } else { + codestream_pos += size; + if (codestream_pos + codestream_unconsumed >= codestream_copy.size()) { + size_t advance = std::min( + codestream_unconsumed, + codestream_unconsumed + codestream_pos - codestream_copy.size()); + AdvanceInput(advance); + codestream_pos -= std::min(codestream_pos, codestream_copy.size()); + codestream_unconsumed = 0; + codestream_copy.clear(); + } + } + } + + JxlDecoderStatus RequestMoreInput() { + if (codestream_copy.empty()) { + size_t avail_codestream = AvailableCodestream(); + codestream_copy.insert(codestream_copy.end(), next_in, + next_in + avail_codestream); + AdvanceInput(avail_codestream); + } else { + AdvanceInput(codestream_unconsumed); + codestream_unconsumed = 0; + } + return JXL_DEC_NEED_MORE_INPUT; + } + + JxlDecoderStatus GetCodestreamInput(jxl::Span<const uint8_t>* span) { + if (codestream_copy.empty() && codestream_pos > 0) { + size_t avail_codestream = AvailableCodestream(); + size_t skip = std::min<size_t>(codestream_pos, avail_codestream); + AdvanceInput(skip); + codestream_pos -= skip; + if (codestream_pos > 0) { + return RequestMoreInput(); + } + } + JXL_ASSERT(codestream_pos <= codestream_copy.size()); + JXL_ASSERT(codestream_unconsumed <= codestream_copy.size()); + size_t avail_codestream = AvailableCodestream(); + if (codestream_copy.empty()) { + if (avail_codestream == 0) { + return RequestMoreInput(); + } + *span = jxl::Span<const uint8_t>(next_in, avail_codestream); + return JXL_DEC_SUCCESS; + } else { + codestream_copy.insert(codestream_copy.end(), + next_in + codestream_unconsumed, + next_in + avail_codestream); + codestream_unconsumed = avail_codestream; + *span = jxl::Span<const uint8_t>(codestream_copy.data() + codestream_pos, + codestream_copy.size() - codestream_pos); + return JXL_DEC_SUCCESS; + } + } + // Whether the decoder can use more codestream input for a purpose it needs. // This returns false if the user didn't subscribe to any events that // require the codestream (e.g. only subscribed to metadata boxes), or all @@ -637,19 +665,21 @@ JxlDecoderStatus JxlDecoderDefaultPixelFormat(const JxlDecoder* dec, void JxlDecoderRewindDecodingState(JxlDecoder* dec) { dec->stage = DecoderStage::kInited; dec->got_signature = false; - dec->first_codestream_seen = false; dec->last_codestream_seen = false; + dec->got_codestream_signature = false; dec->got_basic_info = false; - dec->header_except_icc_bits = 0; + dec->got_transform_data = false; dec->got_all_headers = false; dec->post_headers = false; dec->icc_reader.Reset(); dec->got_preview_image = false; + dec->preview_frame = false; dec->file_pos = 0; dec->box_contents_begin = 0; dec->box_contents_end = 0; dec->box_contents_size = 0; dec->box_size = 0; + dec->header_size = 0; dec->box_contents_unbounded = false; memset(dec->box_type, 0, sizeof(dec->box_type)); memset(dec->box_decoded_type, 0, sizeof(dec->box_decoded_type)); @@ -674,6 +704,7 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) { dec->basic_info_size_hint = InitialBasicInfoSizeHint(); dec->have_container = 0; dec->box_count = 0; + dec->downsampling_target = 8; dec->preview_out_buffer_set = false; dec->image_out_buffer_set = false; dec->preview_out_buffer = nullptr; @@ -692,18 +723,22 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) { dec->passes_state.reset(nullptr); dec->frame_dec.reset(nullptr); - dec->sections.reset(nullptr); + dec->next_section = 0; + dec->section_processed.clear(); dec->frame_dec_in_progress = false; dec->ib.reset(); dec->metadata = jxl::CodecMetadata(); + dec->image_metadata = dec->metadata.m; dec->frame_header.reset(new jxl::FrameHeader(&dec->metadata)); dec->codestream_copy.clear(); + dec->codestream_unconsumed = 0; + dec->codestream_pos = 0; + dec->codestream_bits_ahead = 0; dec->frame_stage = FrameStage::kHeader; - dec->frame_start = 0; - dec->frame_size = 0; + dec->remaining_frame_size = 0; dec->is_last_of_still = false; dec->is_last_total = false; dec->skip_frames = 0; @@ -717,8 +752,10 @@ void JxlDecoderReset(JxlDecoder* dec) { dec->thread_pool.reset(); dec->keep_orientation = false; + dec->unpremul_alpha = false; dec->render_spotcolors = true; dec->coalescing = true; + dec->desired_intensity_target = 0; dec->orig_events_wanted = 0; dec->frame_references.clear(); dec->frame_saved_as.clear(); @@ -794,6 +831,19 @@ void JxlDecoderSkipFrames(JxlDecoder* dec, size_t amount) { } } +JxlDecoderStatus JxlDecoderSkipCurrentFrame(JxlDecoder* dec) { + if (!dec->frame_dec || !dec->frame_dec_in_progress) { + return JXL_DEC_ERROR; + } + dec->frame_stage = FrameStage::kHeader; + dec->AdvanceCodestream(dec->remaining_frame_size); + dec->frame_dec_in_progress = false; + if (dec->is_last_of_still) { + dec->image_out_buffer_set = false; + } + return JXL_DEC_SUCCESS; +} + JXL_EXPORT JxlDecoderStatus JxlDecoderSetParallelRunner(JxlDecoder* dec, JxlParallelRunner parallel_runner, void* parallel_runner_opaque) { @@ -831,6 +881,15 @@ JxlDecoderStatus JxlDecoderSetKeepOrientation(JxlDecoder* dec, return JXL_DEC_SUCCESS; } +JxlDecoderStatus JxlDecoderSetUnpremultiplyAlpha(JxlDecoder* dec, + JXL_BOOL unpremul_alpha) { + if (dec->stage != DecoderStage::kInited) { + return JXL_API_ERROR("Must set unpremul_alpha option before starting"); + } + dec->unpremul_alpha = !!unpremul_alpha; + return JXL_DEC_SUCCESS; +} + JxlDecoderStatus JxlDecoderSetRenderSpotcolors(JxlDecoder* dec, JXL_BOOL render_spotcolors) { if (dec->stage != DecoderStage::kInited) { @@ -887,10 +946,10 @@ bool CanRead(Span<const uint8_t> data, BitReader* reader, T* JXL_RESTRICT t) { // Returns JXL_DEC_SUCCESS if the full bundle was successfully read, status // indicating either error or need more input otherwise. template <class T> -JxlDecoderStatus ReadBundle(Span<const uint8_t> data, BitReader* reader, - T* JXL_RESTRICT t) { +JxlDecoderStatus ReadBundle(JxlDecoder* dec, Span<const uint8_t> data, + BitReader* reader, T* JXL_RESTRICT t) { if (!CanRead(data, reader, t)) { - return JXL_DEC_NEED_MORE_INPUT; + return dec->RequestMoreInput(); } if (!Bundle::Read(reader, t)) { return JXL_DEC_ERROR; @@ -919,33 +978,35 @@ std::unique_ptr<BitReader, std::function<void(BitReader*)>> GetBitReader( }); } -JxlDecoderStatus JxlDecoderReadBasicInfo(JxlDecoder* dec, const uint8_t* in, - size_t size) { - size_t pos = 0; - - // Check and skip the codestream signature - JxlSignature signature = ReadSignature(in, size, &pos); - if (signature == JXL_SIG_NOT_ENOUGH_BYTES) { - return JXL_DEC_NEED_MORE_INPUT; - } - if (signature == JXL_SIG_CONTAINER) { - // There is a container signature where we expect a codestream, container - // is handled at a higher level already. - return JXL_API_ERROR("invalid: nested container"); - } - if (signature != JXL_SIG_CODESTREAM) { - return JXL_API_ERROR("invalid signature"); +JxlDecoderStatus JxlDecoderReadBasicInfo(JxlDecoder* dec) { + if (!dec->got_codestream_signature) { + // Check and skip the codestream signature + Span<const uint8_t> span; + JXL_API_RETURN_IF_ERROR(dec->GetCodestreamInput(&span)); + if (span.size() < 2) { + return dec->RequestMoreInput(); + } + if (span.data()[0] != 0xff || span.data()[1] != jxl::kCodestreamMarker) { + return JXL_API_ERROR("invalid signature"); + } + dec->got_codestream_signature = true; + dec->AdvanceCodestream(2); } - Span<const uint8_t> span(in + pos, size - pos); + Span<const uint8_t> span; + JXL_API_RETURN_IF_ERROR(dec->GetCodestreamInput(&span)); auto reader = GetBitReader(span); - JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dec->metadata.size)); - - dec->metadata.m.nonserialized_only_parse_basic_info = true; - JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dec->metadata.m)); - dec->metadata.m.nonserialized_only_parse_basic_info = false; + JXL_API_RETURN_IF_ERROR( + ReadBundle(dec, span, reader.get(), &dec->metadata.size)); + JXL_API_RETURN_IF_ERROR( + ReadBundle(dec, span, reader.get(), &dec->metadata.m)); + size_t total_bits = reader->TotalBitsConsumed(); + dec->AdvanceCodestream(total_bits / jxl::kBitsPerByte); + dec->codestream_bits_ahead = total_bits % jxl::kBitsPerByte; dec->got_basic_info = true; dec->basic_info_size_hint = 0; + dec->image_metadata = dec->metadata.m; + JXL_DEBUG_V(2, "Decoded BasicInfo: %s", dec->metadata.DebugString().c_str()); if (!CheckSizeLimit(dec, dec->metadata.size.xsize(), dec->metadata.size.ysize())) { @@ -956,41 +1017,26 @@ JxlDecoderStatus JxlDecoderReadBasicInfo(JxlDecoder* dec, const uint8_t* in, } // Reads all codestream headers (but not frame headers) -JxlDecoderStatus JxlDecoderReadAllHeaders(JxlDecoder* dec, const uint8_t* in, - size_t size) { - size_t pos = 0; - - // Check and skip the codestream signature - JxlSignature signature = ReadSignature(in, size, &pos); - if (signature == JXL_SIG_CONTAINER) { - return JXL_API_ERROR("invalid: nested container"); - } - if (signature != JXL_SIG_CODESTREAM) { - return JXL_API_ERROR("invalid signature"); - } - - Span<const uint8_t> span(in + pos, size - pos); - auto reader = GetBitReader(span); - - if (dec->header_except_icc_bits != 0) { - // Headers were decoded already. - reader->SkipBits(dec->header_except_icc_bits); - } else { - SizeHeader dummy_size_header; - JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dummy_size_header)); - - // We already decoded the metadata to dec->metadata.m, no reason to - // overwrite it, use a dummy metadata instead. - ImageMetadata dummy_metadata; - JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dummy_metadata)); - +JxlDecoderStatus JxlDecoderReadAllHeaders(JxlDecoder* dec) { + if (!dec->got_transform_data) { + Span<const uint8_t> span; + JXL_API_RETURN_IF_ERROR(dec->GetCodestreamInput(&span)); + auto reader = GetBitReader(span); + reader->SkipBits(dec->codestream_bits_ahead); dec->metadata.transform_data.nonserialized_xyb_encoded = dec->metadata.m.xyb_encoded; JXL_API_RETURN_IF_ERROR( - ReadBundle(span, reader.get(), &dec->metadata.transform_data)); + ReadBundle(dec, span, reader.get(), &dec->metadata.transform_data)); + size_t total_bits = reader->TotalBitsConsumed(); + dec->AdvanceCodestream(total_bits / jxl::kBitsPerByte); + dec->codestream_bits_ahead = total_bits % jxl::kBitsPerByte; + dec->got_transform_data = true; } - dec->header_except_icc_bits = reader->TotalBitsConsumed(); + Span<const uint8_t> span; + JXL_API_RETURN_IF_ERROR(dec->GetCodestreamInput(&span)); + auto reader = GetBitReader(span); + reader->SkipBits(dec->codestream_bits_ahead); if (dec->metadata.m.color_encoding.WantICC()) { jxl::Status status = @@ -999,22 +1045,20 @@ JxlDecoderStatus JxlDecoderReadAllHeaders(JxlDecoder* dec, const uint8_t* in, // handles reader out of bounds correctly yet (e.g. context map). Not // checking AllReadsWithinBounds can cause reader->Close() to trigger an // assert, but we don't want library to quit program for invalid codestream. - if (!reader->AllReadsWithinBounds()) { - return JXL_DEC_NEED_MORE_INPUT; + if (!reader->AllReadsWithinBounds() || + status.code() == StatusCode::kNotEnoughBytes) { + return dec->RequestMoreInput(); } if (!status) { - if (status.code() == StatusCode::kNotEnoughBytes) { - return JXL_DEC_NEED_MORE_INPUT; - } // Other non-successful status is an error return JXL_DEC_ERROR; } PaddedBytes icc; status = dec->icc_reader.Process(reader.get(), &icc); + if (status.code() == StatusCode::kNotEnoughBytes) { + return dec->RequestMoreInput(); + } if (!status) { - if (status.code() == StatusCode::kNotEnoughBytes) { - return JXL_DEC_NEED_MORE_INPUT; - } // Other non-successful status is an error return JXL_DEC_ERROR; } @@ -1026,17 +1070,20 @@ JxlDecoderStatus JxlDecoderReadAllHeaders(JxlDecoder* dec, const uint8_t* in, dec->got_all_headers = true; JXL_API_RETURN_IF_ERROR(reader->JumpToByteBoundary()); - dec->frame_start = pos + reader->TotalBitsConsumed() / jxl::kBitsPerByte; + dec->AdvanceCodestream(reader->TotalBitsConsumed() / jxl::kBitsPerByte); + dec->codestream_bits_ahead = 0; if (!dec->passes_state) { dec->passes_state.reset(new jxl::PassesDecoderState()); } - dec->default_enc = - ColorEncoding::LinearSRGB(dec->metadata.m.color_encoding.IsGray()); - - JXL_API_RETURN_IF_ERROR(dec->passes_state->output_encoding_info.Set( - dec->metadata, dec->default_enc)); + JXL_API_RETURN_IF_ERROR( + dec->passes_state->output_encoding_info.SetFromMetadata(dec->metadata)); + if (dec->desired_intensity_target > 0) { + dec->passes_state->output_encoding_info.desired_intensity_target = + dec->desired_intensity_target; + } + dec->image_metadata = dec->metadata.m; return JXL_DEC_SUCCESS; } @@ -1086,85 +1133,79 @@ static JxlDecoderStatus ConvertImageInternal( status = jxl::ConvertToExternal( frame, BitsPerChannel(format.data_type), float_format, format.num_channels, format.endianness, stride, dec->thread_pool.get(), - out_image, out_size, out_callback, undo_orientation); + out_image, out_size, out_callback, undo_orientation, + dec->unpremul_alpha); } return status ? JXL_DEC_SUCCESS : JXL_DEC_ERROR; } -// Parses the FrameHeader and the total frame_size, given the initial bytes -// of the frame up to and including the TOC. -// TODO(lode): merge this with FrameDecoder -JxlDecoderStatus ParseFrameHeader(JxlDecoder* dec, - jxl::FrameHeader* frame_header, - const uint8_t* in, size_t size, size_t pos, - bool is_preview, size_t* frame_size, - int* saved_as) { - if (pos >= size) { - return JXL_DEC_NEED_MORE_INPUT; - } - Span<const uint8_t> span(in + pos, size - pos); - auto reader = GetBitReader(span); - - frame_header->nonserialized_is_preview = is_preview; - jxl::Status status = DecodeFrameHeader(reader.get(), frame_header); - jxl::FrameDimensions frame_dim = frame_header->ToFrameDimensions(); - if (!CheckSizeLimit(dec, frame_dim.xsize_upsampled_padded, - frame_dim.ysize_upsampled_padded)) { - return JXL_API_ERROR("frame is too large"); - } - - if (status.code() == StatusCode::kNotEnoughBytes) { - // TODO(lode): prevent asking for way too much input bytes in case of - // invalid header that the decoder thinks is a very long user extension - // instead. Example: fields can currently print something like this: - // "../lib/jxl/fields.cc:416: Skipping 71467322-bit extension(s)" - // Maybe fields.cc should return error in the above case rather than - // print a message. - return JXL_DEC_NEED_MORE_INPUT; - } else if (!status) { - return JXL_API_ERROR("invalid frame header"); - } - - // Read TOC. - uint64_t groups_total_size; - const bool has_ac_global = true; - const size_t toc_entries = - NumTocEntries(frame_dim.num_groups, frame_dim.num_dc_groups, - frame_header->passes.num_passes, has_ac_global); - - std::vector<uint64_t> group_offsets; - std::vector<uint32_t> group_sizes; - status = ReadGroupOffsets(toc_entries, reader.get(), &group_offsets, - &group_sizes, &groups_total_size); - - // TODO(lode): we're actually relying on AllReadsWithinBounds() here - // instead of on status.code(), change the internal TOC C++ code to - // correctly set the status.code() instead so we can rely on that one. - if (!reader->AllReadsWithinBounds() || - status.code() == StatusCode::kNotEnoughBytes) { - return JXL_DEC_NEED_MORE_INPUT; - } else if (!status) { - return JXL_API_ERROR("invalid toc entries"); - } - - JXL_DASSERT((reader->TotalBitsConsumed() % kBitsPerByte) == 0); - JXL_API_RETURN_IF_ERROR(reader->JumpToByteBoundary()); - size_t header_size = (reader->TotalBitsConsumed() >> 3); - *frame_size = header_size + groups_total_size; - - if (saved_as != nullptr) { - *saved_as = FrameDecoder::SavedAs(*frame_header); +JxlDecoderStatus JxlDecoderProcessSections(JxlDecoder* dec) { + Span<const uint8_t> span; + JXL_API_RETURN_IF_ERROR(dec->GetCodestreamInput(&span)); + const auto& toc = dec->frame_dec->Toc(); + size_t pos = 0; + std::vector<jxl::FrameDecoder::SectionInfo> section_info; + std::vector<jxl::FrameDecoder::SectionStatus> section_status; + for (size_t i = dec->next_section; i < toc.size(); ++i) { + if (dec->section_processed[i]) continue; + size_t id = toc[i].id; + size_t size = toc[i].size; + if (OutOfBounds(pos, size, span.size())) { + break; + } + auto br = + new jxl::BitReader(jxl::Span<const uint8_t>(span.data() + pos, size)); + section_info.emplace_back(jxl::FrameDecoder::SectionInfo{br, id}); + section_status.emplace_back(); + pos += size; + } + jxl::Status status = dec->frame_dec->ProcessSections( + section_info.data(), section_info.size(), section_status.data()); + bool out_of_bounds = false; + for (const auto& info : section_info) { + if (!info.br->AllReadsWithinBounds()) { + // Mark out of bounds section, but keep closing and deleting the next + // ones as well. + out_of_bounds = true; + } + JXL_ASSERT(info.br->Close()); + delete info.br; + } + if (out_of_bounds) { + // If any bit reader indicates out of bounds, it's an error, not just + // needing more input, since we ensure only bit readers containing + // a complete section are provided to the FrameDecoder. + return JXL_API_ERROR("frame out of bounds"); + } + if (!status) { + return JXL_API_ERROR("frame processing failed"); + } + bool found_skipped_section = false; + size_t num_done = 0; + size_t processed_bytes = 0; + for (size_t i = 0; i < section_status.size(); ++i) { + auto status = section_status[i]; + if (status == jxl::FrameDecoder::kDone) { + if (!found_skipped_section) { + processed_bytes += toc[dec->next_section + i].size; + ++num_done; + } + dec->section_processed[dec->next_section + i] = 1; + } else if (status == jxl::FrameDecoder::kSkipped) { + found_skipped_section = true; + } else { + return JXL_API_ERROR("unexpected section status"); + } } - + dec->next_section += num_done; + dec->remaining_frame_size -= processed_bytes; + dec->AdvanceCodestream(processed_bytes); return JXL_DEC_SUCCESS; } // TODO(eustas): no CodecInOut -> no image size reinforcement -> possible OOM. -// TODO(lode): allow this function to indicate bytes of in that have already -// been processed and no longer need to be stored in codestream_copy. -JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, - size_t size) { +JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec) { // If no parallel runner is set, use the default // TODO(lode): move this initialization to an appropriate location once the // runner is used to decode pixels. @@ -1174,7 +1215,7 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, // No matter what events are wanted, the basic info is always required. if (!dec->got_basic_info) { - JxlDecoderStatus status = JxlDecoderReadBasicInfo(dec, in, size); + JxlDecoderStatus status = JxlDecoderReadBasicInfo(dec); if (status != JXL_DEC_SUCCESS) return status; } @@ -1183,16 +1224,14 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, return JXL_DEC_BASIC_INFO; } - if (!dec->got_all_headers) { - JxlDecoderStatus status = JxlDecoderReadAllHeaders(dec, in, size); - if (status != JXL_DEC_SUCCESS) return status; + if (!dec->events_wanted) { + dec->stage = DecoderStage::kCodestreamFinished; + return JXL_DEC_SUCCESS; } - if (dec->events_wanted & JXL_DEC_EXTENSIONS) { - dec->events_wanted &= ~JXL_DEC_EXTENSIONS; - if (dec->metadata.m.extensions != 0) { - return JXL_DEC_EXTENSIONS; - } + if (!dec->got_all_headers) { + JxlDecoderStatus status = JxlDecoderReadAllHeaders(dec); + if (status != JXL_DEC_SUCCESS) return status; } if (dec->events_wanted & JXL_DEC_COLOR_ENCODING) { @@ -1200,85 +1239,28 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, return JXL_DEC_COLOR_ENCODING; } - dec->post_headers = true; - - // Decode to pixels, only if required for the events the user wants. - if (!dec->got_preview_image) { - // Parse the preview, or at least its TOC to be able to skip the frame, if - // any frame or image decoding is desired. - bool parse_preview = - (dec->events_wanted & - (JXL_DEC_PREVIEW_IMAGE | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); - - if (!dec->metadata.m.have_preview) { - // There is no preview, mark this as done and go to next step - dec->got_preview_image = true; - } else if (!parse_preview) { - // No preview parsing needed, mark this step as done - dec->got_preview_image = true; - } else { - // Want to decode the preview, not just skip the frame - bool want_preview = (dec->events_wanted & JXL_DEC_PREVIEW_IMAGE); - size_t frame_size; - size_t pos = dec->frame_start; - dec->frame_header.reset(new FrameHeader(&dec->metadata)); - JxlDecoderStatus status = ParseFrameHeader( - dec, dec->frame_header.get(), in, size, pos, true, &frame_size, - /*saved_as=*/nullptr); - if (status != JXL_DEC_SUCCESS) return status; - if (OutOfBounds(pos, frame_size, size)) { - return JXL_DEC_NEED_MORE_INPUT; - } - - if (want_preview && !dec->preview_out_buffer_set) { - return JXL_DEC_NEED_PREVIEW_OUT_BUFFER; - } + if (!dec->events_wanted) { + dec->stage = DecoderStage::kCodestreamFinished; + return JXL_DEC_SUCCESS; + } - jxl::Span<const uint8_t> compressed(in + dec->frame_start, - size - dec->frame_start); - auto reader = GetBitReader(compressed); - jxl::DecompressParams dparams; - dparams.render_spotcolors = dec->render_spotcolors; - dparams.coalescing = true; - jxl::ImageBundle ib(&dec->metadata.m); - PassesDecoderState preview_dec_state; - JXL_API_RETURN_IF_ERROR(preview_dec_state.output_encoding_info.Set( - dec->metadata, - ColorEncoding::LinearSRGB(dec->metadata.m.color_encoding.IsGray()))); - if (!DecodeFrame(dparams, &preview_dec_state, dec->thread_pool.get(), - reader.get(), &ib, dec->metadata, - /*constraints=*/nullptr, - /*is_preview=*/true)) { - return JXL_API_ERROR("decoding preview failed"); - } + dec->post_headers = true; - // Set frame_start to the first non-preview frame. - dec->frame_start += DivCeil(reader->TotalBitsConsumed(), kBitsPerByte); - dec->got_preview_image = true; - - if (want_preview) { - if (dec->preview_out_buffer) { - JxlDecoderStatus status = ConvertImageInternal( - dec, ib, dec->preview_out_format, /*want_extra_channel=*/false, - /*extra_channel_index=*/0, dec->preview_out_buffer, - dec->preview_out_size, - /*out_callback=*/{}); - if (status != JXL_DEC_SUCCESS) return status; - } - return JXL_DEC_PREVIEW_IMAGE; - } - } + if (!dec->got_preview_image && dec->metadata.m.have_preview) { + dec->preview_frame = true; } // Handle frames for (;;) { - if (!(dec->events_wanted & (JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME))) { + bool parse_frames = + (dec->events_wanted & + (JXL_DEC_PREVIEW_IMAGE | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + if (!parse_frames) { break; } if (dec->frame_stage == FrameStage::kHeader && dec->is_last_total) { break; } - if (dec->frame_stage == FrameStage::kHeader) { if (dec->recon_output_jpeg == JpegReconStage::kSettingMetadata || dec->recon_output_jpeg == JpegReconStage::kOutputting) { @@ -1289,17 +1271,64 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, return JXL_API_ERROR( "cannot decode a next frame after JPEG reconstruction frame"); } - size_t pos = dec->frame_start - dec->codestream_pos; - if (pos >= size) { - return JXL_DEC_NEED_MORE_INPUT; + if (!dec->ib) { + dec->ib.reset(new jxl::ImageBundle(&dec->image_metadata)); } + // If JPEG reconstruction is wanted and possible, set the jpeg_data of + // the ImageBundle. + if (!dec->jpeg_decoder.SetImageBundleJpegData(dec->ib.get())) + return JXL_DEC_ERROR; + + dec->frame_dec.reset(new FrameDecoder( + dec->passes_state.get(), dec->metadata, dec->thread_pool.get(), + /*use_slow_rendering_pipeline=*/false)); dec->frame_header.reset(new FrameHeader(&dec->metadata)); - int saved_as = 0; - JxlDecoderStatus status = - ParseFrameHeader(dec, dec->frame_header.get(), in, size, pos, - /*is_preview=*/false, &dec->frame_size, &saved_as); - if (status != JXL_DEC_SUCCESS) return status; + Span<const uint8_t> span; + JXL_API_RETURN_IF_ERROR(dec->GetCodestreamInput(&span)); + auto reader = GetBitReader(span); + bool output_needed = + (dec->preview_frame ? (dec->events_wanted & JXL_DEC_PREVIEW_IMAGE) + : (dec->events_wanted & JXL_DEC_FULL_IMAGE)); + jxl::Status status = dec->frame_dec->InitFrame( + reader.get(), dec->ib.get(), dec->preview_frame, output_needed); + if (!reader->AllReadsWithinBounds() || + status.code() == StatusCode::kNotEnoughBytes) { + return dec->RequestMoreInput(); + } else if (!status) { + return JXL_API_ERROR("invalid frame header"); + } + dec->AdvanceCodestream(reader->TotalBitsConsumed() / kBitsPerByte); + *dec->frame_header = dec->frame_dec->GetFrameHeader(); + jxl::FrameDimensions frame_dim = dec->frame_header->ToFrameDimensions(); + if (!CheckSizeLimit(dec, frame_dim.xsize_upsampled_padded, + frame_dim.ysize_upsampled_padded)) { + return JXL_API_ERROR("frame is too large"); + } + if (dec->cpu_limit_base != 0) { + // No overflow, checked in CheckSizeLimit. + size_t num_pixels = frame_dim.xsize * frame_dim.ysize; + if (dec->used_cpu_base + num_pixels < dec->used_cpu_base) { + return JXL_API_ERROR("used too much CPU"); + } + dec->used_cpu_base += num_pixels; + if (dec->used_cpu_base > dec->cpu_limit_base) { + return JXL_API_ERROR("used too much CPU"); + } + } + dec->remaining_frame_size = dec->frame_dec->SumSectionSizes(); + dec->frame_stage = FrameStage::kTOC; + if (dec->preview_frame) { + if (!(dec->events_wanted & JXL_DEC_PREVIEW_IMAGE)) { + dec->frame_stage = FrameStage::kHeader; + dec->AdvanceCodestream(dec->remaining_frame_size); + dec->got_preview_image = true; + dec->preview_frame = false; + } + continue; + } + + int saved_as = FrameDecoder::SavedAs(*dec->frame_header); // is last in entire codestream dec->is_last_total = dec->frame_header->is_last; // is last of current still @@ -1309,14 +1338,11 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, dec->is_last_of_still |= (!dec->coalescing && dec->frame_header->frame_type == FrameType::kRegularFrame); - const size_t internal_frame_index = dec->internal_frames; const size_t external_frame_index = dec->external_frames; if (dec->is_last_of_still) dec->external_frames++; dec->internal_frames++; - dec->frame_stage = FrameStage::kTOC; - if (dec->skip_frames > 0) { dec->skipping_frame = true; if (dec->is_last_of_still) { @@ -1359,7 +1385,7 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, // Skip all decoding for this frame, since the user is skipping this // frame and no future frames can reference it. dec->frame_stage = FrameStage::kHeader; - dec->frame_start += dec->frame_size; + dec->AdvanceCodestream(dec->remaining_frame_size); continue; } } @@ -1375,61 +1401,44 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, } if (dec->frame_stage == FrameStage::kTOC) { - size_t pos = dec->frame_start - dec->codestream_pos; - if (pos >= size) { - return JXL_DEC_NEED_MORE_INPUT; - } - Span<const uint8_t> span(in + pos, size - pos); - auto reader = GetBitReader(span); - - if (!dec->passes_state) { - dec->passes_state.reset(new jxl::PassesDecoderState()); - } - if (!dec->ib) { - dec->ib.reset(new jxl::ImageBundle(&dec->metadata.m)); - } - - dec->frame_dec.reset(new FrameDecoder( - dec->passes_state.get(), dec->metadata, dec->thread_pool.get(), - /*use_slow_rendering_pipeline=*/false)); dec->frame_dec->SetRenderSpotcolors(dec->render_spotcolors); dec->frame_dec->SetCoalescing(dec->coalescing); - if (dec->events_wanted & JXL_DEC_FRAME_PROGRESSION) { - dec->frame_dec->SetPauseAtProgressive(); - } - // If JPEG reconstruction is wanted and possible, set the jpeg_data of - // the ImageBundle. - if (!dec->jpeg_decoder.SetImageBundleJpegData(dec->ib.get())) - return JXL_DEC_ERROR; - - jxl::Status status = dec->frame_dec->InitFrame( - reader.get(), dec->ib.get(), /*is_preview=*/false, - /*allow_partial_frames=*/true, /*allow_partial_dc_global=*/false, - /*output_needed=*/dec->events_wanted & JXL_DEC_FULL_IMAGE); - if (!status) JXL_API_RETURN_IF_ERROR(status); - - size_t sections_begin = - DivCeil(reader->TotalBitsConsumed(), kBitsPerByte); + if (!dec->preview_frame && + (dec->events_wanted & JXL_DEC_FRAME_PROGRESSION)) { + dec->frame_prog_detail = + dec->frame_dec->SetPauseAtProgressive(dec->prog_detail); + } else { + dec->frame_prog_detail = JxlProgressiveDetail::kFrames; + } + dec->dc_frame_progression_done = 0; - dec->sections.reset( - new Sections(dec->frame_dec.get(), dec->frame_size, sections_begin)); - JXL_API_RETURN_IF_ERROR(dec->sections->Init()); + dec->next_section = 0; + dec->section_processed.clear(); + dec->section_processed.resize(dec->frame_dec->Toc().size(), 0); // If we don't need pixels, we can skip actually decoding the frames - // (kFull / kFullOut). By not updating frame_stage, none of - // these stages will execute, and the loop will continue from the next - // frame. - if (dec->events_wanted & JXL_DEC_FULL_IMAGE) { + // (kFull / kFullOut). + if (dec->preview_frame || (dec->events_wanted & JXL_DEC_FULL_IMAGE)) { dec->frame_dec_in_progress = true; dec->frame_stage = FrameStage::kFull; + } else if (!dec->is_last_total) { + dec->frame_stage = FrameStage::kHeader; + dec->AdvanceCodestream(dec->remaining_frame_size); + continue; + } else { + break; } } bool return_full_image = false; if (dec->frame_stage == FrameStage::kFull) { - if (dec->events_wanted & JXL_DEC_FULL_IMAGE) { + if (dec->preview_frame) { + if (!dec->preview_out_buffer_set) { + return JXL_DEC_NEED_PREVIEW_OUT_BUFFER; + } + } else if (dec->events_wanted & JXL_DEC_FULL_IMAGE) { if (!dec->image_out_buffer_set && (!dec->jpeg_decoder.IsOutputSet() || dec->ib->jpeg_data == nullptr) && @@ -1443,7 +1452,10 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, } } - if (dec->image_out_buffer_set && !!dec->image_out_buffer && + dec->frame_dec->MaybeSetUnpremultiplyAlpha(dec->unpremul_alpha); + + if (!dec->preview_frame && dec->image_out_buffer_set && + !!dec->image_out_buffer && dec->image_out_format.data_type == JXL_TYPE_UINT8 && dec->image_out_format.num_channels >= 3 && dec->extra_channel_output.empty()) { @@ -1462,78 +1474,67 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, // TODO(lode): Support more formats than just native endian float32 for // the low-memory callback path - if (dec->image_out_buffer_set && !!dec->image_out_init_callback && - !!dec->image_out_run_callback && + if (!dec->preview_frame && dec->image_out_buffer_set && + !!dec->image_out_init_callback && !!dec->image_out_run_callback && dec->image_out_format.data_type == JXL_TYPE_FLOAT && - dec->image_out_format.num_channels >= 3 && !swap_endianness && + dec->image_out_format.num_channels >= 3 && + dec->extra_channel_output.empty() && !swap_endianness && dec->frame_dec_in_progress) { bool is_rgba = dec->image_out_format.num_channels == 4; dec->frame_dec->MaybeSetFloatCallback( PixelCallback{ dec->image_out_init_callback, dec->image_out_run_callback, dec->image_out_destroy_callback, dec->image_out_init_opaque}, - is_rgba, !dec->keep_orientation); + is_rgba, dec->unpremul_alpha, !dec->keep_orientation); } - size_t pos = dec->frame_start - dec->codestream_pos; - if (pos >= size) { - return JXL_DEC_NEED_MORE_INPUT; - } - dec->sections->SetInput(in + pos, size - pos); - - if (dec->cpu_limit_base != 0) { - FrameDimensions frame_dim = dec->frame_header->ToFrameDimensions(); - // No overflow, checked in ParseHeader. - size_t num_pixels = frame_dim.xsize * frame_dim.ysize; - if (dec->used_cpu_base + num_pixels < dec->used_cpu_base) { - return JXL_API_ERROR("used too much CPU"); - } - dec->used_cpu_base += num_pixels; - if (dec->used_cpu_base > dec->cpu_limit_base) { - return JXL_API_ERROR("used too much CPU"); - } - } + size_t next_num_passes_to_pause = dec->frame_dec->NextNumPassesToPause(); - jxl::Status status = - dec->frame_dec->ProcessSections(dec->sections->section_info.data(), - dec->sections->section_info.size(), - dec->sections->section_status.data()); - JXL_API_RETURN_IF_ERROR(dec->sections->CloseInput()); - if (status.IsFatalError()) { - return JXL_API_ERROR("decoding frame failed"); - } + JXL_API_RETURN_IF_ERROR(JxlDecoderProcessSections(dec)); - // TODO(lode): allow next_in to move forward if sections from the - // beginning of the stream have been processed + bool all_sections_done = dec->frame_dec->HasDecodedAll(); + bool got_dc_only = !all_sections_done && dec->frame_dec->HasDecodedDC(); - bool all_sections_done = !!status && dec->frame_dec->HasDecodedAll(); + if (dec->frame_prog_detail >= JxlProgressiveDetail::kDC && + !dec->dc_frame_progression_done && got_dc_only) { + dec->dc_frame_progression_done = true; + dec->downsampling_target = 8; + return JXL_DEC_FRAME_PROGRESSION; + } - bool got_dc_only = - !!status && !all_sections_done && dec->frame_dec->HasDecodedDC(); + bool new_progression_step_done = + dec->frame_dec->NumCompletePasses() >= next_num_passes_to_pause; - if ((dec->events_wanted & JXL_DEC_FRAME_PROGRESSION) && got_dc_only) { - dec->events_wanted &= ~JXL_DEC_FRAME_PROGRESSION; + if (!all_sections_done && + dec->frame_prog_detail >= JxlProgressiveDetail::kLastPasses && + new_progression_step_done) { + dec->downsampling_target = + dec->frame_header->passes.GetDownsamplingTargetForCompletedPasses( + dec->frame_dec->NumCompletePasses()); return JXL_DEC_FRAME_PROGRESSION; } if (!all_sections_done) { // Not all sections have been processed yet - return JXL_DEC_NEED_MORE_INPUT; + return dec->RequestMoreInput(); + } + + if (!dec->preview_frame) { + size_t internal_index = dec->internal_frames - 1; + JXL_ASSERT(dec->frame_references.size() > internal_index); + // Always fill this in, even if it was already written, it could be that + // this frame was skipped before and set to 255, while only now we know + // the true value. + dec->frame_references[internal_index] = dec->frame_dec->References(); + // Copy exif/xmp metadata from their boxes into the jpeg_data, if + // JPEG reconstruction is requested. + if (dec->jpeg_decoder.IsOutputSet() && dec->ib->jpeg_data != nullptr) { + } } - size_t internal_index = dec->internal_frames - 1; - JXL_ASSERT(dec->frame_references.size() > internal_index); - // Always fill this in, even if it was already written, it could be that - // this frame was skipped before and set to 255, while only now we know - // the true value. - dec->frame_references[internal_index] = dec->frame_dec->References(); if (!dec->frame_dec->FinalizeFrame()) { return JXL_API_ERROR("decoding frame failed"); } - // Copy exif/xmp metadata from their boxes into the jpeg_data, if - // JPEG reconstruction is requested. - if (dec->jpeg_decoder.IsOutputSet() && dec->ib->jpeg_data != nullptr) { - } dec->frame_dec_in_progress = false; dec->frame_stage = FrameStage::kFullOutput; @@ -1542,7 +1543,15 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, bool output_jpeg_reconstruction = false; if (dec->frame_stage == FrameStage::kFullOutput) { - if (dec->is_last_of_still) { + if (dec->preview_frame) { + JxlDecoderStatus status = + ConvertImageInternal(dec, *dec->ib, dec->preview_out_format, + /*want_extra_channel=*/false, + /*extra_channel_index=*/0, + dec->preview_out_buffer, dec->preview_out_size, + /*out_callback=*/{}); + if (status != JXL_DEC_SUCCESS) return status; + } else if (dec->is_last_of_still) { if (dec->events_wanted & JXL_DEC_FULL_IMAGE) { dec->events_wanted &= ~JXL_DEC_FULL_IMAGE; return_full_image = true; @@ -1598,7 +1607,6 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, } dec->frame_stage = FrameStage::kHeader; - dec->frame_start += dec->frame_size; if (output_jpeg_reconstruction) { dec->recon_output_jpeg = JpegReconStage::kSettingMetadata; @@ -1607,7 +1615,12 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in, // The pixels have been output or are not needed, do not keep them in // memory here. dec->ib.reset(); - if (return_full_image && !dec->skipping_frame) { + if (dec->preview_frame) { + dec->got_preview_image = true; + dec->preview_frame = false; + dec->events_wanted &= ~JXL_DEC_PREVIEW_IMAGE; + return JXL_DEC_PREVIEW_IMAGE; + } else if (return_full_image && !dec->skipping_frame) { return JXL_DEC_FULL_IMAGE; } } @@ -1689,14 +1702,15 @@ static JxlDecoderStatus ParseBoxHeader(const uint8_t* in, size_t size, size_t box_start = pos; // Box size, including this header itself. *box_size = LoadBE32(in + pos); - memcpy(type, in + pos + 4, 4); - pos += 8; + pos += 4; if (*box_size == 1) { *header_size = 16; - if (OutOfBounds(pos, 8, size)) return JXL_DEC_NEED_MORE_INPUT; + if (OutOfBounds(pos, 12, size)) return JXL_DEC_NEED_MORE_INPUT; *box_size = LoadBE64(in + pos); pos += 8; } + memcpy(type, in + pos, 4); + pos += 4; *header_size = pos - box_start; if (*box_size > 0 && *box_size < *header_size) { return JXL_API_ERROR("invalid box size"); @@ -1712,6 +1726,8 @@ static JxlDecoderStatus HandleBoxes(JxlDecoder* dec) { // Box handling loop for (;;) { if (dec->box_stage != BoxStage::kHeader) { + dec->AdvanceInput(dec->header_size); + dec->header_size = 0; if ((dec->events_wanted & JXL_DEC_BOX) && dec->box_out_buffer_set_current_box) { uint8_t* next_out = dec->box_out_buffer + dec->box_out_buffer_pos; @@ -1836,6 +1852,18 @@ static JxlDecoderStatus HandleBoxes(JxlDecoder* dec) { return JXL_DEC_NEED_MORE_INPUT; } + bool boxed_codestream_done = + ((dec->events_wanted & JXL_DEC_BOX) && + dec->stage == DecoderStage::kCodestreamFinished && + dec->last_codestream_seen && !dec->JbrdNeedMoreBoxes()); + if (boxed_codestream_done && dec->avail_in >= 2 && + dec->next_in[0] == 0xff && + dec->next_in[1] == jxl::kCodestreamMarker) { + // We detected the start of the next naked codestream, so we can return + // success here. + return JXL_DEC_SUCCESS; + } + uint64_t box_size, header_size; JxlDecoderStatus status = ParseBoxHeader(dec->next_in, dec->avail_in, 0, dec->file_pos, @@ -1862,6 +1890,11 @@ static JxlDecoderStatus HandleBoxes(JxlDecoder* dec) { // The signature box at box_count == 1 is not checked here since that's // already done at the beginning. dec->box_count++; + if (boxed_codestream_done && memcmp(dec->box_type, "JXL ", 4) == 0) { + // We detected the start of the next boxed stream, so we can return + // success here. + return JXL_DEC_SUCCESS; + } if (dec->box_count == 2 && memcmp(dec->box_type, "ftyp", 4) != 0) { return JXL_API_ERROR("the second box must be the ftyp box"); } @@ -1869,16 +1902,14 @@ static JxlDecoderStatus HandleBoxes(JxlDecoder* dec) { return JXL_API_ERROR("the ftyp box must come second"); } - dec->AdvanceInput(header_size); - dec->box_contents_unbounded = (box_size == 0); - dec->box_contents_begin = dec->file_pos; - dec->box_contents_end = dec->box_contents_unbounded - ? 0 - : (dec->file_pos + box_size - header_size); + dec->box_contents_begin = dec->file_pos + header_size; + dec->box_contents_end = + dec->box_contents_unbounded ? 0 : (dec->file_pos + box_size); dec->box_contents_size = dec->box_contents_unbounded ? 0 : (box_size - header_size); dec->box_size = box_size; + dec->header_size = header_size; if (dec->orig_events_wanted & JXL_DEC_JPEG_RECONSTRUCTION) { // Initiate storing of Exif or XMP data for JPEG reconstruction @@ -1960,40 +1991,15 @@ static JxlDecoderStatus HandleBoxes(JxlDecoder* dec) { dec->AdvanceInput(4); dec->box_stage = BoxStage::kCodestream; } else if (dec->box_stage == BoxStage::kCodestream) { - size_t avail_codestream = dec->avail_in; - if (!dec->box_contents_unbounded) { - avail_codestream = std::min<size_t>( - avail_codestream, dec->box_contents_end - dec->file_pos); - } - - bool have_copy = !dec->codestream_copy.empty(); - if (have_copy) { - // TODO(lode): prune the codestream_copy vector if the codestream - // decoder no longer needs data from previous frames. - dec->codestream_copy.insert(dec->codestream_copy.end(), dec->next_in, - dec->next_in + avail_codestream); - dec->AdvanceInput(avail_codestream); - avail_codestream = dec->codestream_copy.size(); - } - - const uint8_t* codestream = - have_copy ? dec->codestream_copy.data() : dec->next_in; - - JxlDecoderStatus status = - jxl::JxlDecoderProcessCodestream(dec, codestream, avail_codestream); + JxlDecoderStatus status = jxl::JxlDecoderProcessCodestream(dec); if (status == JXL_DEC_FULL_IMAGE) { if (dec->recon_output_jpeg != JpegReconStage::kNone) { continue; } } if (status == JXL_DEC_NEED_MORE_INPUT) { - if (!have_copy) { - dec->codestream_copy.insert(dec->codestream_copy.end(), dec->next_in, - dec->next_in + avail_codestream); - dec->AdvanceInput(avail_codestream); - } - - if (dec->file_pos == dec->box_contents_end) { + if (dec->file_pos == dec->box_contents_end && + !dec->box_contents_unbounded) { dec->box_stage = BoxStage::kHeader; continue; } @@ -2006,27 +2012,12 @@ static JxlDecoderStatus HandleBoxes(JxlDecoder* dec) { } if (dec->box_contents_unbounded) { // Last box reached and codestream done, nothing more to do. - dec->AdvanceInput(dec->avail_in); break; } if (dec->events_wanted & JXL_DEC_BOX) { // Codestream done, but there may be more other boxes. dec->box_stage = BoxStage::kSkip; continue; - } else if (!dec->last_codestream_seen && - dec->CanUseMoreCodestreamInput()) { - // Even though the codestream was successfully decoded, the last seen - // jxlp box was not marked as last, so more jxlp boxes are expected. - // Since the codestream already successfully finished, the only valid - // case where this could happen is if there are empty jxlp boxes after - // this. - dec->box_stage = BoxStage::kSkip; - continue; - } else { - // Codestream decoded, and no box output requested, skip all further - // input and return success. - dec->AdvanceInput(dec->avail_in); - break; } } return status; @@ -2092,16 +2083,17 @@ static JxlDecoderStatus HandleBoxes(JxlDecoder* dec) { } // Arbitrarily more bytes may follow, only JxlDecoderCloseInput can // mark the end. + dec->AdvanceInput(dec->avail_in); return JXL_DEC_NEED_MORE_INPUT; } // Amount of remaining bytes in the box that is being skipped. size_t remaining = dec->box_contents_end - dec->file_pos; if (dec->avail_in < remaining) { - // Don't have the full box yet, skip all we have so far - dec->AdvanceInput(dec->avail_in); // Indicate how many more bytes needed starting from next_in. dec->basic_info_size_hint = InitialBasicInfoSizeHint() + dec->box_contents_end - dec->file_pos; + // Don't have the full box yet, skip all we have so far + dec->AdvanceInput(dec->avail_in); return JXL_DEC_NEED_MORE_INPUT; } else { // Full box available, skip all its remaining bytes @@ -2155,15 +2147,8 @@ JxlDecoderStatus JxlDecoderProcessInput(JxlDecoder* dec) { // data may be missing. if (status == JXL_DEC_SUCCESS) { if (dec->CanUseMoreCodestreamInput()) { - if (!dec->last_codestream_seen) { - // In case of jxlp boxes, this means no jxlp box marked as final was - // seen yet. Perhaps there is an empty jxlp box after this, this is not - // an error but will require more input. - return JXL_DEC_NEED_MORE_INPUT; - } return JXL_API_ERROR("codestream never finished"); } - if (dec->JbrdNeedMoreBoxes()) { return JXL_API_ERROR("missing metadata boxes for jpeg reconstruction"); } @@ -2181,6 +2166,8 @@ JxlDecoderStatus JxlDecoderGetBasicInfo(const JxlDecoder* dec, if (!dec->got_basic_info) return JXL_DEC_NEED_MORE_INPUT; if (info) { + memset(info, 0, sizeof(*info)); + const jxl::ImageMetadata& meta = dec->metadata.m; info->have_container = dec->have_container; @@ -2203,6 +2190,9 @@ JxlDecoderStatus JxlDecoderGetBasicInfo(const JxlDecoder* dec, } info->intensity_target = meta.IntensityTarget(); + if (dec->desired_intensity_target > 0) { + info->intensity_target = dec->desired_intensity_target; + } info->min_nits = meta.tone_mapping.min_nits; info->relative_to_max_display = meta.tone_mapping.relative_to_max_display; info->linear_below = meta.tone_mapping.linear_below; @@ -2303,8 +2293,8 @@ namespace { // but ensures that if the color encoding is not the encoding from the // codestream header metadata, it cannot require ICC profile. JxlDecoderStatus GetColorEncodingForTarget( - const JxlDecoder* dec, const JxlPixelFormat* format, - JxlColorProfileTarget target, const jxl::ColorEncoding** encoding) { + const JxlDecoder* dec, JxlColorProfileTarget target, + const jxl::ColorEncoding** encoding) { if (!dec->got_all_headers) return JXL_DEC_NEED_MORE_INPUT; *encoding = nullptr; if (target == JXL_COLOR_PROFILE_TARGET_DATA && dec->metadata.m.xyb_encoded) { @@ -2317,11 +2307,11 @@ JxlDecoderStatus GetColorEncodingForTarget( } // namespace JxlDecoderStatus JxlDecoderGetColorAsEncodedProfile( - const JxlDecoder* dec, const JxlPixelFormat* format, + const JxlDecoder* dec, const JxlPixelFormat* unused_format, JxlColorProfileTarget target, JxlColorEncoding* color_encoding) { const jxl::ColorEncoding* jxl_color_encoding = nullptr; JxlDecoderStatus status = - GetColorEncodingForTarget(dec, format, target, &jxl_color_encoding); + GetColorEncodingForTarget(dec, target, &jxl_color_encoding); if (status) return status; if (jxl_color_encoding->WantICC()) @@ -2334,13 +2324,12 @@ JxlDecoderStatus JxlDecoderGetColorAsEncodedProfile( return JXL_DEC_SUCCESS; } -JxlDecoderStatus JxlDecoderGetICCProfileSize(const JxlDecoder* dec, - const JxlPixelFormat* format, - JxlColorProfileTarget target, - size_t* size) { +JxlDecoderStatus JxlDecoderGetICCProfileSize( + const JxlDecoder* dec, const JxlPixelFormat* unused_format, + JxlColorProfileTarget target, size_t* size) { const jxl::ColorEncoding* jxl_color_encoding = nullptr; JxlDecoderStatus status = - GetColorEncodingForTarget(dec, format, target, &jxl_color_encoding); + GetColorEncodingForTarget(dec, target, &jxl_color_encoding); if (status != JXL_DEC_SUCCESS) return status; if (jxl_color_encoding->WantICC()) { @@ -2363,20 +2352,18 @@ JxlDecoderStatus JxlDecoderGetICCProfileSize(const JxlDecoder* dec, return JXL_DEC_SUCCESS; } -JxlDecoderStatus JxlDecoderGetColorAsICCProfile(const JxlDecoder* dec, - const JxlPixelFormat* format, - JxlColorProfileTarget target, - uint8_t* icc_profile, - size_t size) { +JxlDecoderStatus JxlDecoderGetColorAsICCProfile( + const JxlDecoder* dec, const JxlPixelFormat* unused_format, + JxlColorProfileTarget target, uint8_t* icc_profile, size_t size) { size_t wanted_size; // This also checks the NEED_MORE_INPUT and the unknown/xyb cases JxlDecoderStatus status = - JxlDecoderGetICCProfileSize(dec, format, target, &wanted_size); + JxlDecoderGetICCProfileSize(dec, nullptr, target, &wanted_size); if (status != JXL_DEC_SUCCESS) return status; if (size < wanted_size) return JXL_API_ERROR("ICC profile output too small"); const jxl::ColorEncoding* jxl_color_encoding = nullptr; - status = GetColorEncodingForTarget(dec, format, target, &jxl_color_encoding); + status = GetColorEncodingForTarget(dec, target, &jxl_color_encoding); if (status != JXL_DEC_SUCCESS) return status; memcpy(icc_profile, jxl_color_encoding->ICC().data(), @@ -2414,11 +2401,12 @@ JxlDecoderStatus PrepareSizeCheck(const JxlDecoder* dec, } // namespace +size_t JxlDecoderGetIntendedDownsamplingRatio(JxlDecoder* dec) { + return dec->downsampling_target; +} + JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec) { - if (!dec->image_out_buffer) return JXL_DEC_ERROR; - if (!dec->sections || dec->sections->section_info.empty()) { - return JXL_DEC_ERROR; - } + if (!dec->image_out_buffer_set) return JXL_DEC_ERROR; if (!dec->frame_dec || !dec->frame_dec_in_progress) { return JXL_DEC_ERROR; } @@ -2451,7 +2439,9 @@ JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec) { dec, *dec->ib, dec->image_out_format, /*want_extra_channel=*/false, /*extra_channel_index=*/0, dec->image_out_buffer, dec->image_out_size, - /*out_callback=*/{}); + jxl::PixelCallback{ + dec->image_out_init_callback, dec->image_out_run_callback, + dec->image_out_destroy_callback, dec->image_out_init_opaque}); dec->ib->ShrinkTo(xsize, ysize); if (status != JXL_DEC_SUCCESS) return status; return JXL_DEC_SUCCESS; @@ -2462,8 +2452,9 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderPreviewOutBufferSize( size_t bits; JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits); if (status != JXL_DEC_SUCCESS) return status; - if (format->num_channels < 3 && !dec->metadata.m.color_encoding.IsGray()) { - return JXL_API_ERROR("Grayscale output not possible for color image"); + if (format->num_channels < 3 && + !dec->image_metadata.color_encoding.IsGray()) { + return JXL_API_ERROR("Number of channels is too low for color output"); } size_t xsize = dec->metadata.oriented_preview_xsize(dec->keep_orientation); @@ -2485,8 +2476,9 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderSetPreviewOutBuffer( !(dec->orig_events_wanted & JXL_DEC_PREVIEW_IMAGE)) { return JXL_API_ERROR("No preview out buffer needed at this time"); } - if (format->num_channels < 3 && !dec->metadata.m.color_encoding.IsGray()) { - return JXL_API_ERROR("Grayscale output not possible for color image"); + if (format->num_channels < 3 && + !dec->image_metadata.color_encoding.IsGray()) { + return JXL_API_ERROR("Number of channels is too low for color output"); } size_t min_size; @@ -2538,8 +2530,9 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderImageOutBufferSize( size_t bits; JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits); if (status != JXL_DEC_SUCCESS) return status; - if (format->num_channels < 3 && !dec->metadata.m.color_encoding.IsGray()) { - return JXL_API_ERROR("Grayscale output not possible for color image"); + if (format->num_channels < 3 && + !dec->image_metadata.color_encoding.IsGray()) { + return JXL_API_ERROR("Number of channels is too low for color output"); } size_t xsize, ysize; GetCurrentDimensions(dec, xsize, ysize, true); @@ -2563,8 +2556,9 @@ JxlDecoderStatus JxlDecoderSetImageOutBuffer(JxlDecoder* dec, return JXL_API_ERROR( "Cannot change from image out callback to image out buffer"); } - if (format->num_channels < 3 && !dec->metadata.m.color_encoding.IsGray()) { - return JXL_API_ERROR("Grayscale output not possible for color image"); + if (format->num_channels < 3 && + !dec->image_metadata.color_encoding.IsGray()) { + return JXL_API_ERROR("Number of channels is too low for color output"); } size_t min_size; // This also checks whether the format is valid and supported and basic info @@ -2701,6 +2695,7 @@ JxlDecoderStatus JxlDecoderGetFrameHeader(const JxlDecoder* dec, return JXL_API_ERROR("no frame header available"); } const auto& metadata = dec->metadata.m; + memset(header, 0, sizeof(*header)); if (metadata.have_animation) { header->duration = dec->frame_header->animation_frame.duration; if (metadata.animation.have_timecodes) { @@ -2800,19 +2795,35 @@ JxlDecoderStatus JxlDecoderSetPreferredColorProfile( if (dec->post_headers) { return JXL_API_ERROR("too late to set the color encoding"); } - if (dec->metadata.m.color_encoding.IsGray() != - (color_encoding->color_space == JXL_COLOR_SPACE_GRAY)) { - return JXL_API_ERROR("grayscale mismatch"); + if (dec->image_metadata.color_encoding.IsGray() && + color_encoding->color_space != JXL_COLOR_SPACE_GRAY && + ((dec->preview_out_buffer_set && + dec->preview_out_format.num_channels < 3) || + (dec->image_out_buffer_set && dec->image_out_format.num_channels < 3))) { + return JXL_API_ERROR("Number of channels is too low for color output"); } if (color_encoding->color_space == JXL_COLOR_SPACE_UNKNOWN || color_encoding->color_space == JXL_COLOR_SPACE_XYB) { return JXL_API_ERROR("only RGB or grayscale output supported"); } - JXL_API_RETURN_IF_ERROR(ConvertExternalToInternalColorEncoding( - *color_encoding, &dec->default_enc)); - JXL_API_RETURN_IF_ERROR(dec->passes_state->output_encoding_info.Set( - dec->metadata, dec->default_enc)); + jxl::ColorEncoding c_out; + JXL_API_RETURN_IF_ERROR( + ConvertExternalToInternalColorEncoding(*color_encoding, &c_out)); + auto& output_encoding = dec->passes_state->output_encoding_info; + if (!c_out.SameColorEncoding(output_encoding.color_encoding)) { + JXL_API_RETURN_IF_ERROR(output_encoding.MaybeSetColorEncoding(c_out)); + dec->image_metadata.color_encoding = output_encoding.color_encoding; + } + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderSetDesiredIntensityTarget( + JxlDecoder* dec, float desired_intensity_target) { + if (desired_intensity_target < 0) { + return JXL_API_ERROR("negative intensity target requested"); + } + dec->desired_intensity_target = desired_intensity_target; return JXL_DEC_SUCCESS; } @@ -2882,3 +2893,15 @@ JxlDecoderStatus JxlDecoderGetBoxSizeRaw(const JxlDecoder* dec, } return JXL_DEC_SUCCESS; } + +JxlDecoderStatus JxlDecoderSetProgressiveDetail(JxlDecoder* dec, + JxlProgressiveDetail detail) { + if (detail != kDC && detail != kLastPasses && detail != kPasses) { + return JXL_API_ERROR( + "Values other than kDC (%d), kLastPasses (%d) and kPasses (%d), " + "like %d are not implemented.", + kDC, kLastPasses, kPasses, detail); + } + dec->prog_detail = detail; + return JXL_DEC_SUCCESS; +} diff --git a/media/libjxl/src/lib/jxl/decode_test.cc b/media/libjxl/src/lib/jxl/decode_test.cc index 4f98ffaec3..5b9b735e18 100644 --- a/media/libjxl/src/lib/jxl/decode_test.cc +++ b/media/libjxl/src/lib/jxl/decode_test.cc @@ -17,9 +17,12 @@ #include "jxl/decode_cxx.h" #include "jxl/resizable_parallel_runner_cxx.h" #include "jxl/thread_parallel_runner_cxx.h" -#include "lib/extras/enc/jpg.h" +#include "jxl/types.h" +#include "lib/extras/codec.h" +#include "lib/extras/dec/color_description.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/base/span.h" #include "lib/jxl/base/status.h" #include "lib/jxl/common.h" @@ -31,11 +34,15 @@ #include "lib/jxl/enc_icc_codec.h" #include "lib/jxl/encode_internal.h" #include "lib/jxl/fields.h" +#include "lib/jxl/frame_header.h" #include "lib/jxl/headers.h" #include "lib/jxl/icc_codec.h" +#include "lib/jxl/image_metadata.h" #include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "lib/jxl/progressive_split.h" #include "lib/jxl/test_utils.h" #include "lib/jxl/testdata.h" +#include "lib/jxl/toc.h" //////////////////////////////////////////////////////////////////////////////// @@ -70,6 +77,9 @@ enum CodeStreamBoxFormat { // Have multiple partial codestream boxes, and the first one has a content // of zero length kCSBF_Multi_First_Empty, + // Have multiple partial codestream boxes, and the last one has a content + // of zero length and there is an unknown empty box at the end + kCSBF_Multi_Last_Empty_Other, // Have a compressed exif box before a regular codestream box kCSBF_Brob_Exif, // Not a value but used for counting amount of enum entries @@ -160,6 +170,19 @@ void AppendTestBox(const char* type, const char* contents, size_t contents_size, bytes->append(contents_u, contents_u + contents_size); } +struct TestCodestreamParams { + CompressParams cparams; + CodeStreamBoxFormat box_format = kCSBF_None; + JxlOrientation orientation = JXL_ORIENT_IDENTITY; + bool add_preview = false; + bool add_intrinsic_size = false; + bool add_icc_profile = false; + float intensity_target = 0.0; + std::string color_space; + PaddedBytes* jpeg_codestream = nullptr; + const ProgressiveMode* progressive_mode = nullptr; +}; + // Input pixels always given as 16-bit RGBA, 8 bytes per pixel. // include_alpha determines if the encoded image should contain the alpha // channel. @@ -170,70 +193,83 @@ void AppendTestBox(const char* type, const char* contents, size_t contents_size, // Providing jpeg_codestream will populate the jpeg_codestream with compressed // JPEG bytes, and make it possible to reconstruct those exact JPEG bytes using // the return value _if_ add_container indicates a box format. -PaddedBytes CreateTestJXLCodestream( - Span<const uint8_t> pixels, size_t xsize, size_t ysize, size_t num_channels, - const CompressParams& cparams, CodeStreamBoxFormat add_container, - JxlOrientation orientation, bool add_preview, bool add_intrinsic_size, - bool add_icc_profile = false, PaddedBytes* jpeg_codestream = nullptr) { +PaddedBytes CreateTestJXLCodestream(Span<const uint8_t> pixels, size_t xsize, + size_t ysize, size_t num_channels, + const TestCodestreamParams& params) { // Compress the pixels with JPEG XL. bool grayscale = (num_channels <= 2); - bool include_alpha = !(num_channels & 1) && jpeg_codestream == nullptr; - size_t bitdepth = jpeg_codestream == nullptr ? 16 : 8; + bool include_alpha = !(num_channels & 1) && params.jpeg_codestream == nullptr; + size_t bitdepth = params.jpeg_codestream == nullptr ? 16 : 8; CodecInOut io; io.SetSize(xsize, ysize); - ColorEncoding color_encoding = - jxl::ColorEncoding::SRGB(/*is_gray=*/grayscale); - if (add_icc_profile) { + ColorEncoding color_encoding; + if (params.add_icc_profile) { // the hardcoded ICC profile we attach requires RGB. EXPECT_EQ(false, grayscale); + EXPECT_TRUE(params.color_space.empty()); EXPECT_TRUE(color_encoding.SetICC(GetIccTestProfile())); + } else if (!params.color_space.empty()) { + JxlColorEncoding c; + EXPECT_TRUE(jxl::ParseDescription(params.color_space, &c)); + EXPECT_TRUE(ConvertExternalToInternalColorEncoding(c, &color_encoding)); + EXPECT_EQ(color_encoding.IsGray(), grayscale); + } else { + color_encoding = jxl::ColorEncoding::SRGB(/*is_gray=*/grayscale); } ThreadPool pool(nullptr, nullptr); io.metadata.m.SetUintSamples(bitdepth); if (include_alpha) { io.metadata.m.SetAlphaBits(bitdepth); } + if (params.intensity_target != 0) { + io.metadata.m.SetIntensityTarget(params.intensity_target); + } // Make the grayscale-ness of the io metadata color_encoding and the packed // image match. io.metadata.m.color_encoding = color_encoding; EXPECT_TRUE(ConvertFromExternal( pixels, xsize, ysize, color_encoding, num_channels, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN, - /*flipped_y=*/false, &pool, &io.Main(), /*float_in=*/false, /*align=*/0)); + &pool, &io.Main(), /*float_in=*/false, /*align=*/0)); jxl::PaddedBytes jpeg_data; - if (jpeg_codestream != nullptr) { + if (params.jpeg_codestream != nullptr) { #if JPEGXL_ENABLE_JPEG - jxl::PaddedBytes jpeg_bytes; - EXPECT_TRUE(EncodeImageJPG(&io, jxl::extras::JpegEncoder::kLibJpeg, - /*quality=*/70, jxl::YCbCrChromaSubsampling(), - &pool, &jpeg_bytes)); - jpeg_codestream->append(jpeg_bytes.data(), - jpeg_bytes.data() + jpeg_bytes.size()); + std::vector<uint8_t> jpeg_bytes; + io.jpeg_quality = 70; + EXPECT_TRUE(Encode(io, extras::Codec::kJPG, io.metadata.m.color_encoding, + /*bits_per_sample=*/8, &jpeg_bytes, &pool)); + params.jpeg_codestream->append(jpeg_bytes.data(), + jpeg_bytes.data() + jpeg_bytes.size()); EXPECT_TRUE(jxl::jpeg::DecodeImageJPG( jxl::Span<const uint8_t>(jpeg_bytes.data(), jpeg_bytes.size()), &io)); - EXPECT_TRUE(EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data, cparams)); + EXPECT_TRUE( + EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data, params.cparams)); io.metadata.m.xyb_encoded = false; #else // JPEGXL_ENABLE_JPEG JXL_ABORT( "unable to create reconstructible JPEG without JPEG support enabled"); #endif // JPEGXL_ENABLE_JPEG } - if (add_preview) { + if (params.add_preview) { io.preview_frame = io.Main().Copy(); io.preview_frame.ShrinkTo(xsize / 7, ysize / 7); io.metadata.m.have_preview = true; EXPECT_TRUE(io.metadata.m.preview_size.Set(io.preview_frame.xsize(), io.preview_frame.ysize())); } - if (add_intrinsic_size) { + if (params.add_intrinsic_size) { EXPECT_TRUE(io.metadata.m.intrinsic_size.Set(xsize / 3, ysize / 3)); } - io.metadata.m.orientation = orientation; + io.metadata.m.orientation = params.orientation; AuxOut aux_out; PaddedBytes compressed; PassesEncoderState enc_state; - EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), - &aux_out, &pool)); + if (params.progressive_mode) { + enc_state.progressive_splitter.SetProgressiveMode(*params.progressive_mode); + } + EXPECT_TRUE(EncodeFile(params.cparams, &io, &enc_state, &compressed, + GetJxlCms(), &aux_out, &pool)); + CodeStreamBoxFormat add_container = params.box_format; if (add_container != kCSBF_None) { // Header with signature box and ftyp box. const uint8_t header[] = {0, 0, 0, 0xc, 0x4a, 0x58, 0x4c, 0x20, @@ -245,7 +281,8 @@ PaddedBytes CreateTestJXLCodestream( add_container == kCSBF_Multi_Zero_Terminated || add_container == kCSBF_Multi_Other_Terminated || add_container == kCSBF_Multi_Other_Zero_Terminated || - add_container == kCSBF_Multi_First_Empty; + add_container == kCSBF_Multi_First_Empty || + add_container == kCSBF_Multi_Last_Empty_Other; if (is_multi) { size_t third = compressed.size() / 3; @@ -258,7 +295,7 @@ PaddedBytes CreateTestJXLCodestream( PaddedBytes c; c.append(header, header + sizeof(header)); - if (jpeg_codestream != nullptr) { + if (params.jpeg_codestream != nullptr) { jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false, &c); c.append(jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); @@ -308,8 +345,23 @@ PaddedBytes CreateTestJXLCodestream( c.push_back('x'); c.push_back('l'); c.push_back('p'); - AppendU32BE(jxlp_index++ | 0x80000000, &c); + if (add_container != kCSBF_Multi_Last_Empty_Other) { + AppendU32BE(jxlp_index++ | 0x80000000, &c); + } else { + AppendU32BE(jxlp_index++, &c); + } c.append(compressed2.data(), compressed2.data() + compressed2.size()); + if (add_container == kCSBF_Multi_Last_Empty_Other) { + // Dummy (empty) codestream part + AppendU32BE(12, &c); + c.push_back('j'); + c.push_back('x'); + c.push_back('l'); + c.push_back('p'); + AppendU32BE(jxlp_index++ | 0x80000000, &c); + AppendTestBox(unk3_box_type, unk3_box_contents, unk3_box_size, false, + &c); + } if (add_container == kCSBF_Multi_Other_Terminated) { AppendTestBox(unk3_box_type, unk3_box_contents, unk3_box_size, false, &c); @@ -322,7 +374,7 @@ PaddedBytes CreateTestJXLCodestream( } else { PaddedBytes c; c.append(header, header + sizeof(header)); - if (jpeg_codestream != nullptr) { + if (params.jpeg_codestream != nullptr) { jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false, &c); c.append(jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); @@ -350,12 +402,21 @@ PaddedBytes CreateTestJXLCodestream( return compressed; } +JxlDecoderStatus ProcessInputIgnoreBoxes(JxlDecoder* dec) { + JxlDecoderStatus status; + while ((status = JxlDecoderProcessInput(dec)) == JXL_DEC_BOX) { + continue; + } + return status; +} + // Decodes one-shot with the API for non-streaming decoding tests. std::vector<uint8_t> DecodeWithAPI(JxlDecoder* dec, Span<const uint8_t> compressed, const JxlPixelFormat& format, bool use_callback, bool set_buffer_early, bool use_resizable_runner, + bool require_boxes, bool expect_success, PaddedBytes* icc = nullptr) { JxlThreadParallelRunnerPtr runner_fixed; JxlResizableParallelRunnerPtr runner_resizable; @@ -376,16 +437,20 @@ std::vector<uint8_t> DecodeWithAPI(JxlDecoder* dec, EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetParallelRunner(dec, runner_fn, runner)); + auto process_input = + require_boxes ? ProcessInputIgnoreBoxes : JxlDecoderProcessInput; + EXPECT_EQ( JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents( dec, JXL_DEC_BASIC_INFO | (set_buffer_early ? JXL_DEC_FRAME : 0) | JXL_DEC_PREVIEW_IMAGE | JXL_DEC_FULL_IMAGE | + (require_boxes ? JXL_DEC_BOX : 0) | (icc != nullptr ? JXL_DEC_COLOR_ENCODING : 0))); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, compressed.data(), compressed.size())); - EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_BASIC_INFO, process_input(dec)); size_t buffer_size; EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); @@ -411,7 +476,7 @@ std::vector<uint8_t> DecodeWithAPI(JxlDecoder* dec, num_pixels * bytes_per_pixel); }; - JxlDecoderStatus status = JxlDecoderProcessInput(dec); + JxlDecoderStatus status = process_input(dec); if (status == JXL_DEC_COLOR_ENCODING) { size_t icc_size = 0; @@ -423,7 +488,7 @@ std::vector<uint8_t> DecodeWithAPI(JxlDecoder* dec, dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, icc->data(), icc_size)); - status = JxlDecoderProcessInput(dec); + status = process_input(dec); } std::vector<uint8_t> preview; @@ -435,9 +500,9 @@ std::vector<uint8_t> DecodeWithAPI(JxlDecoder* dec, EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetPreviewOutBuffer(dec, &format, preview.data(), preview.size())); - EXPECT_EQ(JXL_DEC_PREVIEW_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_PREVIEW_IMAGE, process_input(dec)); - status = JxlDecoderProcessInput(dec); + status = process_input(dec); } if (set_buffer_early) { @@ -461,11 +526,15 @@ std::vector<uint8_t> DecodeWithAPI(JxlDecoder* dec, dec, &format, pixels.data(), pixels.size())); } - EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, process_input(dec)); // After the full image was output, JxlDecoderProcessInput should return - // success to indicate all is done. - EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + // success to indicate all is done, unless we requested boxes and the last + // box was not a terminal unbounded box, in which case it should ask for + // more input. + JxlDecoderStatus expected_status = + expect_success ? JXL_DEC_SUCCESS : JXL_DEC_NEED_MORE_INPUT; + EXPECT_EQ(expected_status, process_input(dec)); return pixels; } @@ -474,11 +543,12 @@ std::vector<uint8_t> DecodeWithAPI(JxlDecoder* dec, std::vector<uint8_t> DecodeWithAPI(Span<const uint8_t> compressed, const JxlPixelFormat& format, bool use_callback, bool set_buffer_early, - bool use_resizable_runner) { + bool use_resizable_runner, + bool require_boxes, bool expect_success) { JxlDecoder* dec = JxlDecoderCreate(NULL); std::vector<uint8_t> pixels = DecodeWithAPI(dec, compressed, format, use_callback, set_buffer_early, - use_resizable_runner); + use_resizable_runner, require_boxes, expect_success); JxlDecoderDestroy(dec); return pixels; } @@ -1184,15 +1254,19 @@ TEST_P(DecodeTestParam, PixelTest) { jxl::test::GetSomeTestImage(config.xsize, config.ysize, orig_channels, 0); JxlPixelFormat format_orig = {orig_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; - cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. - cparams.speed_tier = jxl::SpeedTier::kThunder; - cparams.resampling = config.upsampling; - cparams.ec_resampling = config.upsampling; + jxl::TestCodestreamParams params; + // Lossless to verify pixels exactly after roundtrip. + params.cparams.SetLossless(); + params.cparams.speed_tier = jxl::SpeedTier::kThunder; + params.cparams.resampling = config.upsampling; + params.cparams.ec_resampling = config.upsampling; + params.box_format = config.add_container; + params.orientation = config.orientation; + params.add_preview = config.add_preview; + params.add_intrinsic_size = config.add_intrinsic_size; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), config.xsize, - config.ysize, orig_channels, cparams, config.add_container, - config.orientation, config.add_preview, config.add_intrinsic_size); + config.ysize, orig_channels, params); JxlPixelFormat format = {config.output_channels, config.data_type, config.endianness, 0}; @@ -1204,7 +1278,8 @@ TEST_P(DecodeTestParam, PixelTest) { std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), format, config.use_callback, config.set_buffer_early, - config.use_resizable_runner); + config.use_resizable_runner, /*require_boxes=*/false, + /*expect_success=*/true); JxlDecoderReset(dec); EXPECT_EQ(num_pixels * config.output_channels * jxl::test::GetDataBits(config.data_type) / jxl::kBitsPerByte, @@ -1222,12 +1297,12 @@ TEST_P(DecodeTestParam, PixelTest) { if (config.include_alpha) io.metadata.m.SetAlphaBits(16); io.SetSize(config.xsize, config.ysize); - EXPECT_TRUE(ConvertFromExternal( - bytes, config.xsize, config.ysize, color_encoding, - config.output_channels, - /*alpha_is_premultiplied=*/false, 16, JXL_BIG_ENDIAN, - /*flipped_y=*/false, nullptr, &io.Main(), /*float_in=*/false, - /*align=*/0)); + EXPECT_TRUE(ConvertFromExternal(bytes, config.xsize, config.ysize, + color_encoding, config.output_channels, + /*alpha_is_premultiplied=*/false, 16, + JXL_BIG_ENDIAN, nullptr, &io.Main(), + /*float_in=*/false, + /*align=*/0)); for (size_t i = 0; i < pixels.size(); i++) pixels[i] = 0; EXPECT_TRUE(ConvertToExternal( @@ -1452,14 +1527,16 @@ TEST(DecodeTest, PixelTestWithICCProfileLossless) { size_t num_pixels = xsize * ysize; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); JxlPixelFormat format_orig = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; - cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. - cparams.speed_tier = jxl::SpeedTier::kThunder; + jxl::TestCodestreamParams params; + // Lossless to verify pixels exactly after roundtrip. + params.cparams.SetLossless(); + params.cparams.speed_tier = jxl::SpeedTier::kThunder; + params.add_icc_profile = true; // For variation: some have container and no preview, others have preview // and no container. jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 4, - cparams, kCSBF_None, JXL_ORIENT_IDENTITY, false, false, true); + params); for (uint32_t channels = 3; channels <= 4; ++channels) { { @@ -1468,7 +1545,8 @@ TEST(DecodeTest, PixelTestWithICCProfileLossless) { std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), format, /*use_callback=*/false, /*set_buffer_early=*/false, - /*use_resizable_runner=*/false); + /*use_resizable_runner=*/false, /*require_boxes=*/false, + /*expect_success=*/true); JxlDecoderReset(dec); EXPECT_EQ(num_pixels * channels, pixels2.size()); EXPECT_EQ(0u, @@ -1482,7 +1560,8 @@ TEST(DecodeTest, PixelTestWithICCProfileLossless) { std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), format, /*use_callback=*/true, /*set_buffer_early=*/true, - /*use_resizable_runner=*/false); + /*use_resizable_runner=*/false, /*require_boxes=*/false, + /*expect_success=*/true); JxlDecoderReset(dec); EXPECT_EQ(num_pixels * channels * 2, pixels2.size()); EXPECT_EQ(0u, @@ -1496,7 +1575,8 @@ TEST(DecodeTest, PixelTestWithICCProfileLossless) { std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), format, /*use_callback=*/false, /*set_buffer_early=*/false, - /*use_resizable_runner=*/false); + /*use_resizable_runner=*/false, /*reuqire_boxes=*/false, + /*expect_success=*/true); JxlDecoderReset(dec); EXPECT_EQ(num_pixels * channels * 4, pixels2.size()); EXPECT_EQ(0u, @@ -1515,12 +1595,11 @@ TEST(DecodeTest, PixelTestWithICCProfileLossy) { size_t num_pixels = xsize * ysize; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); JxlPixelFormat format_orig = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; + jxl::TestCodestreamParams params; + params.add_icc_profile = true; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, - cparams, kCSBF_None, JXL_ORIENT_IDENTITY, /*add_preview=*/false, - /*add_intrinsic_size=*/false, - /*add_icc_profile=*/true); + params); uint32_t channels = 3; JxlPixelFormat format = {channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; @@ -1529,7 +1608,8 @@ TEST(DecodeTest, PixelTestWithICCProfileLossy) { std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), format, /*use_callback=*/false, /*set_buffer_early=*/true, - /*use_resizable_runner=*/false, /*icc=*/&icc); + /*use_resizable_runner=*/false, /*require_boxes=*/false, + /*expect_success=*/true, /*icc=*/&icc); JxlDecoderReset(dec); EXPECT_EQ(num_pixels * channels * 4, pixels2.size()); @@ -1540,12 +1620,12 @@ TEST(DecodeTest, PixelTestWithICCProfileLossy) { jxl::Span<const uint8_t> span0(pixels.data(), pixels.size()); jxl::CodecInOut io0; io0.SetSize(xsize, ysize); - EXPECT_TRUE(ConvertFromExternal( - span0, xsize, ysize, color_encoding0, /*channels=*/3, - /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - format_orig.endianness, - /*flipped_y=*/false, /*pool=*/nullptr, &io0.Main(), /*float_in=*/false, - /*align=*/0)); + EXPECT_TRUE( + ConvertFromExternal(span0, xsize, ysize, color_encoding0, /*channels=*/3, + /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/16, format_orig.endianness, + /*pool=*/nullptr, &io0.Main(), /*float_in=*/false, + /*align=*/0)); jxl::ColorEncoding color_encoding1; EXPECT_TRUE(color_encoding1.SetICC(std::move(icc))); @@ -1555,17 +1635,209 @@ TEST(DecodeTest, PixelTestWithICCProfileLossy) { EXPECT_TRUE(ConvertFromExternal(span1, xsize, ysize, color_encoding1, channels, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/32, format.endianness, - /*flipped_y=*/false, /*pool=*/nullptr, - &io1.Main(), /*float_in=*/true, /*align=*/0)); + /*pool=*/nullptr, &io1.Main(), + /*float_in=*/true, /*align=*/0)); jxl::ButteraugliParams ba; EXPECT_THAT(ButteraugliDistance(io0, io1, ba, jxl::GetJxlCms(), /*distmap=*/nullptr, nullptr), - IsSlightlyBelow(0.77f)); + IsSlightlyBelow(0.785f)); + + JxlDecoderDestroy(dec); +} + +std::string ColorDescription(JxlColorEncoding c) { + jxl::ColorEncoding color_encoding; + EXPECT_TRUE(ConvertExternalToInternalColorEncoding(c, &color_encoding)); + return Description(color_encoding); +} + +std::string GetOrigProfile(JxlDecoder* dec) { + JxlColorEncoding c; + JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_ORIGINAL; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsEncodedProfile(dec, nullptr, target, &c)); + return ColorDescription(c); +} +std::string GetDataProfile(JxlDecoder* dec) { + JxlColorEncoding c; + JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_DATA; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsEncodedProfile(dec, nullptr, target, &c)); + return ColorDescription(c); +} + +double ButteraugliDistance(size_t xsize, size_t ysize, + const std::vector<uint8_t>& pixels_in, + const jxl::ColorEncoding& color_in, + float intensity_in, + const std::vector<uint8_t>& pixels_out, + const jxl::ColorEncoding& color_out, + float intensity_out) { + jxl::CodecInOut in; + in.metadata.m.color_encoding = color_in; + in.metadata.m.SetIntensityTarget(intensity_in); + EXPECT_TRUE(jxl::ConvertFromExternal( + jxl::Span<const uint8_t>(pixels_in.data(), pixels_in.size()), xsize, + ysize, color_in, color_in.Channels(), + /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/16, JXL_BIG_ENDIAN, + /*pool=*/nullptr, &in.Main(), /*float_in=*/false, /*align=*/0)); + jxl::CodecInOut out; + out.metadata.m.color_encoding = color_out; + out.metadata.m.SetIntensityTarget(intensity_out); + EXPECT_TRUE(jxl::ConvertFromExternal( + jxl::Span<const uint8_t>(pixels_out.data(), pixels_out.size()), xsize, + ysize, color_out, color_out.Channels(), + /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/16, JXL_BIG_ENDIAN, + /*pool=*/nullptr, &out.Main(), /*float_in=*/false, /*align=*/0)); + return ButteraugliDistance(in, out, jxl::ButteraugliParams(), + jxl::GetJxlCms(), nullptr, nullptr); +} + +class DecodeAllEncodingsTest + : public ::testing::TestWithParam<jxl::test::ColorEncodingDescriptor> {}; +JXL_GTEST_INSTANTIATE_TEST_SUITE_P( + DecodeAllEncodingsTestInstantiation, DecodeAllEncodingsTest, + ::testing::ValuesIn(jxl::test::AllEncodings())); +TEST_P(DecodeAllEncodingsTest, PreserveOriginalProfileTest) { + size_t xsize = 123, ysize = 77; + std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + JxlPixelFormat format = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + int events = JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE; + const auto& cdesc = GetParam(); + jxl::ColorEncoding c_in = jxl::test::ColorEncodingFromDescriptor(cdesc); + if (c_in.rendering_intent != jxl::RenderingIntent::kRelative) return; + std::string color_space_in = Description(c_in); + float intensity_in = c_in.tf.IsPQ() ? 10000 : 255; + printf("Testing input color space %s\n", color_space_in.c_str()); + jxl::TestCodestreamParams params; + params.color_space = color_space_in; + params.intensity_target = intensity_in; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, + params); + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec, events)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, data.data(), data.size())); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(xsize, info.xsize); + EXPECT_EQ(ysize, info.ysize); + EXPECT_FALSE(info.uses_original_profile); + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + EXPECT_EQ(GetOrigProfile(dec), color_space_in); + EXPECT_EQ(GetDataProfile(dec), color_space_in); + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + std::vector<uint8_t> out(pixels.size()); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer(dec, &format, out.data(), out.size())); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + double dist = ButteraugliDistance(xsize, ysize, pixels, c_in, intensity_in, + out, c_in, intensity_in); + EXPECT_LT(dist, 1.2); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); JxlDecoderDestroy(dec); } +namespace { +void SetPreferredColorProfileTest( + const jxl::test::ColorEncodingDescriptor& from) { + size_t xsize = 123, ysize = 77; + int events = JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE; + jxl::ColorEncoding c_in = jxl::test::ColorEncodingFromDescriptor(from); + if (c_in.rendering_intent != jxl::RenderingIntent::kRelative) return; + if (c_in.white_point != jxl::WhitePoint::kD65) return; + uint32_t num_channels = c_in.Channels(); + std::vector<uint8_t> pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + std::string color_space_in = Description(c_in); + float intensity_in = c_in.tf.IsPQ() ? 10000 : 255; + jxl::TestCodestreamParams params; + params.color_space = color_space_in; + params.intensity_target = intensity_in; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + num_channels, params); + for (const auto& c1 : jxl::test::AllEncodings()) { + jxl::ColorEncoding c_out = jxl::test::ColorEncodingFromDescriptor(c1); + float intensity_out = intensity_in; + if (c_out.rendering_intent != jxl::RenderingIntent::kRelative) continue; + if ((c_in.primaries == jxl::Primaries::k2100 && + c_out.primaries != jxl::Primaries::k2100) || + (c_in.primaries == jxl::Primaries::kP3 && + c_out.primaries == jxl::Primaries::kSRGB)) { + // Converting to a narrower gamut does not work without gammut mapping. + continue; + } + if (c_out.tf.IsHLG() && intensity_out > 300) { + // The Linear->HLG OOTF function at this intensity level can push + // saturated colors out of gamut, so we would need gamut mapping in + // this case too. + continue; + } + std::string color_space_out = Description(c_out); + if (color_space_in == color_space_out) continue; + printf("Testing input color space %s with output color space %s\n", + color_space_in.c_str(), color_space_out.c_str()); + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec, events)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput(dec, data.data(), data.size())); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(xsize, info.xsize); + EXPECT_EQ(ysize, info.ysize); + EXPECT_FALSE(info.uses_original_profile); + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + EXPECT_EQ(GetOrigProfile(dec), color_space_in); + EXPECT_EQ(GetDataProfile(dec), color_space_in); + JxlColorEncoding encoding_out; + EXPECT_TRUE(jxl::ParseDescription(color_space_out, &encoding_out)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetPreferredColorProfile(dec, &encoding_out)); + EXPECT_EQ(GetOrigProfile(dec), color_space_in); + EXPECT_EQ(GetDataProfile(dec), color_space_out); + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + size_t buffer_size; + JxlPixelFormat out_format = format; + out_format.num_channels = c_out.Channels(); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &out_format, &buffer_size)); + std::vector<uint8_t> out(buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &out_format, out.data(), out.size())); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + double dist = ButteraugliDistance(xsize, ysize, pixels, c_in, intensity_in, + out, c_out, intensity_out); + if (c_in.white_point == c_out.white_point) { + EXPECT_LT(dist, 1.2); + } else { + EXPECT_LT(dist, 4.0); + } + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + JxlDecoderDestroy(dec); + } +} +} // namespace + +TEST(DecodeTest, SetPreferredColorProfileTestFromGray) { + jxl::test::ColorEncodingDescriptor gray = { + jxl::ColorSpace::kGray, jxl::WhitePoint::kD65, jxl::Primaries::kSRGB, + jxl::TransferFunction::kSRGB, jxl::RenderingIntent::kRelative}; + SetPreferredColorProfileTest(gray); +} + +TEST_P(DecodeAllEncodingsTest, SetPreferredColorProfileTest) { + const auto& from = GetParam(); + SetPreferredColorProfileTest(from); +} + // Tests the case of lossy sRGB image without alpha channel, decoded to RGB8 // and to RGBA8 TEST(DecodeTest, PixelTestOpaqueSrgbLossy) { @@ -1577,24 +1849,20 @@ TEST(DecodeTest, PixelTestOpaqueSrgbLossy) { std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); JxlPixelFormat format_orig = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, - cparams, kCSBF_None, JXL_ORIENT_IDENTITY, /*add_preview=*/false, - /*add_intrinsic_size=*/false, - /*add_icc_profile=*/false); + jxl::TestCodestreamParams()); JxlPixelFormat format = {channels, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), format, /*use_callback=*/true, /*set_buffer_early=*/false, - /*use_resizable_runner=*/false); + /*use_resizable_runner=*/false, /*require_boxes=*/false, + /*expect_success*/ true); JxlDecoderReset(dec); EXPECT_EQ(num_pixels * channels, pixels2.size()); - // The input pixels use the profile matching GetIccTestProfile, since we set - // add_icc_profile for CreateTestJXLCodestream to true. jxl::ColorEncoding color_encoding0 = jxl::ColorEncoding::SRGB(false); jxl::Span<const uint8_t> span0(pixels.data(), pixels.size()); jxl::CodecInOut io0; @@ -1603,7 +1871,7 @@ TEST(DecodeTest, PixelTestOpaqueSrgbLossy) { span0, xsize, ysize, color_encoding0, /*channels=*/3, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, format_orig.endianness, - /*flipped_y=*/false, /*pool=*/nullptr, &io0.Main(), /*float_in=*/false, + /*pool=*/nullptr, &io0.Main(), /*float_in=*/false, /*align=*/0)); jxl::ColorEncoding color_encoding1 = jxl::ColorEncoding::SRGB(false); @@ -1612,8 +1880,8 @@ TEST(DecodeTest, PixelTestOpaqueSrgbLossy) { EXPECT_TRUE(ConvertFromExternal(span1, xsize, ysize, color_encoding1, channels, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/8, format.endianness, - /*flipped_y=*/false, /*pool=*/nullptr, - &io1.Main(), /*float_in=*/false, + /*pool=*/nullptr, &io1.Main(), + /*float_in=*/false, /*align=*/0)); jxl::ButteraugliParams ba; @@ -1635,25 +1903,22 @@ TEST(DecodeTest, PixelTestOpaqueSrgbLossyNoise) { std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); JxlPixelFormat format_orig = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; - cparams.noise = jxl::Override::kOn; + jxl::TestCodestreamParams params; + params.cparams.noise = jxl::Override::kOn; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, - cparams, kCSBF_None, JXL_ORIENT_IDENTITY, /*add_preview=*/false, - /*add_intrinsic_size=*/false, - /*add_icc_profile=*/false); + params); JxlPixelFormat format = {channels, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), format, /*use_callback=*/false, /*set_buffer_early=*/true, - /*use_resizable_runner=*/false); + /*use_resizable_runner=*/false, /*require_boxes=*/false, + /*expect_success=*/true); JxlDecoderReset(dec); EXPECT_EQ(num_pixels * channels, pixels2.size()); - // The input pixels use the profile matching GetIccTestProfile, since we set - // add_icc_profile for CreateTestJXLCodestream to true. jxl::ColorEncoding color_encoding0 = jxl::ColorEncoding::SRGB(false); jxl::Span<const uint8_t> span0(pixels.data(), pixels.size()); jxl::CodecInOut io0; @@ -1662,7 +1927,7 @@ TEST(DecodeTest, PixelTestOpaqueSrgbLossyNoise) { span0, xsize, ysize, color_encoding0, /*channels=*/3, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, format_orig.endianness, - /*flipped_y=*/false, /*pool=*/nullptr, &io0.Main(), /*float_in=*/false, + /*pool=*/nullptr, &io0.Main(), /*float_in=*/false, /*align=*/0)); jxl::ColorEncoding color_encoding1 = jxl::ColorEncoding::SRGB(false); @@ -1671,8 +1936,8 @@ TEST(DecodeTest, PixelTestOpaqueSrgbLossyNoise) { EXPECT_TRUE(ConvertFromExternal(span1, xsize, ysize, color_encoding1, channels, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/8, format.endianness, - /*flipped_y=*/false, /*pool=*/nullptr, - &io1.Main(), /*float_in=*/false, + /*pool=*/nullptr, &io1.Main(), + /*float_in=*/false, /*align=*/0)); jxl::ButteraugliParams ba; @@ -1684,6 +1949,169 @@ TEST(DecodeTest, PixelTestOpaqueSrgbLossyNoise) { } } +TEST(DecodeTest, ProcessEmptyInputWithBoxes) { + size_t xsize = 123, ysize = 77; + std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + jxl::CompressParams cparams; + uint32_t channels = 3; + JxlPixelFormat format = {channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + for (int i = 0; i < kCSBF_NUM_ENTRIES; ++i) { + JxlDecoder* dec = JxlDecoderCreate(NULL); + jxl::TestCodestreamParams params; + params.box_format = (CodeStreamBoxFormat)i; + printf("Testing empty input with box format %d\n", (int)params.box_format); + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, + params); + const int events = + JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE | JXL_DEC_COLOR_ENCODING; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec, events)); + EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput(dec, compressed.data(), compressed.size())); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + const size_t remaining = JxlDecoderReleaseInput(dec); + EXPECT_LE(remaining, compressed.size()); + EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, JxlDecoderProcessInput(dec)); + JxlDecoderDestroy(dec); + } +} + +TEST(DecodeTest, ExtraBytesAfterCompressedStream) { + size_t xsize = 123, ysize = 77; + size_t num_pixels = xsize * ysize; + std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + jxl::CompressParams cparams; + for (int i = 0; i < kCSBF_NUM_ENTRIES; ++i) { + CodeStreamBoxFormat box_format = (CodeStreamBoxFormat)i; + if (box_format == kCSBF_Multi_Other_Zero_Terminated) continue; + printf("Testing with box format %d\n", (int)box_format); + size_t last_unknown_box_size = 0; + if (box_format == kCSBF_Single_Other) { + last_unknown_box_size = unk1_box_size + 8; + } else if (box_format == kCSBF_Multi_Other_Terminated) { + last_unknown_box_size = unk3_box_size + 8; + } else if (box_format == kCSBF_Multi_Last_Empty_Other) { + // If boxes are not required, the decoder wont consume the last empty + // jxlp box. + last_unknown_box_size = 12 + unk3_box_size + 8; + } + jxl::TestCodestreamParams params; + params.box_format = box_format; + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, + params); + // Add some more bytes after compressed data. + compressed.push_back(0); + compressed.push_back(1); + compressed.push_back(2); + JxlDecoder* dec = JxlDecoderCreate(NULL); + uint32_t channels = 3; + JxlPixelFormat format = {channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), + format, /*use_callback=*/false, /*set_buffer_early=*/true, + /*use_resizable_runner=*/false, /*require_boxes=*/false, + /*expect_success=*/true); + size_t unconsumed_bytes = JxlDecoderReleaseInput(dec); + EXPECT_EQ(last_unknown_box_size + 3, unconsumed_bytes); + EXPECT_EQ(num_pixels * channels * 4, pixels2.size()); + JxlDecoderDestroy(dec); + } +} + +TEST(DecodeTest, ExtraBytesAfterCompressedStreamRequireBoxes) { + size_t xsize = 123, ysize = 77; + size_t num_pixels = xsize * ysize; + std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + jxl::CompressParams cparams; + for (int i = 0; i < kCSBF_NUM_ENTRIES; ++i) { + CodeStreamBoxFormat box_format = (CodeStreamBoxFormat)i; + if (box_format == kCSBF_Multi_Other_Zero_Terminated) continue; + printf("Testing with box format %d\n", (int)box_format); + bool expect_success = (box_format == kCSBF_None || + box_format == kCSBF_Single_Zero_Terminated || + box_format == kCSBF_Multi_Zero_Terminated); + jxl::TestCodestreamParams params; + params.box_format = box_format; + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, + params); + // Add some more bytes after compressed data. + compressed.push_back(0); + compressed.push_back(1); + compressed.push_back(2); + JxlDecoder* dec = JxlDecoderCreate(NULL); + uint32_t channels = 3; + JxlPixelFormat format = {channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), + format, /*use_callback=*/false, /*set_buffer_early=*/true, + /*use_resizable_runner=*/false, /*require_boxes=*/true, expect_success); + size_t unconsumed_bytes = JxlDecoderReleaseInput(dec); + EXPECT_EQ(3, unconsumed_bytes); + EXPECT_EQ(num_pixels * channels * 4, pixels2.size()); + JxlDecoderDestroy(dec); + } +} + +TEST(DecodeTest, ConcatenatedCompressedStreams) { + size_t xsize = 123, ysize = 77; + size_t num_pixels = xsize * ysize; + std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + jxl::CompressParams cparams; + for (int i = 0; i < kCSBF_NUM_ENTRIES; ++i) { + CodeStreamBoxFormat first_box_format = (CodeStreamBoxFormat)i; + if (first_box_format == kCSBF_Multi_Other_Zero_Terminated) continue; + jxl::TestCodestreamParams params1; + params1.box_format = first_box_format; + jxl::PaddedBytes compressed1 = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, + params1); + for (int j = 0; j < kCSBF_NUM_ENTRIES; ++j) { + CodeStreamBoxFormat second_box_format = (CodeStreamBoxFormat)j; + if (second_box_format == kCSBF_Multi_Other_Zero_Terminated) continue; + printf("Testing with box format pair %d, %d\n", (int)first_box_format, + (int)second_box_format); + jxl::TestCodestreamParams params2; + params2.box_format = second_box_format; + jxl::PaddedBytes compressed2 = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + 3, params2); + jxl::PaddedBytes concat; + concat.append(compressed1); + concat.append(compressed2); + uint32_t channels = 3; + JxlPixelFormat format = {channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + size_t remaining = concat.size(); + for (int part = 0; part < 2; ++part) { + printf(" Decoding part %d\n", part + 1); + JxlDecoder* dec = JxlDecoderCreate(NULL); + size_t pos = concat.size() - remaining; + bool expect_success = + (part == 0 || second_box_format == kCSBF_None || + second_box_format == kCSBF_Single_Zero_Terminated || + second_box_format == kCSBF_Multi_Zero_Terminated); + std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span<const uint8_t>(concat.data() + pos, remaining), + format, /*use_callback=*/false, /*set_buffer_early=*/true, + /*use_resizable_runner=*/false, /*require_boxes=*/true, + expect_success); + EXPECT_EQ(num_pixels * channels * 4, pixels2.size()); + remaining = JxlDecoderReleaseInput(dec); + JxlDecoderDestroy(dec); + } + EXPECT_EQ(0, remaining); + } + } +} + void TestPartialStream(bool reconstructible_jpeg) { size_t xsize = 123, ysize = 77; uint32_t channels = 4; @@ -1693,12 +2121,12 @@ void TestPartialStream(bool reconstructible_jpeg) { std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, channels, 0); JxlPixelFormat format_orig = {channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; + jxl::TestCodestreamParams params; if (reconstructible_jpeg) { - cparams.color_transform = jxl::ColorTransform::kNone; + params.cparams.color_transform = jxl::ColorTransform::kNone; } else { // Lossless to verify pixels exactly after roundtrip. - cparams.SetLossless(); + params.cparams.SetLossless(); } std::vector<uint8_t> pixels2; @@ -1710,14 +2138,13 @@ void TestPartialStream(bool reconstructible_jpeg) { std::vector<jxl::PaddedBytes> codestreams(kCSBF_NUM_ENTRIES); std::vector<jxl::PaddedBytes> jpeg_codestreams(kCSBF_NUM_ENTRIES); for (size_t i = 0; i < kCSBF_NUM_ENTRIES; ++i) { - CodeStreamBoxFormat add_container = (CodeStreamBoxFormat)i; + params.box_format = (CodeStreamBoxFormat)i; + if (reconstructible_jpeg) { + params.jpeg_codestream = &jpeg_codestreams[i]; + } codestreams[i] = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, - channels, cparams, add_container, JXL_ORIENT_IDENTITY, - /*add_preview=*/true, - /*add_intrinsic_size=*/false, - /*add_icc_profile=*/false, - reconstructible_jpeg ? &jpeg_codestreams[i] : nullptr); + channels, params); } // Test multiple step sizes, to test different combinations of the streaming @@ -1877,11 +2304,11 @@ TEST(DecodeTest, PreviewTest) { size_t xsize = 77, ysize = 120; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); - jxl::CompressParams cparams; + jxl::TestCodestreamParams params; + params.add_preview = true; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 3, - cparams, kCSBF_Multi, JXL_ORIENT_IDENTITY, /*add_preview=*/true, - /*add_intrinsic_size=*/false); + params); JxlPixelFormat format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; @@ -1963,12 +2390,13 @@ TEST(DecodeTest, AlignTest) { std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); JxlPixelFormat format_orig = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; - cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. - cparams.speed_tier = jxl::SpeedTier::kThunder; + jxl::TestCodestreamParams params; + // Lossless to verify pixels exactly after roundtrip. + params.cparams.SetLossless(); + params.cparams.speed_tier = jxl::SpeedTier::kThunder; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 4, - cparams, kCSBF_None, JXL_ORIENT_IDENTITY, false, false); + params); size_t align = 17; JxlPixelFormat format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, align}; @@ -1979,7 +2407,8 @@ TEST(DecodeTest, AlignTest) { std::vector<uint8_t> pixels2 = jxl::DecodeWithAPI( jxl::Span<const uint8_t>(compressed.data(), compressed.size()), format, use_callback, /*set_buffer_early=*/false, - /*use_resizable_runner=*/false); + /*use_resizable_runner=*/false, /*require_boxes=*/false, + /*expect_succes=*/true); EXPECT_EQ(expected_line_bytes * ysize, pixels2.size()); EXPECT_EQ(0u, jxl::test::ComparePixels(pixels.data(), pixels2.data(), xsize, ysize, format_orig, format)); @@ -2015,7 +2444,7 @@ TEST(DecodeTest, AnimationTest) { jxl::Span<const uint8_t>(frames[i].data(), frames[i].size()), xsize, ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/3, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle, /*float_in=*/false, /*align=*/0)); bundle.duration = frame_durations[i]; io.frames.push_back(std::move(bundle)); @@ -2119,7 +2548,7 @@ TEST(DecodeTest, AnimationTestStreaming) { jxl::Span<const uint8_t>(frames[i].data(), frames[i].size()), xsize, ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/3, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle, /*float_in=*/false, /*align=*/0)); bundle.duration = frame_durations[i]; io.frames.push_back(std::move(bundle)); @@ -2231,12 +2660,13 @@ TEST(DecodeTest, ExtraChannelTest) { std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); JxlPixelFormat format_orig = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; - cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. - cparams.speed_tier = jxl::SpeedTier::kThunder; + jxl::TestCodestreamParams params; + // Lossless to verify pixels exactly after roundtrip. + params.cparams.SetLossless(); + params.cparams.speed_tier = jxl::SpeedTier::kThunder; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 4, - cparams, kCSBF_None, JXL_ORIENT_IDENTITY, false, false); + params); size_t align = 17; JxlPixelFormat format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, align}; @@ -2303,6 +2733,122 @@ TEST(DecodeTest, ExtraChannelTest) { format_orig_alpha, format_alpha)); } +TEST(DecodeTest, SkipCurrentFrameTest) { + size_t xsize = 90, ysize = 120; + constexpr size_t num_frames = 7; + std::vector<uint8_t> frames[num_frames]; + for (size_t i = 0; i < num_frames; i++) { + frames[i] = jxl::test::GetSomeTestImage(xsize, ysize, 3, i); + } + JxlPixelFormat format = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + jxl::CodecInOut io; + io.SetSize(xsize, ysize); + io.metadata.m.SetUintSamples(16); + io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB(false); + io.metadata.m.have_animation = true; + io.frames.clear(); + io.frames.reserve(num_frames); + io.SetSize(xsize, ysize); + + std::vector<uint32_t> frame_durations(num_frames); + for (size_t i = 0; i < num_frames; ++i) { + frame_durations[i] = 5 + i; + } + + for (size_t i = 0; i < num_frames; ++i) { + jxl::ImageBundle bundle(&io.metadata.m); + if (i & 1) { + // Mark some frames as referenceable, others not. + bundle.use_for_next_frame = true; + } + + EXPECT_TRUE(ConvertFromExternal( + jxl::Span<const uint8_t>(frames[i].data(), frames[i].size()), xsize, + ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/3, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle, + /*float_in=*/false, /*align=*/0)); + bundle.duration = frame_durations[i]; + io.frames.push_back(std::move(bundle)); + } + + jxl::CompressParams cparams; + cparams.speed_tier = jxl::SpeedTier::kThunder; + jxl::AuxOut aux_out; + jxl::PaddedBytes compressed; + jxl::PassesEncoderState enc_state; + jxl::PassDefinition passes[] = { + {2, 0, false, 4}, {4, 0, false, 4}, {8, 2, false, 2}, {8, 0, false, 1}}; + jxl::ProgressiveMode progressive_mode{passes}; + enc_state.progressive_splitter.SetProgressiveMode(progressive_mode); + EXPECT_TRUE(jxl::EncodeFile(cparams, &io, &enc_state, &compressed, + jxl::GetJxlCms(), &aux_out, nullptr)); + + JxlDecoder* dec = JxlDecoderCreate(NULL); + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | + JXL_DEC_FRAME_PROGRESSION | + JXL_DEC_FULL_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetProgressiveDetail(dec, kLastPasses)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + + for (size_t i = 0; i < num_frames; ++i) { + printf("Decoding frame %d\n", (int)i); + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderSkipCurrentFrame(dec)); + std::vector<uint8_t> pixels(buffer_size); + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderSkipCurrentFrame(dec)); + JxlFrameHeader frame_header; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec, &frame_header)); + EXPECT_EQ(frame_durations[i], frame_header.duration); + EXPECT_EQ(i + 1 == num_frames, frame_header.is_last); + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels.data(), pixels.size())); + if (i == 2) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSkipCurrentFrame(dec)); + continue; + } + EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, JxlDecoderProcessInput(dec)); + EXPECT_EQ(8, JxlDecoderGetIntendedDownsamplingRatio(dec)); + if (i == 3) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSkipCurrentFrame(dec)); + continue; + } + EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, JxlDecoderProcessInput(dec)); + EXPECT_EQ(4, JxlDecoderGetIntendedDownsamplingRatio(dec)); + if (i == 4) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSkipCurrentFrame(dec)); + continue; + } + EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, JxlDecoderProcessInput(dec)); + EXPECT_EQ(2, JxlDecoderGetIntendedDownsamplingRatio(dec)); + if (i == 5) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSkipCurrentFrame(dec)); + continue; + } + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderSkipCurrentFrame(dec)); + } + + // After all frames were decoded, JxlDecoderProcessInput should return + // success to indicate all is done. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + + JxlDecoderDestroy(dec); +} + TEST(DecodeTest, SkipFrameTest) { size_t xsize = 90, ysize = 120; constexpr size_t num_frames = 16; @@ -2337,7 +2883,7 @@ TEST(DecodeTest, SkipFrameTest) { jxl::Span<const uint8_t>(frames[i].data(), frames[i].size()), xsize, ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/3, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle, /*float_in=*/false, /*align=*/0)); bundle.duration = frame_durations[i]; io.frames.push_back(std::move(bundle)); @@ -2474,8 +3020,8 @@ TEST(DecodeTest, SkipFrameWithBlendingTest) { xsize, ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/3, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, - &bundle_internal, /*float_in=*/false, /*align=*/0)); + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle_internal, + /*float_in=*/false, /*align=*/0)); bundle_internal.duration = 0; bundle_internal.use_for_next_frame = true; io.frames.push_back(std::move(bundle_internal)); @@ -2490,7 +3036,7 @@ TEST(DecodeTest, SkipFrameWithBlendingTest) { jxl::Span<const uint8_t>(frame.data(), frame.size()), xsize, ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/3, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle, /*float_in=*/false, /*align=*/0)); bundle.duration = frame_durations[i]; // Create some variation in which frames depend on which. @@ -2701,8 +3247,8 @@ TEST(DecodeTest, SkipFrameWithAlphaBlendingTest) { xsize / 2, ysize / 2, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/4, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, - &bundle_internal, /*float_in=*/false, /*align=*/0)); + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle_internal, + /*float_in=*/false, /*align=*/0)); bundle_internal.duration = 0; bundle_internal.use_for_next_frame = true; bundle_internal.origin = {13, 17}; @@ -2722,7 +3268,7 @@ TEST(DecodeTest, SkipFrameWithAlphaBlendingTest) { jxl::Span<const uint8_t>(frame.data(), frame.size()), cropxsize, cropysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/4, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle, /*float_in=*/false, /*align=*/0)); bundle.duration = 5 + i; frame_durations_nc.push_back(5 + i); @@ -2986,7 +3532,7 @@ TEST(DecodeTest, OrientedCroppedFrameTest) { cropysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*channels=*/4, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, - JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + JXL_BIG_ENDIAN, /*pool=*/nullptr, &bundle, /*float_in=*/false, /*align=*/0)); bundle.origin = {cropx0, cropy0}; bundle.use_for_next_frame = true; @@ -3101,6 +3647,561 @@ TEST(DecodeTest, OrientedCroppedFrameTest) { } } +struct FramePositions { + size_t frame_start; + size_t header_end; + size_t toc_end; + std::vector<size_t> section_end; +}; + +struct StreamPositions { + size_t codestream_start; + size_t codestream_end; + size_t basic_info; + size_t jbrd_end = 0; + std::vector<size_t> box_start; + std::vector<FramePositions> frames; +}; + +void AnalyzeCodestream(const jxl::PaddedBytes& data, + StreamPositions* streampos) { + // Unbox data to codestream and mark where it is broken up by boxes. + std::vector<uint8_t> codestream; + std::vector<std::pair<size_t, size_t>> breakpoints; + bool codestream_end = false; + ASSERT_LE(2, data.size()); + if (data[0] == 0xff && data[1] == 0x0a) { + codestream = std::vector<uint8_t>(data.begin(), data.end()); + streampos->codestream_start = 0; + } else { + const uint8_t* in = data.data(); + size_t pos = 0; + while (pos < data.size()) { + ASSERT_LE(pos + 8, data.size()); + streampos->box_start.push_back(pos); + size_t box_size = LoadBE32(in + pos); + if (box_size == 0) box_size = data.size() - pos; + ASSERT_LE(pos + box_size, data.size()); + if (memcmp(in + pos + 4, "jxlc", 4) == 0) { + EXPECT_TRUE(codestream.empty()); + streampos->codestream_start = pos + 8; + codestream.insert(codestream.end(), in + pos + 8, in + pos + box_size); + codestream_end = true; + } else if (memcmp(in + pos + 4, "jxlp", 4) == 0) { + codestream_end = (LoadBE32(in + pos + 8) & 0x80000000); + if (codestream.empty()) { + streampos->codestream_start = pos + 12; + } else if (box_size > 12 || !codestream_end) { + breakpoints.push_back({codestream.size(), 12}); + } + codestream.insert(codestream.end(), in + pos + 12, in + pos + box_size); + } else if (memcmp(in + pos + 4, "jbrd", 4) == 0) { + EXPECT_TRUE(codestream.empty()); + streampos->jbrd_end = pos + box_size; + } else if (!codestream.empty() && !codestream_end) { + breakpoints.push_back({codestream.size(), box_size}); + } + pos += box_size; + } + ASSERT_EQ(pos, data.size()); + } + // Translate codestream positions to boxed stream positions. + size_t offset = streampos->codestream_start; + size_t bp = 0; + auto add_offset = [&](size_t pos) { + while (bp < breakpoints.size() && pos >= breakpoints[bp].first) { + offset += breakpoints[bp++].second; + } + return pos + offset; + }; + // Analyze the unboxed codestream. + jxl::BitReader br( + jxl::Span<const uint8_t>(codestream.data(), codestream.size())); + ASSERT_EQ(br.ReadFixedBits<16>(), 0x0AFF); + jxl::CodecMetadata metadata; + EXPECT_TRUE(ReadSizeHeader(&br, &metadata.size)); + EXPECT_TRUE(ReadImageMetadata(&br, &metadata.m)); + streampos->basic_info = + add_offset(br.TotalBitsConsumed() / jxl::kBitsPerByte); + metadata.transform_data.nonserialized_xyb_encoded = metadata.m.xyb_encoded; + EXPECT_TRUE(jxl::Bundle::Read(&br, &metadata.transform_data)); + EXPECT_TRUE(br.JumpToByteBoundary()); + bool has_preview = metadata.m.have_preview; + while (br.TotalBitsConsumed() < br.TotalBytes() * jxl::kBitsPerByte) { + FramePositions p; + p.frame_start = add_offset(br.TotalBitsConsumed() / jxl::kBitsPerByte); + jxl::FrameHeader frame_header(&metadata); + if (has_preview) { + frame_header.nonserialized_is_preview = true; + has_preview = false; + } + EXPECT_TRUE(ReadFrameHeader(&br, &frame_header)); + p.header_end = + add_offset(jxl::DivCeil(br.TotalBitsConsumed(), jxl::kBitsPerByte)); + jxl::FrameDimensions frame_dim = frame_header.ToFrameDimensions(); + uint64_t groups_total_size; + const size_t toc_entries = jxl::NumTocEntries( + frame_dim.num_groups, frame_dim.num_dc_groups, + frame_header.passes.num_passes, /*has_ac_global=*/true); + std::vector<uint64_t> section_offsets; + std::vector<uint32_t> section_sizes; + EXPECT_TRUE(ReadGroupOffsets(toc_entries, &br, §ion_offsets, + §ion_sizes, &groups_total_size)); + EXPECT_EQ(br.TotalBitsConsumed() % jxl::kBitsPerByte, 0); + size_t sections_start = br.TotalBitsConsumed() / jxl::kBitsPerByte; + p.toc_end = add_offset(sections_start); + for (size_t i = 0; i < toc_entries; ++i) { + size_t end = sections_start + section_offsets[i] + section_sizes[i]; + p.section_end.push_back(add_offset(end)); + } + br.SkipBits(groups_total_size * jxl::kBitsPerByte); + streampos->frames.push_back(p); + } + streampos->codestream_end = add_offset(codestream.size()); + EXPECT_EQ(br.TotalBitsConsumed(), br.TotalBytes() * jxl::kBitsPerByte); + EXPECT_TRUE(br.Close()); +} + +enum ExpectedFlushState { NO_FLUSH, SAME_FLUSH, NEW_FLUSH }; +struct Breakpoint { + size_t file_pos; + ExpectedFlushState expect_flush; +}; + +void VerifyProgression(size_t xsize, size_t ysize, uint32_t num_channels, + const std::vector<uint8_t>& pixels, + const jxl::PaddedBytes& data, + std::vector<Breakpoint> breakpoints) { + // Size large enough for multiple groups, required to have progressive stages. + ASSERT_LT(256, xsize); + ASSERT_LT(256, ysize); + std::vector<uint8_t> pixels2; + pixels2.resize(pixels.size()); + JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + int bp = 0; + const uint8_t* next_in = data.data(); + size_t avail_in = breakpoints[bp].file_pos; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + double prev_dist = 1.0; + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + printf("bp: %d status: 0x%x\n", bp, (int)status); + if (status == JXL_DEC_BASIC_INFO) { + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(info.xsize, xsize); + EXPECT_EQ(info.ysize, ysize); + // Output buffer/callback not yet set + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderFlushImage(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + EXPECT_EQ(pixels2.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer(dec, &format, pixels2.data(), + pixels2.size())); + } else if (status == JXL_DEC_FRAME) { + // Nothing to do. + } else if (status == JXL_DEC_SUCCESS) { + EXPECT_EQ(bp + 1, breakpoints.size()); + break; + } else if (status == JXL_DEC_NEED_MORE_INPUT || + status == JXL_DEC_FULL_IMAGE) { + if (breakpoints[bp].expect_flush == NO_FLUSH) { + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderFlushImage(dec)); + } else { + if (status != JXL_DEC_FULL_IMAGE) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderFlushImage(dec)); + } + double dist = jxl::test::DistanceRMS(pixels2.data(), pixels.data(), + xsize, ysize, format); + if (breakpoints[bp].expect_flush == NEW_FLUSH) { + EXPECT_LT(dist, prev_dist); + prev_dist = dist; + } else { + EXPECT_EQ(dist, prev_dist); + } + } + if (status == JXL_DEC_FULL_IMAGE) { + EXPECT_EQ(bp + 1, breakpoints.size()); + continue; + } + ASSERT_LT(++bp, breakpoints.size()); + next_in += avail_in - JxlDecoderReleaseInput(dec); + avail_in = breakpoints[bp].file_pos - (next_in - data.data()); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + } else { + printf("Unexpected status: 0x%x\n", (int)status); + FAIL(); // unexpected returned status + } + } + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, ProgressionTest) { + size_t xsize = 508, ysize = 470; + uint32_t num_channels = 3; + std::vector<uint8_t> pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + jxl::TestCodestreamParams params; + params.cparams.progressive_dc = 1; + params.add_preview = true; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + num_channels, params); + StreamPositions streampos; + AnalyzeCodestream(data, &streampos); + const std::vector<FramePositions>& fp = streampos.frames; + // We have preview, dc frame and regular frame. + EXPECT_EQ(3, fp.size()); + EXPECT_EQ(7, fp[2].section_end.size()); + EXPECT_EQ(data.size(), fp[2].section_end[6]); + std::vector<Breakpoint> breakpoints{ + {fp[0].frame_start, NO_FLUSH}, // headers + {fp[1].frame_start, NO_FLUSH}, // preview + {fp[2].frame_start, NO_FLUSH}, // dc frame + {fp[2].section_end[0], NO_FLUSH}, // DC global + {fp[2].section_end[1] - 1, NO_FLUSH}, // partial DC group + {fp[2].section_end[1], NEW_FLUSH}, // DC group + {fp[2].section_end[2], SAME_FLUSH}, // AC global + {fp[2].section_end[3], NEW_FLUSH}, // AC group 0 + {fp[2].section_end[4] - 1, SAME_FLUSH}, // partial AC group 1 + {fp[2].section_end[4], NEW_FLUSH}, // AC group 1 + {fp[2].section_end[5], NEW_FLUSH}, // AC group 2 + {data.size() - 1, SAME_FLUSH}, // partial AC group 3 + {data.size(), NEW_FLUSH}}; // full image + VerifyProgression(xsize, ysize, num_channels, pixels, data, breakpoints); +} + +TEST(DecodeTest, ProgressionTestLosslessAlpha) { + size_t xsize = 508, ysize = 470; + uint32_t num_channels = 4; + std::vector<uint8_t> pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + jxl::TestCodestreamParams params; + params.cparams.SetLossless(); + params.cparams.speed_tier = jxl::SpeedTier::kThunder; + params.cparams.responsive = 1; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + num_channels, params); + StreamPositions streampos; + AnalyzeCodestream(data, &streampos); + const std::vector<FramePositions>& fp = streampos.frames; + // We have preview, dc frame and regular frame. + EXPECT_EQ(1, fp.size()); + EXPECT_EQ(7, fp[0].section_end.size()); + EXPECT_EQ(data.size(), fp[0].section_end[6]); + std::vector<Breakpoint> breakpoints{ + {fp[0].frame_start, NO_FLUSH}, // headers + {fp[0].section_end[0] - 1, NO_FLUSH}, // partial DC global + {fp[0].section_end[0], NEW_FLUSH}, // DC global + {fp[0].section_end[1], SAME_FLUSH}, // DC group + {fp[0].section_end[2], SAME_FLUSH}, // AC global + {fp[0].section_end[3], NEW_FLUSH}, // AC group 0 + {fp[0].section_end[4] - 1, SAME_FLUSH}, // partial AC group 1 + {fp[0].section_end[4], NEW_FLUSH}, // AC group 1 + {fp[0].section_end[5], NEW_FLUSH}, // AC group 2 + {data.size() - 1, SAME_FLUSH}, // partial AC group 3 + {data.size(), NEW_FLUSH}}; // full image + VerifyProgression(xsize, ysize, num_channels, pixels, data, breakpoints); +} + +void VerifyFilePosition(size_t expected_pos, const jxl::PaddedBytes& data, + JxlDecoder* dec) { + size_t remaining = JxlDecoderReleaseInput(dec); + size_t pos = data.size() - remaining; + EXPECT_EQ(expected_pos, pos); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput(dec, data.data() + pos, remaining)); +} + +TEST(DecodeTest, InputHandlingTestOneShot) { + size_t xsize = 508, ysize = 470; + uint32_t num_channels = 3; + std::vector<uint8_t> pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + for (int i = 0; i < kCSBF_NUM_ENTRIES; ++i) { + printf("Testing with box format %d\n", i); + jxl::TestCodestreamParams params; + params.cparams.progressive_dc = 1; + params.add_preview = true; + params.box_format = (CodeStreamBoxFormat)i; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + num_channels, params); + JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + StreamPositions streampos; + AnalyzeCodestream(data, &streampos); + const std::vector<FramePositions>& fp = streampos.frames; + // We have preview, dc frame and regular frame. + EXPECT_EQ(3, fp.size()); + + std::vector<uint8_t> pixels2; + pixels2.resize(pixels.size()); + + int kNumEvents = 6; + int events[] = { + JXL_DEC_BASIC_INFO, JXL_DEC_COLOR_ENCODING, JXL_DEC_PREVIEW_IMAGE, + JXL_DEC_FRAME, JXL_DEC_FULL_IMAGE, JXL_DEC_FRAME_PROGRESSION, + }; + size_t end_positions[] = { + streampos.basic_info, fp[0].frame_start, + fp[1].frame_start, fp[2].toc_end, + streampos.codestream_end, streampos.codestream_end}; + int events_wanted = 0; + for (int j = 0; j < kNumEvents; ++j) { + events_wanted |= events[j]; + size_t end_pos = end_positions[j]; + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec, events_wanted)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput(dec, data.data(), data.size())); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + VerifyFilePosition(streampos.basic_info, data, dec); + if (j >= 1) { + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[0].frame_start, data, dec); + } + if (j >= 2) { + EXPECT_EQ(JXL_DEC_NEED_PREVIEW_OUT_BUFFER, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[0].toc_end, data, dec); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderPreviewOutBufferSize(dec, &format, &buffer_size)); + EXPECT_GE(pixels2.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetPreviewOutBuffer(dec, &format, pixels2.data(), + buffer_size)); + EXPECT_EQ(JXL_DEC_PREVIEW_IMAGE, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[1].frame_start, data, dec); + } + if (j >= 3) { + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[2].toc_end, data, dec); + if (j >= 5) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetProgressiveDetail(dec, kDC)); + } + } + if (j >= 4) { + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[2].toc_end, data, dec); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + EXPECT_EQ(pixels2.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer(dec, &format, pixels2.data(), + pixels2.size())); + if (j >= 5) { + EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[2].section_end[1], data, dec); + } + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + VerifyFilePosition(streampos.codestream_end, data, dec); + } + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + VerifyFilePosition(end_pos, data, dec); + JxlDecoderDestroy(dec); + } + } +} + +#if JPEGXL_ENABLE_JPEG +TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(InputHandlingTestJPEGOneshot)) { + size_t xsize = 123; + size_t ysize = 77; + size_t channels = 3; + std::vector<uint8_t> pixels = + jxl::test::GetSomeTestImage(xsize, ysize, channels, /*seed=*/0); + for (int i = 1; i < kCSBF_NUM_ENTRIES; ++i) { + printf("Testing with box format %d\n", i); + jxl::PaddedBytes jpeg_codestream; + jxl::TestCodestreamParams params; + params.cparams.color_transform = jxl::ColorTransform::kNone; + params.jpeg_codestream = &jpeg_codestream; + params.add_preview = true; + params.box_format = (CodeStreamBoxFormat)i; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + channels, params); + JxlPixelFormat format = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + StreamPositions streampos; + AnalyzeCodestream(data, &streampos); + const std::vector<FramePositions>& fp = streampos.frames; + // We have preview and regular frame. + EXPECT_EQ(2, fp.size()); + EXPECT_LT(0, streampos.jbrd_end); + + std::vector<uint8_t> pixels2; + pixels2.resize(pixels.size()); + + int kNumEvents = 6; + int events[] = {JXL_DEC_BASIC_INFO, JXL_DEC_JPEG_RECONSTRUCTION, + JXL_DEC_COLOR_ENCODING, JXL_DEC_PREVIEW_IMAGE, + JXL_DEC_FRAME, JXL_DEC_FULL_IMAGE}; + size_t end_positions[] = {streampos.basic_info, streampos.basic_info, + fp[0].frame_start, fp[1].frame_start, + fp[1].toc_end, streampos.codestream_end}; + int events_wanted = 0; + for (int j = 0; j < kNumEvents; ++j) { + printf("j = %d\n", j); + events_wanted |= events[j]; + size_t end_pos = end_positions[j]; + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec, events_wanted)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput(dec, data.data(), data.size())); + if (j >= 1) { + EXPECT_EQ(JXL_DEC_JPEG_RECONSTRUCTION, JxlDecoderProcessInput(dec)); + VerifyFilePosition(streampos.jbrd_end, data, dec); + } + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + VerifyFilePosition(streampos.basic_info, data, dec); + if (j >= 2) { + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[0].frame_start, data, dec); + } + if (j >= 3) { + EXPECT_EQ(JXL_DEC_NEED_PREVIEW_OUT_BUFFER, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[0].toc_end, data, dec); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderPreviewOutBufferSize(dec, &format, &buffer_size)); + EXPECT_GE(pixels2.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetPreviewOutBuffer(dec, &format, pixels2.data(), + buffer_size)); + EXPECT_EQ(JXL_DEC_PREVIEW_IMAGE, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[1].frame_start, data, dec); + } + if (j >= 4) { + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[1].toc_end, data, dec); + } + if (j >= 5) { + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + VerifyFilePosition(fp[1].toc_end, data, dec); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + EXPECT_EQ(pixels2.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer(dec, &format, pixels2.data(), + pixels2.size())); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + VerifyFilePosition(streampos.codestream_end, data, dec); + } + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + VerifyFilePosition(end_pos, data, dec); + JxlDecoderDestroy(dec); + } + } +} +#endif // JPEGXL_ENABLE_JPEG + +TEST(DecodeTest, InputHandlingTestStreaming) { + size_t xsize = 508, ysize = 470; + uint32_t num_channels = 3; + std::vector<uint8_t> pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + for (int i = 0; i < kCSBF_NUM_ENTRIES; ++i) { + printf("Testing with box format %d\n", i); + fflush(stdout); + jxl::TestCodestreamParams params; + params.cparams.progressive_dc = 1; + params.box_format = (CodeStreamBoxFormat)i; + params.add_preview = true; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + num_channels, params); + JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + StreamPositions streampos; + AnalyzeCodestream(data, &streampos); + const std::vector<FramePositions>& fp = streampos.frames; + // We have preview, dc frame and regular frame. + EXPECT_EQ(3, fp.size()); + std::vector<uint8_t> pixels2; + pixels2.resize(pixels.size()); + int events_wanted = + (JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_PREVIEW_IMAGE | + JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME_PROGRESSION | + JXL_DEC_BOX); + for (size_t increment : {1, 7, 27, 1024}) { + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec, events_wanted)); + size_t file_pos = 0; + size_t box_index = 0; + size_t avail_in = 0; + for (;;) { + const uint8_t* next_in = data.data() + file_pos; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + size_t remaining = JxlDecoderReleaseInput(dec); + size_t consumed = avail_in - remaining; + file_pos += consumed; + avail_in += increment; + avail_in = std::min<size_t>(avail_in, data.size() - file_pos); + if (status == JXL_DEC_BASIC_INFO) { + EXPECT_EQ(file_pos, streampos.basic_info); + } else if (status == JXL_DEC_COLOR_ENCODING) { + EXPECT_EQ(file_pos, streampos.frames[0].frame_start); + } else if (status == JXL_DEC_NEED_PREVIEW_OUT_BUFFER) { + EXPECT_EQ(file_pos, streampos.frames[0].toc_end); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderPreviewOutBufferSize(dec, &format, &buffer_size)); + EXPECT_GE(pixels2.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetPreviewOutBuffer(dec, &format, pixels2.data(), + buffer_size)); + } else if (status == JXL_DEC_PREVIEW_IMAGE) { + EXPECT_EQ(file_pos, streampos.frames[1].frame_start); + } else if (status == JXL_DEC_FRAME) { + EXPECT_EQ(file_pos, streampos.frames[2].toc_end); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetProgressiveDetail(dec, kDC)); + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + EXPECT_EQ(file_pos, streampos.frames[2].toc_end); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + EXPECT_EQ(pixels2.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer(dec, &format, pixels2.data(), + pixels2.size())); + } else if (status == JXL_DEC_FRAME_PROGRESSION) { + EXPECT_EQ(file_pos, streampos.frames[2].section_end[1]); + } else if (status == JXL_DEC_FULL_IMAGE) { + EXPECT_EQ(file_pos, streampos.codestream_end); + } else if (status == JXL_DEC_SUCCESS) { + EXPECT_EQ(file_pos, streampos.codestream_end); + break; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + EXPECT_LT(remaining, 12); + if ((i == kCSBF_None && file_pos >= 2) || + (box_index > 0 && box_index < streampos.box_start.size() && + file_pos >= streampos.box_start[box_index - 1] + 12 && + file_pos < streampos.box_start[box_index])) { + EXPECT_EQ(remaining, 0); + } + if (file_pos == data.size()) break; + } else if (status == JXL_DEC_BOX) { + ASSERT_LT(box_index, streampos.box_start.size()); + EXPECT_EQ(file_pos, streampos.box_start[box_index++]); + } else { + printf("Unexpected status: 0x%x\n", (int)status); + FAIL(); + } + } + JxlDecoderDestroy(dec); + } + } +} + TEST(DecodeTest, FlushTest) { // Size large enough for multiple groups, required to have progressive // stages @@ -3108,10 +4209,11 @@ TEST(DecodeTest, FlushTest) { uint32_t num_channels = 3; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); - jxl::CompressParams cparams; + jxl::TestCodestreamParams params; + params.add_preview = true; jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, - num_channels, cparams, kCSBF_None, JXL_ORIENT_IDENTITY, true, false); + num_channels, params); JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector<uint8_t> pixels2; @@ -3175,6 +4277,92 @@ TEST(DecodeTest, FlushTest) { JxlDecoderDestroy(dec); } +TEST(DecodeTest, FlushTestImageOutCallback) { + // Size large enough for multiple groups, required to have progressive + // stages + size_t xsize = 333, ysize = 300; + uint32_t num_channels = 3; + std::vector<uint8_t> pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + jxl::TestCodestreamParams params; + params.add_preview = true; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + num_channels, params); + JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + std::vector<uint8_t> pixels2; + pixels2.resize(pixels.size()); + + size_t bytes_per_pixel = format.num_channels * 2; + size_t stride = bytes_per_pixel * xsize; + auto callback = [&](size_t x, size_t y, size_t num_pixels, + const void* pixels_row) { + memcpy(pixels2.data() + stride * y + bytes_per_pixel * x, pixels_row, + num_pixels * bytes_per_pixel); + }; + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + + // Ensure that the first part contains at least the full DC of the image, + // otherwise flush does not work. + size_t first_part = data.size() - 1; + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, data.data(), first_part)); + + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(info.xsize, xsize); + EXPECT_EQ(info.ysize, ysize); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + + // Output callback not yet set + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderFlushImage(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutCallback( + dec, &format, + [](void* opaque, size_t x, size_t y, + size_t xsize, const void* pixels_row) { + auto cb = + static_cast<decltype(&callback)>(opaque); + (*cb)(x, y, xsize, pixels_row); + }, + /*opaque=*/&callback)); + + // Must process input further until we get JXL_DEC_NEED_MORE_INPUT, even if + // data was already input before, since the processing of the frame only + // happens at the JxlDecoderProcessInput call after JXL_DEC_FRAME. + EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderFlushImage(dec)); + + // Crude test of actual pixel data: pixel threshold of about 4% (2560/65535). + // 29000 pixels can be above the threshold + EXPECT_LE(jxl::test::ComparePixels(pixels2.data(), pixels.data(), xsize, + ysize, format, format, 2560.0), + 29000u); + + EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, JxlDecoderProcessInput(dec)); + + size_t consumed = first_part - JxlDecoderReleaseInput(dec); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, data.data() + consumed, + data.size() - consumed)); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + // Lower threshold for the final (still lossy) image + EXPECT_LE(jxl::test::ComparePixels(pixels2.data(), pixels.data(), xsize, + ysize, format, format, 2560.0), + 11000u); + + JxlDecoderDestroy(dec); +} + TEST(DecodeTest, FlushTestLossyProgressiveAlpha) { // Size large enough for multiple groups, required to have progressive // stages @@ -3182,10 +4370,11 @@ TEST(DecodeTest, FlushTestLossyProgressiveAlpha) { uint32_t num_channels = 4; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); - jxl::CompressParams cparams; + jxl::TestCodestreamParams params; + params.add_preview = true; jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, - num_channels, cparams, kCSBF_None, JXL_ORIENT_IDENTITY, true, false); + num_channels, params); JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector<uint8_t> pixels2; @@ -3251,12 +4440,13 @@ TEST(DecodeTest, FlushTestLossyProgressiveAlphaUpsampling) { uint32_t num_channels = 4; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); - jxl::CompressParams cparams; - cparams.resampling = 2; - cparams.ec_resampling = 4; + jxl::TestCodestreamParams params; + params.cparams.resampling = 2; + params.cparams.ec_resampling = 4; + params.add_preview = true; jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, - num_channels, cparams, kCSBF_None, JXL_ORIENT_IDENTITY, true, false); + num_channels, params); JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector<uint8_t> pixels2; @@ -3324,13 +4514,14 @@ TEST(DecodeTest, FlushTestLosslessProgressiveAlpha) { uint32_t num_channels = 4; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); - jxl::CompressParams cparams; - cparams.SetLossless(); - cparams.speed_tier = jxl::SpeedTier::kThunder; - cparams.responsive = 1; + jxl::TestCodestreamParams params; + params.cparams.SetLossless(); + params.cparams.speed_tier = jxl::SpeedTier::kThunder; + params.cparams.responsive = 1; + params.add_preview = true; jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, - num_channels, cparams, kCSBF_None, JXL_ORIENT_IDENTITY, true, false); + num_channels, params); JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector<uint8_t> pixels2; @@ -3392,123 +4583,179 @@ TEST(DecodeTest, FlushTestLosslessProgressiveAlpha) { JxlDecoderDestroy(dec); } -TEST(DecodeTest, ProgressiveEventTest) { - for (int single_group = 0; single_group <= 1; ++single_group) { - for (int lossless = 0; lossless <= 1; ++lossless) { - for (uint32_t num_channels = 3; num_channels < 4; ++num_channels) { - // Only few combinations are expected to support outputting the DC. The - // test can be updated if more cases are expected to support it. - bool expect_dc = !single_group && (num_channels & 1) && !lossless; - size_t xsize, ysize; - if (single_group) { - // An image smaller than 256x256 ensures it contains only 1 group. - xsize = 99; - ysize = 100; - } else { - xsize = 257; - ysize = 280; - } - std::vector<uint8_t> pixels = - jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); - jxl::CompressParams cparams; - if (lossless) { - cparams.SetLossless(); - } else { - cparams.butteraugli_distance = 0.5f; - } - - jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( - jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, - ysize, num_channels, cparams, kCSBF_None, JXL_ORIENT_IDENTITY, - false, false); - JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, - 0}; +class DecodeProgressiveTest : public ::testing::TestWithParam<int> {}; +JXL_GTEST_INSTANTIATE_TEST_SUITE_P(DecodeProgressiveTestInstantiation, + DecodeProgressiveTest, + ::testing::Range(0, 8)); +TEST_P(DecodeProgressiveTest, ProgressiveEventTest) { + const int params = GetParam(); + int single_group = params & 1; + int lossless = (params >> 1) & 1; + uint32_t num_channels = 3 + ((params >> 2) & 1); + std::set<JxlProgressiveDetail> progressive_details = {kDC, kLastPasses, + kPasses}; + for (auto prog_detail : progressive_details) { + // Only few combinations are expected to support outputting + // intermediate flushes for complete DC and complete passes. + // The test can be updated if more cases are expected to support it. + bool expect_flush = (num_channels & 1) && !lossless; + size_t xsize, ysize; + if (single_group) { + // An image smaller than 256x256 ensures it contains only 1 group. + xsize = 99; + ysize = 100; + } else { + xsize = 277; + ysize = 280; + } + std::vector<uint8_t> pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + jxl::ColorEncoding color_encoding = jxl::ColorEncoding::SRGB(false); + jxl::CodecInOut io; + EXPECT_TRUE(jxl::ConvertFromExternal( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + color_encoding, num_channels, + /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/16, JXL_BIG_ENDIAN, + /*pool=*/nullptr, &io.Main(), /*float_in=*/false, /*align=*/0)); + jxl::TestCodestreamParams params; + if (lossless) { + params.cparams.SetLossless(); + } else { + params.cparams.butteraugli_distance = 0.5f; + } + jxl::PassDefinition passes[] = {{2, 0, false, 4}, + {4, 0, false, 4}, + {8, 2, false, 2}, + {8, 1, false, 2}, + {8, 0, false, 1}}; + const int kNumPasses = 5; + jxl::ProgressiveMode progressive_mode{passes}; + params.progressive_mode = &progressive_mode; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, + num_channels, params); + JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + for (size_t increment : {(size_t)1, data.size()}) { + printf( + "Testing with single_group=%d, lossless=%d, " + "num_channels=%d, prog_detail=%d, increment=%d\n", + single_group, lossless, (int)num_channels, (int)prog_detail, + (int)increment); + std::vector<std::vector<uint8_t>> passes(kNumPasses + 1); + for (int i = 0; i <= kNumPasses; ++i) { + passes[i].resize(pixels.size()); + } - std::vector<uint8_t> pixels2; - pixels2.resize(pixels.size()); - std::vector<uint8_t> dc; - dc.resize(pixels.size()); + JxlDecoder* dec = JxlDecoderCreate(nullptr); - JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | + JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME_PROGRESSION)); + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderSetProgressiveDetail(dec, kFrames)); + EXPECT_EQ(JXL_DEC_ERROR, + JxlDecoderSetProgressiveDetail(dec, kDCProgressive)); + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderSetProgressiveDetail(dec, kDCGroups)); + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderSetProgressiveDetail(dec, kGroups)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetProgressiveDetail(dec, prog_detail)); - EXPECT_EQ(JXL_DEC_SUCCESS, - JxlDecoderSubscribeEvents( - dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | - JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME_PROGRESSION)); + uint8_t* next_in = data.data(); + size_t avail_in = 0; + size_t pos = 0; - EXPECT_EQ(JXL_DEC_SUCCESS, - JxlDecoderSetInput(dec, data.data(), data.size())); + auto process_input = [&]() { + for (;;) { + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput(dec, next_in, avail_in)); + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + size_t remaining = JxlDecoderReleaseInput(dec); + EXPECT_LE(remaining, avail_in); + next_in += avail_in - remaining; + avail_in = remaining; + if (status == JXL_DEC_NEED_MORE_INPUT && pos < data.size()) { + size_t chunk = std::min<size_t>(increment, data.size() - pos); + pos += chunk; + avail_in += chunk; + continue; + } + return status; + } + }; - EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); - JxlBasicInfo info; - EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); - EXPECT_EQ(info.xsize, xsize); - EXPECT_EQ(info.ysize, ysize); + EXPECT_EQ(JXL_DEC_BASIC_INFO, process_input()); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(info.xsize, xsize); + EXPECT_EQ(info.ysize, ysize); - EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_FRAME, process_input()); - size_t buffer_size; - EXPECT_EQ(JXL_DEC_SUCCESS, - JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); - EXPECT_EQ(pixels2.size(), buffer_size); - EXPECT_EQ(JXL_DEC_SUCCESS, - JxlDecoderSetImageOutBuffer(dec, &format, pixels2.data(), - pixels2.size())); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + EXPECT_EQ(pixels.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, passes[kNumPasses].data(), + passes[kNumPasses].size())); - if (expect_dc) { - EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, JxlDecoderProcessInput(dec)); + auto next_pass = [&](int pass) { + if (prog_detail <= kDC) return kNumPasses; + if (prog_detail <= kLastPasses) { + return std::min(pass + 2, kNumPasses); + } + return pass + 1; + }; + + if (expect_flush) { + // Return a particular downsampling ratio only after the last + // pass for that downsampling was processed. + int expected_downsampling_ratios[] = {8, 8, 4, 4, 2}; + for (int p = 0; p < kNumPasses; p = next_pass(p)) { + EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, process_input()); + EXPECT_EQ(expected_downsampling_ratios[p], + JxlDecoderGetIntendedDownsamplingRatio(dec)); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderFlushImage(dec)); - dc = pixels2; + passes[p] = passes[kNumPasses]; } + } - EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); - EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, process_input()); + EXPECT_EQ(JXL_DEC_SUCCESS, process_input()); - jxl::Image3F imagef_pixels(xsize, ysize); - jxl::Image3F imagef_pixels2(xsize, ysize); - jxl::Image3F imagef_dc(xsize, ysize); - for (size_t c = 0; c < 3; c++) { - for (size_t y = 0; y < ysize; y++) { - float* row_pixels = imagef_pixels.PlaneRow(c, y); - float* row_pixels2 = imagef_pixels2.PlaneRow(c, y); - float* row_dc = imagef_dc.PlaneRow(c, y); - for (size_t x = 0; x < xsize; x++) { - size_t pixel_index = (y * xsize + x) * num_channels * 2 + c * 2; - row_pixels[x] = pixels[pixel_index] / 255.0f; - row_pixels2[x] = pixels2[pixel_index] / 255.0f; - row_dc[x] = dc[pixel_index] / 255.0f; - } - } - } + JxlDecoderDestroy(dec); - jxl::CodecInOut io_pixels; - io_pixels.SetFromImage(std::move(imagef_pixels), - jxl::ColorEncoding::SRGB(false)); - jxl::CodecInOut io_pixels2; - io_pixels2.SetFromImage(std::move(imagef_pixels2), - jxl::ColorEncoding::SRGB(false)); - jxl::CodecInOut io_dc; - io_dc.SetFromImage(std::move(imagef_dc), - jxl::ColorEncoding::SRGB(false)); - - jxl::ButteraugliParams ba; - float distance_full = - ButteraugliDistance(io_pixels, io_pixels2, ba, jxl::GetJxlCms(), - /*distmap=*/nullptr, nullptr); - - if (expect_dc) { - float distance_dc = - ButteraugliDistance(io_pixels, io_dc, ba, jxl::GetJxlCms(), - /*distmap=*/nullptr, nullptr); - EXPECT_LT(distance_full, 2.0f); - EXPECT_LT(distance_dc, 30.0f); - // Verify that the returned DC image is actually DC, by checking that - // it has worse butteraugli score than the full image - EXPECT_LT(distance_full + 1.0f, distance_dc); - } else { - EXPECT_LT(distance_full, 2.0f); - } - JxlDecoderDestroy(dec); + if (!expect_flush) { + continue; + } + jxl::ButteraugliParams ba; + std::vector<float> distances(kNumPasses + 1); + for (int p = 0;; p = next_pass(p)) { + jxl::CodecInOut io1; + EXPECT_TRUE(jxl::ConvertFromExternal( + jxl::Span<const uint8_t>(passes[p].data(), passes[p].size()), xsize, + ysize, color_encoding, num_channels, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, + JXL_BIG_ENDIAN, + /*pool=*/nullptr, &io1.Main(), /*float_in=*/false, + /*align=*/0)); + distances[p] = ButteraugliDistance(io, io1, ba, jxl::GetJxlCms(), + nullptr, nullptr); + if (p == kNumPasses) break; + } + const float kMaxDistance[kNumPasses + 1] = {30.0f, 20.0f, 10.0f, + 5.0f, 3.0f, 2.0f}; + EXPECT_LT(distances[kNumPasses], kMaxDistance[kNumPasses]); + for (int p = 0; p < kNumPasses;) { + int next_p = next_pass(p); + EXPECT_LT(distances[p], kMaxDistance[p]); + // Verify that the returned pass image is actually not the + // same as the next pass image, by checking that it has a bit + // worse butteraugli score. + EXPECT_LT(distances[next_p] * 1.2f, distances[p]); + p = next_p; } } } @@ -3550,22 +4797,21 @@ TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructTestCodestream)) { size_t channels = 3; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, channels, /*seed=*/0); - jxl::CompressParams cparams; - cparams.color_transform = jxl::ColorTransform::kNone; jxl::PaddedBytes jpeg_codestream; + jxl::TestCodestreamParams params; + params.cparams.color_transform = jxl::ColorTransform::kNone; + params.box_format = kCSBF_Single; + params.jpeg_codestream = &jpeg_codestream; + params.add_preview = true; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, - channels, cparams, kCSBF_Single, JXL_ORIENT_IDENTITY, - /*add_preview=*/true, - /*add_intrinsic_size=*/false, - /*add_icc_profile=*/false, &jpeg_codestream); + channels, params); VerifyJPEGReconstruction(compressed, jpeg_codestream); } #endif // JPEGXL_ENABLE_JPEG TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) { - const std::string jpeg_path = - "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg"; const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); jxl::CodecInOut orig_io; ASSERT_TRUE( @@ -3608,12 +4854,14 @@ TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionMetadataTest)) { TEST(DecodeTest, ContinueFinalNonEssentialBoxTest) { size_t xsize = 80, ysize = 90; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); - jxl::CompressParams cparams; - // Lossless to verify pixels exactly after roundtrip. + jxl::TestCodestreamParams params; + params.box_format = kCSBF_Multi_Other_Terminated; + params.add_icc_profile = true; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 4, - cparams, kCSBF_Multi_Other_Terminated, JXL_ORIENT_IDENTITY, false, false, - true); + params); + StreamPositions streampos; + AnalyzeCodestream(compressed, &streampos); // The non-essential final box size including 8-byte header size_t final_box_size = unk3_box_size + 8; @@ -3645,8 +4893,8 @@ TEST(DecodeTest, ContinueFinalNonEssentialBoxTest) { size_t remaining = JxlDecoderReleaseInput(dec); // Since the test was set up to end exactly at the boundary of the final // codestream box, and the decoder returned success, all bytes are expected to - // be consumed. - EXPECT_EQ(0, remaining); + // be consumed until the end of the frame header. + EXPECT_EQ(remaining, last_box_begin - streampos.frames[0].toc_end); // Now set the remaining non-codestream box as input. EXPECT_EQ(JXL_DEC_SUCCESS, @@ -3668,20 +4916,48 @@ bool BoxTypeEquals(const std::string& type_string, JxlBoxType type) { } } // namespace +TEST(DecodeTest, ExtentedBoxSizeTest) { + const std::string jxl_path = "jxl/boxes/square-extended-size-container.jxl"; + const jxl::PaddedBytes orig = jxl::ReadTestData(jxl_path); + JxlDecoder* dec = JxlDecoderCreate(nullptr); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec, JXL_DEC_BOX)); + + JxlBoxType type; + uint64_t box_size; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, orig.data(), orig.size())); + EXPECT_EQ(JXL_DEC_BOX, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxType(dec, type, JXL_FALSE)); + EXPECT_TRUE(BoxTypeEquals("JXL ", type)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxSizeRaw(dec, &box_size)); + EXPECT_EQ(12, box_size); + EXPECT_EQ(JXL_DEC_BOX, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxType(dec, type, JXL_FALSE)); + EXPECT_TRUE(BoxTypeEquals("ftyp", type)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxSizeRaw(dec, &box_size)); + EXPECT_EQ(20, box_size); + EXPECT_EQ(JXL_DEC_BOX, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxType(dec, type, JXL_FALSE)); + EXPECT_TRUE(BoxTypeEquals("jxlc", type)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxSizeRaw(dec, &box_size)); + EXPECT_EQ(72, box_size); + + JxlDecoderDestroy(dec); +} + TEST(DecodeTest, BoxTest) { size_t xsize = 1, ysize = 1; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); - jxl::CompressParams cparams; + jxl::TestCodestreamParams params; + params.box_format = kCSBF_Multi_Other_Terminated; + params.add_icc_profile = true; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 4, - cparams, kCSBF_Multi_Other_Terminated, JXL_ORIENT_IDENTITY, false, false, - true); + params); JxlDecoder* dec = JxlDecoderCreate(nullptr); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec, JXL_DEC_BOX)); - EXPECT_EQ(JXL_DEC_SUCCESS, - JxlDecoderSetInput(dec, compressed.data(), compressed.size())); std::vector<std::string> expected_box_types = { "JXL ", "ftyp", "jxlp", "unk1", "unk2", "jxlp", "jxlp", "jxlp", "unk3"}; @@ -3699,7 +4975,10 @@ TEST(DecodeTest, BoxTest) { EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderGetBoxType(dec, type, JXL_FALSE)); EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderGetBoxSizeRaw(dec, &box_size)); + uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); for (size_t i = 0; i < expected_box_types.size(); i++) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); EXPECT_EQ(JXL_DEC_BOX, JxlDecoderProcessInput(dec)); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxType(dec, type, JXL_FALSE)); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxSizeRaw(dec, &box_size)); @@ -3720,10 +4999,18 @@ TEST(DecodeTest, BoxTest) { : (type[3] == '2' ? unk2_box_size : unk3_box_size); expected_release_size = contents.size() - expected_box_contents_size; } + size_t consumed = avail_in - JxlDecoderReleaseInput(dec); + next_in += consumed; + avail_in -= consumed; } + // After the last DEC_BOX event, check that the input position is exactly at + // the stat of the box header. + EXPECT_EQ(avail_in, expected_box_sizes.back()); + // Even though all input is given, the decoder cannot assume there aren't // more boxes if the input was not closed. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, JxlDecoderProcessInput(dec)); JxlDecoderCloseInput(dec); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); @@ -3734,11 +5021,14 @@ TEST(DecodeTest, BoxTest) { TEST(DecodeTest, ExifBrobBoxTest) { size_t xsize = 1, ysize = 1; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); - jxl::CompressParams cparams; - cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. + jxl::TestCodestreamParams params; + // Lossless to verify pixels exactly after roundtrip. + params.cparams.SetLossless(); + params.box_format = kCSBF_Brob_Exif; + params.add_icc_profile = true; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 4, - cparams, kCSBF_Brob_Exif, JXL_ORIENT_IDENTITY, false, false, true); + params); // Test raw brob box, not brotli-decompressing for (int streaming = 0; streaming < 2; ++streaming) { @@ -3914,12 +5204,15 @@ TEST(DecodeTest, PartialCodestreamBoxTest) { size_t xsize = 23, ysize = 81; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); JxlPixelFormat format_orig = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; - jxl::CompressParams cparams; - cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. - cparams.speed_tier = jxl::SpeedTier::kThunder; + // Lossless to verify pixels exactly after roundtrip. + jxl::TestCodestreamParams params; + params.cparams.SetLossless(); + params.cparams.speed_tier = jxl::SpeedTier::kThunder; + params.box_format = kCSBF_Multi; + params.add_icc_profile = true; jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( jxl::Span<const uint8_t>(pixels.data(), pixels.size()), xsize, ysize, 4, - cparams, kCSBF_Multi, JXL_ORIENT_IDENTITY, false, false, true); + params); std::vector<uint8_t> extracted_codestream; diff --git a/media/libjxl/src/lib/jxl/enc_ac_strategy.cc b/media/libjxl/src/lib/jxl/enc_ac_strategy.cc index 3ef9792428..a6de18fbd7 100644 --- a/media/libjxl/src/lib/jxl/enc_ac_strategy.cc +++ b/media/libjxl/src/lib/jxl/enc_ac_strategy.cc @@ -269,6 +269,14 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::AbsDiff; +using hwy::HWY_NAMESPACE::Eq; +using hwy::HWY_NAMESPACE::IfThenElseZero; +using hwy::HWY_NAMESPACE::IfThenZeroElse; +using hwy::HWY_NAMESPACE::Round; +using hwy::HWY_NAMESPACE::Sqrt; + bool MultiBlockTransformCrossesHorizontalBoundary( const AcStrategyImage& ac_strategy, size_t start_x, size_t y, size_t end_x) { @@ -409,25 +417,25 @@ float EstimateEntropy(const AcStrategy& acs, size_t x, size_t y, auto cost_delta = Set(df, config.cost_delta); for (size_t i = 0; i < num_blocks * kDCTBlockSize; i += Lanes(df)) { const auto in = Load(df, block + c * size + i); - const auto in_y = Load(df, block + size + i) * cmap_factor; + const auto in_y = Mul(Load(df, block + size + i), cmap_factor); const auto im = Load(df, inv_matrix + i); - const auto val = (in - in_y) * im * q; + const auto val = Mul(Sub(in, in_y), Mul(im, q)); const auto rval = Round(val); const auto diff = AbsDiff(val, rval); - info_loss += diff; - info_loss2 += diff * diff; + info_loss = Add(info_loss, diff); + info_loss2 = MulAdd(diff, diff, info_loss2); const auto q = Abs(rval); - const auto q_is_zero = q == Zero(df); - entropy_v += IfThenElseZero(q >= Set(df, 1.5f), cost2); + const auto q_is_zero = Eq(q, Zero(df)); + entropy_v = Add(entropy_v, IfThenElseZero(Ge(q, Set(df, 1.5f)), cost2)); // We used to have q * C here, but that cost model seems to // be punishing large values more than necessary. Sqrt tries // to avoid large values less aggressively. Having high accuracy // around zero is most important at low qualities, and there // we have directly specified costs for 0, 1, and 2. - entropy_v += Sqrt(q) * cost_delta; - nzeros_v += IfThenZeroElse(q_is_zero, Set(df, 1.0f)); + entropy_v = MulAdd(Sqrt(q), cost_delta, entropy_v); + nzeros_v = Add(nzeros_v, IfThenZeroElse(q_is_zero, Set(df, 1.0f))); } - entropy_v += nzeros_v * cost1; + entropy_v = MulAdd(nzeros_v, cost1, entropy_v); entropy += GetLane(SumOfLanes(df, entropy_v)); size_t num_nzeros = GetLane(SumOfLanes(df, nzeros_v)); diff --git a/media/libjxl/src/lib/jxl/enc_ac_strategy.h b/media/libjxl/src/lib/jxl/enc_ac_strategy.h index 6cf82d524c..409f18b891 100644 --- a/media/libjxl/src/lib/jxl/enc_ac_strategy.h +++ b/media/libjxl/src/lib/jxl/enc_ac_strategy.h @@ -56,10 +56,6 @@ struct ACSConfig { JXL_DASSERT(quant_field_row[by * quant_field_stride + bx] > 0); return quant_field_row[by * quant_field_stride + bx]; } - void SetQuant(size_t bx, size_t by, float value) const { - JXL_DASSERT(value > 0); - quant_field_row[by * quant_field_stride + bx] = value; - } }; struct AcStrategyHeuristics { diff --git a/media/libjxl/src/lib/jxl/enc_adaptive_quantization.cc b/media/libjxl/src/lib/jxl/enc_adaptive_quantization.cc index 2d0e31b35f..4d245b41d3 100644 --- a/media/libjxl/src/lib/jxl/enc_adaptive_quantization.cc +++ b/media/libjxl/src/lib/jxl/enc_adaptive_quantization.cc @@ -53,7 +53,13 @@ namespace HWY_NAMESPACE { namespace { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::AbsDiff; +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::And; +using hwy::HWY_NAMESPACE::Max; using hwy::HWY_NAMESPACE::Rebind; +using hwy::HWY_NAMESPACE::Sqrt; +using hwy::HWY_NAMESPACE::ZeroIfNegative; // The following functions modulate an exponent (out_val) and return the updated // value. Their descriptor is limited to 8 lanes for 8x8 blocks. @@ -74,21 +80,21 @@ V ComputeMask(const D d, const V out_val) { const auto kOffset2 = Set(d, 305.04035728311436f); const auto kMul3 = Set(d, 5.0220313103171232f); const auto kOffset3 = Set(d, 2.1925739705298404f); - const auto kOffset4 = Set(d, 0.25f) * kOffset3; + const auto kOffset4 = Mul(Set(d, 0.25f), kOffset3); const auto kMul0 = Set(d, 0.74760422233706747f); const auto k1 = Set(d, 1.0f); // Avoid division by zero. - const auto v1 = Max(out_val * kMul0, Set(d, 1e-3f)); - const auto v2 = k1 / (v1 + kOffset2); - const auto v3 = k1 / MulAdd(v1, v1, kOffset3); - const auto v4 = k1 / MulAdd(v1, v1, kOffset4); + const auto v1 = Max(Mul(out_val, kMul0), Set(d, 1e-3f)); + const auto v2 = Div(k1, Add(v1, kOffset2)); + const auto v3 = Div(k1, MulAdd(v1, v1, kOffset3)); + const auto v4 = Div(k1, MulAdd(v1, v1, kOffset4)); // TODO(jyrki): // A log or two here could make sense. In butteraugli we have effectively // log(log(x + C)) for this kind of use, as a single log is used in // saturating visual masking and here the modulation values are exponential, // another log would counter that. - return kBase + MulAdd(kMul4, v4, MulAdd(kMul2, v2, kMul3 * v3)); + return Add(kBase, MulAdd(kMul4, v4, MulAdd(kMul2, v2, Mul(kMul3, v3)))); } // For converting full vectors to a subset. Assumes `vfull` lanes are identical. @@ -123,11 +129,11 @@ V RatioOfDerivativesOfCubicRootToSimpleGamma(const D d, V v) { const auto kVOffset = Set(d, kSGVOffset * kLog2 + kEpsilon); const auto kDenMul = Set(d, kLog2 * kSGmul); - const auto v2 = v * v; + const auto v2 = Mul(v, v); const auto num = MulAdd(kNumMul, v2, Set(d, kEpsilon)); - const auto den = MulAdd(kDenMul * v, v2, kVOffset); - return invert ? num / den : den / num; + const auto den = MulAdd(Mul(kDenMul, v), v2, kVOffset); + return invert ? Div(num, den) : Div(den, num); } template <bool invert = false> @@ -175,21 +181,20 @@ V GammaModulation(const D d, const size_t x, const size_t y, const float* const JXL_RESTRICT row_in_x = xyb_x.Row(y + dy); const float* const JXL_RESTRICT row_in_y = xyb_y.Row(y + dy); for (size_t dx = 0; dx < 8; dx += Lanes(d)) { - const auto iny = Load(d, row_in_y + x + dx) + bias; + const auto iny = Add(Load(d, row_in_y + x + dx), bias); const auto inx = Load(d, row_in_x + x + dx); - const auto r = iny - inx; - const auto g = iny + inx; + const auto r = Sub(iny, inx); + const auto g = Add(iny, inx); const auto ratio_r = RatioOfDerivativesOfCubicRootToSimpleGamma</*invert=*/true>(d, r); const auto ratio_g = RatioOfDerivativesOfCubicRootToSimpleGamma</*invert=*/true>(d, g); - const auto avg_ratio = half * (ratio_r + ratio_g); + const auto avg_ratio = Mul(half, Add(ratio_r, ratio_g)); - overall_ratio += avg_ratio; + overall_ratio = Add(overall_ratio, avg_ratio); } } - overall_ratio = SumOfLanes(d, overall_ratio); - overall_ratio *= Set(d, 1.0f / 64); + overall_ratio = Mul(SumOfLanes(d, overall_ratio), Set(d, 1.0f / 64)); // ideally -1.0, but likely optimal correction adds some entropy, so slightly // less than that. // ln(2) constant folded in because we want std::log but have FastLog2f. @@ -217,7 +222,7 @@ V ColorModulation(const D d, const size_t x, const size_t y, { // Reduce some bits from areas not blue or red. const float offset = strength * -0.009174542291185913f; - out_val += Set(d, offset); + out_val = Add(out_val, Set(d, offset)); } // Calculate how much of the 8x8 block is covered with blue or red. auto blue_coverage = Zero(d); @@ -227,16 +232,16 @@ V ColorModulation(const D d, const size_t x, const size_t y, const float* const JXL_RESTRICT row_in_y = xyb_y.Row(y + dy); const float* const JXL_RESTRICT row_in_b = xyb_b.Row(y + dy); for (size_t dx = 0; dx < 8; dx += Lanes(d)) { - const auto pixel_x = - Max(Set(d, 0.0f), Load(d, row_in_x + x + dx) - Set(d, kRedRampStart)); + const auto pixel_x = Max( + Set(d, 0.0f), Sub(Load(d, row_in_x + x + dx), Set(d, kRedRampStart))); const auto pixel_y = Load(d, row_in_y + x + dx); const auto pixel_b = - Max(Set(d, 0.0f), - Load(d, row_in_b + x + dx) - pixel_y - Set(d, kBlueRampStart)); + Max(Set(d, 0.0f), Sub(Load(d, row_in_b + x + dx), + Add(pixel_y, Set(d, kBlueRampStart)))); const auto blue_slope = Min(pixel_b, Set(d, kBlueRampLength)); const auto red_slope = Min(pixel_x, Set(d, kRedRampLength)); - red_coverage += red_slope; - blue_coverage += blue_slope; + red_coverage = Add(red_coverage, red_slope); + blue_coverage = Add(blue_coverage, blue_slope); } } @@ -248,14 +253,16 @@ V ColorModulation(const D d, const size_t x, const size_t y, auto overall_red_coverage = SumOfLanes(d, red_coverage); overall_red_coverage = Min(overall_red_coverage, Set(d, ratio * kRedRampLength)); - overall_red_coverage *= Set(d, red_strength / ratio); + overall_red_coverage = + Mul(overall_red_coverage, Set(d, red_strength / ratio)); auto overall_blue_coverage = SumOfLanes(d, blue_coverage); overall_blue_coverage = Min(overall_blue_coverage, Set(d, ratio * kBlueRampLength)); - overall_blue_coverage *= Set(d, blue_strength / ratio); + overall_blue_coverage = + Mul(overall_blue_coverage, Set(d, blue_strength / ratio)); - return overall_red_coverage + overall_blue_coverage + out_val; + return Add(overall_red_coverage, Add(overall_blue_coverage, out_val)); } // Change precision in 8x8 blocks that have high frequency content. @@ -287,10 +294,10 @@ V HfModulation(const D d, const size_t x, const size_t y, const ImageF& xyb, const auto p = Load(d, row_in + dx); const auto pr = LoadU(d, row_in + dx + 1); const auto mask = BitCast(d, Load(du, kMaskRight + dx)); - sum += And(mask, AbsDiff(p, pr)); + sum = Add(sum, And(mask, AbsDiff(p, pr))); const auto pd = Load(d, row_in_next + dx); - sum += AbsDiff(p, pd); + sum = Add(sum, AbsDiff(p, pd)); } } @@ -343,7 +350,7 @@ V MaskingSqrt(const D d, V v) { static const float kMul = 211.50759899638012f; const auto mul_v = Set(d, kMul * 1e8); const auto offset_v = Set(d, kLogOffset); - return Set(d, 0.25f) * Sqrt(MulAdd(v, Sqrt(mul_v), offset_v)); + return Mul(Set(d, 0.25f), Sqrt(MulAdd(v, Sqrt(mul_v), offset_v))); } float MaskingSqrt(const float v) { @@ -524,25 +531,26 @@ struct AdaptiveQuantizationImpl { const auto in_l = LoadU(df, row_in + x - 1); const auto in_t = LoadU(df, row_in2 + x); const auto in_b = LoadU(df, row_in1 + x); - auto base = quarter * (in_r + in_l + in_t + in_b); + auto base = Mul(quarter, Add(Add(in_r, in_l), Add(in_t, in_b))); auto gammacv = RatioOfDerivativesOfCubicRootToSimpleGamma</*invert=*/false>( - df, in + match_gamma_offset_v); - auto diff = gammacv * (in - base); - diff *= diff; + df, Add(in, match_gamma_offset_v)); + auto diff = Mul(gammacv, Sub(in, base)); + diff = Mul(diff, diff); const auto in_x = LoadU(df, row_x_in + x); const auto in_x_r = LoadU(df, row_x_in + x + 1); const auto in_x_l = LoadU(df, row_x_in + x - 1); const auto in_x_t = LoadU(df, row_x_in2 + x); const auto in_x_b = LoadU(df, row_x_in1 + x); - auto base_x = quarter * (in_x_r + in_x_l + in_x_t + in_x_b); - auto diff_x = gammacv * (in_x - base_x); - diff_x *= diff_x; - diff += kXMulv * diff_x; + auto base_x = + Mul(quarter, Add(Add(in_x_r, in_x_l), Add(in_x_t, in_x_b))); + auto diff_x = Mul(gammacv, Sub(in_x, base_x)); + diff_x = Mul(diff_x, diff_x); + diff = MulAdd(kXMulv, diff_x, diff); diff = MaskingSqrt(df, diff); if ((y & 3) != 0) { - diff += LoadU(df, row_out + x - x0); + diff = Add(diff, LoadU(df, row_out + x - x0)); } StoreU(diff, df, row_out + x - x0); } @@ -625,7 +633,6 @@ namespace jxl { HWY_EXPORT(AdaptiveQuantizationMap); namespace { -bool FLAGS_log_search_state = false; // If true, prints the quantization maps at each iteration. bool FLAGS_dump_quant_state = false; @@ -735,6 +742,13 @@ void FindBestQuantization(const ImageBundle& linear, const Image3F& opsin, const JxlCmsInterface& cms, ThreadPool* pool, AuxOut* aux_out) { const CompressParams& cparams = enc_state->cparams; + if (cparams.resampling > 1 && + cparams.original_butteraugli_distance <= 4.0 * cparams.resampling) { + // For downsampled opsin image, the butteraugli based adaptive quantization + // loop would only make the size bigger without improving the distance much, + // so in this case we enable it only for very high butteraugli targets. + return; + } Quantizer& quantizer = enc_state->shared.quantizer; ImageI& raw_quant_field = enc_state->shared.raw_quant_field; ImageF& quant_field = enc_state->initial_quant_field; @@ -761,6 +775,7 @@ void FindBestQuantization(const ImageBundle& linear, const Image3F& opsin, enc_state->shared.frame_header.nonserialized_metadata->ysize()); const float butteraugli_target = cparams.butteraugli_distance; + const float original_butteraugli = cparams.original_butteraugli_distance; ButteraugliParams params = cparams.ba_params; params.intensity_target = linear.metadata()->IntensityTarget(); // Hack the default intensity target value to be 80.0, the intensity @@ -818,18 +833,20 @@ void FindBestQuantization(const ImageBundle& linear, const Image3F& opsin, score = -score; diffmap = ScaleImage(-1.0f, diffmap); } - tile_distmap = TileDistMap(diffmap, 8, 0, enc_state->shared.ac_strategy); + tile_distmap = TileDistMap(diffmap, 8 * cparams.resampling, 0, + enc_state->shared.ac_strategy); if (WantDebugOutput(aux_out)) { aux_out->DumpImage(("dec" + ToString(i)).c_str(), *dec_linear.color()); DumpHeatmaps(aux_out, butteraugli_target, quant_field, tile_distmap, diffmap); } if (aux_out != nullptr) ++aux_out->num_butteraugli_iters; - if (FLAGS_log_search_state) { + if (cparams.log_search_state) { float minval, maxval; ImageMinMax(quant_field, &minval, &maxval); printf("\nButteraugli iter: %d/%d\n", i, cparams.max_butteraugli_iters); - printf("Butteraugli distance: %f\n", score); + printf("Butteraugli distance: %f (target = %f)\n", score, + original_butteraugli); printf("quant range: %f ... %f DC quant: %f\n", minval, maxval, initial_quant_dc); if (FLAGS_dump_quant_state) { @@ -867,7 +884,7 @@ void FindBestQuantization(const ImageBundle& linear, const Image3F& opsin, double cur_pow = 0.0; if (i < 7) { - cur_pow = kPow[i] + (butteraugli_target - 1.0) * kPowMod[i]; + cur_pow = kPow[i] + (original_butteraugli - 1.0) * kPowMod[i]; if (cur_pow < 0) { cur_pow = 0; } @@ -877,7 +894,7 @@ void FindBestQuantization(const ImageBundle& linear, const Image3F& opsin, const float* const JXL_RESTRICT row_dist = tile_distmap.Row(y); float* const JXL_RESTRICT row_q = quant_field.Row(y); for (size_t x = 0; x < quant_field.xsize(); ++x) { - const float diff = row_dist[x] / butteraugli_target; + const float diff = row_dist[x] / original_butteraugli; if (diff > 1.0f) { float old = row_q[x]; row_q[x] *= diff; @@ -896,7 +913,7 @@ void FindBestQuantization(const ImageBundle& linear, const Image3F& opsin, const float* const JXL_RESTRICT row_dist = tile_distmap.Row(y); float* const JXL_RESTRICT row_q = quant_field.Row(y); for (size_t x = 0; x < quant_field.xsize(); ++x) { - const float diff = row_dist[x] / butteraugli_target; + const float diff = row_dist[x] / original_butteraugli; if (diff <= 1.0f) { row_q[x] *= std::pow(diff, cur_pow); } else { @@ -921,10 +938,7 @@ void FindBestQuantizationMaxError(const Image3F& opsin, PassesEncoderState* enc_state, const JxlCmsInterface& cms, ThreadPool* pool, AuxOut* aux_out) { - // TODO(veluca): this only works if opsin is in XYB. The current encoder does - // not have code paths that produce non-XYB opsin here. - JXL_CHECK(enc_state->shared.frame_header.color_transform == - ColorTransform::kXYB); + // TODO(szabadka): Make this work for non-opsin color spaces. const CompressParams& cparams = enc_state->cparams; Quantizer& quantizer = enc_state->shared.quantizer; ImageI& raw_quant_field = enc_state->shared.raw_quant_field; @@ -1065,10 +1079,8 @@ ImageBundle RoundtripImage(const Image3F& opsin, PassesEncoderState* enc_state, PROFILER_ZONE("enc roundtrip"); std::unique_ptr<PassesDecoderState> dec_state = jxl::make_unique<PassesDecoderState>(); - JXL_CHECK(dec_state->output_encoding_info.Set( - *enc_state->shared.metadata, - ColorEncoding::LinearSRGB( - enc_state->shared.metadata->m.color_encoding.IsGray()))); + JXL_CHECK(dec_state->output_encoding_info.SetFromMetadata( + *enc_state->shared.metadata)); dec_state->shared = &enc_state->shared; JXL_ASSERT(opsin.ysize() % kBlockDim == 0); diff --git a/media/libjxl/src/lib/jxl/enc_ans.cc b/media/libjxl/src/lib/jxl/enc_ans.cc index 31bd99bee8..81ff836752 100644 --- a/media/libjxl/src/lib/jxl/enc_ans.cc +++ b/media/libjxl/src/lib/jxl/enc_ans.cc @@ -85,7 +85,7 @@ float EstimateDataBits(const ANSHistBin* histogram, const ANSHistBin* counts, float EstimateDataBitsFlat(const ANSHistBin* histogram, size_t len) { const float flat_bits = std::max(FastLog2f(len), 0.0f); - int total_histogram = 0; + float total_histogram = 0; for (size_t i = 0; i < len; ++i) { total_histogram += histogram[i]; } @@ -442,20 +442,24 @@ size_t BuildAndStoreANSEncodingData( { std::vector<uint8_t> depths(alphabet_size); std::vector<uint16_t> bits(alphabet_size); - BitWriter tmp_writer; - BitWriter* w = writer ? writer : &tmp_writer; - size_t start = w->BitsWritten(); - BitWriter::Allotment allotment( - w, 8 * alphabet_size + 8); // safe upper bound - BuildAndStoreHuffmanTree(histo.data(), alphabet_size, depths.data(), - bits.data(), w); - ReclaimAndCharge(w, &allotment, 0, /*aux_out=*/nullptr); - + if (writer == nullptr) { + BitWriter tmp_writer; + BitWriter::Allotment allotment( + &tmp_writer, 8 * alphabet_size + 8); // safe upper bound + BuildAndStoreHuffmanTree(histo.data(), alphabet_size, depths.data(), + bits.data(), &tmp_writer); + ReclaimAndCharge(&tmp_writer, &allotment, 0, /*aux_out=*/nullptr); + cost = tmp_writer.BitsWritten(); + } else { + size_t start = writer->BitsWritten(); + BuildAndStoreHuffmanTree(histo.data(), alphabet_size, depths.data(), + bits.data(), writer); + cost = writer->BitsWritten() - start; + } for (size_t i = 0; i < alphabet_size; i++) { info[i].bits = depths[i] == 0 ? 0 : bits[i]; info[i].depth = depths[i]; } - cost = w->BitsWritten() - start; } // Estimate data cost. for (size_t i = 0; i < alphabet_size; i++) { @@ -699,9 +703,8 @@ class HistogramBuilder { if (histograms_.size() > 1) { if (!ans_fuzzer_friendly_) { std::vector<uint32_t> histogram_symbols; - ClusterHistograms(params, histograms_, histograms_.size(), - kClustersLimit, &clustered_histograms, - &histogram_symbols); + ClusterHistograms(params, histograms_, kClustersLimit, + &clustered_histograms, &histogram_symbols); for (size_t c = 0; c < histograms_.size(); ++c) { (*context_map)[c] = static_cast<uint8_t>(histogram_symbols[c]); } @@ -719,7 +722,8 @@ class HistogramBuilder { } } if (writer != nullptr) { - EncodeContextMap(*context_map, clustered_histograms.size(), writer); + EncodeContextMap(*context_map, clustered_histograms.size(), writer, + layer, aux_out); } } if (aux_out != nullptr) { diff --git a/media/libjxl/src/lib/jxl/enc_ar_control_field.cc b/media/libjxl/src/lib/jxl/enc_ar_control_field.cc index f8025ac694..9030430e2b 100644 --- a/media/libjxl/src/lib/jxl/enc_ar_control_field.cc +++ b/media/libjxl/src/lib/jxl/enc_ar_control_field.cc @@ -34,6 +34,13 @@ namespace jxl { namespace HWY_NAMESPACE { namespace { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::GetLane; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::Sqrt; + void ProcessTile(const Image3F& opsin, PassesEncoderState* enc_state, const Rect& rect, ArControlFieldHeuristics::TempImages* temp_image) { @@ -114,18 +121,18 @@ void ProcessTile(const Image3F& opsin, PassesEncoderState* enc_state, auto sumsqr = Zero(df); for (size_t c = 0; c < 3; c++) { auto laplacian = - LoadU(df, in_row[c] + cx) * Set(df, kChannelWeights[c]); + Mul(LoadU(df, in_row[c] + cx), Set(df, kChannelWeights[c])); auto sum_oth0 = LoadU(df, in_row[c] + cx - 1); auto sum_oth1 = LoadU(df, in_row[c] + cx + 1); auto sum_oth2 = LoadU(df, in_row_t[c] + cx - 1); auto sum_oth3 = LoadU(df, in_row_t[c] + cx); - sum_oth0 += LoadU(df, in_row_t[c] + cx + 1); - sum_oth1 += LoadU(df, in_row_b[c] + cx - 1); - sum_oth2 += LoadU(df, in_row_b[c] + cx); - sum_oth3 += LoadU(df, in_row_b[c] + cx + 1); - sum_oth0 += sum_oth1; - sum_oth2 += sum_oth3; - sum_oth0 += sum_oth2; + sum_oth0 = Add(sum_oth0, LoadU(df, in_row_t[c] + cx + 1)); + sum_oth1 = Add(sum_oth1, LoadU(df, in_row_b[c] + cx - 1)); + sum_oth2 = Add(sum_oth2, LoadU(df, in_row_b[c] + cx)); + sum_oth3 = Add(sum_oth3, LoadU(df, in_row_b[c] + cx + 1)); + sum_oth0 = Add(sum_oth0, sum_oth1); + sum_oth2 = Add(sum_oth2, sum_oth3); + sum_oth0 = Add(sum_oth0, sum_oth2); laplacian = MulAdd(Set(df, kChannelWeightsLapNeg[c]), sum_oth0, laplacian); sumsqr = MulAdd(laplacian, laplacian, sumsqr); @@ -154,7 +161,7 @@ void ProcessTile(const Image3F& opsin, PassesEncoderState* enc_state, auto sum = Zero(df4); for (size_t iy = 0; iy < 4; iy++) { for (size_t ix = 0; ix < 4; ix += Lanes(df4)) { - sum += LoadU(df4, rows_in[iy] + x * 4 + ix + 2); + sum = Add(sum, LoadU(df4, rows_in[iy] + x * 4 + ix + 2)); } } row_out[x] = GetLane(Sqrt(SumOfLanes(df4, sum))) * (1.0f / 4.0f); @@ -190,7 +197,7 @@ void ProcessTile(const Image3F& opsin, PassesEncoderState* enc_state, auto sum = Zero(df4); for (size_t iy = 0; iy < 4; iy++) { for (size_t ix = 0; ix < 4; ix += Lanes(df4)) { - sum += Load(df4, rows_in[iy] + sx + ix); + sum = Add(sum, Load(df4, rows_in[iy] + sx + ix)); } } row_out[x] = GetLane(Sqrt(SumOfLanes(df4, sum))) * (1.0f / 4.0f); diff --git a/media/libjxl/src/lib/jxl/enc_bit_writer.h b/media/libjxl/src/lib/jxl/enc_bit_writer.h index 5aac355c53..4cac8dfbe5 100644 --- a/media/libjxl/src/lib/jxl/enc_bit_writer.h +++ b/media/libjxl/src/lib/jxl/enc_bit_writer.h @@ -38,10 +38,6 @@ struct BitWriter { BitWriter(BitWriter&&) = default; BitWriter& operator=(BitWriter&&) = default; - explicit BitWriter(PaddedBytes&& donor) - : bits_written_(donor.size() * kBitsPerByte), - storage_(std::move(donor)) {} - size_t BitsWritten() const { return bits_written_; } Span<const uint8_t> GetSpan() const { @@ -60,8 +56,11 @@ struct BitWriter { return std::move(storage_); } + private: // Must be byte-aligned before calling. void AppendByteAligned(const Span<const uint8_t>& span); + + public: // NOTE: no allotment needed, the other BitWriters have already been charged. void AppendByteAligned(const BitWriter& other); void AppendByteAligned(const std::vector<std::unique_ptr<BitWriter>>& others); diff --git a/media/libjxl/src/lib/jxl/enc_butteraugli_pnorm.cc b/media/libjxl/src/lib/jxl/enc_butteraugli_pnorm.cc index ecad5135ed..fe5629dcda 100644 --- a/media/libjxl/src/lib/jxl/enc_butteraugli_pnorm.cc +++ b/media/libjxl/src/lib/jxl/enc_butteraugli_pnorm.cc @@ -24,6 +24,9 @@ namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::GetLane; +using hwy::HWY_NAMESPACE::Mul; using hwy::HWY_NAMESPACE::Rebind; double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params, @@ -63,17 +66,17 @@ double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params, #else const auto d1 = Load(d, row + x); #endif - const auto d2 = d1 * d1 * d1; - sums0 += d2; - const auto d3 = d2 * d2; - sums1 += d3; - const auto d4 = d3 * d3; - sums2 += d4; + const auto d2 = Mul(d1, Mul(d1, d1)); + sums0 = Add(sums0, d2); + const auto d3 = Mul(d2, d2); + sums1 = Add(sums1, d3); + const auto d4 = Mul(d3, d3); + sums2 = Add(sums2, d4); } - Store(sums0 + Load(d, sum_totals0), d, sum_totals0); - Store(sums1 + Load(d, sum_totals1), d, sum_totals1); - Store(sums2 + Load(d, sum_totals2), d, sum_totals2); + Store(Add(sums0, Load(d, sum_totals0)), d, sum_totals0); + Store(Add(sums1, Load(d, sum_totals1)), d, sum_totals1); + Store(Add(sums2, Load(d, sum_totals2)), d, sum_totals2); for (; x < distmap.xsize(); ++x) { const double d1 = row[x]; diff --git a/media/libjxl/src/lib/jxl/enc_cache.cc b/media/libjxl/src/lib/jxl/enc_cache.cc index 493d1ba0a6..a1f2a0887a 100644 --- a/media/libjxl/src/lib/jxl/enc_cache.cc +++ b/media/libjxl/src/lib/jxl/enc_cache.cc @@ -25,6 +25,7 @@ #include "lib/jxl/enc_frame.h" #include "lib/jxl/enc_group.h" #include "lib/jxl/enc_modular.h" +#include "lib/jxl/enc_quant_weights.h" #include "lib/jxl/frame_header.h" #include "lib/jxl/image.h" #include "lib/jxl/image_bundle.h" @@ -62,6 +63,11 @@ Status InitializePassesEncoder(const Image3F& opsin, const JxlCmsInterface& cms, enc_state->coeffs.pop_back(); } + float scale = + shared.quantizer.ScaleGlobalScale(enc_state->cparams.quant_ac_rescale); + DequantMatricesScaleDC(&shared.matrices, scale); + shared.quantizer.RecomputeFromGlobalScale(); + Image3F dc(shared.frame_dim.xsize_blocks, shared.frame_dim.ysize_blocks); JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, shared.frame_dim.num_groups, ThreadPool::NoInit, @@ -72,28 +78,17 @@ Status InitializePassesEncoder(const Image3F& opsin, const JxlCmsInterface& cms, if (shared.frame_header.flags & FrameHeader::kUseDcFrame) { CompressParams cparams = enc_state->cparams; - // Guess a distance that produces good initial results. - cparams.butteraugli_distance = - std::max(kMinButteraugliDistance, - enc_state->cparams.butteraugli_distance * 0.1f); cparams.dots = Override::kOff; cparams.noise = Override::kOff; cparams.patches = Override::kOff; cparams.gaborish = Override::kOff; cparams.epf = 0; - cparams.max_error_mode = true; cparams.resampling = 1; cparams.ec_resampling = 1; - for (size_t c = 0; c < 3; c++) { - cparams.max_error[c] = shared.quantizer.MulDC()[c]; - } - JXL_ASSERT(cparams.progressive_dc > 0); - cparams.progressive_dc--; // The DC frame will have alpha=0. Don't erase its contents. cparams.keep_invisible = Override::kOn; - // No EPF or Gaborish in DC frames. - cparams.epf = 0; - cparams.gaborish = Override::kOff; + JXL_ASSERT(cparams.progressive_dc > 0); + cparams.progressive_dc--; // Use kVarDCT in max_error_mode for intermediate progressive DC, // and kModular for the smallest DC (first in the bitstream) if (cparams.progressive_dc == 0) { @@ -102,6 +97,15 @@ Status InitializePassesEncoder(const Image3F& opsin, const JxlCmsInterface& cms, cparams.butteraugli_distance = std::max(kMinButteraugliDistance, enc_state->cparams.butteraugli_distance * 0.03f); + } else { + cparams.max_error_mode = true; + for (size_t c = 0; c < 3; c++) { + cparams.max_error[c] = shared.quantizer.MulDC()[c]; + } + // Guess a distance that produces good initial results. + cparams.butteraugli_distance = + std::max(kMinButteraugliDistance, + enc_state->cparams.butteraugli_distance * 0.1f); } ImageBundle ib(&shared.metadata->m); // This is a lie - dc is in XYB @@ -134,24 +138,34 @@ Status InitializePassesEncoder(const Image3F& opsin, const JxlCmsInterface& cms, dc_frame_info.dc_level = shared.frame_header.dc_level + 1; dc_frame_info.ib_needs_color_transform = false; dc_frame_info.save_before_color_transform = true; // Implicitly true - // TODO(lode): the EncodeFrame / DecodeFrame pair here is likely broken in - // case of dc_level >= 3, since EncodeFrame may output multiple frames - // to the bitwriter, while DecodeFrame reads only one. + AuxOut dc_aux_out; + if (aux_out) { + dc_aux_out.debug_prefix = aux_out->debug_prefix; + } JXL_CHECK(EncodeFrame(cparams, dc_frame_info, shared.metadata, ib, state.get(), cms, pool, special_frame.get(), - nullptr)); + aux_out ? &dc_aux_out : nullptr)); + if (aux_out) { + for (const auto& l : dc_aux_out.layers) { + aux_out->layers[kLayerDC].Assimilate(l); + } + } const Span<const uint8_t> encoded = special_frame->GetSpan(); enc_state->special_frames.emplace_back(std::move(special_frame)); - BitReader br(encoded); ImageBundle decoded(&shared.metadata->m); std::unique_ptr<PassesDecoderState> dec_state = jxl::make_unique<PassesDecoderState>(); - JXL_CHECK(dec_state->output_encoding_info.Set( - *shared.metadata, - ColorEncoding::LinearSRGB(shared.metadata->m.color_encoding.IsGray()))); - JXL_CHECK(DecodeFrame({}, dec_state.get(), pool, &br, &decoded, - *shared.metadata, /*constraints=*/nullptr)); + JXL_CHECK( + dec_state->output_encoding_info.SetFromMetadata(*shared.metadata)); + const uint8_t* frame_start = encoded.data(); + size_t encoded_size = encoded.size(); + for (int i = 0; i <= cparams.progressive_dc; ++i) { + JXL_CHECK(DecodeFrame(dec_state.get(), pool, frame_start, encoded_size, + &decoded, *shared.metadata)); + frame_start += decoded.decoded_bytes(); + encoded_size -= decoded.decoded_bytes(); + } // TODO(lode): shared.frame_header.dc_level should be equal to // dec_state.shared->frame_header.dc_level - 1 here, since above we set // dc_frame_info.dc_level = shared.frame_header.dc_level + 1, and @@ -161,7 +175,7 @@ Status InitializePassesEncoder(const Image3F& opsin, const JxlCmsInterface& cms, CopyImage(dec_state->shared->dc_frames[shared.frame_header.dc_level]); ZeroFillImage(&shared.quant_dc); shared.dc = &shared.dc_storage; - JXL_CHECK(br.Close()); + JXL_CHECK(encoded_size == 0); } else { auto compute_dc_coeffs = [&](int group_index, int /* thread */) { modular_frame_encoder->AddVarDCTDC( diff --git a/media/libjxl/src/lib/jxl/enc_chroma_from_luma.cc b/media/libjxl/src/lib/jxl/enc_chroma_from_luma.cc index 370595c5e5..4f0798e994 100644 --- a/media/libjxl/src/lib/jxl/enc_chroma_from_luma.cc +++ b/media/libjxl/src/lib/jxl/enc_chroma_from_luma.cc @@ -35,6 +35,13 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Abs; +using hwy::HWY_NAMESPACE::Ge; +using hwy::HWY_NAMESPACE::GetLane; +using hwy::HWY_NAMESPACE::IfThenElse; +using hwy::HWY_NAMESPACE::Lt; + static HWY_FULL(float) df; struct CFLFunction { @@ -72,23 +79,27 @@ struct CFLFunction { for (size_t i = 0; i < num; i += Lanes(df)) { // color residual = ax + b - const auto a = inv_color_factor * Load(df, values_m + i); - const auto b = base_v * Load(df, values_m + i) - Load(df, values_s + i); - const auto v = a * x_v + b; - const auto vpe = a * xpe_v + b; - const auto vme = a * xme_v + b; + const auto a = Mul(inv_color_factor, Load(df, values_m + i)); + const auto b = + Sub(Mul(base_v, Load(df, values_m + i)), Load(df, values_s + i)); + const auto v = MulAdd(a, x_v, b); + const auto vpe = MulAdd(a, xpe_v, b); + const auto vme = MulAdd(a, xme_v, b); const auto av = Abs(v); const auto avpe = Abs(vpe); const auto avme = Abs(vme); - auto d = coeffx2 * (av + one) * a; - auto dpe = coeffx2 * (avpe + one) * a; - auto dme = coeffx2 * (avme + one) * a; - d = IfThenElse(v < zero, zero - d, d); - dpe = IfThenElse(vpe < zero, zero - dpe, dpe); - dme = IfThenElse(vme < zero, zero - dme, dme); - fd_v += IfThenElse(av >= thres, zero, d); - fdpe_v += IfThenElse(av >= thres, zero, dpe); - fdme_v += IfThenElse(av >= thres, zero, dme); + const auto acoeffx2 = Mul(coeffx2, a); + auto d = Mul(acoeffx2, Add(av, one)); + auto dpe = Mul(acoeffx2, Add(avpe, one)); + auto dme = Mul(acoeffx2, Add(avme, one)); + d = IfThenElse(Lt(v, zero), Sub(zero, d), d); + dpe = IfThenElse(Lt(vpe, zero), Sub(zero, dpe), dpe); + dme = IfThenElse(Lt(vme, zero), Sub(zero, dme), dme); + const auto above = Ge(av, thres); + // TODO(eustas): use IfThenElseZero + fd_v = Add(fd_v, IfThenElse(above, zero, d)); + fdpe_v = Add(fdpe_v, IfThenElse(above, zero, dpe)); + fdme_v = Add(fdme_v, IfThenElse(above, zero, dme)); } *fpeps = first_derivative_peps + GetLane(SumOfLanes(df, fdpe_v)); @@ -118,8 +129,9 @@ int32_t FindBestMultiplier(const float* values_m, const float* values_s, const auto base_v = Set(df, base); for (size_t i = 0; i < num; i += Lanes(df)) { // color residual = ax + b - const auto a = inv_color_factor * Load(df, values_m + i); - const auto b = base_v * Load(df, values_m + i) - Load(df, values_s + i); + const auto a = Mul(inv_color_factor, Load(df, values_m + i)); + const auto b = + Sub(Mul(base_v, Load(df, values_m + i)), Load(df, values_s + i)); ca = MulAdd(a, a, ca); cb = MulAdd(a, b, cb); } @@ -163,7 +175,8 @@ void InitDCStorage(size_t num_blocks, ImageF* dc_values) { } } -void ComputeDC(const ImageF& dc_values, bool fast, int* dc_x, int* dc_b) { +void ComputeDC(const ImageF& dc_values, bool fast, int32_t* dc_x, + int32_t* dc_b) { constexpr float kDistanceMultiplierDC = 1e-5f; const float* JXL_RESTRICT dc_values_yx = dc_values.Row(0); const float* JXL_RESTRICT dc_values_x = dc_values.Row(1); @@ -292,12 +305,12 @@ void ComputeTile(const Image3F& opsin, const DequantMatrices& dequant, const auto b_y = Load(df, block_y + i); const auto b_x = Load(df, block_x + i); const auto b_b = Load(df, block_b + i); - const auto qqm_x = qv * Load(df, qm_x + i); - const auto qqm_b = qv * Load(df, qm_b + i); - Store(b_y * qqm_x, df, coeffs_yx + num_ac); - Store(b_x * qqm_x, df, coeffs_x + num_ac); - Store(b_y * qqm_b, df, coeffs_yb + num_ac); - Store(b_b * qqm_b, df, coeffs_b + num_ac); + const auto qqm_x = Mul(qv, Load(df, qm_x + i)); + const auto qqm_b = Mul(qv, Load(df, qm_b + i)); + Store(Mul(b_y, qqm_x), df, coeffs_yx + num_ac); + Store(Mul(b_x, qqm_x), df, coeffs_x + num_ac); + Store(Mul(b_y, qqm_b), df, coeffs_yb + num_ac); + Store(Mul(b_b, qqm_b), df, coeffs_b + num_ac); num_ac += Lanes(df); } } diff --git a/media/libjxl/src/lib/jxl/enc_cluster.cc b/media/libjxl/src/lib/jxl/enc_cluster.cc index 8ae863c47d..c79b3ac834 100644 --- a/media/libjxl/src/lib/jxl/enc_cluster.cc +++ b/media/libjxl/src/lib/jxl/enc_cluster.cc @@ -26,12 +26,18 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Eq; +using hwy::HWY_NAMESPACE::IfThenZeroElse; + template <class V> V Entropy(V count, V inv_total, V total) { const HWY_CAPPED(float, Histogram::kRounding) d; const auto zero = Set(d, 0.0f); - return IfThenZeroElse(count == total, - zero - count * FastLog2f(d, inv_total * count)); + // TODO(eustas): why (0 - x) instead of Neg(x)? + return IfThenZeroElse( + Eq(count, total), + Sub(zero, Mul(count, FastLog2f(d, Mul(inv_total, count))))); } void HistogramEntropy(const Histogram& a) { @@ -47,7 +53,8 @@ void HistogramEntropy(const Histogram& a) { for (size_t i = 0; i < a.data_.size(); i += Lanes(di)) { const auto counts = LoadU(di, &a.data_[i]); - entropy_lanes += Entropy(ConvertTo(df, counts), inv_tot, total); + entropy_lanes = + Add(entropy_lanes, Entropy(ConvertTo(df, counts), inv_tot, total)); } a.entropy_ += GetLane(SumOfLanes(df, entropy_lanes)); } @@ -68,8 +75,8 @@ float HistogramDistance(const Histogram& a, const Histogram& b) { a.data_.size() > i ? LoadU(di, &a.data_[i]) : Zero(di); const auto b_counts = b.data_.size() > i ? LoadU(di, &b.data_[i]) : Zero(di); - const auto counts = ConvertTo(df, a_counts + b_counts); - distance_lanes += Entropy(counts, inv_tot, total); + const auto counts = ConvertTo(df, Add(a_counts, b_counts)); + distance_lanes = Add(distance_lanes, Entropy(counts, inv_tot, total)); } const float total_distance = GetLane(SumOfLanes(df, distance_lanes)); return total_distance - a.entropy_ - b.entropy_; @@ -77,60 +84,44 @@ float HistogramDistance(const Histogram& a, const Histogram& b) { // First step of a k-means clustering with a fancy distance metric. void FastClusterHistograms(const std::vector<Histogram>& in, - const size_t num_contexts_in, size_t max_histograms, - float min_distance, std::vector<Histogram>* out, + size_t max_histograms, std::vector<Histogram>* out, std::vector<uint32_t>* histogram_symbols) { PROFILER_FUNC; + out->clear(); + out->reserve(max_histograms); + histogram_symbols->clear(); + histogram_symbols->resize(in.size(), max_histograms); + + std::vector<float> dists(in.size(), std::numeric_limits<float>::max()); size_t largest_idx = 0; - std::vector<uint32_t> nonempty_histograms; - nonempty_histograms.reserve(in.size()); - for (size_t i = 0; i < num_contexts_in; i++) { - if (in[i].total_count_ == 0) continue; + for (size_t i = 0; i < in.size(); i++) { + if (in[i].total_count_ == 0) { + (*histogram_symbols)[i] = 0; + dists[i] = 0.0f; + continue; + } HistogramEntropy(in[i]); if (in[i].total_count_ > in[largest_idx].total_count_) { largest_idx = i; } - nonempty_histograms.push_back(i); - } - // No symbols. - if (nonempty_histograms.empty()) { - out->resize(1); - histogram_symbols->clear(); - histogram_symbols->resize(in.size(), 0); - return; } - largest_idx = std::find(nonempty_histograms.begin(), - nonempty_histograms.end(), largest_idx) - - nonempty_histograms.begin(); - size_t num_contexts = nonempty_histograms.size(); - out->clear(); - out->reserve(max_histograms); - std::vector<float> dists(num_contexts, std::numeric_limits<float>::max()); - histogram_symbols->clear(); - histogram_symbols->resize(in.size(), max_histograms); - while (out->size() < max_histograms && out->size() < num_contexts) { - (*histogram_symbols)[nonempty_histograms[largest_idx]] = out->size(); - out->push_back(in[nonempty_histograms[largest_idx]]); + constexpr float kMinDistanceForDistinct = 48.0f; + while (out->size() < max_histograms) { + (*histogram_symbols)[largest_idx] = out->size(); + out->push_back(in[largest_idx]); + dists[largest_idx] = 0.0f; largest_idx = 0; - for (size_t i = 0; i < num_contexts; i++) { - dists[i] = std::min( - HistogramDistance(in[nonempty_histograms[i]], out->back()), dists[i]); - // Avoid repeating histograms - if ((*histogram_symbols)[nonempty_histograms[i]] != max_histograms) { - continue; - } + for (size_t i = 0; i < in.size(); i++) { + if (dists[i] == 0.0f) continue; + dists[i] = std::min(HistogramDistance(in[i], out->back()), dists[i]); if (dists[i] > dists[largest_idx]) largest_idx = i; } - if (dists[largest_idx] < min_distance) break; + if (dists[largest_idx] < kMinDistanceForDistinct) break; } - for (size_t i = 0; i < num_contexts_in; i++) { + for (size_t i = 0; i < in.size(); i++) { if ((*histogram_symbols)[i] != max_histograms) continue; - if (in[i].total_count_ == 0) { - (*histogram_symbols)[i] = 0; - continue; - } size_t best = 0; float best_dist = HistogramDistance(in[i], (*out)[best]); for (size_t j = 1; j < out->size(); j++) { @@ -191,25 +182,19 @@ void HistogramReindex(std::vector<Histogram>* out, // placed in 'out', and for each index in 'in', *histogram_symbols will // indicate which of the 'out' histograms is the best approximation. void ClusterHistograms(const HistogramParams params, - const std::vector<Histogram>& in, - const size_t num_contexts, size_t max_histograms, + const std::vector<Histogram>& in, size_t max_histograms, std::vector<Histogram>* out, std::vector<uint32_t>* histogram_symbols) { - constexpr float kMinDistanceForDistinctFast = 64.0f; - constexpr float kMinDistanceForDistinctBest = 16.0f; max_histograms = std::min(max_histograms, params.max_histograms); + max_histograms = std::min(max_histograms, in.size()); if (params.clustering == HistogramParams::ClusteringType::kFastest) { - HWY_DYNAMIC_DISPATCH(FastClusterHistograms) - (in, num_contexts, 4, kMinDistanceForDistinctFast, out, histogram_symbols); - } else if (params.clustering == HistogramParams::ClusteringType::kFast) { - HWY_DYNAMIC_DISPATCH(FastClusterHistograms) - (in, num_contexts, max_histograms, kMinDistanceForDistinctFast, out, - histogram_symbols); - } else { - PROFILER_FUNC; - HWY_DYNAMIC_DISPATCH(FastClusterHistograms) - (in, num_contexts, max_histograms, kMinDistanceForDistinctBest, out, - histogram_symbols); + max_histograms = std::min(max_histograms, static_cast<size_t>(4)); + } + + HWY_DYNAMIC_DISPATCH(FastClusterHistograms) + (in, max_histograms, out, histogram_symbols); + + if (params.clustering == HistogramParams::ClusteringType::kBest) { for (size_t i = 0; i < out->size(); i++) { (*out)[i].entropy_ = ANSPopulationCost((*out)[i].data_.data(), (*out)[i].data_.size()); diff --git a/media/libjxl/src/lib/jxl/enc_cluster.h b/media/libjxl/src/lib/jxl/enc_cluster.h index 622a567950..a06783fcca 100644 --- a/media/libjxl/src/lib/jxl/enc_cluster.h +++ b/media/libjxl/src/lib/jxl/enc_cluster.h @@ -53,8 +53,7 @@ struct Histogram { }; void ClusterHistograms(HistogramParams params, const std::vector<Histogram>& in, - size_t num_contexts, size_t max_histograms, - std::vector<Histogram>* out, + size_t max_histograms, std::vector<Histogram>* out, std::vector<uint32_t>* histogram_symbols); } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/enc_coeff_order.cc b/media/libjxl/src/lib/jxl/enc_coeff_order.cc index c1ec320d98..8d75cc0383 100644 --- a/media/libjxl/src/lib/jxl/enc_coeff_order.cc +++ b/media/libjxl/src/lib/jxl/enc_coeff_order.cc @@ -73,7 +73,8 @@ void ComputeCoeffOrder(SpeedTier speed, const ACImage& acs, if (used_orders != 0) { uint64_t threshold = (std::numeric_limits<uint64_t>::max() >> 32) * block_fraction; - uint64_t s[2] = {0x94D049BB133111EBull, 0xBF58476D1CE4E5B9ull}; + uint64_t s[2] = {static_cast<uint64_t>(0x94D049BB133111EBull), + static_cast<uint64_t>(0xBF58476D1CE4E5B9ull)}; // Xorshift128+ adapted from xorshift128+-inl.h auto use_sample = [&]() { auto s1 = s[0]; diff --git a/media/libjxl/src/lib/jxl/enc_color_management.cc b/media/libjxl/src/lib/jxl/enc_color_management.cc index 0a7e21f3ba..0b031d2dcd 100644 --- a/media/libjxl/src/lib/jxl/enc_color_management.cc +++ b/media/libjxl/src/lib/jxl/enc_color_management.cc @@ -105,7 +105,8 @@ Status BeforeTransform(JxlCms* t, const float* buf_src, float* xform_src, : 10000.f / t->intensity_target); for (size_t i = 0; i < buf_size; i += Lanes(df)) { const auto val = Load(df, buf_src + i); - const auto result = multiplier * TF_PQ().DisplayFromEncoded(df, val); + const auto result = + Mul(multiplier, TF_PQ().DisplayFromEncoded(df, val)); Store(result, df, xform_src + i); } #if JXL_CMS_VERBOSE >= 2 @@ -162,7 +163,8 @@ Status AfterTransform(JxlCms* t, float* JXL_RESTRICT buf_dst, size_t buf_size) { : t->intensity_target * 1e-4f); for (size_t i = 0; i < buf_size; i += Lanes(df)) { const auto val = Load(df, buf_dst + i); - const auto result = TF_PQ().EncodedFromDisplay(df, multiplier * val); + const auto result = + TF_PQ().EncodedFromDisplay(df, Mul(multiplier, val)); Store(result, df, buf_dst + i); } #if JXL_CMS_VERBOSE >= 2 @@ -613,8 +615,15 @@ Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1, JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context, const Profile& profile, const ColorEncoding& c) { - cmsCIEXYZ XYZ = {1.0, 1.0, 1.0}; + const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>( + cmsReadTag(profile.get(), cmsSigMediaWhitePointTag)); + if (white_point != nullptr && + cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag) == nullptr) { + // No chromatic adaptation matrix: the white point is already unadapted. + return *white_point; + } + cmsCIEXYZ XYZ = {1.0, 1.0, 1.0}; Profile profile_xyz; if (!CreateProfileXYZ(context, &profile_xyz)) return XYZ; // Array arguments are one per profile. @@ -637,8 +646,8 @@ JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context, return XYZ; } -Status IdentifyPrimaries(const Profile& profile, const cmsCIEXYZ& wp_unadapted, - ColorEncoding* c) { +Status IdentifyPrimaries(const cmsContext context, const Profile& profile, + const cmsCIEXYZ& wp_unadapted, ColorEncoding* c) { if (!c->HasPrimaries()) return true; if (ColorSpaceFromProfile(profile) == ColorSpace::kUnknown) return true; @@ -649,8 +658,34 @@ Status IdentifyPrimaries(const Profile& profile, const cmsCIEXYZ& wp_unadapted, cmsReadTag(profile.get(), cmsSigGreenColorantTag)); const cmsCIEXYZ* adapted_b = static_cast<const cmsCIEXYZ*>( cmsReadTag(profile.get(), cmsSigBlueColorantTag)); + + cmsCIEXYZ converted_rgb[3]; if (adapted_r == nullptr || adapted_g == nullptr || adapted_b == nullptr) { - return JXL_FAILURE("Failed to retrieve colorants"); + // No colorant tag, determine the XYZ coordinates of the primaries by + // converting from the colorspace. + Profile profile_xyz; + if (!CreateProfileXYZ(context, &profile_xyz)) { + return JXL_FAILURE("Failed to retrieve colorants"); + } + // Array arguments are one per profile. + cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()}; + cmsUInt32Number intents[2] = {INTENT_RELATIVE_COLORIMETRIC, + INTENT_RELATIVE_COLORIMETRIC}; + cmsBool black_compensation[2] = {0, 0}; + cmsFloat64Number adaption[2] = {0.0, 0.0}; + // Only transforming three pixels, so skip expensive optimizations. + cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC; + Transform xform(cmsCreateExtendedTransform( + context, 2, profiles, black_compensation, intents, adaption, nullptr, 0, + Type64(*c), TYPE_XYZ_DBL, flags)); + if (!xform) return JXL_FAILURE("Failed to retrieve colorants"); + + const cmsFloat64Number in[9] = {1.0, 0.0, 0.0, 0.0, 1.0, + 0.0, 0.0, 0.0, 1.0}; + cmsDoTransform(xform.get(), in, &converted_rgb->X, 3); + adapted_r = &converted_rgb[0]; + adapted_g = &converted_rgb[1]; + adapted_b = &converted_rgb[2]; } // TODO(janwas): no longer assume Bradford and D50. @@ -869,7 +904,7 @@ Status ColorEncoding::SetFieldsFromICC() { JXL_RETURN_IF_ERROR(SetWhitePoint(CIExyFromXYZ(wp_unadapted))); // Relies on color_space. - JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, this)); + JXL_RETURN_IF_ERROR(IdentifyPrimaries(context, profile, wp_unadapted, this)); // Relies on color_space/white point/primaries being set already. DetectTransferFunction(context, profile, this); diff --git a/media/libjxl/src/lib/jxl/enc_context_map.cc b/media/libjxl/src/lib/jxl/enc_context_map.cc index 4719a254a9..82e5e61813 100644 --- a/media/libjxl/src/lib/jxl/enc_context_map.cc +++ b/media/libjxl/src/lib/jxl/enc_context_map.cc @@ -56,7 +56,8 @@ std::vector<uint8_t> MoveToFrontTransform(const std::vector<uint8_t>& v) { } // namespace void EncodeContextMap(const std::vector<uint8_t>& context_map, - size_t num_histograms, BitWriter* writer) { + size_t num_histograms, BitWriter* writer, size_t layer, + AuxOut* aux_out) { if (num_histograms == 1) { // Simple code writer->Write(1, 1); @@ -100,7 +101,7 @@ void EncodeContextMap(const std::vector<uint8_t>& context_map, writer->Write(1, 0); writer->Write(1, use_mtf); // Use/don't use MTF. BuildAndEncodeHistograms(params, 1, tokens, &codes, &dummy_context_map, - writer, 0, nullptr); + writer, layer, aux_out); WriteTokens(tokens[0], codes, dummy_context_map, writer); } } @@ -132,7 +133,7 @@ void EncodeBlockCtxMap(const BlockCtxMap& block_ctx_map, BitWriter* writer, for (uint32_t i : qft) { JXL_CHECK(U32Coder::Write(kQFThresholdDist, i - 1, writer)); } - EncodeContextMap(ctx_map, block_ctx_map.num_ctxs, writer); + EncodeContextMap(ctx_map, block_ctx_map.num_ctxs, writer, kLayerAC, aux_out); ReclaimAndCharge(writer, &allotment, kLayerAC, aux_out); } diff --git a/media/libjxl/src/lib/jxl/enc_context_map.h b/media/libjxl/src/lib/jxl/enc_context_map.h index 7f6c624380..57e79a173e 100644 --- a/media/libjxl/src/lib/jxl/enc_context_map.h +++ b/media/libjxl/src/lib/jxl/enc_context_map.h @@ -24,7 +24,8 @@ static const size_t kClustersLimit = 128; // Encodes the given context map to the bit stream. The number of different // histogram ids is given by num_histograms. void EncodeContextMap(const std::vector<uint8_t>& context_map, - size_t num_histograms, BitWriter* writer); + size_t num_histograms, BitWriter* writer, size_t layer, + AuxOut* aux_out); void EncodeBlockCtxMap(const BlockCtxMap& block_ctx_map, BitWriter* writer, AuxOut* aux_out); diff --git a/media/libjxl/src/lib/jxl/enc_detect_dots.cc b/media/libjxl/src/lib/jxl/enc_detect_dots.cc index 33b278e400..f7021d6cce 100644 --- a/media/libjxl/src/lib/jxl/enc_detect_dots.cc +++ b/media/libjxl/src/lib/jxl/enc_detect_dots.cc @@ -45,6 +45,11 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::Sub; + ImageF SumOfSquareDifferences(const Image3F& forig, const Image3F& smooth, ThreadPool* pool) { const HWY_FULL(float) d; @@ -66,16 +71,14 @@ ImageF SumOfSquareDifferences(const Image3F& forig, const Image3F& smooth, float* JXL_RESTRICT sos_row = sum_of_squares.Row(y); for (size_t x = 0; x < forig.xsize(); x += Lanes(d)) { - auto v0 = Load(d, orig_row0 + x) - Load(d, smooth_row0 + x); - auto v1 = Load(d, orig_row1 + x) - Load(d, smooth_row1 + x); - auto v2 = Load(d, orig_row2 + x) - Load(d, smooth_row2 + x); - v0 *= v0; - v1 *= v1; - v2 *= v2; - v0 *= color_coef0; // FMA doesn't help here. - v1 *= color_coef1; - v2 *= color_coef2; - const auto sos = v0 + v1 + v2; // weighted sum of square diffs + auto v0 = Sub(Load(d, orig_row0 + x), Load(d, smooth_row0 + x)); + auto v1 = Sub(Load(d, orig_row1 + x), Load(d, smooth_row1 + x)); + auto v2 = Sub(Load(d, orig_row2 + x), Load(d, smooth_row2 + x)); + v0 = Mul(Mul(v0, v0), color_coef0); + v1 = Mul(Mul(v1, v1), color_coef1); + v2 = Mul(Mul(v2, v2), color_coef2); + const auto sos = + Add(v0, Add(v1, v2)); // weighted sum of square diffs Store(sos, d, sos_row + x); } }, @@ -151,16 +154,18 @@ ImageF ComputeEnergyImage(const Image3F& orig, Image3F* smooth, // Prepare guidance images for dot selection. Image3F forig(orig.xsize(), orig.ysize()); - Image3F tmp(orig.xsize(), orig.ysize()); *smooth = Image3F(orig.xsize(), orig.ysize()); + Rect rect(orig); const auto& weights1 = WeightsSeparable5Gaussian0_65(); const auto& weights3 = WeightsSeparable5Gaussian3(); - Separable5_3(orig, Rect(orig), weights1, pool, &forig); - - Separable5_3(orig, Rect(orig), weights3, pool, &tmp); - Separable5_3(tmp, Rect(tmp), weights3, pool, smooth); + for (size_t c = 0; c < 3; ++c) { + // Use forig as temporary storage to reduce memory and keep it warmer. + Separable5(orig.Plane(c), rect, weights3, pool, &forig.Plane(c)); + Separable5(forig.Plane(c), rect, weights3, pool, &smooth->Plane(c)); + Separable5(orig.Plane(c), rect, weights1, pool, &forig.Plane(c)); + } #if JXL_DEBUG_DOT_DETECT AuxOut aux; diff --git a/media/libjxl/src/lib/jxl/enc_entropy_coder.cc b/media/libjxl/src/lib/jxl/enc_entropy_coder.cc index 07fe5a05b2..c634445e83 100644 --- a/media/libjxl/src/lib/jxl/enc_entropy_coder.cc +++ b/media/libjxl/src/lib/jxl/enc_entropy_coder.cc @@ -38,6 +38,12 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::AndNot; +using hwy::HWY_NAMESPACE::Eq; +using hwy::HWY_NAMESPACE::GetLane; + // Returns number of non-zero coefficients (but skip LLF). // We cannot rely on block[] being all-zero bits, so first truncate to integer. // Also writes the per-8x8 block nzeros starting at nzeros_pos. @@ -71,7 +77,7 @@ int32_t NumNonZeroExceptLLF(const size_t cx, const size_t cy, const auto coef = AndNot(llf_mask, Load(di, &block[y * cx * kBlockDim + x])); - neg_sum_zero += VecFromMask(di, coef == zero); + neg_sum_zero = Add(neg_sum_zero, VecFromMask(di, Eq(coef, zero))); } } } @@ -80,7 +86,7 @@ int32_t NumNonZeroExceptLLF(const size_t cx, const size_t cy, for (size_t y = cy; y < cy * kBlockDim; y++) { for (size_t x = 0; x < cx * kBlockDim; x += Lanes(di)) { const auto coef = Load(di, &block[y * cx * kBlockDim + x]); - neg_sum_zero += VecFromMask(di, coef == zero); + neg_sum_zero = Add(neg_sum_zero, VecFromMask(di, Eq(coef, zero))); } } @@ -121,7 +127,7 @@ int32_t NumNonZero8x8ExceptDC(const int32_t* JXL_RESTRICT block, // DC counts as zero so we don't include it in nzeros. const auto coef = AndNot(dc_mask, Load(di, &block[y * kBlockDim + x])); - neg_sum_zero += VecFromMask(di, coef == zero); + neg_sum_zero = Add(neg_sum_zero, VecFromMask(di, Eq(coef, zero))); } } @@ -129,7 +135,7 @@ int32_t NumNonZero8x8ExceptDC(const int32_t* JXL_RESTRICT block, for (size_t y = 1; y < kBlockDim; y++) { for (size_t x = 0; x < kBlockDim; x += Lanes(di)) { const auto coef = Load(di, &block[y * kBlockDim + x]); - neg_sum_zero += VecFromMask(di, coef == zero); + neg_sum_zero = Add(neg_sum_zero, VecFromMask(di, Eq(coef, zero))); } } diff --git a/media/libjxl/src/lib/jxl/enc_external_image.cc b/media/libjxl/src/lib/jxl/enc_external_image.cc index f283733481..346182b3bf 100644 --- a/media/libjxl/src/lib/jxl/enc_external_image.cc +++ b/media/libjxl/src/lib/jxl/enc_external_image.cc @@ -197,8 +197,8 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize, size_t ysize, const ColorEncoding& c_current, size_t channels, bool alpha_is_premultiplied, size_t bits_per_sample, JxlEndianness endianness, - bool flipped_y, ThreadPool* pool, ImageBundle* ib, - bool float_in, size_t align) { + ThreadPool* pool, ImageBundle* ib, bool float_in, + size_t align) { JXL_CHECK(float_in ? bits_per_sample == 16 || bits_per_sample == 32 : bits_per_sample > 0 && bits_per_sample <= 16); @@ -243,16 +243,12 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize, Image3F color(xsize, ysize); - const auto get_y = [flipped_y, ysize](const size_t y) { - return flipped_y ? ysize - 1 - y : y; - }; - if (float_in) { for (size_t c = 0; c < color_channels; ++c) { JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, [&](const uint32_t task, size_t /*thread*/) { - const size_t y = get_y(task); + const size_t y = task; size_t i = row_size * task + (c * bits_per_sample / jxl::kBitsPerByte); float* JXL_RESTRICT row_out = color.PlaneRow(c, y); @@ -291,7 +287,7 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize, JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, [&](const uint32_t task, size_t /*thread*/) { - const size_t y = get_y(task); + const size_t y = task; size_t i = row_size * task + c * bytes_per_channel; float* JXL_RESTRICT row_out = color.PlaneRow(c, y); if (bits_per_sample <= 8) { @@ -326,7 +322,7 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize, JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, [&](const uint32_t task, size_t /*thread*/) { - const size_t y = get_y(task); + const size_t y = task; size_t i = row_size * task + ((channels - 1) * bits_per_sample / jxl::kBitsPerByte); float* JXL_RESTRICT row_out = alpha.Row(y); @@ -362,7 +358,7 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize, JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, static_cast<uint32_t>(ysize), ThreadPool::NoInit, [&](const uint32_t task, size_t /*thread*/) { - const size_t y = get_y(task); + const size_t y = task; size_t i = row_size * task + (channels - 1) * bytes_per_channel; float* JXL_RESTRICT row_out = alpha.Row(y); if (bits_per_sample <= 8) { @@ -422,8 +418,8 @@ Status BufferToImageBundle(const JxlPixelFormat& pixel_format, uint32_t xsize, JXL_RETURN_IF_ERROR(ConvertFromExternal( jxl::Span<const uint8_t>(static_cast<const uint8_t*>(buffer), size), xsize, ysize, c_current, pixel_format.num_channels, - /*alpha_is_premultiplied=*/false, bitdepth, pixel_format.endianness, - /*flipped_y=*/false, pool, ib, float_in, pixel_format.align)); + /*alpha_is_premultiplied=*/false, bitdepth, pixel_format.endianness, pool, + ib, float_in, pixel_format.align)); ib->VerifyMetadata(); return true; diff --git a/media/libjxl/src/lib/jxl/enc_external_image.h b/media/libjxl/src/lib/jxl/enc_external_image.h index 904babeb54..73b7175a94 100644 --- a/media/libjxl/src/lib/jxl/enc_external_image.h +++ b/media/libjxl/src/lib/jxl/enc_external_image.h @@ -32,8 +32,8 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize, size_t ysize, const ColorEncoding& c_current, size_t channels, bool alpha_is_premultiplied, size_t bits_per_sample, JxlEndianness endianness, - bool flipped_y, ThreadPool* pool, ImageBundle* ib, - bool float_in, size_t align); + ThreadPool* pool, ImageBundle* ib, bool float_in, + size_t align); Status BufferToImageF(const JxlPixelFormat& pixel_format, size_t xsize, size_t ysize, const void* buffer, size_t size, ThreadPool* pool, ImageF* channel); diff --git a/media/libjxl/src/lib/jxl/enc_external_image_gbench.cc b/media/libjxl/src/lib/jxl/enc_external_image_gbench.cc index 8381984742..a123d4b356 100644 --- a/media/libjxl/src/lib/jxl/enc_external_image_gbench.cc +++ b/media/libjxl/src/lib/jxl/enc_external_image_gbench.cc @@ -31,7 +31,6 @@ void BM_EncExternalImage_ConvertImageRGBA(benchmark::State& state) { /*channels=*/4, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/8, JXL_NATIVE_ENDIAN, - /*flipped_y=*/false, /*pool=*/nullptr, &ib, /*float_in=*/false, /*align=*/0)); } } diff --git a/media/libjxl/src/lib/jxl/enc_external_image_test.cc b/media/libjxl/src/lib/jxl/enc_external_image_test.cc index baa9267712..2c5fa5acac 100644 --- a/media/libjxl/src/lib/jxl/enc_external_image_test.cc +++ b/media/libjxl/src/lib/jxl/enc_external_image_test.cc @@ -30,18 +30,18 @@ TEST(ExternalImageTest, InvalidSize) { Span<const uint8_t>(buf, 10), /*xsize=*/10, /*ysize=*/100, /*c_current=*/ColorEncoding::SRGB(), /*channels=*/4, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN, - /*flipped_y=*/false, nullptr, &ib, /*float_in=*/false, /*align=*/0)); + nullptr, &ib, /*float_in=*/false, /*align=*/0)); EXPECT_FALSE(ConvertFromExternal( Span<const uint8_t>(buf, sizeof(buf) - 1), /*xsize=*/10, /*ysize=*/100, /*c_current=*/ColorEncoding::SRGB(), /*channels=*/4, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN, - /*flipped_y=*/false, nullptr, &ib, /*float_in=*/false, /*align=*/0)); - EXPECT_TRUE(ConvertFromExternal( - Span<const uint8_t>(buf, sizeof(buf)), /*xsize=*/10, - /*ysize=*/100, /*c_current=*/ColorEncoding::SRGB(), - /*channels=*/4, /*alpha_is_premultiplied=*/false, - /*bits_per_sample=*/16, JXL_BIG_ENDIAN, - /*flipped_y=*/false, nullptr, &ib, /*float_in=*/false, /*align=*/0)); + nullptr, &ib, /*float_in=*/false, /*align=*/0)); + EXPECT_TRUE( + ConvertFromExternal(Span<const uint8_t>(buf, sizeof(buf)), /*xsize=*/10, + /*ysize=*/100, /*c_current=*/ColorEncoding::SRGB(), + /*channels=*/4, /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/16, JXL_BIG_ENDIAN, nullptr, &ib, + /*float_in=*/false, /*align=*/0)); } #endif @@ -56,12 +56,12 @@ TEST(ExternalImageTest, AlphaMissing) { // has_alpha is true but the ImageBundle has no alpha. Alpha channel should // be ignored. - EXPECT_TRUE(ConvertFromExternal( - Span<const uint8_t>(buf, sizeof(buf)), xsize, ysize, - /*c_current=*/ColorEncoding::SRGB(), - /*channels=*/4, /*alpha_is_premultiplied=*/false, - /*bits_per_sample=*/8, JXL_BIG_ENDIAN, - /*flipped_y=*/false, nullptr, &ib, /*float_in=*/false, /*align=*/0)); + EXPECT_TRUE( + ConvertFromExternal(Span<const uint8_t>(buf, sizeof(buf)), xsize, ysize, + /*c_current=*/ColorEncoding::SRGB(), + /*channels=*/4, /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/8, JXL_BIG_ENDIAN, nullptr, &ib, + /*float_in=*/false, /*align=*/0)); EXPECT_FALSE(ib.HasAlpha()); } diff --git a/media/libjxl/src/lib/jxl/enc_fast_heuristics.cc b/media/libjxl/src/lib/jxl/enc_fast_heuristics.cc deleted file mode 100644 index b9da35967e..0000000000 --- a/media/libjxl/src/lib/jxl/enc_fast_heuristics.cc +++ /dev/null @@ -1,361 +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 <stddef.h> -#include <stdint.h> - -#include <algorithm> -#include <numeric> -#include <string> - -#include "lib/jxl/convolve.h" -#include "lib/jxl/enc_ac_strategy.h" -#include "lib/jxl/enc_adaptive_quantization.h" -#include "lib/jxl/enc_ar_control_field.h" -#include "lib/jxl/enc_cache.h" -#include "lib/jxl/enc_heuristics.h" -#include "lib/jxl/enc_noise.h" -#include "lib/jxl/gaborish.h" -#include "lib/jxl/gauss_blur.h" - -#undef HWY_TARGET_INCLUDE -#define HWY_TARGET_INCLUDE "lib/jxl/enc_fast_heuristics.cc" -#include <hwy/foreach_target.h> -#include <hwy/highway.h> - -HWY_BEFORE_NAMESPACE(); -namespace jxl { -namespace HWY_NAMESPACE { -namespace { -using DF4 = HWY_CAPPED(float, 4); -DF4 df4; -HWY_FULL(float) df; - -Status Heuristics(PassesEncoderState* enc_state, - ModularFrameEncoder* modular_frame_encoder, - const ImageBundle* linear, Image3F* opsin, ThreadPool* pool, - AuxOut* aux_out) { - PROFILER_ZONE("JxlLossyFrameHeuristics uninstrumented"); - CompressParams& cparams = enc_state->cparams; - PassesSharedState& shared = enc_state->shared; - const FrameDimensions& frame_dim = enc_state->shared.frame_dim; - JXL_CHECK(cparams.butteraugli_distance > 0); - - // TODO(veluca): make this tiled. - if (shared.frame_header.loop_filter.gab) { - GaborishInverse(opsin, 0.9908511000000001f, pool); - } - // Compute image of high frequencies by removing a blurred version. - // TODO(veluca): certainly can be made faster, and use less memory... - constexpr size_t pad = 16; - Image3F padded = PadImageMirror(*opsin, pad, pad); - // Make the image (X, Y, B-Y) - // TODO(veluca): SubtractFrom is not parallel *and* not SIMD-fied. - SubtractFrom(padded.Plane(1), &padded.Plane(2)); - // Ensure that OOB access for CfL does nothing. Not necessary if doing things - // properly... - Image3F hf(padded.xsize() + 64, padded.ysize()); - ZeroFillImage(&hf); - hf.ShrinkTo(padded.xsize(), padded.ysize()); - ImageF temp(padded.xsize(), padded.ysize()); - // TODO(veluca): consider some faster blurring method. - auto g = CreateRecursiveGaussian(11.415258091746161); - for (size_t c = 0; c < 3; c++) { - FastGaussian(g, padded.Plane(c), pool, &temp, &hf.Plane(c)); - SubtractFrom(padded.Plane(c), &hf.Plane(c)); - } - // TODO(veluca): DC CfL? - size_t xcolortiles = DivCeil(frame_dim.xsize_blocks, kColorTileDimInBlocks); - size_t ycolortiles = DivCeil(frame_dim.ysize_blocks, kColorTileDimInBlocks); - JXL_RETURN_IF_ERROR(RunOnPool( - pool, 0, xcolortiles * ycolortiles, ThreadPool::NoInit, - [&](size_t tile_id, size_t _) { - size_t tx = tile_id % xcolortiles; - size_t ty = tile_id / xcolortiles; - size_t x0 = tx * kColorTileDim; - size_t x1 = std::min(x0 + kColorTileDim, hf.xsize()); - size_t y0 = ty * kColorTileDim; - size_t y1 = std::min(y0 + kColorTileDim, hf.ysize()); - for (size_t c : {0, 2}) { - static constexpr float kInvColorFactor = 1.0f / kDefaultColorFactor; - auto ca = Zero(df); - auto cb = Zero(df); - const auto inv_color_factor = Set(df, kInvColorFactor); - for (size_t y = y0; y < y1; y++) { - const float* row_m = hf.PlaneRow(1, y); - const float* row_s = hf.PlaneRow(c, y); - for (size_t x = x0; x < x1; x += Lanes(df)) { - // color residual = ax + b - const auto a = inv_color_factor * Load(df, row_m + x); - const auto b = Zero(df) - Load(df, row_s + x); - ca = MulAdd(a, a, ca); - cb = MulAdd(a, b, cb); - } - } - float best = -GetLane(SumOfLanes(df, cb)) / - (GetLane(SumOfLanes(df, ca)) + 1e-9f); - int8_t& res = (c == 0 ? shared.cmap.ytox_map : shared.cmap.ytob_map) - .Row(ty)[tx]; - res = std::max(-128.0f, std::min(127.0f, roundf(best))); - } - }, - "CfL")); - Image3F pooled(frame_dim.xsize_padded / 4, frame_dim.ysize_padded / 4); - Image3F summed(frame_dim.xsize_padded / 4, frame_dim.ysize_padded / 4); - JXL_RETURN_IF_ERROR(RunOnPool( - pool, 0, frame_dim.ysize_padded / 4, ThreadPool::NoInit, - [&](size_t y, size_t _) { - for (size_t c = 0; c < 3; c++) { - float* JXL_RESTRICT row_out = pooled.PlaneRow(c, y); - float* JXL_RESTRICT row_out_avg = summed.PlaneRow(c, y); - const float* JXL_RESTRICT row_in[4]; - for (size_t iy = 0; iy < 4; iy++) { - row_in[iy] = hf.PlaneRow(c, 4 * y + pad + iy); - } - for (size_t x = 0; x < frame_dim.xsize_padded / 4; x++) { - auto max = Zero(df4); - auto sum = Zero(df4); - for (size_t iy = 0; iy < 4; iy++) { - for (size_t ix = 0; ix < 4; ix += Lanes(df4)) { - const auto nn = Abs(Load(df4, row_in[iy] + x * 4 + ix + pad)); - sum += nn; - max = IfThenElse(max > nn, max, nn); - } - } - row_out_avg[x] = GetLane(SumOfLanes(df4, sum)); - row_out[x] = GetLane(MaxOfLanes(df4, max)); - } - } - }, - "MaxPool")); - // TODO(veluca): better handling of the border - // TODO(veluca): consider some faster blurring method. - // TODO(veluca): parallelize. - // Remove noise from the resulting image. - auto g2 = CreateRecursiveGaussian(2.0849544429861884); - constexpr size_t pad2 = 16; - Image3F summed_pad = PadImageMirror(summed, pad2, pad2); - ImageF tmp_out(summed_pad.xsize(), summed_pad.ysize()); - ImageF tmp2(summed_pad.xsize(), summed_pad.ysize()); - Image3F pooled_pad = PadImageMirror(pooled, pad2, pad2); - for (size_t c = 0; c < 3; c++) { - FastGaussian(g2, summed_pad.Plane(c), pool, &tmp2, &tmp_out); - const auto unblurred_multiplier = Set(df, 0.5f); - for (size_t y = 0; y < summed.ysize(); y++) { - float* row = summed.PlaneRow(c, y); - const float* row_blur = tmp_out.Row(y + pad2); - for (size_t x = 0; x < summed.xsize(); x += Lanes(df)) { - const auto b = Load(df, row_blur + x + pad2); - const auto o = Load(df, row + x) * unblurred_multiplier; - const auto m = IfThenElse(b > o, b, o); - Store(m, df, row + x); - } - } - } - for (size_t c = 0; c < 3; c++) { - FastGaussian(g2, pooled_pad.Plane(c), pool, &tmp2, &tmp_out); - const auto unblurred_multiplier = Set(df, 0.5f); - for (size_t y = 0; y < pooled.ysize(); y++) { - float* row = pooled.PlaneRow(c, y); - const float* row_blur = tmp_out.Row(y + pad2); - for (size_t x = 0; x < pooled.xsize(); x += Lanes(df)) { - const auto b = Load(df, row_blur + x + pad2); - const auto o = Load(df, row + x) * unblurred_multiplier; - const auto m = IfThenElse(b > o, b, o); - Store(m, df, row + x); - } - } - } - const static float kChannelMul[3] = { - 7.9644294909680253f, - 0.5700000183257159f, - 0.20267448837597055f, - }; - ImageF pooledhf44(pooled.xsize(), pooled.ysize()); - for (size_t y = 0; y < pooled.ysize(); y++) { - const float* row_in_x = pooled.ConstPlaneRow(0, y); - const float* row_in_y = pooled.ConstPlaneRow(1, y); - const float* row_in_b = pooled.ConstPlaneRow(2, y); - float* row_out = pooledhf44.Row(y); - for (size_t x = 0; x < pooled.xsize(); x += Lanes(df)) { - auto v = Set(df, kChannelMul[0]) * Load(df, row_in_x + x); - v = MulAdd(Set(df, kChannelMul[1]), Load(df, row_in_y + x), v); - v = MulAdd(Set(df, kChannelMul[2]), Load(df, row_in_b + x), v); - Store(v, df, row_out + x); - } - } - ImageF summedhf44(summed.xsize(), summed.ysize()); - for (size_t y = 0; y < summed.ysize(); y++) { - const float* row_in_x = summed.ConstPlaneRow(0, y); - const float* row_in_y = summed.ConstPlaneRow(1, y); - const float* row_in_b = summed.ConstPlaneRow(2, y); - float* row_out = summedhf44.Row(y); - for (size_t x = 0; x < summed.xsize(); x += Lanes(df)) { - auto v = Set(df, kChannelMul[0]) * Load(df, row_in_x + x); - v = MulAdd(Set(df, kChannelMul[1]), Load(df, row_in_y + x), v); - v = MulAdd(Set(df, kChannelMul[2]), Load(df, row_in_b + x), v); - Store(v, df, row_out + x); - } - } - aux_out->DumpPlaneNormalized("pooledhf44", pooledhf44); - aux_out->DumpPlaneNormalized("summedhf44", summedhf44); - - static const float kDcQuantMul = 0.88170190420916206; - static const float kAcQuantMul = 2.5165738934721524; - - float dc_quant = kDcQuantMul * InitialQuantDC(cparams.butteraugli_distance); - float ac_quant_base = kAcQuantMul / cparams.butteraugli_distance; - ImageF quant_field(frame_dim.xsize_blocks, frame_dim.ysize_blocks); - - static_assert(kColorTileDim == 64, "Fix the code below"); - auto mmacs = [&](size_t bx, size_t by, AcStrategy acs, float& min, - float& max) { - min = 1e10; - max = 0; - for (size_t y = 2 * by; y < 2 * (by + acs.covered_blocks_y()); y++) { - const float* row = summedhf44.Row(y); - for (size_t x = 2 * bx; x < 2 * (bx + acs.covered_blocks_x()); x++) { - min = std::min(min, row[x]); - max = std::max(max, row[x]); - } - } - }; - // Multipliers for allowed range of summedhf44. - std::pair<AcStrategy::Type, float> candidates[] = { - // The order is such that, in case of ties, 8x8 is favoured over 4x4 which - // is favoured over 2x2. Similarly, we prefer square transforms over - // same-area rectangular ones. - {AcStrategy::Type::DCT2X2, 1.5f}, - {AcStrategy::Type::DCT4X4, 1.4f}, - {AcStrategy::Type::DCT4X8, 1.2f}, - {AcStrategy::Type::DCT8X4, 1.2f}, - {AcStrategy::Type::AFV0, - 1.15f}, // doesn't really work with these heuristics - {AcStrategy::Type::AFV1, 1.15f}, - {AcStrategy::Type::AFV2, 1.15f}, - {AcStrategy::Type::AFV3, 1.15f}, - {AcStrategy::Type::DCT, 1.0f}, - {AcStrategy::Type::DCT16X8, 0.8f}, - {AcStrategy::Type::DCT8X16, 0.8f}, - {AcStrategy::Type::DCT16X16, 0.2f}, - {AcStrategy::Type::DCT16X32, 0.2f}, - {AcStrategy::Type::DCT32X16, 0.2f}, - {AcStrategy::Type::DCT32X32, 0.2f}, - {AcStrategy::Type::DCT32X64, 0.1f}, - {AcStrategy::Type::DCT64X32, 0.1f}, - {AcStrategy::Type::DCT64X64, 0.04f}, - -#if 0 - {AcStrategy::Type::DCT2X2, 1e+10}, {AcStrategy::Type::DCT4X4, 2.0f}, - {AcStrategy::Type::DCT, 1.0f}, {AcStrategy::Type::DCT16X8, 1.0f}, - {AcStrategy::Type::DCT8X16, 1.0f}, {AcStrategy::Type::DCT32X8, 1.0f}, - {AcStrategy::Type::DCT8X32, 1.0f}, {AcStrategy::Type::DCT32X16, 1.0f}, - {AcStrategy::Type::DCT16X32, 1.0f}, {AcStrategy::Type::DCT64X32, 1.0f}, - {AcStrategy::Type::DCT32X64, 1.0f}, {AcStrategy::Type::DCT16X16, 1.0f}, - {AcStrategy::Type::DCT32X32, 1.0f}, {AcStrategy::Type::DCT64X64, 1.0f}, -#endif - // TODO(veluca): figure out if we want 4x8 and/or AVF. - }; - float max_range = 1e-8f + 0.5f * std::pow(cparams.butteraugli_distance, 0.5f); - // Change quant field and sharpness amounts based on (pooled|summed)hf44, and - // compute block sizes. - // TODO(veluca): maybe this could be done per group: it would allow choosing - // floating blocks better. - JXL_RETURN_IF_ERROR(RunOnPool( - pool, 0, xcolortiles * ycolortiles, ThreadPool::NoInit, - [&](size_t tile_id, size_t _) { - size_t tx = tile_id % xcolortiles; - size_t ty = tile_id / xcolortiles; - size_t x0 = tx * kColorTileDim / kBlockDim; - size_t x1 = std::min(x0 + kColorTileDimInBlocks, quant_field.xsize()); - size_t y0 = ty * kColorTileDim / kBlockDim; - size_t y1 = std::min(y0 + kColorTileDimInBlocks, quant_field.ysize()); - size_t qf_stride = quant_field.PixelsPerRow(); - size_t epf_stride = shared.epf_sharpness.PixelsPerRow(); - bool chosen_mask[64] = {}; - for (size_t y = y0; y < y1; y++) { - uint8_t* epf_row = shared.epf_sharpness.Row(y); - float* qf_row = quant_field.Row(y); - for (size_t x = x0; x < x1; x++) { - if (chosen_mask[(y - y0) * 8 + (x - x0)]) continue; - // Default to DCT8 just in case something funny happens in the loop - // below. - AcStrategy::Type best = AcStrategy::DCT; - size_t best_covered = 1; - float qf = ac_quant_base; - for (size_t i = 0; i < sizeof(candidates) / sizeof(*candidates); - i++) { - AcStrategy acs = AcStrategy::FromRawStrategy(candidates[i].first); - if (y + acs.covered_blocks_y() > y1) continue; - if (x + acs.covered_blocks_x() > x1) continue; - bool fits = true; - for (size_t iy = y; iy < y + acs.covered_blocks_y(); iy++) { - for (size_t ix = x; ix < x + acs.covered_blocks_x(); ix++) { - if (chosen_mask[(iy - y0) * 8 + (ix - x0)]) { - fits = false; - break; - } - } - } - if (!fits) continue; - float min, max; - mmacs(x, y, acs, min, max); - if (max - min > max_range * candidates[i].second) continue; - size_t cb = acs.covered_blocks_x() * acs.covered_blocks_y(); - if (cb >= best_covered) { - best_covered = cb; - best = candidates[i].first; - // TODO(veluca): make this better. - qf = ac_quant_base / - (3.9312946339134007f + 2.6011435675118082f * min); - } - } - shared.ac_strategy.Set(x, y, best); - AcStrategy acs = AcStrategy::FromRawStrategy(best); - for (size_t iy = y; iy < y + acs.covered_blocks_y(); iy++) { - for (size_t ix = x; ix < x + acs.covered_blocks_x(); ix++) { - chosen_mask[(iy - y0) * 8 + (ix - x0)] = 1; - qf_row[ix + (iy - y) * qf_stride] = qf; - } - } - // TODO - for (size_t iy = y; iy < y + acs.covered_blocks_y(); iy++) { - for (size_t ix = x; ix < x + acs.covered_blocks_x(); ix++) { - epf_row[ix + (iy - y) * epf_stride] = 4; - } - } - } - } - }, - "QF+ACS+EPF")); - aux_out->DumpPlaneNormalized("qf", quant_field); - aux_out->DumpPlaneNormalized("epf", shared.epf_sharpness); - DumpAcStrategy(shared.ac_strategy, frame_dim.xsize_padded, - frame_dim.ysize_padded, "acs", aux_out); - - shared.quantizer.SetQuantField(dc_quant, quant_field, - &shared.raw_quant_field); - - return true; -} -} // namespace -// NOLINTNEXTLINE(google-readability-namespace-comments) -} // namespace HWY_NAMESPACE -} // namespace jxl -HWY_AFTER_NAMESPACE(); - -#if HWY_ONCE -namespace jxl { -HWY_EXPORT(Heuristics); -Status FastEncoderHeuristics::LossyFrameHeuristics( - PassesEncoderState* enc_state, ModularFrameEncoder* modular_frame_encoder, - const ImageBundle* linear, Image3F* opsin, const JxlCmsInterface& cms, - ThreadPool* pool, AuxOut* aux_out) { - return HWY_DYNAMIC_DISPATCH(Heuristics)(enc_state, modular_frame_encoder, - linear, opsin, pool, aux_out); -} - -} // namespace jxl -#endif diff --git a/media/libjxl/src/lib/jxl/enc_file.cc b/media/libjxl/src/lib/jxl/enc_file.cc index 45ae82fdf3..0f29bd92df 100644 --- a/media/libjxl/src/lib/jxl/enc_file.cc +++ b/media/libjxl/src/lib/jxl/enc_file.cc @@ -20,7 +20,6 @@ #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_frame.h" #include "lib/jxl/enc_icc_codec.h" -#include "lib/jxl/exif.h" #include "lib/jxl/frame_header.h" #include "lib/jxl/headers.h" #include "lib/jxl/image_bundle.h" @@ -84,8 +83,8 @@ Status PrepareCodecMetadataFromIO(const CompressParams& cparams, metadata->m.xyb_encoded = cparams.color_transform == ColorTransform::kXYB ? true : false; - InterpretExif(io->blobs.exif, metadata); - + // TODO(firsching): move this EncodeFile to test_utils / re-implement this + // using API functions return true; } @@ -170,7 +169,9 @@ Status EncodeFile(const CompressParams& params, const CodecInOut* io, } // Each frame should start on byte boundaries. + BitWriter::Allotment allotment(&writer, 8); writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, kLayerHeader, aux_out); if (cparams.progressive_mode || cparams.qprogressive_mode) { if (cparams.saliency_map != nullptr) { diff --git a/media/libjxl/src/lib/jxl/enc_frame.cc b/media/libjxl/src/lib/jxl/enc_frame.cc index cad6c4699b..f57175ba0a 100644 --- a/media/libjxl/src/lib/jxl/enc_frame.cc +++ b/media/libjxl/src/lib/jxl/enc_frame.cc @@ -497,7 +497,6 @@ class LossyFrameEncoder { Image3F* JXL_RESTRICT opsin, const JxlCmsInterface& cms, ThreadPool* pool, ModularFrameEncoder* modular_frame_encoder, - BitWriter* JXL_RESTRICT writer, FrameHeader* frame_header) { PROFILER_ZONE("ComputeEncodingData uninstrumented"); JXL_ASSERT((opsin->xsize() % kBlockDim) == 0 && @@ -605,7 +604,7 @@ class LossyFrameEncoder { std::vector<int> qt(192); for (size_t c = 0; c < 3; c++) { size_t jpeg_c = jpeg_c_map[c]; - const int* quant = + const int32_t* quant = jpeg_data.quant[jpeg_data.components[jpeg_c].quant_idx].values.data(); dcquantization[c] = 255 * 8.0f / quant[0]; @@ -630,7 +629,7 @@ class LossyFrameEncoder { shared.quantizer.RecomputeFromGlobalScale(); // Per-block dequant scaling should be 1. - FillImage(static_cast<int>(shared.quantizer.InvGlobalScale()), + FillImage(static_cast<int32_t>(shared.quantizer.InvGlobalScale()), &shared.raw_quant_field); std::vector<int32_t> scaled_qtable(192); @@ -936,8 +935,8 @@ class LossyFrameEncoder { Status EncodeGlobalACInfo(BitWriter* writer, ModularFrameEncoder* modular_frame_encoder) { JXL_RETURN_IF_ERROR(DequantMatricesEncode(&enc_state_->shared.matrices, - writer, kLayerDequantTables, - aux_out_, modular_frame_encoder)); + writer, kLayerQuant, aux_out_, + modular_frame_encoder)); if (enc_state_->cparams.speed_tier <= SpeedTier::kTortoise) { if (!doing_jpeg_recompression) ClusterGroups(enc_state_); } @@ -1050,6 +1049,12 @@ Status ParamsPostInit(CompressParams* p) { if (!p->manual_xyb_factors.empty() && p->manual_xyb_factors.size() != 3) { return JXL_FAILURE("Invalid number of XYB quantization factors"); } + if (!p->modular_mode && p->butteraugli_distance == 0.0) { + p->butteraugli_distance = kMinButteraugliDistance; + } + if (p->original_butteraugli_distance == -1.0) { + p->original_butteraugli_distance = p->butteraugli_distance; + } if (p->resampling <= 0) { p->resampling = 1; // For very low bit rates, using 2x2 resampling gives better results on @@ -1071,11 +1076,57 @@ Status EncodeFrame(const CompressParams& cparams_orig, const ImageBundle& ib, PassesEncoderState* passes_enc_state, const JxlCmsInterface& cms, ThreadPool* pool, BitWriter* writer, AuxOut* aux_out) { + CompressParams cparams = cparams_orig; + if (cparams_orig.target_bitrate > 0.0f && + frame_info.frame_type == FrameType::kRegularFrame) { + cparams.target_bitrate = 0.0f; + const float target_bitrate = cparams_orig.target_bitrate; + float bitrate = 0.0f; + float prev_bitrate = 0.0f; + float rescale = 1.0f; + size_t prev_bits = 0; + float error = 0.0f; + float best_error = 100.0f; + float best_rescale = 1.0f; + for (size_t i = 0; i < 10; ++i) { + std::unique_ptr<PassesEncoderState> state = + jxl::make_unique<PassesEncoderState>(); + BitWriter bw; + JXL_CHECK(EncodeFrame(cparams, frame_info, metadata, ib, state.get(), cms, + pool, &bw, nullptr)); + bitrate = bw.BitsWritten() * 1.0 / (ib.xsize() * ib.ysize()); + error = target_bitrate / bitrate - 1.0f; + if (std::abs(error) < std::abs(best_error)) { + best_error = error; + best_rescale = cparams.quant_ac_rescale; + } + if (bw.BitsWritten() == prev_bits || std::abs(error) < 0.0005f) { + break; + } + float lambda = 1.0f; + if (i > 0) { + lambda = (((bitrate / prev_bitrate) - 1.0f) / (rescale - 1.0f)); + } + rescale = (1.0f + ((target_bitrate / bitrate) - 1.0f) / lambda); + if (rescale < 0.0f) { + break; + } + cparams.quant_ac_rescale *= rescale; + prev_bitrate = bitrate; + prev_bits = bw.BitsWritten(); + } + if (aux_out) { + aux_out->max_quant_rescale = best_rescale; + aux_out->min_quant_rescale = best_rescale; + aux_out->min_bitrate_error = best_error; + aux_out->max_bitrate_error = best_error; + } + cparams.quant_ac_rescale = best_rescale; + } ib.VerifyMetadata(); passes_enc_state->special_frames.clear(); - CompressParams cparams = cparams_orig; JXL_RETURN_IF_ERROR(ParamsPostInit(&cparams)); if (cparams.progressive_dc < 0) { @@ -1084,16 +1135,20 @@ Status EncodeFrame(const CompressParams& cparams_orig, cparams.progressive_dc); } cparams.progressive_dc = 0; - // Enable progressive_dc for lower qualities. - if (cparams.butteraugli_distance >= - kMinButteraugliDistanceForProgressiveDc) { + // Enable progressive_dc for lower qualities, except for fast speeds where + // the modular encoder uses fixed tree. + if (cparams.speed_tier <= SpeedTier::kCheetah && + cparams.butteraugli_distance >= + kMinButteraugliDistanceForProgressiveDc) { cparams.progressive_dc = 1; } } if (cparams.ec_resampling < cparams.resampling) { cparams.ec_resampling = cparams.resampling; } - if (cparams.resampling > 1) cparams.progressive_dc = 0; + if (cparams.resampling > 1 || frame_info.is_preview) { + cparams.progressive_dc = 0; + } if (frame_info.dc_level + cparams.progressive_dc > 4) { return JXL_FAILURE("Too many levels of progressive DC"); @@ -1240,7 +1295,7 @@ Status EncodeFrame(const CompressParams& cparams_orig, if (frame_header->encoding == FrameEncoding::kVarDCT) { PadImageToBlockMultipleInPlace(&opsin); JXL_RETURN_IF_ERROR(lossy_frame_encoder.ComputeEncodingData( - ib_or_linear, &opsin, cms, pool, modular_frame_encoder.get(), writer, + ib_or_linear, &opsin, cms, pool, modular_frame_encoder.get(), frame_header.get())); } else if (frame_header->upsampling != 1 && !cparams.already_downsampled) { // In VarDCT mode, LossyFrameHeuristics takes care of running downsampling @@ -1249,7 +1304,7 @@ Status EncodeFrame(const CompressParams& cparams_orig, } } else { JXL_RETURN_IF_ERROR(lossy_frame_encoder.ComputeEncodingData( - &ib, &opsin, cms, pool, modular_frame_encoder.get(), writer, + &ib, &opsin, cms, pool, modular_frame_encoder.get(), frame_header.get())); } if (cparams.ec_resampling != 1 && !cparams.already_downsampled) { @@ -1321,7 +1376,7 @@ Status EncodeFrame(const CompressParams& cparams_orig, JXL_RETURN_IF_ERROR( DequantMatricesEncodeDC(&lossy_frame_encoder.State()->shared.matrices, - get_output(0), kLayerDequantTables, aux_out)); + get_output(0), kLayerQuant, aux_out)); if (frame_header->encoding == FrameEncoding::kVarDCT) { JXL_RETURN_IF_ERROR( lossy_frame_encoder.EncodeGlobalDCInfo(*frame_header, get_output(0))); @@ -1401,7 +1456,9 @@ Status EncodeFrame(const CompressParams& cparams_orig, JXL_RETURN_IF_ERROR(num_errors.load(std::memory_order_relaxed) == 0); for (BitWriter& bw : group_codes) { + BitWriter::Allotment allotment(&bw, 8); bw.ZeroPadToByte(); // end of group. + ReclaimAndCharge(&bw, &allotment, kLayerAC, aux_out); } std::vector<coeff_order_t>* permutation_ptr = nullptr; @@ -1483,7 +1540,6 @@ Status EncodeFrame(const CompressParams& cparams_orig, JXL_RETURN_IF_ERROR( WriteGroupOffsets(group_codes, permutation_ptr, writer, aux_out)); writer->AppendByteAligned(group_codes); - writer->ZeroPadToByte(); // end of frame. return true; } diff --git a/media/libjxl/src/lib/jxl/enc_group.cc b/media/libjxl/src/lib/jxl/enc_group.cc index 5ec53d6618..bf853064ee 100644 --- a/media/libjxl/src/lib/jxl/enc_group.cc +++ b/media/libjxl/src/lib/jxl/enc_group.cc @@ -32,6 +32,14 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Abs; +using hwy::HWY_NAMESPACE::Ge; +using hwy::HWY_NAMESPACE::IfThenElse; +using hwy::HWY_NAMESPACE::IfThenElseZero; +using hwy::HWY_NAMESPACE::MaskFromVec; +using hwy::HWY_NAMESPACE::Round; + // NOTE: caller takes care of extracting quant from rect of RawQuantField. void QuantizeBlockAC(const Quantizer& quantizer, const bool error_diffusion, size_t c, int32_t quant, float qm_multiplier, @@ -83,10 +91,10 @@ void QuantizeBlockAC(const Quantizer& quantizer, const bool error_diffusion, thres[yfix + static_cast<size_t>(x >= xsize * kBlockDim / 2)]); } - const auto q = Load(df, qm + off + x) * quant; + const auto q = Mul(Load(df, qm + off + x), quant); const auto in = Load(df, block_in + off + x); - const auto val = q * in; - const auto nzero_mask = Abs(val) >= thr; + const auto val = Mul(q, in); + const auto nzero_mask = Ge(Abs(val), thr); const auto v = ConvertTo(di, IfThenElseZero(nzero_mask, Round(val))); Store(v, di, block_out + off + x); } @@ -174,7 +182,7 @@ void QuantizeRoundtripYBlockAC(const Quantizer& quantizer, const auto quant = Load(di, quantized + k); const auto adj_quant = AdjustQuantBias(di, 1, quant, biases); const auto dequantm = Load(df, dequant_matrix + k); - Store(adj_quant * dequantm * inv_qac, df, inout + k); + Store(Mul(Mul(adj_quant, dequantm), inv_qac), df, inout + k); } } @@ -286,8 +294,8 @@ void ComputeCoefficients(size_t group_idx, PassesEncoderState* enc_state, const auto in_x = Load(d, coeffs_in + k); const auto in_y = Load(d, coeffs_in + size + k); const auto in_b = Load(d, coeffs_in + 2 * size + k); - const auto out_x = in_x - x_factor * in_y; - const auto out_b = in_b - b_factor * in_y; + const auto out_x = NegMulAdd(x_factor, in_y, in_x); + const auto out_b = NegMulAdd(b_factor, in_y, in_b); Store(out_x, d, coeffs_in + k); Store(out_b, d, coeffs_in + 2 * size + k); } diff --git a/media/libjxl/src/lib/jxl/enc_heuristics.cc b/media/libjxl/src/lib/jxl/enc_heuristics.cc index 090852b765..1ab4ea56c5 100644 --- a/media/libjxl/src/lib/jxl/enc_heuristics.cc +++ b/media/libjxl/src/lib/jxl/enc_heuristics.cc @@ -164,19 +164,6 @@ void FindBestBlockEntropyModel(PassesEncoderState& enc_state) { *std::max_element(ctx_map.begin(), ctx_map.end()) + 1; } -// Returns the target size based on whether bitrate or direct targetsize is -// given. -size_t TargetSize(const CompressParams& cparams, - const FrameDimensions& frame_dim) { - if (cparams.target_size > 0) { - return cparams.target_size; - } - if (cparams.target_bitrate > 0.0) { - return 0.5 + cparams.target_bitrate * frame_dim.xsize * frame_dim.ysize / - kBitsPerByte; - } - return 0; -} } // namespace void FindBestDequantMatrices(const CompressParams& cparams, @@ -772,12 +759,7 @@ Status DefaultEncoderHeuristics::LossyFrameHeuristics( PadImageToBlockMultipleInPlace(opsin); } - const FrameDimensions& frame_dim = enc_state->shared.frame_dim; - size_t target_size = TargetSize(cparams, frame_dim); - size_t opsin_target_size = target_size; - if (cparams.target_size > 0 || cparams.target_bitrate > 0.0) { - cparams.target_size = opsin_target_size; - } else if (cparams.butteraugli_distance < 0) { + if (cparams.butteraugli_distance < 0) { return JXL_FAILURE("Expected non-negative distance"); } @@ -859,6 +841,7 @@ Status DefaultEncoderHeuristics::LossyFrameHeuristics( enc_state->initial_quant_field = InitialQuantField( butteraugli_distance_for_iqf, *opsin, shared.frame_dim, pool, 1.0f, &enc_state->initial_quant_masking); + quantizer.SetQuantField(quant_dc, enc_state->initial_quant_field, nullptr); } // TODO(veluca): do something about animations. diff --git a/media/libjxl/src/lib/jxl/enc_heuristics.h b/media/libjxl/src/lib/jxl/enc_heuristics.h index 4e910efe94..16509f00df 100644 --- a/media/libjxl/src/lib/jxl/enc_heuristics.h +++ b/media/libjxl/src/lib/jxl/enc_heuristics.h @@ -67,15 +67,6 @@ class DefaultEncoderHeuristics : public EncoderHeuristics { const ImageBundle& ib) override; }; -class FastEncoderHeuristics : public EncoderHeuristics { - public: - Status LossyFrameHeuristics(PassesEncoderState* enc_state, - ModularFrameEncoder* modular_frame_encoder, - const ImageBundle* linear, Image3F* opsin, - const JxlCmsInterface& cms, ThreadPool* pool, - AuxOut* aux_out) override; -}; - // Exposed here since it may be used by other EncoderHeuristics implementations // outside this project. void FindBestDequantMatrices(const CompressParams& cparams, diff --git a/media/libjxl/src/lib/jxl/enc_image_bundle.cc b/media/libjxl/src/lib/jxl/enc_image_bundle.cc index fe062475e6..fe6d282a8e 100644 --- a/media/libjxl/src/lib/jxl/enc_image_bundle.cc +++ b/media/libjxl/src/lib/jxl/enc_image_bundle.cc @@ -5,6 +5,7 @@ #include "lib/jxl/enc_image_bundle.h" +#include <atomic> #include <limits> #include <utility> diff --git a/media/libjxl/src/lib/jxl/enc_modular.cc b/media/libjxl/src/lib/jxl/enc_modular.cc index 0ca27d9061..9e34fe8753 100644 --- a/media/libjxl/src/lib/jxl/enc_modular.cc +++ b/media/libjxl/src/lib/jxl/enc_modular.cc @@ -9,6 +9,7 @@ #include <stdint.h> #include <array> +#include <atomic> #include <limits> #include <queue> #include <utility> @@ -164,8 +165,7 @@ Tree PredefinedTree(ModularOptions::TreeKind tree_kind, size_t total_pixels) { -500, -392, -255, -191, -127, -95, -63, -47, -31, -23, -15, -11, -7, -4, -3, -1, 0, 1, 3, 5, 7, 11, 15, 23, 31, 47, 63, 95, 127, 191, 255, 392, 500}; - return MakeFixedTree(kNumNonrefProperties - weighted::kNumProperties, - cutoffs, Predictor::Weighted, total_pixels); + return MakeFixedTree(kWPProp, cutoffs, Predictor::Weighted, total_pixels); } if (tree_kind == ModularOptions::TreeKind::kGradientFixedDC) { std::vector<int32_t> cutoffs = { @@ -300,60 +300,60 @@ Status float_to_int(const float* const row_in, pixel_type* const row_out, ModularFrameEncoder::ModularFrameEncoder(const FrameHeader& frame_header, const CompressParams& cparams_orig) - : frame_dim(frame_header.ToFrameDimensions()), cparams(cparams_orig) { + : frame_dim_(frame_header.ToFrameDimensions()), cparams_(cparams_orig) { size_t num_streams = - ModularStreamId::Num(frame_dim, frame_header.passes.num_passes); - if (cparams.IsLossless()) { - switch (cparams.decoding_speed_tier) { + ModularStreamId::Num(frame_dim_, frame_header.passes.num_passes); + if (cparams_.IsLossless()) { + switch (cparams_.decoding_speed_tier) { case 0: break; case 1: - cparams.options.wp_tree_mode = ModularOptions::TreeMode::kWPOnly; + cparams_.options.wp_tree_mode = ModularOptions::TreeMode::kWPOnly; break; case 2: { - cparams.options.wp_tree_mode = ModularOptions::TreeMode::kGradientOnly; - cparams.options.predictor = Predictor::Gradient; + cparams_.options.wp_tree_mode = ModularOptions::TreeMode::kGradientOnly; + cparams_.options.predictor = Predictor::Gradient; break; } case 3: { // LZ77, no Gradient. - cparams.options.nb_repeats = 0; - cparams.options.predictor = Predictor::Gradient; + cparams_.options.nb_repeats = 0; + cparams_.options.predictor = Predictor::Gradient; break; } default: { // LZ77, no predictor. - cparams.options.nb_repeats = 0; - cparams.options.predictor = Predictor::Zero; + cparams_.options.nb_repeats = 0; + cparams_.options.predictor = Predictor::Zero; break; } } } - if (cparams.decoding_speed_tier >= 1 && cparams.responsive && - cparams.IsLossless()) { - cparams.options.tree_kind = + if (cparams_.decoding_speed_tier >= 1 && cparams_.responsive && + cparams_.IsLossless()) { + cparams_.options.tree_kind = ModularOptions::TreeKind::kTrivialTreeNoPredictor; - cparams.options.nb_repeats = 0; + cparams_.options.nb_repeats = 0; } - stream_images.resize(num_streams); + stream_images_.resize(num_streams); // use a sensible default if nothing explicit is specified: // Squeeze for lossy, no squeeze for lossless - if (cparams.responsive < 0) { - if (cparams.IsLossless()) { - cparams.responsive = 0; + if (cparams_.responsive < 0) { + if (cparams_.IsLossless()) { + cparams_.responsive = 0; } else { - cparams.responsive = 1; + cparams_.responsive = 1; } } - if (cparams.speed_tier > SpeedTier::kWombat) { - cparams.options.splitting_heuristics_node_threshold = 192; + if (cparams_.speed_tier > SpeedTier::kWombat) { + cparams_.options.splitting_heuristics_node_threshold = 192; } else { - cparams.options.splitting_heuristics_node_threshold = 96; + cparams_.options.splitting_heuristics_node_threshold = 96; } { // Set properties. std::vector<uint32_t> prop_order; - if (cparams.responsive) { + if (cparams_.responsive) { // Properties in order of their likelihood of being useful for Squeeze // residuals. prop_order = {0, 1, 4, 5, 6, 7, 8, 15, 9, 10, 11, 12, 13, 14, 2, 3}; @@ -361,93 +361,93 @@ ModularFrameEncoder::ModularFrameEncoder(const FrameHeader& frame_header, // Same, but for the non-Squeeze case. prop_order = {0, 1, 15, 9, 10, 11, 12, 13, 14, 2, 3, 4, 5, 6, 7, 8}; } - switch (cparams.speed_tier) { + switch (cparams_.speed_tier) { case SpeedTier::kSquirrel: - cparams.options.splitting_heuristics_properties.assign( + cparams_.options.splitting_heuristics_properties.assign( prop_order.begin(), prop_order.begin() + 8); - cparams.options.max_property_values = 32; + cparams_.options.max_property_values = 32; break; case SpeedTier::kKitten: - cparams.options.splitting_heuristics_properties.assign( + cparams_.options.splitting_heuristics_properties.assign( prop_order.begin(), prop_order.begin() + 10); - cparams.options.max_property_values = 64; + cparams_.options.max_property_values = 64; break; case SpeedTier::kTortoise: - cparams.options.splitting_heuristics_properties = prop_order; - cparams.options.max_property_values = 256; + cparams_.options.splitting_heuristics_properties = prop_order; + cparams_.options.max_property_values = 256; break; default: - cparams.options.splitting_heuristics_properties.assign( + cparams_.options.splitting_heuristics_properties.assign( prop_order.begin(), prop_order.begin() + 6); - cparams.options.max_property_values = 16; + cparams_.options.max_property_values = 16; break; } - if (cparams.speed_tier > SpeedTier::kTortoise) { + if (cparams_.speed_tier > SpeedTier::kTortoise) { // Gradient in previous channels. - for (int i = 0; i < cparams.options.max_properties; i++) { - cparams.options.splitting_heuristics_properties.push_back( + for (int i = 0; i < cparams_.options.max_properties; i++) { + cparams_.options.splitting_heuristics_properties.push_back( kNumNonrefProperties + i * 4 + 3); } } else { // All the extra properties in Tortoise mode. - for (int i = 0; i < cparams.options.max_properties * 4; i++) { - cparams.options.splitting_heuristics_properties.push_back( + for (int i = 0; i < cparams_.options.max_properties * 4; i++) { + cparams_.options.splitting_heuristics_properties.push_back( kNumNonrefProperties + i); } } } - if (cparams.options.predictor == static_cast<Predictor>(-1)) { + if (cparams_.options.predictor == static_cast<Predictor>(-1)) { // no explicit predictor(s) given, set a good default - if ((cparams.speed_tier <= SpeedTier::kTortoise || - cparams.modular_mode == false) && - cparams.IsLossless() && cparams.responsive == false) { + if ((cparams_.speed_tier <= SpeedTier::kTortoise || + cparams_.modular_mode == false) && + cparams_.IsLossless() && cparams_.responsive == false) { // TODO(veluca): allow all predictors that don't break residual // multipliers in lossy mode. - cparams.options.predictor = Predictor::Variable; - } else if (cparams.responsive || cparams.lossy_palette) { + cparams_.options.predictor = Predictor::Variable; + } else if (cparams_.responsive || cparams_.lossy_palette) { // zero predictor for Squeeze residues and lossy palette - cparams.options.predictor = Predictor::Zero; - } else if (!cparams.IsLossless()) { + cparams_.options.predictor = Predictor::Zero; + } else if (!cparams_.IsLossless()) { // If not responsive and lossy. TODO(veluca): use near_lossless instead? - cparams.options.predictor = Predictor::Gradient; - } else if (cparams.speed_tier < SpeedTier::kFalcon) { + cparams_.options.predictor = Predictor::Gradient; + } else if (cparams_.speed_tier < SpeedTier::kFalcon) { // try median and weighted predictor for anything else - cparams.options.predictor = Predictor::Best; - } else if (cparams.speed_tier == SpeedTier::kFalcon) { + cparams_.options.predictor = Predictor::Best; + } else if (cparams_.speed_tier == SpeedTier::kFalcon) { // just weighted predictor in falcon mode - cparams.options.predictor = Predictor::Weighted; - } else if (cparams.speed_tier > SpeedTier::kFalcon) { + cparams_.options.predictor = Predictor::Weighted; + } else if (cparams_.speed_tier > SpeedTier::kFalcon) { // just gradient predictor in thunder mode - cparams.options.predictor = Predictor::Gradient; + cparams_.options.predictor = Predictor::Gradient; } } else { - delta_pred = cparams.options.predictor; - if (cparams.lossy_palette) cparams.options.predictor = Predictor::Zero; - } - if (!cparams.IsLossless()) { - if (cparams.options.predictor == Predictor::Weighted || - cparams.options.predictor == Predictor::Variable || - cparams.options.predictor == Predictor::Best) - cparams.options.predictor = Predictor::Zero; - } - tree_splits.push_back(0); - if (cparams.modular_mode == false) { - cparams.options.fast_decode_multiplier = 1.0f; - tree_splits.push_back(ModularStreamId::VarDCTDC(0).ID(frame_dim)); - tree_splits.push_back(ModularStreamId::ModularDC(0).ID(frame_dim)); - tree_splits.push_back(ModularStreamId::ACMetadata(0).ID(frame_dim)); - tree_splits.push_back(ModularStreamId::QuantTable(0).ID(frame_dim)); - tree_splits.push_back(ModularStreamId::ModularAC(0, 0).ID(frame_dim)); - ac_metadata_size.resize(frame_dim.num_dc_groups); - extra_dc_precision.resize(frame_dim.num_dc_groups); - } - tree_splits.push_back(num_streams); - cparams.options.max_chan_size = frame_dim.group_dim; - cparams.options.group_dim = frame_dim.group_dim; + delta_pred_ = cparams_.options.predictor; + if (cparams_.lossy_palette) cparams_.options.predictor = Predictor::Zero; + } + if (!cparams_.IsLossless()) { + if (cparams_.options.predictor == Predictor::Weighted || + cparams_.options.predictor == Predictor::Variable || + cparams_.options.predictor == Predictor::Best) + cparams_.options.predictor = Predictor::Zero; + } + tree_splits_.push_back(0); + if (cparams_.modular_mode == false) { + cparams_.options.fast_decode_multiplier = 1.0f; + tree_splits_.push_back(ModularStreamId::VarDCTDC(0).ID(frame_dim_)); + tree_splits_.push_back(ModularStreamId::ModularDC(0).ID(frame_dim_)); + tree_splits_.push_back(ModularStreamId::ACMetadata(0).ID(frame_dim_)); + tree_splits_.push_back(ModularStreamId::QuantTable(0).ID(frame_dim_)); + tree_splits_.push_back(ModularStreamId::ModularAC(0, 0).ID(frame_dim_)); + ac_metadata_size.resize(frame_dim_.num_dc_groups); + extra_dc_precision.resize(frame_dim_.num_dc_groups); + } + tree_splits_.push_back(num_streams); + cparams_.options.max_chan_size = frame_dim_.group_dim; + cparams_.options.group_dim = frame_dim_.group_dim; // TODO(veluca): figure out how to use different predictor sets per channel. - stream_options.resize(num_streams, cparams.options); + stream_options_.resize(num_streams, cparams_.options); } bool do_transform(Image& image, const Transform& tr, @@ -469,28 +469,29 @@ Status ModularFrameEncoder::ComputeEncodingData( Image3F* JXL_RESTRICT color, const std::vector<ImageF>& extra_channels, PassesEncoderState* JXL_RESTRICT enc_state, const JxlCmsInterface& cms, ThreadPool* pool, AuxOut* aux_out, bool do_color) { - const FrameDimensions& frame_dim = enc_state->shared.frame_dim; + JXL_DEBUG_V(6, "Computing modular encoding data for frame %s", + frame_header.DebugString().c_str()); if (do_color && frame_header.loop_filter.gab) { GaborishInverse(color, 0.9908511000000001f, pool); } if (do_color && metadata.bit_depth.bits_per_sample <= 16 && - cparams.speed_tier < SpeedTier::kCheetah && - cparams.decoding_speed_tier < 2) { + cparams_.speed_tier < SpeedTier::kCheetah && + cparams_.decoding_speed_tier < 2) { FindBestPatchDictionary(*color, enc_state, cms, nullptr, aux_out, - cparams.color_transform == ColorTransform::kXYB); + cparams_.color_transform == ColorTransform::kXYB); PatchDictionaryEncoder::SubtractFrom( enc_state->shared.image_features.patches, color); } // Convert ImageBundle to modular Image object - const size_t xsize = frame_dim.xsize; - const size_t ysize = frame_dim.ysize; + const size_t xsize = frame_dim_.xsize; + const size_t ysize = frame_dim_.ysize; int nb_chans = 3; if (metadata.color_encoding.IsGray() && - cparams.color_transform == ColorTransform::kNone) { + cparams_.color_transform == ColorTransform::kNone) { nb_chans = 1; } if (!do_color) nb_chans = 0; @@ -498,11 +499,11 @@ Status ModularFrameEncoder::ComputeEncodingData( nb_chans += extra_channels.size(); bool fp = metadata.bit_depth.floating_point_sample && - cparams.color_transform != ColorTransform::kXYB; + cparams_.color_transform != ColorTransform::kXYB; // bits_per_sample is just metadata for XYB images. if (metadata.bit_depth.bits_per_sample >= 32 && do_color && - cparams.color_transform != ColorTransform::kXYB) { + cparams_.color_transform != ColorTransform::kXYB) { if (metadata.bit_depth.bits_per_sample == 32 && fp == false) { return JXL_FAILURE("uint32_t not supported in enc_modular"); } else if (metadata.bit_depth.bits_per_sample > 32) { @@ -513,22 +514,22 @@ Status ModularFrameEncoder::ComputeEncodingData( // in the non-float case, there is an implicit 0 sign bit int max_bitdepth = do_color ? metadata.bit_depth.bits_per_sample + (fp ? 0 : 1) : 0; - Image& gi = stream_images[0]; + Image& gi = stream_images_[0]; gi = Image(xsize, ysize, metadata.bit_depth.bits_per_sample, nb_chans); int c = 0; - if (cparams.color_transform == ColorTransform::kXYB && - cparams.modular_mode == true) { + if (cparams_.color_transform == ColorTransform::kXYB && + cparams_.modular_mode == true) { float enc_factors[3] = {32768.0f, 2048.0f, 2048.0f}; - if (cparams.butteraugli_distance > 0 && !cparams.responsive) { + if (cparams_.butteraugli_distance > 0 && !cparams_.responsive) { // quantize XYB here and then treat it as a lossless image - enc_factors[0] *= 1.f / (1.f + 23.f * cparams.butteraugli_distance); - enc_factors[1] *= 1.f / (1.f + 14.f * cparams.butteraugli_distance); - enc_factors[2] *= 1.f / (1.f + 14.f * cparams.butteraugli_distance); - cparams.butteraugli_distance = 0; + enc_factors[0] *= 1.f / (1.f + 23.f * cparams_.butteraugli_distance); + enc_factors[1] *= 1.f / (1.f + 14.f * cparams_.butteraugli_distance); + enc_factors[2] *= 1.f / (1.f + 14.f * cparams_.butteraugli_distance); + cparams_.butteraugli_distance = 0; } - if (cparams.manual_xyb_factors.size() == 3) { + if (cparams_.manual_xyb_factors.size() == 3) { DequantMatricesSetCustomDC(&enc_state->shared.matrices, - cparams.manual_xyb_factors.data()); + cparams_.manual_xyb_factors.data()); // TODO(jon): update max_bitdepth in this case } else { DequantMatricesSetCustomDC(&enc_state->shared.matrices, enc_factors); @@ -539,17 +540,17 @@ Status ModularFrameEncoder::ComputeEncodingData( if (do_color) { for (; c < 3; c++) { if (metadata.color_encoding.IsGray() && - cparams.color_transform == ColorTransform::kNone && - c != (cparams.color_transform == ColorTransform::kXYB ? 1 : 0)) + cparams_.color_transform == ColorTransform::kNone && + c != (cparams_.color_transform == ColorTransform::kXYB ? 1 : 0)) continue; int c_out = c; // XYB is encoded as YX(B-Y) - if (cparams.color_transform == ColorTransform::kXYB && c < 2) + if (cparams_.color_transform == ColorTransform::kXYB && c < 2) c_out = 1 - c_out; double factor = maxval; - if (cparams.color_transform == ColorTransform::kXYB) + if (cparams_.color_transform == ColorTransform::kXYB) factor = enc_state->shared.matrices.InvDCQuant(c); - if (c == 2 && cparams.color_transform == ColorTransform::kXYB) { + if (c == 2 && cparams_.color_transform == ColorTransform::kXYB) { JXL_ASSERT(!fp); for (size_t y = 0; y < ysize; ++y) { const float* const JXL_RESTRICT row_in = color->PlaneRow(c, y); @@ -591,15 +592,15 @@ Status ModularFrameEncoder::ComputeEncodingData( } } if (metadata.color_encoding.IsGray() && - cparams.color_transform == ColorTransform::kNone) + cparams_.color_transform == ColorTransform::kNone) c = 1; } for (size_t ec = 0; ec < extra_channels.size(); ec++, c++) { const ExtraChannelInfo& eci = metadata.extra_channel_info[ec]; size_t ecups = frame_header.extra_channel_upsampling[ec]; - gi.channel[c].shrink(DivCeil(frame_dim.xsize_upsampled, ecups), - DivCeil(frame_dim.ysize_upsampled, ecups)); + gi.channel[c].shrink(DivCeil(frame_dim_.xsize_upsampled, ecups), + DivCeil(frame_dim_.ysize_upsampled, ecups)); gi.channel[c].hshift = gi.channel[c].vshift = CeilLog2Nonzero(ecups) - CeilLog2Nonzero(frame_header.upsampling); @@ -625,25 +626,25 @@ Status ModularFrameEncoder::ComputeEncodingData( } JXL_ASSERT(c == nb_chans); - int level_max_bitdepth = (cparams.level == 5 ? 16 : 32); + int level_max_bitdepth = (cparams_.level == 5 ? 16 : 32); if (max_bitdepth > level_max_bitdepth) return JXL_FAILURE( "Bitdepth too high for level %i (need %i bits, have only %i in this " "level)", - cparams.level, max_bitdepth, level_max_bitdepth); + cparams_.level, max_bitdepth, level_max_bitdepth); // Set options and apply transformations - if (cparams.butteraugli_distance > 0) { - if (cparams.palette_colors != 0) { + if (cparams_.butteraugli_distance > 0) { + if (cparams_.palette_colors != 0) { JXL_DEBUG_V(3, "Lossy encode, not doing palette transforms"); } - if (cparams.color_transform == ColorTransform::kXYB) { - cparams.channel_colors_pre_transform_percent = 0; + if (cparams_.color_transform == ColorTransform::kXYB) { + cparams_.channel_colors_pre_transform_percent = 0; } - cparams.channel_colors_percent = 0; - cparams.palette_colors = 0; - cparams.lossy_palette = false; + cparams_.channel_colors_percent = 0; + cparams_.palette_colors = 0; + cparams_.lossy_palette = false; } // if few colors, do all-channel palette before trying channel palette @@ -653,7 +654,8 @@ Status ModularFrameEncoder::ComputeEncodingData( // signaling cost for almost no benefit // - if the palette needs more colors, then channel palette might help to // reduce palette signaling cost - if (cparams.palette_colors != 0 && cparams.speed_tier < SpeedTier::kFalcon) { + if (cparams_.palette_colors != 0 && + cparams_.speed_tier < SpeedTier::kFalcon) { // all-channel palette (e.g. RGBA) if (gi.channel.size() > 1) { Transform maybe_palette(TransformId::kPalette); @@ -661,22 +663,24 @@ Status ModularFrameEncoder::ComputeEncodingData( maybe_palette.num_c = gi.channel.size() - gi.nb_meta_channels; maybe_palette.nb_colors = std::min(std::min(200, (int)(xsize * ysize / 8)), - std::abs(cparams.palette_colors) / 16); - maybe_palette.ordered_palette = cparams.palette_colors >= 0; + std::abs(cparams_.palette_colors) / 16); + maybe_palette.ordered_palette = cparams_.palette_colors >= 0; maybe_palette.lossy_palette = false; do_transform(gi, maybe_palette, weighted::Header(), pool); } } // Global channel palette - if (cparams.channel_colors_pre_transform_percent > 0 && - !cparams.lossy_palette && - (cparams.speed_tier <= SpeedTier::kThunder || + if (cparams_.channel_colors_pre_transform_percent > 0 && + !cparams_.lossy_palette && + (cparams_.speed_tier <= SpeedTier::kThunder || (do_color && metadata.bit_depth.bits_per_sample > 8))) { // single channel palette (like FLIF's ChannelCompact) size_t nb_channels = gi.channel.size() - gi.nb_meta_channels; + int orig_bitdepth = max_bitdepth; + max_bitdepth = 0; for (size_t i = 0; i < nb_channels; i++) { - int min, max; + int32_t min, max; compute_minmax(gi.channel[gi.nb_meta_channels + i], &min, &max); int64_t colors = max - min + 1; JXL_DEBUG_V(10, "Channel %" PRIuS ": range=%i..%i", i, min, max); @@ -689,35 +693,39 @@ Status ModularFrameEncoder::ComputeEncodingData( // image itself) maybe_palette_1.nb_colors = std::min( (int)(xsize * ysize / 16), - (int)(cparams.channel_colors_pre_transform_percent / 100. * colors)); + (int)(cparams_.channel_colors_pre_transform_percent / 100. * colors)); if (do_transform(gi, maybe_palette_1, weighted::Header(), pool)) { // effective bit depth is lower, adjust quantization accordingly compute_minmax(gi.channel[gi.nb_meta_channels + i], &min, &max); if (max < maxval) maxval = max; - } + int ch_bitdepth = + (max > 0 ? CeilLog2Nonzero(static_cast<uint32_t>(max)) : 0); + if (ch_bitdepth > max_bitdepth) max_bitdepth = ch_bitdepth; + } else + max_bitdepth = orig_bitdepth; } } // Global palette - if ((cparams.palette_colors != 0 || cparams.lossy_palette) && - cparams.speed_tier < SpeedTier::kFalcon) { + if ((cparams_.palette_colors != 0 || cparams_.lossy_palette) && + cparams_.speed_tier < SpeedTier::kFalcon) { // all-channel palette (e.g. RGBA) if (gi.channel.size() - gi.nb_meta_channels > 1) { Transform maybe_palette(TransformId::kPalette); maybe_palette.begin_c = gi.nb_meta_channels; maybe_palette.num_c = gi.channel.size() - gi.nb_meta_channels; maybe_palette.nb_colors = - std::min((int)(xsize * ysize / 8), std::abs(cparams.palette_colors)); - maybe_palette.ordered_palette = cparams.palette_colors >= 0; + std::min((int)(xsize * ysize / 8), std::abs(cparams_.palette_colors)); + maybe_palette.ordered_palette = cparams_.palette_colors >= 0; maybe_palette.lossy_palette = - (cparams.lossy_palette && maybe_palette.num_c == 3); + (cparams_.lossy_palette && maybe_palette.num_c == 3); if (maybe_palette.lossy_palette) { - maybe_palette.predictor = delta_pred; + maybe_palette.predictor = delta_pred_; } // TODO(veluca): use a custom weighted header if using the weighted // predictor. do_transform(gi, maybe_palette, weighted::Header(), pool, - cparams.options.zero_tokens); + cparams_.options.zero_tokens); } // all-minus-one-channel palette (RGB with separate alpha, or CMY with // separate K) @@ -726,71 +734,70 @@ Status ModularFrameEncoder::ComputeEncodingData( maybe_palette_3.begin_c = gi.nb_meta_channels; maybe_palette_3.num_c = gi.channel.size() - gi.nb_meta_channels - 1; maybe_palette_3.nb_colors = - std::min((int)(xsize * ysize / 8), std::abs(cparams.palette_colors)); - maybe_palette_3.ordered_palette = cparams.palette_colors >= 0; - maybe_palette_3.lossy_palette = cparams.lossy_palette; + std::min((int)(xsize * ysize / 8), std::abs(cparams_.palette_colors)); + maybe_palette_3.ordered_palette = cparams_.palette_colors >= 0; + maybe_palette_3.lossy_palette = cparams_.lossy_palette; if (maybe_palette_3.lossy_palette) { - maybe_palette_3.predictor = delta_pred; + maybe_palette_3.predictor = delta_pred_; } do_transform(gi, maybe_palette_3, weighted::Header(), pool, - cparams.options.zero_tokens); + cparams_.options.zero_tokens); } } // don't do an RCT if we're short on bits - if (cparams.color_transform == ColorTransform::kNone && do_color && !fp && + if (cparams_.color_transform == ColorTransform::kNone && do_color && gi.channel.size() - gi.nb_meta_channels >= 3 && max_bitdepth + 1 < level_max_bitdepth) { - if (cparams.colorspace == 1 || - (cparams.colorspace < 0 && - (!cparams.IsLossless() || cparams.speed_tier > SpeedTier::kHare))) { + if (cparams_.colorspace < 0 && + (!cparams_.IsLossless() || cparams_.speed_tier > SpeedTier::kHare)) { Transform ycocg{TransformId::kRCT}; ycocg.rct_type = 6; ycocg.begin_c = gi.nb_meta_channels; do_transform(gi, ycocg, weighted::Header(), pool); max_bitdepth++; - } else if (cparams.colorspace >= 2) { + } else if (cparams_.colorspace > 0) { Transform sg(TransformId::kRCT); sg.begin_c = gi.nb_meta_channels; - sg.rct_type = cparams.colorspace - 2; + sg.rct_type = cparams_.colorspace; do_transform(gi, sg, weighted::Header(), pool); max_bitdepth++; } } // don't do squeeze if we don't have some spare bits - if (cparams.responsive && !gi.channel.empty() && + if (cparams_.responsive && !gi.channel.empty() && max_bitdepth + 2 < level_max_bitdepth) { Transform t(TransformId::kSqueeze); - t.squeezes = cparams.squeezes; + t.squeezes = cparams_.squeezes; do_transform(gi, t, weighted::Header(), pool); max_bitdepth += 2; } if (max_bitdepth + 1 > level_max_bitdepth) { // force no group RCTs if we don't have a spare bit - cparams.colorspace = 0; + cparams_.colorspace = 0; } JXL_ASSERT(max_bitdepth <= level_max_bitdepth); std::vector<uint32_t> quants; - if (cparams.butteraugli_distance > 0) { + if (cparams_.butteraugli_distance > 0) { quants.resize(gi.channel.size(), 1); - float quality = 0.25f * cparams.butteraugli_distance; + float quality = 0.25f * cparams_.butteraugli_distance; JXL_DEBUG_V(2, "Adding quantization constants corresponding to distance %.3f ", quality); - if (!cparams.responsive) { + if (!cparams_.responsive) { JXL_DEBUG_V(1, "Warning: lossy compression without Squeeze " "transform is just color quantization."); quality *= 0.1f; } - if (cparams.color_transform != ColorTransform::kXYB) { + if (cparams_.color_transform != ColorTransform::kXYB) { quality *= maxval / 255.f; } - if (cparams.options.nb_repeats == 0) { + if (cparams_.options.nb_repeats == 0) { return JXL_FAILURE("nb_repeats = 0 not supported with modular lossy!"); } for (uint32_t i = gi.nb_meta_channels; i < gi.channel.size(); i++) { @@ -803,14 +810,14 @@ Status ModularFrameEncoder::ComputeEncodingData( int component = (do_color ? 0 : 3) + ((i - gi.nb_meta_channels) % nb_chans); // last 4 channels are final chroma residuals - if (nb_chans > 2 && i >= gi.channel.size() - 4 && cparams.responsive) { + if (nb_chans > 2 && i >= gi.channel.size() - 4 && cparams_.responsive) { component = 1; } - if (cparams.color_transform == ColorTransform::kXYB && component < 3) { + if (cparams_.color_transform == ColorTransform::kXYB && component < 3) { q = quality * squeeze_quality_factor_xyb * squeeze_xyb_qtable[component][shift]; } else { - if (cparams.colorspace != 0 && component > 0 && component < 3) { + if (cparams_.colorspace != 0 && component > 0 && component < 3) { q = quality * squeeze_quality_factor * squeeze_chroma_qtable[shift]; } else { q = quality * squeeze_quality_factor * squeeze_luma_factor * @@ -832,14 +839,14 @@ Status ModularFrameEncoder::ComputeEncodingData( }; std::vector<GroupParams> stream_params; - stream_options[0] = cparams.options; + stream_options_[0] = cparams_.options; // DC - for (size_t group_id = 0; group_id < frame_dim.num_dc_groups; group_id++) { - const size_t gx = group_id % frame_dim.xsize_dc_groups; - const size_t gy = group_id / frame_dim.xsize_dc_groups; - const Rect rect(gx * frame_dim.dc_group_dim, gy * frame_dim.dc_group_dim, - frame_dim.dc_group_dim, frame_dim.dc_group_dim); + for (size_t group_id = 0; group_id < frame_dim_.num_dc_groups; group_id++) { + const size_t gx = group_id % frame_dim_.xsize_dc_groups; + const size_t gy = group_id / frame_dim_.xsize_dc_groups; + const Rect rect(gx * frame_dim_.dc_group_dim, gy * frame_dim_.dc_group_dim, + frame_dim_.dc_group_dim, frame_dim_.dc_group_dim); // minShift==3 because (frame_dim.dc_group_dim >> 3) == frame_dim.group_dim // maxShift==1000 is infinity stream_params.push_back( @@ -847,11 +854,11 @@ Status ModularFrameEncoder::ComputeEncodingData( } // AC global -> nothing. // AC - for (size_t group_id = 0; group_id < frame_dim.num_groups; group_id++) { - const size_t gx = group_id % frame_dim.xsize_groups; - const size_t gy = group_id / frame_dim.xsize_groups; - const Rect mrect(gx * frame_dim.group_dim, gy * frame_dim.group_dim, - frame_dim.group_dim, frame_dim.group_dim); + for (size_t group_id = 0; group_id < frame_dim_.num_groups; group_id++) { + const size_t gx = group_id % frame_dim_.xsize_groups; + const size_t gy = group_id / frame_dim_.xsize_groups; + const Rect mrect(gx * frame_dim_.group_dim, gy * frame_dim_.group_dim, + frame_dim_.group_dim, frame_dim_.group_dim); for (size_t i = 0; i < enc_state->progressive_splitter.GetNumPasses(); i++) { int maxShift, minShift; @@ -866,24 +873,24 @@ Status ModularFrameEncoder::ComputeEncodingData( stream_params.push_back(GroupParams{Rect(0, 0, xsize, ysize), 0, 1000, ModularStreamId::Global()}); } - gi_channel.resize(stream_images.size()); + gi_channel_.resize(stream_images_.size()); JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, stream_params.size(), ThreadPool::NoInit, [&](const uint32_t i, size_t /* thread */) { - stream_options[stream_params[i].id.ID(frame_dim)] = cparams.options; + stream_options_[stream_params[i].id.ID(frame_dim_)] = cparams_.options; JXL_CHECK(PrepareStreamParams( - stream_params[i].rect, cparams, stream_params[i].minShift, + stream_params[i].rect, cparams_, stream_params[i].minShift, stream_params[i].maxShift, stream_params[i].id, do_color)); }, "ChooseParams")); { // Clear out channels that have been copied to groups. - Image& full_image = stream_images[0]; + Image& full_image = stream_images_[0]; size_t c = full_image.nb_meta_channels; for (; c < full_image.channel.size(); c++) { Channel& fc = full_image.channel[c]; - if (fc.w > frame_dim.group_dim || fc.h > frame_dim.group_dim) break; + if (fc.w > frame_dim_.group_dim || fc.h > frame_dim_.group_dim) break; } for (; c < full_image.channel.size(); c++) { full_image.channel[c].plane = ImageI(); @@ -891,102 +898,100 @@ Status ModularFrameEncoder::ComputeEncodingData( } if (!quants.empty()) { - for (uint32_t stream_id = 0; stream_id < stream_images.size(); + for (uint32_t stream_id = 0; stream_id < stream_images_.size(); stream_id++) { // skip non-modular stream_ids - if (stream_id > 0 && gi_channel[stream_id].empty()) continue; - Image& image = stream_images[stream_id]; - const ModularOptions& options = stream_options[stream_id]; + if (stream_id > 0 && gi_channel_[stream_id].empty()) continue; + const Image& image = stream_images_[stream_id]; + const ModularOptions& options = stream_options_[stream_id]; for (uint32_t i = image.nb_meta_channels; i < image.channel.size(); i++) { if (i >= image.nb_meta_channels && (image.channel[i].w > options.max_chan_size || image.channel[i].h > options.max_chan_size)) { continue; } - if (stream_id > 0 && gi_channel[stream_id].empty()) continue; + if (stream_id > 0 && gi_channel_[stream_id].empty()) continue; size_t ch_id = stream_id == 0 ? i - : gi_channel[stream_id][i - image.nb_meta_channels]; + : gi_channel_[stream_id][i - image.nb_meta_channels]; uint32_t q = quants[ch_id]; // Inform the tree splitting heuristics that each channel in each group // used this quantization factor. This will produce a tree with the // given multipliers. - if (multiplier_info.empty() || - multiplier_info.back().range[1][0] != stream_id || - multiplier_info.back().multiplier != q) { + if (multiplier_info_.empty() || + multiplier_info_.back().range[1][0] != stream_id || + multiplier_info_.back().multiplier != q) { StaticPropRange range; range[0] = {{i, i + 1}}; range[1] = {{stream_id, stream_id + 1}}; - multiplier_info.push_back({range, (uint32_t)q}); + multiplier_info_.push_back({range, (uint32_t)q}); } else { // Previous channel in the same group had the same quantization // factor. Don't provide two different ranges, as that creates // unnecessary nodes. - multiplier_info.back().range[0][1] = i + 1; + multiplier_info_.back().range[0][1] = i + 1; } } } // Merge group+channel settings that have the same channels and quantization // factors, to avoid unnecessary nodes. - std::sort(multiplier_info.begin(), multiplier_info.end(), + std::sort(multiplier_info_.begin(), multiplier_info_.end(), [](ModularMultiplierInfo a, ModularMultiplierInfo b) { return std::make_tuple(a.range, a.multiplier) < std::make_tuple(b.range, b.multiplier); }); size_t new_num = 1; - for (size_t i = 1; i < multiplier_info.size(); i++) { - ModularMultiplierInfo& prev = multiplier_info[new_num - 1]; - ModularMultiplierInfo& cur = multiplier_info[i]; + for (size_t i = 1; i < multiplier_info_.size(); i++) { + ModularMultiplierInfo& prev = multiplier_info_[new_num - 1]; + ModularMultiplierInfo& cur = multiplier_info_[i]; if (prev.range[0] == cur.range[0] && prev.multiplier == cur.multiplier && prev.range[1][1] == cur.range[1][0]) { prev.range[1][1] = cur.range[1][1]; } else { - multiplier_info[new_num++] = multiplier_info[i]; + multiplier_info_[new_num++] = multiplier_info_[i]; } } - multiplier_info.resize(new_num); + multiplier_info_.resize(new_num); } - JXL_RETURN_IF_ERROR(ValidateChannelDimensions(gi, stream_options[0])); + JXL_RETURN_IF_ERROR(ValidateChannelDimensions(gi, stream_options_[0])); - return PrepareEncoding(pool, enc_state->shared.frame_dim, - enc_state->heuristics.get(), aux_out); + return PrepareEncoding(frame_header, pool, enc_state->heuristics.get(), + aux_out); } -Status ModularFrameEncoder::PrepareEncoding(ThreadPool* pool, - const FrameDimensions& frame_dim, +Status ModularFrameEncoder::PrepareEncoding(const FrameHeader& frame_header, + ThreadPool* pool, EncoderHeuristics* heuristics, AuxOut* aux_out) { - if (!tree.empty()) return true; + if (!tree_.empty()) return true; // Compute tree. - size_t num_streams = stream_images.size(); - stream_headers.resize(num_streams); - tokens.resize(num_streams); + size_t num_streams = stream_images_.size(); + stream_headers_.resize(num_streams); + tokens_.resize(num_streams); - if (heuristics->CustomFixedTreeLossless(frame_dim, &tree)) { + if (heuristics->CustomFixedTreeLossless(frame_dim_, &tree_)) { // Using a fixed tree. - } else if (cparams.speed_tier < SpeedTier::kFalcon || quality != 100 || - !cparams.modular_mode) { + } else if (cparams_.speed_tier < SpeedTier::kFalcon || + !cparams_.modular_mode) { // Avoid creating a tree with leaves that don't correspond to any pixels. std::vector<size_t> useful_splits; - useful_splits.reserve(tree_splits.size()); - for (size_t chunk = 0; chunk < tree_splits.size() - 1; chunk++) { + useful_splits.reserve(tree_splits_.size()); + for (size_t chunk = 0; chunk < tree_splits_.size() - 1; chunk++) { bool has_pixels = false; - size_t start = tree_splits[chunk]; - size_t stop = tree_splits[chunk + 1]; + size_t start = tree_splits_[chunk]; + size_t stop = tree_splits_[chunk + 1]; for (size_t i = start; i < stop; i++) { - for (const Channel& c : stream_images[i].channel) { - if (c.w && c.h) has_pixels = true; - } + if (!stream_images_[i].empty()) has_pixels = true; } if (has_pixels) { - useful_splits.push_back(tree_splits[chunk]); + useful_splits.push_back(tree_splits_[chunk]); } } // Don't do anything if modular mode does not have any pixels in this image if (useful_splits.empty()) return true; - useful_splits.push_back(tree_splits.back()); + useful_splits.push_back(tree_splits_.back()); std::atomic_flag invalid_force_wp = ATOMIC_FLAG_INIT; @@ -998,27 +1003,29 @@ Status ModularFrameEncoder::PrepareEncoding(ThreadPool* pool, size_t total_pixels = 0; uint32_t start = useful_splits[chunk]; uint32_t stop = useful_splits[chunk + 1]; + while (start < stop && stream_images_[start].empty()) ++start; + while (start < stop && stream_images_[stop - 1].empty()) --stop; uint32_t max_c = 0; - if (stream_options[start].tree_kind != + if (stream_options_[start].tree_kind != ModularOptions::TreeKind::kLearn) { for (size_t i = start; i < stop; i++) { - for (const Channel& ch : stream_images[i].channel) { + for (const Channel& ch : stream_images_[i].channel) { total_pixels += ch.w * ch.h; } } trees[chunk] = - PredefinedTree(stream_options[start].tree_kind, total_pixels); + PredefinedTree(stream_options_[start].tree_kind, total_pixels); return; } TreeSamples tree_samples; - if (!tree_samples.SetPredictor(stream_options[start].predictor, - stream_options[start].wp_tree_mode)) { + if (!tree_samples.SetPredictor(stream_options_[start].predictor, + stream_options_[start].wp_tree_mode)) { invalid_force_wp.test_and_set(std::memory_order_acq_rel); return; } if (!tree_samples.SetProperties( - stream_options[start].splitting_heuristics_properties, - stream_options[start].wp_tree_mode)) { + stream_options_[start].splitting_heuristics_properties, + stream_options_[start].wp_tree_mode)) { invalid_force_wp.test_and_set(std::memory_order_acq_rel); return; } @@ -1027,66 +1034,72 @@ Status ModularFrameEncoder::PrepareEncoding(ThreadPool* pool, std::vector<uint32_t> group_pixel_count; std::vector<uint32_t> channel_pixel_count; for (size_t i = start; i < stop; i++) { - max_c = std::max<uint32_t>(stream_images[i].channel.size(), max_c); - CollectPixelSamples(stream_images[i], stream_options[i], i, + max_c = std::max<uint32_t>(stream_images_[i].channel.size(), max_c); + CollectPixelSamples(stream_images_[i], stream_options_[i], i, group_pixel_count, channel_pixel_count, pixel_samples, diff_samples); } StaticPropRange range; range[0] = {{0, max_c}}; range[1] = {{start, stop}}; - auto local_multiplier_info = multiplier_info; + auto local_multiplier_info = multiplier_info_; tree_samples.PreQuantizeProperties( range, local_multiplier_info, group_pixel_count, channel_pixel_count, pixel_samples, diff_samples, - stream_options[start].max_property_values); + stream_options_[start].max_property_values); for (size_t i = start; i < stop; i++) { JXL_CHECK(ModularGenericCompress( - stream_images[i], stream_options[i], /*writer=*/nullptr, + stream_images_[i], stream_options_[i], /*writer=*/nullptr, /*aux_out=*/nullptr, 0, i, &tree_samples, &total_pixels)); } // TODO(veluca): parallelize more. trees[chunk] = LearnTree(std::move(tree_samples), total_pixels, - stream_options[start], local_multiplier_info, range); + stream_options_[start], local_multiplier_info, range); }, "LearnTrees")); if (invalid_force_wp.test_and_set(std::memory_order_acq_rel)) { return JXL_FAILURE("PrepareEncoding: force_no_wp with {Weighted}"); } - tree.clear(); - MergeTrees(trees, useful_splits, 0, useful_splits.size() - 1, &tree); + tree_.clear(); + MergeTrees(trees, useful_splits, 0, useful_splits.size() - 1, &tree_); } else { // Fixed tree. size_t total_pixels = 0; - for (const Image& img : stream_images) { + for (const Image& img : stream_images_) { for (const Channel& ch : img.channel) { total_pixels += ch.w * ch.h; } } - if (cparams.speed_tier <= SpeedTier::kFalcon) { - tree = PredefinedTree(ModularOptions::TreeKind::kWPFixedDC, total_pixels); - } else if (cparams.speed_tier <= SpeedTier::kThunder) { - tree = PredefinedTree(ModularOptions::TreeKind::kGradientFixedDC, - total_pixels); + if (cparams_.speed_tier <= SpeedTier::kFalcon) { + tree_ = + PredefinedTree(ModularOptions::TreeKind::kWPFixedDC, total_pixels); + } else if (cparams_.speed_tier <= SpeedTier::kThunder) { + tree_ = PredefinedTree(ModularOptions::TreeKind::kGradientFixedDC, + total_pixels); } else { - tree = {PropertyDecisionNode::Leaf(Predictor::Gradient)}; + tree_ = {PropertyDecisionNode::Leaf(Predictor::Gradient)}; } } - tree_tokens.resize(1); - tree_tokens[0].clear(); + tree_tokens_.resize(1); + tree_tokens_[0].clear(); Tree decoded_tree; - TokenizeTree(tree, &tree_tokens[0], &decoded_tree); - JXL_ASSERT(tree.size() == decoded_tree.size()); - tree = std::move(decoded_tree); + TokenizeTree(tree_, &tree_tokens_[0], &decoded_tree); + JXL_ASSERT(tree_.size() == decoded_tree.size()); + tree_ = std::move(decoded_tree); if (WantDebugOutput(aux_out)) { - PrintTree(tree, aux_out->debug_prefix + "/global_tree"); + if (frame_header.dc_level > 0) { + PrintTree(tree_, aux_out->debug_prefix + "/dc_frame_level" + + std::to_string(frame_header.dc_level) + "_tree"); + } else { + PrintTree(tree_, aux_out->debug_prefix + "/global_tree"); + } } - image_widths.resize(num_streams); + image_widths_.resize(num_streams); JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, num_streams, ThreadPool::NoInit, [&](const uint32_t stream_id, size_t /* thread */) { @@ -1095,15 +1108,15 @@ Status ModularFrameEncoder::PrepareEncoding(ThreadPool* pool, my_aux_out.dump_image = aux_out->dump_image; my_aux_out.debug_prefix = aux_out->debug_prefix; } - tokens[stream_id].clear(); + tokens_[stream_id].clear(); JXL_CHECK(ModularGenericCompress( - stream_images[stream_id], stream_options[stream_id], + stream_images_[stream_id], stream_options_[stream_id], /*writer=*/nullptr, &my_aux_out, 0, stream_id, /*tree_samples=*/nullptr, /*total_pixels=*/nullptr, - /*tree=*/&tree, /*header=*/&stream_headers[stream_id], - /*tokens=*/&tokens[stream_id], - /*widths=*/&image_widths[stream_id])); + /*tree=*/&tree_, /*header=*/&stream_headers_[stream_id], + /*tokens=*/&tokens_[stream_id], + /*widths=*/&image_widths_[stream_id])); }, "ComputeTokens")); return true; @@ -1113,7 +1126,7 @@ Status ModularFrameEncoder::EncodeGlobalInfo(BitWriter* writer, AuxOut* aux_out) { BitWriter::Allotment allotment(writer, 1); // If we are using brotli, or not using modular mode. - if (tree_tokens.empty() || tree_tokens[0].empty()) { + if (tree_tokens_.empty() || tree_tokens_[0].empty()) { writer->Write(1, 0); ReclaimAndCharge(writer, &allotment, kLayerModularTree, aux_out); return true; @@ -1123,66 +1136,66 @@ Status ModularFrameEncoder::EncodeGlobalInfo(BitWriter* writer, // Write tree HistogramParams params; - if (cparams.speed_tier > SpeedTier::kKitten) { + if (cparams_.speed_tier > SpeedTier::kKitten) { params.clustering = HistogramParams::ClusteringType::kFast; params.ans_histogram_strategy = - cparams.speed_tier > SpeedTier::kThunder + cparams_.speed_tier > SpeedTier::kThunder ? HistogramParams::ANSHistogramStrategy::kFast : HistogramParams::ANSHistogramStrategy::kApproximate; params.lz77_method = - cparams.decoding_speed_tier >= 3 && cparams.modular_mode - ? (cparams.speed_tier >= SpeedTier::kFalcon + cparams_.decoding_speed_tier >= 3 && cparams_.modular_mode + ? (cparams_.speed_tier >= SpeedTier::kFalcon ? HistogramParams::LZ77Method::kRLE : HistogramParams::LZ77Method::kLZ77) : HistogramParams::LZ77Method::kNone; // Near-lossless DC, as well as modular mode, require choosing hybrid uint // more carefully. if ((!extra_dc_precision.empty() && extra_dc_precision[0] != 0) || - (cparams.modular_mode && cparams.speed_tier < SpeedTier::kCheetah)) { + (cparams_.modular_mode && cparams_.speed_tier < SpeedTier::kCheetah)) { params.uint_method = HistogramParams::HybridUintMethod::kFast; } else { params.uint_method = HistogramParams::HybridUintMethod::kNone; } - } else if (cparams.speed_tier <= SpeedTier::kTortoise) { + } else if (cparams_.speed_tier <= SpeedTier::kTortoise) { params.lz77_method = HistogramParams::LZ77Method::kOptimal; } else { params.lz77_method = HistogramParams::LZ77Method::kLZ77; } - if (cparams.decoding_speed_tier >= 1) { + if (cparams_.decoding_speed_tier >= 1) { params.max_histograms = 12; } - if (cparams.decoding_speed_tier >= 1 && cparams.responsive) { - params.lz77_method = cparams.speed_tier >= SpeedTier::kCheetah + if (cparams_.decoding_speed_tier >= 1 && cparams_.responsive) { + params.lz77_method = cparams_.speed_tier >= SpeedTier::kCheetah ? HistogramParams::LZ77Method::kRLE - : cparams.speed_tier >= SpeedTier::kKitten + : cparams_.speed_tier >= SpeedTier::kKitten ? HistogramParams::LZ77Method::kLZ77 : HistogramParams::LZ77Method::kOptimal; } - if (cparams.decoding_speed_tier >= 2 && cparams.responsive) { + if (cparams_.decoding_speed_tier >= 2 && cparams_.responsive) { params.uint_method = HistogramParams::HybridUintMethod::k000; params.force_huffman = true; } - BuildAndEncodeHistograms(params, kNumTreeContexts, tree_tokens, &code, - &context_map, writer, kLayerModularTree, aux_out); - WriteTokens(tree_tokens[0], code, context_map, writer, kLayerModularTree, + BuildAndEncodeHistograms(params, kNumTreeContexts, tree_tokens_, &code_, + &context_map_, writer, kLayerModularTree, aux_out); + WriteTokens(tree_tokens_[0], code_, context_map_, writer, kLayerModularTree, aux_out); - params.image_widths = image_widths; + params.image_widths = image_widths_; // Write histograms. - BuildAndEncodeHistograms(params, (tree.size() + 1) / 2, tokens, &code, - &context_map, writer, kLayerModularGlobal, aux_out); + BuildAndEncodeHistograms(params, (tree_.size() + 1) / 2, tokens_, &code_, + &context_map_, writer, kLayerModularGlobal, aux_out); return true; } Status ModularFrameEncoder::EncodeStream(BitWriter* writer, AuxOut* aux_out, size_t layer, const ModularStreamId& stream) { - size_t stream_id = stream.ID(frame_dim); - if (stream_images[stream_id].channel.empty()) { + size_t stream_id = stream.ID(frame_dim_); + if (stream_images_[stream_id].channel.empty()) { return true; // Image with no channels, header never gets decoded. } JXL_RETURN_IF_ERROR( - Bundle::Write(stream_headers[stream_id], writer, layer, aux_out)); - WriteTokens(tokens[stream_id], code, context_map, writer, layer, aux_out); + Bundle::Write(stream_headers_[stream_id], writer, layer, aux_out)); + WriteTokens(tokens_[stream_id], code_, context_map_, writer, layer, aux_out); return true; } @@ -1277,22 +1290,22 @@ float EstimateCost(const Image& img) { } // namespace Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, - const CompressParams& cparams, + const CompressParams& cparams_, int minShift, int maxShift, const ModularStreamId& stream, bool do_color) { - size_t stream_id = stream.ID(frame_dim); - Image& full_image = stream_images[0]; + size_t stream_id = stream.ID(frame_dim_); + Image& full_image = stream_images_[0]; const size_t xsize = rect.xsize(); const size_t ysize = rect.ysize(); - Image& gi = stream_images[stream_id]; + Image& gi = stream_images_[stream_id]; if (stream_id > 0) { gi = Image(xsize, ysize, full_image.bitdepth, 0); // start at the first bigger-than-frame_dim.group_dim non-metachannel size_t c = full_image.nb_meta_channels; for (; c < full_image.channel.size(); c++) { Channel& fc = full_image.channel[c]; - if (fc.w > frame_dim.group_dim || fc.h > frame_dim.group_dim) break; + if (fc.w > frame_dim_.group_dim || fc.h > frame_dim_.group_dim) break; } for (; c < full_image.channel.size(); c++) { Channel& fc = full_image.channel[c]; @@ -1302,7 +1315,7 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, Rect r(rect.x0() >> fc.hshift, rect.y0() >> fc.vshift, rect.xsize() >> fc.hshift, rect.ysize() >> fc.vshift, fc.w, fc.h); if (r.xsize() == 0 || r.ysize() == 0) continue; - gi_channel[stream_id].push_back(c); + gi_channel_[stream_id].push_back(c); Channel gc(r.xsize(), r.ysize()); gc.hshift = fc.hshift; gc.vshift = fc.vshift; @@ -1319,15 +1332,15 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, // Local palette // TODO(veluca): make this work with quantize-after-prediction in lossy // mode. - if (cparams.butteraugli_distance == 0.f && cparams.palette_colors != 0 && - cparams.speed_tier < SpeedTier::kCheetah) { + if (cparams_.butteraugli_distance == 0.f && cparams_.palette_colors != 0 && + cparams_.speed_tier < SpeedTier::kCheetah) { // all-channel palette (e.g. RGBA) if (gi.channel.size() - gi.nb_meta_channels > 1) { Transform maybe_palette(TransformId::kPalette); maybe_palette.begin_c = gi.nb_meta_channels; maybe_palette.num_c = gi.channel.size() - gi.nb_meta_channels; - maybe_palette.nb_colors = std::abs(cparams.palette_colors); - maybe_palette.ordered_palette = cparams.palette_colors >= 0; + maybe_palette.nb_colors = std::abs(cparams_.palette_colors); + maybe_palette.ordered_palette = cparams_.palette_colors >= 0; do_transform(gi, maybe_palette, weighted::Header()); } // all-minus-one-channel palette (RGB with separate alpha, or CMY with @@ -1336,9 +1349,9 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, Transform maybe_palette_3(TransformId::kPalette); maybe_palette_3.begin_c = gi.nb_meta_channels; maybe_palette_3.num_c = gi.channel.size() - gi.nb_meta_channels - 1; - maybe_palette_3.nb_colors = std::abs(cparams.palette_colors); - maybe_palette_3.ordered_palette = cparams.palette_colors >= 0; - maybe_palette_3.lossy_palette = cparams.lossy_palette; + maybe_palette_3.nb_colors = std::abs(cparams_.palette_colors); + maybe_palette_3.ordered_palette = cparams_.palette_colors >= 0; + maybe_palette_3.lossy_palette = cparams_.lossy_palette; if (maybe_palette_3.lossy_palette) { maybe_palette_3.predictor = Predictor::Weighted; } @@ -1347,14 +1360,14 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, } // Local channel palette - if (cparams.channel_colors_percent > 0 && - cparams.butteraugli_distance == 0.f && !cparams.lossy_palette && - cparams.speed_tier < SpeedTier::kCheetah && - !(cparams.responsive && cparams.decoding_speed_tier >= 1)) { + if (cparams_.channel_colors_percent > 0 && + cparams_.butteraugli_distance == 0.f && !cparams_.lossy_palette && + cparams_.speed_tier < SpeedTier::kCheetah && + !(cparams_.responsive && cparams_.decoding_speed_tier >= 1)) { // single channel palette (like FLIF's ChannelCompact) size_t nb_channels = gi.channel.size() - gi.nb_meta_channels; for (size_t i = 0; i < nb_channels; i++) { - int min, max; + int32_t min, max; compute_minmax(gi.channel[gi.nb_meta_channels + i], &min, &max); int colors = max - min + 1; JXL_DEBUG_V(10, "Channel %" PRIuS ": range=%i..%i", i, min, max); @@ -1367,7 +1380,7 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, // image itself) maybe_palette_1.nb_colors = std::min((int)(xsize * ysize * 0.8), - (int)(cparams.channel_colors_percent / 100. * colors)); + (int)(cparams_.channel_colors_percent / 100. * colors)); do_transform(gi, maybe_palette_1, weighted::Header()); } } @@ -1375,15 +1388,15 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, // lossless and no specific color transform specified: try Nothing, YCoCg, // and 17 RCTs - if (cparams.color_transform == ColorTransform::kNone && - cparams.IsLossless() && cparams.colorspace < 0 && + if (cparams_.color_transform == ColorTransform::kNone && + cparams_.IsLossless() && cparams_.colorspace < 0 && gi.channel.size() - gi.nb_meta_channels >= 3 && - cparams.responsive == false && do_color && - cparams.speed_tier <= SpeedTier::kHare) { + cparams_.responsive == false && do_color && + cparams_.speed_tier <= SpeedTier::kHare) { Transform sg(TransformId::kRCT); sg.begin_c = gi.nb_meta_channels; size_t nb_rcts_to_try = 0; - switch (cparams.speed_tier) { + switch (cparams_.speed_tier) { case SpeedTier::kLightning: case SpeedTier::kThunder: case SpeedTier::kFalcon: @@ -1437,22 +1450,22 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, // No need to try anything, just use the default options. } size_t nb_wp_modes = 1; - if (cparams.speed_tier <= SpeedTier::kTortoise) { + if (cparams_.speed_tier <= SpeedTier::kTortoise) { nb_wp_modes = 5; - } else if (cparams.speed_tier <= SpeedTier::kKitten) { + } else if (cparams_.speed_tier <= SpeedTier::kKitten) { nb_wp_modes = 2; } if (nb_wp_modes > 1 && - (stream_options[stream_id].predictor == Predictor::Weighted || - stream_options[stream_id].predictor == Predictor::Best || - stream_options[stream_id].predictor == Predictor::Variable)) { + (stream_options_[stream_id].predictor == Predictor::Weighted || + stream_options_[stream_id].predictor == Predictor::Best || + stream_options_[stream_id].predictor == Predictor::Variable)) { float best_cost = std::numeric_limits<float>::max(); - stream_options[stream_id].wp_mode = 0; + stream_options_[stream_id].wp_mode = 0; for (size_t i = 0; i < nb_wp_modes; i++) { float cost = EstimateWPCost(gi, i); if (cost < best_cost) { best_cost = cost; - stream_options[stream_id].wp_mode = i; + stream_options_[stream_id].wp_mode = i; } } } @@ -1492,27 +1505,28 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, extra_dc_precision[group_index] = nl_dc ? 1 : 0; float mul = 1 << extra_dc_precision[group_index]; - size_t stream_id = ModularStreamId::VarDCTDC(group_index).ID(frame_dim); - stream_options[stream_id].max_chan_size = 0xFFFFFF; - stream_options[stream_id].predictor = Predictor::Weighted; - stream_options[stream_id].wp_tree_mode = ModularOptions::TreeMode::kWPOnly; - if (cparams.speed_tier >= SpeedTier::kSquirrel) { - stream_options[stream_id].tree_kind = ModularOptions::TreeKind::kWPFixedDC; - } - if (cparams.speed_tier < SpeedTier::kSquirrel && !nl_dc) { - stream_options[stream_id].predictor = - (cparams.speed_tier < SpeedTier::kKitten ? Predictor::Variable - : Predictor::Best); - stream_options[stream_id].wp_tree_mode = ModularOptions::TreeMode::kDefault; - stream_options[stream_id].tree_kind = ModularOptions::TreeKind::kLearn; - } - if (cparams.decoding_speed_tier >= 1) { - stream_options[stream_id].tree_kind = + size_t stream_id = ModularStreamId::VarDCTDC(group_index).ID(frame_dim_); + stream_options_[stream_id].max_chan_size = 0xFFFFFF; + stream_options_[stream_id].predictor = Predictor::Weighted; + stream_options_[stream_id].wp_tree_mode = ModularOptions::TreeMode::kWPOnly; + if (cparams_.speed_tier >= SpeedTier::kSquirrel) { + stream_options_[stream_id].tree_kind = ModularOptions::TreeKind::kWPFixedDC; + } + if (cparams_.speed_tier < SpeedTier::kSquirrel && !nl_dc) { + stream_options_[stream_id].predictor = + (cparams_.speed_tier < SpeedTier::kKitten ? Predictor::Variable + : Predictor::Best); + stream_options_[stream_id].wp_tree_mode = + ModularOptions::TreeMode::kDefault; + stream_options_[stream_id].tree_kind = ModularOptions::TreeKind::kLearn; + } + if (cparams_.decoding_speed_tier >= 1) { + stream_options_[stream_id].tree_kind = ModularOptions::TreeKind::kGradientFixedDC; } - stream_images[stream_id] = Image(r.xsize(), r.ysize(), 8, 3); - if (nl_dc && stream_options[stream_id].tree_kind == + stream_images_[stream_id] = Image(r.xsize(), r.ysize(), 8, 3); + if (nl_dc && stream_options_[stream_id].tree_kind == ModularOptions::TreeKind::kGradientFixedDC) { JXL_ASSERT(enc_state->shared.frame_header.chroma_subsampling.Is444()); for (size_t c : {1, 0, 2}) { @@ -1521,8 +1535,8 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, float cfl_factor = enc_state->shared.cmap.DCFactors()[c]; for (size_t y = 0; y < r.ysize(); y++) { int32_t* quant_row = - stream_images[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); - size_t stride = stream_images[stream_id] + stream_images_[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); + size_t stride = stream_images_[stream_id] .channel[c < 2 ? c ^ 1 : c] .plane.PixelsPerRow(); const float* row = r.ConstPlaneRow(dc, c, y); @@ -1533,7 +1547,7 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, } } else { int32_t* quant_row_y = - stream_images[stream_id].channel[0].plane.Row(y); + stream_images_[stream_id].channel[0].plane.Row(y); for (size_t x = 0; x < r.xsize(); x++) { quant_row[x] = QuantizeGradient( quant_row, stride, c, x, y, r.xsize(), @@ -1552,8 +1566,8 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, weighted::State wp_state(header, r.xsize(), r.ysize()); for (size_t y = 0; y < r.ysize(); y++) { int32_t* quant_row = - stream_images[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); - size_t stride = stream_images[stream_id] + stream_images_[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); + size_t stride = stream_images_[stream_id] .channel[c < 2 ? c ^ 1 : c] .plane.PixelsPerRow(); const float* row = r.ConstPlaneRow(dc, c, y); @@ -1565,7 +1579,7 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, } } else { int32_t* quant_row_y = - stream_images[stream_id].channel[0].plane.Row(y); + stream_images_[stream_id].channel[0].plane.Row(y); for (size_t x = 0; x < r.xsize(); x++) { quant_row[x] = QuantizeWP( quant_row, stride, c, x, y, r.xsize(), &wp_state, @@ -1582,7 +1596,7 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, float cfl_factor = enc_state->shared.cmap.DCFactors()[c]; for (size_t y = 0; y < r.ysize(); y++) { int32_t* quant_row = - stream_images[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); + stream_images_[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); const float* row = r.ConstPlaneRow(dc, c, y); if (c == 1) { for (size_t x = 0; x < r.xsize(); x++) { @@ -1590,7 +1604,7 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, } } else { int32_t* quant_row_y = - stream_images[stream_id].channel[0].plane.Row(y); + stream_images_[stream_id].channel[0].plane.Row(y); for (size_t x = 0; x < r.xsize(); x++) { quant_row[x] = roundf((row[x] - quant_row_y[x] * (y_factor * cfl_factor)) * @@ -1611,7 +1625,7 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, float inv_factor = enc_state->shared.quantizer.GetInvDcStep(c) * mul; size_t ys = rect.ysize(); size_t xs = rect.xsize(); - Channel& ch = stream_images[stream_id].channel[c < 2 ? c ^ 1 : c]; + Channel& ch = stream_images_[stream_id].channel[c < 2 ? c ^ 1 : c]; ch.w = xs; ch.h = ys; ch.shrink(); @@ -1626,7 +1640,7 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, } DequantDC(r, &enc_state->shared.dc_storage, &enc_state->shared.quant_dc, - stream_images[stream_id], enc_state->shared.quantizer.MulDC(), + stream_images_[stream_id], enc_state->shared.quantizer.MulDC(), 1.0 / mul, enc_state->shared.cmap.DCFactors(), enc_state->shared.frame_header.chroma_subsampling, enc_state->shared.block_ctx_map); @@ -1635,26 +1649,26 @@ void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, void ModularFrameEncoder::AddACMetadata(size_t group_index, bool jpeg_transcode, PassesEncoderState* enc_state) { const Rect r = enc_state->shared.DCGroupRect(group_index); - size_t stream_id = ModularStreamId::ACMetadata(group_index).ID(frame_dim); - stream_options[stream_id].max_chan_size = 0xFFFFFF; - stream_options[stream_id].wp_tree_mode = ModularOptions::TreeMode::kNoWP; + size_t stream_id = ModularStreamId::ACMetadata(group_index).ID(frame_dim_); + stream_options_[stream_id].max_chan_size = 0xFFFFFF; + stream_options_[stream_id].wp_tree_mode = ModularOptions::TreeMode::kNoWP; if (jpeg_transcode) { - stream_options[stream_id].tree_kind = + stream_options_[stream_id].tree_kind = ModularOptions::TreeKind::kJpegTranscodeACMeta; - } else if (cparams.speed_tier >= SpeedTier::kFalcon) { - stream_options[stream_id].tree_kind = + } else if (cparams_.speed_tier >= SpeedTier::kFalcon) { + stream_options_[stream_id].tree_kind = ModularOptions::TreeKind::kFalconACMeta; - } else if (cparams.speed_tier > SpeedTier::kKitten) { - stream_options[stream_id].tree_kind = ModularOptions::TreeKind::kACMeta; + } else if (cparams_.speed_tier > SpeedTier::kKitten) { + stream_options_[stream_id].tree_kind = ModularOptions::TreeKind::kACMeta; } // If we are using a non-constant CfL field, and are in a slow enough mode, // re-enable tree computation for it. - if (cparams.speed_tier < SpeedTier::kSquirrel && - cparams.force_cfl_jpeg_recompression) { - stream_options[stream_id].tree_kind = ModularOptions::TreeKind::kLearn; + if (cparams_.speed_tier < SpeedTier::kSquirrel && + cparams_.force_cfl_jpeg_recompression) { + stream_options_[stream_id].tree_kind = ModularOptions::TreeKind::kLearn; } // YToX, YToB, ACS + QF, EPF - Image& image = stream_images[stream_id]; + Image& image = stream_images_[stream_id]; image = Image(r.xsize(), r.ysize(), 8, 4); static_assert(kColorTileDimInBlocks == 8, "Color tile size changed"); Rect cr(r.x0() >> 3, r.y0() >> 3, (r.xsize() + 7) >> 3, (r.ysize() + 7) >> 3); @@ -1668,11 +1682,11 @@ void ModularFrameEncoder::AddACMetadata(size_t group_index, bool jpeg_transcode, size_t num = 0; for (size_t y = 0; y < r.ysize(); y++) { AcStrategyRow row_acs = enc_state->shared.ac_strategy.ConstRow(r, y); - const int* row_qf = r.ConstRow(enc_state->shared.raw_quant_field, y); + const int32_t* row_qf = r.ConstRow(enc_state->shared.raw_quant_field, y); const uint8_t* row_epf = r.ConstRow(enc_state->shared.epf_sharpness, y); - int* out_acs = image.channel[2].plane.Row(0); - int* out_qf = image.channel[2].plane.Row(1); - int* row_out_epf = image.channel[3].plane.Row(y); + int32_t* out_acs = image.channel[2].plane.Row(0); + int32_t* out_qf = image.channel[2].plane.Row(1); + int32_t* row_out_epf = image.channel[3].plane.Row(y); for (size_t x = 0; x < r.xsize(); x++) { row_out_epf[x] = row_epf[x]; if (!row_acs[x].IsFirstBlock()) continue; @@ -1700,7 +1714,7 @@ void ModularFrameEncoder::EncodeQuantTable( Image image(size_x, size_y, 8, 3); for (size_t c = 0; c < 3; c++) { for (size_t y = 0; y < size_y; y++) { - int* JXL_RESTRICT row = image.channel[c].Row(y); + int32_t* JXL_RESTRICT row = image.channel[c].Row(y); for (size_t x = 0; x < size_x; x++) { row[x] = (*encoding.qraw.qtable)[c * size_x * size_y + y * size_x + x]; } @@ -1713,14 +1727,14 @@ void ModularFrameEncoder::EncodeQuantTable( void ModularFrameEncoder::AddQuantTable(size_t size_x, size_t size_y, const QuantEncoding& encoding, size_t idx) { - size_t stream_id = ModularStreamId::QuantTable(idx).ID(frame_dim); + size_t stream_id = ModularStreamId::QuantTable(idx).ID(frame_dim_); JXL_ASSERT(encoding.qraw.qtable != nullptr); JXL_ASSERT(size_x * size_y * 3 == encoding.qraw.qtable->size()); - Image& image = stream_images[stream_id]; + Image& image = stream_images_[stream_id]; image = Image(size_x, size_y, 8, 3); for (size_t c = 0; c < 3; c++) { for (size_t y = 0; y < size_y; y++) { - int* JXL_RESTRICT row = image.channel[c].Row(y); + int32_t* JXL_RESTRICT row = image.channel[c].Row(y); for (size_t x = 0; x < size_x; x++) { row[x] = (*encoding.qraw.qtable)[c * size_x * size_y + y * size_x + x]; } diff --git a/media/libjxl/src/lib/jxl/enc_modular.h b/media/libjxl/src/lib/jxl/enc_modular.h index 10287dec88..02477ee65d 100644 --- a/media/libjxl/src/lib/jxl/enc_modular.h +++ b/media/libjxl/src/lib/jxl/enc_modular.h @@ -63,29 +63,28 @@ class ModularFrameEncoder { std::vector<uint8_t> extra_dc_precision; private: - Status PrepareEncoding(ThreadPool* pool, const FrameDimensions& frame_dim, + Status PrepareEncoding(const FrameHeader& frame_header, ThreadPool* pool, EncoderHeuristics* heuristics, AuxOut* aux_out = nullptr); Status PrepareStreamParams(const Rect& rect, const CompressParams& cparams, int minShift, int maxShift, const ModularStreamId& stream, bool do_color); - std::vector<Image> stream_images; - std::vector<ModularOptions> stream_options; + std::vector<Image> stream_images_; + std::vector<ModularOptions> stream_options_; - Tree tree; - std::vector<std::vector<Token>> tree_tokens; - std::vector<GroupHeader> stream_headers; - std::vector<std::vector<Token>> tokens; - EntropyEncodingData code; - std::vector<uint8_t> context_map; - FrameDimensions frame_dim; - CompressParams cparams; - float quality = 100.f; - std::vector<size_t> tree_splits; - std::vector<ModularMultiplierInfo> multiplier_info; - std::vector<std::vector<uint32_t>> gi_channel; - std::vector<size_t> image_widths; - Predictor delta_pred = Predictor::Average4; + Tree tree_; + std::vector<std::vector<Token>> tree_tokens_; + std::vector<GroupHeader> stream_headers_; + std::vector<std::vector<Token>> tokens_; + EntropyEncodingData code_; + std::vector<uint8_t> context_map_; + FrameDimensions frame_dim_; + CompressParams cparams_; + std::vector<size_t> tree_splits_; + std::vector<ModularMultiplierInfo> multiplier_info_; + std::vector<std::vector<uint32_t>> gi_channel_; + std::vector<size_t> image_widths_; + Predictor delta_pred_ = Predictor::Average4; }; } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/enc_params.h b/media/libjxl/src/lib/jxl/enc_params.h index 9445817f94..2e16fae864 100644 --- a/media/libjxl/src/lib/jxl/enc_params.h +++ b/media/libjxl/src/lib/jxl/enc_params.h @@ -202,6 +202,7 @@ struct CompressParams { // Prints extra information during/after encoding. bool verbose = false; + bool log_search_state = false; ButteraugliParams ba_params; @@ -242,8 +243,6 @@ struct CompressParams { color_transform = jxl::ColorTransform::kNone; } - bool use_new_heuristics = false; - // Down/upsample the image before encoding / after decoding by this factor. // The resampling value can also be set to <= 0 to automatically choose based // on distance, however EncodeFrame doesn't support this, so it is @@ -253,6 +252,11 @@ struct CompressParams { int ec_resampling = -1; // Skip the downsampling before encoding if this is true. bool already_downsampled = false; + // Butteraugli target distance on the original full size image, this can be + // different from butteraugli_distance if resampling was used. + float original_butteraugli_distance = -1.0f; + + float quant_ac_rescale = 1.0; // Codestream level to conform to. // -1: don't care diff --git a/media/libjxl/src/lib/jxl/enc_patch_dictionary.cc b/media/libjxl/src/lib/jxl/enc_patch_dictionary.cc index 69c2423083..ff57ff049e 100644 --- a/media/libjxl/src/lib/jxl/enc_patch_dictionary.cc +++ b/media/libjxl/src/lib/jxl/enc_patch_dictionary.cc @@ -10,6 +10,7 @@ #include <sys/types.h> #include <algorithm> +#include <atomic> #include <string> #include <tuple> #include <utility> @@ -44,35 +45,37 @@ void PatchDictionaryEncoder::Encode(const PatchDictionary& pdic, AuxOut* aux_out) { JXL_ASSERT(pdic.HasAny()); std::vector<std::vector<Token>> tokens(1); + size_t num_ec = pdic.shared_->metadata->m.num_extra_channels; auto add_num = [&](int context, size_t num) { tokens[0].emplace_back(context, num); }; size_t num_ref_patch = 0; for (size_t i = 0; i < pdic.positions_.size();) { - size_t i_start = i; + size_t ref_pos_idx = pdic.positions_[i].ref_pos_idx; while (i < pdic.positions_.size() && - pdic.positions_[i].ref_pos == pdic.positions_[i_start].ref_pos) { + pdic.positions_[i].ref_pos_idx == ref_pos_idx) { i++; } num_ref_patch++; } add_num(kNumRefPatchContext, num_ref_patch); + size_t blend_pos = 0; for (size_t i = 0; i < pdic.positions_.size();) { size_t i_start = i; + size_t ref_pos_idx = pdic.positions_[i].ref_pos_idx; + const auto& ref_pos = pdic.ref_positions_[ref_pos_idx]; while (i < pdic.positions_.size() && - pdic.positions_[i].ref_pos == pdic.positions_[i_start].ref_pos) { + pdic.positions_[i].ref_pos_idx == ref_pos_idx) { i++; } size_t num = i - i_start; JXL_ASSERT(num > 0); - add_num(kReferenceFrameContext, pdic.positions_[i_start].ref_pos.ref); - add_num(kPatchReferencePositionContext, - pdic.positions_[i_start].ref_pos.x0); - add_num(kPatchReferencePositionContext, - pdic.positions_[i_start].ref_pos.y0); - add_num(kPatchSizeContext, pdic.positions_[i_start].ref_pos.xsize - 1); - add_num(kPatchSizeContext, pdic.positions_[i_start].ref_pos.ysize - 1); + add_num(kReferenceFrameContext, ref_pos.ref); + add_num(kPatchReferencePositionContext, ref_pos.x0); + add_num(kPatchReferencePositionContext, ref_pos.y0); + add_num(kPatchSizeContext, ref_pos.xsize - 1); + add_num(kPatchSizeContext, ref_pos.ysize - 1); add_num(kPatchCountContext, num - 1); for (size_t j = i_start; j < i; j++) { const PatchPosition& pos = pdic.positions_[j]; @@ -85,11 +88,8 @@ void PatchDictionaryEncoder::Encode(const PatchDictionary& pdic, add_num(kPatchOffsetContext, PackSigned(pos.y - pdic.positions_[j - 1].y)); } - JXL_ASSERT(pdic.shared_->metadata->m.extra_channel_info.size() + 1 == - pos.blending.size()); - for (size_t i = 0; - i < pdic.shared_->metadata->m.extra_channel_info.size() + 1; i++) { - const PatchBlending& info = pos.blending[i]; + for (size_t j = 0; j < num_ec + 1; ++j, ++blend_pos) { + const PatchBlending& info = pdic.blendings_[blend_pos]; add_num(kPatchBlendModeContext, static_cast<uint32_t>(info.mode)); if (UsesAlpha(info.mode) && pdic.shared_->metadata->m.extra_channel_info.size() > 1) { @@ -113,46 +113,48 @@ void PatchDictionaryEncoder::Encode(const PatchDictionary& pdic, // static void PatchDictionaryEncoder::SubtractFrom(const PatchDictionary& pdic, Image3F* opsin) { + size_t num_ec = pdic.shared_->metadata->m.num_extra_channels; // TODO(veluca): this can likely be optimized knowing it runs on full images. for (size_t y = 0; y < opsin->ysize(); y++) { - if (y + 1 >= pdic.patch_starts_.size()) continue; float* JXL_RESTRICT rows[3] = { opsin->PlaneRow(0, y), opsin->PlaneRow(1, y), opsin->PlaneRow(2, y), }; - for (size_t id = pdic.patch_starts_[y]; id < pdic.patch_starts_[y + 1]; - id++) { - const PatchPosition& pos = pdic.positions_[pdic.sorted_patches_[id]]; + for (size_t pos_idx : pdic.GetPatchesForRow(y)) { + const size_t blending_idx = pos_idx * (num_ec + 1); + const PatchPosition& pos = pdic.positions_[pos_idx]; + const PatchReferencePosition& ref_pos = + pdic.ref_positions_[pos.ref_pos_idx]; + const PatchBlendMode mode = pdic.blendings_[blending_idx].mode; size_t by = pos.y; size_t bx = pos.x; - size_t xsize = pos.ref_pos.xsize; + size_t xsize = ref_pos.xsize; JXL_DASSERT(y >= by); - JXL_DASSERT(y < by + pos.ref_pos.ysize); + JXL_DASSERT(y < by + ref_pos.ysize); size_t iy = y - by; - size_t ref = pos.ref_pos.ref; + size_t ref = ref_pos.ref; const float* JXL_RESTRICT ref_rows[3] = { pdic.shared_->reference_frames[ref].frame->color()->ConstPlaneRow( - 0, pos.ref_pos.y0 + iy) + - pos.ref_pos.x0, + 0, ref_pos.y0 + iy) + + ref_pos.x0, pdic.shared_->reference_frames[ref].frame->color()->ConstPlaneRow( - 1, pos.ref_pos.y0 + iy) + - pos.ref_pos.x0, + 1, ref_pos.y0 + iy) + + ref_pos.x0, pdic.shared_->reference_frames[ref].frame->color()->ConstPlaneRow( - 2, pos.ref_pos.y0 + iy) + - pos.ref_pos.x0, + 2, ref_pos.y0 + iy) + + ref_pos.x0, }; for (size_t ix = 0; ix < xsize; ix++) { for (size_t c = 0; c < 3; c++) { - if (pos.blending[0].mode == PatchBlendMode::kAdd) { + if (mode == PatchBlendMode::kAdd) { rows[c][bx + ix] -= ref_rows[c][ix]; - } else if (pos.blending[0].mode == PatchBlendMode::kReplace) { + } else if (mode == PatchBlendMode::kReplace) { rows[c][bx + ix] = 0; - } else if (pos.blending[0].mode == PatchBlendMode::kNone) { + } else if (mode == PatchBlendMode::kNone) { // Nothing to do. } else { - JXL_ABORT("Blending mode %u not yet implemented", - (uint32_t)pos.blending[0].mode); + JXL_ABORT("Blending mode %u not yet implemented", (uint32_t)mode); } } } @@ -674,12 +676,15 @@ void FindBestPatchDictionary(const Image3F& opsin, // TODO(veluca): figure out a better way to fill the image. ZeroFillImage(&reference_frame); std::vector<PatchPosition> positions; + std::vector<PatchReferencePosition> pref_positions; + std::vector<PatchBlending> blendings; float* JXL_RESTRICT ref_rows[3] = { reference_frame.PlaneRow(0, 0), reference_frame.PlaneRow(1, 0), reference_frame.PlaneRow(2, 0), }; size_t ref_stride = reference_frame.PixelsPerRow(); + size_t num_ec = state->shared.metadata->m.num_extra_channels; for (size_t i = 0; i < info.size(); i++) { PatchReferencePosition ref_pos; @@ -696,35 +701,37 @@ void FindBestPatchDictionary(const Image3F& opsin, } } } - // Add color channels, ignore other channels. - std::vector<PatchBlending> blending_info( - state->shared.metadata->m.extra_channel_info.size() + 1, - PatchBlending{PatchBlendMode::kNone, 0, false}); - blending_info[0].mode = PatchBlendMode::kAdd; for (const auto& pos : info[i].second) { positions.emplace_back( - PatchPosition{pos.first, pos.second, blending_info, ref_pos}); + PatchPosition{pos.first, pos.second, pref_positions.size()}); + // Add blending for color channels, ignore other channels. + blendings.push_back({PatchBlendMode::kAdd, 0, false}); + for (size_t j = 0; j < num_ec; ++j) { + blendings.push_back({PatchBlendMode::kNone, 0, false}); + } } + pref_positions.emplace_back(std::move(ref_pos)); } CompressParams cparams = state->cparams; // Recursive application of patches could create very weird issues. cparams.patches = Override::kOff; - RoundtripPatchFrame(&reference_frame, state, 0, cparams, cms, pool, true); + RoundtripPatchFrame(&reference_frame, state, 0, cparams, cms, pool, aux_out, + /*subtract=*/true); // TODO(veluca): this assumes that applying patches is commutative, which is // not true for all blending modes. This code only produces kAdd patches, so // this works out. - std::sort(positions.begin(), positions.end()); - PatchDictionaryEncoder::SetPositions(&state->shared.image_features.patches, - std::move(positions)); + PatchDictionaryEncoder::SetPositions( + &state->shared.image_features.patches, std::move(positions), + std::move(pref_positions), std::move(blendings)); } void RoundtripPatchFrame(Image3F* reference_frame, PassesEncoderState* JXL_RESTRICT state, int idx, CompressParams& cparams, const JxlCmsInterface& cms, - ThreadPool* pool, bool subtract) { + ThreadPool* pool, AuxOut* aux_out, bool subtract) { FrameInfo patch_frame_info; cparams.resampling = 1; cparams.ec_resampling = 1; @@ -763,29 +770,36 @@ void RoundtripPatchFrame(Image3F* reference_frame, } PassesEncoderState roundtrip_state; auto special_frame = std::unique_ptr<BitWriter>(new BitWriter()); + AuxOut patch_aux_out; JXL_CHECK(EncodeFrame(cparams, patch_frame_info, state->shared.metadata, ib, &roundtrip_state, cms, pool, special_frame.get(), - nullptr)); + aux_out ? &patch_aux_out : nullptr)); + if (aux_out) { + for (const auto& l : patch_aux_out.layers) { + aux_out->layers[kLayerDictionary].Assimilate(l); + } + } const Span<const uint8_t> encoded = special_frame->GetSpan(); state->special_frames.emplace_back(std::move(special_frame)); if (subtract) { - BitReader br(encoded); ImageBundle decoded(&state->shared.metadata->m); PassesDecoderState dec_state; - JXL_CHECK(dec_state.output_encoding_info.Set( - *state->shared.metadata, - ColorEncoding::LinearSRGB( - state->shared.metadata->m.color_encoding.IsGray()))); - JXL_CHECK(DecodeFrame({}, &dec_state, pool, &br, &decoded, - *state->shared.metadata, /*constraints=*/nullptr)); + JXL_CHECK(dec_state.output_encoding_info.SetFromMetadata( + *state->shared.metadata)); + const uint8_t* frame_start = encoded.data(); + size_t encoded_size = encoded.size(); + JXL_CHECK(DecodeFrame(&dec_state, pool, frame_start, encoded_size, &decoded, + *state->shared.metadata)); + frame_start += decoded.decoded_bytes(); + encoded_size -= decoded.decoded_bytes(); size_t ref_xsize = dec_state.shared_storage.reference_frames[idx].storage.color()->xsize(); // if the frame itself uses patches, we need to decode another frame if (!ref_xsize) { - JXL_CHECK(DecodeFrame({}, &dec_state, pool, &br, &decoded, - *state->shared.metadata, /*constraints=*/nullptr)); + JXL_CHECK(DecodeFrame(&dec_state, pool, frame_start, encoded_size, + &decoded, *state->shared.metadata)); } - JXL_CHECK(br.Close()); + JXL_CHECK(encoded_size == 0); state->shared.reference_frames[idx] = std::move(dec_state.shared_storage.reference_frames[idx]); } else { diff --git a/media/libjxl/src/lib/jxl/enc_patch_dictionary.h b/media/libjxl/src/lib/jxl/enc_patch_dictionary.h index 8bacf7589a..090827f680 100644 --- a/media/libjxl/src/lib/jxl/enc_patch_dictionary.h +++ b/media/libjxl/src/lib/jxl/enc_patch_dictionary.h @@ -81,14 +81,13 @@ class PatchDictionaryEncoder { size_t layer, AuxOut* aux_out); static void SetPositions(PatchDictionary* pdic, - std::vector<PatchPosition> positions) { - if (pdic->positions_.empty()) { - pdic->positions_ = std::move(positions); - } else { - pdic->positions_.insert(pdic->positions_.end(), positions.begin(), - positions.end()); - } - pdic->ComputePatchCache(); + std::vector<PatchPosition> positions, + std::vector<PatchReferencePosition> ref_positions, + std::vector<PatchBlending> blendings) { + pdic->positions_ = std::move(positions); + pdic->ref_positions_ = std::move(ref_positions); + pdic->blendings_ = std::move(blendings); + pdic->ComputePatchTree(); } static void SubtractFrom(const PatchDictionary& pdic, Image3F* opsin); @@ -102,7 +101,7 @@ void FindBestPatchDictionary(const Image3F& opsin, void RoundtripPatchFrame(Image3F* reference_frame, PassesEncoderState* JXL_RESTRICT state, int idx, CompressParams& cparams, const JxlCmsInterface& cms, - ThreadPool* pool, bool subtract); + ThreadPool* pool, AuxOut* aux_out, bool subtract); } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/enc_photon_noise_test.cc b/media/libjxl/src/lib/jxl/enc_photon_noise_test.cc index 3790fdee99..83707255de 100644 --- a/media/libjxl/src/lib/jxl/enc_photon_noise_test.cc +++ b/media/libjxl/src/lib/jxl/enc_photon_noise_test.cc @@ -5,7 +5,7 @@ #include "lib/jxl/enc_photon_noise.h" -#include "gmock/gmock.h" +#include "lib/jxl/test_utils.h" namespace jxl { namespace { diff --git a/media/libjxl/src/lib/jxl/enc_quant_weights.cc b/media/libjxl/src/lib/jxl/enc_quant_weights.cc index 33d0e47bae..d8a9931a54 100644 --- a/media/libjxl/src/lib/jxl/enc_quant_weights.cc +++ b/media/libjxl/src/lib/jxl/enc_quant_weights.cc @@ -105,10 +105,9 @@ Status EncodeQuant(const QuantEncoding& encoding, size_t idx, size_t size_x, JXL_RETURN_IF_ERROR(F16Coder::Write( encoding.afv_weights[c][i] * (i < 6 ? 1.0f / 64 : 1.0f), writer)); } - JXL_RETURN_IF_ERROR(EncodeDctParams(encoding.dct_params, writer)); - JXL_RETURN_IF_ERROR( - EncodeDctParams(encoding.dct_params_afv_4x4, writer)); } + JXL_RETURN_IF_ERROR(EncodeDctParams(encoding.dct_params, writer)); + JXL_RETURN_IF_ERROR(EncodeDctParams(encoding.dct_params_afv_4x4, writer)); break; } } @@ -176,6 +175,14 @@ void DequantMatricesSetCustomDC(DequantMatrices* matrices, const float* dc) { JXL_CHECK(br.Close()); } +void DequantMatricesScaleDC(DequantMatrices* matrices, const float scale) { + float dc[3]; + for (size_t c = 0; c < 3; ++c) { + dc[c] = matrices->InvDCQuant(c) * (1.0f / scale); + } + DequantMatricesSetCustomDC(matrices, dc); +} + void DequantMatricesSetCustom(DequantMatrices* matrices, const std::vector<QuantEncoding>& encodings, ModularFrameEncoder* encoder) { diff --git a/media/libjxl/src/lib/jxl/enc_quant_weights.h b/media/libjxl/src/lib/jxl/enc_quant_weights.h index 89033d8cbb..fe5273cf78 100644 --- a/media/libjxl/src/lib/jxl/enc_quant_weights.h +++ b/media/libjxl/src/lib/jxl/enc_quant_weights.h @@ -20,6 +20,8 @@ Status DequantMatricesEncodeDC(const DequantMatrices* matrices, // precision. void DequantMatricesSetCustomDC(DequantMatrices* matrices, const float* dc); +void DequantMatricesScaleDC(DequantMatrices* matrices, const float scale); + void DequantMatricesSetCustom(DequantMatrices* matrices, const std::vector<QuantEncoding>& encodings, ModularFrameEncoder* encoder); diff --git a/media/libjxl/src/lib/jxl/enc_xyb.cc b/media/libjxl/src/lib/jxl/enc_xyb.cc index 7421361cf8..577e29686b 100644 --- a/media/libjxl/src/lib/jxl/enc_xyb.cc +++ b/media/libjxl/src/lib/jxl/enc_xyb.cc @@ -22,79 +22,23 @@ #include "lib/jxl/color_management.h" #include "lib/jxl/enc_bit_writer.h" #include "lib/jxl/enc_image_bundle.h" +#include "lib/jxl/fast_math-inl.h" #include "lib/jxl/fields.h" #include "lib/jxl/image_bundle.h" #include "lib/jxl/image_ops.h" #include "lib/jxl/opsin_params.h" #include "lib/jxl/transfer_functions-inl.h" + HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. -using hwy::HWY_NAMESPACE::ShiftRight; - -// Returns cbrt(x) + add with 6 ulp max error. -// Modified from vectormath_exp.h, Apache 2 license. -// https://www.agner.org/optimize/vectorclass.zip -template <class V> -V CubeRootAndAdd(const V x, const V add) { - const HWY_FULL(float) df; - const HWY_FULL(int32_t) di; - - const auto kExpBias = Set(di, 0x54800000); // cast(1.) + cast(1.) / 3 - const auto kExpMul = Set(di, 0x002AAAAA); // shifted 1/3 - const auto k1_3 = Set(df, 1.0f / 3); - const auto k4_3 = Set(df, 4.0f / 3); - - const auto xa = x; // assume inputs never negative - const auto xa_3 = k1_3 * xa; - - // Multiply exponent by -1/3 - const auto m1 = BitCast(di, xa); - // Special case for 0. 0 is represented with an exponent of 0, so the - // "kExpBias - 1/3 * exp" below gives the wrong result. The IfThenZeroElse() - // sets those values as 0, which prevents having NaNs in the computations - // below. - const auto m2 = - IfThenZeroElse(m1 == Zero(di), kExpBias - (ShiftRight<23>(m1)) * kExpMul); - auto r = BitCast(df, m2); - - // Newton-Raphson iterations - for (int i = 0; i < 3; i++) { - const auto r2 = r * r; - r = NegMulAdd(xa_3, r2 * r2, k4_3 * r); - } - // Final iteration - auto r2 = r * r; - r = MulAdd(k1_3, NegMulAdd(xa, r2 * r2, r), r); - r2 = r * r; - r = MulAdd(r2, x, add); - - return r; -} - -// Ensures infinity norm is bounded. -void TestCubeRoot() { - const HWY_FULL(float) d; - float max_err = 0.0f; - for (uint64_t x5 = 0; x5 < 2000000; x5++) { - const float x = x5 * 1E-5f; - const float expected = cbrtf(x); - HWY_ALIGN float approx[MaxLanes(d)]; - Store(CubeRootAndAdd(Set(d, x), Zero(d)), d, approx); - - // All lanes are same - for (size_t i = 1; i < Lanes(d); ++i) { - JXL_ASSERT(std::abs(approx[0] - approx[i]) <= 1.2E-7f); - } - - const float err = std::abs(approx[0] - expected); - max_err = std::max(max_err, err); - } - // printf("max err %e\n", max_err); - JXL_ASSERT(max_err < 8E-7f); -} +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::Sub; +using hwy::HWY_NAMESPACE::ZeroIfNegative; // 4x3 matrix * 3x1 SIMD vectors template <class V> @@ -124,8 +68,8 @@ void StoreXYB(const V r, V g, const V b, float* JXL_RESTRICT valx, float* JXL_RESTRICT valy, float* JXL_RESTRICT valz) { const HWY_FULL(float) d; const V half = Set(d, 0.5f); - Store(half * (r - g), d, valx); - Store(half * (r + g), d, valy); + Store(Mul(half, Sub(r, g)), d, valx); + Store(Mul(half, Add(r, g)), d, valy); Store(b, d, valz); } @@ -354,10 +298,10 @@ Status RgbToYcbcr(const ImageF& r_plane, const ImageF& g_plane, const auto kB = Set(df, 0.114f); const auto kAmpR = Set(df, 0.701f); const auto kAmpB = Set(df, 0.886f); - const auto kDiffR = kAmpR + kR; - const auto kDiffB = kAmpB + kB; - const auto kNormR = Set(df, 1.0f) / (kAmpR + kG + kB); - const auto kNormB = Set(df, 1.0f) / (kR + kG + kAmpB); + const auto kDiffR = Add(kAmpR, kR); + const auto kDiffB = Add(kAmpB, kB); + const auto kNormR = Div(Set(df, 1.0f), (Add(kAmpR, Add(kG, kB)))); + const auto kNormB = Div(Set(df, 1.0f), (Add(kR, Add(kG, kAmpB)))); constexpr size_t kGroupArea = kGroupDim * kGroupDim; const size_t lines_per_group = DivCeil(kGroupArea, xsize); @@ -376,15 +320,15 @@ Status RgbToYcbcr(const ImageF& r_plane, const ImageF& g_plane, const auto r = Load(df, r_row + x); const auto g = Load(df, g_row + x); const auto b = Load(df, b_row + x); - const auto r_base = r * kR; - const auto r_diff = r * kDiffR; - const auto g_base = g * kG; - const auto b_base = b * kB; - const auto b_diff = b * kDiffB; - const auto y_base = r_base + g_base + b_base; - const auto y_vec = y_base - k128; - const auto cb_vec = (b_diff - y_base) * kNormB; - const auto cr_vec = (r_diff - y_base) * kNormR; + const auto r_base = Mul(r, kR); + const auto r_diff = Mul(r, kDiffR); + const auto g_base = Mul(g, kG); + const auto b_base = Mul(b, kB); + const auto b_diff = Mul(b, kDiffB); + const auto y_base = Add(r_base, Add(g_base, b_base)); + const auto y_vec = Sub(y_base, k128); + const auto cb_vec = Mul(Sub(b_diff, y_base), kNormB); + const auto cr_vec = Mul(Sub(r_diff, y_base), kNormR); Store(y_vec, df, y_row + x); Store(cb_vec, df, cb_row + x); Store(cr_vec, df, cr_row + x); @@ -417,9 +361,6 @@ Status RgbToYcbcr(const ImageF& r_plane, const ImageF& g_plane, cb_plane, cr_plane, pool); } -HWY_EXPORT(TestCubeRoot); -void TestCubeRoot() { return HWY_DYNAMIC_DISPATCH(TestCubeRoot)(); } - // DEPRECATED Image3F OpsinDynamicsImage(const Image3B& srgb8, const JxlCmsInterface& cms) { ImageMetadata metadata; diff --git a/media/libjxl/src/lib/jxl/enc_xyb.h b/media/libjxl/src/lib/jxl/enc_xyb.h index aaa0ccc920..de8f2e3ff0 100644 --- a/media/libjxl/src/lib/jxl/enc_xyb.h +++ b/media/libjxl/src/lib/jxl/enc_xyb.h @@ -37,9 +37,6 @@ Status RgbToYcbcr(const ImageF& r_plane, const ImageF& g_plane, // DEPRECATED, used by opsin_image_wrapper. Image3F OpsinDynamicsImage(const Image3B& srgb8, const JxlCmsInterface& cms); -// For opsin_image_test. -void TestCubeRoot(); - } // namespace jxl #endif // LIB_JXL_ENC_XYB_H_ diff --git a/media/libjxl/src/lib/jxl/encode.cc b/media/libjxl/src/lib/jxl/encode.cc index a42f9ff4ec..8e02dd6301 100644 --- a/media/libjxl/src/lib/jxl/encode.cc +++ b/media/libjxl/src/lib/jxl/encode.cc @@ -14,6 +14,7 @@ #include "jxl/codestream_header.h" #include "jxl/types.h" #include "lib/jxl/aux_out.h" +#include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/span.h" #include "lib/jxl/codec_in_out.h" #include "lib/jxl/enc_color_management.h" @@ -21,20 +22,29 @@ #include "lib/jxl/enc_file.h" #include "lib/jxl/enc_icc_codec.h" #include "lib/jxl/encode_internal.h" +#include "lib/jxl/exif.h" #include "lib/jxl/jpeg/enc_jpeg_data.h" #include "lib/jxl/sanitizers.h" // Debug-printing failure macro similar to JXL_FAILURE, but for the status code // JXL_ENC_ERROR #ifdef JXL_CRASH_ON_ERROR -#define JXL_API_ERROR(format, ...) \ +#define JXL_API_ERROR(enc, error_code, format, ...) \ + (enc->error = error_code, \ + ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__), \ + ::jxl::Abort(), JXL_ENC_ERROR) +#define JXL_API_ERROR_NOSET(format, ...) \ (::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__), \ ::jxl::Abort(), JXL_ENC_ERROR) #else // JXL_CRASH_ON_ERROR -#define JXL_API_ERROR(format, ...) \ - (((JXL_DEBUG_ON_ERROR) && \ +#define JXL_API_ERROR(enc, error_code, format, ...) \ + (enc->error = error_code, \ + ((JXL_DEBUG_ON_ERROR) && \ ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__)), \ JXL_ENC_ERROR) +#define JXL_API_ERROR_NOSET(format, ...) \ + (::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__), \ + JXL_ENC_ERROR) #endif // JXL_CRASH_ON_ERROR namespace jxl {} // namespace jxl @@ -80,7 +90,7 @@ JxlEncoderStatus BrotliCompress(int quality, const uint8_t* in, size_t in_size, std::unique_ptr<BrotliEncoderState, decltype(BrotliEncoderDestroyInstance)*> enc(BrotliEncoderCreateInstance(nullptr, nullptr, nullptr), BrotliEncoderDestroyInstance); - if (!enc) return JXL_API_ERROR("BrotliEncoderCreateInstance failed"); + if (!enc) return JXL_API_ERROR_NOSET("BrotliEncoderCreateInstance failed"); BrotliEncoderSetParameter(enc.get(), BROTLI_PARAM_QUALITY, quality); BrotliEncoderSetParameter(enc.get(), BROTLI_PARAM_SIZE_HINT, in_size); @@ -100,7 +110,7 @@ JxlEncoderStatus BrotliCompress(int quality, const uint8_t* in, size_t in_size, if (!BrotliEncoderCompressStream(enc.get(), BROTLI_OPERATION_FINISH, &avail_in, &next_in, &avail_out, &next_out, &total_out)) { - return JXL_API_ERROR("Brotli compression failed"); + return JXL_API_ERROR_NOSET("Brotli compression failed"); } size_t out_size = next_out - temp_buffer.data(); jxl::msan::UnpoisonMemory(next_out - out_size, out_size); @@ -193,16 +203,93 @@ JxlEncoderStatus CheckValidBitdepth(uint32_t bits_per_sample, // The spec allows up to 31 for bits_per_sample here, but // the code does not (yet) support it. if (!(bits_per_sample > 0 && bits_per_sample <= 24)) { - return JXL_API_ERROR("Invalid value for bits_per_sample"); + return JXL_API_ERROR_NOSET("Invalid value for bits_per_sample"); } } else if ((exponent_bits_per_sample > 8) || (bits_per_sample > 24 + exponent_bits_per_sample) || (bits_per_sample < 3 + exponent_bits_per_sample)) { - return JXL_API_ERROR("Invalid float description"); + return JXL_API_ERROR_NOSET("Invalid float description"); } return JXL_ENC_SUCCESS; } +bool EncodeFrameIndexBox(const jxl::JxlEncoderFrameIndexBox& frame_index_box, + jxl::BitWriter& writer) { + bool ok = true; + int NF = 0; + for (size_t i = 0; i < frame_index_box.entries.size(); ++i) { + if (i == 0 || frame_index_box.entries[i].to_be_indexed) { + ++NF; + } + } + // Frame index box contents varint + 8 bytes + // continue with NF * 3 * varint + // varint max length is 10 for 64 bit numbers, and these numbers + // are limited to 63 bits. + static const int kVarintMaxLength = 10; + static const int kFrameIndexBoxHeaderLength = kVarintMaxLength + 8; + static const int kFrameIndexBoxElementLength = 3 * kVarintMaxLength; + const int buffer_size = + kFrameIndexBoxHeaderLength + NF * kFrameIndexBoxElementLength; + std::vector<uint8_t> buffer_vec(buffer_size); + uint8_t* buffer = buffer_vec.data(); + size_t output_pos = 0; + ok &= jxl::EncodeVarInt(NF, buffer_vec.size(), &output_pos, buffer); + StoreBE32(frame_index_box.TNUM, &buffer[output_pos]); + output_pos += 4; + StoreBE32(frame_index_box.TDEN, &buffer[output_pos]); + output_pos += 4; + // When we record a frame in the index, the record needs to know + // how many frames until the next indexed frame. That is why + // we store the 'prev' record. That 'prev' record needs to store + // the offset byte position to previously recorded indexed frame, + // that's why we also trace previous to the previous frame. + int prev_prev_ix = -1; // For position offset (OFFi) delta coding. + int prev_ix = 0; + int T_prev = 0; + int T = 0; + for (size_t i = 1; i < frame_index_box.entries.size(); ++i) { + if (frame_index_box.entries[i].to_be_indexed) { + // Now we can record the previous entry, since we need to store + // there how many frames until the next one. + int64_t OFFi = frame_index_box.entries[prev_ix].OFFi; + if (prev_prev_ix != -1) { + // Offi needs to be offset of start byte of this frame compared to start + // byte of previous frame from this index in the JPEG XL codestream. For + // the first frame, this is the offset from the first byte of the JPEG + // XL codestream. + OFFi -= frame_index_box.entries[prev_prev_ix].OFFi; + } + int32_t Ti = T_prev; + int32_t Fi = i - prev_ix; + ok &= jxl::EncodeVarInt(OFFi, buffer_vec.size(), &output_pos, buffer); + ok &= jxl::EncodeVarInt(Ti, buffer_vec.size(), &output_pos, buffer); + ok &= jxl::EncodeVarInt(Fi, buffer_vec.size(), &output_pos, buffer); + prev_prev_ix = prev_ix; + prev_ix = i; + T_prev = T; + T += frame_index_box.entries[i].duration; + } + } + { + // Last frame. + size_t i = frame_index_box.entries.size(); + int64_t OFFi = frame_index_box.entries[prev_ix].OFFi; + if (prev_prev_ix != -1) { + OFFi -= frame_index_box.entries[prev_prev_ix].OFFi; + } + int32_t Ti = T_prev; + int32_t Fi = i - prev_ix; + ok &= jxl::EncodeVarInt(OFFi, buffer_vec.size(), &output_pos, buffer); + ok &= jxl::EncodeVarInt(Ti, buffer_vec.size(), &output_pos, buffer); + ok &= jxl::EncodeVarInt(Fi, buffer_vec.size(), &output_pos, buffer); + } + // Enough buffer has been allocated, this function should never fail in + // writing. + JXL_ASSERT(ok); + return ok; +} + } // namespace JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { @@ -222,31 +309,36 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { // indicate full incompatibility. JXL_ASSERT(required_level == -1 || required_level == 5 || required_level == 10); + // codestream_level == -1 means auto-set to the required level + if (codestream_level == -1) codestream_level = required_level; if (codestream_level == 5 && required_level != 5) { // If the required level is 10, return error rather than automatically // setting the level to 10, to avoid inadvertently creating a level 10 // JXL file while intending to target a level 5 decoder. return JXL_API_ERROR( - "%s", + this, JXL_ENC_ERR_API_USAGE, "%s", ("Codestream level verification for level 5 failed: " + level_message) .c_str()); } - if (codestream_level == 10 && required_level == -1) { + if (required_level == -1) { return JXL_API_ERROR( - "%s", ("Codestream level verification for level 10 failed: " + - level_message) - .c_str()); + this, JXL_ENC_ERR_API_USAGE, "%s", + ("Codestream level verification for level 10 failed: " + + level_message) + .c_str()); } jxl::BitWriter writer; if (!WriteHeaders(&metadata, &writer, nullptr)) { - return JXL_API_ERROR("Failed to write codestream header"); + return JXL_API_ERROR(this, JXL_ENC_ERR_GENERIC, + "Failed to write codestream header"); } // Only send ICC (at least several hundred bytes) if fields aren't enough. if (metadata.m.color_encoding.WantICC()) { if (!jxl::WriteICC(metadata.m.color_encoding.ICC(), &writer, jxl::kLayerHeader, nullptr)) { - return JXL_API_ERROR("Failed to write ICC profile"); + return JXL_API_ERROR(this, JXL_ENC_ERR_GENERIC, + "Failed to write ICC profile"); } } // TODO(lode): preview should be added here if a preview image is added @@ -311,7 +403,8 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { num_queued_frames--; for (unsigned idx = 0; idx < input_frame->ec_initialized.size(); idx++) { if (!input_frame->ec_initialized[idx]) { - return JXL_API_ERROR("Extra channel %u is not initialized", idx); + return JXL_API_ERROR(this, JXL_ENC_ERR_API_USAGE, + "Extra channel %u is not initialized", idx); } } @@ -351,6 +444,8 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { ib.duration = 0; ib.timecode = 0; } + frame_index_box.AddFrame(codestream_bytes_written_end_of_frame, ib.duration, + input_frame->option_values.frame_index_box); ib.blendmode = static_cast<jxl::BlendMode>( input_frame->option_values.header.layer_info.blend_info.blendmode); ib.blend = @@ -398,7 +493,7 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { &metadata, input_frame->frame, &enc_state, cms, thread_pool.get(), &writer, /*aux_out=*/nullptr)) { - return JXL_API_ERROR("Failed to encode frame"); + return JXL_API_ERROR(this, JXL_ENC_ERR_GENERIC, "Failed to encode frame"); } codestream_bytes_written_beginning_of_frame = codestream_bytes_written_end_of_frame; @@ -426,6 +521,12 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { bytes.data() + bytes.size()); last_used_cparams = input_frame->option_values.cparams; + if (last_frame && frame_index_box.StoreFrameIndexBox()) { + bytes.clear(); + EncodeFrameIndexBox(frame_index_box, writer); + jxl::AppendBoxHeader(jxl::MakeBoxType("jxli"), bytes.size(), + /*unbounded=*/false, &output_byte_queue); + } } else { // Not a frame, so is a box instead jxl::MemoryManagerUniquePtr<jxl::JxlEncoderQueuedBox> box = @@ -443,7 +544,8 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { BrotliCompress((brotli_effort >= 0 ? brotli_effort : 4), box->contents.data(), box->contents.size(), &compressed)) { - return JXL_API_ERROR("Brotli compression for brob box failed"); + return JXL_API_ERROR(this, JXL_ENC_ERR_GENERIC, + "Brotli compression for brob box failed"); } jxl::AppendBoxHeader(jxl::MakeBoxType("brob"), compressed.size(), false, &output_byte_queue); @@ -462,13 +564,28 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { JxlEncoderStatus JxlEncoderSetColorEncoding(JxlEncoder* enc, const JxlColorEncoding* color) { + if (!enc->basic_info_set) { + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Basic info not yet set"); + } if (enc->color_encoding_set) { - // Already set - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "Color encoding is already set"); } if (!jxl::ConvertExternalToInternalColorEncoding( *color, &enc->metadata.m.color_encoding)) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_GENERIC, "Error in color conversion"); + } + if (enc->metadata.m.color_encoding.GetColorSpace() == + jxl::ColorSpace::kGray) { + if (enc->basic_info.num_color_channels != 1) + return JXL_API_ERROR( + enc, JXL_ENC_ERR_API_USAGE, + "Cannot use grayscale color encoding with num_color_channels != 1"); + } else { + if (enc->basic_info.num_color_channels != 3) + return JXL_API_ERROR( + enc, JXL_ENC_ERR_API_USAGE, + "Cannot use RGB color encoding with num_color_channels != 3"); } enc->color_encoding_set = true; if (!enc->intensity_target_set) { @@ -480,19 +597,42 @@ JxlEncoderStatus JxlEncoderSetColorEncoding(JxlEncoder* enc, JxlEncoderStatus JxlEncoderSetICCProfile(JxlEncoder* enc, const uint8_t* icc_profile, size_t size) { + if (!enc->basic_info_set) { + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Basic info not yet set"); + } if (enc->color_encoding_set) { - // Already set - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "ICC profile is already set"); } jxl::PaddedBytes icc; icc.assign(icc_profile, icc_profile + size); if (!enc->metadata.m.color_encoding.SetICC(std::move(icc))) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_BAD_INPUT, + "ICC profile could not be set"); + } + if (enc->metadata.m.color_encoding.GetColorSpace() == + jxl::ColorSpace::kGray) { + if (enc->basic_info.num_color_channels != 1) + return JXL_API_ERROR( + enc, JXL_ENC_ERR_BAD_INPUT, + "Cannot use grayscale ICC profile with num_color_channels != 1"); + } else { + if (enc->basic_info.num_color_channels != 3) + return JXL_API_ERROR( + enc, JXL_ENC_ERR_BAD_INPUT, + "Cannot use RGB ICC profile with num_color_channels != 3"); + // TODO(jon): also check that a kBlack extra channel is provided in the CMYK + // case } enc->color_encoding_set = true; if (!enc->intensity_target_set) { jxl::SetIntensityTarget(&enc->metadata.m); } + + if (!enc->basic_info.uses_original_profile) { + enc->metadata.m.color_encoding.DecideIfWantICC(); + } + return JXL_ENC_SUCCESS; } @@ -566,11 +706,11 @@ void JxlEncoderInitBlendInfo(JxlBlendInfo* blend_info) { JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, const JxlBasicInfo* info) { if (!enc->metadata.size.Set(info->xsize, info->ysize)) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Invalid dimensions"); } if (JXL_ENC_SUCCESS != CheckValidBitdepth(info->bits_per_sample, info->exponent_bits_per_sample)) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Invalid bit depth"); } enc->metadata.m.bit_depth.bits_per_sample = info->bits_per_sample; enc->metadata.m.bit_depth.exponent_bits_per_sample = @@ -587,6 +727,7 @@ JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, enc->metadata.m.extra_channel_info.resize(enc->metadata.m.num_extra_channels); if (info->num_extra_channels == 0 && info->alpha_bits) { return JXL_API_ERROR( + enc, JXL_ENC_ERR_API_USAGE, "when alpha_bits is non-zero, the number of channels must be at least " "1"); } @@ -598,7 +739,8 @@ JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, channel_info.bits_per_sample = info->alpha_bits; channel_info.exponent_bits_per_sample = info->alpha_exponent_bits; if (JxlEncoderSetExtraChannelInfo(enc, 0, &channel_info)) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "Problem setting extra channel info for alpha"); } } @@ -606,7 +748,12 @@ JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, if (info->orientation > 0 && info->orientation <= 8) { enc->metadata.m.orientation = info->orientation; } else { - return JXL_API_ERROR("Invalid value for orientation field"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "Invalid value for orientation field"); + } + if (info->num_color_channels != 1 && info->num_color_channels != 3) { + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "Invalid number of color channels"); } if (info->intensity_target != 0) { enc->metadata.m.SetIntensityTarget(info->intensity_target); @@ -622,16 +769,19 @@ JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, enc->metadata.m.tone_mapping.relative_to_max_display = info->relative_to_max_display; enc->metadata.m.tone_mapping.linear_below = info->linear_below; + enc->basic_info = *info; enc->basic_info_set = true; enc->metadata.m.have_animation = info->have_animation; if (info->have_animation) { if (info->animation.tps_denominator < 1) { return JXL_API_ERROR( + enc, JXL_ENC_ERR_API_USAGE, "If animation is used, tps_denominator must be >= 1"); } if (info->animation.tps_numerator < 1) { - return JXL_API_ERROR("If animation is used, tps_numerator must be >= 1"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "If animation is used, tps_numerator must be >= 1"); } enc->metadata.m.animation.tps_numerator = info->animation.tps_numerator; enc->metadata.m.animation.tps_denominator = info->animation.tps_denominator; @@ -641,11 +791,13 @@ JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, std::string level_message; int required_level = VerifyLevelSettings(enc, &level_message); if (required_level == -1 || - static_cast<int>(enc->codestream_level) < required_level) { - return JXL_API_ERROR("%s", ("Codestream level verification for level " + - std::to_string(enc->codestream_level) + - " failed: " + level_message) - .c_str()); + (static_cast<int>(enc->codestream_level) < required_level && + enc->codestream_level != -1)) { + return JXL_API_ERROR( + enc, JXL_ENC_ERR_API_USAGE, "%s", + ("Codestream level verification for level " + + std::to_string(enc->codestream_level) + " failed: " + level_message) + .c_str()); } return JXL_ENC_SUCCESS; } @@ -668,11 +820,12 @@ void JxlEncoderInitExtraChannelInfo(JxlExtraChannelType type, JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelInfo( JxlEncoder* enc, size_t index, const JxlExtraChannelInfo* info) { if (index >= enc->metadata.m.num_extra_channels) { - return JXL_API_ERROR("Invalid value for the index of extra channel"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "Invalid value for the index of extra channel"); } if (JXL_ENC_SUCCESS != CheckValidBitdepth(info->bits_per_sample, info->exponent_bits_per_sample)) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Invalid bit depth"); } jxl::ExtraChannelInfo& channel = enc->metadata.m.extra_channel_info[index]; @@ -693,11 +846,13 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelInfo( std::string level_message; int required_level = VerifyLevelSettings(enc, &level_message); if (required_level == -1 || - static_cast<int>(enc->codestream_level) < required_level) { - return JXL_API_ERROR("%s", ("Codestream level verification for level " + - std::to_string(enc->codestream_level) + - " failed: " + level_message) - .c_str()); + (static_cast<int>(enc->codestream_level) < required_level && + enc->codestream_level != -1)) { + return JXL_API_ERROR( + enc, JXL_ENC_ERR_API_USAGE, "%s", + ("Codestream level verification for level " + + std::to_string(enc->codestream_level) + " failed: " + level_message) + .c_str()); } return JXL_ENC_SUCCESS; } @@ -707,7 +862,8 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelName(JxlEncoder* enc, const char* name, size_t size) { if (index >= enc->metadata.m.num_extra_channels) { - return JXL_API_ERROR("Invalid value for the index of extra channel"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "Invalid value for the index of extra channel"); } enc->metadata.m.extra_channel_info[index].name = std::string(name, name + size); @@ -741,7 +897,8 @@ JxlEncoderStatus JxlEncoderSetFrameLossless( JxlEncoderFrameSettings* frame_settings, const JXL_BOOL lossless) { if (lossless && frame_settings->enc->basic_info_set && frame_settings->enc->metadata.m.xyb_encoded) { - return JXL_API_ERROR("Set use_original_profile=true for lossless encoding"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Set use_original_profile=true for lossless encoding"); } frame_settings->values.lossless = lossless; return JXL_ENC_SUCCESS; @@ -762,7 +919,8 @@ JxlEncoderStatus JxlEncoderOptionsSetEffort( JxlEncoderStatus JxlEncoderSetFrameDistance( JxlEncoderFrameSettings* frame_settings, float distance) { if (distance < 0.f || distance > 25.f) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Distance has to be in [0.0..25.0]"); } if (distance > 0.f && distance < 0.01f) { distance = 0.01f; @@ -785,18 +943,44 @@ JxlEncoderStatus JxlEncoderOptionsSetDecodingSpeed( JxlEncoderStatus JxlEncoderFrameSettingsSetOption( JxlEncoderFrameSettings* frame_settings, JxlEncoderFrameSettingId option, - int32_t value) { + int64_t value) { + // check if value is -1, 0 or 1 for Override-type options + switch (option) { + case JXL_ENC_FRAME_SETTING_NOISE: + case JXL_ENC_FRAME_SETTING_DOTS: + case JXL_ENC_FRAME_SETTING_PATCHES: + case JXL_ENC_FRAME_SETTING_GABORISH: + case JXL_ENC_FRAME_SETTING_MODULAR: + case JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE: + case JXL_ENC_FRAME_SETTING_GROUP_ORDER: + case JXL_ENC_FRAME_SETTING_RESPONSIVE: + case JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC: + case JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC: + case JXL_ENC_FRAME_SETTING_LOSSY_PALETTE: + case JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL: + if (value < -1 || value > 1) { + return JXL_API_ERROR( + frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be -1 (default), 0 (off) or 1 (on)"); + } + break; + default: + break; + } + switch (option) { case JXL_ENC_FRAME_SETTING_EFFORT: if (value < 1 || value > 9) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, + "Encode effort has to be in [1..9]"); } frame_settings->values.cparams.speed_tier = static_cast<jxl::SpeedTier>(10 - value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_BROTLI_EFFORT: if (value < -1 || value > 11) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Brotli effort has to be in [-1..11]"); } // set cparams for brotli use in JPEG frames frame_settings->values.cparams.brotli_effort = value; @@ -805,13 +989,15 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption( return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_DECODING_SPEED: if (value < 0 || value > 4) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, + "Decoding speed has to be in [0..4]"); } frame_settings->values.cparams.decoding_speed_tier = value; return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_RESAMPLING: if (value != -1 && value != 1 && value != 2 && value != 4 && value != 8) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Resampling factor has to be 1, 2, 4 or 8"); } frame_settings->values.cparams.resampling = value; return JXL_ENC_SUCCESS; @@ -823,7 +1009,8 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption( // JxlEncoderFrameSettingsSetOption due to the extra channel index // argument required. if (value != -1 && value != 1 && value != 2 && value != 4 && value != 8) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Resampling factor has to be 1, 2, 4 or 8"); } frame_settings->values.cparams.ec_resampling = value; return JXL_ENC_SUCCESS; @@ -833,98 +1020,72 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption( } frame_settings->values.cparams.already_downsampled = (value == 1); return JXL_ENC_SUCCESS; - case JXL_ENC_FRAME_SETTING_PHOTON_NOISE: - if (value < 0) return JXL_ENC_ERROR; - // TODO(lode): add encoder setting to set the 8 floating point values of - // the noise synthesis parameters per frame for more fine grained control. - frame_settings->values.cparams.photon_noise_iso = - static_cast<float>(value); - return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_NOISE: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.noise = static_cast<jxl::Override>(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_DOTS: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.dots = static_cast<jxl::Override>(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_PATCHES: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.patches = static_cast<jxl::Override>(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_EPF: - if (value < -1 || value > 3) return JXL_ENC_ERROR; + if (value < -1 || value > 3) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "EPF value has to be in [-1..3]"); + } frame_settings->values.cparams.epf = static_cast<int>(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_GABORISH: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.gaborish = static_cast<jxl::Override>(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_MODULAR: - if (value == 1) { - frame_settings->values.cparams.modular_mode = true; - } else if (value == -1 || value == 0) { - frame_settings->values.cparams.modular_mode = false; - } else { - return JXL_ENC_ERROR; - } + frame_settings->values.cparams.modular_mode = (value == 1); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.keep_invisible = static_cast<jxl::Override>(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_GROUP_ORDER: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.centerfirst = (value == 1); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X: - if (value < -1) return JXL_ENC_ERROR; + if (value < -1) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Center x coordinate has to be -1 or positive"); + } frame_settings->values.cparams.center_x = static_cast<size_t>(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y: - if (value < -1) return JXL_ENC_ERROR; + if (value < -1) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Center y coordinate has to be -1 or positive"); + } frame_settings->values.cparams.center_y = static_cast<size_t>(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_RESPONSIVE: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.responsive = value; return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.progressive_mode = value; return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC: - if (value < -1 || value > 1) return JXL_ENC_ERROR; frame_settings->values.cparams.qprogressive_mode = value; return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC: - if (value < -1 || value > 2) return JXL_ENC_ERROR; - frame_settings->values.cparams.progressive_dc = value; - return JXL_ENC_SUCCESS; - case JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT: - if (value < -1 || value > 100) return JXL_ENC_ERROR; - if (value == -1) { - frame_settings->values.cparams.channel_colors_pre_transform_percent = - 95.0f; - } else { - frame_settings->values.cparams.channel_colors_pre_transform_percent = - static_cast<float>(value); - } - return JXL_ENC_SUCCESS; - case JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT: - if (value < -1 || value > 100) return JXL_ENC_ERROR; - if (value == -1) { - frame_settings->values.cparams.channel_colors_percent = 80.0f; - } else { - frame_settings->values.cparams.channel_colors_percent = - static_cast<float>(value); + if (value < -1 || value > 2) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Progressive DC has to be in [-1..2]"); } + frame_settings->values.cparams.progressive_dc = value; return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_PALETTE_COLORS: - if (value < -1 || value > 70913) return JXL_ENC_ERROR; + if (value < -1 || value > 70913) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..70913]"); + } if (value == -1) { frame_settings->values.cparams.palette_colors = 1 << 10; } else { @@ -932,7 +1093,6 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption( } return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_LOSSY_PALETTE: - if (value < -1 || value > 1) return JXL_ENC_ERROR; // TODO(lode): the defaults of some palette settings depend on others. // See the logic in cjxl. Similar for other settings. This should be // handled in the encoder during JxlEncoderProcessOutput (or, @@ -940,7 +1100,10 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption( frame_settings->values.cparams.lossy_palette = (value == 1); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM: - if (value < -1 || value > 2) return JXL_ENC_ERROR; + if (value < -1 || value > 2) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..2]"); + } if (value == -1) { frame_settings->values.cparams.color_transform = jxl::ColorTransform::kXYB; @@ -950,11 +1113,17 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption( } return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE: - if (value < -1 || value > 35) return JXL_ENC_ERROR; - frame_settings->values.cparams.colorspace = value + 2; + if (value < -1 || value > 41) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..41]"); + } + frame_settings->values.cparams.colorspace = value; return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE: - if (value < -1 || value > 3) return JXL_ENC_ERROR; + if (value < -1 || value > 3) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..3]"); + } // TODO(lode): the default behavior of this parameter for cjxl is // to choose 1 or 2 depending on the situation. This behavior needs to be // implemented either in the C++ library by allowing to set this to -1, or @@ -966,30 +1135,23 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption( } return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR: - if (value < -1 || value > 15) return JXL_ENC_ERROR; + if (value < -1 || value > 15) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..15]"); + } frame_settings->values.cparams.options.predictor = static_cast<jxl::Predictor>(value); return JXL_ENC_SUCCESS; - case JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT: - if (value < -1) return JXL_ENC_ERROR; - // This value is called "iterations" or "nb_repeats" in cjxl, but is in - // fact a fraction in range 0.0-1.0, with the defautl value 0.5. - // Convert from integer percentage to floating point fraction here. - if (value == -1) { - // TODO(lode): for this and many other settings, avoid duplicating the - // default values here and in enc_params.h and options.h, have one - // location where the defaults are specified. - frame_settings->values.cparams.options.nb_repeats = 0.5f; - } else { - frame_settings->values.cparams.options.nb_repeats = value * 0.01f; - } - return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS: // The max allowed value can in theory be higher. However, it depends on // the effort setting. 11 is the highest safe value that doesn't cause // tree_samples to be >= 64 in the encoder. The specification may allow // more than this. With more fine tuning higher values could be allowed. - if (value < -1 || value > 11) return JXL_ENC_ERROR; + // For N-channel images, the largest useful value is N-1. + if (value < -1 || value > 11) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..11]"); + } if (value == -1) { frame_settings->values.cparams.options.max_properties = 0; } else { @@ -997,18 +1159,115 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption( } return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL: - if (value < -1 || value > 1) return JXL_ENC_ERROR; if (value == -1) { frame_settings->values.cparams.force_cfl_jpeg_recompression = true; } else { frame_settings->values.cparams.force_cfl_jpeg_recompression = value; } return JXL_ENC_SUCCESS; + case JXL_ENC_FRAME_INDEX_BOX: + frame_settings->values.frame_index_box = true; + return JXL_ENC_SUCCESS; + case JXL_ENC_FRAME_SETTING_PHOTON_NOISE: + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, + "Float option, try setting it with " + "JxlEncoderFrameSettingsSetFloatOption"); default: - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, + "Unknown option"); } } +JxlEncoderStatus JxlEncoderFrameSettingsSetFloatOption( + JxlEncoderFrameSettings* frame_settings, JxlEncoderFrameSettingId option, + float value) { + switch (option) { + case JXL_ENC_FRAME_SETTING_PHOTON_NOISE: + if (value < 0) return JXL_ENC_ERROR; + // TODO(lode): add encoder setting to set the 8 floating point values of + // the noise synthesis parameters per frame for more fine grained control. + frame_settings->values.cparams.photon_noise_iso = value; + return JXL_ENC_SUCCESS; + case JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT: + if (value < -1.f || value > 100.f) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be smaller than 100"); + } + // This value is called "iterations" or "nb_repeats" in cjxl, but is in + // fact a fraction in range 0.0-1.0, with the default value 0.5. + // Convert from floating point percentage to floating point fraction here. + if (value < -.5f) { + // TODO(lode): for this and many other settings (also in + // JxlEncoderFrameSettingsSetOption), avoid duplicating the default + // values here and in enc_params.h and options.h, have one location + // where the defaults are specified. + frame_settings->values.cparams.options.nb_repeats = 0.5f; + } else { + frame_settings->values.cparams.options.nb_repeats = value * 0.01f; + } + return JXL_ENC_SUCCESS; + case JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT: + if (value < -1.f || value > 100.f) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..100]"); + } + if (value < -.5f) { + frame_settings->values.cparams.channel_colors_pre_transform_percent = + 95.0f; + } else { + frame_settings->values.cparams.channel_colors_pre_transform_percent = + value; + } + return JXL_ENC_SUCCESS; + case JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT: + if (value < -1.f || value > 100.f) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..100]"); + } + if (value < -.5f) { + frame_settings->values.cparams.channel_colors_percent = 80.0f; + } else { + frame_settings->values.cparams.channel_colors_percent = value; + } + return JXL_ENC_SUCCESS; + case JXL_ENC_FRAME_SETTING_EFFORT: + case JXL_ENC_FRAME_SETTING_DECODING_SPEED: + case JXL_ENC_FRAME_SETTING_RESAMPLING: + case JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING: + case JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED: + case JXL_ENC_FRAME_SETTING_NOISE: + case JXL_ENC_FRAME_SETTING_DOTS: + case JXL_ENC_FRAME_SETTING_PATCHES: + case JXL_ENC_FRAME_SETTING_EPF: + case JXL_ENC_FRAME_SETTING_GABORISH: + case JXL_ENC_FRAME_SETTING_MODULAR: + case JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE: + case JXL_ENC_FRAME_SETTING_GROUP_ORDER: + case JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X: + case JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y: + case JXL_ENC_FRAME_SETTING_RESPONSIVE: + case JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC: + case JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC: + case JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC: + case JXL_ENC_FRAME_SETTING_PALETTE_COLORS: + case JXL_ENC_FRAME_SETTING_LOSSY_PALETTE: + case JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM: + case JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE: + case JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE: + case JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR: + case JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS: + case JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL: + case JXL_ENC_FRAME_INDEX_BOX: + case JXL_ENC_FRAME_SETTING_BROTLI_EFFORT: + case JXL_ENC_FRAME_SETTING_FILL_ENUM: + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, + "Int option, try setting it with " + "JxlEncoderFrameSettingsSetOption"); + default: + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, + "Unknown option"); + } +} JxlEncoder* JxlEncoderCreate(const JxlMemoryManager* memory_manager) { JxlMemoryManager local_memory_manager; if (!jxl::MemoryManagerInit(&local_memory_manager, memory_manager)) { @@ -1049,7 +1308,8 @@ void JxlEncoderReset(JxlEncoder* enc) { enc->intensity_target_set = false; enc->use_container = false; enc->use_boxes = false; - enc->codestream_level = 5; + enc->codestream_level = -1; + JxlEncoderInitBasicInfo(&enc->basic_info); } void JxlEncoderDestroy(JxlEncoder* enc) { @@ -1061,10 +1321,13 @@ void JxlEncoderDestroy(JxlEncoder* enc) { } } +JxlEncoderError JxlEncoderGetError(JxlEncoder* enc) { return enc->error; } + JxlEncoderStatus JxlEncoderUseContainer(JxlEncoder* enc, JXL_BOOL use_container) { if (enc->wrote_bytes) { - return JXL_API_ERROR("this setting can only be set at the beginning"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "this setting can only be set at the beginning"); } enc->use_container = static_cast<bool>(use_container); return JXL_ENC_SUCCESS; @@ -1073,16 +1336,20 @@ JxlEncoderStatus JxlEncoderUseContainer(JxlEncoder* enc, JxlEncoderStatus JxlEncoderStoreJPEGMetadata(JxlEncoder* enc, JXL_BOOL store_jpeg_metadata) { if (enc->wrote_bytes) { - return JXL_API_ERROR("this setting can only be set at the beginning"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "this setting can only be set at the beginning"); } enc->store_jpeg_metadata = static_cast<bool>(store_jpeg_metadata); return JXL_ENC_SUCCESS; } JxlEncoderStatus JxlEncoderSetCodestreamLevel(JxlEncoder* enc, int level) { - if (level != 5 && level != 10) return JXL_API_ERROR("invalid level"); + if (level != -1 && level != 5 && level != 10) { + return JXL_API_ERROR(enc, JXL_ENC_ERR_NOT_SUPPORTED, "invalid level"); + } if (enc->wrote_bytes) { - return JXL_API_ERROR("this setting can only be set at the beginning"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "this setting can only be set at the beginning"); } enc->codestream_level = level; return JXL_ENC_SUCCESS; @@ -1100,11 +1367,15 @@ void JxlEncoderSetCms(JxlEncoder* enc, JxlCmsInterface cms) { JxlEncoderStatus JxlEncoderSetParallelRunner(JxlEncoder* enc, JxlParallelRunner parallel_runner, void* parallel_runner_opaque) { - if (enc->thread_pool) return JXL_API_ERROR("parallel runner already set"); + if (enc->thread_pool) { + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "parallel runner already set"); + } enc->thread_pool = jxl::MemoryManagerMakeUnique<jxl::ThreadPool>( &enc->memory_manager, parallel_runner, parallel_runner_opaque); if (!enc->thread_pool) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(enc, JXL_ENC_ERR_GENERIC, + "error setting parallel runner"); } return JXL_ENC_SUCCESS; } @@ -1125,7 +1396,8 @@ JxlEncoderStatus GetCurrentDimensions( ysize = jxl::DivCeil(ysize, factor); } if (xsize == 0 || ysize == 0) { - return JXL_API_ERROR("zero-sized frame is not allowed"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "zero-sized frame is not allowed"); } return JXL_ENC_SUCCESS; } @@ -1135,19 +1407,22 @@ JxlEncoderStatus JxlEncoderAddJPEGFrame( const JxlEncoderFrameSettings* frame_settings, const uint8_t* buffer, size_t size) { if (frame_settings->enc->frames_closed) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Frame input is already closed"); } jxl::CodecInOut io; if (!jxl::jpeg::DecodeImageJPG(jxl::Span<const uint8_t>(buffer, size), &io)) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_BAD_INPUT, + "Error during decode of input JPEG"); } if (!frame_settings->enc->color_encoding_set) { if (!SetColorEncodingFromJpegData( *io.Main().jpeg_data, &frame_settings->enc->metadata.m.color_encoding)) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_BAD_INPUT, + "Error in input JPEG color space"); } } @@ -1159,21 +1434,52 @@ JxlEncoderStatus JxlEncoderAddJPEGFrame( basic_info.uses_original_profile = true; if (JxlEncoderSetBasicInfo(frame_settings->enc, &basic_info) != JXL_ENC_SUCCESS) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, + "Error setting basic info"); } } if (frame_settings->enc->metadata.m.xyb_encoded) { - // Can't XYB encode a lossless JPEG. - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Can't XYB encode a lossless JPEG"); + } + if (!io.blobs.exif.empty()) { + JxlOrientation orientation = static_cast<JxlOrientation>( + frame_settings->enc->metadata.m.orientation); + jxl::InterpretExif(io.blobs.exif, &orientation); + frame_settings->enc->metadata.m.orientation = orientation; + + size_t exif_size = io.blobs.exif.size(); + // Exif data in JPEG is limited to 64k + if (exif_size > 0xFFFF) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, + "Exif larger than possible in JPEG?"); + } + exif_size += 4; // prefix 4 zero bytes for tiff offset + std::vector<uint8_t> exif(exif_size); + memcpy(exif.data() + 4, io.blobs.exif.data(), io.blobs.exif.size()); + JxlEncoderUseBoxes(frame_settings->enc); + JxlEncoderAddBox(frame_settings->enc, "Exif", exif.data(), exif_size, + /*compress_box=*/JXL_TRUE); + } + if (!io.blobs.xmp.empty()) { + JxlEncoderUseBoxes(frame_settings->enc); + JxlEncoderAddBox(frame_settings->enc, "xml ", io.blobs.xmp.data(), + io.blobs.xmp.size(), /*compress_box=*/JXL_TRUE); + } + if (!io.blobs.jumbf.empty()) { + JxlEncoderUseBoxes(frame_settings->enc); + JxlEncoderAddBox(frame_settings->enc, "jumb", io.blobs.jumbf.data(), + io.blobs.jumbf.size(), /*compress_box=*/JXL_TRUE); } - if (frame_settings->enc->store_jpeg_metadata) { jxl::jpeg::JPEGData data_in = *io.Main().jpeg_data; jxl::PaddedBytes jpeg_data; if (!jxl::jpeg::EncodeJPEGData(data_in, &jpeg_data, frame_settings->values.cparams)) { - return JXL_ENC_ERROR; + return JXL_API_ERROR( + frame_settings->enc, JXL_ENC_ERR_JBRD, + "JPEG bitstream reconstruction data cannot be encoded"); } frame_settings->enc->jpeg_metadata = std::vector<uint8_t>( jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); @@ -1188,17 +1494,21 @@ JxlEncoderStatus JxlEncoderAddJPEGFrame( jxl::ImageBundle(&frame_settings->enc->metadata.m), {}}); if (!queued_frame) { - return JXL_ENC_ERROR; + // TODO(jon): when can this happen? is this an API usage error? + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, + "No frame queued?"); } queued_frame->frame.SetFromImage(std::move(*io.Main().color()), io.Main().c_current()); size_t xsize, ysize; if (GetCurrentDimensions(frame_settings, xsize, ysize) != JXL_ENC_SUCCESS) { - return JXL_API_ERROR("bad dimensions"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, + "bad dimensions"); } if (xsize != static_cast<size_t>(io.Main().jpeg_data->width) || ysize != static_cast<size_t>(io.Main().jpeg_data->height)) { - return JXL_API_ERROR("JPEG dimensions don't match frame dimensions"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, + "JPEG dimensions don't match frame dimensions"); } std::vector<jxl::ImageF> extra_channels( frame_settings->enc->metadata.m.num_extra_channels); @@ -1224,11 +1534,24 @@ JxlEncoderStatus JxlEncoderAddImageFrame( // Basic Info must be set, and color encoding must be set directly, // or set to XYB via JxlBasicInfo.uses_original_profile = JXL_FALSE // Otherwise, this is an API misuse. - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Basic info or color encoding not set yet"); } if (frame_settings->enc->frames_closed) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Frame input already closed"); + } + if (pixel_format->num_channels < 3) { + if (frame_settings->enc->basic_info.num_color_channels != 1) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Grayscale pixel format input for an RGB image"); + } + } else { + if (frame_settings->enc->basic_info.num_color_channels != 3) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "RGB pixel format input for a grayscale image"); + } } auto queued_frame = jxl::MemoryManagerMakeUnique<jxl::JxlEncoderQueuedFrame>( @@ -1241,7 +1564,9 @@ JxlEncoderStatus JxlEncoderAddImageFrame( {}}); if (!queued_frame) { - return JXL_ENC_ERROR; + // TODO(jon): when can this happen? is this an API usage error? + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, + "No frame queued?"); } jxl::ColorEncoding c_current; @@ -1261,11 +1586,14 @@ JxlEncoderStatus JxlEncoderAddImageFrame( static_cast<size_t>(num_channels == 2 || num_channels == 4); if (has_interleaved_alpha > frame_settings->enc->metadata.m.num_extra_channels) { - return JXL_API_ERROR("number of extra channels mismatch"); + return JXL_API_ERROR( + frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "number of extra channels mismatch (need 1 extra channel for alpha)"); } size_t xsize, ysize; if (GetCurrentDimensions(frame_settings, xsize, ysize) != JXL_ENC_SUCCESS) { - return JXL_API_ERROR("bad dimensions"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, + "bad dimensions"); } std::vector<jxl::ImageF> extra_channels( frame_settings->enc->metadata.m.num_extra_channels); @@ -1298,11 +1626,13 @@ JxlEncoderStatus JxlEncoderAddImageFrame( if (!jxl::BufferToImageBundle(*pixel_format, xsize, ysize, buffer, size, frame_settings->enc->thread_pool.get(), c_current, &(queued_frame->frame))) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Invalid input buffer"); } if (frame_settings->values.lossless && frame_settings->enc->metadata.m.xyb_encoded) { - return JXL_API_ERROR("Set use_original_profile=true for lossless encoding"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Set use_original_profile=true for lossless encoding"); } queued_frame->option_values.cparams.level = frame_settings->enc->codestream_level; @@ -1313,7 +1643,8 @@ JxlEncoderStatus JxlEncoderAddImageFrame( JxlEncoderStatus JxlEncoderUseBoxes(JxlEncoder* enc) { if (enc->wrote_bytes) { - return JXL_API_ERROR("this setting can only be set at the beginning"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "this setting can only be set at the beginning"); } enc->use_boxes = true; return JXL_ENC_SUCCESS; @@ -1324,21 +1655,25 @@ JxlEncoderStatus JxlEncoderAddBox(JxlEncoder* enc, const JxlBoxType type, JXL_BOOL compress_box) { if (!enc->use_boxes) { return JXL_API_ERROR( + enc, JXL_ENC_ERR_API_USAGE, "must set JxlEncoderUseBoxes at the beginning to add boxes"); } if (compress_box) { if (memcmp("jxl", type, 3) == 0) { return JXL_API_ERROR( + enc, JXL_ENC_ERR_API_USAGE, "brob box may not contain a type starting with \"jxl\""); } if (memcmp("jbrd", type, 4) == 0) { - return JXL_API_ERROR("jbrd box may not be brob compressed"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "jbrd box may not be brob compressed"); } if (memcmp("brob", type, 4) == 0) { // The compress_box will compress an existing non-brob box into a brob // box. If already giving a valid brotli-compressed brob box, set // compress_box to false since it is already compressed. - return JXL_API_ERROR("a brob box cannot contain another brob box"); + return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, + "a brob box cannot contain another brob box"); } } @@ -1356,27 +1691,33 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelBuffer( const JxlEncoderOptions* frame_settings, const JxlPixelFormat* pixel_format, const void* buffer, size_t size, uint32_t index) { if (index >= frame_settings->enc->metadata.m.num_extra_channels) { - return JXL_API_ERROR("Invalid value for the index of extra channel"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Invalid value for the index of extra channel"); } if (!frame_settings->enc->basic_info_set || !frame_settings->enc->color_encoding_set) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Basic info has to be set first"); } if (frame_settings->enc->input_queue.empty()) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "First add image frame, then extra channels"); } if (frame_settings->enc->frames_closed) { - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Frame input already closed"); } size_t xsize, ysize; if (GetCurrentDimensions(frame_settings, xsize, ysize) != JXL_ENC_SUCCESS) { - return JXL_API_ERROR("bad dimensions"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, + "bad dimensions"); } if (!jxl::BufferToImageF(*pixel_format, xsize, ysize, buffer, size, frame_settings->enc->thread_pool.get(), &frame_settings->enc->input_queue.back() .frame->frame.extra_channels()[index])) { - return JXL_API_ERROR("Failed to set buffer for extra channel"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Failed to set buffer for extra channel"); } frame_settings->enc->input_queue.back().frame->ec_initialized[index] = 1; @@ -1418,13 +1759,15 @@ JxlEncoderStatus JxlEncoderProcessOutput(JxlEncoder* enc, uint8_t** next_out, JxlEncoderStatus JxlEncoderSetFrameHeader(JxlEncoderOptions* frame_settings, const JxlFrameHeader* frame_header) { if (frame_header->layer_info.blend_info.source > 3) { - return JXL_API_ERROR("invalid blending source index"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "invalid blending source index"); } // If there are no extra channels, it's ok for the value to be 0. if (frame_header->layer_info.blend_info.alpha != 0 && frame_header->layer_info.blend_info.alpha >= frame_settings->enc->metadata.m.extra_channel_info.size()) { - return JXL_API_ERROR("alpha blend channel index out of bounds"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "alpha blend channel index out of bounds"); } frame_settings->values.header = *frame_header; @@ -1439,7 +1782,8 @@ JxlEncoderStatus JxlEncoderSetExtraChannelBlendInfo( JxlEncoderOptions* frame_settings, size_t index, const JxlBlendInfo* blend_info) { if (index >= frame_settings->enc->metadata.m.num_extra_channels) { - return JXL_API_ERROR("Invalid value for the index of extra channel"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Invalid value for the index of extra channel"); } if (frame_settings->values.extra_channel_blend_info.size() != @@ -1457,7 +1801,8 @@ JxlEncoderStatus JxlEncoderSetFrameName(JxlEncoderFrameSettings* frame_settings, const char* frame_name) { std::string str = frame_name ? frame_name : ""; if (str.size() > 1071) { - return JXL_API_ERROR("frame name can be max 1071 bytes long"); + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "frame name can be max 1071 bytes long"); } frame_settings->values.frame_name = str; frame_settings->values.header.name_length = str.size(); diff --git a/media/libjxl/src/lib/jxl/encode_internal.h b/media/libjxl/src/lib/jxl/encode_internal.h index 52301327a6..9f82546417 100644 --- a/media/libjxl/src/lib/jxl/encode_internal.h +++ b/media/libjxl/src/lib/jxl/encode_internal.h @@ -20,6 +20,84 @@ namespace jxl { +/* Frame index box 'jxli' will start with Varint() for +NF: has type Varint(): number of frames listed in the index. +TNUM: has type u32: numerator of tick unit. +TDEN: has type u32: denominator of tick unit. Value 0 means the file is +ill-formed. per frame i listed: OFFi: has type Varint(): offset of start byte of +this frame compared to start byte of previous frame from this index in the JPEG +XL codestream. For the first frame, this is the offset from the first byte of +the JPEG XL codestream. Ti: has type Varint(): duration in ticks between the +start of this frame and the start of the next frame in the index. If this is the +last frame in the index, this is the duration in ticks between the start of this +frame and the end of the stream. A tick lasts TNUM / TDEN seconds. Fi: has type +Varint(): amount of frames the next frame in the index occurs after this frame. +If this is the last frame in the index, this is the amount of frames after this +frame in the remainder of the stream. Only frames that are presented by the +decoder are counted for this purpose, this excludes frames that are not intended +for display but for compositing with other frames, such as frames that aren't +the last frame with a duration of 0 ticks. + +All the frames listed in jxli are keyframes and the first frame is +present in the list. +There shall be either zero or one Frame Index boxes in a JPEG XL file. +The offsets OFFi per frame are given as bytes in the codestream, not as +bytes in the file format using the box structure. This means if JPEG XL Partial +Codestream boxes are used, the offset is counted within the concatenated +codestream, bytes from box headers or non-codestream boxes are not counted. +*/ + +typedef struct JxlEncoderFrameIndexBoxEntryStruct { + bool to_be_indexed; + uint32_t duration; + uint64_t OFFi; +} JxlEncoderFrameIndexBoxEntry; + +typedef struct JxlEncoderFrameIndexBoxStruct { + // We always need to record the first frame entry, so presence of the + // first entry alone is not an indication if it was requested to be + // stored. + bool index_box_requested_through_api = false; + + int64_t NF() const { return entries.size(); } + bool StoreFrameIndexBox() { + for (auto e : entries) { + if (e.to_be_indexed) { + return true; + } + } + return false; + } + int32_t TNUM = 1; + int32_t TDEN = 1000; + + std::vector<JxlEncoderFrameIndexBoxEntry> entries; + + // That way we can ensure that every index box will have the first frame. + // If the API user decides to mark it as an indexed frame, we call + // the AddFrame again, this time with requested. + void AddFrame(uint64_t OFFi, uint32_t duration, bool to_be_indexed) { + // We call AddFrame to every frame. + // Recording the first frame is required by the standard. + // Knowing the last frame is required, since the last indexed frame + // needs to know how many frames until the end. + // To be able to tell how many frames there are between each index + // entry we just record every frame here. + if (entries.size() == 1) { + if (OFFi == entries[0].OFFi) { + // API use for the first frame, let's clear the already recorded first + // frame. + entries.clear(); + } + } + JxlEncoderFrameIndexBoxEntry e; + e.to_be_indexed = to_be_indexed; + e.OFFi = OFFi; + e.duration = duration; + entries.push_back(e); + } +} JxlEncoderFrameIndexBox; + // The encoder options (such as quality, compression speed, ...) for a single // frame, but not encoder-wide options such as box-related options. typedef struct JxlEncoderFrameSettingsValuesStruct { @@ -30,6 +108,7 @@ typedef struct JxlEncoderFrameSettingsValuesStruct { JxlFrameHeader header; std::vector<JxlBlendInfo> extra_channel_blend_info; std::string frame_name; + bool frame_index_box = false; } JxlEncoderFrameSettingsValues; typedef std::array<uint8_t, 4> BoxType; @@ -106,6 +185,7 @@ void AppendBoxHeader(const jxl::BoxType& type, size_t size, bool unbounded, // Internal use only struct, can only be initialized correctly by // JxlEncoderCreate. struct JxlEncoderStruct { + JxlEncoderError error = JxlEncoderError::JXL_ENC_ERR_OK; JxlMemoryManager memory_manager; jxl::MemoryManagerUniquePtr<jxl::ThreadPool> thread_pool{ nullptr, jxl::MemoryManagerDeleteHelper(&memory_manager)}; @@ -126,6 +206,7 @@ struct JxlEncoderStruct { // or accross multiple jxlp boxes. size_t codestream_bytes_written_beginning_of_frame; size_t codestream_bytes_written_end_of_frame; + jxl::JxlEncoderFrameIndexBox frame_index_box; // Force using the container even if not needed bool use_container; @@ -135,7 +216,7 @@ struct JxlEncoderStruct { // TODO(lode): move level into jxl::CompressParams since some C++ // implementation decisions should be based on it: level 10 allows more // features to be used. - uint32_t codestream_level; + int32_t codestream_level; bool store_jpeg_metadata; jxl::CodecMetadata metadata; std::vector<uint8_t> jpeg_metadata; @@ -145,6 +226,7 @@ struct JxlEncoderStruct { // reconstruction box. bool wrote_bytes; jxl::CompressParams last_used_cparams; + JxlBasicInfo basic_info; // Encoder wrote a jxlp (partial codestream) box, so any next codestream // parts must also be written in jxlp boxes, a single jxlc box cannot be diff --git a/media/libjxl/src/lib/jxl/encode_test.cc b/media/libjxl/src/lib/jxl/encode_test.cc index e569e212f5..4f1ef0b2b8 100644 --- a/media/libjxl/src/lib/jxl/encode_test.cc +++ b/media/libjxl/src/lib/jxl/encode_test.cc @@ -11,7 +11,7 @@ #include "jxl/decode_cxx.h" #include "jxl/encode_cxx.h" #include "lib/extras/codec.h" -#include "lib/jxl/dec_file.h" +#include "lib/extras/dec/jxl.h" #include "lib/jxl/enc_butteraugli_pnorm.h" #include "lib/jxl/encode_internal.h" #include "lib/jxl/jpeg/dec_jpeg_data.h" @@ -58,8 +58,7 @@ TEST(EncodeTest, AddJPEGAfterCloseTest) { JxlEncoderCloseInput(enc.get()); - const std::string jpeg_path = - "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg"; const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); JxlEncoderFrameSettings* frame_settings = @@ -110,7 +109,7 @@ TEST(EncodeTest, AddFrameBeforeBasicInfoTest) { JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/pixel_format.num_channels < 3); - EXPECT_EQ(JXL_ENC_SUCCESS, + EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); @@ -159,7 +158,9 @@ TEST(EncodeTest, DefaultParallelRunnerTest) { } void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc, - const JxlEncoderFrameSettings* frame_settings) { + const JxlEncoderFrameSettings* frame_settings, + size_t max_compressed_size, + bool lossy_use_original_profile) { JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); @@ -170,7 +171,7 @@ void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc, jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; - if (frame_settings->values.lossless) { + if (frame_settings->values.lossless || lossy_use_original_profile) { basic_info.uses_original_profile = true; } else { basic_info.uses_original_profile = false; @@ -179,9 +180,15 @@ void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc, EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc, 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info)); JxlColorEncoding color_encoding; - JxlColorEncodingSetToSRGB(&color_encoding, - /*is_gray=*/pixel_format.num_channels < 3); + JxlColorEncodingSetToSRGB(&color_encoding, true); + EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetColorEncoding(enc, &color_encoding)); + JxlColorEncodingSetToSRGB(&color_encoding, false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc, &color_encoding)); + pixel_format.num_channels = 1; + EXPECT_EQ(JXL_ENC_ERROR, + JxlEncoderAddImageFrame(frame_settings, &pixel_format, + pixels.data(), pixels.size())); + pixel_format.num_channels = 4; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); @@ -201,11 +208,11 @@ void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc, } } compressed.resize(next_out - compressed.data()); + EXPECT_LE(compressed.size(), max_compressed_size); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); - jxl::DecompressParams dparams; jxl::CodecInOut decoded_io; - EXPECT_TRUE(jxl::DecodeFile( - dparams, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), + EXPECT_TRUE(jxl::test::DecodeFile( + {}, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), &decoded_io, /*pool=*/nullptr)); EXPECT_LE( @@ -213,13 +220,14 @@ void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc, #if JXL_HIGH_PRECISION 1.8); #else - 4.8); + 8.0); #endif } void VerifyFrameEncoding(JxlEncoder* enc, const JxlEncoderFrameSettings* frame_settings) { - VerifyFrameEncoding(63, 129, enc, frame_settings); + VerifyFrameEncoding(63, 129, enc, frame_settings, 2600, + /*lossy_use_original_profile=*/false); } TEST(EncodeTest, FrameEncodingTest) { @@ -233,12 +241,14 @@ TEST(EncodeTest, EncoderResetTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); VerifyFrameEncoding(50, 200, enc.get(), - JxlEncoderFrameSettingsCreate(enc.get(), nullptr)); + JxlEncoderFrameSettingsCreate(enc.get(), nullptr), 4300, + false); // Encoder should become reusable for a new image from scratch after using // reset. JxlEncoderReset(enc.get()); VerifyFrameEncoding(157, 77, enc.get(), - JxlEncoderFrameSettingsCreate(enc.get(), nullptr)); + JxlEncoderFrameSettingsCreate(enc.get(), nullptr), 2300, + false); } TEST(EncodeTest, CmsTest) { @@ -311,7 +321,7 @@ TEST(EncodeTest, frame_settingsTest) { JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetFrameLossless(frame_settings, JXL_TRUE)); - VerifyFrameEncoding(enc.get(), frame_settings); + VerifyFrameEncoding(63, 129, enc.get(), frame_settings, 3600, false); EXPECT_EQ(true, enc->last_used_cparams.IsLossless()); } @@ -321,7 +331,7 @@ TEST(EncodeTest, frame_settingsTest) { JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetFrameDistance(frame_settings, 0.5)); - VerifyFrameEncoding(enc.get(), frame_settings); + VerifyFrameEncoding(63, 129, enc.get(), frame_settings, 3000, false); EXPECT_EQ(0.5, enc->last_used_cparams.butteraugli_distance); } @@ -393,11 +403,12 @@ TEST(EncodeTest, frame_settingsTest) { EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); - EXPECT_EQ(JXL_ENC_SUCCESS, - JxlEncoderFrameSettingsSetOption( - frame_settings, JXL_ENC_FRAME_SETTING_PHOTON_NOISE, 1777)); + EXPECT_EQ( + JXL_ENC_SUCCESS, + JxlEncoderFrameSettingsSetFloatOption( + frame_settings, JXL_ENC_FRAME_SETTING_PHOTON_NOISE, 1777.777)); VerifyFrameEncoding(enc.get(), frame_settings); - EXPECT_EQ(1777.0f, enc->last_used_cparams.photon_noise_iso); + EXPECT_NEAR(1777.777f, enc->last_used_cparams.photon_noise_iso, 1E-6); } { @@ -406,13 +417,13 @@ TEST(EncodeTest, frame_settingsTest) { JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, - JxlEncoderFrameSettingsSetOption( + JxlEncoderFrameSettingsSetFloatOption( frame_settings, - JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, 55)); + JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, 55.0f)); EXPECT_EQ(JXL_ENC_SUCCESS, - JxlEncoderFrameSettingsSetOption( + JxlEncoderFrameSettingsSetFloatOption( frame_settings, - JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, 25)); + JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, 25.0f)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_PALETTE_COLORS, 70000)); @@ -420,9 +431,10 @@ TEST(EncodeTest, frame_settingsTest) { JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_LOSSY_PALETTE, 1)); VerifyFrameEncoding(enc.get(), frame_settings); - EXPECT_EQ(55.0f, - enc->last_used_cparams.channel_colors_pre_transform_percent); - EXPECT_EQ(25.0f, enc->last_used_cparams.channel_colors_percent); + EXPECT_NEAR(55.0f, + enc->last_used_cparams.channel_colors_pre_transform_percent, + 1E-6); + EXPECT_NEAR(25.0f, enc->last_used_cparams.channel_colors_percent, 1E-6); EXPECT_EQ(70000, enc->last_used_cparams.palette_colors); EXPECT_EQ(true, enc->last_used_cparams.lossy_palette); } @@ -442,22 +454,20 @@ TEST(EncodeTest, frame_settingsTest) { EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, 14)); - EXPECT_EQ(JXL_ENC_SUCCESS, - JxlEncoderFrameSettingsSetOption( - frame_settings, - JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, 77)); + EXPECT_EQ( + JXL_ENC_SUCCESS, + JxlEncoderFrameSettingsSetFloatOption( + frame_settings, + JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, 77.0f)); EXPECT_EQ( JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, 7)); VerifyFrameEncoding(enc.get(), frame_settings); - // It was set to 30, but becomes 32 because in the C++ implementation, the - // numerical RCT values are shifted 2 compared to the specification. The - // API uses the values seen in the JXL specification. - EXPECT_EQ(32, enc->last_used_cparams.colorspace); + EXPECT_EQ(30, enc->last_used_cparams.colorspace); EXPECT_EQ(2, enc->last_used_cparams.modular_group_size_shift); EXPECT_EQ(jxl::Predictor::Best, enc->last_used_cparams.options.predictor); - EXPECT_EQ(0.77f, enc->last_used_cparams.options.nb_repeats); + EXPECT_NEAR(0.77f, enc->last_used_cparams.options.nb_repeats, 1E-6); EXPECT_EQ(7, enc->last_used_cparams.options.max_properties); } @@ -486,6 +496,36 @@ TEST(EncodeTest, frame_settingsTest) { } } +TEST(EncodeTest, LossyEncoderUseOriginalProfileTest) { + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + ASSERT_NE(nullptr, enc.get()); + JxlEncoderFrameSettings* frame_settings = + JxlEncoderFrameSettingsCreate(enc.get(), NULL); + VerifyFrameEncoding(63, 129, enc.get(), frame_settings, 4100, true); + } + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + ASSERT_NE(nullptr, enc.get()); + JxlEncoderFrameSettings* frame_settings = + JxlEncoderFrameSettingsCreate(enc.get(), NULL); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderFrameSettingsSetOption( + frame_settings, JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, 2)); + VerifyFrameEncoding(63, 129, enc.get(), frame_settings, 4500, true); + } + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + ASSERT_NE(nullptr, enc.get()); + JxlEncoderFrameSettings* frame_settings = + JxlEncoderFrameSettingsCreate(enc.get(), NULL); + ASSERT_EQ(JXL_ENC_SUCCESS, + JxlEncoderFrameSettingsSetOption( + frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, 8)); + VerifyFrameEncoding(63, 129, enc.get(), frame_settings, 3700, true); + } +} + namespace { // Returns a copy of buf from offset to offset+size, or a new zeroed vector if // the result would have been out of bounds taking integer overflow into @@ -759,6 +799,7 @@ TEST(EncodeTest, CodestreamLevelVerificationTest) { // Set an image dimension that is too large for level 5, but fits in level 10 basic_info.xsize = 1ull << 30ull; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 5)); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); @@ -771,8 +812,7 @@ TEST(EncodeTest, CodestreamLevelVerificationTest) { } TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) { - const std::string jpeg_path = - "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg"; const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); JxlEncoderPtr enc = JxlEncoderMake(nullptr); @@ -800,61 +840,11 @@ TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) { compressed.resize(next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); - Container container = {}; - jxl::Span<const uint8_t> encoded_span = - jxl::Span<const uint8_t>(compressed.data(), compressed.size()); - EXPECT_TRUE(container.Decode(&encoded_span)); - EXPECT_EQ(0u, encoded_span.size()); - bool found_jbrd = false; - bool found_jxlc = false; - bool found_jxlp = false; - size_t jbrd_index = 0; - std::vector<uint8_t> codestream_bytes; - // The encoder is allowed to either emit a jxlc or one or more jxlp. - for (size_t i = 0; i < container.boxes.size(); ++i) { - if (memcmp("jbrd", container.boxes[i].type, 4) == 0) { - EXPECT_EQ(false, found_jxlc); // Max 1 jbrd - found_jbrd = true; - jbrd_index = i; - } - if (memcmp("jxlc", container.boxes[i].type, 4) == 0) { - EXPECT_EQ(false, found_jxlc); // Max 1 jxlc - EXPECT_EQ(false, found_jxlp); // Can't mix jxlc and jxlp - found_jxlc = true; - codestream_bytes.insert( - codestream_bytes.end(), container.boxes[i].data.data(), - container.boxes[i].data.data() + container.boxes[i].data.size()); - } - if (memcmp("jxlp", container.boxes[i].type, 4) == 0) { - EXPECT_EQ(false, found_jxlc); // Can't mix jxlc and jxlp - found_jxlp = true; - // Append all data except the first 4 box content bytes which are the - // jxpl box counter. - codestream_bytes.insert( - codestream_bytes.end(), container.boxes[i].data.data() + 4, - container.boxes[i].data.data() + container.boxes[i].data.size()); - } - } - EXPECT_EQ(true, found_jbrd); - EXPECT_EQ(true, found_jxlc || found_jxlp); - - jxl::CodecInOut decoded_io; - decoded_io.Main().jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>(); - EXPECT_TRUE(jxl::jpeg::DecodeJPEGData(container.boxes[jbrd_index].data, - decoded_io.Main().jpeg_data.get())); - - jxl::DecompressParams dparams; - dparams.keep_dct = true; - jxl::Span<const uint8_t> codestream_span = jxl::Span<const uint8_t>( - codestream_bytes.data(), codestream_bytes.size()); - EXPECT_TRUE(jxl::DecodeFile(dparams, codestream_span, &decoded_io, nullptr)); - + jxl::extras::JXLDecompressParams dparams; std::vector<uint8_t> decoded_jpeg_bytes; - auto write = [&decoded_jpeg_bytes](const uint8_t* buf, size_t len) { - decoded_jpeg_bytes.insert(decoded_jpeg_bytes.end(), buf, buf + len); - return len; - }; - EXPECT_TRUE(jxl::jpeg::WriteJpeg(*decoded_io.Main().jpeg_data, write)); + jxl::extras::PackedPixelFile ppf; + EXPECT_TRUE(DecodeImageJXL(compressed.data(), compressed.size(), dparams, + nullptr, &ppf, &decoded_jpeg_bytes)); EXPECT_EQ(decoded_jpeg_bytes.size(), orig.size()); EXPECT_EQ(0, memcmp(decoded_jpeg_bytes.data(), orig.data(), orig.size())); @@ -1306,8 +1296,9 @@ TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGFrameTest)) { for (int skip_basic_info = 0; skip_basic_info < 2; skip_basic_info++) { for (int skip_color_encoding = 0; skip_color_encoding < 2; skip_color_encoding++) { - const std::string jpeg_path = - "third_party/imagecompression.info/flower_foveon_cropped.jpg"; + // cannot set color encoding if basic info is not set + if (skip_basic_info && !skip_color_encoding) continue; + const std::string jpeg_path = "jxl/flower/flower_cropped.jpg"; const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); jxl::CodecInOut orig_io; ASSERT_TRUE(SetFromBytes(jxl::Span<const uint8_t>(orig), &orig_io, @@ -1354,11 +1345,9 @@ TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGFrameTest)) { compressed.resize(next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); - jxl::DecompressParams dparams; jxl::CodecInOut decoded_io; - EXPECT_TRUE(jxl::DecodeFile( - dparams, - jxl::Span<const uint8_t>(compressed.data(), compressed.size()), + EXPECT_TRUE(jxl::test::DecodeFile( + {}, jxl::Span<const uint8_t>(compressed.data(), compressed.size()), &decoded_io, /*pool=*/nullptr)); EXPECT_LE( diff --git a/media/libjxl/src/lib/jxl/epf.cc b/media/libjxl/src/lib/jxl/epf.cc index 872720b9d0..7288ed9ca6 100644 --- a/media/libjxl/src/lib/jxl/epf.cc +++ b/media/libjxl/src/lib/jxl/epf.cc @@ -62,7 +62,7 @@ void ComputeSigma(const Rect& block_rect, PassesDecoderState* state) { const uint8_t* JXL_RESTRICT sharpness_row = block_rect.ConstRow(state->shared->epf_sharpness, by); AcStrategyRow acs_row = ac_strategy.ConstRow(block_rect, by); - const int* const JXL_RESTRICT row_quant = + const int32_t* const JXL_RESTRICT row_quant = block_rect.ConstRow(state->shared->raw_quant_field, by); for (size_t bx = 0; bx < block_rect.xsize(); bx++) { diff --git a/media/libjxl/src/lib/jxl/exif.cc b/media/libjxl/src/lib/jxl/exif.cc deleted file mode 100644 index ae27dd064b..0000000000 --- a/media/libjxl/src/lib/jxl/exif.cc +++ /dev/null @@ -1,89 +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 "lib/jxl/exif.h" - -namespace jxl { - -constexpr uint16_t kExifOrientationTag = 274; - -// Checks if a blob looks like Exif, and if so, sets bigendian -// according to the tiff endianness -bool IsExif(const std::vector<uint8_t>& exif, bool* bigendian) { - if (exif.size() < 12) return false; // not enough bytes for a valid exif blob - const uint8_t* t = exif.data(); - if (LoadLE32(t) == 0x2A004D4D) { - *bigendian = true; - return true; - } else if (LoadLE32(t) == 0x002A4949) { - *bigendian = false; - return true; - } - return false; // not a valid tiff header -} - -// Finds the position of an Exif tag, or 0 if it is not found -size_t FindExifTagPosition(const std::vector<uint8_t>& exif, uint16_t tagname) { - bool bigendian; - if (!IsExif(exif, &bigendian)) return 0; - const uint8_t* t = exif.data() + 4; - uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t)); - if (exif.size() < 12 + offset + 2 || offset < 8) return 0; - t += offset - 4; - uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 2; - while (nb_tags > 0) { - if (t + 12 >= exif.data() + exif.size()) return 0; - uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 2; - if (tag == tagname) return static_cast<size_t>(t - exif.data()); - t += 10; - nb_tags--; - } - return 0; -} - -// TODO (jon): tag 1 can be used to represent Adobe RGB 1998 if it has value -// "R03" -// TODO (jon): set intrinsic dimensions according to -// https://discourse.wicg.io/t/proposal-exif-image-resolution-auto-and-from-image/4326/24 -void InterpretExif(const std::vector<uint8_t>& exif, CodecMetadata* metadata) { - bool bigendian; - if (!IsExif(exif, &bigendian)) return; - size_t o_pos = FindExifTagPosition(exif, kExifOrientationTag); - if (o_pos) { - const uint8_t* t = exif.data() + o_pos; - uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 2; - uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t)); - t += 4; - uint16_t value = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 4; - if (type == 3 && count == 1 && value >= 1 && value <= 8) { - metadata->m.orientation = value; - } - } -} - -void ResetExifOrientation(std::vector<uint8_t>& exif) { - bool bigendian; - if (!IsExif(exif, &bigendian)) return; - size_t o_pos = FindExifTagPosition(exif, kExifOrientationTag); - if (o_pos) { - uint8_t* t = exif.data() + o_pos; - uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 2; - uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t)); - t += 4; - if (type == 3 && count == 1) { - if (bigendian) - StoreBE16(1, t); - else - StoreLE16(1, t); - } - } -} - -} // namespace jxl diff --git a/media/libjxl/src/lib/jxl/exif.h b/media/libjxl/src/lib/jxl/exif.h index 0520cbfd31..06652fc730 100644 --- a/media/libjxl/src/lib/jxl/exif.h +++ b/media/libjxl/src/lib/jxl/exif.h @@ -9,18 +9,76 @@ // Basic parsing of Exif (just enough for the render-impacting things // like orientation) +#include "jxl/codestream_header.h" #include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/image_metadata.h" namespace jxl { +constexpr uint16_t kExifOrientationTag = 274; + +// Checks if a blob looks like Exif, and if so, sets bigendian +// according to the tiff endianness +inline bool IsExif(const std::vector<uint8_t>& exif, bool* bigendian) { + if (exif.size() < 12) return false; // not enough bytes for a valid exif blob + const uint8_t* t = exif.data(); + if (LoadLE32(t) == 0x2A004D4D) { + *bigendian = true; + return true; + } else if (LoadLE32(t) == 0x002A4949) { + *bigendian = false; + return true; + } + return false; // not a valid tiff header +} + +// Finds the position of an Exif tag, or 0 if it is not found +inline size_t FindExifTagPosition(const std::vector<uint8_t>& exif, + uint16_t tagname) { + bool bigendian; + if (!IsExif(exif, &bigendian)) return 0; + const uint8_t* t = exif.data() + 4; + uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t)); + if (exif.size() < 12 + offset + 2 || offset < 8) return 0; + t += offset - 4; + uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + while (nb_tags > 0) { + if (t + 12 >= exif.data() + exif.size()) return 0; + uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + if (tag == tagname) return static_cast<size_t>(t - exif.data()); + t += 10; + nb_tags--; + } + return 0; +} + +// TODO (jon): tag 1 can be used to represent Adobe RGB 1998 if it has value +// "R03" +// TODO (jon): set intrinsic dimensions according to +// https://discourse.wicg.io/t/proposal-exif-image-resolution-auto-and-from-image/4326/24 // Parses the Exif data just enough to extract any render-impacting info. // If the Exif data is invalid or could not be parsed, then it is treated // as a no-op. -void InterpretExif(const std::vector<uint8_t>& exif, CodecMetadata* metadata); - -// Sets the Exif orientation to the identity, to avoid repeated orientation -void ResetExifOrientation(std::vector<uint8_t>& exif); +inline void InterpretExif(const std::vector<uint8_t>& exif, + JxlOrientation* orientation) { + bool bigendian; + if (!IsExif(exif, &bigendian)) return; + size_t o_pos = FindExifTagPosition(exif, kExifOrientationTag); + if (o_pos) { + const uint8_t* t = exif.data() + o_pos; + uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t)); + t += 4; + uint16_t value = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 4; + if (type == 3 && count == 1 && value >= 1 && value <= 8) { + *orientation = static_cast<JxlOrientation>(value); + } + } +} } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/fast_math-inl.h b/media/libjxl/src/lib/jxl/fast_math-inl.h index 60be66829a..5c48034290 100644 --- a/media/libjxl/src/lib/jxl/fast_math-inl.h +++ b/media/libjxl/src/lib/jxl/fast_math-inl.h @@ -21,9 +21,24 @@ namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Abs; +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Eq; +using hwy::HWY_NAMESPACE::Floor; +using hwy::HWY_NAMESPACE::Ge; +using hwy::HWY_NAMESPACE::GetLane; +using hwy::HWY_NAMESPACE::IfThenElse; +using hwy::HWY_NAMESPACE::IfThenZeroElse; +using hwy::HWY_NAMESPACE::Le; +using hwy::HWY_NAMESPACE::Min; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::NegMulAdd; using hwy::HWY_NAMESPACE::Rebind; using hwy::HWY_NAMESPACE::ShiftLeft; using hwy::HWY_NAMESPACE::ShiftRight; +using hwy::HWY_NAMESPACE::Sub; +using hwy::HWY_NAMESPACE::Xor; // Computes base-2 logarithm like std::log2. Undefined if negative / NaN. // L1 error ~3.9E-6 @@ -41,12 +56,13 @@ V FastLog2f(const DF df, V x) { const auto x_bits = BitCast(di, x); // Range reduction to [-1/3, 1/3] - 3 integer, 2 float ops - const auto exp_bits = x_bits - Set(di, 0x3f2aaaab); // = 2/3 + const auto exp_bits = Sub(x_bits, Set(di, 0x3f2aaaab)); // = 2/3 // Shifted exponent = log2; also used to clear mantissa. const auto exp_shifted = ShiftRight<23>(exp_bits); - const auto mantissa = BitCast(df, x_bits - ShiftLeft<23>(exp_shifted)); + const auto mantissa = BitCast(df, Sub(x_bits, ShiftLeft<23>(exp_shifted))); const auto exp_val = ConvertTo(df, exp_shifted); - return EvalRationalPolynomial(df, mantissa - Set(df, 1.0f), p, q) + exp_val; + return Add(EvalRationalPolynomial(df, Sub(mantissa, Set(df, 1.0f)), p, q), + exp_val); } // max relative error ~3e-7 @@ -54,22 +70,23 @@ template <class DF, class V> V FastPow2f(const DF df, V x) { const Rebind<int32_t, DF> di; auto floorx = Floor(x); - auto exp = BitCast(df, ShiftLeft<23>(ConvertTo(di, floorx) + Set(di, 127))); - auto frac = x - floorx; - auto num = frac + Set(df, 1.01749063e+01); + auto exp = + BitCast(df, ShiftLeft<23>(Add(ConvertTo(di, floorx), Set(di, 127)))); + auto frac = Sub(x, floorx); + auto num = Add(frac, Set(df, 1.01749063e+01)); num = MulAdd(num, frac, Set(df, 4.88687798e+01)); num = MulAdd(num, frac, Set(df, 9.85506591e+01)); - num = num * exp; + num = Mul(num, exp); auto den = MulAdd(frac, Set(df, 2.10242958e-01), Set(df, -2.22328856e-02)); den = MulAdd(den, frac, Set(df, -1.94414990e+01)); den = MulAdd(den, frac, Set(df, 9.85506633e+01)); - return num / den; + return Div(num, den); } // max relative error ~3e-5 template <class DF, class V> V FastPowf(const DF df, V base, V exponent) { - return FastPow2f(df, FastLog2f(df, base) * exponent); + return FastPow2f(df, Mul(FastLog2f(df, base), exponent)); } // Computes cosine like std::cos. @@ -79,18 +96,18 @@ V FastCosf(const DF df, V x) { // Step 1: range reduction to [0, 2pi) const auto pi2 = Set(df, kPi * 2.0f); const auto pi2_inv = Set(df, 0.5f / kPi); - const auto npi2 = Floor(x * pi2_inv) * pi2; - const auto xmodpi2 = x - npi2; + const auto npi2 = Mul(Floor(Mul(x, pi2_inv)), pi2); + const auto xmodpi2 = Sub(x, npi2); // Step 2: range reduction to [0, pi] - const auto x_pi = Min(xmodpi2, pi2 - xmodpi2); + const auto x_pi = Min(xmodpi2, Sub(pi2, xmodpi2)); // Step 3: range reduction to [0, pi/2] - const auto above_pihalf = x_pi >= Set(df, kPi / 2.0f); - const auto x_pihalf = IfThenElse(above_pihalf, Set(df, kPi) - x_pi, x_pi); + const auto above_pihalf = Ge(x_pi, Set(df, kPi / 2.0f)); + const auto x_pihalf = IfThenElse(above_pihalf, Sub(Set(df, kPi), x_pi), x_pi); // Step 4: Taylor-like approximation, scaled by 2**0.75 to make angle // duplication steps faster, on x/4. - const auto xs = x_pihalf * Set(df, 0.25f); - const auto x2 = xs * xs; - const auto x4 = x2 * x2; + const auto xs = Mul(x_pihalf, Set(df, 0.25f)); + const auto x2 = Mul(xs, xs); + const auto x4 = Mul(x2, x2); const auto cosx_prescaling = MulAdd(x4, Set(df, 0.06960438), MulAdd(x2, Set(df, -0.84087373), Set(df, 1.68179268))); @@ -101,7 +118,7 @@ V FastCosf(const DF df, V x) { // Step 6: change sign if needed. const Rebind<uint32_t, DF> du; auto signbit = ShiftLeft<31>(BitCast(du, VecFromMask(df, above_pihalf))); - return BitCast(df, signbit ^ BitCast(du, cosx_scale2)); + return BitCast(df, Xor(signbit, BitCast(du, cosx_scale2))); } // Computes the error function like std::erf. @@ -111,7 +128,7 @@ V FastErff(const DF df, V x) { // Formula from // https://en.wikipedia.org/wiki/Error_function#Numerical_approximations // but constants have been recomputed. - const auto xle0 = x <= Zero(df); + const auto xle0 = Le(x, Zero(df)); const auto absx = Abs(x); // Compute 1 - 1 / ((((x * a + b) * x + c) * x + d) * x + 1)**4 const auto denom1 = @@ -119,13 +136,13 @@ V FastErff(const DF df, V x) { const auto denom2 = MulAdd(denom1, absx, Set(df, 2.32120216e-01)); const auto denom3 = MulAdd(denom2, absx, Set(df, 2.77820801e-01)); const auto denom4 = MulAdd(denom3, absx, Set(df, 1.0f)); - const auto denom5 = denom4 * denom4; - const auto inv_denom5 = Set(df, 1.0f) / denom5; + const auto denom5 = Mul(denom4, denom4); + const auto inv_denom5 = Div(Set(df, 1.0f), denom5); const auto result = NegMulAdd(inv_denom5, inv_denom5, Set(df, 1.0f)); // Change sign if needed. const Rebind<uint32_t, DF> du; auto signbit = ShiftLeft<31>(BitCast(du, VecFromMask(df, xle0))); - return BitCast(df, signbit ^ BitCast(du, result)); + return BitCast(df, Xor(signbit, BitCast(du, result))); } inline float FastLog2f(float f) { @@ -153,6 +170,47 @@ inline float FastErff(float f) { return GetLane(FastErff(D, Set(D, f))); } +// Returns cbrt(x) + add with 6 ulp max error. +// Modified from vectormath_exp.h, Apache 2 license. +// https://www.agner.org/optimize/vectorclass.zip +template <class V> +V CubeRootAndAdd(const V x, const V add) { + const HWY_FULL(float) df; + const HWY_FULL(int32_t) di; + + const auto kExpBias = Set(di, 0x54800000); // cast(1.) + cast(1.) / 3 + const auto kExpMul = Set(di, 0x002AAAAA); // shifted 1/3 + const auto k1_3 = Set(df, 1.0f / 3); + const auto k4_3 = Set(df, 4.0f / 3); + + const auto xa = x; // assume inputs never negative + const auto xa_3 = Mul(k1_3, xa); + + // Multiply exponent by -1/3 + const auto m1 = BitCast(di, xa); + // Special case for 0. 0 is represented with an exponent of 0, so the + // "kExpBias - 1/3 * exp" below gives the wrong result. The IfThenZeroElse() + // sets those values as 0, which prevents having NaNs in the computations + // below. + // TODO(eustas): use fused op + const auto m2 = IfThenZeroElse( + Eq(m1, Zero(di)), Sub(kExpBias, Mul((ShiftRight<23>(m1)), kExpMul))); + auto r = BitCast(df, m2); + + // Newton-Raphson iterations + for (int i = 0; i < 3; i++) { + const auto r2 = Mul(r, r); + r = NegMulAdd(xa_3, Mul(r2, r2), Mul(k4_3, r)); + } + // Final iteration + auto r2 = Mul(r, r); + r = MulAdd(k1_3, NegMulAdd(xa, Mul(r2, r2), r), r); + r2 = Mul(r, r); + r = MulAdd(r2, x, add); + + return r; +} + // NOLINTNEXTLINE(google-readability-namespace-comments) } // namespace HWY_NAMESPACE } // namespace jxl @@ -161,6 +219,8 @@ HWY_AFTER_NAMESPACE(); #endif // LIB_JXL_FAST_MATH_INL_H_ #if HWY_ONCE +#ifndef FAST_MATH_ONCE +#define FAST_MATH_ONCE namespace jxl { inline float FastLog2f(float f) { return HWY_STATIC_DISPATCH(FastLog2f)(f); } @@ -172,4 +232,5 @@ inline float FastCosf(float f) { return HWY_STATIC_DISPATCH(FastCosf)(f); } inline float FastErff(float f) { return HWY_STATIC_DISPATCH(FastErff)(f); } } // namespace jxl +#endif // FAST_MATH_ONCE #endif // HWY_ONCE diff --git a/media/libjxl/src/lib/jxl/fast_math_test.cc b/media/libjxl/src/lib/jxl/fast_math_test.cc index e8958ab12b..897aadc120 100644 --- a/media/libjxl/src/lib/jxl/fast_math_test.cc +++ b/media/libjxl/src/lib/jxl/fast_math_test.cc @@ -13,7 +13,6 @@ #include "lib/jxl/dec_xyb-inl.h" #include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_xyb.h" -#include "lib/jxl/fast_math-inl.h" #include "lib/jxl/transfer_functions-inl.h" // Test utils @@ -51,7 +50,7 @@ HWY_NOINLINE void TestFastPow2() { const float actual = GetLane(actual_v); const float expected = std::pow(2, f); const float rel_err = std::abs(expected - actual) / expected; - EXPECT_LT(rel_err, 3.1E-7) << "f = " << f; + EXPECT_LT(rel_err, 3.1E-6) << "f = " << f; max_rel_err = std::max(max_rel_err, rel_err); } printf("max rel err %e\n", static_cast<double>(max_rel_err)); @@ -107,6 +106,22 @@ HWY_NOINLINE void TestFastErf() { printf("max abs err %e\n", static_cast<double>(max_abs_err)); } +HWY_NOINLINE void TestCubeRoot() { + const HWY_FULL(float) d; + for (uint64_t x5 = 0; x5 < 2000000; x5++) { + const float x = x5 * 1E-5f; + const float expected = cbrtf(x); + HWY_ALIGN float approx[MaxLanes(d)]; + Store(CubeRootAndAdd(Set(d, x), Zero(d)), d, approx); + + // All lanes are same + for (size_t i = 1; i < Lanes(d); ++i) { + EXPECT_NEAR(approx[0], approx[i], 5E-7f); + } + EXPECT_NEAR(approx[0], expected, 8E-7f); + } +} + HWY_NOINLINE void TestFastSRGB() { constexpr size_t kNumTrials = 1 << 23; Rng rng(1); @@ -261,6 +276,7 @@ HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastPow2); HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastPow); HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastCos); HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastErf); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestCubeRoot); HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastSRGB); HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastPQDFE); HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastPQEFD); diff --git a/media/libjxl/src/lib/jxl/fields_test.cc b/media/libjxl/src/lib/jxl/fields_test.cc index 50e3ac2212..c11b052301 100644 --- a/media/libjxl/src/lib/jxl/fields_test.cc +++ b/media/libjxl/src/lib/jxl/fields_test.cc @@ -228,7 +228,7 @@ TEST(FieldsTest, TestRoundtripSize) { // Ensure all values can be reached by the encoding. TEST(FieldsTest, TestCropRect) { CodecMetadata metadata; - for (int32_t i = -1000; i < 19000; ++i) { + for (int32_t i = -999; i < 19000; ++i) { FrameHeader f(&metadata); f.custom_size_or_origin = true; f.frame_origin.x0 = i; diff --git a/media/libjxl/src/lib/jxl/frame_header.cc b/media/libjxl/src/lib/jxl/frame_header.cc index b08c351e81..e69a12c51c 100644 --- a/media/libjxl/src/lib/jxl/frame_header.cc +++ b/media/libjxl/src/lib/jxl/frame_header.cc @@ -5,6 +5,8 @@ #include "lib/jxl/frame_header.h" +#include <sstream> + #include "lib/jxl/aux_out.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/base/status.h" @@ -120,10 +122,16 @@ Status Passes::VisitFields(Visitor* JXL_RESTRICT visitor) { for (uint32_t i = 0; i < num_downsample; ++i) { JXL_QUIET_RETURN_IF_ERROR( visitor->U32(Val(1), Val(2), Val(4), Val(8), 1, &downsample[i])); + if (i > 0 && downsample[i] >= downsample[i - 1]) { + return JXL_FAILURE("downsample sequence should be decreasing"); + } } for (uint32_t i = 0; i < num_downsample; ++i) { JXL_QUIET_RETURN_IF_ERROR( visitor->U32(Val(0), Val(1), Val(2), Bits(3), 0, &last_pass[i])); + if (i > 0 && last_pass[i] <= last_pass[i - 1]) { + return JXL_FAILURE("last_pass sequence should be increasing"); + } if (last_pass[i] >= num_passes) { return JXL_FAILURE("last_pass %u >= num_passes %u", last_pass[i], num_passes); @@ -133,6 +141,31 @@ Status Passes::VisitFields(Visitor* JXL_RESTRICT visitor) { return true; } + +std::string Passes::DebugString() const { + std::ostringstream os; + os << "p=" << num_passes; + if (num_downsample) { + os << ",ds="; + for (uint32_t i = 0; i < num_downsample; ++i) { + os << last_pass[i] << ":" << downsample[i]; + if (i + 1 < num_downsample) os << ";"; + } + } + bool have_shifts = false; + for (uint32_t i = 0; i < num_passes; ++i) { + if (shift[i]) have_shifts = true; + } + if (have_shifts) { + os << ",shifts="; + for (uint32_t i = 0; i < num_passes; ++i) { + os << shift[i]; + if (i + 1 < num_passes) os << ";"; + } + } + return os.str(); +} + FrameHeader::FrameHeader(const CodecMetadata* metadata) : animation_frame(metadata), nonserialized_metadata(metadata) { Bundle::Init(this); @@ -271,6 +304,11 @@ Status FrameHeader::VisitFields(Visitor* JXL_RESTRICT visitor) { // Frame size JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &frame_size.xsize)); JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &frame_size.ysize)); + if (custom_size_or_origin && + (frame_size.xsize == 0 || frame_size.ysize == 0)) { + return JXL_FAILURE( + "Invalid crop dimensions for frame: zero width or height"); + } int32_t image_xsize = default_xsize(); int32_t image_ysize = default_ysize(); if (frame_type == FrameType::kRegularFrame || @@ -368,4 +406,65 @@ Status FrameHeader::VisitFields(Visitor* JXL_RESTRICT visitor) { return visitor->EndExtensions(); } +std::string FrameHeader::DebugString() const { + std::ostringstream os; + os << (encoding == FrameEncoding::kVarDCT ? "VarDCT" : "Modular"); + os << ","; + os << (frame_type == FrameType::kRegularFrame ? "Regular" + : frame_type == FrameType::kDCFrame ? "DC" + : frame_type == FrameType::kReferenceOnly ? "Reference" + : "SkipProgressive"); + if (frame_type == FrameType::kDCFrame) { + os << "(lv" << dc_level << ")"; + } + + if (flags) { + os << ","; + uint32_t remaining = flags; + +#define TEST_FLAG(name) \ + if (flags & Flags::k##name) { \ + remaining &= ~Flags::k##name; \ + os << #name; \ + if (remaining) os << "|"; \ + } + TEST_FLAG(Noise); + TEST_FLAG(Patches); + TEST_FLAG(Splines); + TEST_FLAG(UseDcFrame); + TEST_FLAG(SkipAdaptiveDCSmoothing); +#undef TEST_FLAG + } + + os << ","; + os << (color_transform == ColorTransform::kXYB ? "XYB" + : color_transform == ColorTransform::kYCbCr ? "YCbCr" + : "None"); + + if (encoding == FrameEncoding::kModular) { + os << ",shift=" << group_size_shift; + } else if (color_transform == ColorTransform::kXYB) { + os << ",qm=" << x_qm_scale << ";" << b_qm_scale; + } + if (frame_type != FrameType::kReferenceOnly) { + os << "," << passes.DebugString(); + } + if (custom_size_or_origin) { + os << ",xs=" << frame_size.xsize; + os << ",ys=" << frame_size.ysize; + if (frame_type == FrameType::kRegularFrame || + frame_type == FrameType::kSkipProgressive) { + os << ",x0=" << frame_origin.x0; + os << ",y0=" << frame_origin.y0; + } + } + if (upsampling > 1) os << ",up=" << upsampling; + if (loop_filter.gab) os << ",Gaborish"; + if (loop_filter.epf_iters > 0) os << ",epf=" << loop_filter.epf_iters; + if (animation_frame.duration > 0) os << ",dur=" << animation_frame.duration; + if (save_as_reference > 0) os << ",ref=" << save_as_reference; + if (is_last) os << ",last"; + return os.str(); +} + } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/frame_header.h b/media/libjxl/src/lib/jxl/frame_header.h index e0f2150dea..7eb2f3578b 100644 --- a/media/libjxl/src/lib/jxl/frame_header.h +++ b/media/libjxl/src/lib/jxl/frame_header.h @@ -92,8 +92,8 @@ struct YCbCrChromaSubsampling : public Fields { uint8_t MaxHShift() const { return maxhs_; } uint8_t MaxVShift() const { return maxvs_; } - uint8_t RawHShift(size_t c) { return kHShift[channel_mode_[c]]; } - uint8_t RawVShift(size_t c) { return kVShift[channel_mode_[c]]; } + uint8_t RawHShift(size_t c) const { return kHShift[channel_mode_[c]]; } + uint8_t RawVShift(size_t c) const { return kVShift[channel_mode_[c]]; } // Uses JPEG channel order (Y, Cb, Cr). Status Set(const uint8_t* hsample, const uint8_t* vsample) { @@ -262,10 +262,10 @@ struct Passes : public Fields { void GetDownsamplingBracket(size_t pass, int& minShift, int& maxShift) const { maxShift = 2; - minShift = 0; + minShift = 3; for (size_t i = 0;; i++) { for (uint32_t j = 0; j < num_downsample; ++j) { - if (i <= last_pass[j]) { + if (i == last_pass[j]) { if (downsample[j] == 8) minShift = 3; if (downsample[j] == 4) minShift = 2; if (downsample[j] == 2) minShift = 1; @@ -275,10 +275,22 @@ struct Passes : public Fields { if (i == num_passes - 1) minShift = 0; if (i == pass) return; maxShift = minShift - 1; - minShift = 0; } } + uint32_t GetDownsamplingTargetForCompletedPasses(uint32_t num_p) const { + if (num_p >= num_passes) return 1; + uint32_t retval = 8; + for (uint32_t i = 0; i < num_downsample; ++i) { + if (num_p > last_pass[i]) { + retval = std::min(retval, downsample[i]); + } + } + return retval; + } + + std::string DebugString() const; + uint32_t num_passes; // <= kMaxNumPasses uint32_t num_downsample; // <= num_passes @@ -473,6 +485,8 @@ struct FrameHeader : public Fields { frame_type == FrameType::kSkipProgressive; } + std::string DebugString() const; + uint64_t extensions; }; diff --git a/media/libjxl/src/lib/jxl/gauss_blur.cc b/media/libjxl/src/lib/jxl/gauss_blur.cc index d93942b128..930ffb4a39 100644 --- a/media/libjxl/src/lib/jxl/gauss_blur.cc +++ b/media/libjxl/src/lib/jxl/gauss_blur.cc @@ -26,7 +26,12 @@ namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; using hwy::HWY_NAMESPACE::Broadcast; +using hwy::HWY_NAMESPACE::GetLane; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::NegMulSub; #if HWY_TARGET != HWY_SCALAR using hwy::HWY_NAMESPACE::ShiftLeftLanes; #endif @@ -72,9 +77,9 @@ void FastGaussian1D(const hwy::AlignedUniquePtr<RecursiveGaussian>& rg, const V sum = Set(d, left_val + right_val); // (Only processing a single lane here, no need to broadcast) - V out_1 = sum * mul_in_1; - V out_3 = sum * mul_in_3; - V out_5 = sum * mul_in_5; + V out_1 = Mul(sum, mul_in_1); + V out_3 = Mul(sum, mul_in_3); + V out_5 = Mul(sum, mul_in_5); out_1 = MulAdd(mul_prev2_1, prev2_1, out_1); out_3 = MulAdd(mul_prev2_3, prev2_3, out_3); @@ -91,7 +96,7 @@ void FastGaussian1D(const hwy::AlignedUniquePtr<RecursiveGaussian>& rg, prev_5 = out_5; if (n >= 0) { - out[n] = GetLane(out_1 + out_3 + out_5); + out[n] = GetLane(Add(out_1, Add(out_3, out_5))); } } @@ -108,7 +113,7 @@ void FastGaussian1D(const hwy::AlignedUniquePtr<RecursiveGaussian>& rg, // Unrolled, no bounds checking needed. for (; n < width - N + 1 - (JXL_GAUSS_MAX_LANES - 1); n += Lanes(d)) { - const V sum = LoadU(d, in + n - N - 1) + LoadU(d, in + n + N - 1); + const V sum = Add(LoadU(d, in + n - N - 1), LoadU(d, in + n + N - 1)); // To get a vector of output(s), we multiply broadcasted vectors (of each // input plus the two previous outputs) and add them all together. @@ -116,9 +121,9 @@ void FastGaussian1D(const hwy::AlignedUniquePtr<RecursiveGaussian>& rg, // horizontal adds or transposing 4x4 values because they run on a different // port, concurrently with the FMA. const V in0 = Broadcast<0>(sum); - V out_1 = in0 * mul_in_1; - V out_3 = in0 * mul_in_3; - V out_5 = in0 * mul_in_5; + V out_1 = Mul(in0, mul_in_1); + V out_3 = Mul(in0, mul_in_3); + V out_5 = Mul(in0, mul_in_5); #if HWY_TARGET != HWY_SCALAR && JXL_GAUSS_MAX_LANES >= 2 const V in1 = Broadcast<1>(sum); @@ -162,7 +167,7 @@ void FastGaussian1D(const hwy::AlignedUniquePtr<RecursiveGaussian>& rg, prev_5 = Broadcast<JXL_GAUSS_MAX_LANES - 1>(out_5); #endif - Store(out_1 + out_3 + out_5, d, out + n); + Store(Add(out_1, Add(out_3, out_5)), d, out + n); } // Remainder handling with bounds checks @@ -174,9 +179,9 @@ void FastGaussian1D(const hwy::AlignedUniquePtr<RecursiveGaussian>& rg, const V sum = Set(d, left_val + right_val); // (Only processing a single lane here, no need to broadcast) - V out_1 = sum * mul_in_1; - V out_3 = sum * mul_in_3; - V out_5 = sum * mul_in_5; + V out_1 = Mul(sum, mul_in_1); + V out_3 = Mul(sum, mul_in_3); + V out_5 = Mul(sum, mul_in_5); out_1 = MulAdd(mul_prev2_1, prev2_1, out_1); out_3 = MulAdd(mul_prev2_3, prev2_3, out_3); @@ -192,7 +197,7 @@ void FastGaussian1D(const hwy::AlignedUniquePtr<RecursiveGaussian>& rg, prev_3 = out_3; prev_5 = out_5; - out[n] = GetLane(out_1 + out_3 + out_5); + out[n] = GetLane(Add(out_1, Add(out_3, out_5))); } } @@ -235,7 +240,7 @@ class TwoInputs { Vec<HWY_FULL(float)> operator()(const size_t offset) const { const auto in1 = Load(HWY_FULL(float)(), pos1_ + offset); const auto in2 = Load(HWY_FULL(float)(), pos2_ + offset); - return in1 + in2; + return Add(in1, in2); } private: @@ -280,7 +285,7 @@ void VerticalBlock(const V& d1_1, const V& d1_3, const V& d1_5, const V& n2_1, Store(y1, d, y_1 + kLanes * n_0 + idx_vec * kVN); Store(y3, d, y_3 + kLanes * n_0 + idx_vec * kVN); Store(y5, d, y_5 + kLanes * n_0 + idx_vec * kVN); - output(y1 + y3 + y5, out_pos, idx_vec * kVN); + output(Add(y1, Add(y3, y5)), out_pos, idx_vec * kVN); } // NOTE: flushing cache line out_pos hurts performance - less so with // clflushopt than clflush but still a significant slowdown. @@ -377,10 +382,12 @@ void FastGaussianVertical(const hwy::AlignedUniquePtr<RecursiveGaussian>& rg, constexpr size_t kCacheLineLanes = 64 / sizeof(float); constexpr size_t kVN = MaxLanes(HWY_FULL(float)()); - constexpr size_t kCacheLineVectors = kCacheLineLanes / kVN; + constexpr size_t kCacheLineVectors = + (kVN < kCacheLineLanes) ? (kCacheLineLanes / kVN) : 4; + constexpr size_t kFastPace = kCacheLineVectors * kVN; size_t x = 0; - for (; x + kCacheLineLanes <= in.xsize(); x += kCacheLineLanes) { + for (; x + kFastPace <= in.xsize(); x += kFastPace) { VerticalStrip<kCacheLineVectors>(rg, in, x, out); } for (; x < in.xsize(); x += kVN) { diff --git a/media/libjxl/src/lib/jxl/gauss_blur_gbench.cc b/media/libjxl/src/lib/jxl/gauss_blur_gbench.cc index f6aba0c460..b1bb64abc5 100644 --- a/media/libjxl/src/lib/jxl/gauss_blur_gbench.cc +++ b/media/libjxl/src/lib/jxl/gauss_blur_gbench.cc @@ -13,7 +13,8 @@ namespace jxl { namespace { -ImageF Convolve(const ImageF& in, const std::vector<float>& kernel) { +JXL_MAYBE_UNUSED ImageF Convolve(const ImageF& in, + const std::vector<float>& kernel) { return ConvolveAndSample(in, kernel, 1); } @@ -35,9 +36,8 @@ void BM_GaussBlur1d(benchmark::State& state) { for (auto _ : state) { FastGaussian1D(rg, in.Row(0), length, out.Row(0)); // Prevent optimizing out - const float actual = out.ConstRow(0)[length / 2]; - const float rel_err = std::abs(actual - expected) / expected; - JXL_ASSERT(rel_err < 9E-5); + JXL_ASSERT(std::abs(out.ConstRow(0)[length / 2] - expected) / expected < + 9E-5); } state.SetItemsProcessed(length * state.iterations()); } @@ -59,9 +59,9 @@ void BM_GaussBlur2d(benchmark::State& state) { for (auto _ : state) { FastGaussian(rg, in, null_pool, &temp, &out); // Prevent optimizing out - const float actual = out.ConstRow(ysize / 2)[xsize / 2]; - const float rel_err = std::abs(actual - expected) / expected; - JXL_ASSERT(rel_err < 9E-5); + JXL_ASSERT(std::abs(out.ConstRow(ysize / 2)[xsize / 2] - expected) / + expected < + 9E-5); } state.SetItemsProcessed(xsize * ysize * state.iterations()); } @@ -81,11 +81,11 @@ void BM_GaussBlurFir(benchmark::State& state) { const std::vector<float> kernel = GaussianKernel(static_cast<int>(4 * sigma), static_cast<float>(sigma)); for (auto _ : state) { - const ImageF out = Convolve(in, kernel); // Prevent optimizing out - const float actual = out.ConstRow(ysize / 2)[xsize / 2]; - const float rel_err = std::abs(actual - expected) / expected; - JXL_ASSERT(rel_err < 9E-5); + JXL_ASSERT(std::abs(Convolve(in, kernel).ConstRow(ysize / 2)[xsize / 2] - + expected) / + expected < + 9E-5); } state.SetItemsProcessed(xsize * ysize * state.iterations()); } @@ -110,9 +110,9 @@ void BM_GaussBlurSep7(benchmark::State& state) { for (auto _ : state) { Separable7(in, Rect(in), weights, null_pool, &out); // Prevent optimizing out - const float actual = out.ConstRow(ysize / 2)[xsize / 2]; - const float rel_err = std::abs(actual - expected) / expected; - JXL_ASSERT(rel_err < 9E-5); + JXL_ASSERT(std::abs(out.ConstRow(ysize / 2)[xsize / 2] - expected) / + expected < + 9E-5); } state.SetItemsProcessed(xsize * ysize * state.iterations()); } diff --git a/media/libjxl/src/lib/jxl/gradient_test.cc b/media/libjxl/src/lib/jxl/gradient_test.cc index a64e36ee32..0351904d39 100644 --- a/media/libjxl/src/lib/jxl/gradient_test.cc +++ b/media/libjxl/src/lib/jxl/gradient_test.cc @@ -22,8 +22,6 @@ #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/color_management.h" #include "lib/jxl/common.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_file.h" @@ -31,6 +29,7 @@ #include "lib/jxl/image.h" #include "lib/jxl/image_bundle.h" #include "lib/jxl/image_ops.h" +#include "lib/jxl/test_utils.h" namespace jxl { namespace { @@ -151,8 +150,6 @@ void TestGradient(ThreadPool* pool, uint32_t color0, uint32_t color1, if (fast_mode) { cparams.speed_tier = SpeedTier::kSquirrel; } - DecompressParams dparams; - Image3F gradient = GenerateTestGradient(color0, color1, angle, xsize, ysize); CodecInOut io; @@ -167,7 +164,7 @@ void TestGradient(ThreadPool* pool, uint32_t color0, uint32_t color1, PassesEncoderState enc_state; EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, pool)); EXPECT_TRUE( io2.Main().TransformTo(io2.metadata.m.color_encoding, GetJxlCms(), pool)); diff --git a/media/libjxl/src/lib/jxl/headers.cc b/media/libjxl/src/lib/jxl/headers.cc index 7add681e1a..7c560e52a0 100644 --- a/media/libjxl/src/lib/jxl/headers.cc +++ b/media/libjxl/src/lib/jxl/headers.cc @@ -194,20 +194,6 @@ Status ReadSizeHeader(BitReader* JXL_RESTRICT reader, Status WriteSizeHeader(const SizeHeader& size, BitWriter* JXL_RESTRICT writer, size_t layer, AuxOut* aux_out) { - const size_t max_bits = Bundle::MaxBits(size); - if (max_bits != SizeHeader::kMaxBits) { - JXL_ABORT("Please update SizeHeader::kMaxBits from %" PRIuS " to %" PRIuS - "\n", - SizeHeader::kMaxBits, max_bits); - } - - // Only check the number of non-extension bits (extensions are unbounded). - // (Bundle::Write will call CanEncode again, but it is fast because SizeHeader - // is tiny.) - size_t extension_bits, total_bits; - JXL_RETURN_IF_ERROR(Bundle::CanEncode(size, &extension_bits, &total_bits)); - JXL_ASSERT(total_bits - extension_bits < SizeHeader::kMaxBits); - return Bundle::Write(size, writer, layer, aux_out); } diff --git a/media/libjxl/src/lib/jxl/headers.h b/media/libjxl/src/lib/jxl/headers.h index 56ed72e31f..a9be252c27 100644 --- a/media/libjxl/src/lib/jxl/headers.h +++ b/media/libjxl/src/lib/jxl/headers.h @@ -29,10 +29,6 @@ static constexpr uint8_t kCodestreamMarker = 0x0A; // can preallocate early. class SizeHeader : public Fields { public: - // All fields are valid after reading at most this many bits. WriteSizeHeader - // verifies this matches Bundle::MaxBits(SizeHeader). - static constexpr size_t kMaxBits = 78; - SizeHeader(); JXL_FIELDS_NAME(SizeHeader) diff --git a/media/libjxl/src/lib/jxl/image.h b/media/libjxl/src/lib/jxl/image.h index 5874e3057d..5fe2c558cf 100644 --- a/media/libjxl/src/lib/jxl/image.h +++ b/media/libjxl/src/lib/jxl/image.h @@ -14,6 +14,7 @@ #include <string.h> #include <algorithm> +#include <sstream> #include <utility> // std::move #include "lib/jxl/base/cache_aligned.h" @@ -242,8 +243,8 @@ class RectT { JXL_MUST_USE_RESULT RectT Intersection(const RectT& other) const { return RectT(std::max(x0_, other.x0_), std::max(y0_, other.y0_), xsize_, - ysize_, std::min(x0_ + xsize_, other.x0_ + other.xsize_), - std::min(y0_ + ysize_, other.y0_ + other.ysize_)); + ysize_, std::min(x1(), other.x1()), + std::min(y1(), other.y1())); } JXL_MUST_USE_RESULT RectT Translate(int64_t x_offset, @@ -282,8 +283,8 @@ class RectT { } bool IsInside(const RectT& other) const { - return x0_ >= other.x0() && x0_ + xsize_ <= other.x0() + other.xsize_ && - y0_ >= other.y0() && y0_ + ysize_ <= other.y0() + other.ysize(); + return x0_ >= other.x0() && x1() <= other.x1() && y0_ >= other.y0() && + y1() <= other.y1(); } // Returns true if this Rect fully resides in the given image. ImageT could be @@ -300,6 +301,32 @@ class RectT { T x1() const { return x0_ + xsize_; } T y1() const { return y0_ + ysize_; } + RectT<T> ShiftLeft(size_t shiftx, size_t shifty) const { + return RectT<T>(x0_ * (1 << shiftx), y0_ * (1 << shifty), xsize_ << shiftx, + ysize_ << shifty); + } + RectT<T> ShiftLeft(size_t shift) const { return ShiftLeft(shift, shift); } + + // Requires x0(), y0() to be multiples of 1<<shiftx, 1<<shifty. + RectT<T> CeilShiftRight(size_t shiftx, size_t shifty) const { + JXL_ASSERT(x0_ % (1 << shiftx) == 0); + JXL_ASSERT(y0_ % (1 << shifty) == 0); + return RectT<T>(x0_ / (1 << shiftx), y0_ / (1 << shifty), + DivCeil(xsize_, T{1} << shiftx), + DivCeil(ysize_, T{1} << shifty)); + } + RectT<T> CeilShiftRight(std::pair<size_t, size_t> shift) const { + return CeilShiftRight(shift.first, shift.second); + } + RectT<T> CeilShiftRight(size_t shift) const { + return CeilShiftRight(shift, shift); + } + + template <typename U> + RectT<U> As() const { + return RectT<U>(U(x0_), U(y0_), U(xsize_), U(ysize_)); + } + private: // Returns size_max, or whatever is left in [begin, end). static constexpr size_t ClampedSize(T begin, size_t size_max, T end) { @@ -315,6 +342,14 @@ class RectT { size_t ysize_; }; +template <typename T> +std::string Description(RectT<T> r) { + std::ostringstream os; + os << "[" << r.x0() << ".." << r.x1() << ")x" + << "[" << r.y0() << ".." << r.y1() << ")"; + return os.str(); +} + using Rect = RectT<size_t>; // Currently, we abuse Image to either refer to an image that owns its storage diff --git a/media/libjxl/src/lib/jxl/image_bundle.cc b/media/libjxl/src/lib/jxl/image_bundle.cc index 189b9768c3..dfbc02ddb2 100644 --- a/media/libjxl/src/lib/jxl/image_bundle.cc +++ b/media/libjxl/src/lib/jxl/image_bundle.cc @@ -8,7 +8,6 @@ #include <limits> #include <utility> -#include "lib/jxl/alpha.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/base/printf_macros.h" @@ -118,32 +117,6 @@ void ImageBundle::SetAlpha(ImageF&& alpha, bool alpha_is_premultiplied) { // num_extra_channels is automatically set in visitor VerifySizes(); } -void ImageBundle::PremultiplyAlpha() { - if (!HasAlpha()) return; - if (!HasColor()) return; - const ExtraChannelInfo* eci = metadata_->Find(ExtraChannel::kAlpha); - if (eci->alpha_associated) return; // already premultiplied - JXL_CHECK(color_.ysize() == alpha()->ysize()); - JXL_CHECK(color_.xsize() == alpha()->xsize()); - for (size_t y = 0; y < color_.ysize(); y++) { - ::jxl::PremultiplyAlpha(color_.PlaneRow(0, y), color_.PlaneRow(1, y), - color_.PlaneRow(2, y), alpha()->Row(y), - color_.xsize()); - } -} -void ImageBundle::UnpremultiplyAlpha() { - if (!HasAlpha()) return; - if (!HasColor()) return; - const ExtraChannelInfo* eci = metadata_->Find(ExtraChannel::kAlpha); - if (!eci->alpha_associated) return; // already unpremultiplied - JXL_CHECK(color_.ysize() == alpha()->ysize()); - JXL_CHECK(color_.xsize() == alpha()->xsize()); - for (size_t y = 0; y < color_.ysize(); y++) { - ::jxl::UnpremultiplyAlpha(color_.PlaneRow(0, y), color_.PlaneRow(1, y), - color_.PlaneRow(2, y), alpha()->Row(y), - color_.xsize()); - } -} void ImageBundle::SetExtraChannels(std::vector<ImageF>&& extra_channels) { for (const ImageF& plane : extra_channels) { diff --git a/media/libjxl/src/lib/jxl/image_bundle.h b/media/libjxl/src/lib/jxl/image_bundle.h index de5951ddc2..d233abbbc1 100644 --- a/media/libjxl/src/lib/jxl/image_bundle.h +++ b/media/libjxl/src/lib/jxl/image_bundle.h @@ -170,10 +170,6 @@ class ImageBundle { const ExtraChannelInfo* eci = metadata_->Find(ExtraChannel::kAlpha); return (eci == nullptr) ? false : eci->alpha_associated; } - // Premultiply alpha (if it isn't already premultiplied) - void PremultiplyAlpha(); - // Unpremultiply alpha (if it isn't already non-premultiplied) - void UnpremultiplyAlpha(); const ImageF& alpha() const; ImageF* alpha(); diff --git a/media/libjxl/src/lib/jxl/image_metadata.cc b/media/libjxl/src/lib/jxl/image_metadata.cc index 409ba732ac..7a1ee1c6b4 100644 --- a/media/libjxl/src/lib/jxl/image_metadata.cc +++ b/media/libjxl/src/lib/jxl/image_metadata.cc @@ -59,6 +59,14 @@ Status BitDepth::VisitFields(Visitor* JXL_RESTRICT visitor) { return true; } +std::string BitDepth::DebugString() const { + std::ostringstream os; + os << (floating_point_sample ? "F" : "U"); + os << bits_per_sample; + if (floating_point_sample) os << "." << exponent_bits_per_sample; + return os.str(); +} + CustomTransformData::CustomTransformData() { Bundle::Init(this); } Status CustomTransformData::VisitFields(Visitor* JXL_RESTRICT visitor) { if (visitor->AllDefault(*this, &all_default)) { @@ -244,6 +252,22 @@ Status ExtraChannelInfo::VisitFields(Visitor* JXL_RESTRICT visitor) { return true; } +std::string ExtraChannelInfo::DebugString() const { + std::ostringstream os; + os << (type == ExtraChannel::kAlpha ? "Alpha" + : type == ExtraChannel::kDepth ? "Depth" + : type == ExtraChannel::kSpotColor ? "Spot" + : type == ExtraChannel::kSelectionMask ? "Mask" + : type == ExtraChannel::kBlack ? "Black" + : type == ExtraChannel::kCFA ? "CFA" + : type == ExtraChannel::kThermal ? "Thermal" + : "Unknown"); + if (type == ExtraChannel::kAlpha && alpha_associated) os << "(premul)"; + os << " " << bit_depth.DebugString(); + os << " shift: " << dim_shift; + return os.str(); +} + ImageMetadata::ImageMetadata() { Bundle::Init(this); } Status ImageMetadata::VisitFields(Visitor* JXL_RESTRICT visitor) { if (visitor->AllDefault(*this, &all_default)) { @@ -418,4 +442,36 @@ void ImageMetadata::SetAlphaBits(uint32_t bits, bool alpha_is_premultiplied) { num_extra_channels = extra_channel_info.size(); if (bits > 12) modular_16_bit_buffer_sufficient = false; } + +std::string ImageMetadata::DebugString() const { + std::ostringstream os; + os << bit_depth.DebugString(); + if (modular_16_bit_buffer_sufficient) { + os << " (modular 16)"; + } + os << (xyb_encoded ? " xyb encoded" : " orig profile"); + os << " " << Description(color_encoding); + if (num_extra_channels > 0) { + os << " extra channels:"; + for (size_t i = 0; i < num_extra_channels; ++i) { + os << " (" << extra_channel_info[i].DebugString() << ")"; + if (i + 1 < num_extra_channels) os << ","; + } + } + if (have_preview) { + os << " preview: " << preview_size.xsize() << "x" << preview_size.ysize(); + } + if (orientation != 1) { + os << " orientation: " << orientation; + } + return os.str(); +} + +std::string CodecMetadata::DebugString() const { + std::ostringstream os; + os << size.xsize() << "x" << size.ysize(); + os << " " << m.DebugString(); + return os.str(); +} + } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/image_metadata.h b/media/libjxl/src/lib/jxl/image_metadata.h index ee6c05b425..9008e42f63 100644 --- a/media/libjxl/src/lib/jxl/image_metadata.h +++ b/media/libjxl/src/lib/jxl/image_metadata.h @@ -12,6 +12,7 @@ #include <stddef.h> #include <stdint.h> +#include <string> #include <vector> #include "jxl/codestream_header.h" @@ -79,6 +80,8 @@ struct BitDepth : public Fields { Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + std::string DebugString() const; + // Whether the original (uncompressed) samples are floating point or // unsigned integer. bool floating_point_sample; @@ -107,6 +110,8 @@ struct ExtraChannelInfo : public Fields { Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + std::string DebugString() const; + mutable bool all_default; ExtraChannel type; @@ -276,6 +281,8 @@ struct ImageMetadata : public Fields { bool ExtraFieldsDefault() const; + std::string DebugString() const; + mutable bool all_default; BitDepth bit_depth; @@ -407,6 +414,8 @@ struct CodecMetadata { return m.preview_size.ysize(); } } + + std::string DebugString() const; }; } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/jpeg/jpeg_data.cc b/media/libjxl/src/lib/jxl/jpeg/jpeg_data.cc index 50a184e407..a78d77c966 100644 --- a/media/libjxl/src/lib/jxl/jpeg/jpeg_data.cc +++ b/media/libjxl/src/lib/jxl/jpeg/jpeg_data.cc @@ -365,6 +365,11 @@ Status JPEGData::VisitFields(Visitor* visitor) { if (visitor->IsReading()) inter_marker_data_sizes.emplace_back(len); } uint32_t tail_data_len = tail_data.size(); + if (!visitor->IsReading() && tail_data_len > 4260096) { + error = JPEGReadError::TAIL_DATA_TOO_LARGE; + return JXL_FAILURE("Tail data too large (max size = 4260096, size = %u).", + tail_data_len); + } JXL_RETURN_IF_ERROR(visitor->U32(Val(0), BitsOffset(8, 1), BitsOffset(16, 257), BitsOffset(22, 65793), 0, &tail_data_len)); diff --git a/media/libjxl/src/lib/jxl/jpeg/jpeg_data.h b/media/libjxl/src/lib/jxl/jpeg/jpeg_data.h index 29aefbcb2c..8fbc8696ec 100644 --- a/media/libjxl/src/lib/jxl/jpeg/jpeg_data.h +++ b/media/libjxl/src/lib/jxl/jpeg/jpeg_data.h @@ -105,6 +105,7 @@ enum struct JPEGReadError { EOB_RUN_TOO_LONG, IMAGE_TOO_LARGE, INVALID_QUANT_TBL_PRECISION, + TAIL_DATA_TOO_LARGE }; // Quantization values for an 8x8 pixel block. diff --git a/media/libjxl/src/lib/jxl/jxl_test.cc b/media/libjxl/src/lib/jxl/jxl_test.cc index 997dea9643..63ce6125f6 100644 --- a/media/libjxl/src/lib/jxl/jxl_test.cc +++ b/media/libjxl/src/lib/jxl/jxl_test.cc @@ -3,11 +3,15 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include "lib/extras/dec/jxl.h" + #include <stdint.h> #include <stdio.h> #include <array> +#include <future> #include <string> +#include <tuple> #include <utility> #include <vector> @@ -24,8 +28,6 @@ #include "lib/jxl/codec_y4m_testonly.h" #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/color_management.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/enc_butteraugli_comparator.h" #include "lib/jxl/enc_butteraugli_pnorm.h" #include "lib/jxl/enc_cache.h" @@ -36,8 +38,10 @@ #include "lib/jxl/image_bundle.h" #include "lib/jxl/image_ops.h" #include "lib/jxl/image_test_utils.h" +#include "lib/jxl/jpeg/dec_jpeg_data.h" #include "lib/jxl/jpeg/dec_jpeg_data_writer.h" #include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "lib/jxl/jpeg/jpeg_data.h" #include "lib/jxl/modular/options.h" #include "lib/jxl/test_utils.h" #include "lib/jxl/testdata.h" @@ -63,14 +67,13 @@ TEST(JxlTest, HeaderSize) { CompressParams cparams; cparams.butteraugli_distance = 1.5; - DecompressParams dparams; ThreadPool* pool = nullptr; { CodecInOut io2; AuxOut aux_out; - Roundtrip(&io, cparams, dparams, pool, &io2, &aux_out); - EXPECT_LE(aux_out.layers[kLayerHeader].total_bits, 34u); + Roundtrip(&io, cparams, {}, pool, &io2, &aux_out); + EXPECT_LE(aux_out.layers[kLayerHeader].total_bits, 41u); } { @@ -80,8 +83,8 @@ TEST(JxlTest, HeaderSize) { alpha.Row(0)[0] = 1; io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); AuxOut aux_out; - Roundtrip(&io, cparams, dparams, pool, &io2, &aux_out); - EXPECT_LE(aux_out.layers[kLayerHeader].total_bits, 46u); + Roundtrip(&io, cparams, {}, pool, &io2, &aux_out); + EXPECT_LE(aux_out.layers[kLayerHeader].total_bits, 49u); } } @@ -91,10 +94,9 @@ TEST(JxlTest, RoundtripSinglePixel) { CompressParams cparams; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; ThreadPool* pool = nullptr; CodecInOut io2; - Roundtrip(&io, cparams, dparams, pool, &io2); + Roundtrip(&io, cparams, {}, pool, &io2); } // Changing serialized signature causes Decode to fail. @@ -105,7 +107,6 @@ TEST(JxlTest, RoundtripMarker) { CompressParams cparams; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; AuxOut* aux_out = nullptr; ThreadPool* pool = nullptr; @@ -116,7 +117,7 @@ TEST(JxlTest, RoundtripMarker) { aux_out, pool)); compressed[i] ^= 0xFF; CodecInOut io2; - EXPECT_FALSE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_FALSE(test::DecodeFile({}, compressed, &io2, pool)); } } #endif @@ -124,7 +125,7 @@ TEST(JxlTest, RoundtripMarker) { TEST(JxlTest, RoundtripTinyFast) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(32, 32); @@ -132,20 +133,18 @@ TEST(JxlTest, RoundtripTinyFast) { CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; cparams.butteraugli_distance = 4.0f; - DecompressParams dparams; CodecInOut io2; - const size_t enc_bytes = Roundtrip(&io, cparams, dparams, pool, &io2); + const size_t enc_bytes = Roundtrip(&io, cparams, {}, pool, &io2); printf("32x32 image size %" PRIuS " bytes\n", enc_bytes); } TEST(JxlTest, RoundtripSmallD1) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CompressParams cparams; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; CodecInOut io_out; size_t compressed_size; @@ -155,7 +154,7 @@ TEST(JxlTest, RoundtripSmallD1) { ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); - compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + compressed_size = Roundtrip(&io, cparams, {}, pool, &io_out); EXPECT_LE(compressed_size, 1000u); EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), @@ -169,12 +168,11 @@ TEST(JxlTest, RoundtripSmallD1) { ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_dim, pool)); io_dim.metadata.m.SetIntensityTarget(100); io_dim.ShrinkTo(io_dim.xsize() / 8, io_dim.ysize() / 8); - EXPECT_LT(Roundtrip(&io_dim, cparams, dparams, pool, &io_out), - compressed_size); + EXPECT_LT(Roundtrip(&io_dim, cparams, {}, pool, &io_out), compressed_size); EXPECT_THAT( ButteraugliDistance(io_dim, io_out, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(1.5)); + IsSlightlyBelow(1.1)); EXPECT_EQ(io_dim.metadata.m.IntensityTarget(), io_out.metadata.m.IntensityTarget()); } @@ -183,7 +181,7 @@ TEST(JxlTest, RoundtripSmallD1) { TEST(JxlTest, RoundtripOtherTransforms) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/64px/a2d1un_nkitzmiller_srgb8.png"); + ReadTestData("external/wesaturate/64px/a2d1un_nkitzmiller_srgb8.png"); std::unique_ptr<CodecInOut> io = jxl::make_unique<CodecInOut>(); ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), io.get(), pool)); @@ -192,11 +190,10 @@ TEST(JxlTest, RoundtripOtherTransforms) { cparams.speed_tier = SpeedTier::kKitten; cparams.color_transform = ColorTransform::kNone; cparams.butteraugli_distance = 5.0f; - DecompressParams dparams; std::unique_ptr<CodecInOut> io2 = jxl::make_unique<CodecInOut>(); const size_t compressed_size = - Roundtrip(io.get(), cparams, dparams, pool, io2.get()); + Roundtrip(io.get(), cparams, {}, pool, io2.get()); EXPECT_LE(compressed_size, 23000u); EXPECT_THAT(ButteraugliDistance(*io, *io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), @@ -205,7 +202,7 @@ TEST(JxlTest, RoundtripOtherTransforms) { // Check the consistency when performing another roundtrip. std::unique_ptr<CodecInOut> io3 = jxl::make_unique<CodecInOut>(); const size_t compressed_size2 = - Roundtrip(io.get(), cparams, dparams, pool, io3.get()); + Roundtrip(io.get(), cparams, {}, pool, io3.get()); EXPECT_LE(compressed_size2, 23000u); EXPECT_THAT(ButteraugliDistance(*io, *io3, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), @@ -215,37 +212,51 @@ TEST(JxlTest, RoundtripOtherTransforms) { TEST(JxlTest, RoundtripResample2) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize(), io.ysize()); CompressParams cparams; cparams.resampling = 2; cparams.speed_tier = SpeedTier::kFalcon; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 17000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 17000u); EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), IsSlightlyBelow(90)); } +TEST(JxlTest, RoundtripResample2Slow) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); + io.ShrinkTo(io.xsize(), io.ysize()); + CompressParams cparams; + cparams.resampling = 2; + cparams.butteraugli_distance = 10; + cparams.speed_tier = SpeedTier::kTortoise; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 5000u); + EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), + IsSlightlyBelow(250)); +} + TEST(JxlTest, RoundtripResample2MT) { ThreadPoolInternal pool(4); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); // image has to be large enough to have multiple groups after downsampling CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); CompressParams cparams; cparams.resampling = 2; cparams.speed_tier = SpeedTier::kFalcon; - DecompressParams dparams; CodecInOut io2; // TODO(veluca): Figure out why msan and release produce different // file size. - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 64500u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 200000u); EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), - IsSlightlyBelow(320)); + IsSlightlyBelow(340)); } // Roundtrip the image using a parallel runner that executes single-threaded but @@ -253,8 +264,7 @@ TEST(JxlTest, RoundtripResample2MT) { TEST(JxlTest, RoundtripOutOfOrderProcessing) { FakeParallelRunner fake_pool(/*order_seed=*/123, /*num_threads=*/8); ThreadPool pool(&JxlFakeParallelRunner, &fake_pool); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); // Image size is selected so that the block border needed is larger than the @@ -265,9 +275,8 @@ TEST(JxlTest, RoundtripOutOfOrderProcessing) { // Force epf so we end up needing a lot of border. cparams.epf = 3; - DecompressParams dparams; CodecInOut io2; - Roundtrip(&io, cparams, dparams, &pool, &io2); + Roundtrip(&io, cparams, {}, &pool, &io2); EXPECT_GE(1.5, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool)); @@ -276,8 +285,7 @@ TEST(JxlTest, RoundtripOutOfOrderProcessing) { TEST(JxlTest, RoundtripOutOfOrderProcessingBorder) { FakeParallelRunner fake_pool(/*order_seed=*/47, /*num_threads=*/8); ThreadPool pool(&JxlFakeParallelRunner, &fake_pool); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); // Image size is selected so that the block border needed is larger than the @@ -289,9 +297,8 @@ TEST(JxlTest, RoundtripOutOfOrderProcessingBorder) { cparams.epf = 3; cparams.resampling = 2; - DecompressParams dparams; CodecInOut io2; - Roundtrip(&io, cparams, dparams, &pool, &io2); + Roundtrip(&io, cparams, {}, &pool, &io2); EXPECT_GE(2.8, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool)); @@ -300,15 +307,14 @@ TEST(JxlTest, RoundtripOutOfOrderProcessingBorder) { TEST(JxlTest, RoundtripResample4) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize(), io.ysize()); CompressParams cparams; cparams.resampling = 4; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 6000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 6000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(22)); @@ -317,15 +323,14 @@ TEST(JxlTest, RoundtripResample4) { TEST(JxlTest, RoundtripResample8) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize(), io.ysize()); CompressParams cparams; cparams.resampling = 8; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2100u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 2100u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(50)); @@ -334,17 +339,16 @@ TEST(JxlTest, RoundtripResample8) { TEST(JxlTest, RoundtripUnalignedD2) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 12, io.ysize() / 7); CompressParams cparams; cparams.butteraugli_distance = 2.0; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 700u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 700u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(1.7)); @@ -354,26 +358,24 @@ TEST(JxlTest, RoundtripUnalignedD2) { TEST(JxlTest, RoundtripMultiGroupNL) { ThreadPoolInternal pool(4); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); io.ShrinkTo(600, 1024); // partial X, full Y group CompressParams cparams; - DecompressParams dparams; cparams.fast_mode = true; cparams.butteraugli_distance = 1.0f; CodecInOut io2; - Roundtrip(&io, cparams, dparams, &pool, &io2); + Roundtrip(&io, cparams, {}, &pool, &io2); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool), IsSlightlyBelow(0.9f)); cparams.butteraugli_distance = 2.0f; CodecInOut io3; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 80000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io3), 80000u); EXPECT_THAT(ButteraugliDistance(io, io3, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool), IsSlightlyBelow(1.5f)); @@ -382,50 +384,91 @@ TEST(JxlTest, RoundtripMultiGroupNL) { #endif TEST(JxlTest, RoundtripMultiGroup) { + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); + CodecInOut io; + { + ThreadPoolInternal pool(4); + ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); + } + io.ShrinkTo(600, 1024); + + auto test = [&](jxl::SpeedTier speed_tier, float target_distance, + size_t expected_size, float expected_distance) { + ThreadPoolInternal pool(4); + CompressParams cparams; + cparams.butteraugli_distance = target_distance; + cparams.speed_tier = speed_tier; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), expected_size); + EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), + IsSlightlyBelow(expected_distance)); + }; + + auto run_kitten = std::async(std::launch::async, test, SpeedTier::kKitten, + 1.0f, 55000u, 11); + auto run_wombat = std::async(std::launch::async, test, SpeedTier::kWombat, + 2.0f, 34000u, 18); +} + +TEST(JxlTest, RoundtripRGBToGrayscale) { ThreadPoolInternal pool(4); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); io.ShrinkTo(600, 1024); CompressParams cparams; - DecompressParams dparams; - cparams.butteraugli_distance = 1.0f; - cparams.speed_tier = SpeedTier::kKitten; + cparams.speed_tier = SpeedTier::kFalcon; + + extras::JXLDecompressParams dparams; + dparams.color_space = "Gra_D65_Rel_SRG"; + CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u); - EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), - IsSlightlyBelow(15)); + EXPECT_FALSE(io.Main().IsGray()); + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 55000u); + EXPECT_TRUE(io2.Main().IsGray()); - cparams.butteraugli_distance = 2.0f; - cparams.speed_tier = SpeedTier::kWombat; - CodecInOut io3; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 22100u); - EXPECT_THAT(ComputeDistance2(io.Main(), io3.Main(), GetJxlCms()), - IsSlightlyBelow(24)); + // Convert original to grayscale here, because TransformTo refuses to + // convert between grayscale and RGB. + ColorEncoding srgb_lin = ColorEncoding::LinearSRGB(/*is_gray=*/false); + ASSERT_TRUE(io.TransformTo(srgb_lin, GetJxlCms(), &pool)); + Image3F* color = io.Main().color(); + for (size_t y = 0; y < color->ysize(); ++y) { + float* row_r = color->PlaneRow(0, y); + float* row_g = color->PlaneRow(1, y); + float* row_b = color->PlaneRow(2, y); + for (size_t x = 0; x < color->xsize(); ++x) { + float luma = 0.2126 * row_r[x] + 0.7152 * row_g[x] + 0.0722 * row_b[x]; + row_r[x] = row_g[x] = row_b[x] = luma; + } + } + ColorEncoding srgb_gamma = ColorEncoding::SRGB(/*is_gray=*/false); + ASSERT_TRUE(io.TransformTo(srgb_gamma, GetJxlCms(), &pool)); + io.metadata.m.color_encoding = io2.Main().c_current(); + io.Main().OverrideProfile(io2.Main().c_current()); + EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), + /*distmap=*/nullptr, &pool), + IsSlightlyBelow(1.7)); } TEST(JxlTest, RoundtripLargeFast) { ThreadPoolInternal pool(8); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 265000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 450800u); } TEST(JxlTest, RoundtripDotsForceEpf) { ThreadPoolInternal pool(8); const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); + ReadTestData("external/wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); @@ -433,10 +476,9 @@ TEST(JxlTest, RoundtripDotsForceEpf) { cparams.epf = 2; cparams.dots = Override::kOn; cparams.speed_tier = SpeedTier::kSquirrel; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 265000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 450000u); } // Checks for differing size/distance in two consecutive runs of distance 2, @@ -444,25 +486,23 @@ TEST(JxlTest, RoundtripDotsForceEpf) { // Failing this may be a sign of race conditions or invalid memory accesses. TEST(JxlTest, RoundtripD2Consistent) { ThreadPoolInternal pool(8); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; cparams.butteraugli_distance = 2.0; - DecompressParams dparams; // Try each xsize mod kBlockDim to verify right border handling. for (size_t xsize = 48; xsize > 40; --xsize) { io.ShrinkTo(xsize, 15); CodecInOut io2; - const size_t size2 = Roundtrip(&io, cparams, dparams, &pool, &io2); + const size_t size2 = Roundtrip(&io, cparams, {}, &pool, &io2); CodecInOut io3; - const size_t size3 = Roundtrip(&io, cparams, dparams, &pool, &io3); + const size_t size3 = Roundtrip(&io, cparams, {}, &pool, &io3); // Exact same compressed size. EXPECT_EQ(size2, size3); @@ -476,31 +516,37 @@ TEST(JxlTest, RoundtripD2Consistent) { // Same as above, but for full image, testing multiple groups. TEST(JxlTest, RoundtripLargeConsistent) { - ThreadPoolInternal pool(8); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; - ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); + { + ThreadPoolInternal pool(8); + ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); + } CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; cparams.butteraugli_distance = 2.0; - DecompressParams dparams; + + auto roundtrip_and_compare = [&]() { + ThreadPoolInternal pool(8); + CodecInOut io2; + size_t size = Roundtrip(&io, cparams, {}, &pool, &io2); + double dist = ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()); + return std::tuple<size_t, double>(size, dist); + }; // Try each xsize mod kBlockDim to verify right border handling. - CodecInOut io2; - const size_t size2 = Roundtrip(&io, cparams, dparams, &pool, &io2); + auto future2 = std::async(std::launch::async, roundtrip_and_compare); + auto future3 = std::async(std::launch::async, roundtrip_and_compare); - CodecInOut io3; - const size_t size3 = Roundtrip(&io, cparams, dparams, &pool, &io3); + const auto result2 = future2.get(); + const auto result3 = future3.get(); // Exact same compressed size. - EXPECT_EQ(size2, size3); + EXPECT_EQ(std::get<0>(result2), std::get<0>(result3)); // Exact same distance. - const float dist2 = ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()); - const float dist3 = ComputeDistance2(io.Main(), io3.Main(), GetJxlCms()); - EXPECT_EQ(dist2, dist3); + EXPECT_EQ(std::get<1>(result2), std::get<1>(result3)); } #if JXL_TEST_NL @@ -508,17 +554,16 @@ TEST(JxlTest, RoundtripLargeConsistent) { TEST(JxlTest, RoundtripSmallNL) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); CompressParams cparams; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 1500u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 1500u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(1.7)); @@ -529,7 +574,7 @@ TEST(JxlTest, RoundtripSmallNL) { TEST(JxlTest, RoundtripNoGaborishNoAR) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); @@ -537,10 +582,9 @@ TEST(JxlTest, RoundtripNoGaborishNoAR) { cparams.gaborish = Override::kOff; cparams.epf = 0; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 40000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 40000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(2.0)); @@ -549,7 +593,7 @@ TEST(JxlTest, RoundtripNoGaborishNoAR) { TEST(JxlTest, RoundtripSmallNoGaborish) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); @@ -557,10 +601,9 @@ TEST(JxlTest, RoundtripSmallNoGaborish) { CompressParams cparams; cparams.gaborish = Override::kOff; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 900u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 900u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(1.2)); @@ -589,10 +632,9 @@ TEST(JxlTest, RoundtripSmallPatchesAlpha) { CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; cparams.butteraugli_distance = 0.1f; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 2000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(0.04f)); @@ -617,10 +659,9 @@ TEST(JxlTest, RoundtripSmallPatches) { CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; cparams.butteraugli_distance = 0.1f; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 2000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(0.04f)); @@ -638,7 +679,6 @@ TEST(JxlTest, RoundtripImageBundleOriginalBits) { io.SetFromImage(std::move(image), ColorEncoding::LinearSRGB()); CompressParams cparams; - DecompressParams dparams; // Test unsigned integers from 1 to 32 bits for (uint32_t bit_depth = 1; bit_depth <= 32; bit_depth++) { @@ -651,7 +691,7 @@ TEST(JxlTest, RoundtripImageBundleOriginalBits) { io.metadata.m.SetUintSamples(bit_depth); CodecInOut io2; - Roundtrip(&io, cparams, dparams, pool, &io2); + Roundtrip(&io, cparams, {}, pool, &io2); EXPECT_EQ(bit_depth, io2.metadata.m.bit_depth.bits_per_sample); EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample); @@ -688,7 +728,7 @@ TEST(JxlTest, RoundtripImageBundleOriginalBits) { io.metadata.m.bit_depth.exponent_bits_per_sample = exponent_bit_depth; CodecInOut io2; - Roundtrip(&io, cparams, dparams, pool, &io2); + Roundtrip(&io, cparams, {}, pool, &io2); EXPECT_EQ(bit_depth, io2.metadata.m.bit_depth.bits_per_sample); EXPECT_TRUE(io2.metadata.m.bit_depth.floating_point_sample); @@ -701,7 +741,7 @@ TEST(JxlTest, RoundtripImageBundleOriginalBits) { TEST(JxlTest, RoundtripGrayscale) { ThreadPool* pool = nullptr; const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); + "external/wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); ASSERT_NE(io.xsize(), 0u); @@ -718,13 +758,12 @@ TEST(JxlTest, RoundtripGrayscale) { { CompressParams cparams; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; PaddedBytes compressed; EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, pool)); EXPECT_TRUE(io2.Main().IsGray()); EXPECT_LE(compressed.size(), 7000u); @@ -738,13 +777,12 @@ TEST(JxlTest, RoundtripGrayscale) { { CompressParams cparams; cparams.butteraugli_distance = 8.0; - DecompressParams dparams; PaddedBytes compressed; EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, pool)); EXPECT_TRUE(io2.Main().IsGray()); EXPECT_LE(compressed.size(), 1300u); @@ -752,12 +790,32 @@ TEST(JxlTest, RoundtripGrayscale) { /*distmap=*/nullptr, pool), IsSlightlyBelow(6.0)); } + + { + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + + PaddedBytes compressed; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), + aux_out, pool)); + + CodecInOut io2; + extras::JXLDecompressParams dparams; + dparams.color_space = "RGB_D65_SRG_Rel_SRG"; + EXPECT_TRUE(test::DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_FALSE(io2.Main().IsGray()); + + EXPECT_LE(compressed.size(), 7000u); + EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), + /*distmap=*/nullptr, pool), + IsSlightlyBelow(1.6)); + } } TEST(JxlTest, RoundtripAlpha) { ThreadPool* pool = nullptr; - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); @@ -768,7 +826,6 @@ TEST(JxlTest, RoundtripAlpha) { CompressParams cparams; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); @@ -779,20 +836,28 @@ TEST(JxlTest, RoundtripAlpha) { PaddedBytes compressed; EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); - CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); - EXPECT_LE(compressed.size(), 10077u); + for (bool use_image_callback : {false, true}) { + for (bool unpremul_alpha : {false, true}) { + CodecInOut io2; + extras::JXLDecompressParams dparams; + dparams.use_image_callback = use_image_callback; + dparams.unpremultiply_alpha = unpremul_alpha; + EXPECT_TRUE(test::DecodeFile(dparams, compressed, &io2, pool)); - EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), - /*distmap=*/nullptr, pool), - IsSlightlyBelow(1.2)); + EXPECT_LE(compressed.size(), 10077u); + + EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), + /*distmap=*/nullptr, pool), + IsSlightlyBelow(1.2)); + } + } } TEST(JxlTest, RoundtripAlphaPremultiplied) { ThreadPool* pool = nullptr; - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png"); CodecInOut io, io_nopremul; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_nopremul, pool)); @@ -805,34 +870,61 @@ TEST(JxlTest, RoundtripAlphaPremultiplied) { CompressParams cparams; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; - io.PremultiplyAlpha(); + EXPECT_FALSE(io.Main().AlphaIsPremultiplied()); + EXPECT_TRUE(io.PremultiplyAlpha()); EXPECT_TRUE(io.Main().AlphaIsPremultiplied()); + + EXPECT_FALSE(io_nopremul.Main().AlphaIsPremultiplied()); + PassesEncoderState enc_state; AuxOut* aux_out = nullptr; PaddedBytes compressed; EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); - CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); - EXPECT_LE(compressed.size(), 10000u); - - EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), + for (bool use_image_callback : {false, true}) { + for (bool unpremul_alpha : {false, true}) { + for (bool use_uint8 : {false, true}) { + printf( + "Testing premultiplied alpha using %s %s requesting " + "%spremultiplied output.\n", + use_uint8 ? "uint8" : "float", + use_image_callback ? "image callback" : "image_buffer", + unpremul_alpha ? "un" : ""); + CodecInOut io2; + extras::JXLDecompressParams dparams; + dparams.use_image_callback = use_image_callback; + dparams.unpremultiply_alpha = unpremul_alpha; + if (use_uint8) { + dparams.accepted_formats = { + {4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}}; + } + EXPECT_TRUE(test::DecodeFile(dparams, compressed, &io2, pool)); + + EXPECT_LE(compressed.size(), 10000u); + EXPECT_EQ(unpremul_alpha, !io2.Main().AlphaIsPremultiplied()); + if (!unpremul_alpha) { + EXPECT_THAT( + ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(1.2)); - io2.Main().UnpremultiplyAlpha(); - EXPECT_THAT( - ButteraugliDistance(io_nopremul, io2, cparams.ba_params, GetJxlCms(), - /*distmap=*/nullptr, pool), - IsSlightlyBelow(1.35)); + IsSlightlyBelow(1.25)); + EXPECT_TRUE(io2.UnpremultiplyAlpha()); + EXPECT_FALSE(io2.Main().AlphaIsPremultiplied()); + } + EXPECT_THAT(ButteraugliDistance(io_nopremul, io2, cparams.ba_params, + GetJxlCms(), + /*distmap=*/nullptr, pool), + IsSlightlyBelow(1.35)); + } + } + } } TEST(JxlTest, RoundtripAlphaResampling) { ThreadPool* pool = nullptr; - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); @@ -845,7 +937,6 @@ TEST(JxlTest, RoundtripAlphaResampling) { cparams.ec_resampling = 2; cparams.butteraugli_distance = 1.0; cparams.speed_tier = SpeedTier::kHare; - DecompressParams dparams; PassesEncoderState enc_state; AuxOut* aux_out = nullptr; @@ -853,7 +944,7 @@ TEST(JxlTest, RoundtripAlphaResampling) { EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, pool)); EXPECT_LE(compressed.size(), 15000u); @@ -864,8 +955,8 @@ TEST(JxlTest, RoundtripAlphaResampling) { TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) { ThreadPool* pool = nullptr; - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); @@ -877,7 +968,6 @@ TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) { cparams.ec_resampling = 2; cparams.butteraugli_distance = 1.0; cparams.speed_tier = SpeedTier::kFalcon; - DecompressParams dparams; PassesEncoderState enc_state; AuxOut* aux_out = nullptr; @@ -885,7 +975,7 @@ TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) { EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, pool)); EXPECT_LE(compressed.size(), 34200u); @@ -896,8 +986,8 @@ TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) { TEST(JxlTest, RoundtripAlphaNonMultipleOf8) { ThreadPool* pool = nullptr; - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); @@ -908,7 +998,6 @@ TEST(JxlTest, RoundtripAlphaNonMultipleOf8) { CompressParams cparams; cparams.butteraugli_distance = 1.0; - DecompressParams dparams; EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); @@ -920,7 +1009,7 @@ TEST(JxlTest, RoundtripAlphaNonMultipleOf8) { EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, pool)); EXPECT_LE(compressed.size(), 180u); @@ -966,7 +1055,6 @@ TEST(JxlTest, RoundtripAlpha16) { CompressParams cparams; cparams.butteraugli_distance = 0.5; cparams.speed_tier = SpeedTier::kWombat; - DecompressParams dparams; io.metadata.m.SetUintSamples(16); EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB()); @@ -976,7 +1064,7 @@ TEST(JxlTest, RoundtripAlpha16) { EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, &pool)); CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, &pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, &pool)); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool), IsSlightlyBelow(0.8)); @@ -995,16 +1083,15 @@ CompressParams CParamsForLossless() { TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); CompressParams cparams = CParamsForLossless(); - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 3500000u); // If this test fails with a very close to 0.0 but not exactly 0.0 butteraugli // distance, then there is likely a floating point issue, that could be // happening either in io or io2. The values of io are generated by @@ -1023,25 +1110,24 @@ TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8)) { TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathWP)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); CompressParams cparams = CParamsForLossless(); cparams.speed_tier = SpeedTier::kFalcon; cparams.options.skip_encoder_fast_path = true; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 3500000u); EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0); } TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathGradient)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); @@ -1049,17 +1135,16 @@ TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathGradient)) { cparams.speed_tier = SpeedTier::kThunder; cparams.options.skip_encoder_fast_path = true; cparams.options.predictor = {Predictor::Gradient}; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 3500000u); EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0); } TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderVeryFastPathGradient)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); @@ -1067,37 +1152,35 @@ TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderVeryFastPathGradient)) { cparams.speed_tier = SpeedTier::kLightning; cparams.options.skip_encoder_fast_path = true; cparams.options.predictor = {Predictor::Gradient}; - DecompressParams dparams; CodecInOut io2, io3; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 3500000u); EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0); cparams.options.skip_encoder_fast_path = false; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 3500000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io3), 3500000u); EXPECT_EQ(ComputeDistance2(io.Main(), io3.Main(), GetJxlCms()), 0.0); } TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8Falcon)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); CompressParams cparams = CParamsForLossless(); cparams.speed_tier = SpeedTier::kFalcon; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 3500000u); EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool)); } TEST(JxlTest, RoundtripLossless8Alpha) { ThreadPool* pool = nullptr; - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); EXPECT_EQ(8u, io.metadata.m.GetAlphaBits()); @@ -1106,10 +1189,9 @@ TEST(JxlTest, RoundtripLossless8Alpha) { EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); CompressParams cparams = CParamsForLossless(); - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 350000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 350000u); // If fails, see note about floating point in RoundtripLossless8. EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0); EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha())); @@ -1149,10 +1231,9 @@ TEST(JxlTest, RoundtripLossless16Alpha) { EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); CompressParams cparams = CParamsForLossless(); - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 7100u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 7100u); // If this test fails with a very close to 0.0 but not exactly 0.0 butteraugli // distance, then there is likely a floating point issue, that could be // happening either in io or io2. The values of io are generated by @@ -1205,10 +1286,9 @@ TEST(JxlTest, RoundtripLossless16AlphaNotMisdetectedAs8Bit) { EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); CompressParams cparams = CParamsForLossless(); - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 3100u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 3100u); EXPECT_EQ(16u, io2.metadata.m.GetAlphaBits()); EXPECT_EQ(16u, io2.metadata.m.bit_depth.bits_per_sample); EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample); @@ -1220,18 +1300,15 @@ TEST(JxlTest, RoundtripLossless16AlphaNotMisdetectedAs8Bit) { TEST(JxlTest, RoundtripYCbCr420) { ThreadPool* pool = nullptr; - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); - const PaddedBytes yuv420 = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.ffmpeg.y4m"); + const PaddedBytes yuv420 = ReadTestData("jxl/flower/flower.png.ffmpeg.y4m"); CodecInOut io2; ASSERT_TRUE(test::DecodeImageY4M(Span<const uint8_t>(yuv420), &io2)); CompressParams cparams = CParamsForLossless(); cparams.speed_tier = SpeedTier::kThunder; - DecompressParams dparams; PassesEncoderState enc_state; AuxOut* aux_out = nullptr; @@ -1239,19 +1316,19 @@ TEST(JxlTest, RoundtripYCbCr420) { EXPECT_TRUE(EncodeFile(cparams, &io2, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io3; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io3, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io3, pool)); - EXPECT_LE(compressed.size(), 1325000u); + EXPECT_LE(compressed.size(), 2000000u); // we're comparing an original PNG with a YCbCr 4:2:0 version EXPECT_THAT(ComputeDistance2(io.Main(), io3.Main(), GetJxlCms()), - IsSlightlyBelow(8.5)); + IsSlightlyBelow(4.3)); } TEST(JxlTest, RoundtripDots) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); + ReadTestData("external/wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); @@ -1261,7 +1338,6 @@ TEST(JxlTest, RoundtripDots) { cparams.dots = Override::kOn; cparams.butteraugli_distance = 0.04; cparams.speed_tier = SpeedTier::kSquirrel; - DecompressParams dparams; EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); @@ -1273,7 +1349,7 @@ TEST(JxlTest, RoundtripDots) { EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, pool)); EXPECT_LE(compressed.size(), 400000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), @@ -1284,7 +1360,7 @@ TEST(JxlTest, RoundtripDots) { TEST(JxlTest, RoundtripNoise) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); + ReadTestData("external/wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); @@ -1293,7 +1369,6 @@ TEST(JxlTest, RoundtripNoise) { CompressParams cparams; cparams.noise = Override::kOn; cparams.speed_tier = SpeedTier::kSquirrel; - DecompressParams dparams; EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); @@ -1305,7 +1380,7 @@ TEST(JxlTest, RoundtripNoise) { EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io2; - EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(test::DecodeFile({}, compressed, &io2, pool)); EXPECT_LE(compressed.size(), 40000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), @@ -1316,19 +1391,18 @@ TEST(JxlTest, RoundtripNoise) { TEST(JxlTest, RoundtripLossless8Gray) { ThreadPool* pool = nullptr; const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); + "external/wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); CompressParams cparams = CParamsForLossless(); - DecompressParams dparams; EXPECT_TRUE(io.Main().IsGray()); EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 130000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 130000u); // If fails, see note about floating point in RoundtripLossless8. EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0); EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), @@ -1349,9 +1423,8 @@ TEST(JxlTest, RoundtripAnimation) { ASSERT_EQ(4u, io.frames.size()); CompressParams cparams; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 3000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 3000u); EXPECT_EQ(io2.frames.size(), io.frames.size()); test::CoalesceGIFAnimationWithAlpha(&io); @@ -1372,9 +1445,8 @@ TEST(JxlTest, RoundtripLosslessAnimation) { ASSERT_EQ(4u, io.frames.size()); CompressParams cparams = CParamsForLossless(); - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 1200u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 1200u); EXPECT_EQ(io2.frames.size(), io.frames.size()); test::CoalesceGIFAnimationWithAlpha(&io); @@ -1392,46 +1464,19 @@ TEST(JxlTest, RoundtripAnimationPatches) { CompressParams cparams; cparams.patches = Override::kOn; - DecompressParams dparams; CodecInOut io2; // 40k with no patches, 27k with patch frames encoded multiple times. - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 24000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 24000u); EXPECT_EQ(io2.frames.size(), io.frames.size()); // >10 with broken patches EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(1.5)); + IsSlightlyBelow(1.2)); } #endif // JPEGXL_ENABLE_GIF -namespace { - -jxl::Status DecompressJxlToJPEGForTest( - const jpegxl::tools::JpegXlContainer& container, jxl::ThreadPool* pool, - jxl::PaddedBytes* output) { - output->clear(); - jxl::Span<const uint8_t> compressed(container.codestream); - - JXL_RETURN_IF_ERROR(compressed.size() >= 2); - - // JXL case - // Decode to DCT when possible and generate a JPG file. - jxl::CodecInOut io; - jxl::DecompressParams params; - params.keep_dct = true; - if (!jpegxl::tools::DecodeJpegXlToJpeg(params, container, &io, pool)) { - return JXL_FAILURE("Failed to decode JXL to JPEG"); - } - if (!jpeg::EncodeImageJPGCoefficients(&io, output)) { - return JXL_FAILURE("Failed to generate JPEG"); - } - return true; -} - -} // namespace - size_t RoundtripJpeg(const PaddedBytes& jpeg_in, ThreadPool* pool) { CodecInOut io; EXPECT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(jpeg_in), &io)); @@ -1453,11 +1498,13 @@ size_t RoundtripJpeg(const PaddedBytes& jpeg_in, ThreadPool* pool) { enc_container.jpeg_reconstruction_size = jpeg_data.size(); EXPECT_TRUE(EncodeJpegXlContainerOneShot(enc_container, &compressed)); - jpegxl::tools::JpegXlContainer container; - EXPECT_TRUE(DecodeJpegXlContainerOneShot(compressed.data(), compressed.size(), - &container)); - PaddedBytes out; - EXPECT_TRUE(DecompressJxlToJPEGForTest(container, pool, &out)); + jxl::extras::JXLDecompressParams dparams; + dparams.runner = pool->runner(); + dparams.runner_opaque = pool->runner_opaque(); + std::vector<uint8_t> out; + jxl::extras::PackedPixelFile ppf; + EXPECT_TRUE(DecodeImageJXL(compressed.data(), compressed.size(), dparams, + nullptr, &ppf, &out)); EXPECT_EQ(out.size(), jpeg_in.size()); size_t failures = 0; for (size_t i = 0; i < std::min(out.size(), jpeg_in.size()); i++) { @@ -1474,18 +1521,16 @@ size_t RoundtripJpeg(const PaddedBytes& jpeg_in, ThreadPool* pool) { TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression444)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg"); - // JPEG size is 326'916 bytes. - EXPECT_LE(RoundtripJpeg(orig, &pool), 256000u); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png.im_q85_444.jpg"); + // JPEG size is 696,659 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 570000u); } #if JPEGXL_ENABLE_JPEG TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png.im_q85_444.jpg"); CodecInOut io; ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io)); @@ -1495,10 +1540,8 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels)) { CompressParams cparams; cparams.color_transform = jxl::ColorTransform::kYCbCr; - DecompressParams dparams; - CodecInOut io3; - Roundtrip(&io, cparams, dparams, &pool, &io3); + Roundtrip(&io, cparams, {}, &pool, &io3); // TODO(eustas): investigate, why SJPEG and JpegRecompression pixels are // different. @@ -1508,8 +1551,7 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels)) { TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png.im_q85_420.jpg"); CodecInOut io; ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io)); @@ -1519,10 +1561,8 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420)) { CompressParams cparams; cparams.color_transform = jxl::ColorTransform::kYCbCr; - DecompressParams dparams; - CodecInOut io3; - Roundtrip(&io, cparams, dparams, &pool, &io3); + Roundtrip(&io, cparams, {}, &pool, &io3); EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()), IsSlightlyBelow(11)); @@ -1531,8 +1571,7 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420)) { TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420EarlyFlush)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png.im_q85_420.jpg"); CodecInOut io; ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io)); @@ -1542,21 +1581,20 @@ TEST(JxlTest, CompressParams cparams; cparams.color_transform = jxl::ColorTransform::kYCbCr; - DecompressParams dparams; + extras::JXLDecompressParams dparams; dparams.max_downsampling = 8; CodecInOut io3; Roundtrip(&io, cparams, dparams, &pool, &io3); EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()), - IsSlightlyBelow(650)); + IsSlightlyBelow(4410)); } TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420Mul16)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon_cropped.jpg"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower_cropped.jpg"); CodecInOut io; ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io)); @@ -1566,10 +1604,8 @@ TEST(JxlTest, CompressParams cparams; cparams.color_transform = jxl::ColorTransform::kYCbCr; - DecompressParams dparams; - CodecInOut io3; - Roundtrip(&io, cparams, dparams, &pool, &io3); + Roundtrip(&io, cparams, {}, &pool, &io3); EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()), IsSlightlyBelow(4)); @@ -1578,9 +1614,8 @@ TEST(JxlTest, TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels_asymmetric)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/" - "flower_foveon.png.im_q85_asymmetric.jpg"); + const PaddedBytes orig = + ReadTestData("jxl/flower/flower.png.im_q85_asymmetric.jpg"); CodecInOut io; ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io)); @@ -1590,10 +1625,8 @@ TEST(JxlTest, CompressParams cparams; cparams.color_transform = jxl::ColorTransform::kYCbCr; - DecompressParams dparams; - CodecInOut io3; - Roundtrip(&io, cparams, dparams, &pool, &io3); + Roundtrip(&io, cparams, {}, &pool, &io3); EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()), IsSlightlyBelow(10)); @@ -1603,94 +1636,108 @@ TEST(JxlTest, TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionGray)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_gray.jpg"); - // JPEG size is 167'025 bytes. - EXPECT_LE(RoundtripJpeg(orig, &pool), 140000u); + const PaddedBytes orig = + ReadTestData("jxl/flower/flower.png.im_q85_gray.jpg"); + // JPEG size is 456,528 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 390000u); } TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression420)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg"); - // JPEG size is 226'018 bytes. - EXPECT_LE(RoundtripJpeg(orig, &pool), 181050u); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png.im_q85_420.jpg"); + // JPEG size is 546,797 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 460000u); } TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression_luma_subsample)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/" - "flower_foveon.png.im_q85_luma_subsample.jpg"); - // JPEG size is 216'069 bytes. - EXPECT_LE(RoundtripJpeg(orig, &pool), 181000u); + const PaddedBytes orig = + ReadTestData("jxl/flower/flower.png.im_q85_luma_subsample.jpg"); + // JPEG size is 400,724 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 330000u); } TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression444_12)) { // 444 JPEG that has an interesting sampling-factor (1x2, 1x2, 1x2). ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_444_1x2.jpg"); - // JPEG size is 329'942 bytes. - EXPECT_LE(RoundtripJpeg(orig, &pool), 256000u); + const PaddedBytes orig = + ReadTestData("jxl/flower/flower.png.im_q85_444_1x2.jpg"); + // JPEG size is 703,874 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 570000u); } TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression422)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_422.jpg"); - // JPEG size is 265'590 bytes. - EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png.im_q85_422.jpg"); + // JPEG size is 522,057 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 500000u); } TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression440)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/flower_foveon.png.im_q85_440.jpg"); - // JPEG size is 262'249 bytes. - EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png.im_q85_440.jpg"); + // JPEG size is 603,623 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 510000u); } TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression_asymmetric)) { // 2x vertical downsample of one chroma channel, 2x horizontal downsample of // the other. ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/" - "flower_foveon.png.im_q85_asymmetric.jpg"); - // JPEG size is 262'249 bytes. - EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u); + const PaddedBytes orig = + ReadTestData("jxl/flower/flower.png.im_q85_asymmetric.jpg"); + // JPEG size is 604,601 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 510000u); } TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression420Progr)) { ThreadPoolInternal pool(8); - const PaddedBytes orig = ReadTestData( - "third_party/imagecompression.info/" - "flower_foveon.png.im_q85_420_progr.jpg"); - EXPECT_LE(RoundtripJpeg(orig, &pool), 181000u); + const PaddedBytes orig = + ReadTestData("jxl/flower/flower.png.im_q85_420_progr.jpg"); + // JPEG size is 522,057 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 460000u); } TEST(JxlTest, RoundtripProgressive) { ThreadPoolInternal pool(4); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); + io.ShrinkTo(600, 1024); + + CompressParams cparams; + + cparams.butteraugli_distance = 1.0f; + cparams.progressive_dc = 1; + cparams.responsive = true; + cparams.progressive_mode = true; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 61700u); + EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), + /*distmap=*/nullptr, &pool), + IsSlightlyBelow(1.17f)); +} + +TEST(JxlTest, RoundtripProgressiveLevel2Slow) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); io.ShrinkTo(600, 1024); CompressParams cparams; - DecompressParams dparams; cparams.butteraugli_distance = 1.0f; - cparams.progressive_dc = true; + cparams.progressive_dc = 2; + cparams.speed_tier = SpeedTier::kTortoise; cparams.responsive = true; cparams.progressive_mode = true; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, &pool, &io2), 71000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool), - IsSlightlyBelow(1.1f)); + IsSlightlyBelow(1.2f)); } } // namespace diff --git a/media/libjxl/src/lib/jxl/libjxl.pc.in b/media/libjxl/src/lib/jxl/libjxl.pc.in index 5dca2ac168..4a7af65b7c 100644 --- a/media/libjxl/src/lib/jxl/libjxl.pc.in +++ b/media/libjxl/src/lib/jxl/libjxl.pc.in @@ -1,7 +1,7 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ -includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=@PKGCONFIG_TARGET_LIBS@ +includedir=@PKGCONFIG_TARGET_INCLUDES@ Name: libjxl Description: Loads and saves JPEG XL files @@ -10,3 +10,4 @@ Requires.private: @JPEGXL_LIBRARY_REQUIRES@ Libs: -L${libdir} -ljxl Libs.private: -lm Cflags: -I${includedir} +Cflags.private: -DJXL_STATIC_DEFINE diff --git a/media/libjxl/src/lib/jxl/modular/encoding/context_predict.h b/media/libjxl/src/lib/jxl/modular/encoding/context_predict.h index 2865a4dabb..914cd6a4e4 100644 --- a/media/libjxl/src/lib/jxl/modular/encoding/context_predict.h +++ b/media/libjxl/src/lib/jxl/modular/encoding/context_predict.h @@ -62,7 +62,7 @@ struct State { pixel_type_w pred = 0; // *before* removing the added bits. std::vector<uint32_t> pred_errors[kNumPredictors]; std::vector<int32_t> error; - Header header; + const Header header; // Allows to approximate division by a number from 1 to 64. uint32_t divlookup[64]; diff --git a/media/libjxl/src/lib/jxl/modular/encoding/enc_encoding.cc b/media/libjxl/src/lib/jxl/modular/encoding/enc_encoding.cc index 1934b5e8d6..eeed2aee5c 100644 --- a/media/libjxl/src/lib/jxl/modular/encoding/enc_encoding.cc +++ b/media/libjxl/src/lib/jxl/modular/encoding/enc_encoding.cc @@ -87,7 +87,8 @@ void GatherTreeData(const Image &image, pixel_type chan, size_t group_id, } uint64_t threshold = (std::numeric_limits<uint64_t>::max() >> 32) * pixel_fraction; - uint64_t s[2] = {0x94D049BB133111EBull, 0xBF58476D1CE4E5B9ull}; + uint64_t s[2] = {static_cast<uint64_t>(0x94D049BB133111EBull), + static_cast<uint64_t>(0xBF58476D1CE4E5B9ull)}; // Xorshift128+ adapted from xorshift128+-inl.h auto use_sample = [&]() { auto s1 = s[0]; diff --git a/media/libjxl/src/lib/jxl/modular/encoding/enc_ma.cc b/media/libjxl/src/lib/jxl/modular/encoding/enc_ma.cc index 7700ecc26d..90b11baa06 100644 --- a/media/libjxl/src/lib/jxl/modular/encoding/enc_ma.cc +++ b/media/libjxl/src/lib/jxl/modular/encoding/enc_ma.cc @@ -28,6 +28,11 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Eq; +using hwy::HWY_NAMESPACE::IfThenElse; +using hwy::HWY_NAMESPACE::Lt; + const HWY_FULL(float) df; const HWY_FULL(int32_t) di; size_t Padded(size_t x) { return RoundUpTo(x, Lanes(df)); } @@ -40,8 +45,8 @@ float EstimateBits(const int32_t *counts, int32_t *rounded_counts, const auto zero_i = Zero(di); for (size_t i = 0; i < num_symbols; i += Lanes(df)) { auto counts_v = LoadU(di, &counts[i]); - counts_v = IfThenElse(counts_v == zero_i, zero_i, - IfThenElse(counts_v < min, min, counts_v)); + counts_v = IfThenElse(Eq(counts_v, zero_i), zero_i, + IfThenElse(Lt(counts_v, min), min, counts_v)); StoreU(counts_v, di, &rounded_counts[i]); } // Compute entropy of the "rounded" probabilities. @@ -54,11 +59,11 @@ float EstimateBits(const int32_t *counts, int32_t *rounded_counts, for (size_t i = 0; i < num_symbols; i += Lanes(df)) { const auto counts_v = ConvertTo(df, LoadU(di, &counts[i])); const auto round_counts_v = LoadU(di, &rounded_counts[i]); - const auto probs = ConvertTo(df, round_counts_v) * inv_total; - const auto nbps = IfThenElse(round_counts_v == total_v, BitCast(di, zero), + const auto probs = Mul(ConvertTo(df, round_counts_v), inv_total); + const auto nbps = IfThenElse(Eq(round_counts_v, total_v), BitCast(di, zero), BitCast(di, FastLog2f(df, probs))); - bits_lanes -= - IfThenElse(counts_v == zero, zero, counts_v * BitCast(df, nbps)); + bits_lanes = Sub(bits_lanes, IfThenElse(Eq(counts_v, zero), zero, + Mul(counts_v, BitCast(df, nbps)))); } return GetLane(SumOfLanes(df, bits_lanes)); } @@ -504,7 +509,7 @@ void ComputeBestTree(TreeSamples &tree_samples, float threshold, tree); } -constexpr int TreeSamples::kPropertyRange; +constexpr int32_t TreeSamples::kPropertyRange; constexpr uint32_t TreeSamples::kDedupEntryUnused; Status TreeSamples::SetPredictor(Predictor predictor, @@ -722,12 +727,12 @@ void TreeSamples::ThreeShuffle(size_t a, size_t b, size_t c) { } namespace { -std::vector<int> QuantizeHistogram(const std::vector<uint32_t> &histogram, - size_t num_chunks) { +std::vector<int32_t> QuantizeHistogram(const std::vector<uint32_t> &histogram, + size_t num_chunks) { if (histogram.empty()) return {}; // TODO(veluca): selecting distinct quantiles is likely not the best // way to go about this. - std::vector<int> thresholds; + std::vector<int32_t> thresholds; size_t sum = std::accumulate(histogram.begin(), histogram.end(), 0LU); size_t cumsum = 0; size_t threshold = 0; @@ -741,8 +746,8 @@ std::vector<int> QuantizeHistogram(const std::vector<uint32_t> &histogram, return thresholds; } -std::vector<int> QuantizeSamples(const std::vector<int32_t> &samples, - size_t num_chunks) { +std::vector<int32_t> QuantizeSamples(const std::vector<int32_t> &samples, + size_t num_chunks) { if (samples.empty()) return {}; int min = *std::min_element(samples.begin(), samples.end()); constexpr int kRange = 512; @@ -752,7 +757,7 @@ std::vector<int> QuantizeSamples(const std::vector<int32_t> &samples, uint32_t sample_offset = std::min(std::max(s, -kRange), kRange) - min; counts[sample_offset]++; } - std::vector<int> thresholds = QuantizeHistogram(counts, num_chunks); + std::vector<int32_t> thresholds = QuantizeHistogram(counts, num_chunks); for (auto &v : thresholds) v += min; return thresholds; } @@ -810,15 +815,15 @@ void TreeSamples::PreQuantizeProperties( return QuantizeHistogram(group_pixel_count, max_property_values); }; auto quantize_coordinate = [&]() { - std::vector<int> quantized; + std::vector<int32_t> quantized; quantized.reserve(max_property_values - 1); for (size_t i = 0; i + 1 < max_property_values; i++) { quantized.push_back((i + 1) * 256 / max_property_values - 1); } return quantized; }; - std::vector<int> abs_pixel_thr; - std::vector<int> pixel_thr; + std::vector<int32_t> abs_pixel_thr; + std::vector<int32_t> pixel_thr; auto quantize_pixel_property = [&]() { if (pixel_thr.empty()) { pixel_thr = QuantizeSamples(pixel_samples, max_property_values); @@ -833,8 +838,8 @@ void TreeSamples::PreQuantizeProperties( } return abs_pixel_thr; }; - std::vector<int> abs_diff_thr; - std::vector<int> diff_thr; + std::vector<int32_t> abs_diff_thr; + std::vector<int32_t> diff_thr; auto quantize_diff_property = [&]() { if (diff_thr.empty()) { diff_thr = QuantizeSamples(diff_samples, max_property_values); @@ -851,16 +856,16 @@ void TreeSamples::PreQuantizeProperties( }; auto quantize_wp = [&]() { if (max_property_values < 32) { - return std::vector<int>{-127, -63, -31, -15, -7, -3, -1, 0, - 1, 3, 7, 15, 31, 63, 127}; + return std::vector<int32_t>{-127, -63, -31, -15, -7, -3, -1, 0, + 1, 3, 7, 15, 31, 63, 127}; } if (max_property_values < 64) { - return std::vector<int>{-255, -191, -127, -95, -63, -47, -31, -23, - -15, -11, -7, -5, -3, -1, 0, 1, - 3, 5, 7, 11, 15, 23, 31, 47, - 63, 95, 127, 191, 255}; + return std::vector<int32_t>{-255, -191, -127, -95, -63, -47, -31, -23, + -15, -11, -7, -5, -3, -1, 0, 1, + 3, 5, 7, 11, 15, 23, 31, 47, + 63, 95, 127, 191, 255}; } - return std::vector<int>{ + return std::vector<int32_t>{ -255, -223, -191, -159, -127, -111, -95, -79, -63, -55, -47, -39, -31, -27, -23, -19, -15, -13, -11, -9, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, diff --git a/media/libjxl/src/lib/jxl/modular/encoding/enc_ma.h b/media/libjxl/src/lib/jxl/modular/encoding/enc_ma.h index d0a90cc952..ede37c8023 100644 --- a/media/libjxl/src/lib/jxl/modular/encoding/enc_ma.h +++ b/media/libjxl/src/lib/jxl/modular/encoding/enc_ma.h @@ -114,13 +114,13 @@ struct TreeSamples { // Property values, quantized to at most 256 distinct values. std::vector<std::vector<uint8_t>> props; // Decompactification info for `props`. - std::vector<std::vector<int>> compact_properties; + std::vector<std::vector<int32_t>> compact_properties; // List of properties to use. std::vector<uint32_t> props_to_use; // List of predictors to use. std::vector<Predictor> predictors; // Mapping property value -> quantized property value. - static constexpr int kPropertyRange = 511; + static constexpr int32_t kPropertyRange = 511; std::vector<std::vector<uint8_t>> property_mapping; // Number of samples seen. size_t num_samples = 0; diff --git a/media/libjxl/src/lib/jxl/modular/encoding/encoding.cc b/media/libjxl/src/lib/jxl/modular/encoding/encoding.cc index b34cf78d2a..9d2c3e5cf9 100644 --- a/media/libjxl/src/lib/jxl/modular/encoding/encoding.cc +++ b/media/libjxl/src/lib/jxl/modular/encoding/encoding.cc @@ -591,18 +591,19 @@ Status ModularGenericDecompress(BitReader *br, Image &image, #endif GroupHeader local_header; if (header == nullptr) header = &local_header; + size_t bit_pos = br->TotalBitsConsumed(); auto dec_status = ModularDecode(br, image, *header, group_id, options, tree, code, ctx_map, allow_truncated_group); if (!allow_truncated_group) JXL_RETURN_IF_ERROR(dec_status); if (dec_status.IsFatalError()) return dec_status; if (undo_transforms) image.undo_transforms(header->wp_header); if (image.error) return JXL_FAILURE("Corrupt file. Aborting."); - size_t bit_pos = br->TotalBitsConsumed(); JXL_DEBUG_V(4, "Modular-decoded a %" PRIuS "x%" PRIuS " nbchans=%" PRIuS " image from %" PRIuS " bytes", image.w, image.h, image.channel.size(), (br->TotalBitsConsumed() - bit_pos) / 8); + JXL_DEBUG_V(5, "Modular image: %s", image.DebugString().c_str()); (void)bit_pos; #ifdef JXL_ENABLE_ASSERT // Check that after applying all transforms we are back to the requested image diff --git a/media/libjxl/src/lib/jxl/modular/modular_image.cc b/media/libjxl/src/lib/jxl/modular/modular_image.cc index 6cfdf71505..785d0c5443 100644 --- a/media/libjxl/src/lib/jxl/modular/modular_image.cc +++ b/media/libjxl/src/lib/jxl/modular/modular_image.cc @@ -5,6 +5,8 @@ #include "lib/jxl/modular/modular_image.h" +#include <sstream> + #include "lib/jxl/base/status.h" #include "lib/jxl/common.h" #include "lib/jxl/modular/transform/transform.h" @@ -58,4 +60,18 @@ Image Image::clone() { return c; } +std::string Image::DebugString() const { + std::ostringstream os; + os << w << "x" << h << ", depth: " << bitdepth; + if (!channel.empty()) { + os << ", channels:"; + for (size_t i = 0; i < channel.size(); ++i) { + os << " " << channel[i].w << "x" << channel[i].h + << "(shift: " << channel[i].hshift << "," << channel[i].vshift << ")"; + if (i < nb_meta_channels) os << "*"; + } + } + return os.str(); +} + } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/modular/modular_image.h b/media/libjxl/src/lib/jxl/modular/modular_image.h index 4d11e7abea..3e9b5a8a08 100644 --- a/media/libjxl/src/lib/jxl/modular/modular_image.h +++ b/media/libjxl/src/lib/jxl/modular/modular_image.h @@ -11,6 +11,7 @@ #include <stdio.h> #include <string.h> +#include <string> #include <utility> #include <vector> @@ -97,10 +98,19 @@ class Image { Image& operator=(Image&& other) noexcept; Image(Image&& other) noexcept = default; + bool empty() const { + for (const auto& ch : channel) { + if (ch.w && ch.h) return false; + } + return true; + } + Image clone(); void undo_transforms(const weighted::Header& wp_header, jxl::ThreadPool* pool = nullptr); + + std::string DebugString() const; }; } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/modular/transform/enc_palette.cc b/media/libjxl/src/lib/jxl/modular/transform/enc_palette.cc index 1ae7353276..7065f80817 100644 --- a/media/libjxl/src/lib/jxl/modular/transform/enc_palette.cc +++ b/media/libjxl/src/lib/jxl/modular/transform/enc_palette.cc @@ -179,7 +179,43 @@ Status FwdPaletteIteration(Image &input, uint32_t begin_c, uint32_t end_c, size_t lookup_table_size = static_cast<int64_t>(maxval) - static_cast<int64_t>(minval) + 1; if (lookup_table_size > palette_internal::kMaxPaletteLookupTableSize) { - return false; // too large lookup table + // a lookup table would use too much memory, instead use a slower approach + // with std::set + std::set<pixel_type> chpalette; + pixel_type idx = 0; + for (size_t y = 0; y < h; y++) { + const pixel_type *p = input.channel[begin_c].Row(y); + for (size_t x = 0; x < w; x++) { + const bool new_color = chpalette.insert(p[x]).second; + if (new_color) { + idx++; + if (idx > (int)nb_colors) return false; + } + } + } + JXL_DEBUG_V(6, "Channel %i uses only %i colors.", begin_c, idx); + Channel pch(idx, 1); + pch.hshift = -1; + nb_colors = idx; + idx = 0; + pixel_type *JXL_RESTRICT p_palette = pch.Row(0); + for (pixel_type p : chpalette) { + p_palette[idx++] = p; + } + for (size_t y = 0; y < h; y++) { + pixel_type *p = input.channel[begin_c].Row(y); + for (size_t x = 0; x < w; x++) { + for (idx = 0; p[x] != p_palette[idx] && idx < (int)nb_colors; idx++) { + } + JXL_DASSERT(idx < (int)nb_colors); + p[x] = idx; + } + } + predictor = Predictor::Zero; + input.nb_meta_channels++; + input.channel.insert(input.channel.begin(), std::move(pch)); + + return true; } lookup.resize(lookup_table_size, 0); pixel_type idx = 0; @@ -304,7 +340,7 @@ Status FwdPaletteIteration(Image &input, uint32_t begin_c, uint32_t end_c, pixel_type *JXL_RESTRICT p_palette = pch.Row(0); intptr_t onerow = pch.plane.PixelsPerRow(); intptr_t onerow_image = input.channel[begin_c].plane.PixelsPerRow(); - const int bit_depth = input.bitdepth; + const int bit_depth = std::min(input.bitdepth, 24); if (lossy) { for (uint32_t i = 0; i < nb_deltas; i++) { diff --git a/media/libjxl/src/lib/jxl/modular/transform/palette.h b/media/libjxl/src/lib/jxl/modular/transform/palette.h index 5fd533fb71..ed2d33bed5 100644 --- a/media/libjxl/src/lib/jxl/modular/transform/palette.h +++ b/media/libjxl/src/lib/jxl/modular/transform/palette.h @@ -6,6 +6,8 @@ #ifndef LIB_JXL_MODULAR_TRANSFORM_PALETTE_H_ #define LIB_JXL_MODULAR_TRANSFORM_PALETTE_H_ +#include <atomic> + #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/status.h" #include "lib/jxl/common.h" @@ -139,7 +141,7 @@ static Status InvPalette(Image &input, uint32_t begin_c, uint32_t nb_colors, const pixel_type *JXL_RESTRICT p_palette = input.channel[0].Row(0); intptr_t onerow = input.channel[0].plane.PixelsPerRow(); intptr_t onerow_image = input.channel[c0].plane.PixelsPerRow(); - const int bit_depth = input.bitdepth; + const int bit_depth = std::min(input.bitdepth, 24); if (w == 0) { // Nothing to do. @@ -152,7 +154,7 @@ static Status InvPalette(Image &input, uint32_t begin_c, uint32_t nb_colors, const size_t y = task; pixel_type *p = input.channel[c0].Row(y); for (size_t x = 0; x < w; x++) { - const int index = Clamp1(p[x], 0, (pixel_type)palette.w - 1); + const int index = Clamp1<int>(p[x], 0, (pixel_type)palette.w - 1); p[x] = palette_internal::GetPaletteValue( p_palette, index, /*c=*/0, /*palette_size=*/palette.w, diff --git a/media/libjxl/src/lib/jxl/modular/transform/rct.cc b/media/libjxl/src/lib/jxl/modular/transform/rct.cc index 68f07ea267..f3002a5ac3 100644 --- a/media/libjxl/src/lib/jxl/modular/transform/rct.cc +++ b/media/libjxl/src/lib/jxl/modular/transform/rct.cc @@ -12,7 +12,10 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; using hwy::HWY_NAMESPACE::ShiftRight; +using hwy::HWY_NAMESPACE::Sub; template <int transform_type> void InvRCTRow(const pixel_type* in0, const pixel_type* in1, @@ -31,10 +34,10 @@ void InvRCTRow(const pixel_type* in0, const pixel_type* in1, auto Y = Load(d, in0 + x); auto Co = Load(d, in1 + x); auto Cg = Load(d, in2 + x); - Y -= ShiftRight<1>(Cg); - auto G = Cg + Y; - Y -= ShiftRight<1>(Co); - auto R = Y + Co; + Y = Sub(Y, ShiftRight<1>(Cg)); + auto G = Add(Cg, Y); + Y = Sub(Y, ShiftRight<1>(Co)); + auto R = Add(Y, Co); Store(R, d, out0 + x); Store(G, d, out1 + x); Store(Y, d, out2 + x); @@ -42,11 +45,11 @@ void InvRCTRow(const pixel_type* in0, const pixel_type* in1, auto First = Load(d, in0 + x); auto Second = Load(d, in1 + x); auto Third = Load(d, in2 + x); - if (third) Third += First; + if (third) Third = Add(Third, First); if (second == 1) { - Second += First; + Second = Add(Second, First); } else if (second == 2) { - Second += ShiftRight<1>(First + Third); + Second = Add(Second, ShiftRight<1>(Add(First, Third))); } Store(First, d, out0 + x); Store(Second, d, out1 + x); diff --git a/media/libjxl/src/lib/jxl/modular/transform/squeeze.cc b/media/libjxl/src/lib/jxl/modular/transform/squeeze.cc index 3f4689145b..34311895dc 100644 --- a/media/libjxl/src/lib/jxl/modular/transform/squeeze.cc +++ b/media/libjxl/src/lib/jxl/modular/transform/squeeze.cc @@ -23,9 +23,23 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Abs; +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::And; +using hwy::HWY_NAMESPACE::Gt; +using hwy::HWY_NAMESPACE::IfThenElse; +using hwy::HWY_NAMESPACE::IfThenZeroElse; +using hwy::HWY_NAMESPACE::Lt; +using hwy::HWY_NAMESPACE::MulEven; +using hwy::HWY_NAMESPACE::Ne; +using hwy::HWY_NAMESPACE::Neg; +using hwy::HWY_NAMESPACE::OddEven; using hwy::HWY_NAMESPACE::RebindToUnsigned; using hwy::HWY_NAMESPACE::ShiftLeft; using hwy::HWY_NAMESPACE::ShiftRight; +using hwy::HWY_NAMESPACE::Sub; +using hwy::HWY_NAMESPACE::Xor; #if HWY_TARGET != HWY_SCALAR @@ -44,12 +58,12 @@ JXL_INLINE void FastUnsqueeze(const pixel_type *JXL_RESTRICT p_residual, auto next_avg = Load(d, p_navg + x); auto top = Load(d, p_pout + x); // Equivalent to SmoothTendency(top,avg,next_avg), but without branches - auto Ba = top - avg; - auto an = avg - next_avg; - auto nonmono = Ba ^ an; + auto Ba = Sub(top, avg); + auto an = Sub(avg, next_avg); + auto nonmono = Xor(Ba, an); auto absBa = Abs(Ba); auto absan = Abs(an); - auto absBn = Abs(top - next_avg); + auto absBn = Abs(Sub(top, next_avg)); // Compute a3 = absBa / 3 auto a3e = BitCast(d, ShiftRight<32>(MulEven(absBa, onethird))); auto a3oi = MulEven(Reverse(d, absBa), onethird); @@ -57,26 +71,27 @@ JXL_INLINE void FastUnsqueeze(const pixel_type *JXL_RESTRICT p_residual, d, Reverse(hwy::HWY_NAMESPACE::Repartition<pixel_type_w, decltype(d)>(), a3oi)); auto a3 = OddEven(a3o, a3e); - a3 += absBn + Set(d, 2); + a3 = Add(a3, Add(absBn, Set(d, 2))); auto absdiff = ShiftRight<2>(a3); - auto skipdiff = Ba != Zero(d); - skipdiff = And(skipdiff, an != Zero(d)); - skipdiff = And(skipdiff, nonmono < Zero(d)); - auto absBa2 = ShiftLeft<1>(absBa) + (absdiff & Set(d, 1)); - absdiff = - IfThenElse(absdiff > absBa2, ShiftLeft<1>(absBa) + Set(d, 1), absdiff); + auto skipdiff = Ne(Ba, Zero(d)); + skipdiff = And(skipdiff, Ne(an, Zero(d))); + skipdiff = And(skipdiff, Lt(nonmono, Zero(d))); + auto absBa2 = Add(ShiftLeft<1>(absBa), And(absdiff, Set(d, 1))); + absdiff = IfThenElse(Gt(absdiff, absBa2), + Add(ShiftLeft<1>(absBa), Set(d, 1)), absdiff); auto absan2 = ShiftLeft<1>(absan); - absdiff = - IfThenElse(absdiff + (absdiff & Set(d, 1)) > absan2, absan2, absdiff); - auto diff1 = IfThenElse(top < next_avg, Neg(absdiff), absdiff); + absdiff = IfThenElse(Gt(Add(absdiff, And(absdiff, Set(d, 1))), absan2), + absan2, absdiff); + auto diff1 = IfThenElse(Lt(top, next_avg), Neg(absdiff), absdiff); auto tendency = IfThenZeroElse(skipdiff, diff1); auto diff_minus_tendency = Load(d, p_residual + x); - auto diff = diff_minus_tendency + tendency; - auto out = avg + ShiftRight<1>( - diff + BitCast(d, ShiftRight<31>(BitCast(du, diff)))); + auto diff = Add(diff_minus_tendency, tendency); + auto out = + Add(avg, ShiftRight<1>( + Add(diff, BitCast(d, ShiftRight<31>(BitCast(du, diff)))))); Store(out, d, p_out + x); - Store(out - diff, d, p_nout + x); + Store(Sub(out, diff), d, p_nout + x); } } @@ -114,15 +129,15 @@ Status InvHSqueeze(Image &input, uint32_t c, uint32_t rc, ThreadPool *pool) { const pixel_type *JXL_RESTRICT p_avg = chin.Row(y); pixel_type *JXL_RESTRICT p_out = chout.Row(y); for (size_t x = x0; x < chin_residual.w; x++) { - pixel_type diff_minus_tendency = p_residual[x]; - pixel_type avg = p_avg[x]; - pixel_type next_avg = (x + 1 < chin.w ? p_avg[x + 1] : avg); - pixel_type left = (x ? p_out[(x << 1) - 1] : avg); - pixel_type tendency = SmoothTendency(left, avg, next_avg); - pixel_type diff = diff_minus_tendency + tendency; - pixel_type A = avg + (diff / 2); + pixel_type_w diff_minus_tendency = p_residual[x]; + pixel_type_w avg = p_avg[x]; + pixel_type_w next_avg = (x + 1 < chin.w ? p_avg[x + 1] : avg); + pixel_type_w left = (x ? p_out[(x << 1) - 1] : avg); + pixel_type_w tendency = SmoothTendency(left, avg, next_avg); + pixel_type_w diff = diff_minus_tendency + tendency; + pixel_type_w A = avg + (diff / 2); p_out[(x << 1)] = A; - pixel_type B = A - diff; + pixel_type_w B = A - diff; p_out[(x << 1) + 1] = B; } if (chout.w & 1) p_out[chout.w - 1] = p_avg[chin.w - 1]; @@ -243,13 +258,13 @@ Status InvVSqueeze(Image &input, uint32_t c, uint32_t rc, ThreadPool *pool) { } #endif for (; x < w; x++) { - pixel_type avg = p_avg[x]; - pixel_type next_avg = p_navg[x]; - pixel_type top = p_pout[x]; - pixel_type tendency = SmoothTendency(top, avg, next_avg); - pixel_type diff_minus_tendency = p_residual[x]; - pixel_type diff = diff_minus_tendency + tendency; - pixel_type out = avg + (diff / 2); + pixel_type_w avg = p_avg[x]; + pixel_type_w next_avg = p_navg[x]; + pixel_type_w top = p_pout[x]; + pixel_type_w tendency = SmoothTendency(top, avg, next_avg); + pixel_type_w diff_minus_tendency = p_residual[x]; + pixel_type_w diff = diff_minus_tendency + tendency; + pixel_type_w out = avg + (diff / 2); p_out[x] = out; // If the chin_residual.h == chin.h, the output has an even number // of rows so the next line is fine. Otherwise, this loop won't @@ -450,6 +465,8 @@ Status MetaSqueeze(Image &image, std::vector<SqueezeParams> *parameters) { image.channel.insert(image.channel.begin() + offset + (c - beginc), std::move(dummy)); + JXL_DEBUG_V(8, "MetaSqueeze applied, current image: %s", + image.DebugString().c_str()); } } return true; @@ -457,4 +474,4 @@ Status MetaSqueeze(Image &image, std::vector<SqueezeParams> *parameters) { } // namespace jxl -#endif
\ No newline at end of file +#endif diff --git a/media/libjxl/src/lib/jxl/modular/transform/transform.cc b/media/libjxl/src/lib/jxl/modular/transform/transform.cc index 89f4586a56..d9f2b435bf 100644 --- a/media/libjxl/src/lib/jxl/modular/transform/transform.cc +++ b/media/libjxl/src/lib/jxl/modular/transform/transform.cc @@ -39,8 +39,7 @@ Status Transform::Inverse(Image &input, const weighted::Header &wp_header, } Status Transform::MetaApply(Image &input) { - JXL_DEBUG_V(6, "Input channels (%" PRIuS ", %" PRIuS " meta): ", - input.channel.size(), input.nb_meta_channels); + JXL_DEBUG_V(6, "MetaApply input: %s", input.DebugString().c_str()); switch (id) { case TransformId::kRCT: JXL_DEBUG_V(2, "Transform: kRCT, rct_type=%" PRIu32, rct_type); @@ -68,9 +67,6 @@ Status Transform::MetaApply(Image &input) { "Transform: kPalette, begin_c=%" PRIu32 ", num_c=%" PRIu32 ", nb_colors=%" PRIu32 ", nb_deltas=%" PRIu32, begin_c, num_c, nb_colors, nb_deltas); - if (input.bitdepth > 24) { - return JXL_FAILURE("Palette is not allowed for bitdepth > 24"); - } return MetaPalette(input, begin_c, begin_c + num_c - 1, nb_colors, nb_deltas, lossy_palette); default: diff --git a/media/libjxl/src/lib/jxl/modular_test.cc b/media/libjxl/src/lib/jxl/modular_test.cc index db8c8f31b2..c87be68223 100644 --- a/media/libjxl/src/lib/jxl/modular_test.cc +++ b/media/libjxl/src/lib/jxl/modular_test.cc @@ -13,6 +13,7 @@ #include "gtest/gtest.h" #include "lib/extras/codec.h" +#include "lib/extras/dec/jxl.h" #include "lib/jxl/aux_out.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/data_parallel.h" @@ -22,20 +23,20 @@ #include "lib/jxl/codec_in_out.h" #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/color_management.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/enc_butteraugli_comparator.h" #include "lib/jxl/enc_butteraugli_pnorm.h" #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_file.h" #include "lib/jxl/enc_params.h" +#include "lib/jxl/enc_toc.h" #include "lib/jxl/image.h" #include "lib/jxl/image_bundle.h" #include "lib/jxl/image_ops.h" #include "lib/jxl/image_test_utils.h" #include "lib/jxl/modular/encoding/enc_encoding.h" #include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/encoding/ma_common.h" #include "lib/jxl/test_utils.h" #include "lib/jxl/testdata.h" @@ -45,12 +46,10 @@ using test::Roundtrip; void TestLosslessGroups(size_t group_size_shift) { ThreadPool* pool = nullptr; - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CompressParams cparams; cparams.SetLossless(); cparams.modular_group_size_shift = group_size_shift; - DecompressParams dparams; CodecInOut io_out; size_t compressed_size; @@ -59,7 +58,7 @@ void TestLosslessGroups(size_t group_size_shift) { ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 4, io.ysize() / 4); - compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + compressed_size = Roundtrip(&io, cparams, {}, pool, &io_out); EXPECT_LE(compressed_size, 280000u); EXPECT_LE(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), @@ -79,7 +78,7 @@ TEST(ModularTest, JXL_TSAN_SLOW_TEST(RoundtripLosslessGroups1024)) { TEST(ModularTest, RoundtripLosslessCustomWP_PermuteRCT) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CompressParams cparams; cparams.SetLossless(); // 9 = permute to GBR, to test the special case of permutation-only @@ -87,7 +86,6 @@ TEST(ModularTest, RoundtripLosslessCustomWP_PermuteRCT) { // slowest speed so different WP modes are tried cparams.speed_tier = SpeedTier::kTortoise; cparams.options.predictor = {Predictor::Weighted}; - DecompressParams dparams; CodecInOut io_out; size_t compressed_size; @@ -96,7 +94,7 @@ TEST(ModularTest, RoundtripLosslessCustomWP_PermuteRCT) { ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(100, 100); - compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + compressed_size = Roundtrip(&io, cparams, {}, pool, &io_out); EXPECT_LE(compressed_size, 10150u); EXPECT_LE(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), @@ -106,15 +104,13 @@ TEST(ModularTest, RoundtripLosslessCustomWP_PermuteRCT) { TEST(ModularTest, RoundtripLossyDeltaPalette) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CompressParams cparams; cparams.modular_mode = true; cparams.color_transform = jxl::ColorTransform::kNone; cparams.lossy_palette = true; cparams.palette_colors = 0; - DecompressParams dparams; - CodecInOut io_out; size_t compressed_size; @@ -122,7 +118,7 @@ TEST(ModularTest, RoundtripLossyDeltaPalette) { ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(300, 100); - compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + compressed_size = Roundtrip(&io, cparams, {}, pool, &io_out); EXPECT_LE(compressed_size, 6800u); cparams.ba_params.intensity_target = 80.0f; EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), @@ -132,15 +128,13 @@ TEST(ModularTest, RoundtripLossyDeltaPalette) { TEST(ModularTest, RoundtripLossyDeltaPaletteWP) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CompressParams cparams; cparams.SetLossless(); cparams.lossy_palette = true; cparams.palette_colors = 0; cparams.options.predictor = jxl::Predictor::Weighted; - DecompressParams dparams; - CodecInOut io_out; size_t compressed_size; @@ -148,7 +142,7 @@ TEST(ModularTest, RoundtripLossyDeltaPaletteWP) { ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(300, 100); - compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + compressed_size = Roundtrip(&io, cparams, {}, pool, &io_out); EXPECT_LE(compressed_size, 7000u); cparams.ba_params.intensity_target = 80.0f; EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), @@ -159,11 +153,10 @@ TEST(ModularTest, RoundtripLossyDeltaPaletteWP) { TEST(ModularTest, RoundtripLossy) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CompressParams cparams; cparams.modular_mode = true; cparams.butteraugli_distance = 2.f; - DecompressParams dparams; CodecInOut io_out; size_t compressed_size; @@ -171,7 +164,7 @@ TEST(ModularTest, RoundtripLossy) { CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); - compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + compressed_size = Roundtrip(&io, cparams, {}, pool, &io_out); EXPECT_LE(compressed_size, 30000u); cparams.ba_params.intensity_target = 80.0f; EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), @@ -182,11 +175,10 @@ TEST(ModularTest, RoundtripLossy) { TEST(ModularTest, RoundtripLossy16) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png"); + ReadTestData("external/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png"); CompressParams cparams; cparams.modular_mode = true; cparams.butteraugli_distance = 2.f; - DecompressParams dparams; CodecInOut io_out; size_t compressed_size; @@ -196,7 +188,7 @@ TEST(ModularTest, RoundtripLossy16) { JXL_CHECK(io.TransformTo(ColorEncoding::SRGB(), GetJxlCms(), pool)); io.metadata.m.color_encoding = ColorEncoding::SRGB(); - compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + compressed_size = Roundtrip(&io, cparams, {}, pool, &io_out); EXPECT_LE(compressed_size, 300u); cparams.ba_params.intensity_target = 80.0f; EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), @@ -248,8 +240,8 @@ TEST(ModularTest, RoundtripExtraProperties) { TEST(ModularTest, RoundtripLosslessCustomSqueeze) { ThreadPool* pool = nullptr; - const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + const PaddedBytes orig = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); @@ -271,10 +263,9 @@ TEST(ModularTest, RoundtripLosslessCustomSqueeze) { p.in_place = true; p.horizontal = false; cparams.squeezes.push_back(p); - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 265000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 265000u); EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool)); } @@ -316,7 +307,7 @@ TEST_P(ModularTestParam, RoundtripLossless) { ThreadPool* pool = nullptr; Rng generator(123); const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io1; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io1, pool)); @@ -357,9 +348,8 @@ TEST_P(ModularTestParam, RoundtripLossless) { cparams.options.predictor = {Predictor::Zero}; cparams.speed_tier = SpeedTier::kThunder; cparams.responsive = responsive; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), bitdepth * xsize * ysize / 3); EXPECT_LE(0, ComputeDistance2(io.Main(), io2.Main(), GetJxlCms())); size_t different = 0; @@ -411,13 +401,145 @@ TEST(ModularTest, RoundtripLosslessCustomFloat) { cparams.options.predictor = {Predictor::Zero}; cparams.speed_tier = SpeedTier::kThunder; cparams.decoding_speed_tier = 2; - DecompressParams dparams; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 23000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 23000u); EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool)); } +void WriteHeaders(BitWriter* writer, size_t xsize, size_t ysize) { + BitWriter::Allotment allotment(writer, 16); + writer->Write(8, 0xFF); + writer->Write(8, kCodestreamMarker); + ReclaimAndCharge(writer, &allotment, 0, nullptr); + CodecMetadata metadata; + EXPECT_TRUE(metadata.size.Set(xsize, ysize)); + EXPECT_TRUE(WriteSizeHeader(metadata.size, writer, 0, nullptr)); + metadata.m.color_encoding = ColorEncoding::LinearSRGB(/*is_gray=*/true); + metadata.m.xyb_encoded = false; + metadata.m.SetUintSamples(31); + EXPECT_TRUE(WriteImageMetadata(metadata.m, writer, 0, nullptr)); + metadata.transform_data.nonserialized_xyb_encoded = metadata.m.xyb_encoded; + EXPECT_TRUE(Bundle::Write(metadata.transform_data, writer, 0, nullptr)); + writer->ZeroPadToByte(); + FrameHeader frame_header(&metadata); + frame_header.encoding = FrameEncoding::kModular; + frame_header.loop_filter.gab = false; + frame_header.loop_filter.epf_iters = 0; + EXPECT_TRUE(WriteFrameHeader(frame_header, writer, nullptr)); +} + +// Tree with single node, zero predictor, offset is 1 and multiplier is 1, +// entropy code is prefix tree with alphabet size 256 and all bits lengths 8. +void WriteHistograms(BitWriter* writer) { + writer->Write(1, 1); // default DC quant + writer->Write(1, 1); // has_tree + // tree histograms + writer->Write(1, 0); // LZ77 disabled + writer->Write(3, 1); // simple context map + writer->Write(1, 1); // prefix code + writer->Write(7, 0x63); // UnintConfig(3, 2, 1) + writer->Write(12, 0xfef); // alphabet_size = 256 + writer->Write(32, 0x10003); // all bit lengths 8 + // tree tokens + writer->Write(8, 0); // tree leaf + writer->Write(8, 0); // zero predictor + writer->Write(8, 64); // offset = UnpackSigned(ReverseBits(64)) = 1 + writer->Write(16, 0); // multiplier = 1 + // histograms + writer->Write(1, 0); // LZ77 disabled + writer->Write(1, 1); // prefix code + writer->Write(7, 0x63); // UnintConfig(3, 2, 1) + writer->Write(12, 0xfef); // alphabet_size = 256 + writer->Write(32, 0x10003); // all bit lengths 8 +} + +TEST(ModularTest, PredictorIntegerOverflow) { + const size_t xsize = 1; + const size_t ysize = 1; + BitWriter writer; + WriteHeaders(&writer, xsize, ysize); + std::vector<BitWriter> group_codes(1); + { + BitWriter* bw = &group_codes[0]; + BitWriter::Allotment allotment(bw, 1 << 20); + WriteHistograms(bw); + GroupHeader header; + header.use_global_tree = true; + EXPECT_TRUE(Bundle::Write(header, bw, 0, nullptr)); + // After UnpackSigned this becomes (1 << 31) - 1, the largest pixel_type, + // and after adding the offset we get -(1 << 31). + bw->Write(8, 119); + bw->Write(28, 0xfffffff); + bw->ZeroPadToByte(); + ReclaimAndCharge(bw, &allotment, 0, nullptr); + } + EXPECT_TRUE(WriteGroupOffsets(group_codes, nullptr, &writer, nullptr)); + writer.AppendByteAligned(group_codes); + + PaddedBytes compressed = std::move(writer).TakeBytes(); + extras::PackedPixelFile ppf; + extras::JXLDecompressParams params; + params.accepted_formats.push_back({1, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}); + EXPECT_TRUE(DecodeImageJXL(compressed.data(), compressed.size(), params, + nullptr, &ppf)); + ASSERT_EQ(1, ppf.frames.size()); + const auto& img = ppf.frames[0].color; + const auto pixels = reinterpret_cast<const float*>(img.pixels()); + EXPECT_EQ(-1.0f, pixels[0]); +} + +TEST(ModularTest, UnsqueezeIntegerOverflow) { + // Image width is 9 so we can test both the SIMD and non-vector code paths. + const size_t xsize = 9; + const size_t ysize = 2; + BitWriter writer; + WriteHeaders(&writer, xsize, ysize); + std::vector<BitWriter> group_codes(1); + { + BitWriter* bw = &group_codes[0]; + BitWriter::Allotment allotment(bw, 1 << 20); + WriteHistograms(bw); + GroupHeader header; + header.use_global_tree = true; + header.transforms.emplace_back(); + header.transforms[0].id = TransformId::kSqueeze; + SqueezeParams params; + params.horizontal = false; + params.in_place = true; + params.begin_c = 0; + params.num_c = 1; + header.transforms[0].squeezes.emplace_back(params); + EXPECT_TRUE(Bundle::Write(header, bw, 0, nullptr)); + for (size_t i = 0; i < xsize * ysize; ++i) { + // After UnpackSigned and adding offset, this becomes (1 << 31) - 1, both + // in the image and in the residual channels, and unsqueeze makes them + // ~(3 << 30) and (1 << 30) (in pixel_type_w) and the first wraps around + // to about -(1 << 30). + bw->Write(8, 119); + bw->Write(28, 0xffffffe); + } + bw->ZeroPadToByte(); + ReclaimAndCharge(bw, &allotment, 0, nullptr); + } + EXPECT_TRUE(WriteGroupOffsets(group_codes, nullptr, &writer, nullptr)); + writer.AppendByteAligned(group_codes); + + PaddedBytes compressed = std::move(writer).TakeBytes(); + extras::PackedPixelFile ppf; + extras::JXLDecompressParams params; + params.accepted_formats.push_back({1, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}); + EXPECT_TRUE(DecodeImageJXL(compressed.data(), compressed.size(), params, + nullptr, &ppf)); + ASSERT_EQ(1, ppf.frames.size()); + const auto& img = ppf.frames[0].color; + const auto pixels = reinterpret_cast<const float*>(img.pixels()); + for (size_t x = 0; x < xsize; ++x) { + EXPECT_NEAR(-0.5f, pixels[x], 1e-10); + EXPECT_NEAR(0.5f, pixels[xsize + x], 1e-10); + } +} + } // namespace } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/opsin_image_test.cc b/media/libjxl/src/lib/jxl/opsin_image_test.cc index 2a95e2917b..7573d6b8bd 100644 --- a/media/libjxl/src/lib/jxl/opsin_image_test.cc +++ b/media/libjxl/src/lib/jxl/opsin_image_test.cc @@ -19,11 +19,6 @@ namespace jxl { namespace { -class OpsinImageTargetTest : public hwy::TestWithParamTarget {}; -HWY_TARGET_INSTANTIATE_TEST_SUITE_P(OpsinImageTargetTest); - -TEST_P(OpsinImageTargetTest, MaxCubeRootError) { TestCubeRoot(); } - // Convert a single linear sRGB color to xyb, using the exact image conversion // procedure that jpeg xl uses. void LinearSrgbToOpsin(float rgb_r, float rgb_g, float rgb_b, diff --git a/media/libjxl/src/lib/jxl/passes_test.cc b/media/libjxl/src/lib/jxl/passes_test.cc index ad44e70b89..a58aadc011 100644 --- a/media/libjxl/src/lib/jxl/passes_test.cc +++ b/media/libjxl/src/lib/jxl/passes_test.cc @@ -5,6 +5,7 @@ #include <stddef.h> +#include <future> #include <string> #include <utility> @@ -18,8 +19,6 @@ #include "lib/jxl/base/thread_pool_internal.h" #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/common.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/enc_butteraugli_comparator.h" #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_color_management.h" @@ -37,7 +36,7 @@ using test::Roundtrip; TEST(PassesTest, RoundtripSmallPasses) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); @@ -45,10 +44,9 @@ TEST(PassesTest, RoundtripSmallPasses) { CompressParams cparams; cparams.butteraugli_distance = 1.0; cparams.progressive_mode = true; - DecompressParams dparams; CodecInOut io2; - Roundtrip(&io, cparams, dparams, pool, &io2); + Roundtrip(&io, cparams, {}, pool, &io2); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(1.0)); @@ -57,7 +55,7 @@ TEST(PassesTest, RoundtripSmallPasses) { TEST(PassesTest, RoundtripUnalignedPasses) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 12, io.ysize() / 7); @@ -65,56 +63,51 @@ TEST(PassesTest, RoundtripUnalignedPasses) { CompressParams cparams; cparams.butteraugli_distance = 2.0; cparams.progressive_mode = true; - DecompressParams dparams; CodecInOut io2; - Roundtrip(&io, cparams, dparams, pool, &io2); + Roundtrip(&io, cparams, {}, pool, &io2); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(1.6)); } TEST(PassesTest, RoundtripMultiGroupPasses) { - ThreadPoolInternal pool(4); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; - ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); + { + ThreadPoolInternal pool(4); + ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); + } io.ShrinkTo(600, 1024); // partial X, full Y group - CompressParams cparams; - DecompressParams dparams; + auto test = [&](float target_distance, float threshold) { + ThreadPoolInternal pool(4); + CompressParams cparams; + cparams.butteraugli_distance = target_distance; + cparams.progressive_mode = true; + CodecInOut io2; + Roundtrip(&io, cparams, {}, &pool, &io2); + EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), + /*distmap=*/nullptr, &pool), + IsSlightlyBelow(target_distance + threshold)); + }; - cparams.butteraugli_distance = 1.0f; - cparams.progressive_mode = true; - CodecInOut io2; - Roundtrip(&io, cparams, dparams, &pool, &io2); - EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), - /*distmap=*/nullptr, &pool), - IsSlightlyBelow(1.2f)); - - cparams.butteraugli_distance = 2.0f; - CodecInOut io3; - Roundtrip(&io, cparams, dparams, &pool, &io3); - EXPECT_THAT(ButteraugliDistance(io, io3, cparams.ba_params, GetJxlCms(), - /*distmap=*/nullptr, &pool), - IsSlightlyBelow(2.0f)); + auto run1 = std::async(std::launch::async, test, 1.0f, 0.3f); + auto run2 = std::async(std::launch::async, test, 2.0f, 0.3f); } TEST(PassesTest, RoundtripLargeFastPasses) { ThreadPoolInternal pool(8); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; cparams.progressive_mode = true; - DecompressParams dparams; CodecInOut io2; - Roundtrip(&io, cparams, dparams, &pool, &io2); + Roundtrip(&io, cparams, {}, &pool, &io2); } // Checks for differing size/distance in two consecutive runs of distance 2, @@ -122,8 +115,7 @@ TEST(PassesTest, RoundtripLargeFastPasses) { // Failing this may be a sign of race conditions or invalid memory accesses. TEST(PassesTest, RoundtripProgressiveConsistent) { ThreadPoolInternal pool(8); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); @@ -131,17 +123,16 @@ TEST(PassesTest, RoundtripProgressiveConsistent) { cparams.speed_tier = SpeedTier::kSquirrel; cparams.progressive_mode = true; cparams.butteraugli_distance = 2.0; - DecompressParams dparams; // Try each xsize mod kBlockDim to verify right border handling. for (size_t xsize = 48; xsize > 40; --xsize) { io.ShrinkTo(xsize, 15); CodecInOut io2; - const size_t size2 = Roundtrip(&io, cparams, dparams, &pool, &io2); + const size_t size2 = Roundtrip(&io, cparams, {}, &pool, &io2); CodecInOut io3; - const size_t size3 = Roundtrip(&io, cparams, dparams, &pool, &io3); + const size_t size3 = Roundtrip(&io, cparams, {}, &pool, &io3); // Exact same compressed size. EXPECT_EQ(size2, size3); @@ -160,7 +151,7 @@ TEST(PassesTest, RoundtripProgressiveConsistent) { TEST(PassesTest, AllDownsampleFeasible) { ThreadPoolInternal pool(8); const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); @@ -189,10 +180,10 @@ TEST(PassesTest, AllDownsampleFeasible) { auto check = [&](const uint32_t task, size_t /* thread */) -> void { const size_t downsampling = downsamplings[task]; - DecompressParams dparams; + extras::JXLDecompressParams dparams; dparams.max_downsampling = downsampling; CodecInOut output; - ASSERT_TRUE(DecodeFile(dparams, compressed, &output, nullptr)); + ASSERT_TRUE(test::DecodeFile(dparams, compressed, &output, nullptr)); EXPECT_EQ(output.xsize(), io.xsize()) << "downsampling = " << downsampling; EXPECT_EQ(output.ysize(), io.ysize()) << "downsampling = " << downsampling; EXPECT_LE(ButteraugliDistance(io, output, cparams.ba_params, GetJxlCms(), @@ -207,7 +198,7 @@ TEST(PassesTest, AllDownsampleFeasible) { TEST(PassesTest, AllDownsampleFeasibleQProgressive) { ThreadPoolInternal pool(8); const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); @@ -236,10 +227,10 @@ TEST(PassesTest, AllDownsampleFeasibleQProgressive) { auto check = [&](const uint32_t task, size_t /* thread */) -> void { const size_t downsampling = downsamplings[task]; - DecompressParams dparams; + extras::JXLDecompressParams dparams; dparams.max_downsampling = downsampling; CodecInOut output; - ASSERT_TRUE(DecodeFile(dparams, compressed, &output, nullptr)); + ASSERT_TRUE(test::DecodeFile(dparams, compressed, &output, nullptr)); EXPECT_EQ(output.xsize(), io.xsize()) << "downsampling = " << downsampling; EXPECT_EQ(output.ysize(), io.ysize()) << "downsampling = " << downsampling; EXPECT_LE(ButteraugliDistance(io, output, cparams.ba_params, GetJxlCms(), @@ -254,7 +245,7 @@ TEST(PassesTest, AllDownsampleFeasibleQProgressive) { TEST(PassesTest, ProgressiveDownsample2DegradesCorrectlyGrayscale) { ThreadPoolInternal pool(8); const PaddedBytes orig = ReadTestData( - "third_party/wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); + "external/wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); CodecInOut io_orig; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_orig, &pool)); Rect rect(0, 0, io_orig.xsize(), 128); @@ -281,14 +272,14 @@ TEST(PassesTest, ProgressiveDownsample2DegradesCorrectlyGrayscale) { EXPECT_LE(compressed.size(), 10000u); - DecompressParams dparams; + extras::JXLDecompressParams dparams; dparams.max_downsampling = 1; CodecInOut output; - ASSERT_TRUE(DecodeFile(dparams, compressed, &output, nullptr)); + ASSERT_TRUE(test::DecodeFile(dparams, compressed, &output, nullptr)); dparams.max_downsampling = 2; CodecInOut output_d2; - ASSERT_TRUE(DecodeFile(dparams, compressed, &output_d2, nullptr)); + ASSERT_TRUE(test::DecodeFile(dparams, compressed, &output_d2, nullptr)); // 0 if reading all the passes, ~15 if skipping the 8x pass. float butteraugli_distance_down2_full = @@ -301,8 +292,7 @@ TEST(PassesTest, ProgressiveDownsample2DegradesCorrectlyGrayscale) { TEST(PassesTest, ProgressiveDownsample2DegradesCorrectly) { ThreadPoolInternal pool(8); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io_orig; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_orig, &pool)); Rect rect(0, 0, io_orig.xsize(), 128); @@ -328,14 +318,14 @@ TEST(PassesTest, ProgressiveDownsample2DegradesCorrectly) { EXPECT_LE(compressed.size(), 220000u); - DecompressParams dparams; + extras::JXLDecompressParams dparams; dparams.max_downsampling = 1; CodecInOut output; - ASSERT_TRUE(DecodeFile(dparams, compressed, &output, nullptr)); + ASSERT_TRUE(test::DecodeFile(dparams, compressed, &output, nullptr)); dparams.max_downsampling = 2; CodecInOut output_d2; - ASSERT_TRUE(DecodeFile(dparams, compressed, &output_d2, nullptr)); + ASSERT_TRUE(test::DecodeFile(dparams, compressed, &output_d2, nullptr)); // 0 if reading all the passes, ~15 if skipping the 8x pass. float butteraugli_distance_down2_full = @@ -348,8 +338,7 @@ TEST(PassesTest, ProgressiveDownsample2DegradesCorrectly) { TEST(PassesTest, NonProgressiveDCImage) { ThreadPoolInternal pool(8); - const PaddedBytes orig = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes orig = ReadTestData("jxl/flower/flower.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); @@ -366,10 +355,10 @@ TEST(PassesTest, NonProgressiveDCImage) { // Even in non-progressive mode, it should be possible to return a DC-only // image. - DecompressParams dparams; + extras::JXLDecompressParams dparams; dparams.max_downsampling = 100; CodecInOut output; - ASSERT_TRUE(DecodeFile(dparams, compressed, &output, &pool)); + ASSERT_TRUE(test::DecodeFile(dparams, compressed, &output, &pool)); EXPECT_EQ(output.xsize(), io.xsize()); EXPECT_EQ(output.ysize(), io.ysize()); } @@ -377,7 +366,7 @@ TEST(PassesTest, NonProgressiveDCImage) { TEST(PassesTest, RoundtripSmallNoGaborishPasses) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); @@ -386,10 +375,9 @@ TEST(PassesTest, RoundtripSmallNoGaborishPasses) { cparams.gaborish = Override::kOff; cparams.butteraugli_distance = 1.0; cparams.progressive_mode = true; - DecompressParams dparams; CodecInOut io2; - Roundtrip(&io, cparams, dparams, pool, &io2); + Roundtrip(&io, cparams, {}, pool, &io2); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), IsSlightlyBelow(1.2)); diff --git a/media/libjxl/src/lib/jxl/patch_dictionary_test.cc b/media/libjxl/src/lib/jxl/patch_dictionary_test.cc index 01dcbd554f..3a34b83d08 100644 --- a/media/libjxl/src/lib/jxl/patch_dictionary_test.cc +++ b/media/libjxl/src/lib/jxl/patch_dictionary_test.cc @@ -5,7 +5,6 @@ #include "gtest/gtest.h" #include "lib/extras/codec.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/enc_butteraugli_comparator.h" #include "lib/jxl/enc_params.h" #include "lib/jxl/image_test_utils.h" @@ -26,11 +25,10 @@ TEST(PatchDictionaryTest, GrayscaleModular) { CompressParams cparams; cparams.SetLossless(); cparams.patches = jxl::Override::kOn; - DecompressParams dparams; CodecInOut io2; // Without patches: ~25k - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 8000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 8000u); VerifyRelativeError(*io.Main().color(), *io2.Main().color(), 1e-7f, 0); } @@ -42,11 +40,10 @@ TEST(PatchDictionaryTest, GrayscaleVarDCT) { CompressParams cparams; cparams.patches = jxl::Override::kOn; - DecompressParams dparams; CodecInOut io2; // Without patches: ~47k - EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 14000u); + EXPECT_LE(Roundtrip(&io, cparams, {}, pool, &io2), 14000u); // Without patches: ~1.2 EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), diff --git a/media/libjxl/src/lib/jxl/preview_test.cc b/media/libjxl/src/lib/jxl/preview_test.cc index a8342d3a2a..35ec70b57e 100644 --- a/media/libjxl/src/lib/jxl/preview_test.cc +++ b/media/libjxl/src/lib/jxl/preview_test.cc @@ -16,8 +16,6 @@ #include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/codec_in_out.h" #include "lib/jxl/color_encoding_internal.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/enc_butteraugli_comparator.h" #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_file.h" @@ -34,7 +32,7 @@ using test::Roundtrip; TEST(PreviewTest, RoundtripGivenPreview) { ThreadPool* pool = nullptr; const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool)); io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); @@ -50,10 +48,9 @@ TEST(PreviewTest, RoundtripGivenPreview) { CompressParams cparams; cparams.butteraugli_distance = 2.0; cparams.speed_tier = SpeedTier::kSquirrel; - DecompressParams dparams; CodecInOut io2; - Roundtrip(&io, cparams, dparams, pool, &io2); + Roundtrip(&io, cparams, {}, pool, &io2); EXPECT_EQ(preview_xsize, io2.metadata.m.preview_size.xsize()); EXPECT_EQ(preview_ysize, io2.metadata.m.preview_size.ysize()); EXPECT_EQ(preview_xsize, io2.preview_frame.xsize()); diff --git a/media/libjxl/src/lib/jxl/progressive_split.h b/media/libjxl/src/lib/jxl/progressive_split.h index 68ab7bc9dc..aeae980447 100644 --- a/media/libjxl/src/lib/jxl/progressive_split.h +++ b/media/libjxl/src/lib/jxl/progressive_split.h @@ -115,7 +115,10 @@ class ProgressiveSplitter { min_downsampling_factor != kNoDownsamplingFactor) { passes->downsample[passes->num_downsample] = min_downsampling_factor; passes->last_pass[passes->num_downsample] = i; - passes->num_downsample += 1; + if (mode_.passes[i + 1].suitable_for_downsampling_of_at_least < + min_downsampling_factor) { + passes->num_downsample += 1; + } } } } diff --git a/media/libjxl/src/lib/jxl/quant_weights.cc b/media/libjxl/src/lib/jxl/quant_weights.cc index e8d9a10ed6..756a481414 100644 --- a/media/libjxl/src/lib/jxl/quant_weights.cc +++ b/media/libjxl/src/lib/jxl/quant_weights.cc @@ -32,6 +32,11 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Lt; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::Sqrt; + // kQuantWeights[N * N * c + N * y + x] is the relative weight of the (x, y) // coefficient in component c. Higher weights correspond to finer quantization // intervals and more bits spent in encoding. @@ -104,14 +109,14 @@ hwy::HWY_NAMESPACE::Vec<DF4> InterpolateVec( auto idx = ConvertTo(di, scaled_pos); - auto frac = scaled_pos - ConvertTo(DF4(), idx); + auto frac = Sub(scaled_pos, ConvertTo(DF4(), idx)); // TODO(veluca): in theory, this could be done with 8 TableLookupBytes, but // it's probably slower. auto a = GatherIndex(DF4(), array, idx); auto b = GatherIndex(DF4(), array + 1, idx); - return a * FastPowf(DF4(), b / a, frac); + return Mul(a, FastPowf(DF4(), Div(b, a), frac)); } // Computes quant weights for a COLS*ROWS-sized transform, using num_bands @@ -139,7 +144,8 @@ Status GetQuantWeights( float dy = y * rcprow; float dy2 = dy * dy; for (uint32_t x = 0; x < COLS; x += Lanes(DF4())) { - auto dx = (Set(DF4(), x) + Load(DF4(), l0123)) * Set(DF4(), rcpcol); + auto dx = + Mul(Add(Set(DF4(), x), Load(DF4(), l0123)), Set(DF4(), rcpcol)); auto scaled_distance = Sqrt(MulAdd(dx, dx, Set(DF4(), dy2))); auto weight = num_bands == 1 ? Set(DF4(), bands[0]) : InterpolateVec(scaled_distance, bands); @@ -318,11 +324,11 @@ Status ComputeQuantTable(const QuantEncoding& encoding, HWY_CAPPED(float, 64) d; for (size_t i = 0; i < num * 3; i += Lanes(d)) { auto inv_val = LoadU(d, weights.data() + i); - if (JXL_UNLIKELY(!AllFalse(d, inv_val >= Set(d, 1.0f / kAlmostZero)) || - !AllFalse(d, inv_val < Set(d, kAlmostZero)))) { + if (JXL_UNLIKELY(!AllFalse(d, Ge(inv_val, Set(d, 1.0f / kAlmostZero))) || + !AllFalse(d, Lt(inv_val, Set(d, kAlmostZero))))) { return JXL_FAILURE("Invalid quantization table"); } - auto val = Set(d, 1.0f) / inv_val; + auto val = Div(Set(d, 1.0f), inv_val); StoreU(val, d, table + *pos + i); StoreU(inv_val, d, inv_table + *pos + i); } @@ -450,9 +456,9 @@ Status Decode(BitReader* br, QuantEncoding* encoding, size_t required_size_x, for (size_t i = 0; i < 6; i++) { encoding->afv_weights[c][i] *= 64; } - JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params)); - JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params_afv_4x4)); } + JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params)); + JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params_afv_4x4)); break; } case QuantEncoding::kQuantModeDCT: { diff --git a/media/libjxl/src/lib/jxl/quantizer-inl.h b/media/libjxl/src/lib/jxl/quantizer-inl.h index 3de8758eac..64d273c552 100644 --- a/media/libjxl/src/lib/jxl/quantizer-inl.h +++ b/media/libjxl/src/lib/jxl/quantizer-inl.h @@ -19,8 +19,16 @@ namespace HWY_NAMESPACE { namespace { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::And; +using hwy::HWY_NAMESPACE::AndNot; +using hwy::HWY_NAMESPACE::ApproximateReciprocal; +using hwy::HWY_NAMESPACE::Gt; +using hwy::HWY_NAMESPACE::IfThenElse; +using hwy::HWY_NAMESPACE::IfThenElseZero; +using hwy::HWY_NAMESPACE::Lt; using hwy::HWY_NAMESPACE::Rebind; using hwy::HWY_NAMESPACE::Vec; +using hwy::HWY_NAMESPACE::Xor; template <class DI> HWY_INLINE HWY_MAYBE_UNUSED Vec<Rebind<float, DI>> AdjustQuantBias( @@ -44,8 +52,8 @@ HWY_INLINE HWY_MAYBE_UNUSED Vec<Rebind<float, DI>> AdjustQuantBias( // Integer comparison is not helpful because Clang incurs bypass penalties // from unnecessarily mixing integer and float. - const auto is_01 = abs_quant < Set(df, 1.125f); - const auto not_0 = abs_quant > Zero(df); + const auto is_01 = Lt(abs_quant, Set(df, 1.125f)); + const auto not_0 = Gt(abs_quant, Zero(df)); // Bitwise logic is faster than quant * biases[c]. const auto one_bias = IfThenElseZero(not_0, Xor(Set(df, biases[c]), sign)); diff --git a/media/libjxl/src/lib/jxl/quantizer.cc b/media/libjxl/src/lib/jxl/quantizer.cc index c8c8837477..814aea276b 100644 --- a/media/libjxl/src/lib/jxl/quantizer.cc +++ b/media/libjxl/src/lib/jxl/quantizer.cc @@ -19,9 +19,9 @@ namespace jxl { -static const int kDefaultQuant = 64; +static const int32_t kDefaultQuant = 64; -constexpr int Quantizer::kQuantMax; +constexpr int32_t Quantizer::kQuantMax; Quantizer::Quantizer(const DequantMatrices* dequant) : Quantizer(dequant, kDefaultQuant, kGlobalScaleDenom / kDefaultQuant) {} @@ -39,7 +39,7 @@ Quantizer::Quantizer(const DequantMatrices* dequant, int quant_dc, void Quantizer::ComputeGlobalScaleAndQuant(float quant_dc, float quant_median, float quant_median_absd) { // Target value for the median value in the quant field. - const float kQuantFieldTarget = 3.80987740592518214386f; + const float kQuantFieldTarget = 5; // We reduce the median of the quant field by the median absolute deviation: // higher resolution on highly varying quant fields. float scale = kGlobalScaleDenom * (quant_median - quant_median_absd) / @@ -49,9 +49,9 @@ void Quantizer::ComputeGlobalScaleAndQuant(float quant_dc, float quant_median, if (scale > (1 << 15)) scale = 1 << 15; int new_global_scale = static_cast<int>(scale); // Ensure that quant_dc_ will always be at least - // kGlobalScaleDenom/kGlobalScaleNumerator. + // 0.625 * kGlobalScaleDenom/kGlobalScaleNumerator = 10. const int scaled_quant_dc = - static_cast<int>(quant_dc * kGlobalScaleNumerator); + static_cast<int>(quant_dc * kGlobalScaleNumerator * 1.6); if (new_global_scale > scaled_quant_dc) { new_global_scale = scaled_quant_dc; if (new_global_scale <= 0) new_global_scale = 1; @@ -70,7 +70,7 @@ void Quantizer::ComputeGlobalScaleAndQuant(float quant_dc, float quant_median, } void Quantizer::SetQuantFieldRect(const ImageF& qf, const Rect& rect, - ImageI* JXL_RESTRICT raw_quant_field) { + ImageI* JXL_RESTRICT raw_quant_field) const { for (size_t y = 0; y < rect.ysize(); ++y) { const float* JXL_RESTRICT row_qf = rect.ConstRow(qf, y); int32_t* JXL_RESTRICT row_qi = rect.Row(raw_quant_field, y); @@ -83,7 +83,6 @@ void Quantizer::SetQuantFieldRect(const ImageF& qf, const Rect& rect, void Quantizer::SetQuantField(const float quant_dc, const ImageF& qf, ImageI* JXL_RESTRICT raw_quant_field) { - JXL_CHECK(SameSize(*raw_quant_field, qf)); std::vector<float> data(qf.xsize() * qf.ysize()); for (size_t y = 0; y < qf.ysize(); ++y) { const float* JXL_RESTRICT row_qf = qf.Row(y); @@ -103,13 +102,16 @@ void Quantizer::SetQuantField(const float quant_dc, const ImageF& qf, deviations.end()); const float quant_median_absd = deviations[deviations.size() / 2]; ComputeGlobalScaleAndQuant(quant_dc, quant_median, quant_median_absd); - SetQuantFieldRect(qf, Rect(qf), raw_quant_field); + if (raw_quant_field) { + JXL_CHECK(SameSize(*raw_quant_field, qf)); + SetQuantFieldRect(qf, Rect(qf), raw_quant_field); + } } void Quantizer::SetQuant(float quant_dc, float quant_ac, ImageI* JXL_RESTRICT raw_quant_field) { ComputeGlobalScaleAndQuant(quant_dc, quant_ac, 0); - int val = ClampVal(quant_ac * inv_global_scale_ + 0.5f); + int32_t val = ClampVal(quant_ac * inv_global_scale_ + 0.5f); FillImage(val, raw_quant_field); } diff --git a/media/libjxl/src/lib/jxl/quantizer.h b/media/libjxl/src/lib/jxl/quantizer.h index 1ff593e5c1..09e2e5e45c 100644 --- a/media/libjxl/src/lib/jxl/quantizer.h +++ b/media/libjxl/src/lib/jxl/quantizer.h @@ -68,10 +68,19 @@ class Quantizer { explicit Quantizer(const DequantMatrices* dequant); Quantizer(const DequantMatrices* dequant, int quant_dc, int global_scale); - static constexpr int kQuantMax = 256; + static constexpr int32_t kQuantMax = 256; - static JXL_INLINE int ClampVal(float val) { - return static_cast<int>(std::max(1.0f, std::min<float>(val, kQuantMax))); + static JXL_INLINE int32_t ClampVal(float val) { + return static_cast<int32_t>( + std::max(1.0f, std::min<float>(val, kQuantMax))); + } + + float ScaleGlobalScale(const float scale) { + int new_global_scale = static_cast<int>(global_scale_ * scale + 0.5f); + float scale_out = new_global_scale * 1.0f / global_scale_; + global_scale_ = new_global_scale; + RecomputeFromGlobalScale(); + return scale_out; } // Recomputes other derived fields after global_scale_ has changed. @@ -93,7 +102,7 @@ class Quantizer { JXL_INLINE float InvGlobalScale() const { return inv_global_scale_; } void SetQuantFieldRect(const ImageF& qf, const Rect& rect, - ImageI* JXL_RESTRICT raw_quant_field); + ImageI* JXL_RESTRICT raw_quant_field) const; void SetQuantField(float quant_dc, const ImageF& qf, ImageI* JXL_RESTRICT raw_quant_field); diff --git a/media/libjxl/src/lib/jxl/quantizer_test.cc b/media/libjxl/src/lib/jxl/quantizer_test.cc index 60c9901828..d570bf6d40 100644 --- a/media/libjxl/src/lib/jxl/quantizer_test.cc +++ b/media/libjxl/src/lib/jxl/quantizer_test.cc @@ -42,7 +42,7 @@ TEST(QuantizerTest, BitStreamRoundtripSameQuant) { EXPECT_TRUE(quantizer1.Encode(&writer, 0, nullptr)); writer.ZeroPadToByte(); const size_t bits_written = writer.BitsWritten(); - Quantizer quantizer2(&dequant, qxsize, qysize); + Quantizer quantizer2(&dequant); BitReader reader(writer.GetSpan()); EXPECT_TRUE(quantizer2.Decode(&reader)); EXPECT_TRUE(reader.JumpToByteBoundary()); @@ -66,7 +66,7 @@ TEST(QuantizerTest, BitStreamRoundtripRandomQuant) { EXPECT_TRUE(quantizer1.Encode(&writer, 0, nullptr)); writer.ZeroPadToByte(); const size_t bits_written = writer.BitsWritten(); - Quantizer quantizer2(&dequant, qxsize, qysize); + Quantizer quantizer2(&dequant); BitReader reader(writer.GetSpan()); EXPECT_TRUE(quantizer2.Decode(&reader)); EXPECT_TRUE(reader.JumpToByteBoundary()); diff --git a/media/libjxl/src/lib/jxl/rational_polynomial-inl.h b/media/libjxl/src/lib/jxl/rational_polynomial-inl.h index 87bddd1bb2..176e24092c 100644 --- a/media/libjxl/src/lib/jxl/rational_polynomial-inl.h +++ b/media/libjxl/src/lib/jxl/rational_polynomial-inl.h @@ -20,6 +20,10 @@ namespace jxl { namespace HWY_NAMESPACE { namespace { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Div; +using hwy::HWY_NAMESPACE::MulAdd; + // Primary template: default to actual division. template <typename T, class V> struct FastDivision { @@ -31,14 +35,14 @@ struct FastDivision<float, V> { // One Newton-Raphson iteration. static HWY_INLINE V ReciprocalNR(const V x) { const auto rcp = ApproximateReciprocal(x); - const auto sum = rcp + rcp; - const auto x_rcp = x * rcp; + const auto sum = Add(rcp, rcp); + const auto x_rcp = Mul(x, rcp); return NegMulAdd(x_rcp, rcp, sum); } V operator()(const V n, const V d) const { #if 1 // Faster on SKX - return n / d; + return Div(n, d); #else return n * ReciprocalNR(d); #endif diff --git a/media/libjxl/src/lib/jxl/rational_polynomial_test.cc b/media/libjxl/src/lib/jxl/rational_polynomial_test.cc index e0d5a6ee21..13fc044a55 100644 --- a/media/libjxl/src/lib/jxl/rational_polynomial_test.cc +++ b/media/libjxl/src/lib/jxl/rational_polynomial_test.cc @@ -26,8 +26,11 @@ using T = float; // required by EvalLog2 using D = HWY_FULL(T); // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::GetLane; using hwy::HWY_NAMESPACE::ShiftLeft; using hwy::HWY_NAMESPACE::ShiftRight; +using hwy::HWY_NAMESPACE::Sub; // Generic: only computes polynomial struct EvalPoly { @@ -50,17 +53,17 @@ struct EvalLog2 { const HWY_FULL(int32_t) di; const auto x_bits = BitCast(di, vx); // Cannot handle negative numbers / NaN. - JXL_DASSERT(AllTrue(di, Abs(x_bits) == x_bits)); + JXL_DASSERT(AllTrue(di, Eq(Abs(x_bits), x_bits))); // Range reduction to [-1/3, 1/3] - 3 integer, 2 float ops - const auto exp_bits = x_bits - Set(di, 0x3f2aaaab); // = 2/3 + const auto exp_bits = Sub(x_bits, Set(di, 0x3f2aaaab)); // = 2/3 // Shifted exponent = log2; also used to clear mantissa. const auto exp_shifted = ShiftRight<23>(exp_bits); - const auto mantissa = BitCast(d, x_bits - ShiftLeft<23>(exp_shifted)); + const auto mantissa = BitCast(d, Sub(x_bits, ShiftLeft<23>(exp_shifted))); const auto exp_val = ConvertTo(d, exp_shifted); - vx = mantissa - Set(d, 1.0f); + vx = Sub(mantissa, Set(d, 1.0f)); - const auto approx = EvalRationalPolynomial(d, vx, p, q) + exp_val; + const auto approx = Add(EvalRationalPolynomial(d, vx, p, q), exp_val); return GetLane(approx); } }; diff --git a/media/libjxl/src/lib/jxl/render_pipeline/low_memory_render_pipeline.cc b/media/libjxl/src/lib/jxl/render_pipeline/low_memory_render_pipeline.cc index 89a636b109..91147303a6 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/low_memory_render_pipeline.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/low_memory_render_pipeline.cc @@ -397,17 +397,21 @@ void LowMemoryRenderPipeline::PrepareForThreadsInternal(size_t num, } } if (first_image_dim_stage_ != stages_.size()) { - out_of_frame_data_.resize(num); - size_t left_padding = std::max<ssize_t>(0, frame_origin_.x0); + RectT<ssize_t> image_rect(0, 0, frame_dimensions_.xsize_upsampled, + frame_dimensions_.ysize_upsampled); + RectT<ssize_t> full_image_rect(0, 0, full_image_xsize_, full_image_ysize_); + image_rect = image_rect.Translate(frame_origin_.x0, frame_origin_.y0); + image_rect = image_rect.Intersection(full_image_rect); + if (image_rect.xsize() == 0 || image_rect.ysize() == 0) { + image_rect = RectT<ssize_t>(0, 0, 0, 0); + } + size_t left_padding = image_rect.x0(); size_t middle_padding = group_dim; - ssize_t last_x = - frame_origin_.x0 + std::min(frame_dimensions_.xsize_groups * group_dim, - frame_dimensions_.xsize_upsampled); - last_x = Clamp1<ssize_t>(last_x, 0, full_image_xsize_); - size_t right_padding = full_image_xsize_ - last_x; + size_t right_padding = full_image_xsize_ - image_rect.x1(); size_t out_of_frame_xsize = padding + std::max(left_padding, std::max(middle_padding, right_padding)); + out_of_frame_data_.resize(num); for (size_t t = 0; t < num; t++) { out_of_frame_data_[t] = ImageF(out_of_frame_xsize, shifts.size()); } @@ -478,72 +482,131 @@ JXL_INLINE void ApplyXMirroring(float* row, ssize_t borderx, ssize_t group_x0, } } -} // namespace +// Information about where the *output* of each stage is stored. +class Rows { + public: + Rows(const std::vector<std::unique_ptr<RenderPipelineStage>>& stages, + const Rect data_max_color_channel_rect, int group_data_x_border, + int group_data_y_border, + const std::vector<std::pair<size_t, size_t>>& group_data_shift, + size_t base_color_shift, std::vector<std::vector<ImageF>>& thread_data, + std::vector<ImageF>& input_data) { + size_t num_stages = stages.size(); + size_t num_channels = input_data.size(); + + JXL_ASSERT(thread_data.size() == num_channels); + JXL_ASSERT(group_data_shift.size() == num_channels); + +#if JXL_ENABLE_ASSERT + for (const auto& td : thread_data) { + JXL_ASSERT(td.size() == num_stages); + } +#endif -void LowMemoryRenderPipeline::RenderRect(size_t thread_id, - std::vector<ImageF>& input_data, - Rect data_max_color_channel_rect, - Rect image_max_color_channel_rect) { - // For each stage, the rect corresponding to the image area currently being - // processed. - std::vector<Rect> group_rect; - group_rect.resize(stages_.size()); - for (size_t i = 0; i < stages_.size(); i++) { - size_t x0 = (image_max_color_channel_rect.x0() << base_color_shift_) >> - channel_shifts_[i][anyc_[i]].first; - size_t x1 = DivCeil(std::min(frame_dimensions_.xsize_upsampled, - (image_max_color_channel_rect.x0() + - image_max_color_channel_rect.xsize()) - << base_color_shift_), - 1 << channel_shifts_[i][anyc_[i]].first); - size_t y0 = (image_max_color_channel_rect.y0() << base_color_shift_) >> - channel_shifts_[i][anyc_[i]].second; - size_t y1 = DivCeil(std::min(frame_dimensions_.ysize_upsampled, - (image_max_color_channel_rect.y0() + - image_max_color_channel_rect.ysize()) - << base_color_shift_), - 1 << channel_shifts_[i][anyc_[i]].second); - group_rect[i] = Rect(x0, y0, x1 - x0, y1 - y0); + rows_.resize(num_stages + 1, std::vector<RowInfo>(num_channels)); + + for (size_t i = 0; i < num_stages; i++) { + for (size_t c = 0; c < input_data.size(); c++) { + if (stages[i]->GetChannelMode(c) == RenderPipelineChannelMode::kInOut) { + rows_[i + 1][c].ymod_minus_1 = thread_data[c][i].ysize() - 1; + rows_[i + 1][c].base_ptr = thread_data[c][i].Row(0); + rows_[i + 1][c].stride = thread_data[c][i].PixelsPerRow(); + } + } + } + + for (size_t c = 0; c < input_data.size(); c++) { + auto channel_group_data_rect = + data_max_color_channel_rect.As<ssize_t>() + .Translate(-group_data_x_border, -group_data_y_border) + .ShiftLeft(base_color_shift) + .CeilShiftRight(group_data_shift[c]) + .Translate(group_data_x_border - ssize_t(kRenderPipelineXOffset), + group_data_y_border); + rows_[0][c].base_ptr = channel_group_data_rect.Row(&input_data[c], 0); + rows_[0][c].stride = input_data[c].PixelsPerRow(); + rows_[0][c].ymod_minus_1 = -1; + } + } + + // Stage -1 refers to the input data; all other values must be nonnegative and + // refer to the data for the output of that stage. + JXL_INLINE float* GetBuffer(int stage, int y, size_t c) const { + JXL_DASSERT(stage >= -1); + const RowInfo& info = rows_[stage + 1][c]; + return info.base_ptr + ssize_t(info.stride) * (y & info.ymod_minus_1); } + private: struct RowInfo { + // Pointer to beginning of the first row. float* base_ptr; + // Modulo value for the y axis minus 1 (ymod is guaranteed to be a power of + // 2, which allows efficient mod computation by masking). int ymod_minus_1; + // Number of floats per row. size_t stride; }; + std::vector<std::vector<RowInfo>> rows_; +}; - std::vector<std::vector<RowInfo>> rows( - stages_.size() + 1, std::vector<RowInfo>(input_data.size())); +} // namespace +void LowMemoryRenderPipeline::RenderRect(size_t thread_id, + std::vector<ImageF>& input_data, + Rect data_max_color_channel_rect, + Rect image_max_color_channel_rect) { + // For each stage, the rect corresponding to the image area currently being + // processed, in the coordinates of that stage (i.e. with the scaling factor + // that that stage has). + std::vector<Rect> group_rect; + group_rect.resize(stages_.size()); + Rect image_area_rect = + image_max_color_channel_rect.ShiftLeft(base_color_shift_) + .Crop(frame_dimensions_.xsize_upsampled, + frame_dimensions_.ysize_upsampled); for (size_t i = 0; i < stages_.size(); i++) { - for (size_t c = 0; c < input_data.size(); c++) { - if (stages_[i]->GetChannelMode(c) == RenderPipelineChannelMode::kInOut) { - rows[i + 1][c].ymod_minus_1 = stage_data_[thread_id][c][i].ysize() - 1; - rows[i + 1][c].base_ptr = stage_data_[thread_id][c][i].Row(0); - rows[i + 1][c].stride = stage_data_[thread_id][c][i].PixelsPerRow(); - } - } - } - - for (size_t c = 0; c < input_data.size(); c++) { - int xoff = int(data_max_color_channel_rect.x0()) - - int(LowMemoryRenderPipeline::group_data_x_border_); - xoff = xoff * (1 << base_color_shift_) >> channel_shifts_[0][c].first; - xoff += LowMemoryRenderPipeline::group_data_x_border_; - int yoff = int(data_max_color_channel_rect.y0()) - - int(LowMemoryRenderPipeline::group_data_y_border_); - yoff = yoff * (1 << base_color_shift_) >> channel_shifts_[0][c].second; - yoff += LowMemoryRenderPipeline::group_data_y_border_; - rows[0][c].base_ptr = - input_data[c].Row(yoff) + xoff - kRenderPipelineXOffset; - rows[0][c].stride = input_data[c].PixelsPerRow(); - rows[0][c].ymod_minus_1 = -1; - } - - auto get_row_buffer = [&](int stage, int y, size_t c) { - const RowInfo& info = rows[stage + 1][c]; - return info.base_ptr + ssize_t(info.stride) * (y & info.ymod_minus_1); - }; + group_rect[i] = + image_area_rect.CeilShiftRight(channel_shifts_[i][anyc_[i]]); + } + + ssize_t frame_x0 = + first_image_dim_stage_ == stages_.size() ? 0 : frame_origin_.x0; + ssize_t frame_y0 = + first_image_dim_stage_ == stages_.size() ? 0 : frame_origin_.y0; + size_t full_image_xsize = first_image_dim_stage_ == stages_.size() + ? frame_dimensions_.xsize_upsampled + : full_image_xsize_; + size_t full_image_ysize = first_image_dim_stage_ == stages_.size() + ? frame_dimensions_.ysize_upsampled + : full_image_ysize_; + + // Compute actual x-axis bounds for the current image area in the context of + // the full image this frame is part of. As the left boundary may be negative, + // we also create the x_pixels_skip value, defined as follows: + // - both x_pixels_skip and full_image_x0 are >= 0, and at least one is 0; + // - full_image_x0 - x_pixels_skip is the position of the current frame area + // in the full image. + ssize_t full_image_x0 = frame_x0 + image_area_rect.x0(); + ssize_t x_pixels_skip = 0; + if (full_image_x0 < 0) { + x_pixels_skip = -full_image_x0; + full_image_x0 = 0; + } + ssize_t full_image_x1 = frame_x0 + image_area_rect.x1(); + full_image_x1 = std::min<ssize_t>(full_image_x1, full_image_xsize); + + // If the current image area is entirely outside of the visible image, there + // is no point in proceeding. Note: this uses the assumption that if there is + // a stage with observable effects (i.e. a kInput stage), it only appears + // after the stage that switches to image dimensions. + if (full_image_x1 <= full_image_x0) return; + + // Data structures to hold information about input/output rows and their + // buffers. + Rows rows(stages_, data_max_color_channel_rect, group_data_x_border_, + group_data_y_border_, channel_shifts_[0], base_color_shift_, + stage_data_[thread_id], input_data); std::vector<RenderPipelineStage::RowInfo> input_rows(first_trailing_stage_ + 1); @@ -557,6 +620,51 @@ void LowMemoryRenderPipeline::RenderRect(size_t thread_id, RenderPipelineStage::RowInfo output_rows(input_data.size(), std::vector<float*>(8)); + // Fills in input_rows and output_rows for a given y value (relative to the + // start of the group, measured in actual pixels at the appropriate vertical + // scaling factor) and a given stage, applying mirroring if necessary. This + // function is somewhat inefficient for trailing kInOut or kInput stages, + // where just filling the input row once ought to be sufficient. + auto prepare_io_rows = [&](int y, size_t i) { + ssize_t bordery = stages_[i]->settings_.border_y; + size_t shifty = stages_[i]->settings_.shift_y; + auto make_row = [&](size_t c, ssize_t iy) { + size_t mirrored_y = GetMirroredY(y + iy - bordery, group_rect[i].y0(), + image_rect_[i].ysize()); + input_rows[i][c][iy] = + rows.GetBuffer(stage_input_for_channel_[i][c], mirrored_y, c); + ApplyXMirroring(input_rows[i][c][iy], stages_[i]->settings_.border_x, + group_rect[i].x0(), group_rect[i].xsize(), + image_rect_[i].xsize()); + }; + for (size_t c = 0; c < input_data.size(); c++) { + RenderPipelineChannelMode mode = stages_[i]->GetChannelMode(c); + if (mode == RenderPipelineChannelMode::kIgnored) { + continue; + } + // If we already have rows from a previous iteration, we can just shift + // the rows by 1 and insert the new one. + if (input_rows[i][c].size() == 2 * size_t(bordery) + 1) { + for (ssize_t iy = 0; iy < 2 * bordery; iy++) { + input_rows[i][c][iy] = input_rows[i][c][iy + 1]; + } + make_row(c, bordery * 2); + } else { + input_rows[i][c].resize(2 * bordery + 1); + for (ssize_t iy = 0; iy < 2 * bordery + 1; iy++) { + make_row(c, iy); + } + } + + // If necessary, get the output buffers. + if (mode == RenderPipelineChannelMode::kInOut) { + for (size_t iy = 0; iy < (1u << shifty); iy++) { + output_rows[c][iy] = rows.GetBuffer(i, y * (1 << shifty) + iy, c); + } + } + } + }; + // We pretend that every stage has a vertical shift of 0, i.e. it is as tall // as the final image. // We call each such row a "virtual" row, because it may or may not correspond @@ -567,10 +675,9 @@ void LowMemoryRenderPipeline::RenderRect(size_t thread_id, virtual_ypadding_for_output_.end()); for (int vy = -num_extra_rows; - vy < int(group_rect.back().ysize()) + num_extra_rows; vy++) { + vy < int(image_area_rect.ysize()) + num_extra_rows; vy++) { for (size_t i = 0; i < first_trailing_stage_; i++) { - int virtual_y_offset = num_extra_rows - virtual_ypadding_for_output_[i]; - int stage_vy = vy - virtual_y_offset; + int stage_vy = vy - num_extra_rows + virtual_ypadding_for_output_[i]; if (stage_vy % (1 << channel_shifts_[i][anyc_[i]].second) != 0) { continue; @@ -580,8 +687,6 @@ void LowMemoryRenderPipeline::RenderRect(size_t thread_id, continue; } - ssize_t bordery = stages_[i]->settings_.border_y; - size_t shifty = stages_[i]->settings_.shift_y; int y = stage_vy >> channel_shifts_[i][anyc_[i]].second; ssize_t image_y = ssize_t(group_rect[i].y0()) + y; @@ -590,96 +695,54 @@ void LowMemoryRenderPipeline::RenderRect(size_t thread_id, continue; } - // Get the actual input rows and potentially apply mirroring. - for (size_t c = 0; c < input_data.size(); c++) { - RenderPipelineChannelMode mode = stages_[i]->GetChannelMode(c); - if (mode == RenderPipelineChannelMode::kIgnored) { - continue; - } - auto make_row = [&](ssize_t iy) { - size_t mirrored_y = GetMirroredY(y + iy - bordery, group_rect[i].y0(), - image_rect_[i].ysize()); - input_rows[i][c][iy] = - get_row_buffer(stage_input_for_channel_[i][c], mirrored_y, c); - ApplyXMirroring(input_rows[i][c][iy], stages_[i]->settings_.border_x, - group_rect[i].x0(), group_rect[i].xsize(), - image_rect_[i].xsize()); - }; - // If we already have rows from a previous iteration, we can just shift - // the rows by 1 and insert the new one. - if (input_rows[i][c].size() == 2 * size_t(bordery) + 1) { - for (ssize_t iy = 0; iy < 2 * bordery; iy++) { - input_rows[i][c][iy] = input_rows[i][c][iy + 1]; - } - make_row(bordery * 2); - } else { - input_rows[i][c].resize(2 * bordery + 1); - for (ssize_t iy = 0; iy < 2 * bordery + 1; iy++) { - make_row(iy); - } - } + // Get the input/output rows and potentially apply mirroring to the input. + prepare_io_rows(y, i); - // If necessary, get the output buffers. - if (mode == RenderPipelineChannelMode::kInOut) { - for (size_t iy = 0; iy < (1u << shifty); iy++) { - output_rows[c][iy] = get_row_buffer(i, y * (1 << shifty) + iy, c); - } - } - } // Produce output rows. stages_[i]->ProcessRow(input_rows[i], output_rows, xpadding_for_output_[i], group_rect[i].xsize(), - group_rect[i].x0(), group_rect[i].y0() + y, - thread_id); + group_rect[i].x0(), image_y, thread_id); } // Process trailing stages, i.e. the final set of non-kInOut stages; they // all have the same input buffer and no need to use any mirroring. int y = vy - num_extra_rows; - if (y < 0 || y >= ssize_t(frame_dimensions_.ysize_upsampled)) continue; for (size_t c = 0; c < input_data.size(); c++) { - input_rows[first_trailing_stage_][c][0] = get_row_buffer( - stage_input_for_channel_[first_trailing_stage_][c], y, c); - } - - for (size_t i = first_trailing_stage_; i < first_image_dim_stage_; i++) { - stages_[i]->ProcessRow(input_rows[first_trailing_stage_], output_rows, - /*xextra=*/0, group_rect[i].xsize(), - group_rect[i].x0(), group_rect[i].y0() + y, - thread_id); + // Skip pixels that are not part of the actual final image area. + input_rows[first_trailing_stage_][c][0] = + rows.GetBuffer(stage_input_for_channel_[first_trailing_stage_][c], y, + c) + + x_pixels_skip; } - if (first_image_dim_stage_ == stages_.size()) continue; - - ssize_t full_image_y = - y + frame_origin_.y0 + group_rect[first_image_dim_stage_].y0(); - if (full_image_y < 0 || full_image_y >= ssize_t(full_image_ysize_)) { + // Check that we are not outside of the bounds for the current rendering + // rect. Not doing so might result in overwriting some rows that have been + // written (or will be written) by other threads. + if (y < 0 || y >= ssize_t(image_area_rect.ysize())) { continue; } - ssize_t full_image_x0 = - frame_origin_.x0 + group_rect[first_image_dim_stage_].x0(); - - if (full_image_x0 < 0) { - // Skip pixels. - for (size_t c = 0; c < input_data.size(); c++) { - input_rows[first_trailing_stage_][c][0] -= full_image_x0; - } - full_image_x0 = 0; + // Avoid running pipeline stages on pixels that are outside the full image + // area. As trailing stages have no borders, this is a free optimization + // (and may be necessary for correctness, as some stages assume coordinates + // are within bounds). + ssize_t full_image_y = frame_y0 + image_area_rect.y0() + y; + if (full_image_y < 0 || full_image_y >= ssize_t(full_image_ysize)) { + continue; } - ssize_t full_image_x1 = frame_origin_.x0 + - group_rect[first_image_dim_stage_].x0() + - group_rect[first_image_dim_stage_].xsize(); - full_image_x1 = std::min<ssize_t>(full_image_x1, full_image_xsize_); - if (full_image_x1 <= full_image_x0) continue; - - for (size_t i = first_image_dim_stage_; i < stages_.size(); i++) { + for (size_t i = first_trailing_stage_; i < stages_.size(); i++) { + // Before the first_image_dim_stage_, coordinates are relative to the + // current frame. + size_t x0 = + i < first_image_dim_stage_ ? full_image_x0 - frame_x0 : full_image_x0; + size_t y = + i < first_image_dim_stage_ ? full_image_y - frame_y0 : full_image_y; stages_[i]->ProcessRow(input_rows[first_trailing_stage_], output_rows, - /*xextra=*/0, full_image_x1 - full_image_x0, - full_image_x0, full_image_y, thread_id); + /*xextra=*/0, full_image_x1 - full_image_x0, x0, y, + thread_id); } } } @@ -727,15 +790,27 @@ void LowMemoryRenderPipeline::ProcessBuffers(size_t group_id, RectT<ssize_t> full_image_rect(0, 0, full_image_xsize_, full_image_ysize_); group_rect = group_rect.Translate(frame_origin_.x0, frame_origin_.y0); image_rect = image_rect.Translate(frame_origin_.x0, frame_origin_.y0); - group_rect = - group_rect.Intersection(image_rect).Intersection(full_image_rect); + image_rect = image_rect.Intersection(full_image_rect); + group_rect = group_rect.Intersection(image_rect); size_t x0 = group_rect.x0(); size_t y0 = group_rect.y0(); size_t x1 = group_rect.x1(); size_t y1 = group_rect.y1(); + JXL_DEBUG_V(6, + "Rendering padding for full image rect %s " + "outside group rect %s", + Description(full_image_rect).c_str(), + Description(group_rect).c_str()); + + if (group_id == 0 && (image_rect.xsize() == 0 || image_rect.ysize() == 0)) { + // If this frame does not intersect with the full image, we have to + // initialize the whole image area with RenderPadding. + RenderPadding(thread_id, + Rect(0, 0, full_image_xsize_, full_image_ysize_)); + } - // Do not render padding if group is empty; if group is empty x0, y0 might - // have arbitrary values (from frame_origin). + // Render padding for groups that intersect with the full image. The case + // where no groups intersect was handled above. if (group_rect.xsize() > 0 && group_rect.ysize() > 0) { if (gx == 0 && gy == 0) { RenderPadding(thread_id, Rect(0, 0, x0, y0)); diff --git a/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline.cc b/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline.cc index 0b50bbb311..68b6ef613f 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline.cc @@ -108,9 +108,9 @@ void RenderPipeline::InputReady( const std::vector<std::pair<ImageF*, Rect>>& buffers) { JXL_DASSERT(group_id < group_completed_passes_.size()); group_completed_passes_[group_id]++; - for (const auto& buf : buffers) { - (void)buf; - JXL_CHECK_IMAGE_INITIALIZED(*buf.first, buf.second); + for (size_t i = 0; i < buffers.size(); ++i) { + (void)i; + JXL_CHECK_PLANE_INITIALIZED(*buffers[i].first, buffers[i].second, i); } ProcessBuffers(group_id, thread_id); diff --git a/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline_stage.h b/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline_stage.h index dd99ba9f33..d1a0074161 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline_stage.h +++ b/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline_stage.h @@ -31,7 +31,10 @@ enum class RenderPipelineChannelMode { kInPlace = 1, // This channel is modified and written to a new buffer. kInOut = 2, - // This channel is only read. + // This channel is only read. These are the only stages that are assumed to + // have observable effects, i.e. calls to ProcessRow for other stages may be + // omitted if it can be shown they can't affect any kInput stage ProcessRow + // call that happens inside image boundaries. kInput = 3, }; @@ -100,6 +103,10 @@ class RenderPipelineStage { size_t xextra, size_t xsize, size_t xpos, size_t ypos, size_t thread_id) const = 0; + // How each channel will be processed. Channels are numbered starting from + // color channels (always 3) and followed by all other channels. + virtual RenderPipelineChannelMode GetChannelMode(size_t c) const = 0; + protected: explicit RenderPipelineStage(Settings settings) : settings_(settings) {} @@ -112,10 +119,6 @@ class RenderPipelineStage { virtual Status PrepareForThreads(size_t num_threads) { return true; } - // How each channel will be processed. Channels are numbered starting from - // color channels (always 3) and followed by all other channels. - virtual RenderPipelineChannelMode GetChannelMode(size_t c) const = 0; - // Returns a pointer to the input row of channel `c` with offset `y`. // `y` must be in [-settings_.border_y, settings_.border_y]. `c` must be such // that `GetChannelMode(c) != kIgnored`. The returned pointer points to the @@ -140,6 +143,8 @@ class RenderPipelineStage { // Indicates whether, from this stage on, the pipeline will operate on an // image- rather than frame-sized buffer. Only one stage in the pipeline // should return true, and it should implement ProcessPaddingRow below too. + // It is assumed that, if there is a SwitchToImageDimensions() == true stage, + // all kInput stages appear after it. virtual bool SwitchToImageDimensions() const { return false; } // If SwitchToImageDimensions returns true, then this should set xsize and diff --git a/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline_test.cc b/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline_test.cc index aee171e049..3cece172e2 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline_test.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/render_pipeline_test.cc @@ -14,8 +14,11 @@ #include "gtest/gtest.h" #include "lib/extras/codec.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/dec_frame.h" #include "lib/jxl/enc_params.h" #include "lib/jxl/fake_parallel_runner_testonly.h" +#include "lib/jxl/icc_codec.h" #include "lib/jxl/image_test_utils.h" #include "lib/jxl/jpeg/enc_jpeg_data.h" #include "lib/jxl/render_pipeline/test_render_pipeline_stages.h" @@ -25,6 +28,61 @@ namespace jxl { namespace { +Status DecodeFile(const Span<const uint8_t> file, bool use_slow_pipeline, + CodecInOut* io, ThreadPool* pool) { + Status ret = true; + { + BitReader reader(file); + BitReaderScopedCloser reader_closer(&reader, &ret); + JXL_RETURN_IF_ERROR(reader.ReadFixedBits<16>() == 0x0AFF); + JXL_RETURN_IF_ERROR(ReadSizeHeader(&reader, &io->metadata.size)); + JXL_RETURN_IF_ERROR(ReadImageMetadata(&reader, &io->metadata.m)); + io->metadata.transform_data.nonserialized_xyb_encoded = + io->metadata.m.xyb_encoded; + JXL_RETURN_IF_ERROR(Bundle::Read(&reader, &io->metadata.transform_data)); + size_t xsize = io->metadata.xsize(); + size_t ysize = io->metadata.ysize(); + JXL_RETURN_IF_ERROR(VerifyDimensions(&io->constraints, xsize, ysize)); + if (io->metadata.m.color_encoding.WantICC()) { + PaddedBytes icc; + JXL_RETURN_IF_ERROR(ReadICC(&reader, &icc)); + JXL_RETURN_IF_ERROR(io->metadata.m.color_encoding.SetICC(std::move(icc))); + } + PassesDecoderState dec_state; + JXL_RETURN_IF_ERROR( + dec_state.output_encoding_info.SetFromMetadata(io->metadata)); + JXL_RETURN_IF_ERROR(reader.JumpToByteBoundary()); + io->frames.clear(); + do { + io->frames.emplace_back(&io->metadata.m); + // Skip frames that are not displayed. + do { + size_t frame_start = reader.TotalBitsConsumed() / kBitsPerByte; + size_t size_left = file.size() - frame_start; + JXL_RETURN_IF_ERROR( + DecodeFrame(&dec_state, pool, file.data() + frame_start, size_left, + &io->frames.back(), io->metadata, use_slow_pipeline)); + reader.SkipBits(io->frames.back().decoded_bytes() * kBitsPerByte); + } while (dec_state.shared->frame_header.frame_type != + FrameType::kRegularFrame && + dec_state.shared->frame_header.frame_type != + FrameType::kSkipProgressive); + } while (!dec_state.shared->frame_header.is_last); + + if (io->frames.empty()) return JXL_FAILURE("Not enough data."); + + if (reader.TotalBitsConsumed() != file.size() * kBitsPerByte) { + return JXL_FAILURE("Reader position not at EOF."); + } + if (!reader.AllReadsWithinBounds()) { + return JXL_FAILURE("Reader out of bounds read."); + } + io->CheckMetadata(); + // reader is closed here. + } + return ret; +} + TEST(RenderPipelineTest, Build) { RenderPipeline::Builder builder(/*num_c=*/1); builder.AddStage(jxl::make_unique<UpsampleXSlowStage>()); @@ -163,15 +221,13 @@ TEST_P(RenderPipelineTestParam, PipelineTest) { ASSERT_TRUE(EncodeFile(config.cparams, &io, &enc_state, &compressed, GetJxlCms(), /*aux_out=*/nullptr, &pool)); - DecompressParams dparams; - - dparams.render_spotcolors = true; CodecInOut io_default; - ASSERT_TRUE(DecodeFile(dparams, compressed, &io_default, &pool)); + ASSERT_TRUE(DecodeFile(Span<const uint8_t>(compressed), + /*use_slow_pipeline=*/false, &io_default, &pool)); CodecInOut io_slow_pipeline; - dparams.use_slow_render_pipeline = true; - ASSERT_TRUE(DecodeFile(dparams, compressed, &io_slow_pipeline, &pool)); + ASSERT_TRUE(DecodeFile(Span<const uint8_t>(compressed), + /*use_slow_pipeline=*/true, &io_slow_pipeline, &pool)); ASSERT_EQ(io_default.frames.size(), io_slow_pipeline.frames.size()); for (size_t i = 0; i < io_default.frames.size(); i++) { @@ -222,7 +278,7 @@ std::vector<RenderPipelineTestInputSettings> GeneratePipelineTests() { for (auto size : sizes) { RenderPipelineTestInputSettings settings; - settings.input_path = "third_party/imagecompression.info/flower_foveon.png"; + settings.input_path = "jxl/flower/flower.png"; settings.xsize = size.first; settings.ysize = size.second; @@ -354,16 +410,14 @@ std::vector<RenderPipelineTestInputSettings> GeneratePipelineTests() { { auto s = settings; - s.input_path = - "third_party/imagecompression.info/flower_foveon_alpha.png"; + s.input_path = "jxl/flower/flower_alpha.png"; s.cparams_descr = "AlphaVarDCT"; all_tests.push_back(s); } { auto s = settings; - s.input_path = - "third_party/imagecompression.info/flower_foveon_alpha.png"; + s.input_path = "jxl/flower/flower_alpha.png"; s.cparams_descr = "AlphaVarDCTUpsamplingEPF"; s.cparams.epf = 1; s.cparams.ec_resampling = 2; @@ -374,16 +428,14 @@ std::vector<RenderPipelineTestInputSettings> GeneratePipelineTests() { auto s = settings; s.cparams.modular_mode = true; s.cparams.butteraugli_distance = 0; - s.input_path = - "third_party/imagecompression.info/flower_foveon_alpha.png"; + s.input_path = "jxl/flower/flower_alpha.png"; s.cparams_descr = "AlphaLossless"; all_tests.push_back(s); } { auto s = settings; - s.input_path = - "third_party/imagecompression.info/flower_foveon_alpha.png"; + s.input_path = "jxl/flower/flower_alpha.png"; s.cparams_descr = "AlphaDownsample"; s.cparams.ec_resampling = 2; all_tests.push_back(s); @@ -398,11 +450,10 @@ std::vector<RenderPipelineTestInputSettings> GeneratePipelineTests() { } #if JPEGXL_ENABLE_TRANSCODE_JPEG - for (const char* input : - {"third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg", - "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg", - "third_party/imagecompression.info/flower_foveon.png.im_q85_422.jpg", - "third_party/imagecompression.info/flower_foveon.png.im_q85_440.jpg"}) { + for (const char* input : {"jxl/flower/flower.png.im_q85_444.jpg", + "jxl/flower/flower.png.im_q85_420.jpg", + "jxl/flower/flower.png.im_q85_422.jpg", + "jxl/flower/flower.png.im_q85_440.jpg"}) { RenderPipelineTestInputSettings settings; settings.input_path = input; settings.jpeg_transcode = true; @@ -423,6 +474,26 @@ std::vector<RenderPipelineTestInputSettings> GeneratePipelineTests() { all_tests.push_back(settings); } + { + RenderPipelineTestInputSettings settings; + settings.input_path = "jxl/grayscale_patches.png"; + settings.xsize = 1011; + settings.ysize = 277; + settings.cparams.photon_noise_iso = 1000; + settings.cparams_descr = "PatchesAndNoise"; + all_tests.push_back(settings); + } + + { + RenderPipelineTestInputSettings settings; + settings.input_path = "jxl/grayscale_patches.png"; + settings.xsize = 1011; + settings.ysize = 277; + settings.cparams.resampling = 2; + settings.cparams_descr = "PatchesAndUps2"; + all_tests.push_back(settings); + } + return all_tests; } @@ -461,12 +532,12 @@ TEST(RenderPipelineDecodingTest, Animation) { PaddedBytes compressed = ReadTestData("jxl/blending/cropped_traffic_light.jxl"); - DecompressParams dparams; CodecInOut io_default; - ASSERT_TRUE(DecodeFile(dparams, compressed, &io_default, &pool)); + ASSERT_TRUE(DecodeFile(Span<const uint8_t>(compressed), + /*use_slow_pipeline=*/false, &io_default, &pool)); CodecInOut io_slow_pipeline; - dparams.use_slow_render_pipeline = true; - ASSERT_TRUE(DecodeFile(dparams, compressed, &io_slow_pipeline, &pool)); + ASSERT_TRUE(DecodeFile(Span<const uint8_t>(compressed), + /*use_slow_pipeline=*/true, &io_slow_pipeline, &pool)); ASSERT_EQ(io_default.frames.size(), io_slow_pipeline.frames.size()); for (size_t i = 0; i < io_default.frames.size(); i++) { diff --git a/media/libjxl/src/lib/jxl/render_pipeline/simple_render_pipeline.cc b/media/libjxl/src/lib/jxl/render_pipeline/simple_render_pipeline.cc index 9e4e90fa2f..6e6bcb764d 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/simple_render_pipeline.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/simple_render_pipeline.cc @@ -63,7 +63,7 @@ void SimpleRenderPipeline::ProcessBuffers(size_t group_id, size_t thread_id) { for (size_t c = 0; c < channel_data_.size(); c++) { Rect r = MakeChannelRect(group_id, c); (void)r; - JXL_CHECK_IMAGE_INITIALIZED(channel_data_[c], r); + JXL_CHECK_PLANE_INITIALIZED(channel_data_[c], r, c); } if (PassesWithAllInput() <= processed_passes_) return; @@ -202,9 +202,10 @@ void SimpleRenderPipeline::ProcessBuffers(size_t group_id, size_t thread_id) { 1 << channel_shifts_[next_stage][c].second); channel_data_[c].ShrinkTo(xsize + 2 * kRenderPipelineXOffset, ysize + 2 * kRenderPipelineXOffset); - JXL_CHECK_IMAGE_INITIALIZED( + JXL_CHECK_PLANE_INITIALIZED( channel_data_[c], - Rect(kRenderPipelineXOffset, kRenderPipelineXOffset, xsize, ysize)); + Rect(kRenderPipelineXOffset, kRenderPipelineXOffset, xsize, ysize), + c); } if (stage->SwitchToImageDimensions()) { diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_chroma_upsampling.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_chroma_upsampling.cc index 35dfded46e..9b73ee91f1 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_chroma_upsampling.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_chroma_upsampling.cc @@ -16,6 +16,10 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; + class HorizontalChromaUpsamplingStage : public RenderPipelineStage { public: explicit HorizontalChromaUpsamplingStage(size_t channel) @@ -35,7 +39,7 @@ class HorizontalChromaUpsamplingStage : public RenderPipelineStage { float* row_out = GetOutputRow(output_rows, c_, 0); for (ssize_t x = -xextra; x < static_cast<ssize_t>(xsize + xextra); x += Lanes(df)) { - auto current = LoadU(df, row_in + x) * threefour; + auto current = Mul(LoadU(df, row_in + x), threefour); auto prev = LoadU(df, row_in + x - 1); auto next = LoadU(df, row_in + x + 1); auto left = MulAdd(onefour, prev, current); @@ -80,7 +84,7 @@ class VerticalChromaUpsamplingStage : public RenderPipelineStage { auto it = LoadU(df, row_top + x); auto im = LoadU(df, row_mid + x); auto ib = LoadU(df, row_bot + x); - auto im_scaled = im * threefour; + auto im_scaled = Mul(im, threefour); Store(MulAdd(it, onefour, im_scaled), df, row_out0 + x); Store(MulAdd(ib, onefour, im_scaled), df, row_out1 + x); } diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_epf.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_epf.cc index adb9c1fb9c..d59c497843 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_epf.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_epf.cc @@ -21,7 +21,14 @@ namespace HWY_NAMESPACE { using DF = HWY_CAPPED(float, 8); // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::AbsDiff; +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Div; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; using hwy::HWY_NAMESPACE::Vec; +using hwy::HWY_NAMESPACE::VFromD; +using hwy::HWY_NAMESPACE::ZeroIfNegative; JXL_INLINE Vec<DF> Weight(Vec<DF> sad, Vec<DF> inv_sigma, Vec<DF> thres) { auto v = MulAdd(sad, inv_sigma, Set(DF(), 1.0f)); @@ -52,7 +59,7 @@ class EPF0Stage : public RenderPipelineStage { : LoadU(DF(), rows[2][3 + row] + x); auto weight = Weight(sad, inv_sigma, Set(DF(), lf_.epf_pass1_zeroflush)); - *w += weight; + *w = Add(*w, weight); *X = MulAdd(weight, cx, *X); *Y = MulAdd(weight, cy, *Y); *B = MulAdd(weight, cb, *B); @@ -62,6 +69,11 @@ class EPF0Stage : public RenderPipelineStage { size_t xextra, size_t xsize, size_t xpos, size_t ypos, size_t thread_id) const final { DF df; + + using V = decltype(Zero(df)); + V t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, tA, tB; + V* sads[12] = {&t0, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9, &tA, &tB}; + xextra = RoundUpTo(xextra, Lanes(df)); const float* JXL_RESTRICT row_sigma = sigma_->Row(ypos / kBlockDim + kSigmaPadding); @@ -93,16 +105,15 @@ class EPF0Stage : public RenderPipelineStage { if (row_sigma[bx] < kMinSigma) { for (size_t c = 0; c < 3; c++) { auto px = Load(df, rows[c][3 + 0] + x); - Store(px, df, GetOutputRow(output_rows, c, 0) + x); + StoreU(px, df, GetOutputRow(output_rows, c, 0) + x); } continue; } const auto sm = Load(df, sad_mul + ix); - const auto inv_sigma = Set(df, row_sigma[bx]) * sm; + const auto inv_sigma = Mul(Set(df, row_sigma[bx]), sm); - decltype(Zero(df)) sads[12]; - for (size_t i = 0; i < 12; i++) sads[i] = Zero(df); + for (size_t i = 0; i < 12; i++) *sads[i] = Zero(df); constexpr std::array<int, 2> sads_off[12] = { {{-2, 0}}, {{-1, -1}}, {{-1, 0}}, {{-1, 1}}, {{0, -2}}, {{0, -1}}, {{0, 1}}, {{0, 2}}, {{1, -1}}, {{1, 0}}, {{1, 1}}, {{2, 0}}, @@ -122,9 +133,9 @@ class EPF0Stage : public RenderPipelineStage { const auto c11 = LoadU(df, rows[c][3 + sads_off[i][0] + plus_off[j][0]] + x + sads_off[i][1] + plus_off[j][1]); - sad += AbsDiff(r11, c11); + sad = Add(sad, AbsDiff(r11, c11)); } - sads[i] = MulAdd(sad, scale, sads[i]); + *sads[i] = MulAdd(sad, scale, *sads[i]); } } const auto x_cc = Load(df, rows[0][3 + 0] + x); @@ -138,17 +149,17 @@ class EPF0Stage : public RenderPipelineStage { for (size_t i = 0; i < 12; i++) { AddPixel</*aligned=*/false>(/*row=*/sads_off[i][0], rows, - x + sads_off[i][1], sads[i], inv_sigma, &X, + x + sads_off[i][1], *sads[i], inv_sigma, &X, &Y, &B, &w); } #if JXL_HIGH_PRECISION - auto inv_w = Set(df, 1.0f) / w; + auto inv_w = Div(Set(df, 1.0f), w); #else auto inv_w = ApproximateReciprocal(w); #endif - Store(X * inv_w, df, GetOutputRow(output_rows, 0, 0) + x); - Store(Y * inv_w, df, GetOutputRow(output_rows, 1, 0) + x); - Store(B * inv_w, df, GetOutputRow(output_rows, 2, 0) + x); + StoreU(Mul(X, inv_w), df, GetOutputRow(output_rows, 0, 0) + x); + StoreU(Mul(Y, inv_w), df, GetOutputRow(output_rows, 1, 0) + x); + StoreU(Mul(B, inv_w), df, GetOutputRow(output_rows, 2, 0) + x); } } @@ -188,7 +199,7 @@ class EPF1Stage : public RenderPipelineStage { : LoadU(DF(), rows[2][2 + row] + x); auto weight = Weight(sad, inv_sigma, Set(DF(), lf_.epf_pass1_zeroflush)); - *w += weight; + *w = Add(*w, weight); *X = MulAdd(weight, cx, *X); *Y = MulAdd(weight, cy, *Y); *B = MulAdd(weight, cb, *B); @@ -236,7 +247,7 @@ class EPF1Stage : public RenderPipelineStage { } const auto sm = Load(df, sad_mul + ix); - const auto inv_sigma = Set(df, row_sigma[bx]) * sm; + const auto inv_sigma = Mul(Set(df, row_sigma[bx]), sm); auto sad0 = Zero(df); auto sad1 = Zero(df); auto sad2 = Zero(df); @@ -259,41 +270,41 @@ class EPF1Stage : public RenderPipelineStage { const auto p02 = LoadU(df, rows[c][2 + 0] + x - 2); const auto p12 = LoadU(df, rows[c][2 + 0] + x - 1); - sad1c += AbsDiff(p02, p12); // SAD 1, 2 - sad0c += AbsDiff(p11, p12); // SAD 2, 1 + sad1c = Add(sad1c, AbsDiff(p02, p12)); // SAD 1, 2 + sad0c = Add(sad0c, AbsDiff(p11, p12)); // SAD 2, 1 const auto p22 = LoadU(df, rows[c][2 + 0] + x); t = AbsDiff(p12, p22); - sad1c += t; // SAD 1, 2 - sad2c += t; // SAD 3, 2 + sad1c = Add(sad1c, t); // SAD 1, 2 + sad2c = Add(sad2c, t); // SAD 3, 2 t = AbsDiff(p22, p21); auto sad3c = t; // SAD 2, 3 - sad0c += t; // SAD 2, 1 + sad0c = Add(sad0c, t); // SAD 2, 1 const auto p32 = LoadU(df, rows[c][2 + 0] + x + 1); - sad0c += AbsDiff(p31, p32); // SAD 2, 1 + sad0c = Add(sad0c, AbsDiff(p31, p32)); // SAD 2, 1 t = AbsDiff(p22, p32); - sad1c += t; // SAD 1, 2 - sad2c += t; // SAD 3, 2 + sad1c = Add(sad1c, t); // SAD 1, 2 + sad2c = Add(sad2c, t); // SAD 3, 2 const auto p42 = LoadU(df, rows[c][2 + 0] + x + 2); - sad2c += AbsDiff(p42, p32); // SAD 3, 2 + sad2c = Add(sad2c, AbsDiff(p42, p32)); // SAD 3, 2 const auto p13 = LoadU(df, rows[c][2 + 1] + x - 1); - sad3c += AbsDiff(p13, p12); // SAD 2, 3 + sad3c = Add(sad3c, AbsDiff(p13, p12)); // SAD 2, 3 const auto p23 = Load(df, rows[c][2 + 1] + x); t = AbsDiff(p22, p23); - sad0c += t; // SAD 2, 1 - sad3c += t; // SAD 2, 3 - sad1c += AbsDiff(p13, p23); // SAD 1, 2 + sad0c = Add(sad0c, t); // SAD 2, 1 + sad3c = Add(sad3c, t); // SAD 2, 3 + sad1c = Add(sad1c, AbsDiff(p13, p23)); // SAD 1, 2 const auto p33 = LoadU(df, rows[c][2 + 1] + x + 1); - sad2c += AbsDiff(p33, p23); // SAD 3, 2 - sad3c += AbsDiff(p33, p32); // SAD 2, 3 + sad2c = Add(sad2c, AbsDiff(p33, p23)); // SAD 3, 2 + sad3c = Add(sad3c, AbsDiff(p33, p32)); // SAD 2, 3 const auto p24 = Load(df, rows[c][2 + 2] + x); - sad3c += AbsDiff(p24, p23); // SAD 2, 3 + sad3c = Add(sad3c, AbsDiff(p24, p23)); // SAD 2, 3 auto scale = Set(df, lf_.epf_channel_scale[c]); sad0 = MulAdd(sad0c, scale, sad0); @@ -322,13 +333,13 @@ class EPF1Stage : public RenderPipelineStage { AddPixel</*aligned=*/true>(/*row=*/1, rows, x, sad3, inv_sigma, &X, &Y, &B, &w); #if JXL_HIGH_PRECISION - auto inv_w = Set(df, 1.0f) / w; + auto inv_w = Div(Set(df, 1.0f), w); #else auto inv_w = ApproximateReciprocal(w); #endif - Store(X * inv_w, df, GetOutputRow(output_rows, 0, 0) + x); - Store(Y * inv_w, df, GetOutputRow(output_rows, 1, 0) + x); - Store(B * inv_w, df, GetOutputRow(output_rows, 2, 0) + x); + Store(Mul(X, inv_w), df, GetOutputRow(output_rows, 0, 0) + x); + Store(Mul(Y, inv_w), df, GetOutputRow(output_rows, 1, 0) + x); + Store(Mul(B, inv_w), df, GetOutputRow(output_rows, 2, 0) + x); } } @@ -367,13 +378,13 @@ class EPF2Stage : public RenderPipelineStage { auto cb = aligned ? Load(DF(), rows[2][1 + row] + x) : LoadU(DF(), rows[2][1 + row] + x); - auto sad = AbsDiff(cx, rx) * Set(DF(), lf_.epf_channel_scale[0]); + auto sad = Mul(AbsDiff(cx, rx), Set(DF(), lf_.epf_channel_scale[0])); sad = MulAdd(AbsDiff(cy, ry), Set(DF(), lf_.epf_channel_scale[1]), sad); sad = MulAdd(AbsDiff(cb, rb), Set(DF(), lf_.epf_channel_scale[2]), sad); auto weight = Weight(sad, inv_sigma, Set(DF(), lf_.epf_pass2_zeroflush)); - *w += weight; + *w = Add(*w, weight); *X = MulAdd(weight, cx, *X); *Y = MulAdd(weight, cy, *Y); *B = MulAdd(weight, cb, *B); @@ -421,7 +432,7 @@ class EPF2Stage : public RenderPipelineStage { } const auto sm = Load(df, sad_mul + ix); - const auto inv_sigma = Set(df, row_sigma[bx]) * sm; + const auto inv_sigma = Mul(Set(df, row_sigma[bx]), sm); const auto x_cc = Load(df, rows[0][1 + 0] + x); const auto y_cc = Load(df, rows[1][1 + 0] + x); @@ -444,13 +455,13 @@ class EPF2Stage : public RenderPipelineStage { AddPixel</*aligned=*/true>(/*row=*/1, rows, x, x_cc, y_cc, b_cc, inv_sigma, &X, &Y, &B, &w); #if JXL_HIGH_PRECISION - auto inv_w = Set(df, 1.0f) / w; + auto inv_w = Div(Set(df, 1.0f), w); #else auto inv_w = ApproximateReciprocal(w); #endif - Store(X * inv_w, df, GetOutputRow(output_rows, 0, 0) + x); - Store(Y * inv_w, df, GetOutputRow(output_rows, 1, 0) + x); - Store(B * inv_w, df, GetOutputRow(output_rows, 2, 0) + x); + Store(Mul(X, inv_w), df, GetOutputRow(output_rows, 0, 0) + x); + Store(Mul(Y, inv_w), df, GetOutputRow(output_rows, 1, 0) + x); + Store(Mul(B, inv_w), df, GetOutputRow(output_rows, 2, 0) + x); } } @@ -507,7 +518,6 @@ std::unique_ptr<RenderPipelineStage> GetEPFStage(const LoopFilter& lf, default: JXL_ABORT("Invalid EPF stage"); } - return nullptr; } } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_from_linear.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_from_linear.cc new file mode 100644 index 0000000000..81f546c6bd --- /dev/null +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_from_linear.cc @@ -0,0 +1,189 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/render_pipeline/stage_from_linear.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/render_pipeline/stage_from_linear.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/jxl/dec_tone_mapping-inl.h" +#include "lib/jxl/sanitizers.h" +#include "lib/jxl/transfer_functions-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::IfThenZeroElse; + +template <typename Op> +struct PerChannelOp { + explicit PerChannelOp(Op op) : op(op) {} + template <typename D, typename T> + void Transform(D d, T* r, T* g, T* b) const { + *r = op.Transform(d, *r); + *g = op.Transform(d, *g); + *b = op.Transform(d, *b); + } + + Op op; +}; +template <typename Op> +PerChannelOp<Op> MakePerChannelOp(Op&& op) { + return PerChannelOp<Op>(std::forward<Op>(op)); +} + +struct OpLinear { + template <typename D, typename T> + T Transform(D d, const T& linear) const { + return linear; + } +}; + +struct OpRgb { + template <typename D, typename T> + T Transform(D d, const T& linear) const { +#if JXL_HIGH_PRECISION + return TF_SRGB().EncodedFromDisplay(d, linear); +#else + return FastLinearToSRGB(d, linear); +#endif + } +}; + +struct OpPq { + template <typename D, typename T> + T Transform(D d, const T& linear) const { + return TF_PQ().EncodedFromDisplay(d, linear); + } +}; + +struct OpHlg { + explicit OpHlg(const float luminances[3], const float intensity_target) + : hlg_ootf_(HlgOOTF::ToSceneLight(/*display_luminance=*/intensity_target, + luminances)) {} + + template <typename D, typename T> + void Transform(D d, T* r, T* g, T* b) const { + hlg_ootf_.Apply(r, g, b); + *r = TF_HLG().EncodedFromDisplay(d, *r); + *g = TF_HLG().EncodedFromDisplay(d, *g); + *b = TF_HLG().EncodedFromDisplay(d, *b); + } + HlgOOTF hlg_ootf_; +}; + +struct Op709 { + template <typename D, typename T> + T Transform(D d, const T& linear) const { + return TF_709().EncodedFromDisplay(d, linear); + } +}; + +struct OpGamma { + const float inverse_gamma; + template <typename D, typename T> + T Transform(D d, const T& linear) const { + return IfThenZeroElse(Le(linear, Set(d, 1e-5f)), + FastPowf(d, linear, Set(d, inverse_gamma))); + } +}; + +template <typename Op> +class FromLinearStage : public RenderPipelineStage { + public: + explicit FromLinearStage(Op op) + : RenderPipelineStage(RenderPipelineStage::Settings()), + op_(std::move(op)) {} + + void ProcessRow(const RowInfo& input_rows, const RowInfo& output_rows, + size_t xextra, size_t xsize, size_t xpos, size_t ypos, + size_t thread_id) const final { + PROFILER_ZONE("FromLinear"); + const HWY_FULL(float) d; + const size_t xsize_v = RoundUpTo(xsize, Lanes(d)); + float* JXL_RESTRICT row0 = GetInputRow(input_rows, 0, 0); + float* JXL_RESTRICT row1 = GetInputRow(input_rows, 1, 0); + float* JXL_RESTRICT row2 = GetInputRow(input_rows, 2, 0); + // All calculations are lane-wise, still some might require + // value-dependent behaviour (e.g. NearestInt). Temporary unpoison last + // vector tail. + msan::UnpoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + for (ssize_t x = -xextra; x < (ssize_t)(xsize + xextra); x += Lanes(d)) { + auto r = LoadU(d, row0 + x); + auto g = LoadU(d, row1 + x); + auto b = LoadU(d, row2 + x); + op_.Transform(d, &r, &g, &b); + StoreU(r, d, row0 + x); + StoreU(g, d, row1 + x); + StoreU(b, d, row2 + x); + } + msan::PoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + } + + RenderPipelineChannelMode GetChannelMode(size_t c) const final { + return c < 3 ? RenderPipelineChannelMode::kInPlace + : RenderPipelineChannelMode::kIgnored; + } + + const char* GetName() const override { return "FromLinear"; } + + private: + Op op_; +}; + +template <typename Op> +std::unique_ptr<FromLinearStage<Op>> MakeFromLinearStage(Op&& op) { + return jxl::make_unique<FromLinearStage<Op>>(std::forward<Op>(op)); +} + +std::unique_ptr<RenderPipelineStage> GetFromLinearStage( + const OutputEncodingInfo& output_encoding_info) { + if (output_encoding_info.color_encoding.tf.IsLinear()) { + return MakeFromLinearStage(MakePerChannelOp(OpLinear())); + } else if (output_encoding_info.color_encoding.tf.IsSRGB()) { + return MakeFromLinearStage(MakePerChannelOp(OpRgb())); + } else if (output_encoding_info.color_encoding.tf.IsPQ()) { + return MakeFromLinearStage(MakePerChannelOp(OpPq())); + } else if (output_encoding_info.color_encoding.tf.IsHLG()) { + return MakeFromLinearStage( + OpHlg(output_encoding_info.luminances, + output_encoding_info.desired_intensity_target)); + } else if (output_encoding_info.color_encoding.tf.Is709()) { + return MakeFromLinearStage(MakePerChannelOp(Op709())); + } else if (output_encoding_info.color_encoding.tf.IsGamma() || + output_encoding_info.color_encoding.tf.IsDCI()) { + return MakeFromLinearStage( + MakePerChannelOp(OpGamma{output_encoding_info.inverse_gamma})); + } else { + // This is a programming error. + JXL_ABORT("Invalid target encoding"); + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(GetFromLinearStage); + +std::unique_ptr<RenderPipelineStage> GetFromLinearStage( + const OutputEncodingInfo& output_encoding_info) { + return HWY_DYNAMIC_DISPATCH(GetFromLinearStage)(output_encoding_info); +} + +} // namespace jxl +#endif diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_from_linear.h b/media/libjxl/src/lib/jxl/render_pipeline/stage_from_linear.h new file mode 100644 index 0000000000..548ab50b8c --- /dev/null +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_from_linear.h @@ -0,0 +1,20 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_JXL_RENDER_PIPELINE_STAGE_FROM_LINEAR_H_ +#define LIB_JXL_RENDER_PIPELINE_STAGE_FROM_LINEAR_H_ + +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/render_pipeline/render_pipeline_stage.h" + +namespace jxl { + +// Converts the color channels from linear to the specified output encoding. +std::unique_ptr<RenderPipelineStage> GetFromLinearStage( + const OutputEncodingInfo& output_encoding_info); + +} // namespace jxl + +#endif // LIB_JXL_RENDER_PIPELINE_STAGE_FROM_LINEAR_H_ diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_gaborish.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_gaborish.cc index 2450d08cfe..fc90acb476 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_gaborish.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_gaborish.cc @@ -14,6 +14,11 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; + class GaborishStage : public RenderPipelineStage { public: explicit GaborishStage(const LoopFilter& lf) @@ -74,9 +79,9 @@ class GaborishStage : public RenderPipelineStage { const auto bl = LoadU(d, row_b + x - 1); const auto br = LoadU(d, row_b + x + 1); const auto sum0 = m; - const auto sum1 = (l + r) + (t + b); - const auto sum2 = (tl + tr) + (bl + br); - auto pixels = MulAdd(sum2, w2, MulAdd(sum1, w1, sum0 * w0)); + const auto sum1 = Add(Add(l, r), Add(t, b)); + const auto sum2 = Add(Add(tl, tr), Add(bl, br)); + auto pixels = MulAdd(sum2, w2, MulAdd(sum1, w1, Mul(sum0, w0))); Store(pixels, d, row_out + x); } } diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_noise.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_noise.cc index 1ed98bf27e..9f0cee316a 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_noise.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_noise.cc @@ -10,7 +10,6 @@ #include <hwy/foreach_target.h> #include <hwy/highway.h> -#include "lib/jxl/fast_math-inl.h" #include "lib/jxl/sanitizers.h" #include "lib/jxl/transfer_functions-inl.h" @@ -19,11 +18,13 @@ namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Max; using hwy::HWY_NAMESPACE::ShiftRight; using hwy::HWY_NAMESPACE::Vec; +using hwy::HWY_NAMESPACE::ZeroIfNegative; using D = HWY_CAPPED(float, kBlockDim); -using DI = hwy::HWY_NAMESPACE::Rebind<int, D>; +using DI = hwy::HWY_NAMESPACE::Rebind<int32_t, D>; using DI8 = hwy::HWY_NAMESPACE::Repartition<uint8_t, D>; // [0, max_value] @@ -64,12 +65,12 @@ class StrengthEvalLut { V operator()(const V vx) const { constexpr size_t kScale = NoiseParams::kNumNoisePoints - 2; - auto scaled_vx = Max(Zero(D()), vx * Set(D(), kScale)); + auto scaled_vx = Max(Zero(D()), Mul(vx, Set(D(), kScale))); auto floor_x = Floor(scaled_vx); - auto frac_x = scaled_vx - floor_x; - floor_x = IfThenElse(scaled_vx >= Set(D(), kScale), Set(D(), kScale - 1), + auto frac_x = Sub(scaled_vx, floor_x); + floor_x = IfThenElse(Ge(scaled_vx, Set(D(), kScale)), Set(D(), kScale - 1), floor_x); - frac_x = IfThenElse(scaled_vx >= Set(D(), kScale), Set(D(), 1), frac_x); + frac_x = IfThenElse(Ge(scaled_vx, Set(D(), kScale)), Set(D(), 1), frac_x); auto floor_x_int = ConvertTo(DI(), floor_x); #if HWY_TARGET == HWY_SCALAR auto low = Set(D(), noise_params_.lut[floor_x_int.raw]); @@ -77,10 +78,10 @@ class StrengthEvalLut { #else // Set each lane's bytes to {0, 0, 2x+1, 2x}. auto floorx_indices_low = - floor_x_int * Set(DI(), 0x0202) + Set(DI(), 0x0100); + Add(Mul(floor_x_int, Set(DI(), 0x0202)), Set(DI(), 0x0100)); // Set each lane's bytes to {2x+1, 2x, 0, 0}. auto floorx_indices_hi = - floor_x_int * Set(DI(), 0x02020000) + Set(DI(), 0x01000000); + Add(Mul(floor_x_int, Set(DI(), 0x02020000)), Set(DI(), 0x01000000)); // load LUT auto low16 = BitCast(DI(), LoadDup128(DI8(), low16_lut)); auto lowm = Set(DI(), 0xFFFF); @@ -88,16 +89,16 @@ class StrengthEvalLut { auto him = Set(DI(), 0xFFFF0000); // low = noise_params.lut[floor_x] auto low = - BitCast(D(), (TableLookupBytes(low16, floorx_indices_low) & lowm) | - (TableLookupBytes(hi16, floorx_indices_hi) & him)); + BitCast(D(), Or(And(TableLookupBytes(low16, floorx_indices_low), lowm), + And(TableLookupBytes(hi16, floorx_indices_hi), him))); // hi = noise_params.lut[floor_x+1] - floorx_indices_low += Set(DI(), 0x0202); - floorx_indices_hi += Set(DI(), 0x02020000); + floorx_indices_low = Add(floorx_indices_low, Set(DI(), 0x0202)); + floorx_indices_hi = Add(floorx_indices_hi, Set(DI(), 0x02020000)); auto hi = - BitCast(D(), (TableLookupBytes(low16, floorx_indices_low) & lowm) | - (TableLookupBytes(hi16, floorx_indices_hi) & him)); + BitCast(D(), Or(And(TableLookupBytes(low16, floorx_indices_low), lowm), + And(TableLookupBytes(hi16, floorx_indices_hi), him))); #endif - return MulAdd(hi - low, frac_x, low); + return MulAdd(Sub(hi, low), frac_x, low); } private: @@ -119,22 +120,25 @@ void AddNoiseToRGB(const D d, const Vec<D> rnd_noise_r, const auto kRGCorr = Set(d, 0.9921875f); // 127/128 const auto kRGNCorr = Set(d, 0.0078125f); // 1/128 - const auto red_noise = kRGNCorr * rnd_noise_r * noise_strength_r + - kRGCorr * rnd_noise_cor * noise_strength_r; - const auto green_noise = kRGNCorr * rnd_noise_g * noise_strength_g + - kRGCorr * rnd_noise_cor * noise_strength_g; - - auto vx = Load(d, out_x); - auto vy = Load(d, out_y); - auto vb = Load(d, out_b); - - vx += red_noise - green_noise + Set(d, ytox) * (red_noise + green_noise); - vy += red_noise + green_noise; - vb += Set(d, ytob) * (red_noise + green_noise); - - Store(vx, d, out_x); - Store(vy, d, out_y); - Store(vb, d, out_b); + const auto red_noise = + Mul(noise_strength_r, + MulAdd(kRGNCorr, rnd_noise_r, Mul(kRGCorr, rnd_noise_cor))); + const auto green_noise = + Mul(noise_strength_g, + MulAdd(kRGNCorr, rnd_noise_g, Mul(kRGCorr, rnd_noise_cor))); + + auto vx = LoadU(d, out_x); + auto vy = LoadU(d, out_y); + auto vb = LoadU(d, out_b); + + const auto rg_noise = Add(red_noise, green_noise); + vx = Add(MulAdd(Set(d, ytox), rg_noise, Sub(red_noise, green_noise)), vx); + vy = Add(vy, rg_noise); + vb = MulAdd(Set(d, ytob), rg_noise, vb); + + StoreU(vx, d, out_x); + StoreU(vy, d, out_y); + StoreU(vb, d, out_b); } class AddNoiseStage : public RenderPipelineStage { @@ -181,16 +185,17 @@ class AddNoiseStage : public RenderPipelineStage { msan::UnpoisonMemory(row_x + xsize, (xsize_v - xsize) * sizeof(float)); msan::UnpoisonMemory(row_y + xsize, (xsize_v - xsize) * sizeof(float)); for (size_t x = 0; x < xsize_v; x += Lanes(d)) { - const auto vx = Load(d, row_x + x); - const auto vy = Load(d, row_y + x); - const auto in_g = vy - vx; - const auto in_r = vy + vx; - const auto noise_strength_g = NoiseStrength(noise_model, in_g * half); - const auto noise_strength_r = NoiseStrength(noise_model, in_r * half); - const auto addit_rnd_noise_red = Load(d, row_rnd_r + x) * norm_const; - const auto addit_rnd_noise_green = Load(d, row_rnd_g + x) * norm_const; + const auto vx = LoadU(d, row_x + x); + const auto vy = LoadU(d, row_y + x); + const auto in_g = Sub(vy, vx); + const auto in_r = Add(vy, vx); + const auto noise_strength_g = NoiseStrength(noise_model, Mul(in_g, half)); + const auto noise_strength_r = NoiseStrength(noise_model, Mul(in_r, half)); + const auto addit_rnd_noise_red = Mul(LoadU(d, row_rnd_r + x), norm_const); + const auto addit_rnd_noise_green = + Mul(LoadU(d, row_rnd_g + x), norm_const); const auto addit_rnd_noise_correlated = - Load(d, row_rnd_c + x) * norm_const; + Mul(LoadU(d, row_rnd_c + x), norm_const); AddNoiseToRGB(D(), addit_rnd_noise_red, addit_rnd_noise_green, addit_rnd_noise_correlated, noise_strength_g, noise_strength_r, ytox, ytob, row_x + x, row_y + x, @@ -242,21 +247,22 @@ class ConvolveNoiseStage : public RenderPipelineStage { float* JXL_RESTRICT row_out = GetOutputRow(output_rows, c, 0); for (ssize_t x = -RoundUpTo(xextra, Lanes(d)); x < (ssize_t)(xsize + xextra); x += Lanes(d)) { - const auto p00 = Load(d, rows[2] + x); + const auto p00 = LoadU(d, rows[2] + x); auto others = Zero(d); + // TODO(eustas): sum loaded values to reduce the calculation chain for (ssize_t i = -2; i <= 2; i++) { - others += LoadU(d, rows[0] + x + i); - others += LoadU(d, rows[1] + x + i); - others += LoadU(d, rows[3] + x + i); - others += LoadU(d, rows[4] + x + i); + others = Add(others, LoadU(d, rows[0] + x + i)); + others = Add(others, LoadU(d, rows[1] + x + i)); + others = Add(others, LoadU(d, rows[3] + x + i)); + others = Add(others, LoadU(d, rows[4] + x + i)); } - others += LoadU(d, rows[2] + x - 2); - others += LoadU(d, rows[2] + x - 1); - others += LoadU(d, rows[2] + x + 1); - others += LoadU(d, rows[2] + x + 2); + others = Add(others, LoadU(d, rows[2] + x - 2)); + others = Add(others, LoadU(d, rows[2] + x - 1)); + others = Add(others, LoadU(d, rows[2] + x + 1)); + others = Add(others, LoadU(d, rows[2] + x + 2)); // 4 * (1 - box kernel) - auto pixels = MulAdd(others, Set(d, 0.16), p00 * Set(d, -3.84)); - Store(pixels, d, row_out + x); + auto pixels = MulAdd(others, Set(d, 0.16), Mul(p00, Set(d, -3.84))); + StoreU(pixels, d, row_out + x); } } } diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_patches.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_patches.cc index 2c3c300617..527be03839 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_patches.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_patches.cc @@ -9,36 +9,40 @@ namespace jxl { namespace { class PatchDictionaryStage : public RenderPipelineStage { public: - explicit PatchDictionaryStage(const PatchDictionary* patches) + PatchDictionaryStage(const PatchDictionary* patches, size_t num_channels) : RenderPipelineStage(RenderPipelineStage::Settings()), - patches_(*patches) {} + patches_(*patches), + num_channels_(num_channels) {} void ProcessRow(const RowInfo& input_rows, const RowInfo& output_rows, size_t xextra, size_t xsize, size_t xpos, size_t ypos, size_t thread_id) const final { PROFILER_ZONE("RenderPatches"); - std::vector<float*> row_ptrs(input_rows.size()); - for (size_t i = 0; i < row_ptrs.size(); i++) { - row_ptrs[i] = GetInputRow(input_rows, i, 0) - xextra; + JXL_ASSERT(xpos == 0 || xpos >= xextra); + size_t x0 = xpos ? xpos - xextra : 0; + std::vector<float*> row_ptrs(num_channels_); + for (size_t i = 0; i < num_channels_; i++) { + row_ptrs[i] = GetInputRow(input_rows, i, 0) + x0 - xpos; } - patches_.AddOneRow(row_ptrs.data(), ypos, xpos - xextra, - xsize + 2 * xextra); + patches_.AddOneRow(row_ptrs.data(), ypos, x0, xsize + xextra + xpos - x0); } RenderPipelineChannelMode GetChannelMode(size_t c) const final { - return RenderPipelineChannelMode::kInPlace; + return c < num_channels_ ? RenderPipelineChannelMode::kInPlace + : RenderPipelineChannelMode::kIgnored; } const char* GetName() const override { return "Patches"; } private: const PatchDictionary& patches_; + const size_t num_channels_; }; } // namespace std::unique_ptr<RenderPipelineStage> GetPatchesStage( - const PatchDictionary* patches) { - return jxl::make_unique<PatchDictionaryStage>(patches); + const PatchDictionary* patches, size_t num_channels) { + return jxl::make_unique<PatchDictionaryStage>(patches, num_channels); } } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_patches.h b/media/libjxl/src/lib/jxl/render_pipeline/stage_patches.h index e4ac8d251c..b35abdc2eb 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_patches.h +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_patches.h @@ -15,7 +15,7 @@ namespace jxl { // Draws patches if applicable. std::unique_ptr<RenderPipelineStage> GetPatchesStage( - const PatchDictionary* patches); + const PatchDictionary* patches, size_t num_channels); } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_to_linear.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_to_linear.cc new file mode 100644 index 0000000000..bf79481e4e --- /dev/null +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_to_linear.cc @@ -0,0 +1,200 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/render_pipeline/stage_to_linear.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/render_pipeline/stage_to_linear.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/jxl/dec_tone_mapping-inl.h" +#include "lib/jxl/sanitizers.h" +#include "lib/jxl/transfer_functions-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::IfThenZeroElse; + +template <typename Op> +struct PerChannelOp { + explicit PerChannelOp(Op op) : op(op) {} + template <typename D, typename T> + void Transform(D d, T* r, T* g, T* b) const { + *r = op.Transform(d, *r); + *g = op.Transform(d, *g); + *b = op.Transform(d, *b); + } + + Op op; +}; +template <typename Op> +PerChannelOp<Op> MakePerChannelOp(Op&& op) { + return PerChannelOp<Op>(std::forward<Op>(op)); +} + +struct OpLinear { + template <typename D, typename T> + T Transform(D d, const T& encoded) const { + return encoded; + } +}; + +struct OpRgb { + template <typename D, typename T> + T Transform(D d, const T& encoded) const { + return TF_SRGB().DisplayFromEncoded(encoded); + } +}; + +struct OpPq { + template <typename D, typename T> + T Transform(D d, const T& encoded) const { + return TF_PQ().DisplayFromEncoded(d, encoded); + } +}; + +struct OpHlg { + explicit OpHlg(const float luminances[3], const float intensity_target) + : hlg_ootf_(HlgOOTF::FromSceneLight( + /*display_luminance=*/intensity_target, luminances)) {} + + template <typename D, typename T> + void Transform(D d, T* r, T* g, T* b) const { + for (T* val : {r, g, b}) { + float vals[MaxLanes(d)]; + Store(*val, d, vals); + for (size_t i = 0; i < Lanes(d); ++i) { + vals[i] = TF_HLG().DisplayFromEncoded(vals[i]); + } + *val = Load(d, vals); + } + hlg_ootf_.Apply(r, g, b); + } + HlgOOTF hlg_ootf_; +}; + +struct Op709 { + template <typename D, typename T> + T Transform(D d, const T& encoded) const { + return TF_709().DisplayFromEncoded(d, encoded); + } +}; + +struct OpGamma { + const float gamma; + template <typename D, typename T> + T Transform(D d, const T& encoded) const { + return IfThenZeroElse(Le(encoded, Set(d, 1e-5f)), + FastPowf(d, encoded, Set(d, gamma))); + } +}; + +struct OpInvalid { + template <typename D, typename T> + void Transform(D d, T* r, T* g, T* b) const {} +}; + +template <typename Op> +class ToLinearStage : public RenderPipelineStage { + public: + explicit ToLinearStage(Op op) + : RenderPipelineStage(RenderPipelineStage::Settings()), + op_(std::move(op)) {} + + explicit ToLinearStage() + : RenderPipelineStage(RenderPipelineStage::Settings()), valid_(false) {} + + void ProcessRow(const RowInfo& input_rows, const RowInfo& output_rows, + size_t xextra, size_t xsize, size_t xpos, size_t ypos, + size_t thread_id) const final { + PROFILER_ZONE("ToLinear"); + + const HWY_FULL(float) d; + const size_t xsize_v = RoundUpTo(xsize, Lanes(d)); + float* JXL_RESTRICT row0 = GetInputRow(input_rows, 0, 0); + float* JXL_RESTRICT row1 = GetInputRow(input_rows, 1, 0); + float* JXL_RESTRICT row2 = GetInputRow(input_rows, 2, 0); + // All calculations are lane-wise, still some might require + // value-dependent behaviour (e.g. NearestInt). Temporary unpoison last + // vector tail. + msan::UnpoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + for (ssize_t x = -xextra; x < (ssize_t)(xsize + xextra); x += Lanes(d)) { + auto r = LoadU(d, row0 + x); + auto g = LoadU(d, row1 + x); + auto b = LoadU(d, row2 + x); + op_.Transform(d, &r, &g, &b); + StoreU(r, d, row0 + x); + StoreU(g, d, row1 + x); + StoreU(b, d, row2 + x); + } + msan::PoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + } + + RenderPipelineChannelMode GetChannelMode(size_t c) const final { + return c < 3 ? RenderPipelineChannelMode::kInPlace + : RenderPipelineChannelMode::kIgnored; + } + + const char* GetName() const override { return "ToLinear"; } + + private: + Status IsInitialized() const override { return valid_; } + + Op op_; + bool valid_ = true; +}; + +template <typename Op> +std::unique_ptr<ToLinearStage<Op>> MakeToLinearStage(Op&& op) { + return jxl::make_unique<ToLinearStage<Op>>(std::forward<Op>(op)); +} + +std::unique_ptr<RenderPipelineStage> GetToLinearStage( + const OutputEncodingInfo& output_encoding_info) { + if (output_encoding_info.color_encoding.tf.IsLinear()) { + return MakeToLinearStage(MakePerChannelOp(OpLinear())); + } else if (output_encoding_info.color_encoding.tf.IsSRGB()) { + return MakeToLinearStage(MakePerChannelOp(OpRgb())); + } else if (output_encoding_info.color_encoding.tf.IsPQ()) { + return MakeToLinearStage(MakePerChannelOp(OpPq())); + } else if (output_encoding_info.color_encoding.tf.IsHLG()) { + return MakeToLinearStage(OpHlg(output_encoding_info.luminances, + output_encoding_info.orig_intensity_target)); + } else if (output_encoding_info.color_encoding.tf.Is709()) { + return MakeToLinearStage(MakePerChannelOp(Op709())); + } else if (output_encoding_info.color_encoding.tf.IsGamma() || + output_encoding_info.color_encoding.tf.IsDCI()) { + return MakeToLinearStage( + MakePerChannelOp(OpGamma{1.f / output_encoding_info.inverse_gamma})); + } else { + return jxl::make_unique<ToLinearStage<OpInvalid>>(); + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(GetToLinearStage); + +std::unique_ptr<RenderPipelineStage> GetToLinearStage( + const OutputEncodingInfo& output_encoding_info) { + return HWY_DYNAMIC_DISPATCH(GetToLinearStage)(output_encoding_info); +} + +} // namespace jxl +#endif diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_to_linear.h b/media/libjxl/src/lib/jxl/render_pipeline/stage_to_linear.h new file mode 100644 index 0000000000..ccee7b09f0 --- /dev/null +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_to_linear.h @@ -0,0 +1,21 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_JXL_RENDER_PIPELINE_STAGE_TO_LINEAR_H_ +#define LIB_JXL_RENDER_PIPELINE_STAGE_TO_LINEAR_H_ + +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/render_pipeline/render_pipeline_stage.h" + +namespace jxl { + +// Converts the color channels from `output_encoding_info.color_encoding` to +// linear. +std::unique_ptr<RenderPipelineStage> GetToLinearStage( + const OutputEncodingInfo& output_encoding_info); + +} // namespace jxl + +#endif // LIB_JXL_RENDER_PIPELINE_STAGE_TO_LINEAR_H_ diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_tone_mapping.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_tone_mapping.cc new file mode 100644 index 0000000000..7609534a5b --- /dev/null +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_tone_mapping.cc @@ -0,0 +1,151 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/render_pipeline/stage_tone_mapping.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/render_pipeline/stage_tone_mapping.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/jxl/dec_tone_mapping-inl.h" +#include "lib/jxl/dec_xyb-inl.h" +#include "lib/jxl/sanitizers.h" +#include "lib/jxl/transfer_functions-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +class ToneMappingStage : public RenderPipelineStage { + public: + explicit ToneMappingStage(OutputEncodingInfo output_encoding_info) + : RenderPipelineStage(RenderPipelineStage::Settings()), + output_encoding_info_(std::move(output_encoding_info)) { + if (output_encoding_info_.desired_intensity_target == + output_encoding_info_.orig_intensity_target) { + // No tone mapping requested. + return; + } + if (output_encoding_info_.orig_color_encoding.tf.IsPQ() && + output_encoding_info_.desired_intensity_target < + output_encoding_info_.orig_intensity_target) { + tone_mapper_ = jxl::make_unique<ToneMapper>( + /*source_range=*/std::pair<float, float>( + 0, output_encoding_info_.orig_intensity_target), + /*target_range=*/ + std::pair<float, float>( + 0, output_encoding_info_.desired_intensity_target), + output_encoding_info_.luminances); + } else if (output_encoding_info_.orig_color_encoding.tf.IsHLG() && + !output_encoding_info_.color_encoding.tf.IsHLG()) { + hlg_ootf_ = jxl::make_unique<HlgOOTF>( + /*source_luminance=*/output_encoding_info_.orig_intensity_target, + /*target_luminance=*/output_encoding_info_.desired_intensity_target, + output_encoding_info_.luminances); + } + + if (output_encoding_info_.color_encoding.tf.IsPQ() && + (tone_mapper_ || hlg_ootf_)) { + to_intensity_target_ = + 10000.f / output_encoding_info_.orig_intensity_target; + from_desired_intensity_target_ = + output_encoding_info_.desired_intensity_target / 10000.f; + } + } + + bool IsNeeded() const { return tone_mapper_ || hlg_ootf_; } + + void ProcessRow(const RowInfo& input_rows, const RowInfo& output_rows, + size_t xextra, size_t xsize, size_t xpos, size_t ypos, + size_t thread_id) const final { + PROFILER_ZONE("ToneMapping"); + + if (!(tone_mapper_ || hlg_ootf_)) return; + + const HWY_FULL(float) d; + const size_t xsize_v = RoundUpTo(xsize, Lanes(d)); + float* JXL_RESTRICT row0 = GetInputRow(input_rows, 0, 0); + float* JXL_RESTRICT row1 = GetInputRow(input_rows, 1, 0); + float* JXL_RESTRICT row2 = GetInputRow(input_rows, 2, 0); + // All calculations are lane-wise, still some might require + // value-dependent behaviour (e.g. NearestInt). Temporary unpoison last + // vector tail. + msan::UnpoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + for (ssize_t x = -xextra; x < (ssize_t)(xsize + xextra); x += Lanes(d)) { + auto r = LoadU(d, row0 + x); + auto g = LoadU(d, row1 + x); + auto b = LoadU(d, row2 + x); + if (tone_mapper_ || hlg_ootf_) { + r = Mul(r, Set(d, to_intensity_target_)); + g = Mul(g, Set(d, to_intensity_target_)); + b = Mul(b, Set(d, to_intensity_target_)); + if (tone_mapper_) { + tone_mapper_->ToneMap(&r, &g, &b); + } else { + JXL_ASSERT(hlg_ootf_); + hlg_ootf_->Apply(&r, &g, &b); + } + if (tone_mapper_ || hlg_ootf_->WarrantsGamutMapping()) { + GamutMap(&r, &g, &b, output_encoding_info_.luminances); + } + r = Mul(r, Set(d, from_desired_intensity_target_)); + g = Mul(g, Set(d, from_desired_intensity_target_)); + b = Mul(b, Set(d, from_desired_intensity_target_)); + } + StoreU(r, d, row0 + x); + StoreU(g, d, row1 + x); + StoreU(b, d, row2 + x); + } + msan::PoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + } + + RenderPipelineChannelMode GetChannelMode(size_t c) const final { + return c < 3 ? RenderPipelineChannelMode::kInPlace + : RenderPipelineChannelMode::kIgnored; + } + + const char* GetName() const override { return "ToneMapping"; } + + private: + using ToneMapper = Rec2408ToneMapper<HWY_FULL(float)>; + OutputEncodingInfo output_encoding_info_; + std::unique_ptr<ToneMapper> tone_mapper_; + std::unique_ptr<HlgOOTF> hlg_ootf_; + // When the target colorspace is PQ, 1 represents 10000 nits instead of + // orig_intensity_target. This temporarily changes this if the tone mappers + // require it. + float to_intensity_target_ = 1.f; + float from_desired_intensity_target_ = 1.f; +}; + +std::unique_ptr<RenderPipelineStage> GetToneMappingStage( + const OutputEncodingInfo& output_encoding_info) { + auto stage = jxl::make_unique<ToneMappingStage>(output_encoding_info); + if (!stage->IsNeeded()) return nullptr; + return stage; +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(GetToneMappingStage); + +std::unique_ptr<RenderPipelineStage> GetToneMappingStage( + const OutputEncodingInfo& output_encoding_info) { + return HWY_DYNAMIC_DISPATCH(GetToneMappingStage)(output_encoding_info); +} + +} // namespace jxl +#endif diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_tone_mapping.h b/media/libjxl/src/lib/jxl/render_pipeline/stage_tone_mapping.h new file mode 100644 index 0000000000..99824f8511 --- /dev/null +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_tone_mapping.h @@ -0,0 +1,37 @@ +// Copyright (c) the JPEG XL Project 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 LIB_JXL_RENDER_PIPELINE_STAGE_TONE_MAPPING_H_ +#define LIB_JXL_RENDER_PIPELINE_STAGE_TONE_MAPPING_H_ +#include <math.h> +#include <stdint.h> +#include <stdio.h> + +#include <algorithm> +#include <utility> +#include <vector> + +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/render_pipeline/render_pipeline_stage.h" + +namespace jxl { + +// Tone maps the image if appropriate. It must be in linear space and +// `output_encoding_info.luminances` must contain the luminance for the +// primaries of that space. It must also be encoded such that (1, 1, 1) +// represents `output_encoding_info.orig_intensity_target` nits, unless +// `output_encoding_info.color_encoding.tf.IsPQ()`, in which case (1, 1, 1) must +// represent 10000 nits. This corresponds to what XYBStage outputs. After this +// stage, (1, 1, 1) will represent +// `output_encoding_info.desired_intensity_target` nits, except in the PQ +// special case in which it remains 10000. +// +// If no tone mapping is necessary, this will return nullptr. +std::unique_ptr<RenderPipelineStage> GetToneMappingStage( + const OutputEncodingInfo& output_encoding_info); + +} // namespace jxl + +#endif // LIB_JXL_RENDER_PIPELINE_STAGE_TONE_MAPPING_H_ diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_upsampling.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_upsampling.cc index 56de46caed..a75e259865 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_upsampling.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_upsampling.cc @@ -17,6 +17,12 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Clamp; +using hwy::HWY_NAMESPACE::Max; +using hwy::HWY_NAMESPACE::Min; +using hwy::HWY_NAMESPACE::MulAdd; + class UpsamplingStage : public RenderPipelineStage { public: explicit UpsamplingStage(const CustomTransformData& ups_factors, size_t c, @@ -101,10 +107,27 @@ class UpsamplingStage : public RenderPipelineStage { void ProcessRowImpl(const RowInfo& input_rows, const RowInfo& output_rows, ssize_t x0, ssize_t x1) const { static HWY_FULL(float) df; + using V = hwy::HWY_NAMESPACE::Vec<HWY_FULL(float)>; + V ups0, ups1, ups2, ups3, ups4, ups5, ups6, ups7; + (void)ups2, (void)ups3, (void)ups4, (void)ups5, (void)ups6, (void)ups7; + V* ups[N]; + if (N >= 2) { + ups[0] = &ups0; + ups[1] = &ups1; + } + if (N >= 4) { + ups[2] = &ups2; + ups[3] = &ups3; + } + if (N == 8) { + ups[4] = &ups4; + ups[5] = &ups5; + ups[6] = &ups6; + ups[7] = &ups7; + } for (size_t oy = 0; oy < N; oy++) { float* dst_row = GetOutputRow(output_rows, c_, oy); for (ssize_t x = x0; x < x1; x += Lanes(df)) { - hwy::HWY_NAMESPACE::Vec<HWY_FULL(float)> ups[8]; for (size_t ox = 0; ox < N; ox++) { auto result = Zero(df); auto min = LoadU(df, GetInputRow(input_rows, c_, 0) + x); @@ -118,17 +141,17 @@ class UpsamplingStage : public RenderPipelineStage { } } // Avoid overshooting. - ups[ox] = Clamp(result, min, max); + *ups[ox] = Clamp(result, min, max); } if (N == 2) { - StoreInterleaved(df, ups[0], ups[1], dst_row + x * N); + StoreInterleaved(df, ups0, ups1, dst_row + x * N); } if (N == 4) { - StoreInterleaved(df, ups[0], ups[1], ups[2], ups[3], dst_row + x * N); + StoreInterleaved(df, ups0, ups1, ups2, ups3, dst_row + x * N); } if (N == 8) { - StoreInterleaved(df, ups[0], ups[1], ups[2], ups[3], ups[4], ups[5], - ups[6], ups[7], dst_row + x * N); + StoreInterleaved(df, ups0, ups1, ups2, ups3, ups4, ups5, ups6, ups7, + dst_row + x * N); } } } diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_write.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_write.cc index fafa7c409d..71c4d97d55 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_write.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_write.cc @@ -5,6 +5,7 @@ #include "lib/jxl/render_pipeline/stage_write.h" +#include "lib/jxl/alpha.h" #include "lib/jxl/common.h" #include "lib/jxl/dec_cache.h" #include "lib/jxl/image_bundle.h" @@ -19,6 +20,12 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Clamp; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::NearestInt; +using hwy::HWY_NAMESPACE::U8FromU32; + template <typename D, typename V> void StoreRGBA(D d, V r, V g, V b, V a, bool alpha, size_t n, size_t extra, uint8_t* buf) { @@ -53,20 +60,20 @@ void StoreRGBA(D d, V r, V g, V b, V a, bool alpha, size_t n, size_t extra, // TODO(veluca): implement this for x86. size_t mul = alpha ? 4 : 3; HWY_ALIGN uint8_t bytes[16]; - Store(r, d, bytes); + StoreU(r, d, bytes); for (size_t i = 0; i < n; i++) { buf[mul * i] = bytes[i]; } - Store(g, d, bytes); + StoreU(g, d, bytes); for (size_t i = 0; i < n; i++) { buf[mul * i + 1] = bytes[i]; } - Store(b, d, bytes); + StoreU(b, d, bytes); for (size_t i = 0; i < n; i++) { buf[mul * i + 2] = bytes[i]; } if (alpha) { - Store(a, d, bytes); + StoreU(a, d, bytes); for (size_t i = 0; i < n; i++) { buf[4 * i + 3] = bytes[i]; } @@ -115,10 +122,10 @@ class WriteToU8Stage : public RenderPipelineStage { } for (ssize_t x = 0; x < x1; x += Lanes(d)) { - auto rf = Clamp(zero, Load(d, row_in_r + x), one) * mul; - auto gf = Clamp(zero, Load(d, row_in_g + x), one) * mul; - auto bf = Clamp(zero, Load(d, row_in_b + x), one) * mul; - auto af = row_in_a ? Clamp(zero, Load(d, row_in_a + x), one) * mul + auto rf = Mul(Clamp(zero, LoadU(d, row_in_r + x), one), mul); + auto gf = Mul(Clamp(zero, LoadU(d, row_in_g + x), one), mul); + auto bf = Mul(Clamp(zero, LoadU(d, row_in_b + x), one), mul); + auto af = row_in_a ? Mul(Clamp(zero, LoadU(d, row_in_a + x), one), mul) : Set(d, 255.0f); auto r8 = U8FromU32(BitCast(du, NearestInt(rf))); auto g8 = U8FromU32(BitCast(du, NearestInt(gf))); @@ -272,13 +279,14 @@ class WriteToPixelCallbackStage : public RenderPipelineStage { public: WriteToPixelCallbackStage(const PixelCallback& pixel_callback, size_t width, size_t height, bool rgba, bool has_alpha, - size_t alpha_c) + bool unpremul_alpha, size_t alpha_c) : RenderPipelineStage(RenderPipelineStage::Settings()), pixel_callback_(pixel_callback), width_(width), height_(height), rgba_(rgba), has_alpha_(has_alpha), + unpremul_alpha_(unpremul_alpha), alpha_c_(alpha_c), opaque_alpha_(kMaxPixelsPerCall, 1.0f) {} @@ -309,6 +317,7 @@ class WriteToPixelCallbackStage : public RenderPipelineStage { // No xextra offset; opaque_alpha_ is a way to set all values to 1.0f. line_buffers[3] = opaque_alpha_.data(); } + // TODO(veluca): SIMD. ssize_t limit = std::min(xextra + xsize, width_ - xpos); for (ssize_t x0 = -xextra; x0 < limit; x0 += kMaxPixelsPerCall) { @@ -324,6 +333,10 @@ class WriteToPixelCallbackStage : public RenderPipelineStage { temp[j++] = line_buffers[3][ix]; } } + if (has_alpha_ && rgba_ && unpremul_alpha_) { + // TODO(szabadka) SIMDify (possibly in a separate pipeline stage). + UnpremultiplyAlpha(temp, ix); + } pixel_callback_.run(run_opaque_, thread_id, xpos + x0, ypos, ix, temp); for (size_t c = 0; c < 3; c++) line_buffers[c] += kMaxPixelsPerCall; if (has_alpha_) line_buffers[3] += kMaxPixelsPerCall; @@ -357,6 +370,7 @@ class WriteToPixelCallbackStage : public RenderPipelineStage { size_t height_; bool rgba_; bool has_alpha_; + bool unpremul_alpha_; size_t alpha_c_; std::vector<float> opaque_alpha_; std::vector<CacheAlignedUniquePtr> temp_; @@ -385,9 +399,9 @@ std::unique_ptr<RenderPipelineStage> GetWriteToU8Stage(uint8_t* rgb, std::unique_ptr<RenderPipelineStage> GetWriteToPixelCallbackStage( const PixelCallback& pixel_callback, size_t width, size_t height, bool rgba, - bool has_alpha, size_t alpha_c) { + bool has_alpha, bool unpremul_alpha, size_t alpha_c) { return jxl::make_unique<WriteToPixelCallbackStage>( - pixel_callback, width, height, rgba, has_alpha, alpha_c); + pixel_callback, width, height, rgba, has_alpha, unpremul_alpha, alpha_c); } } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_write.h b/media/libjxl/src/lib/jxl/render_pipeline/stage_write.h index b5e152e9c7..b942fd6645 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_write.h +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_write.h @@ -30,7 +30,7 @@ std::unique_ptr<RenderPipelineStage> GetWriteToU8Stage(uint8_t* rgb, // Gets a stage to write to a pixel callback. std::unique_ptr<RenderPipelineStage> GetWriteToPixelCallbackStage( const PixelCallback& pixel_callback, size_t width, size_t height, bool rgba, - bool has_alpha, size_t alpha_c); + bool has_alpha, bool unpremul_alpha, size_t alpha_c); } // namespace jxl diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_xyb.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_xyb.cc index c943752109..0022a61127 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_xyb.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_xyb.cc @@ -11,110 +11,17 @@ #include <hwy/highway.h> #include "lib/jxl/dec_xyb-inl.h" -#include "lib/jxl/fast_math-inl.h" #include "lib/jxl/sanitizers.h" -#include "lib/jxl/transfer_functions-inl.h" HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { -template <typename Op> -struct PerChannelOp { - explicit PerChannelOp(Op op) : op(op) {} - template <typename D, typename T> - void Transform(D d, T* r, T* g, T* b) const { - *r = op.Transform(d, *r); - *g = op.Transform(d, *g); - *b = op.Transform(d, *b); - } - - Op op; -}; -template <typename Op> -PerChannelOp<Op> MakePerChannelOp(Op&& op) { - return PerChannelOp<Op>(std::forward<Op>(op)); -} - -struct OpLinear { - template <typename D, typename T> - T Transform(D d, const T& linear) const { - return linear; - } -}; - -struct OpRgb { - template <typename D, typename T> - T Transform(D d, const T& linear) const { -#if JXL_HIGH_PRECISION - return TF_SRGB().EncodedFromDisplay(d, linear); -#else - return FastLinearToSRGB(d, linear); -#endif - } -}; - -struct OpPq { - template <typename D, typename T> - T Transform(D d, const T& linear) const { - return TF_PQ().EncodedFromDisplay(d, linear); - } -}; - -struct OpHlg { - explicit OpHlg(const float luminances[3], const float intensity_target) - : luminances(luminances), exponent(1.0f) { - if (295 <= intensity_target && intensity_target <= 305) { - apply_inverse_ootf = false; - return; - } - exponent = - (1 / 1.2f) * std::pow(1.111f, -std::log2(intensity_target * 1e-3f)) - 1; - } - template <typename D, typename T> - void Transform(D d, T* r, T* g, T* b) const { - if (apply_inverse_ootf) { - const T luminance = Set(d, luminances[0]) * *r + - Set(d, luminances[1]) * *g + - Set(d, luminances[2]) * *b; - const T ratio = - Min(FastPowf(d, luminance, Set(d, exponent)), Set(d, 1e9)); - *r *= ratio; - *g *= ratio; - *b *= ratio; - } - *r = TF_HLG().EncodedFromDisplay(d, *r); - *g = TF_HLG().EncodedFromDisplay(d, *g); - *b = TF_HLG().EncodedFromDisplay(d, *b); - } - bool apply_inverse_ootf = true; - const float* luminances; - float exponent; -}; - -struct Op709 { - template <typename D, typename T> - T Transform(D d, const T& linear) const { - return TF_709().EncodedFromDisplay(d, linear); - } -}; - -struct OpGamma { - const float inverse_gamma; - template <typename D, typename T> - T Transform(D d, const T& linear) const { - return IfThenZeroElse(linear <= Set(d, 1e-5f), - FastPowf(d, linear, Set(d, inverse_gamma))); - } -}; - -template <typename Op> class XYBStage : public RenderPipelineStage { public: - XYBStage(OpsinParams opsin_params, Op op) + explicit XYBStage(const OpsinParams& opsin_params) : RenderPipelineStage(RenderPipelineStage::Settings()), - opsin_params_(opsin_params), - op_(op) {} + opsin_params_(opsin_params) {} void ProcessRow(const RowInfo& input_rows, const RowInfo& output_rows, size_t xextra, size_t xsize, size_t xpos, size_t ypos, @@ -122,6 +29,7 @@ class XYBStage : public RenderPipelineStage { PROFILER_ZONE("UndoXYB"); const HWY_FULL(float) d; + JXL_ASSERT(xextra == 0); const size_t xsize_v = RoundUpTo(xsize, Lanes(d)); float* JXL_RESTRICT row0 = GetInputRow(input_rows, 0, 0); float* JXL_RESTRICT row1 = GetInputRow(input_rows, 1, 0); @@ -132,19 +40,20 @@ class XYBStage : public RenderPipelineStage { msan::UnpoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); msan::UnpoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); msan::UnpoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + // TODO(eustas): when using frame origin, addresses might be unaligned; + // making them aligned will void performance penalty. for (ssize_t x = -xextra; x < (ssize_t)(xsize + xextra); x += Lanes(d)) { - const auto in_opsin_x = Load(d, row0 + x); - const auto in_opsin_y = Load(d, row1 + x); - const auto in_opsin_b = Load(d, row2 + x); + const auto in_opsin_x = LoadU(d, row0 + x); + const auto in_opsin_y = LoadU(d, row1 + x); + const auto in_opsin_b = LoadU(d, row2 + x); auto r = Undefined(d); auto g = Undefined(d); auto b = Undefined(d); XybToRgb(d, in_opsin_x, in_opsin_y, in_opsin_b, opsin_params_, &r, &g, &b); - op_.Transform(d, &r, &g, &b); - Store(r, d, row0 + x); - Store(g, d, row1 + x); - Store(b, d, row2 + x); + StoreU(r, d, row0 + x); + StoreU(g, d, row1 + x); + StoreU(b, d, row2 + x); } msan::PoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); msan::PoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); @@ -159,43 +68,12 @@ class XYBStage : public RenderPipelineStage { const char* GetName() const override { return "XYB"; } private: - OpsinParams opsin_params_; - Op op_; + const OpsinParams opsin_params_; }; -template <typename Op> -std::unique_ptr<XYBStage<Op>> MakeXYBStage(const OpsinParams& opsin_params, - Op&& op) { - return jxl::make_unique<XYBStage<Op>>(opsin_params, std::forward<Op>(op)); -} - std::unique_ptr<RenderPipelineStage> GetXYBStage( - const OutputEncodingInfo& output_encoding_info) { - if (output_encoding_info.color_encoding.tf.IsLinear()) { - return MakeXYBStage(output_encoding_info.opsin_params, - MakePerChannelOp(OpLinear())); - } else if (output_encoding_info.color_encoding.tf.IsSRGB()) { - return MakeXYBStage(output_encoding_info.opsin_params, - MakePerChannelOp(OpRgb())); - } else if (output_encoding_info.color_encoding.tf.IsPQ()) { - return MakeXYBStage(output_encoding_info.opsin_params, - MakePerChannelOp(OpPq())); - } else if (output_encoding_info.color_encoding.tf.IsHLG()) { - return MakeXYBStage(output_encoding_info.opsin_params, - OpHlg(output_encoding_info.luminances, - output_encoding_info.intensity_target)); - } else if (output_encoding_info.color_encoding.tf.Is709()) { - return MakeXYBStage(output_encoding_info.opsin_params, - MakePerChannelOp(Op709())); - } else if (output_encoding_info.color_encoding.tf.IsGamma() || - output_encoding_info.color_encoding.tf.IsDCI()) { - return MakeXYBStage( - output_encoding_info.opsin_params, - MakePerChannelOp(OpGamma{output_encoding_info.inverse_gamma})); - } else { - // This is a programming error. - JXL_ABORT("Invalid target encoding"); - } + const OpsinParams& opsin_params) { + return jxl::make_unique<XYBStage>(opsin_params); } // NOLINTNEXTLINE(google-readability-namespace-comments) @@ -209,8 +87,8 @@ namespace jxl { HWY_EXPORT(GetXYBStage); std::unique_ptr<RenderPipelineStage> GetXYBStage( - const OutputEncodingInfo& output_encoding_info) { - return HWY_DYNAMIC_DISPATCH(GetXYBStage)(output_encoding_info); + const OpsinParams& opsin_params) { + return HWY_DYNAMIC_DISPATCH(GetXYBStage)(opsin_params); } namespace { diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_xyb.h b/media/libjxl/src/lib/jxl/render_pipeline/stage_xyb.h index ff4fdd20a4..2bc5075193 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_xyb.h +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_xyb.h @@ -5,22 +5,16 @@ #ifndef LIB_JXL_RENDER_PIPELINE_STAGE_XYB_H_ #define LIB_JXL_RENDER_PIPELINE_STAGE_XYB_H_ -#include <math.h> #include <stdint.h> -#include <stdio.h> - -#include <algorithm> -#include <utility> -#include <vector> #include "lib/jxl/dec_xyb.h" #include "lib/jxl/render_pipeline/render_pipeline_stage.h" namespace jxl { -// Converts the color channels from XYB to the specified output encoding. +// Converts the color channels from XYB to linear with appropriate primaries. std::unique_ptr<RenderPipelineStage> GetXYBStage( - const OutputEncodingInfo& output_encoding_info); + const OpsinParams& output_encoding_info); // Gets a stage to convert with fixed point arithmetic from XYB to sRGB8 and // write to a uint8 buffer. diff --git a/media/libjxl/src/lib/jxl/render_pipeline/stage_ycbcr.cc b/media/libjxl/src/lib/jxl/render_pipeline/stage_ycbcr.cc index fa8b2a907b..5cba4a7d41 100644 --- a/media/libjxl/src/lib/jxl/render_pipeline/stage_ycbcr.cc +++ b/media/libjxl/src/lib/jxl/render_pipeline/stage_ycbcr.cc @@ -14,6 +14,10 @@ HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::MulAdd; + class kYCbCrStage : public RenderPipelineStage { public: kYCbCrStage() : RenderPipelineStage(RenderPipelineStage::Settings()) {} @@ -36,16 +40,18 @@ class kYCbCrStage : public RenderPipelineStage { float* JXL_RESTRICT row0 = GetInputRow(input_rows, 0, 0); float* JXL_RESTRICT row1 = GetInputRow(input_rows, 1, 0); float* JXL_RESTRICT row2 = GetInputRow(input_rows, 2, 0); + // TODO(eustas): when using frame origin, addresses might be unaligned; + // making them aligned will void performance penalty. for (size_t x = 0; x < xsize; x += Lanes(df)) { - const auto y_vec = Load(df, row1 + x) + c128; - const auto cb_vec = Load(df, row0 + x); - const auto cr_vec = Load(df, row2 + x); - const auto r_vec = crcr * cr_vec + y_vec; - const auto g_vec = cgcr * cr_vec + cgcb * cb_vec + y_vec; - const auto b_vec = cbcb * cb_vec + y_vec; - Store(r_vec, df, row0 + x); - Store(g_vec, df, row1 + x); - Store(b_vec, df, row2 + x); + const auto y_vec = Add(LoadU(df, row1 + x), c128); + const auto cb_vec = LoadU(df, row0 + x); + const auto cr_vec = LoadU(df, row2 + x); + const auto r_vec = MulAdd(crcr, cr_vec, y_vec); + const auto g_vec = MulAdd(cgcr, cr_vec, MulAdd(cgcb, cb_vec, y_vec)); + const auto b_vec = MulAdd(cbcb, cb_vec, y_vec); + StoreU(r_vec, df, row0 + x); + StoreU(g_vec, df, row1 + x); + StoreU(b_vec, df, row2 + x); } } diff --git a/media/libjxl/src/lib/jxl/roundtrip_test.cc b/media/libjxl/src/lib/jxl/roundtrip_test.cc index bd3c3f2866..d057172665 100644 --- a/media/libjxl/src/lib/jxl/roundtrip_test.cc +++ b/media/libjxl/src/lib/jxl/roundtrip_test.cc @@ -101,7 +101,7 @@ jxl::CodecInOut ConvertTestImage(const std::vector<uint8_t>& buf, color_encoding, pixel_format.num_channels, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/bitdepth, pixel_format.endianness, - /*flipped_y=*/false, /*pool=*/nullptr, &io.Main(), float_in, + /*pool=*/nullptr, &io.Main(), float_in, /*align=*/0)); return io; } @@ -729,7 +729,7 @@ TEST(RoundtripTest, TestICCProfile) { jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &format); basic_info.xsize = xsize; basic_info.ysize = ysize; - basic_info.uses_original_profile = JXL_FALSE; + basic_info.uses_original_profile = JXL_TRUE; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info)); EXPECT_EQ(JXL_ENC_SUCCESS, @@ -800,8 +800,7 @@ TEST(RoundtripTest, TestICCProfile) { #if JPEGXL_ENABLE_JPEG // Loading .jpg files requires libjpeg support. TEST(RoundtripTest, JXL_TRANSCODE_JPEG_TEST(TestJPEGReconstruction)) { - const std::string jpeg_path = - "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg"; const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); jxl::CodecInOut orig_io; ASSERT_TRUE( diff --git a/media/libjxl/src/lib/jxl/sanitizers.h b/media/libjxl/src/lib/jxl/sanitizers.h index 5cd501798e..ce0bd8dc63 100644 --- a/media/libjxl/src/lib/jxl/sanitizers.h +++ b/media/libjxl/src/lib/jxl/sanitizers.h @@ -169,7 +169,7 @@ static JXL_INLINE JXL_MAYBE_UNUSED void PrintImageUninitialized( // (not poisoned). If any of the values is poisoned it will abort. template <typename T> static JXL_INLINE JXL_MAYBE_UNUSED void CheckImageInitialized( - const Plane<T>& im, const Rect& r, const char* message) { + const Plane<T>& im, const Rect& r, size_t c, const char* message) { JXL_ASSERT(r.x0() <= im.xsize()); JXL_ASSERT(r.x0() + r.xsize() <= im.xsize()); JXL_ASSERT(r.y0() <= im.ysize()); @@ -188,10 +188,11 @@ static JXL_INLINE JXL_MAYBE_UNUSED void CheckImageInitialized( static_cast<uint64_t>(r.x0()), static_cast<uint64_t>(r.y0()), static_cast<uint64_t>(r.xsize()), static_cast<uint64_t>(r.ysize())); size_t x = ret / sizeof(*row); - JXL_DEBUG( - 1, "CheckImageInitialized failed at x=%" PRIu64 ", y=%" PRIu64 ": %s", - static_cast<uint64_t>(r.x0() + x), static_cast<uint64_t>(y), - message ? message : ""); + JXL_DEBUG(1, + "CheckImageInitialized failed at x=%" PRIu64 ", y=%" PRIu64 + ", c=%" PRIu64 ": %s", + static_cast<uint64_t>(r.x0() + x), static_cast<uint64_t>(y), + static_cast<uint64_t>(c), message ? message : ""); PrintImageUninitialized(im); } // This will report an error if memory is not initialized. @@ -205,13 +206,16 @@ static JXL_INLINE JXL_MAYBE_UNUSED void CheckImageInitialized( for (size_t c = 0; c < 3; c++) { std::string str_message(message); str_message += " c=" + std::to_string(c); - CheckImageInitialized(im.Plane(c), r, str_message.c_str()); + CheckImageInitialized(im.Plane(c), r, c, str_message.c_str()); } } #define JXL_CHECK_IMAGE_INITIALIZED(im, r) \ ::jxl::msan::CheckImageInitialized(im, r, "im=" #im ", r=" #r); +#define JXL_CHECK_PLANE_INITIALIZED(im, r, c) \ + ::jxl::msan::CheckImageInitialized(im, r, c, "im=" #im ", r=" #r ", c=" #c); + #else // JXL_MEMORY_SANITIZER // In non-msan mode these functions don't use volatile since it is not needed @@ -228,6 +232,7 @@ template <typename T> static JXL_INLINE JXL_MAYBE_UNUSED void PoisonImage(const Plane<T>& im) {} #define JXL_CHECK_IMAGE_INITIALIZED(im, r) +#define JXL_CHECK_PLANE_INITIALIZED(im, r, c) #endif diff --git a/media/libjxl/src/lib/jxl/speed_tier_test.cc b/media/libjxl/src/lib/jxl/speed_tier_test.cc index 6005f629e7..9c120fe54c 100644 --- a/media/libjxl/src/lib/jxl/speed_tier_test.cc +++ b/media/libjxl/src/lib/jxl/speed_tier_test.cc @@ -12,8 +12,6 @@ #include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/base/thread_pool_internal.h" #include "lib/jxl/codec_in_out.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/enc_butteraugli_comparator.h" #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_file.h" @@ -85,7 +83,7 @@ JXL_GTEST_INSTANTIATE_TEST_SUITE_P( TEST_P(SpeedTierTest, Roundtrip) { const PaddedBytes orig = - ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CodecInOut io; ThreadPoolInternal pool(8); ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool)); @@ -98,10 +96,9 @@ TEST_P(SpeedTierTest, Roundtrip) { CompressParams cparams; cparams.speed_tier = params.speed_tier; - DecompressParams dparams; CodecInOut io2; - test::Roundtrip(&io, cparams, dparams, nullptr, &io2); + test::Roundtrip(&io, cparams, {}, nullptr, &io2); // Can be 2.2 in non-hare mode. EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), diff --git a/media/libjxl/src/lib/jxl/splines.cc b/media/libjxl/src/lib/jxl/splines.cc index 11613fc667..edaaf2738d 100644 --- a/media/libjxl/src/lib/jxl/splines.cc +++ b/media/libjxl/src/lib/jxl/splines.cc @@ -28,6 +28,13 @@ namespace jxl { namespace HWY_NAMESPACE { namespace { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::MulAdd; +using hwy::HWY_NAMESPACE::MulSub; +using hwy::HWY_NAMESPACE::Sqrt; +using hwy::HWY_NAMESPACE::Sub; + // Given a set of DCT coefficients, this returns the result of performing cosine // interpolation on the original samples. float ContinuousIDCT(const float dct[32], const float t) { @@ -48,9 +55,9 @@ float ContinuousIDCT(const float dct[32], const float t) { auto result = Zero(df); const auto tandhalf = Set(df, t + 0.5f); for (int i = 0; i < 32; i += Lanes(df)) { - auto cos_arg = LoadU(df, kMultipliers + i) * tandhalf; + auto cos_arg = Mul(LoadU(df, kMultipliers + i), tandhalf); auto cos = FastCosf(df, cos_arg); - auto local_res = LoadU(df, dct + i) * cos; + auto local_res = Mul(LoadU(df, dct + i), cos); result = MulAdd(Set(df, kSqrt2), local_res, result); } return GetLane(SumOfLanes(df, result)); @@ -65,15 +72,16 @@ void DrawSegment(DF df, const SplineSegment& segment, const bool add, const auto one_over_2s2 = Set(df, 0.353553391f); const auto sigma_over_4_times_intensity = Set(df, segment.sigma_over_4_times_intensity); - const auto dx = ConvertTo(df, Iota(di, x)) - Set(df, segment.center_x); + const auto dx = Sub(ConvertTo(df, Iota(di, x)), Set(df, segment.center_x)); const auto dy = Set(df, y - segment.center_y); - const auto sqd = MulAdd(dx, dx, dy * dy); + const auto sqd = MulAdd(dx, dx, Mul(dy, dy)); const auto distance = Sqrt(sqd); const auto one_dimensional_factor = - FastErff(df, MulAdd(distance, half, one_over_2s2) * inv_sigma) - - FastErff(df, MulSub(distance, half, one_over_2s2) * inv_sigma); - auto local_intensity = sigma_over_4_times_intensity * one_dimensional_factor * - one_dimensional_factor; + Sub(FastErff(df, Mul(MulAdd(distance, half, one_over_2s2), inv_sigma)), + FastErff(df, Mul(MulSub(distance, half, one_over_2s2), inv_sigma))); + auto local_intensity = + Mul(sigma_over_4_times_intensity, + Mul(one_dimensional_factor, one_dimensional_factor)); for (size_t c = 0; c < 3; ++c) { const auto cm = Set(df, add ? segment.color[c] : -segment.color[c]); const auto in = LoadU(df, rows[c] + x); diff --git a/media/libjxl/src/lib/jxl/splines_test.cc b/media/libjxl/src/lib/jxl/splines_test.cc index 558db32d85..09b2dd5623 100644 --- a/media/libjxl/src/lib/jxl/splines_test.cc +++ b/media/libjxl/src/lib/jxl/splines_test.cc @@ -5,15 +5,13 @@ #include "lib/jxl/splines.h" -#include "gmock/gmock.h" -#include "gtest/gtest.h" #include "lib/extras/codec.h" #include "lib/jxl/base/printf_macros.h" -#include "lib/jxl/dec_file.h" #include "lib/jxl/enc_butteraugli_comparator.h" #include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_splines.h" #include "lib/jxl/image_test_utils.h" +#include "lib/jxl/test_utils.h" #include "lib/jxl/testdata.h" namespace jxl { @@ -328,8 +326,8 @@ TEST(SplinesTest, ClearedEveryFrame) { CodecInOut io_actual; const PaddedBytes bytes_actual = ReadTestData("jxl/spline_on_first_frame.jxl"); - ASSERT_TRUE(DecodeFile(DecompressParams(), bytes_actual, &io_actual, - /*pool=*/nullptr)); + ASSERT_TRUE(test::DecodeFile({}, bytes_actual, &io_actual, + /*pool=*/nullptr)); ASSERT_TRUE(io_actual.TransformTo(ColorEncoding::SRGB(), GetJxlCms())); for (size_t c = 0; c < 3; ++c) { diff --git a/media/libjxl/src/lib/jxl/test_image.h b/media/libjxl/src/lib/jxl/test_image.h new file mode 100644 index 0000000000..0093443682 --- /dev/null +++ b/media/libjxl/src/lib/jxl/test_image.h @@ -0,0 +1,103 @@ +// Copyright (c) the JPEG XL Project 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 LIB_JXL_TEST_IMAGE_H_ +#define LIB_JXL_TEST_IMAGE_H_ + +#include <stdint.h> + +#include <vector> + +#include "lib/jxl/base/random.h" + +namespace jxl { +namespace test { + +// Returns a test image with some autogenerated pixel content, using 16 bits per +// channel, big endian order, 1 to 4 channels +// The seed parameter allows to create images with different pixel content. +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. + 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 a0 = rng(32768); + uint16_t r1 = rng(32768); + uint16_t g1 = rng(32768); + uint16_t b1 = rng(32768); + uint16_t a1 = 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; + // 16 bits per channel, big endian, 4 channels + std::vector<uint8_t> pixels(num_pixels * num_channels * 2); + // Create pixel content to test, actual content does not matter as long as it + // can be compared after roundtrip. + 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; + uint16_t a = a0 * (ysize - y - 1) / ysize + a1 * 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; + a = 32768 + x * 256 - y; + } else if (x > rect_x0 && x < rect_x1 && y > rect_y0 && y < rect_y1) { + r = rng(65536); + g = rng(65536); + b = rng(65536); + a = rng(65536); + } + size_t i = (y * xsize + x) * 2 * num_channels; + pixels[i + 0] = (r >> 8); + pixels[i + 1] = (r & 255); + if (num_channels >= 2) { + // This may store what is called 'g' in the alpha channel of a 2-channel + // image, but that's ok since the content is arbitrary + pixels[i + 2] = (g >> 8); + pixels[i + 3] = (g & 255); + } + if (num_channels >= 3) { + pixels[i + 4] = (b >> 8); + pixels[i + 5] = (b & 255); + } + if (num_channels >= 4) { + pixels[i + 6] = (a >> 8); + pixels[i + 7] = (a & 255); + } + } + } + return pixels; +} + +} // namespace test +} // namespace jxl + +#endif // LIB_JXL_TEST_IMAGE_H_ diff --git a/media/libjxl/src/lib/jxl/test_utils.h b/media/libjxl/src/lib/jxl/test_utils.h index b1cc84e87b..b55cc3d205 100644 --- a/media/libjxl/src/lib/jxl/test_utils.h +++ b/media/libjxl/src/lib/jxl/test_utils.h @@ -8,22 +8,30 @@ // Macros and functions useful for tests. +// gmock unconditionally redefines those macros (to wrong values). +// Lets include it only here and mitigate the problem. +#pragma push_macro("PRIdS") +#pragma push_macro("PRIuS") #include "gmock/gmock.h" +#pragma pop_macro("PRIuS") +#pragma pop_macro("PRIdS") + #include "gtest/gtest.h" #include "jxl/codestream_header.h" #include "jxl/encode.h" +#include "lib/extras/dec/jxl.h" +#include "lib/extras/packed_image_convert.h" #include "lib/jxl/aux_out_fwd.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/random.h" #include "lib/jxl/codec_in_out.h" #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/common.h" // JPEGXL_ENABLE_TRANSCODE_JPEG -#include "lib/jxl/dec_file.h" -#include "lib/jxl/dec_params.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/test_image.h" #ifdef JXL_DISABLE_SLOW_TESTS #define JXL_SLOW_TEST(X) DISABLED_##X @@ -81,6 +89,11 @@ void JxlBasicInfoSetFromPixelFormat(JxlBasicInfo* basic_info, default: JXL_ABORT("Unhandled JxlDataType"); } + if (pixel_format->num_channels < 3) { + basic_info->num_color_channels = 1; + } else { + basic_info->num_color_channels = 3; + } if (pixel_format->num_channels == 2 || pixel_format->num_channels == 4) { basic_info->alpha_exponent_bits = basic_info->exponent_bits_per_sample; basic_info->alpha_bits = basic_info->bits_per_sample; @@ -92,8 +105,9 @@ void JxlBasicInfoSetFromPixelFormat(JxlBasicInfo* basic_info, } MATCHER_P(MatchesPrimariesAndTransferFunction, color_encoding, "") { - return arg.primaries == color_encoding.primaries && - arg.tf.IsSame(color_encoding.tf); + return (arg.ICC() == color_encoding.ICC() || + (arg.primaries == color_encoding.primaries && + arg.tf.IsSame(color_encoding.tf))); } MATCHER(MatchesPrimariesAndTransferFunction, "") { @@ -102,9 +116,23 @@ MATCHER(MatchesPrimariesAndTransferFunction, "") { result_listener); } +template <typename Source> +Status DecodeFile(extras::JXLDecompressParams dparams, const Source& file, + CodecInOut* JXL_RESTRICT io, ThreadPool* pool) { + if (pool && !dparams.runner_opaque) { + dparams.runner = pool->runner(); + dparams.runner_opaque = pool->runner_opaque(); + } + extras::PackedPixelFile ppf; + JXL_RETURN_IF_ERROR(DecodeImageJXL(file.data(), file.size(), dparams, + /*decoded_bytes=*/nullptr, &ppf)); + JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); + return true; +} + // Returns compressed size [bytes]. size_t Roundtrip(const CodecInOut* io, const CompressParams& cparams, - const DecompressParams& dparams, ThreadPool* pool, + extras::JXLDecompressParams dparams, ThreadPool* pool, CodecInOut* JXL_RESTRICT io2, AuxOut* aux_out = nullptr) { PaddedBytes compressed; @@ -208,6 +236,7 @@ static inline ColorEncoding ColorEncodingFromDescriptor( c.primaries = desc.primaries; c.tf.SetTransferFunction(desc.tf); c.rendering_intent = desc.rendering_intent; + JXL_CHECK(c.CreateICC()); return c; } @@ -264,88 +293,6 @@ std::vector<ColorEncodingDescriptor> AllEncodings() { return all_encodings; } -// Returns a test image with some autogenerated pixel content, using 16 bits per -// channel, big endian order, 1 to 4 channels -// The seed parameter allows to create images with different pixel content. -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. - 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 a0 = rng(32768); - uint16_t r1 = rng(32768); - uint16_t g1 = rng(32768); - uint16_t b1 = rng(32768); - uint16_t a1 = 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; - // 16 bits per channel, big endian, 4 channels - std::vector<uint8_t> pixels(num_pixels * num_channels * 2); - // Create pixel content to test, actual content does not matter as long as it - // can be compared after roundtrip. - 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; - uint16_t a = a0 * (ysize - y - 1) / ysize + a1 * 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; - a = 32768 + x * 256 - y; - } else if (x > rect_x0 && x < rect_x1 && y > rect_y0 && y < rect_y1) { - r = rng(65536); - g = rng(65536); - b = rng(65536); - a = rng(65536); - } - size_t i = (y * xsize + x) * 2 * num_channels; - pixels[i + 0] = (r >> 8); - pixels[i + 1] = (r & 255); - if (num_channels >= 2) { - // This may store what is called 'g' in the alpha channel of a 2-channel - // image, but that's ok since the content is arbitrary - pixels[i + 2] = (g >> 8); - pixels[i + 3] = (g & 255); - } - if (num_channels >= 3) { - pixels[i + 4] = (b >> 8); - pixels[i + 5] = (b & 255); - } - if (num_channels >= 4) { - pixels[i + 6] = (a >> 8); - pixels[i + 7] = (a & 255); - } - } - } - return pixels; -} - // Returns a CodecInOut based on the buf, xsize, ysize, and the assumption // that the buffer was created using `GetSomeTestImage`. jxl::CodecInOut SomeTestImageToCodecInOut(const std::vector<uint8_t>& buf, @@ -360,7 +307,7 @@ jxl::CodecInOut SomeTestImageToCodecInOut(const std::vector<uint8_t>& buf, jxl::Span<const uint8_t>(buf.data(), buf.size()), xsize, ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/num_channels < 3), num_channels, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN, - /*flipped_y=*/false, /*pool=*/nullptr, + /*pool=*/nullptr, /*ib=*/&io.Main(), /*float_in=*/false, 0)); return io; } @@ -454,8 +401,8 @@ size_t GetDataBits(JxlDataType data_type) { // precisions needed for the maximum data types the API supports: uint32_t // integers, and, single precision float. The values are in range 0-1 for SDR. std::vector<double> ConvertToRGBA32(const uint8_t* pixels, size_t xsize, - size_t ysize, - const JxlPixelFormat& format) { + size_t ysize, const JxlPixelFormat& format, + double factor = 0.0) { std::vector<double> result(xsize * ysize * 4); size_t num_channels = format.num_channels; bool gray = num_channels == 1 || num_channels == 2; @@ -467,7 +414,8 @@ std::vector<double> ConvertToRGBA32(const uint8_t* pixels, size_t xsize, if (format.align > 1) stride = jxl::RoundUpTo(stride, format.align); if (format.data_type == JXL_TYPE_UINT8) { - double mul = 1.0 / 255.0; // Multiplier to bring to 0-1.0 range + // Multiplier to bring to 0-1.0 range + double mul = factor > 0.0 ? factor : 1.0 / 255.0; for (size_t y = 0; y < ysize; ++y) { for (size_t x = 0; x < xsize; ++x) { size_t j = (y * xsize + x) * 4; @@ -483,7 +431,8 @@ std::vector<double> ConvertToRGBA32(const uint8_t* pixels, size_t xsize, } } } else if (format.data_type == JXL_TYPE_UINT16) { - double mul = 1.0 / 65535.0; // Multiplier to bring to 0-1.0 range + // Multiplier to bring to 0-1.0 range + double mul = factor > 0.0 ? factor : 1.0 / 65535.0; for (size_t y = 0; y < ysize; ++y) { for (size_t x = 0; x < xsize; ++x) { size_t j = (y * xsize + x) * 4; @@ -624,6 +573,26 @@ size_t ComparePixels(const uint8_t* a, const uint8_t* b, size_t xsize, } return numdiff; } +double DistanceRMS(const uint8_t* a, const uint8_t* b, size_t xsize, + size_t ysize, const JxlPixelFormat& format) { + // Convert both images to equal full precision for comparison. + std::vector<double> a_full = ConvertToRGBA32(a, xsize, ysize, format); + std::vector<double> b_full = ConvertToRGBA32(b, xsize, ysize, format); + double sum = 0.0; + for (size_t y = 0; y < ysize; y++) { + double row_sum = 0.0; + for (size_t x = 0; x < xsize; x++) { + size_t i = (y * xsize + x) * 4; + for (size_t c = 0; c < format.num_channels; ++c) { + double diff = a_full[i + c] - b_full[i + c]; + row_sum += diff * diff; + } + } + sum += row_sum; + } + sum /= (xsize * ysize); + return sqrt(sum); +} } // namespace test bool operator==(const jxl::PaddedBytes& a, const jxl::PaddedBytes& b) { diff --git a/media/libjxl/src/lib/jxl/testdata.h b/media/libjxl/src/lib/jxl/testdata.h index 28d1015d0b..d387219bb8 100644 --- a/media/libjxl/src/lib/jxl/testdata.h +++ b/media/libjxl/src/lib/jxl/testdata.h @@ -19,39 +19,7 @@ namespace jxl { static inline PaddedBytes ReadTestData(const std::string& filename) { std::string full_path = std::string(TEST_DATA_PATH "/") + filename; PaddedBytes data; - bool ok = ReadFile(full_path, &data); -#ifdef __EMSCRIPTEN__ - // Fallback in case FS is not supported in current JS engine. - if (!ok) { - // {size_t size, uint8_t* bytes} pair. - uint32_t size_bytes[2] = {0, 0}; - EM_ASM( - { - let buffer = null; - try { - buffer = readbuffer(UTF8ToString($0)); - } catch { - } - if (!buffer) return; - let bytes = new Uint8Array(buffer); - let size = bytes.length; - let out = _malloc(size); - if (!out) return; - HEAP8.set(bytes, out); - HEAP32[$1 >> 2] = size; - HEAP32[($1 + 4) >> 2] = out; - }, - full_path.c_str(), size_bytes); - size_t size = size_bytes[0]; - uint8_t* bytes = reinterpret_cast<uint8_t*>(size_bytes[1]); - if (size) { - data.append(bytes, bytes + size); - free(reinterpret_cast<void*>(bytes)); - ok = true; - } - } -#endif - JXL_CHECK(ok); + JXL_CHECK(ReadFile(full_path, &data)); return data; } diff --git a/media/libjxl/src/lib/jxl/toc.cc b/media/libjxl/src/lib/jxl/toc.cc index 7d48c1a5e0..24cdd02c65 100644 --- a/media/libjxl/src/lib/jxl/toc.cc +++ b/media/libjxl/src/lib/jxl/toc.cc @@ -20,10 +20,9 @@ size_t MaxBits(const size_t num_sizes) { return 1 + kBitsPerByte + entry_bits + kBitsPerByte; } -Status ReadGroupOffsets(size_t toc_entries, BitReader* JXL_RESTRICT reader, - std::vector<uint64_t>* JXL_RESTRICT offsets, - std::vector<uint32_t>* JXL_RESTRICT sizes, - uint64_t* total_size) { +Status ReadToc(size_t toc_entries, BitReader* JXL_RESTRICT reader, + std::vector<uint32_t>* JXL_RESTRICT sizes, + std::vector<coeff_order_t>* JXL_RESTRICT permutation) { if (toc_entries > 65536) { // Prevent out of memory if invalid JXL codestream causes a bogus amount // of toc_entries such as 2720436919446 to be computed. @@ -33,8 +32,6 @@ Status ReadGroupOffsets(size_t toc_entries, BitReader* JXL_RESTRICT reader, sizes->clear(); sizes->resize(toc_entries); - offsets->clear(); - offsets->resize(toc_entries); if (reader->TotalBitsConsumed() >= reader->TotalBytes() * kBitsPerByte) { return JXL_STATUS(StatusCode::kNotEnoughBytes, "Not enough bytes for TOC"); } @@ -51,16 +48,13 @@ Status ReadGroupOffsets(size_t toc_entries, BitReader* JXL_RESTRICT reader, return JXL_STATUS(StatusCode::kNotEnoughBytes, "Not enough bytes for TOC"); }; - JXL_DASSERT(offsets != nullptr && sizes != nullptr); - std::vector<coeff_order_t> permutation; - if (reader->ReadFixedBits<1>() == 1 && toc_entries > 0) { - // Skip permutation description if the toc_entries is 0. + JXL_DASSERT(toc_entries > 0); + if (reader->ReadFixedBits<1>() == 1) { JXL_RETURN_IF_ERROR(check_bit_budget(toc_entries)); - permutation.resize(toc_entries); - JXL_RETURN_IF_ERROR( - DecodePermutation(/*skip=*/0, toc_entries, permutation.data(), reader)); + permutation->resize(toc_entries); + JXL_RETURN_IF_ERROR(DecodePermutation(/*skip=*/0, toc_entries, + permutation->data(), reader)); } - JXL_RETURN_IF_ERROR(reader->JumpToByteBoundary()); JXL_RETURN_IF_ERROR(check_bit_budget(toc_entries)); for (size_t i = 0; i < toc_entries; ++i) { @@ -68,6 +62,18 @@ Status ReadGroupOffsets(size_t toc_entries, BitReader* JXL_RESTRICT reader, } JXL_RETURN_IF_ERROR(reader->JumpToByteBoundary()); JXL_RETURN_IF_ERROR(check_bit_budget(0)); + return true; +} + +Status ReadGroupOffsets(size_t toc_entries, BitReader* JXL_RESTRICT reader, + std::vector<uint64_t>* JXL_RESTRICT offsets, + std::vector<uint32_t>* JXL_RESTRICT sizes, + uint64_t* total_size) { + std::vector<coeff_order_t> permutation; + JXL_RETURN_IF_ERROR(ReadToc(toc_entries, reader, sizes, &permutation)); + + offsets->clear(); + offsets->resize(toc_entries); // Prefix sum starting with 0 and ending with the offset of the last group uint64_t offset = 0; diff --git a/media/libjxl/src/lib/jxl/toc.h b/media/libjxl/src/lib/jxl/toc.h index ffebdf9115..a97197ad45 100644 --- a/media/libjxl/src/lib/jxl/toc.h +++ b/media/libjxl/src/lib/jxl/toc.h @@ -13,6 +13,7 @@ #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order_fwd.h" #include "lib/jxl/dec_bit_reader.h" #include "lib/jxl/field_encodings.h" @@ -40,6 +41,10 @@ static JXL_INLINE size_t NumTocEntries(size_t num_groups, size_t num_dc_groups, num_groups * num_passes; } +Status ReadToc(size_t toc_entries, BitReader* JXL_RESTRICT reader, + std::vector<uint32_t>* JXL_RESTRICT sizes, + std::vector<coeff_order_t>* JXL_RESTRICT permutation); + Status ReadGroupOffsets(size_t toc_entries, BitReader* JXL_RESTRICT reader, std::vector<uint64_t>* JXL_RESTRICT offsets, std::vector<uint32_t>* JXL_RESTRICT sizes, diff --git a/media/libjxl/src/lib/jxl/toc_test.cc b/media/libjxl/src/lib/jxl/toc_test.cc index 62eec5a101..2f3bf5b231 100644 --- a/media/libjxl/src/lib/jxl/toc_test.cc +++ b/media/libjxl/src/lib/jxl/toc_test.cc @@ -81,7 +81,7 @@ void Roundtrip(size_t num_entries, bool permute, Rng* rng) { TEST(TocTest, Test) { Rng rng(0); - for (size_t num_entries = 0; num_entries < 10; ++num_entries) { + for (size_t num_entries = 1; num_entries < 10; ++num_entries) { for (bool permute : std::vector<bool>{false, true}) { Roundtrip(num_entries, permute, &rng); } diff --git a/media/libjxl/src/lib/jxl/transfer_functions-inl.h b/media/libjxl/src/lib/jxl/transfer_functions-inl.h index c40eafca26..9f4c10c76d 100644 --- a/media/libjxl/src/lib/jxl/transfer_functions-inl.h +++ b/media/libjxl/src/lib/jxl/transfer_functions-inl.h @@ -18,12 +18,23 @@ #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/status.h" +#include "lib/jxl/fast_math-inl.h" #include "lib/jxl/rational_polynomial-inl.h" HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::And; +using hwy::HWY_NAMESPACE::AndNot; +using hwy::HWY_NAMESPACE::Gt; +using hwy::HWY_NAMESPACE::IfThenElse; +using hwy::HWY_NAMESPACE::Lt; +using hwy::HWY_NAMESPACE::Or; +using hwy::HWY_NAMESPACE::Sqrt; +using hwy::HWY_NAMESPACE::TableLookupBytes; + // Definitions for BT.2100-2 transfer functions (used inside/outside SIMD): // "display" is linear light (nits) normalized to [0, 1]. // "encoded" is a nonlinear encoding (e.g. PQ) in [0, 1]. @@ -56,11 +67,11 @@ class TF_HLG { const V kSign = BitCast(d, Set(du, 0x80000000u)); const V original_sign = And(x, kSign); x = AndNot(kSign, x); // abs - const V below_div12 = Sqrt(Set(d, 3.0f) * x); + const V below_div12 = Sqrt(Mul(Set(d, 3.0f), x)); const V e = MulAdd(Set(d, kA * 0.693147181f), FastLog2f(d, MulAdd(Set(d, 12), x, Set(d, -kB))), Set(d, kC)); - const V magnitude = IfThenElse(x <= Set(d, kDiv12), below_div12, e); + const V magnitude = IfThenElse(Le(x, Set(d, kDiv12)), below_div12, e); return Or(AndNot(kSign, magnitude), original_sign); } @@ -123,10 +134,18 @@ class TF_709 { // Maximum error 1e-6. template <class D, class V> JXL_INLINE V EncodedFromDisplay(D d, V x) const { - auto low = Set(d, kMulLow) * x; + auto low = Mul(Set(d, kMulLow), x); auto hi = MulAdd(Set(d, kMulHi), FastPowf(d, x, Set(d, kPowHi)), Set(d, kSub)); - return IfThenElse(x <= Set(d, kThresh), low, hi); + return IfThenElse(Le(x, Set(d, kThresh)), low, hi); + } + + template <class D, class V> + JXL_INLINE V DisplayFromEncoded(D d, V x) const { + auto low = Mul(Set(d, kInvMulLow), x); + auto hi = FastPowf(d, MulAdd(x, Set(d, kInvMulHi), Set(d, kInvAdd)), + Set(d, kInvPowHi)); + return IfThenElse(Lt(x, Set(d, kInvThresh)), low, hi); } private: @@ -135,6 +154,12 @@ class TF_709 { static constexpr double kMulHi = 1.099; static constexpr double kPowHi = 0.45; static constexpr double kSub = -0.099; + + static constexpr double kInvThresh = 0.081; + static constexpr double kInvMulLow = 1 / 4.5; + static constexpr double kInvMulHi = 1 / 1.099; + static constexpr double kInvPowHi = 1 / 0.45; + static constexpr double kInvAdd = 0.099 * kInvMulHi; }; // Perceptual Quantization @@ -225,7 +250,7 @@ class TF_PQ { HWY_REP4(-2.072546e+05f), }; - auto magnitude = IfThenElse(x < Set(d, 1e-4f), + auto magnitude = IfThenElse(Lt(x, Set(d, 1e-4f)), EvalRationalPolynomial(d, xto025, plo, qlo), EvalRationalPolynomial(d, xto025, p, q)); return Or(AndNot(kSign, magnitude), original_sign); @@ -268,10 +293,10 @@ class TF_SRGB { -5.512498495e-02f, 6.521209011e-03f, 6.521209011e-03f, 6.521209011e-03f, 6.521209011e-03f, }; - const V linear = x * Set(d, kLowDivInv); + const V linear = Mul(x, Set(d, kLowDivInv)); const V poly = EvalRationalPolynomial(d, x, p, q); const V magnitude = - IfThenElse(x > Set(d, kThreshSRGBToLinear), poly, linear); + IfThenElse(Gt(x, Set(d, kThreshSRGBToLinear)), poly, linear); return Or(AndNot(kSign, magnitude), original_sign); } @@ -300,10 +325,10 @@ class TF_SRGB { 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f, 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f, }; - const V linear = x * Set(d, kLowDiv); + const V linear = Mul(x, Set(d, kLowDiv)); const V poly = EvalRationalPolynomial(d, Sqrt(x), p, q); const V magnitude = - IfThenElse(x > Set(d, kThreshLinearToSRGB), poly, linear); + IfThenElse(Gt(x, Set(d, kThreshLinearToSRGB)), poly, linear); return Or(AndNot(kSign, magnitude), original_sign); } @@ -320,8 +345,8 @@ V FastLinearToSRGB(D d, V v) { const hwy::HWY_NAMESPACE::Rebind<uint32_t, D> du; const hwy::HWY_NAMESPACE::Rebind<int32_t, D> di; // Convert to 0.25 - 0.5 range. - auto v025_05 = - BitCast(d, (BitCast(du, v) | Set(du, 0x3e800000)) & Set(du, 0x3effffff)); + auto v025_05 = BitCast( + d, And(Or(BitCast(du, v), Set(du, 0x3e800000)), Set(du, 0x3effffff))); // third degree polynomial approximation between 0.25 and 0.5 // of 1.055/2^(7/2.4) * x^(1/2.4) * 0.5. A degree 4 polynomial only improves // accuracy by about 3x. @@ -353,7 +378,7 @@ V FastLinearToSRGB(D d, V v) { using hwy::HWY_NAMESPACE::ShiftLeft; using hwy::HWY_NAMESPACE::ShiftRight; // Every lane of exp is now (if cast to byte) {0, 0, 0, <index for lookup>}. - auto exp = ShiftRight<23>(BitCast(di, v)) - Set(di, 118); + auto exp = Sub(ShiftRight<23>(BitCast(di, v)), Set(di, 118)); auto pow25to18bits = TableLookupBytes( LoadDup128(di, reinterpret_cast<const int32_t*>(k2to512powers_25to18bits)), @@ -366,9 +391,9 @@ V FastLinearToSRGB(D d, V v) { // we take advantage of the fact that each table has its position 0 equal to // 0. // We can now just reassemble the float. - auto mul = - BitCast(d, ShiftLeft<18>(pow25to18bits) | ShiftLeft<10>(pow17to10bits) | - Set(di, k2to512powers_basebits)); + auto mul = BitCast( + d, Or(Or(ShiftLeft<18>(pow25to18bits), ShiftLeft<10>(pow17to10bits)), + Set(di, k2to512powers_basebits))); #else // Fallback for scalar. uint32_t exp = ((BitCast(di, v).raw >> 23) - 118) & 0xf; @@ -376,7 +401,7 @@ V FastLinearToSRGB(D d, V v) { (k2to512powers_17to10bits[exp] << 10) | k2to512powers_basebits)); #endif - return IfThenElse(v < Set(d, 0.0031308f), v * Set(d, 12.92f), + return IfThenElse(Lt(v, Set(d, 0.0031308f)), Mul(v, Set(d, 12.92f)), MulAdd(pow, mul, Set(d, -0.055))); } diff --git a/media/libjxl/src/lib/jxl/version.h.in b/media/libjxl/src/lib/jxl/version.h.in new file mode 100644 index 0000000000..d077abec79 --- /dev/null +++ b/media/libjxl/src/lib/jxl/version.h.in @@ -0,0 +1,39 @@ +/* Copyright (c) the JPEG XL Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +/** @addtogroup libjxl_common + * @{ + * @file version.h + * @brief libjxl version information + */ + +#ifndef JXL_VERSION_H_ +#define JXL_VERSION_H_ + +#define JPEGXL_MAJOR_VERSION @JPEGXL_MAJOR_VERSION@ ///< JPEG XL Major version +#define JPEGXL_MINOR_VERSION @JPEGXL_MINOR_VERSION@ ///< JPEG XL Minor version +#define JPEGXL_PATCH_VERSION @JPEGXL_PATCH_VERSION@ ///< JPEG XL Patch version + +/** Can be used to conditionally compile code for a specific JXL version + * @param[maj] major version + * @param[min] minor version + * + * @code + * #if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0,8,0) + * // use old/deprecated api + * #else + * // use current api + * #endif + * @endcode + */ +#define JPEGXL_COMPUTE_NUMERIC_VERSION(major,minor,patch) ((major<<24) | (minor<<16) | (patch<<8) | 0) + +/* Numeric representation of the version */ +#define JPEGXL_NUMERIC_VERSION JPEGXL_COMPUTE_NUMERIC_VERSION(JPEGXL_MAJOR_VERSION,JPEGXL_MINOR_VERSION,JPEGXL_PATCH_VERSION) + +#endif /* JXL_VERSION_H_ */ + +/** @}*/ diff --git a/media/libjxl/src/lib/jxl/xorshift128plus-inl.h b/media/libjxl/src/lib/jxl/xorshift128plus-inl.h index 9430c175d4..a473d591f2 100644 --- a/media/libjxl/src/lib/jxl/xorshift128plus-inl.h +++ b/media/libjxl/src/lib/jxl/xorshift128plus-inl.h @@ -21,8 +21,10 @@ namespace HWY_NAMESPACE { namespace { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; using hwy::HWY_NAMESPACE::ShiftLeft; using hwy::HWY_NAMESPACE::ShiftRight; +using hwy::HWY_NAMESPACE::Xor; // Adapted from https://github.com/vpxyz/xorshift/blob/master/xorshift128plus/ // (MIT-license) @@ -60,11 +62,11 @@ class Xorshift128Plus { for (size_t i = 0; i < N; i += Lanes(d)) { auto s1 = Load(d, s0_ + i); const auto s0 = Load(d, s1_ + i); - const auto bits = s1 + s0; // b, c + const auto bits = Add(s1, s0); // b, c Store(s0, d, s0_ + i); - s1 ^= ShiftLeft<23>(s1); + s1 = Xor(s1, ShiftLeft<23>(s1)); Store(bits, d, random_bits + i); - s1 ^= s0 ^ ShiftRight<18>(s1) ^ ShiftRight<5>(s0); + s1 = Xor(s1, Xor(s0, Xor(ShiftRight<18>(s1), ShiftRight<5>(s0)))); Store(s1, d, s1_ + i); } #else diff --git a/media/libjxl/src/lib/jxl/xorshift128plus_test.cc b/media/libjxl/src/lib/jxl/xorshift128plus_test.cc index 4db86e7c71..7514f0e4a8 100644 --- a/media/libjxl/src/lib/jxl/xorshift128plus_test.cc +++ b/media/libjxl/src/lib/jxl/xorshift128plus_test.cc @@ -24,7 +24,9 @@ namespace jxl { namespace HWY_NAMESPACE { // These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Or; using hwy::HWY_NAMESPACE::ShiftRight; +using hwy::HWY_NAMESPACE::Sub; // Define to nonzero in order to print the (new) golden outputs. #define PRINT_RESULTS 0 @@ -310,8 +312,8 @@ void TestFloat() { Load(du, reinterpret_cast<const uint32_t*>(batch) + i); // 1.0 + 23 random mantissa bits = [1, 2) const auto rand12 = - BitCast(df, ShiftRight<9>(bits) | Set(du, 0x3F800000)); - const auto rand01 = rand12 - Set(df, 1.0f); + BitCast(df, Or(ShiftRight<9>(bits), Set(du, 0x3F800000))); + const auto rand01 = Sub(rand12, Set(df, 1.0f)); Store(rand01, df, lanes); for (float lane : lanes) { sum += lane; diff --git a/media/libjxl/src/lib/jxl_extras.cmake b/media/libjxl/src/lib/jxl_extras.cmake index bfb33f9b2f..fd801fc6aa 100644 --- a/media/libjxl/src/lib/jxl_extras.cmake +++ b/media/libjxl/src/lib/jxl_extras.cmake @@ -12,51 +12,75 @@ set(JPEGXL_EXTRAS_SOURCES extras/dec/color_hints.h extras/dec/decode.cc extras/dec/decode.h + extras/dec/jxl.cc + extras/dec/jxl.h extras/dec/pgx.cc extras/dec/pgx.h extras/dec/pnm.cc extras/dec/pnm.h + extras/enc/encode.cc + extras/enc/encode.h + extras/enc/npy.cc + extras/enc/npy.h extras/enc/pgx.cc extras/enc/pgx.h extras/enc/pnm.cc extras/enc/pnm.h + extras/exif.cc + extras/exif.h extras/hlg.cc extras/hlg.h extras/packed_image.h extras/packed_image_convert.cc extras/packed_image_convert.h + extras/render_hdr.cc + extras/render_hdr.h extras/time.cc extras/time.h extras/tone_mapping.cc extras/tone_mapping.h ) -set(JPEGXL_EXTRAS_DEC_SOURCES +set(JPEGXL_EXTRAS_CODEC_SOURCES extras/dec/color_description.cc extras/dec/color_description.h extras/dec/color_hints.cc extras/dec/color_hints.h extras/dec/decode.cc extras/dec/decode.h + extras/dec/jxl.cc + extras/dec/jxl.h extras/dec/pgx.cc extras/dec/pgx.h extras/dec/pnm.cc extras/dec/pnm.h + extras/enc/encode.cc + extras/enc/encode.h + extras/enc/npy.cc + extras/enc/npy.h + extras/enc/pgx.cc + extras/enc/pgx.h + extras/enc/pnm.cc + extras/enc/pnm.h + extras/exif.cc + extras/exif.h extras/packed_image.h + extras/time.cc + extras/time.h ) -add_library(jxl_extras_dec-obj OBJECT "${JPEGXL_EXTRAS_DEC_SOURCES}") -target_compile_options(jxl_extras_dec-obj PRIVATE "${JPEGXL_INTERNAL_FLAGS}") -target_compile_definitions(jxl_extras_dec-obj PRIVATE -DJXL_EXPORT=) -set_property(TARGET jxl_extras_dec-obj PROPERTY POSITION_INDEPENDENT_CODE ON) -target_include_directories(jxl_extras_dec-obj PUBLIC +add_library(jxl_extras_codec-obj OBJECT "${JPEGXL_EXTRAS_CODEC_SOURCES}") +target_compile_options(jxl_extras_codec-obj PRIVATE "${JPEGXL_INTERNAL_FLAGS}") +target_compile_definitions(jxl_extras_codec-obj PRIVATE -DJXL_EXPORT=) +set_property(TARGET jxl_extras_codec-obj PROPERTY POSITION_INDEPENDENT_CODE ON) +target_include_directories(jxl_extras_codec-obj PUBLIC ${PROJECT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_BINARY_DIR}/include $<TARGET_PROPERTY:hwy,INTERFACE_INCLUDE_DIRECTORIES> ) -set(JXL_EXTRAS_DEC_INTERNAL_LIBRARIES) -set(JXL_EXTRAS_DEC_PUBLIC_COMPILE_DEFINITIONS) +set(JXL_EXTRAS_CODEC_INTERNAL_LIBRARIES) +set(JXL_EXTRAS_CODEC_PUBLIC_COMPILE_DEFINITIONS) # We only define a static library for jxl_extras since it uses internal parts # of jxl library which are not accessible from outside the library in the @@ -68,18 +92,18 @@ set_property(TARGET jxl_extras-static PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(jxl_extras-static PUBLIC "${PROJECT_SOURCE_DIR}") target_link_libraries(jxl_extras-static PUBLIC jxl-static - jxl_extras_dec-static + jxl_threads-static ) find_package(GIF 5.1) if(GIF_FOUND) - target_sources(jxl_extras_dec-obj PRIVATE + target_sources(jxl_extras_codec-obj PRIVATE extras/dec/gif.cc extras/dec/gif.h ) - target_include_directories(jxl_extras_dec-obj PRIVATE "${GIF_INCLUDE_DIRS}") - list(APPEND JXL_EXTRAS_DEC_INTERNAL_LIBRARIES ${GIF_LIBRARIES}) - list(APPEND JXL_EXTRAS_DEC_PUBLIC_DEFINITIONS -DJPEGXL_ENABLE_GIF=1) + target_include_directories(jxl_extras_codec-obj PRIVATE "${GIF_INCLUDE_DIRS}") + list(APPEND JXL_EXTRAS_CODEC_INTERNAL_LIBRARIES ${GIF_LIBRARIES}) + list(APPEND JXL_EXTRAS_CODEC_PUBLIC_DEFINITIONS -DJPEGXL_ENABLE_GIF=1) if(JPEGXL_DEP_LICENSE_DIR) configure_file("${JPEGXL_DEP_LICENSE_DIR}/libgif-dev/copyright" ${PROJECT_BINARY_DIR}/LICENSE.libgif COPYONLY) @@ -88,14 +112,18 @@ endif() find_package(JPEG) if(JPEG_FOUND) - target_sources(jxl_extras_dec-obj PRIVATE + target_sources(jxl_extras_codec-obj PRIVATE extras/dec/jpg.cc extras/dec/jpg.h + extras/enc/jpg.cc + extras/enc/jpg.h ) - target_include_directories(jxl_extras_dec-obj PRIVATE "${JPEG_INCLUDE_DIRS}") - list(APPEND JXL_EXTRAS_DEC_INTERNAL_LIBRARIES ${JPEG_LIBRARIES}) - list(APPEND JXL_EXTRAS_DEC_PUBLIC_DEFINITIONS -DJPEGXL_ENABLE_JPEG=1) + target_include_directories(jxl_extras_codec-obj PRIVATE "${JPEG_INCLUDE_DIRS}") + list(APPEND JXL_EXTRAS_CODEC_INTERNAL_LIBRARIES ${JPEG_LIBRARIES}) + list(APPEND JXL_EXTRAS_CODEC_PUBLIC_DEFINITIONS -DJPEGXL_ENABLE_JPEG=1) target_sources(jxl_extras-static PRIVATE + extras/dec/jpg.cc + extras/dec/jpg.h extras/enc/jpg.cc extras/enc/jpg.h ) @@ -112,14 +140,18 @@ if(NOT JPEGXL_BUNDLE_LIBPNG) find_package(PNG) endif() if(PNG_FOUND) - target_sources(jxl_extras_dec-obj PRIVATE + target_sources(jxl_extras_codec-obj PRIVATE extras/dec/apng.cc extras/dec/apng.h + extras/enc/apng.cc + extras/enc/apng.h ) - target_include_directories(jxl_extras_dec-obj PRIVATE "${PNG_INCLUDE_DIRS}") - list(APPEND JXL_EXTRAS_DEC_INTERNAL_LIBRARIES ${PNG_LIBRARIES}) - list(APPEND JXL_EXTRAS_DEC_PUBLIC_DEFINITIONS -DJPEGXL_ENABLE_APNG=1) + target_include_directories(jxl_extras_codec-obj PRIVATE "${PNG_INCLUDE_DIRS}") + list(APPEND JXL_EXTRAS_CODEC_INTERNAL_LIBRARIES ${PNG_LIBRARIES}) + list(APPEND JXL_EXTRAS_CODEC_PUBLIC_DEFINITIONS -DJPEGXL_ENABLE_APNG=1) target_sources(jxl_extras-static PRIVATE + extras/dec/apng.cc + extras/dec/apng.h extras/enc/apng.cc extras/enc/apng.h ) @@ -138,14 +170,18 @@ endif () if (JPEGXL_ENABLE_OPENEXR) pkg_check_modules(OpenEXR IMPORTED_TARGET OpenEXR) if (OpenEXR_FOUND) - target_sources(jxl_extras_dec-obj PRIVATE + target_sources(jxl_extras_codec-obj PRIVATE extras/dec/exr.cc extras/dec/exr.h + extras/enc/exr.cc + extras/enc/exr.h ) - list(APPEND JXL_EXTRAS_DEC_PUBLIC_DEFINITIONS -DJPEGXL_ENABLE_EXR=1) - target_include_directories(jxl_extras_dec-obj PRIVATE "${OpenEXR_INCLUDE_DIRS}") - list(APPEND JXL_EXTRAS_DEC_INTERNAL_LIBRARIES PkgConfig::OpenEXR) + list(APPEND JXL_EXTRAS_CODEC_PUBLIC_DEFINITIONS -DJPEGXL_ENABLE_EXR=1) + target_include_directories(jxl_extras_codec-obj PRIVATE "${OpenEXR_INCLUDE_DIRS}") + list(APPEND JXL_EXTRAS_CODEC_INTERNAL_LIBRARIES PkgConfig::OpenEXR) target_sources(jxl_extras-static PRIVATE + extras/dec/exr.cc + extras/dec/exr.h extras/enc/exr.cc extras/enc/exr.h ) @@ -159,27 +195,25 @@ if (OpenEXR_FOUND) # Actually those flags counteract the ones set in JPEGXL_INTERNAL_FLAGS. if (NOT WIN32) set_source_files_properties(extras/dec/exr.cc extras/enc/exr.cc PROPERTIES COMPILE_FLAGS -fexceptions) - if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") set_source_files_properties(extras/dec/exr.cc extras/enc/exr.cc PROPERTIES COMPILE_FLAGS -fcxx-exceptions) endif() endif() endif() # OpenEXR_FOUND endif() # JPEGXL_ENABLE_OPENEXR -target_compile_definitions(jxl_extras_dec-obj PRIVATE ${JXL_EXTRAS_DEC_PUBLIC_DEFINITIONS}) +target_compile_definitions(jxl_extras_codec-obj PRIVATE ${JXL_EXTRAS_CODEC_PUBLIC_DEFINITIONS}) ### Static library. -add_library(jxl_extras_dec-static STATIC $<TARGET_OBJECTS:jxl_extras_dec-obj>) -target_compile_definitions(jxl_extras_dec-static PUBLIC ${JXL_EXTRAS_DEC_PUBLIC_DEFINITIONS}) -target_link_libraries(jxl_extras_dec-static PRIVATE ${JXL_EXTRAS_DEC_INTERNAL_LIBRARIES}) +add_library(jxl_extras_codec-static STATIC $<TARGET_OBJECTS:jxl_extras_codec-obj>) +target_compile_definitions(jxl_extras_codec-static PUBLIC ${JXL_EXTRAS_CODEC_PUBLIC_DEFINITIONS}) +target_link_libraries(jxl_extras_codec-static PRIVATE ${JXL_EXTRAS_CODEC_INTERNAL_LIBRARIES} jxl) ### Shared library. -if (((NOT DEFINED "${TARGET_SUPPORTS_SHARED_LIBS}") OR - TARGET_SUPPORTS_SHARED_LIBS) AND NOT JPEGXL_STATIC AND BUILD_SHARED_LIBS) -add_library(jxl_extras_dec SHARED $<TARGET_OBJECTS:jxl_extras_dec-obj>) -target_compile_definitions(jxl_extras_dec PUBLIC ${JXL_EXTRAS_DEC_PUBLIC_DEFINITIONS}) -target_link_libraries(jxl_extras_dec PRIVATE ${JXL_EXTRAS_DEC_INTERNAL_LIBRARIES} jxl) +if (BUILD_SHARED_LIBS) +add_library(jxl_extras_codec SHARED $<TARGET_OBJECTS:jxl_extras_codec-obj>) +target_compile_definitions(jxl_extras_codec PUBLIC ${JXL_EXTRAS_CODEC_PUBLIC_DEFINITIONS}) +target_link_libraries(jxl_extras_codec PRIVATE ${JXL_EXTRAS_CODEC_INTERNAL_LIBRARIES} jxl) else() -add_library(jxl_extras_dec ALIAS jxl_extras_dec-static) -endif() # TARGET_SUPPORTS_SHARED_LIBS AND NOT JPEGXL_STATIC AND - # BUILD_SHARED_LIBS +add_library(jxl_extras_codec ALIAS jxl_extras_codec-static) +endif() # BUILD_SHARED_LIBS diff --git a/media/libjxl/src/lib/jxl_tests.cmake b/media/libjxl/src/lib/jxl_tests.cmake index 3fcee6d205..c858ae97b9 100644 --- a/media/libjxl/src/lib/jxl_tests.cmake +++ b/media/libjxl/src/lib/jxl_tests.cmake @@ -61,6 +61,7 @@ set(TEST_FILES ### Files before this line are handled by build_cleaner.py # TODO(deymo): Move this to tools/ ../tools/box/box_test.cc + ../tools/djxl_fuzzer_test.cc ) # Test-only library code. @@ -72,6 +73,7 @@ set(TESTLIB_FILES jxl/dec_transforms_testonly.h jxl/fake_parallel_runner_testonly.h jxl/image_test_utils.h + jxl/test_image.h jxl/test_utils.h jxl/testdata.h ) @@ -96,7 +98,11 @@ file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tests) foreach (TESTFILE IN LISTS TEST_FILES) # The TESTNAME is the name without the extension or directory. get_filename_component(TESTNAME ${TESTFILE} NAME_WE) - add_executable(${TESTNAME} ${TESTFILE}) + if(TESTFILE STREQUAL ../tools/djxl_fuzzer_test.cc) + add_executable(${TESTNAME} ${TESTFILE} ../tools/djxl_fuzzer.cc) + else() + add_executable(${TESTNAME} ${TESTFILE}) + endif() if(JPEGXL_EMSCRIPTEN) # The emscripten linking step takes too much memory and crashes during the # wasm-opt step when using -O2 optimization level @@ -105,6 +111,10 @@ foreach (TESTFILE IN LISTS TEST_FILES) -s USE_LIBPNG=1 \ -s TOTAL_MEMORY=1536MB \ -s SINGLE_FILE=1 \ + -s PROXY_TO_PTHREAD \ + -s EXIT_RUNTIME=1 \ + -s USE_PTHREADS=1 \ + -s NODERAWFS=1 \ ") endif() target_compile_options(${TESTNAME} PRIVATE @@ -115,8 +125,6 @@ foreach (TESTFILE IN LISTS TEST_FILES) ) target_link_libraries(${TESTNAME} box - jxl-static - jxl_threads-static jxl_extras-static jxl_testlib-static gmock @@ -125,10 +133,10 @@ foreach (TESTFILE IN LISTS TEST_FILES) ) # Output test targets in the test directory. set_target_properties(${TESTNAME} PROPERTIES PREFIX "tests/") - if (WIN32 AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") + if (WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set_target_properties(${TESTNAME} PROPERTIES COMPILE_FLAGS "-Wno-error") endif () - if(${CMAKE_VERSION} VERSION_LESS "3.10.3") + if(CMAKE_VERSION VERSION_LESS "3.10.3") gtest_discover_tests(${TESTNAME} TIMEOUT 240) else () gtest_discover_tests(${TESTNAME} DISCOVERY_TIMEOUT 240) diff --git a/media/libjxl/src/lib/jxl_threads.cmake b/media/libjxl/src/lib/jxl_threads.cmake index d0d2d00560..006e71eb68 100644 --- a/media/libjxl/src/lib/jxl_threads.cmake +++ b/media/libjxl/src/lib/jxl_threads.cmake @@ -40,7 +40,7 @@ set_target_properties(${_target} PROPERTIES # Always install the library as jxl_threads.{a,so} file without the "-static" # suffix, except in Windows. -if (NOT WIN32) +if (NOT WIN32 OR MINGW) set_target_properties(${_target} PROPERTIES OUTPUT_NAME "jxl_threads") endif() install(TARGETS ${_target} @@ -63,8 +63,7 @@ target_compile_definitions(jxl_threads-static ### Public shared library. -if (((NOT DEFINED "${TARGET_SUPPORTS_SHARED_LIBS}") OR - TARGET_SUPPORTS_SHARED_LIBS) AND NOT JPEGXL_STATIC AND BUILD_SHARED_LIBS) +if (BUILD_SHARED_LIBS) add_library(jxl_threads SHARED ${JPEGXL_THREADS_SOURCES}) _set_jxl_threads(jxl_threads) @@ -104,11 +103,24 @@ add_library(jxl_threads ALIAS jxl_threads-static) generate_export_header(jxl_threads-static BASE_NAME JXL_THREADS EXPORT_FILE_NAME include/jxl/jxl_threads_export.h) -endif() # TARGET_SUPPORTS_SHARED_LIBS AND NOT JPEGXL_STATIC AND - # BUILD_SHARED_LIBS +endif() # BUILD_SHARED_LIBS ### Add a pkg-config file for libjxl_threads. + +# Allow adding prefix if CMAKE_INSTALL_INCLUDEDIR not absolute. +if(IS_ABSOLUTE "${CMAKE_INSTALL_INCLUDEDIR}") + set(PKGCONFIG_TARGET_INCLUDES "${CMAKE_INSTALL_INCLUDEDIR}") +else() + set(PKGCONFIG_TARGET_INCLUDES "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") +endif() +# Allow adding prefix if CMAKE_INSTALL_LIBDIR not absolute. +if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(PKGCONFIG_TARGET_LIBS "${CMAKE_INSTALL_LIBDIR}") +else() + set(PKGCONFIG_TARGET_LIBS "\${exec_prefix}/${CMAKE_INSTALL_LIBDIR}") +endif() + set(JPEGXL_THREADS_LIBRARY_REQUIRES "") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/threads/libjxl_threads.pc.in" "libjxl_threads.pc" @ONLY) diff --git a/media/libjxl/src/lib/lib.gni b/media/libjxl/src/lib/lib.gni index 68afda3a86..2914de9918 100644 --- a/media/libjxl/src/lib/lib.gni +++ b/media/libjxl/src/lib/lib.gni @@ -80,8 +80,12 @@ libjxl_dec_sources = [ "jxl/compressed_dc.cc", "jxl/compressed_dc.h", "jxl/convolve-inl.h", - "jxl/convolve.cc", "jxl/convolve.h", + "jxl/convolve_separable5.cc", + "jxl/convolve_separable7.cc", + "jxl/convolve_slow.cc", + "jxl/convolve_symmetric3.cc", + "jxl/convolve_symmetric5.cc", "jxl/dct-inl.h", "jxl/dct_block-inl.h", "jxl/dct_scales.cc", @@ -108,9 +112,9 @@ libjxl_dec_sources = [ "jxl/dec_modular.h", "jxl/dec_noise.cc", "jxl/dec_noise.h", - "jxl/dec_params.h", "jxl/dec_patch_dictionary.cc", "jxl/dec_patch_dictionary.h", + "jxl/dec_tone_mapping-inl.h", "jxl/dec_transforms-inl.h", "jxl/dec_xyb-inl.h", "jxl/dec_xyb.cc", @@ -124,7 +128,6 @@ libjxl_dec_sources = [ "jxl/entropy_coder.h", "jxl/epf.cc", "jxl/epf.h", - "jxl/exif.cc", "jxl/exif.h", "jxl/fast_dct-inl.h", "jxl/fast_dct.cc", @@ -216,6 +219,8 @@ libjxl_dec_sources = [ "jxl/render_pipeline/stage_chroma_upsampling.h", "jxl/render_pipeline/stage_epf.cc", "jxl/render_pipeline/stage_epf.h", + "jxl/render_pipeline/stage_from_linear.cc", + "jxl/render_pipeline/stage_from_linear.h", "jxl/render_pipeline/stage_gaborish.cc", "jxl/render_pipeline/stage_gaborish.h", "jxl/render_pipeline/stage_noise.cc", @@ -226,6 +231,10 @@ libjxl_dec_sources = [ "jxl/render_pipeline/stage_splines.h", "jxl/render_pipeline/stage_spot.cc", "jxl/render_pipeline/stage_spot.h", + "jxl/render_pipeline/stage_to_linear.cc", + "jxl/render_pipeline/stage_to_linear.h", + "jxl/render_pipeline/stage_tone_mapping.cc", + "jxl/render_pipeline/stage_tone_mapping.h", "jxl/render_pipeline/stage_upsampling.cc", "jxl/render_pipeline/stage_upsampling.h", "jxl/render_pipeline/stage_write.cc", @@ -251,8 +260,6 @@ libjxl_enc_sources = [ "jxl/butteraugli/butteraugli.cc", "jxl/butteraugli/butteraugli.h", "jxl/butteraugli_wrapper.cc", - "jxl/dec_file.cc", - "jxl/dec_file.h", "jxl/enc_ac_strategy.cc", "jxl/enc_ac_strategy.h", "jxl/enc_adaptive_quantization.cc", @@ -288,7 +295,6 @@ libjxl_enc_sources = [ "jxl/enc_entropy_coder.h", "jxl/enc_external_image.cc", "jxl/enc_external_image.h", - "jxl/enc_fast_heuristics.cc", "jxl/enc_file.cc", "jxl/enc_file.h", "jxl/enc_frame.cc", @@ -431,6 +437,7 @@ libjxl_testlib_sources = [ "jxl/dec_transforms_testonly.h", "jxl/fake_parallel_runner_testonly.h", "jxl/image_test_utils.h", + "jxl/test_image.h", "jxl/test_utils.h", "jxl/testdata.h", ] @@ -444,19 +451,29 @@ libjxl_extras_sources = [ "extras/dec/color_hints.h", "extras/dec/decode.cc", "extras/dec/decode.h", + "extras/dec/jxl.cc", + "extras/dec/jxl.h", "extras/dec/pgx.cc", "extras/dec/pgx.h", "extras/dec/pnm.cc", "extras/dec/pnm.h", + "extras/enc/encode.cc", + "extras/enc/encode.h", + "extras/enc/npy.cc", + "extras/enc/npy.h", "extras/enc/pgx.cc", "extras/enc/pgx.h", "extras/enc/pnm.cc", "extras/enc/pnm.h", + "extras/exif.cc", + "extras/exif.h", "extras/hlg.cc", "extras/hlg.h", "extras/packed_image.h", "extras/packed_image_convert.cc", "extras/packed_image_convert.h", + "extras/render_hdr.cc", + "extras/render_hdr.h", "extras/time.cc", "extras/time.h", "extras/tone_mapping.cc", diff --git a/media/libjxl/src/lib/profiler/tsc_timer.h b/media/libjxl/src/lib/profiler/tsc_timer.h index e3e1fb6ba7..9387f4195b 100644 --- a/media/libjxl/src/lib/profiler/tsc_timer.h +++ b/media/libjxl/src/lib/profiler/tsc_timer.h @@ -19,6 +19,9 @@ #ifndef NOMINMAX #define NOMINMAX #endif // NOMINMAX +#ifndef NOGDI +#define NOGDI +#endif // NOGDI #include <windows.h> // Undef macros to avoid collisions #undef LoadFence diff --git a/media/libjxl/src/lib/threads/libjxl_threads.pc.in b/media/libjxl/src/lib/threads/libjxl_threads.pc.in index 8a3275cf1c..50b937a840 100644 --- a/media/libjxl/src/lib/threads/libjxl_threads.pc.in +++ b/media/libjxl/src/lib/threads/libjxl_threads.pc.in @@ -1,7 +1,7 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ -includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=@PKGCONFIG_TARGET_LIBS@ +includedir=@PKGCONFIG_TARGET_INCLUDES@ Name: libjxl_threads Description: JPEG XL multi-thread runner using std::threads. @@ -10,3 +10,4 @@ Requires.private: @JPEGXL_THREADS_LIBRARY_REQUIRES@ Libs: -L${libdir} -ljxl_threads Libs.private: -lm Cflags: -I${includedir} +Cflags.private: -DJXL_THREADS_STATIC_DEFINE diff --git a/media/libjxl/src/lib/threads/thread_parallel_runner_internal.cc b/media/libjxl/src/lib/threads/thread_parallel_runner_internal.cc index e868622e9b..2b05ad9928 100644 --- a/media/libjxl/src/lib/threads/thread_parallel_runner_internal.cc +++ b/media/libjxl/src/lib/threads/thread_parallel_runner_internal.cc @@ -173,14 +173,8 @@ void ThreadParallelRunner::ThreadFunc(ThreadParallelRunner* self, } ThreadParallelRunner::ThreadParallelRunner(const int num_worker_threads) -#if defined(__EMSCRIPTEN__) - : num_worker_threads_(0), num_threads_(1) { - // TODO(eustas): find out if pthreads would work for us. - (void)num_worker_threads; -#else : num_worker_threads_(num_worker_threads), num_threads_(std::max(num_worker_threads, 1)) { -#endif PROFILER_ZONE("ThreadParallelRunner ctor"); threads_.reserve(num_worker_threads_); diff --git a/media/libjxl/src/lib/threads/thread_parallel_runner_test.cc b/media/libjxl/src/lib/threads/thread_parallel_runner_test.cc index c68b645df2..2293b5ceb4 100644 --- a/media/libjxl/src/lib/threads/thread_parallel_runner_test.cc +++ b/media/libjxl/src/lib/threads/thread_parallel_runner_test.cc @@ -3,6 +3,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +#include <atomic> + #include "gtest/gtest.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/thread_pool_internal.h" diff --git a/media/libjxl/src/plugins/gdk-pixbuf/CMakeLists.txt b/media/libjxl/src/plugins/gdk-pixbuf/CMakeLists.txt index 1d595008a5..e56d312b71 100644 --- a/media/libjxl/src/plugins/gdk-pixbuf/CMakeLists.txt +++ b/media/libjxl/src/plugins/gdk-pixbuf/CMakeLists.txt @@ -33,7 +33,7 @@ install(TARGETS pixbufloader-jxl LIBRARY DESTINATION "${GDK_PIXBUF_MODULEDIR}") # /usr/share/thumbnailers/gdk-pixbuf-thumbnailer.thumbnailer install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/jxl.thumbnailer DESTINATION share/thumbnailers/) -if(BUILD_TESTING AND NOT MINGW) +if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING) pkg_check_modules(Gdk IMPORTED_TARGET gdk-2.0) if (Gdk_FOUND) # Test for loading a .jxl file using the pixbufloader library via GDK. This @@ -71,7 +71,7 @@ if(BUILD_TESTING AND NOT MINGW) COMMAND ${XVFB_PROGRAM_PREFIX} $<TARGET_FILE:pixbufloader_test> "${CMAKE_CURRENT_SOURCE_DIR}/loaders_test.cache" - "${CMAKE_SOURCE_DIR}/third_party/testdata/jxl/blending/cropped_traffic_light.jxl" + "${CMAKE_SOURCE_DIR}/testdata/jxl/blending/cropped_traffic_light.jxl" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) set_tests_properties(pixbufloader_test_jxl PROPERTIES SKIP_RETURN_CODE 254) diff --git a/media/libjxl/src/third_party/CMakeLists.txt b/media/libjxl/src/third_party/CMakeLists.txt index 11cc4ab876..50cc72c92a 100644 --- a/media/libjxl/src/third_party/CMakeLists.txt +++ b/media/libjxl/src/third_party/CMakeLists.txt @@ -12,79 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Enable tests in third_party/ as well. -enable_testing() -include(CTest) - -if(BUILD_TESTING) -# Add GTest from source and alias it to what the find_package(GTest) workflow -# defines. Omitting googletest/ directory would require it to be available in -# the base system instead, but it would work just fine. This makes packages -# using GTest and calling find_package(GTest) actually work. -if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/googletest/CMakeLists.txt" AND - NOT JPEGXL_FORCE_SYSTEM_GTEST) - add_subdirectory(googletest EXCLUDE_FROM_ALL) - - set(GTEST_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/googletest/googletest") - set(GTEST_INCLUDE_DIR "$<TARGET_PROPERTY:INCLUDE_DIRECTORIES,gtest>" - CACHE STRING "") - set(GMOCK_INCLUDE_DIR "$<TARGET_PROPERTY:INCLUDE_DIRECTORIES,gmock>") - set(GTEST_LIBRARY "$<TARGET_FILE:gtest>") - set(GTEST_MAIN_LIBRARY "$<TARGET_FILE:gtest_main>") - add_library(GTest::GTest ALIAS gtest) - add_library(GTest::Main ALIAS gtest_main) - - set_target_properties(gtest PROPERTIES POSITION_INDEPENDENT_CODE TRUE) - set_target_properties(gmock PROPERTIES POSITION_INDEPENDENT_CODE TRUE) - set_target_properties(gtest_main PROPERTIES POSITION_INDEPENDENT_CODE TRUE) - set_target_properties(gmock_main PROPERTIES POSITION_INDEPENDENT_CODE TRUE) - - # googletest doesn't compile clean with clang-cl (-Wundef) - if (WIN32 AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") - set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-error") - set_target_properties(gmock PROPERTIES COMPILE_FLAGS "-Wno-error") - set_target_properties(gtest_main PROPERTIES COMPILE_FLAGS "-Wno-error") - set_target_properties(gmock_main PROPERTIES COMPILE_FLAGS "-Wno-error") - endif () - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/googletest/LICENSE" - ${PROJECT_BINARY_DIR}/LICENSE.googletest COPYONLY) -else() - if(JPEGXL_DEP_LICENSE_DIR) - configure_file("${JPEGXL_DEP_LICENSE_DIR}/googletest/copyright" - ${PROJECT_BINARY_DIR}/LICENSE.googletest COPYONLY) - endif() # JPEGXL_DEP_LICENSE_DIR +if((SANITIZER STREQUAL "asan") OR (SANITIZER STREQUAL "msan")) + set(BUILD_TESTING OFF) endif() -find_package(GTest) -if (NOT GTEST_FOUND) - set(BUILD_TESTING OFF CACHE BOOL "Build tests" FORCE) - message(SEND_ERROR "GTest not found. Install googletest package " - "(libgtest-dev) in the system or download googletest to " - "third_party/googletest from https://github.com/google/googletest ." - "To disable tests instead re-run cmake with -DBUILD_TESTING=OFF.") -endif() # NOT GTEST_FOUND - -# Look for gmock in the system too. -if (NOT DEFINED GMOCK_INCLUDE_DIR) - find_path( - GMOCK_INCLUDE_DIR "gmock/gmock.h" - HINTS ${GTEST_INCLUDE_DIRS}) - if ("${GMOCK_INCLUDE_DIR}" STREQUAL "GMOCK_INCLUDE_DIR-NOTFOUND") - set(BUILD_TESTING OFF CACHE BOOL "Build tests" FORCE) - message(SEND_ERROR "GMock not found. Install googletest package " - "(libgmock-dev) in the system or download googletest to " - "third_party/googletest from https://github.com/google/googletest ." - "To disable tests instead re-run cmake with -DBUILD_TESTING=OFF.") - else() - message(STATUS "Found GMock: ${GMOCK_INCLUDE_DIR}") - endif() # GMOCK_INCLUDE_DIR-NOTFOUND -endif() # NOT DEFINED GMOCK_INCLUDE_DIR -endif() # BUILD_TESTING # Highway set(HWY_SYSTEM_GTEST ON CACHE INTERNAL "") set(HWY_FORCE_STATIC_LIBS ON CACHE INTERNAL "") +set(HWY_ENABLE_CONTRIB OFF CACHE INTERNAL "") +set(HWY_ENABLE_EXAMPLES OFF CACHE INTERNAL "") if((SANITIZER STREQUAL "asan") OR (SANITIZER STREQUAL "msan")) - set(HWY_EXAMPLES_TESTS_INSTALL OFF CACHE INTERNAL "") + set(HWY_ENABLE_INSTALL OFF CACHE INTERNAL "") endif() if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/highway/CMakeLists.txt" AND NOT JPEGXL_FORCE_SYSTEM_HWY) @@ -123,7 +61,11 @@ if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/brotli/c/include/brotli/decode.h" OR else() # Compile brotli from sources. set(BROTLI_DISABLE_TESTS ON CACHE STRING "Disable Brotli tests") - add_subdirectory(brotli EXCLUDE_FROM_ALL) + # Override default "no-install" policy. + if((NOT SANITIZER STREQUAL "asan") AND (NOT SANITIZER STREQUAL "msan")) + set(BROTLI_BUNDLED_MODE OFF CACHE INTERNAL "") + endif() + add_subdirectory(brotli) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/brotli/LICENSE" ${PROJECT_BINARY_DIR}/LICENSE.brotli COPYONLY) if(BROTLI_EMSCRIPTEN) @@ -157,37 +99,6 @@ if (JPEGXL_ENABLE_VIEWERS OR NOT JPEGXL_ENABLE_SKCMS) endif() endif() -# gflags -if(JPEGXL_ENABLE_TOOLS) - if (JPEGXL_BUNDLE_GFLAGS) - # Compile gflags from sources. - set(GFLAGS_BUILD_TESTING OFF) - add_subdirectory(gflags) - add_library(jxl_gflags ALIAS gflags_nothreads_static) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/gflags/COPYING.txt" - ${PROJECT_BINARY_DIR}/LICENSE.gflags COPYONLY) - else() - # Use sytem libgflags-dev - find_package(gflags) - if (NOT gflags_FOUND) - message(FATAL_ERROR - "Gflags not found, install libgflags-dev or download gflags source to" - "third_party/gflags from https://github.com/gflags/gflags. You can use" - " ${PROJECT_SOURCE_DIR}/deps.sh to download the dependency.") - endif() - if(JPEGXL_DEP_LICENSE_DIR) - configure_file("${JPEGXL_DEP_LICENSE_DIR}/libgflags-dev/copyright" - ${PROJECT_BINARY_DIR}/LICENSE.gflags COPYONLY) - endif() # JPEGXL_DEP_LICENSE_DIR - add_library(jxl_gflags INTERFACE) - if(JPEGXL_STATIC) - target_link_libraries(jxl_gflags INTERFACE gflags_static) - else() - target_link_libraries(jxl_gflags INTERFACE gflags) - endif() - endif() -endif() - # libpng if (JPEGXL_EMSCRIPTEN) if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libpng/CMakeLists.txt") @@ -244,4 +155,3 @@ if (JPEGXL_ENABLE_SJPEG) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/sjpeg/COPYING" ${PROJECT_BINARY_DIR}/LICENSE.sjpeg COPYONLY) endif () - diff --git a/media/libjxl/src/third_party/lcms2.cmake b/media/libjxl/src/third_party/lcms2.cmake index c33f877659..c4551de862 100644 --- a/media/libjxl/src/third_party/lcms2.cmake +++ b/media/libjxl/src/third_party/lcms2.cmake @@ -44,7 +44,7 @@ add_library(lcms2 STATIC EXCLUDE_FROM_ALL target_include_directories(lcms2 PUBLIC "${CMAKE_CURRENT_LIST_DIR}/lcms/include") # This warning triggers with gcc-8. -if (${CMAKE_C_COMPILER_ID} MATCHES "GNU") +if (CMAKE_C_COMPILER_ID MATCHES "GNU") target_compile_options(lcms2 PRIVATE # gcc-only flags. diff --git a/media/libjxl/src/third_party/testing.cmake b/media/libjxl/src/third_party/testing.cmake new file mode 100644 index 0000000000..9f49737e55 --- /dev/null +++ b/media/libjxl/src/third_party/testing.cmake @@ -0,0 +1,83 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Enable tests in third_party/ as well. +enable_testing() +include(CTest) + +set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third_party") + +if(BUILD_TESTING) +# Add GTest from source and alias it to what the find_package(GTest) workflow +# defines. Omitting googletest/ directory would require it to be available in +# the base system instead, but it would work just fine. This makes packages +# using GTest and calling find_package(GTest) actually work. +if (EXISTS "${SOURCE_DIR}/googletest/CMakeLists.txt" AND + NOT JPEGXL_FORCE_SYSTEM_GTEST) + add_subdirectory(third_party/googletest EXCLUDE_FROM_ALL) + + set(GTEST_ROOT "${SOURCE_DIR}/googletest/googletest") + set(GTEST_INCLUDE_DIR "$<TARGET_PROPERTY:INCLUDE_DIRECTORIES,gtest>" + CACHE STRING "") + set(GMOCK_INCLUDE_DIR "$<TARGET_PROPERTY:INCLUDE_DIRECTORIES,gmock>") + set(GTEST_LIBRARY "$<TARGET_FILE:gtest>") + set(GTEST_MAIN_LIBRARY "$<TARGET_FILE:gtest_main>") + add_library(GTest::GTest ALIAS gtest) + add_library(GTest::Main ALIAS gtest_main) + + set_target_properties(gtest PROPERTIES POSITION_INDEPENDENT_CODE TRUE) + set_target_properties(gmock PROPERTIES POSITION_INDEPENDENT_CODE TRUE) + set_target_properties(gtest_main PROPERTIES POSITION_INDEPENDENT_CODE TRUE) + set_target_properties(gmock_main PROPERTIES POSITION_INDEPENDENT_CODE TRUE) + + # googletest doesn't compile clean with clang-cl (-Wundef) + if (WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-error") + set_target_properties(gmock PROPERTIES COMPILE_FLAGS "-Wno-error") + set_target_properties(gtest_main PROPERTIES COMPILE_FLAGS "-Wno-error") + set_target_properties(gmock_main PROPERTIES COMPILE_FLAGS "-Wno-error") + endif () + configure_file("${SOURCE_DIR}/googletest/LICENSE" + ${PROJECT_BINARY_DIR}/LICENSE.googletest COPYONLY) +else() + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/googletest/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.googletest COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR +endif() +find_package(GTest) +if (NOT GTEST_FOUND) + set(BUILD_TESTING OFF CACHE BOOL "Build tests" FORCE) + message(SEND_ERROR "GTest not found. Install googletest package " + "(libgtest-dev) in the system or download googletest to " + "third_party/googletest from https://github.com/google/googletest ." + "To disable tests instead re-run cmake with -DBUILD_TESTING=OFF.") +endif() # NOT GTEST_FOUND + +# Look for gmock in the system too. +if (NOT DEFINED GMOCK_INCLUDE_DIR) + find_path( + GMOCK_INCLUDE_DIR "gmock/gmock.h" + HINTS ${GTEST_INCLUDE_DIRS}) + if (NOT GMOCK_INCLUDE_DIR) + set(BUILD_TESTING OFF CACHE BOOL "Build tests" FORCE) + message(SEND_ERROR "GMock not found. Install googletest package " + "(libgmock-dev) in the system or download googletest to " + "third_party/googletest from https://github.com/google/googletest ." + "To disable tests instead re-run cmake with -DBUILD_TESTING=OFF.") + else() + message(STATUS "Found GMock: ${GMOCK_INCLUDE_DIR}") + endif() # NOT GMOCK_INCLUDE_DIR +endif() # NOT DEFINED GMOCK_INCLUDE_DIR +endif() # BUILD_TESTING diff --git a/media/libjxl/src/tools/CMakeLists.txt b/media/libjxl/src/tools/CMakeLists.txt index 9ee2e53fcb..934ed895c0 100644 --- a/media/libjxl/src/tools/CMakeLists.txt +++ b/media/libjxl/src/tools/CMakeLists.txt @@ -58,17 +58,19 @@ endif() # JPEGXL_ENABLE_VIEWERS # Tools are added conditionally below. set(TOOL_BINARIES) +# Tools that depend on jxl internal functions. +set(INTERNAL_TOOL_BINARIES) add_library(jxl_tool STATIC EXCLUDE_FROM_ALL cmdline.cc codec_config.cc + speed_stats.cc + file_io.cc tool_version.cc ) target_compile_options(jxl_tool PUBLIC "${JPEGXL_INTERNAL_FLAGS}") -target_link_libraries(jxl_tool jxl-static) - -target_include_directories(jxl_tool - PUBLIC "${PROJECT_SOURCE_DIR}") +target_include_directories(jxl_tool PUBLIC "${PROJECT_SOURCE_DIR}") +target_link_libraries(jxl_tool hwy) # The JPEGXL_VERSION is set from the builders. if(NOT DEFINED JPEGXL_VERSION OR JPEGXL_VERSION STREQUAL "") @@ -119,85 +121,34 @@ else() endif() if(JPEGXL_ENABLE_TOOLS) - list(APPEND TOOL_BINARIES - cjxl - cjxl_ng - djxl - djxl_ng - cjpeg_hdr - jxlinfo - ) - # Main compressor. - add_executable(cjxl - cjxl.cc - speed_stats.cc - cjxl_main.cc - ) + add_executable(cjxl cjxl_main.cc) target_link_libraries(cjxl - box - jxl-static - jxl_extras-static - jxl_threads-static - ) - - - add_executable(cjxl_ng - cjxl_ng_main.cc - ) - target_link_libraries(cjxl_ng jxl - jxl_extras_dec-static + jxl_extras_codec-static jxl_threads - hwy - jxl_gflags - ) - target_include_directories(cjxl_ng PRIVATE "${PROJECT_SOURCE_DIR}") - if(JPEGXL_EMSCRIPTEN) - set_target_properties(cjxl_ng PROPERTIES LINK_FLAGS "-s USE_LIBPNG=1") - endif() - - add_executable(djxl_ng - djxl_ng_main.cc - ) - target_link_libraries(djxl_ng - jxl - jxl_gflags - ) - - add_executable(cjpeg_hdr - cjpeg_hdr.cc - ) - target_link_libraries(cjpeg_hdr - box - jxl-static - jxl_extras-static - jxl_threads-static + jxl_tool ) + list(APPEND TOOL_BINARIES cjxl) # Main decompressor. - add_library(djxltool STATIC - djxl.cc - speed_stats.cc - ) - target_link_libraries(djxltool - box - jxl-static - jxl_extras-static - jxl_threads-static + add_executable(djxl djxl_main.cc) + target_link_libraries(djxl + jxl + jxl_extras_codec-static + jxl_threads + jxl_tool ) + list(APPEND TOOL_BINARIES djxl) - add_executable(djxl - djxl_main.cc - ) - target_link_libraries(djxl djxltool) + add_executable(cjpeg_hdr cjpeg_hdr.cc) + list(APPEND INTERNAL_TOOL_BINARIES cjpeg_hdr) - add_executable(jxlinfo - jxlinfo.c - ) + add_executable(jxlinfo jxlinfo.c) target_link_libraries(jxlinfo jxl) + list(APPEND TOOL_BINARIES jxlinfo) - if(NOT ${SANITIZER} STREQUAL "none") + if(NOT SANITIZER STREQUAL "none") # Linking a C test binary with the C++ JPEG XL implementation when using # address sanitizer is not well supported by clang 9, so force using clang++ # for linking this test if a sanitizer is used. @@ -207,8 +158,8 @@ if(JPEGXL_ENABLE_TOOLS) endif() # JPEGXL_ENABLE_TOOLS # Other developer tools. -if(${JPEGXL_ENABLE_DEVTOOLS}) - list(APPEND TOOL_BINARIES +if(JPEGXL_ENABLE_DEVTOOLS) + list(APPEND INTERNAL_TOOL_BINARIES fuzzer_corpus butteraugli_main decode_and_encode @@ -239,8 +190,8 @@ if(${JPEGXL_ENABLE_DEVTOOLS}) endif() # JPEGXL_ENABLE_DEVTOOLS # Benchmark tools. -if(${JPEGXL_ENABLE_BENCHMARK}) - list(APPEND TOOL_BINARIES +if(JPEGXL_ENABLE_BENCHMARK AND JPEGXL_ENABLE_TOOLS) + list(APPEND INTERNAL_TOOL_BINARIES benchmark_xl ) @@ -256,8 +207,6 @@ if(${JPEGXL_ENABLE_BENCHMARK}) benchmark/benchmark_codec_custom.h benchmark/benchmark_codec_jxl.cc benchmark/benchmark_codec_jxl.h - speed_stats.cc - speed_stats.h ../third_party/dirent.cc ) target_link_libraries(benchmark_xl Threads::Threads) @@ -296,7 +245,7 @@ if(${JPEGXL_ENABLE_BENCHMARK}) # Use the static version of webp if available. find_library(WebP_STATIC_LINK_LIBRARY NAMES libwebp.a PATHS "${WebP_LIBDIR}") - if("${WebP_STATIC_LINK_LIBRARY}" STREQUAL "WebP_STATIC_LINK_LIBRARY-NOTFOUND") + if(NOT WebP_STATIC_LINK_LIBRARY) message(WARNING "Using dynamic libwebp") target_link_libraries(benchmark_xl PkgConfig::WebP) else() @@ -304,7 +253,7 @@ if(${JPEGXL_ENABLE_BENCHMARK}) target_include_directories(benchmark_xl PRIVATE ${WebP_STATIC_INCLUDE_DIRS}) target_compile_options(benchmark_xl PRIVATE ${WebP_STATIC_CFLAGS_OTHER}) - endif() + endif() # NOT WebP_STATIC_LINK_LIBRARY endif() pkg_check_modules(AVIF IMPORTED_TARGET libavif) @@ -319,20 +268,28 @@ if(${JPEGXL_ENABLE_BENCHMARK}) 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_tool + ) +endforeach() + +list(APPEND TOOL_BINARIES ${INTERNAL_TOOL_BINARIES}) + foreach(BINARY IN LISTS TOOL_BINARIES) - target_include_directories("${BINARY}" PRIVATE "${PROJECT_SOURCE_DIR}") - target_link_libraries("${BINARY}" box jxl-static jxl_extras-static jxl_threads-static jxl_tool) if(JPEGXL_EMSCRIPTEN) set_target_properties(${BINARY} PROPERTIES LINK_FLAGS "-s USE_LIBPNG=1") endif() - endforeach() + install(TARGETS ${TOOL_BINARIES} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") message(STATUS "Building tools: ${TOOL_BINARIES}") set(FUZZER_BINARIES color_encoding_fuzzer decode_basic_info_fuzzer + cjxl_fuzzer djxl_fuzzer icc_codec_fuzzer fields_fuzzer @@ -343,7 +300,7 @@ set(FUZZER_BINARIES # Fuzzers. foreach(FUZZER IN LISTS FUZZER_BINARIES) - if(${JPEGXL_ENABLE_FUZZERS}) + if(JPEGXL_ENABLE_FUZZERS) set(BINARY "${FUZZER}") add_executable("${BINARY}" "${BINARY}.cc") target_link_libraries("${BINARY}" ${JPEGXL_FUZZER_LINK_FLAGS}) @@ -355,16 +312,14 @@ foreach(FUZZER IN LISTS FUZZER_BINARIES) "fuzzer_stub.cc" "${FUZZER}.cc") endif() # JPEGXL_ENABLE_FUZZERS target_include_directories("${BINARY}" PRIVATE "${CMAKE_SOURCE_DIR}") - if(${FUZZER} STREQUAL djxl_fuzzer) + if(FUZZER STREQUAL djxl_fuzzer) target_link_libraries("${BINARY}" jxl_dec-static jxl_threads-static ) else() target_link_libraries("${BINARY}" - jxl-static jxl_extras-static - jxl_threads-static jxl_tool ) endif() @@ -378,7 +333,7 @@ if(BUILD_TESTING AND TARGET jxl AND NOT JPEGXL_EMSCRIPTEN) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tests) add_executable(libjxl_test libjxl_test.c) set_property(TARGET libjxl_test PROPERTY C_STANDARD 99) -if(NOT ${SANITIZER} STREQUAL "none") +if(NOT SANITIZER STREQUAL "none") # Linking a C test binary with the C++ JPEG XL implementation when using # address sanitizer is not well supported by clang 9, so force using clang++ # for linking this test if a sanitizer is used. @@ -393,12 +348,23 @@ if(NOT WIN32) endif() # NOT WIN32 endif() # NOT MSVC -add_test(NAME LibraryCLinkageTest COMMAND libjxl_test) +add_test( + NAME LibraryCLinkageTest + COMMAND libjxl_test + WORKING_DIRECTORY $<TARGET_FILE_DIR:jxl> +) +# if user decide to set CMAKE_SKIP_RPATH:BOOL=ON make sure libjxl.so.0.7 can +# still be found: +if(UNIX AND CMAKE_SKIP_RPATH) + set_property(TEST LibraryCLinkageTest PROPERTY ENVIRONMENT + LD_LIBRARY_PATH=${CMAKE_CURRENT_BINARY_DIR}/.. + ) +endif() endif() # BUILD_TESTING AND TARGET jxl AND NOT JPEGXL_EMSCRIPTEN # Tools defined in subdirectories. -if(${JPEGXL_ENABLE_VIEWERS}) +if(JPEGXL_ENABLE_VIEWERS) add_subdirectory(viewer) add_subdirectory(comparison_viewer) add_subdirectory(flicker_test) @@ -408,33 +374,39 @@ add_subdirectory(box) add_subdirectory(conformance) -if ("${JPEGXL_EMSCRIPTEN}") +if (JPEGXL_ENABLE_TOOLS AND JPEGXL_EMSCRIPTEN) # WASM API facade. add_executable(jxl_emcc jxl_emcc.cc) -target_link_libraries(jxl_emcc jxl-static jxl_extras-static) +target_link_libraries(jxl_emcc + jxl_extras-static +) set_target_properties(jxl_emcc PROPERTIES LINK_FLAGS "\ -O3\ --closure 1 \ - -s ALLOW_MEMORY_GROWTH=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=[\ - _jxlCompress,\ - _jxlDecompress,\ + _malloc,\ _free,\ - _malloc\ + _jxlCreateInstance,\ + _jxlDestroyInstance,\ + _jxlFlush,\ + _jxlProcessInput\ ]\"\ ") -endif () # JPEGXL_EMSCRIPTEN +endif () # JPEGXL_ENABLE_TOOLS AND JPEGXL_EMSCRIPTEN if(JPEGXL_ENABLE_JNI) find_package(JNI QUIET) find_package(Java QUIET) -if ("${JNI_FOUND}" AND "${Java_FOUND}") +if (JNI_FOUND AND Java_FOUND) include(UseJava) # decoder_jni_onload.cc might be necessary for Android; not used yet. @@ -467,7 +439,7 @@ if ("${JNI_FOUND}" AND "${Java_FOUND}") ) get_target_property(JXL_JNI_WRAPPER_TEST_JAR jxl_jni_wrapper_test JAR_FILE) - if(NOT "${SANITIZER}" MATCHES ".san") + if(NOT SANITIZER MATCHES ".san") # NB: Vanilla OpenJDK 8 / 11 are known to work well (i.e. either # "which java" or JAVA_HOME environment variable point to the path like # "/usr/lib/jvm/java-xx-openjdk-yyy" on Debian Linux). @@ -481,3 +453,17 @@ if ("${JNI_FOUND}" AND "${Java_FOUND}") endif() # JPEGXL_ENABLE_FUZZERS 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)) +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}") + endif() +endif() +endif() # BUILD_TESTING diff --git a/media/libjxl/src/tools/args.h b/media/libjxl/src/tools/args.h index cc26a35dc7..7d04ce3a7b 100644 --- a/media/libjxl/src/tools/args.h +++ b/media/libjxl/src/tools/args.h @@ -40,43 +40,6 @@ static inline bool ParseOverride(const char* arg, jxl::Override* out) { return JXL_FAILURE("Args"); } -static inline bool ParseUnsigned(const char* arg, size_t* out) { - char* end; - *out = static_cast<size_t>(strtoull(arg, &end, 0)); - if (end[0] != '\0') { - fprintf(stderr, "Unable to interpret as unsigned integer: %s.\n", arg); - return JXL_FAILURE("Args"); - } - return true; -} - -static inline bool ParseUint32(const char* arg, uint32_t* out) { - size_t value = 0; - bool ret = ParseUnsigned(arg, &value); - if (ret) *out = value; - return ret; -} - -static inline bool ParseSigned(const char* arg, int* out) { - char* end; - *out = static_cast<int>(strtol(arg, &end, 0)); - if (end[0] != '\0') { - fprintf(stderr, "Unable to interpret as signed integer: %s.\n", arg); - return JXL_FAILURE("Args"); - } - return true; -} - -static inline bool ParseFloat(const char* arg, float* out) { - char* end; - *out = static_cast<float>(strtod(arg, &end)); - if (end[0] != '\0') { - fprintf(stderr, "Unable to interpret as float: %s.\n", arg); - return JXL_FAILURE("Args"); - } - return true; -} - static inline bool ParseFloatPair(const char* arg, std::pair<float, float>* out) { int parsed = sscanf(arg, "%f,%f", &out->first, &out->second); @@ -91,16 +54,6 @@ static inline bool ParseFloatPair(const char* arg, return true; } -static inline bool ParseDouble(const char* arg, double* out) { - char* end; - *out = static_cast<double>(strtod(arg, &end)); - if (end[0] != '\0') { - fprintf(stderr, "Unable to interpret as double: %s.\n", arg); - return JXL_FAILURE("Args"); - } - return true; -} - static inline bool ParseAndAppendKeyValue(const char* arg, jxl::extras::ColorHints* out) { const char* eq = strchr(arg, '='); @@ -132,26 +85,11 @@ static inline bool ParsePredictor(const char* arg, jxl::Predictor* out) { return true; } -static inline bool ParseString(const char* arg, std::string* out) { - out->assign(arg); - return true; -} - static inline bool ParseCString(const char* arg, const char** out) { *out = arg; return true; } -static inline bool SetBooleanTrue(bool* out) { - *out = true; - return true; -} - -static inline bool SetBooleanFalse(bool* out) { - *out = false; - return true; -} - static inline bool IncrementUnsigned(size_t* out) { (*out)++; return true; diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec.cc b/media/libjxl/src/tools/benchmark/benchmark_codec.cc index 2deb7409dd..230665bbae 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_codec.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_codec.cc @@ -94,7 +94,6 @@ Status ImageCodec::ParseParam(const std::string& param) { } return true; } else if (param[0] == 'r') { - butteraugli_target_ = -1.0; ba_params_.hf_asymmetry = args_.ba_params.hf_asymmetry; bitrate_target_ = strtof(param.substr(1).c_str(), nullptr); return true; @@ -109,7 +108,7 @@ class NoneCodec : public ImageCodec { Status ParseParam(const std::string& param) override { return true; } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, PaddedBytes* compressed, + ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) override { PROFILER_ZONE("NoneCompress"); const double start = Now(); diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec.h b/media/libjxl/src/tools/benchmark/benchmark_codec.h index abc5c1a618..e554fc2802 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_codec.h +++ b/media/libjxl/src/tools/benchmark/benchmark_codec.h @@ -59,7 +59,8 @@ class ImageCodec { virtual bool IsJpegTranscoder() const { return false; } virtual Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, PaddedBytes* compressed, + ThreadPoolInternal* pool, + std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) = 0; virtual Status Decompress(const std::string& filename, @@ -72,7 +73,7 @@ class ImageCodec { virtual Status CanRecompressJpeg() const { return false; } virtual Status RecompressJpeg(const std::string& filename, const std::string& data, - PaddedBytes* compressed, + std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) { return false; } diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc index 68f47e69a4..fbe36b5a05 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_avif.cc @@ -213,7 +213,7 @@ class AvifCodec : public ImageCodec { } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, PaddedBytes* compressed, + ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) override { double elapsed_convert_image = 0; const double start = Now(); @@ -324,7 +324,7 @@ class AvifCodec : public ImageCodec { rgb_image.height * rgb_image.rowBytes), rgb_image.width, rgb_image.height, color, (has_alpha ? 4 : 3), /*alpha_is_premultiplied=*/false, rgb_image.depth, - JXL_NATIVE_ENDIAN, /*flipped_y=*/false, pool, &ib, + JXL_NATIVE_ENDIAN, pool, &ib, /*float_in=*/false, /*align=*/0)); io->frames.push_back(std::move(ib)); io->dec_pixels += rgb_image.width * rgb_image.height; diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc index a93fd50193..eefae6e65c 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_custom.cc @@ -86,7 +86,7 @@ class CustomCodec : public ImageCodec { } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, PaddedBytes* compressed, + ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) override { JXL_RETURN_IF_ERROR(param_index_ > 2); @@ -98,10 +98,8 @@ class CustomCodec : public ImageCodec { saved_intensity_target_ = io->metadata.m.IntensityTarget(); const size_t bits = io->metadata.m.bit_depth.bits_per_sample; - PaddedBytes png; JXL_RETURN_IF_ERROR( - extras::EncodeImageAPNG(io, io->Main().c_current(), bits, pool, &png)); - JXL_RETURN_IF_ERROR(WriteFile(png, png_filename)); + EncodeToFile(*io, io->Main().c_current(), bits, png_filename, pool)); std::vector<std::string> arguments = compress_args_; arguments.push_back(png_filename); arguments.push_back(encoded_filename); diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc index 78b304e832..ae3215a32f 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jpeg.cc @@ -24,51 +24,31 @@ #include "lib/jxl/codec_in_out.h" #include "tools/cmdline.h" -using jxl::extras::JpegEncoder; - namespace jxl { namespace { struct JPEGArgs { - JpegEncoder encoder = JpegEncoder::kLibJpeg; - YCbCrChromaSubsampling chroma_subsampling; + std::string jpeg_encoder = "libjpeg"; + std::string chroma_subsampling = "444"; }; JPEGArgs* const jpegargs = new JPEGArgs; -bool ParseChromaSubsampling(const char* param, - YCbCrChromaSubsampling* subsampling) { - std::vector<std::pair< - std::string, std::pair<std::array<uint8_t, 3>, std::array<uint8_t, 3>>>> - options = {{"444", {{{1, 1, 1}}, {{1, 1, 1}}}}, - {"420", {{{2, 1, 1}}, {{2, 1, 1}}}}, - {"422", {{{2, 1, 1}}, {{1, 1, 1}}}}, - {"440", {{{1, 1, 1}}, {{2, 1, 1}}}}}; - for (const auto& option : options) { - if (param == option.first) { - JXL_CHECK(subsampling->Set(option.second.first.data(), - option.second.second.data())); - return true; - } - } - return false; -} - } // namespace Status AddCommandLineOptionsJPEGCodec(BenchmarkArgs* args) { args->cmdline.AddOptionValue( '\0', "chroma_subsampling", "444/422/420/411", "default JPEG chroma subsampling (default: 444).", - &jpegargs->chroma_subsampling, &ParseChromaSubsampling); + &jpegargs->chroma_subsampling, &jpegxl::tools::ParseString); return true; } class JPEGCodec : public ImageCodec { public: explicit JPEGCodec(const BenchmarkArgs& args) : ImageCodec(args) { - encoder_ = jpegargs->encoder; + jpeg_encoder_ = jpegargs->jpeg_encoder; chroma_subsampling_ = jpegargs->chroma_subsampling; } @@ -77,24 +57,35 @@ class JPEGCodec : public ImageCodec { return true; } if (param == "sjpeg") { - encoder_ = JpegEncoder::kSJpeg; + jpeg_encoder_ = param; return true; } if (param.compare(0, 3, "yuv") == 0) { if (param.size() != 6) return false; - return ParseChromaSubsampling(param.c_str() + 3, &chroma_subsampling_); + chroma_subsampling_ = param.substr(3); + return true; } return false; } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, PaddedBytes* compressed, + ThreadPoolInternal* 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}; + 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(EncodeImageJPG(io, encoder_, - static_cast<int>(std::round(q_target_)), - chroma_subsampling_, pool, compressed)); + JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool)); const double end = Now(); + *compressed = encoded.bitstreams.back(); speed_stats->NotifyElapsed(end - start); return true; } @@ -114,8 +105,8 @@ class JPEGCodec : public ImageCodec { } protected: - JpegEncoder encoder_; - YCbCrChromaSubsampling chroma_subsampling_; + std::string jpeg_encoder_; + std::string chroma_subsampling_; }; ImageCodec* CreateNewJPEGCodec(const BenchmarkArgs& args) { diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc index 6dd1ad016e..6557858320 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_jxl.cc @@ -12,9 +12,13 @@ #include <utility> #include <vector> -#include "jxl/decode_cxx.h" #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/jpg.h" +#endif +#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" @@ -22,8 +26,6 @@ #include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/base/span.h" #include "lib/jxl/codec_in_out.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/dec_params.h" #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_external_image.h" @@ -58,6 +60,7 @@ struct JxlArgs { Override dots; Override patches; + bool log_search_state; std::string debug_image_dir; }; @@ -85,6 +88,9 @@ 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 " @@ -113,7 +119,9 @@ class JxlCodec : public ImageCodec { std::istringstream parser(param.substr(kEcResamplingPrefix.size())); parser >> cparams_.ec_resampling; } else if (ImageCodec::ParseParam(param)) { - // Nothing to do. + if (param[0] == 'd' && butteraugli_target_ == 0.0) { + cparams_.SetLossless(); + } } else if (param == "uint8") { uint8_ = true; } else if (param[0] == 'u') { @@ -178,8 +186,6 @@ class JxlCodec : public ImageCodec { return JXL_FAILURE("Invalid group size shift value"); } cparams_.modular_group_size_shift = gsize; - } else if (param == "new_heuristics") { - cparams_.use_new_heuristics = true; } else if (param == "plt") { cparams_.options.max_properties = 0; cparams_.options.nb_repeats = 0; @@ -193,6 +199,8 @@ class JxlCodec : public ImageCodec { if (cparams_.epf > 3) { return JXL_FAILURE("Invalid epf value"); } + } else if (param.substr(0, 2) == "nr") { + normalize_bitrate_ = true; } else if (param.substr(0, 16) == "faster_decoding=") { cparams_.decoding_speed_tier = strtol(param.substr(16).c_str(), nullptr, 10); @@ -205,6 +213,7 @@ class JxlCodec : public ImageCodec { 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; } @@ -215,7 +224,7 @@ class JxlCodec : public ImageCodec { } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, PaddedBytes* compressed, + ThreadPoolInternal* 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) { @@ -248,15 +257,34 @@ class JxlCodec : public ImageCodec { cparams_.color_transform = ColorTransform::kXYB; } + 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; - if (cparams_.use_new_heuristics) { - passes_encoder_state.heuristics = - jxl::make_unique<jxl::FastEncoderHeuristics>(); - } + PaddedBytes compressed_padded; JXL_RETURN_IF_ERROR(EncodeFile(cparams_, io, &passes_encoder_state, - compressed, GetJxlCms(), &cinfo_, pool)); + &compressed_padded, GetJxlCms(), &cinfo_, + pool)); const double end = Now(); + compressed->assign(compressed_padded.begin(), compressed_padded.end()); speed_stats->NotifyElapsed(end - start); return true; } @@ -265,121 +293,25 @@ class JxlCodec : public ImageCodec { const Span<const uint8_t> compressed, ThreadPoolInternal* pool, CodecInOut* io, jpegxl::tools::SpeedStats* speed_stats) override { - io->frames.clear(); - if (dparams_.max_passes != DecompressParams().max_passes || - dparams_.max_downsampling != DecompressParams().max_downsampling) { - // Must use the C++ API to honor non-default dparams. - if (uint8_) { - return JXL_FAILURE( - "trying to use decompress params that are not all available in " - "either decoding API"); - } - const double start = Now(); - JXL_RETURN_IF_ERROR(DecodeFile(dparams_, compressed, io, pool)); - const double end = Now(); - speed_stats->NotifyElapsed(end - start); - return true; - } - - double elapsed_convert_image = 0; + dparams_.runner = pool->runner(); + dparams_.runner_opaque = pool->runner_opaque(); + JxlDataType data_type = uint8_ ? JXL_TYPE_UINT8 : JXL_TYPE_FLOAT; + dparams_.accepted_formats = {{3, data_type, JXL_NATIVE_ENDIAN, 0}, + {4, data_type, JXL_NATIVE_ENDIAN, 0}}; + // By default, the decoder will undo exif orientation, giving an image + // with identity exif rotation as result. However, the benchmark does + // not undo exif orientation of the originals, and compares against the + // originals, so we must set the option to keep the original orientation + // instead. + dparams_.keep_orientation = true; + extras::PackedPixelFile ppf; + size_t decoded_bytes; const double start = Now(); - { - std::vector<uint8_t> pixel_data; - PaddedBytes icc_profile; - auto runner = JxlThreadParallelRunnerMake(nullptr, pool->NumThreads()); - auto dec = JxlDecoderMake(nullptr); - // By default, the decoder will undo exif orientation, giving an image - // with identity exif rotation as result. However, the benchmark does - // not undo exif orientation of the originals, and compares against the - // originals, so we must set the option to keep the original orientation - // instead. - JxlDecoderSetKeepOrientation(dec.get(), JXL_TRUE); - JXL_RETURN_IF_ERROR( - JXL_DEC_SUCCESS == - JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | - JXL_DEC_COLOR_ENCODING | - JXL_DEC_FULL_IMAGE)); - JxlBasicInfo info{}; - JxlPixelFormat format = {/*num_channels=*/3, - /*data_type=*/JXL_TYPE_FLOAT, - /*endianness=*/JXL_NATIVE_ENDIAN, - /*align=*/0}; - if (uint8_) { - format.data_type = JXL_TYPE_UINT8; - } - JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); - JxlDecoderStatus status; - while ((status = JxlDecoderProcessInput(dec.get())) != JXL_DEC_SUCCESS) { - switch (status) { - case JXL_DEC_ERROR: - return JXL_FAILURE("decoder error"); - case JXL_DEC_NEED_MORE_INPUT: - return JXL_FAILURE("decoder requests more input"); - case JXL_DEC_BASIC_INFO: - JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS == - JxlDecoderGetBasicInfo(dec.get(), &info)); - format.num_channels = info.num_color_channels; - if (info.alpha_bits != 0) { - ++format.num_channels; - io->metadata.m.extra_channel_info.resize(1); - io->metadata.m.extra_channel_info[0].type = - jxl::ExtraChannel::kAlpha; - } - break; - case JXL_DEC_COLOR_ENCODING: { - size_t icc_size; - JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS == - JxlDecoderGetICCProfileSize( - dec.get(), &format, - JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)); - icc_profile.resize(icc_size); - JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS == - JxlDecoderGetColorAsICCProfile( - dec.get(), &format, - JXL_COLOR_PROFILE_TARGET_DATA, - icc_profile.data(), icc_profile.size())); - break; - } - case JXL_DEC_NEED_IMAGE_OUT_BUFFER: { - size_t buffer_size; - JXL_RETURN_IF_ERROR( - JXL_DEC_SUCCESS == - JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)); - JXL_RETURN_IF_ERROR(buffer_size == - info.xsize * info.ysize * format.num_channels * - (uint8_ ? sizeof(uint8_t) : sizeof(float))); - pixel_data.resize(buffer_size); - JXL_RETURN_IF_ERROR(JXL_DEC_SUCCESS == - JxlDecoderSetImageOutBuffer(dec.get(), &format, - pixel_data.data(), - buffer_size)); - break; - } - case JXL_DEC_FULL_IMAGE: { - const double start_convert_image = Now(); - { - ColorEncoding color_encoding; - JXL_RETURN_IF_ERROR( - color_encoding.SetICC(PaddedBytes(icc_profile))); - ImageBundle frame(&io->metadata.m); - JXL_RETURN_IF_ERROR(BufferToImageBundle( - format, info.xsize, info.ysize, pixel_data.data(), - pixel_data.size(), pool, color_encoding, &frame)); - io->frames.push_back(std::move(frame)); - io->dec_pixels += info.xsize * info.ysize; - } - const double end_convert_image = Now(); - elapsed_convert_image += end_convert_image - start_convert_image; - break; - } - default: - return JXL_FAILURE("unrecognized status %d", - static_cast<int>(status)); - } - } - } + JXL_RETURN_IF_ERROR(DecodeImageJXL(compressed.data(), compressed.size(), + dparams_, &decoded_bytes, &ppf)); const double end = Now(); - speed_stats->NotifyElapsed(end - start - elapsed_convert_image); + speed_stats->NotifyElapsed(end - start); + JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); return true; } @@ -394,8 +326,9 @@ class JxlCodec : public ImageCodec { AuxOut cinfo_; CompressParams cparams_; bool has_ctransform_ = false; - DecompressParams dparams_; + extras::JXLDecompressParams dparams_; bool uint8_ = false; + bool normalize_bitrate_ = false; }; ImageCodec* CreateNewJxlCodec(const BenchmarkArgs& args) { diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc index e479d7ad24..b310b11bd9 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_png.cc @@ -12,6 +12,7 @@ #include <string> +#include "lib/extras/codec.h" #include "lib/extras/dec/apng.h" #include "lib/extras/enc/apng.h" #include "lib/extras/packed_image.h" @@ -40,12 +41,12 @@ class PNGCodec : public ImageCodec { Status ParseParam(const std::string& param) override { return true; } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, PaddedBytes* compressed, + ThreadPoolInternal* 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(extras::EncodeImageAPNG(io, io->Main().c_current(), - bits, pool, compressed)); + JXL_RETURN_IF_ERROR(Encode(*io, extras::Codec::kPNG, io->Main().c_current(), + bits, compressed, pool)); const double end = Now(); speed_stats->NotifyElapsed(end - start); return true; @@ -72,4 +73,4 @@ ImageCodec* CreateNewPNGCodec(const BenchmarkArgs& args) { } // namespace jxl -#endif
\ No newline at end of file +#endif diff --git a/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc index 376018c364..3b1bb264d3 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_codec_webp.cc @@ -40,11 +40,10 @@ Status FromSRGB(const size_t xsize, const size_t ysize, const bool is_gray, const ColorEncoding& c = ColorEncoding::SRGB(is_gray); const size_t bits_per_sample = (is_16bit ? 2 : 1) * kBitsPerByte; const Span<const uint8_t> span(pixels, end - pixels); - return ConvertFromExternal(span, xsize, ysize, c, - (is_gray ? 1 : 3) + (has_alpha ? 1 : 0), - alpha_is_premultiplied, bits_per_sample, - endianness, /*flipped_y=*/false, pool, ib, - /*float_in=*/false, /*align=*/0); + 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); } struct WebPArgs { @@ -86,7 +85,7 @@ class WebPCodec : public ImageCodec { } Status Compress(const std::string& filename, const CodecInOut* io, - ThreadPoolInternal* pool, PaddedBytes* compressed, + ThreadPoolInternal* pool, std::vector<uint8_t>* compressed, jpegxl::tools::SpeedStats* speed_stats) override { const double start = Now(); const ImageBundle& ib = io->Main(); @@ -105,7 +104,7 @@ class WebPCodec : public ImageCodec { size_t xsize = ib.oriented_xsize(); size_t ysize = ib.oriented_ysize(); size_t stride = xsize * num_chans; - PaddedBytes srgb(stride * ysize); + std::vector<uint8_t> srgb(stride * ysize); JXL_RETURN_IF_ERROR(ConvertToExternal( *transformed, 8, /*float_out=*/false, num_chans, JXL_BIG_ENDIAN, stride, pool, srgb.data(), srgb.size(), @@ -216,17 +215,18 @@ class WebPCodec : public ImageCodec { static int WebPStringWrite(const uint8_t* data, size_t data_size, const WebPPicture* const picture) { if (data_size) { - PaddedBytes* const out = static_cast<PaddedBytes*>(picture->custom_ptr); + std::vector<uint8_t>* const out = + static_cast<std::vector<uint8_t>*>(picture->custom_ptr); const size_t pos = out->size(); out->resize(pos + data_size); memcpy(out->data() + pos, data, data_size); } return 1; } - Status CompressInternal(const PaddedBytes& srgb, size_t xsize, size_t ysize, - size_t num_chans, int quality, - PaddedBytes* compressed) { - *compressed = PaddedBytes(); + Status CompressInternal(const std::vector<uint8_t>& srgb, size_t xsize, + size_t ysize, size_t num_chans, int quality, + std::vector<uint8_t>* compressed) { + compressed->clear(); WebPConfig config; WebPConfigInit(&config); JXL_ASSERT(!lossless_ || !near_lossless_); // can't have both diff --git a/media/libjxl/src/tools/benchmark/benchmark_stats.cc b/media/libjxl/src/tools/benchmark/benchmark_stats.cc index ef5932aa9d..f22e89c84f 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_stats.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_stats.cc @@ -166,6 +166,13 @@ 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; diff --git a/media/libjxl/src/tools/benchmark/benchmark_xl.cc b/media/libjxl/src/tools/benchmark/benchmark_xl.cc index e91fbb8c5a..fed5e9b1ba 100644 --- a/media/libjxl/src/tools/benchmark/benchmark_xl.cc +++ b/media/libjxl/src/tools/benchmark/benchmark_xl.cc @@ -72,7 +72,7 @@ Status ReadPNG(const std::string& filename, Image3F* image) { void DoCompress(const std::string& filename, const CodecInOut& io, const std::vector<std::string>& extra_metrics_commands, ImageCodec* codec, ThreadPoolInternal* inner_pool, - PaddedBytes* compressed, BenchmarkStats* s) { + std::vector<uint8_t>* compressed, BenchmarkStats* s) { PROFILER_FUNC; ++s->total_input_files; @@ -131,10 +131,9 @@ void DoCompress(const std::string& filename, const CodecInOut& io, } if (valid && Args()->decode_only) { - std::string data_in; + std::vector<uint8_t> data_in; JXL_CHECK(ReadFile(filename, &data_in)); - compressed->append((uint8_t*)data_in.data(), - (uint8_t*)data_in.data() + data_in.size()); + compressed->insert(compressed->end(), data_in.begin(), data_in.end()); } // Decompress @@ -266,9 +265,16 @@ 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; - // Make compatible for filename - std::replace(codec_name.begin(), codec_name.end(), ':', '_'); - std::string compressed_fn = outdir + "/" + name + "." + codec_name; + 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 decompressed_fn = compressed_fn + Args()->output_extension; #if JPEGXL_ENABLE_APNG std::string heatmap_fn = compressed_fn + ".heatmap.png"; @@ -1043,7 +1049,7 @@ class Benchmark { Task& t = (*tasks)[i]; const CodecInOut& image = loaded_images[t.idx_image]; t.image = ℑ - PaddedBytes compressed; + std::vector<uint8_t> compressed; DoCompress(fnames[t.idx_image], image, extra_metrics_commands, t.codec.get(), inner_pools[thread].get(), &compressed, &t.stats); diff --git a/media/libjxl/src/tools/box/CMakeLists.txt b/media/libjxl/src/tools/box/CMakeLists.txt index 3072cfef0b..c79add000b 100644 --- a/media/libjxl/src/tools/box/CMakeLists.txt +++ b/media/libjxl/src/tools/box/CMakeLists.txt @@ -18,7 +18,7 @@ target_include_directories(box "${PROJECT_SOURCE_DIR}" ) -if(${JPEGXL_ENABLE_DEVTOOLS}) +if(JPEGXL_ENABLE_DEVTOOLS) add_executable(box_list box_list_main.cc ) diff --git a/media/libjxl/src/tools/box/box.cc b/media/libjxl/src/tools/box/box.cc index a60af5b14b..db73c7ca72 100644 --- a/media/libjxl/src/tools/box/box.cc +++ b/media/libjxl/src/tools/box/box.cc @@ -38,17 +38,15 @@ jxl::Status ParseBoxHeader(const uint8_t** next_in, size_t* available_in, // Total box_size including this header itself. uint64_t box_size = LoadBE32(in + pos); - memcpy(box->type, in + pos + 4, 4); - - pos += 8; - + 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); @@ -282,53 +280,6 @@ jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container, // TODO(veluca): the format defined here encode some things multiple times. Fix // that. -jxl::Status DecodeJpegXlToJpeg(jxl::DecompressParams params, - const JpegXlContainer& container, - jxl::CodecInOut* io, jxl::ThreadPool* pool) { - params.keep_dct = true; - if (container.jpeg_reconstruction == nullptr) { - return JXL_FAILURE( - "Cannot decode to JPEG without a JPEG reconstruction box"); - } - - io->Main().jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>(); - - JXL_RETURN_IF_ERROR(DecodeJPEGData( - jxl::Span<const uint8_t>(container.jpeg_reconstruction, - container.jpeg_reconstruction_size), - io->Main().jpeg_data.get())); - - auto& jpeg_data = io->Main().jpeg_data; - bool have_exif = false, have_xmp = false; - for (size_t i = 0; i < jpeg_data->app_data.size(); i++) { - if (jpeg_data->app_marker_type[i] == jxl::jpeg::AppMarkerType::kExif) { - if (have_exif) - return JXL_FAILURE("Unexpected: more than one Exif box required?"); - if (jpeg_data->app_data[i].size() != container.exif_size + 9) { - return JXL_FAILURE( - "Exif box size does not match JPEG reconstruction data"); - } - have_exif = true; - memcpy(&jpeg_data->app_data[i][3 + 6], container.exif, - container.exif_size); - } - if (jpeg_data->app_marker_type[i] == jxl::jpeg::AppMarkerType::kXMP) { - if (have_xmp) - return JXL_FAILURE("Unexpected: more than one XMP box required?"); - if (jpeg_data->app_data[i].size() != container.xml[0].second + 32) { - return JXL_FAILURE( - "XMP box size does not match JPEG reconstruction data"); - } - have_xmp = true; - memcpy(&jpeg_data->app_data[i][3 + 29], container.xml[0].first, - container.xml[0].second); - } - } - - JXL_RETURN_IF_ERROR(DecodeFile( - params, jxl::Span<const uint8_t>(container.codestream), io, pool)); - return true; -} } // namespace tools } // namespace jpegxl diff --git a/media/libjxl/src/tools/box/box.h b/media/libjxl/src/tools/box/box.h index d6fd34fb67..4cc3058982 100644 --- a/media/libjxl/src/tools/box/box.h +++ b/media/libjxl/src/tools/box/box.h @@ -14,7 +14,6 @@ #include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/base/status.h" #include "lib/jxl/codec_in_out.h" -#include "lib/jxl/dec_file.h" #include "lib/jxl/enc_file.h" namespace jpegxl { @@ -108,12 +107,6 @@ jxl::Status DecodeJpegXlContainerOneShot(const uint8_t* data, size_t size, jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container, jxl::PaddedBytes* out); -// TODO(veluca): this doesn't really belong here. -jxl::Status DecodeJpegXlToJpeg(jxl::DecompressParams params, - const JpegXlContainer& container, - jxl::CodecInOut* io, - jxl::ThreadPool* pool = nullptr); - } // namespace tools } // namespace jpegxl diff --git a/media/libjxl/src/tools/build_cleaner.py b/media/libjxl/src/tools/build_cleaner.py index 0a0df75635..76857d7995 100755 --- a/media/libjxl/src/tools/build_cleaner.py +++ b/media/libjxl/src/tools/build_cleaner.py @@ -54,7 +54,7 @@ def SplitLibFiles(repo_files): """ testonly = ( - 'testdata.h', 'test_utils.h', '_test.h', '_test.cc', + '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' ) @@ -100,10 +100,6 @@ def SplitLibFiles(repo_files): "lib/jxl/progressive_split.h", # TODO(deymo): Add luminance.cc and luminance.h here too. Currently used # by aux_out.h. - # dec_file is not intended to be part of the decoder library, so move it - # to the encoder source set - "lib/jxl/dec_file.cc", - "lib/jxl/dec_file.h", ]) # Temporarily remove enc_bit_writer from the encoder sources: a lot of # decoder source code still needs to be split up into encoder and decoder. diff --git a/media/libjxl/src/tools/butteraugli_main.cc b/media/libjxl/src/tools/butteraugli_main.cc index f212aadd7c..247ade8d47 100644 --- a/media/libjxl/src/tools/butteraugli_main.cc +++ b/media/libjxl/src/tools/butteraugli_main.cc @@ -100,9 +100,11 @@ Status RunButteraugli(const char* pathname1, const char* pathname2, int main(int argc, char** argv) { if (argc < 3) { fprintf(stderr, - "Usage: %s <reference> <distorted> [--distmap <distmap>] " - "[--intensity_target <intensity_target>]\n" - "[--colorspace <colorspace_hint>]\n" + "Usage: %s <reference> <distorted>\n" + " [--distmap <distmap>]\n" + " [--intensity_target <intensity_target>]\n" + " [--colorspace <colorspace_hint>]\n" + " [--pnorm <pth norm>]\n" "NOTE: images get converted to linear sRGB for butteraugli. Images" " without attached profiles (such as ppm or pfm) are interpreted" " as nonlinear sRGB. The hint format is RGB_D65_SRG_Rel_Lin for" @@ -121,7 +123,7 @@ int main(int argc, char** argv) { } else if (std::string(argv[i]) == "--colorspace" && i + 1 < argc) { colorspace = argv[++i]; } else if (std::string(argv[i]) == "--intensity_target" && i + 1 < argc) { - intensity_target = std::stof(std::string(argv[i + 1])); + intensity_target = std::stof(std::string(argv[++i])); } else if (std::string(argv[i]) == "--pnorm" && i + 1 < argc) { char* end; p = strtod(argv[++i], &end); diff --git a/media/libjxl/src/tools/cjxl.cc b/media/libjxl/src/tools/cjxl.cc deleted file mode 100644 index 7d61feba64..0000000000 --- a/media/libjxl/src/tools/cjxl.cc +++ /dev/null @@ -1,769 +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/cjxl.h" - -#include <math.h> -#include <stdint.h> -#include <stdio.h> - -#include <algorithm> -#include <string> -#include <utility> -#include <vector> - -#include "lib/extras/codec.h" -#include "lib/extras/time.h" -#include "lib/jxl/aux_out.h" -#include "lib/jxl/base/cache_aligned.h" -#include "lib/jxl/base/compiler_specific.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/file_io.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/printf_macros.h" -#include "lib/jxl/base/profiler.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/base/thread_pool_internal.h" -#include "lib/jxl/codec_in_out.h" -#include "lib/jxl/common.h" -#include "lib/jxl/enc_cache.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_file.h" -#include "lib/jxl/enc_params.h" -#include "lib/jxl/frame_header.h" -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" -#include "lib/jxl/jpeg/enc_jpeg_data.h" -#include "lib/jxl/modular/encoding/encoding.h" -#include "tools/args.h" -#include "tools/box/box.h" -#include "tools/speed_stats.h" - -namespace jpegxl { -namespace tools { -namespace { - -static inline bool ParseSpeedTier(const char* arg, jxl::SpeedTier* out) { - return jxl::ParseSpeedTier(arg, out); -} -static inline bool ParseColorTransform(const char* arg, - jxl::ColorTransform* out) { - size_t value = 0; - bool ret = ParseUnsigned(arg, &value); - if (ret && value > 2) ret = false; - if (ret) *out = jxl::ColorTransform(value); - return ret; -} -static inline bool ParseIntensityTarget(const char* arg, float* out) { - return ParseFloat(arg, out) && *out > 0; -} -static inline bool ParsePhotonNoiseParameter(const char* arg, float* out) { - return strncmp(arg, "ISO", 3) == 0 && ParseFloat(arg + 3, out) && *out > 0; -} - -// Proposes a distance to try for a given bpp target. This could depend -// on the entropy in the image, too, but let's start with something. -static double ApproximateDistanceForBPP(double bpp) { - return 1.704 * pow(bpp, -0.804); -} - -jxl::Status LoadSaliencyMap(const std::string& filename_heatmap, - jxl::ThreadPool* pool, jxl::ImageF* out_map) { - jxl::CodecInOut io_heatmap; - if (!SetFromFile(filename_heatmap, jxl::extras::ColorHints(), &io_heatmap, - pool)) { - return JXL_FAILURE("Could not load heatmap."); - } - *out_map = std::move(io_heatmap.Main().color()->Plane(0)); - return true; -} - -void SetParametersForSizeOrBitrate(jxl::ThreadPoolInternal* pool, - const size_t pixels, CompressArgs* args) { - CompressArgs s = *args; // Args for search. - - // If fixed size, convert to bitrate. - if (s.params.target_size > 0) { - s.params.target_bitrate = s.params.target_size * 8.0 / pixels; - s.params.target_size = 0; - } - const double target_size = s.params.target_bitrate * (1 / 8.) * pixels; - - double dist = ApproximateDistanceForBPP(s.params.target_bitrate); - s.params.target_bitrate = 0; - double best_dist = 1.0; - double best_loss = 1e99; - - jxl::CodecInOut io; - double decode_mps = 0; - if (!LoadAll(*args, pool, &io, &decode_mps)) { - s.params.butteraugli_distance = static_cast<float>(dist); - printf("couldn't load image\n"); - return; - } - - for (int i = 0; i < 7; ++i) { - s.params.butteraugli_distance = static_cast<float>(dist); - jxl::PaddedBytes candidate; - bool ok = - CompressJxl(io, decode_mps, pool, s, &candidate, /*print_stats=*/false); - if (!ok) { - printf( - "Compression error occurred during the search for best size. " - "Trying with butteraugli distance %.15g\n", - best_dist); - break; - } - printf("Butteraugli distance %.3f yields %6" PRIuS " bytes, %.3f bpp.\n", - dist, candidate.size(), candidate.size() * 8.0 / pixels); - const double ratio = static_cast<double>(candidate.size()) / target_size; - const double loss = std::max(ratio, 1.0 / std::max(ratio, 1e-30)); - if (best_loss > loss) { - best_dist = dist; - best_loss = loss; - } - dist *= ratio; - if (dist < 0.01) { - dist = 0.01; - } - if (dist >= 16.0) { - dist = 16.0; - } - } - args->params.butteraugli_distance = static_cast<float>(best_dist); - args->params.target_bitrate = 0; - args->params.target_size = 0; -} - -const char* ModeFromArgs(const CompressArgs& args) { - if (args.jpeg_transcode) return "JPEG"; - if (args.params.modular_mode) return "Modular"; - return "VarDCT"; -} - -std::string QualityFromArgs(const CompressArgs& args) { - char buf[100]; - if (args.jpeg_transcode) { - snprintf(buf, sizeof(buf), "lossless transcode"); - } else if (args.params.IsLossless()) { - snprintf(buf, sizeof(buf), "lossless"); - } else { - snprintf(buf, sizeof(buf), "d%.3f", args.params.butteraugli_distance); - } - return buf; -} - -void PrintMode(jxl::ThreadPoolInternal* pool, const jxl::CodecInOut& io, - const double decode_mps, const CompressArgs& args) { - const char* mode = ModeFromArgs(args); - const char* speed = SpeedTierName(args.params.speed_tier); - const std::string quality = QualityFromArgs(args); - fprintf(stderr, - "Read %" PRIuS "x%" PRIuS - " image, %.1f MP/s\n" - "Encoding [%s%s, %s, %s", - io.xsize(), io.ysize(), decode_mps, - (args.use_container ? "Container | " : ""), mode, quality.c_str(), - speed); - if (args.use_container) { - if (args.jpeg_transcode && args.store_jpeg_metadata) - fprintf(stderr, " | JPEG reconstruction data"); - if (!io.blobs.exif.empty()) - fprintf(stderr, " | %" PRIuS "-byte Exif", io.blobs.exif.size()); - if (!io.blobs.xmp.empty()) - fprintf(stderr, " | %" PRIuS "-byte XMP", io.blobs.xmp.size()); - if (!io.blobs.jumbf.empty()) - fprintf(stderr, " | %" PRIuS "-byte JUMBF", io.blobs.jumbf.size()); - } - fprintf(stderr, "], %" PRIuS " threads.\n", pool->NumWorkerThreads()); -} - -} // namespace - -void CompressArgs::AddCommandLineOptions(CommandLineParser* cmdline) { - // Positional arguments. - cmdline->AddPositionalOption("INPUT", /* required = */ true, - "the input can be " -#if JPEGXL_ENABLE_APNG - "PNG, APNG, " -#endif -#if JPEGXL_ENABLE_GIF - "GIF, " -#endif -#if JPEGXL_ENABLE_JPEG - "JPEG, " -#else - "JPEG (lossless recompression only), " -#endif -#if JPEGXL_ENABLE_EXR - "EXR, " -#endif - "PPM, PFM, or PGX", - &file_in); - cmdline->AddPositionalOption( - "OUTPUT", /* required = */ true, - "the compressed JXL output file (can be omitted for benchmarking)", - &file_out); - - // Flags. - // TODO(lode): also add options to add exif/xmp/other metadata in the - // container. - // TODO(lode): decide on good name for this flag: box, container, bmff, ... - cmdline->AddOptionFlag( - '\0', "container", - "Always encode using container format (default: only if needed)", - &use_container, &SetBooleanTrue, 1); - - cmdline->AddOptionFlag('\0', "strip", - "Do not encode using container format (strips " - "Exif/XMP/JPEG bitstream reconstruction data)", - &no_container, &SetBooleanTrue, 2); - - cmdline->AddOptionFlag('\0', "strip_jpeg_metadata", - "Do not encode JPEG bitstream reconstruction data", - &store_jpeg_metadata, &SetBooleanFalse, 2); - - // Target distance/size/bpp - opt_distance_id = cmdline->AddOptionValue( - 'd', "distance", "maxError", - ("Max. butteraugli distance, lower = higher quality. Range: 0 .. 25.\n" - " 0.0 = mathematically lossless. Default for already-lossy input " - "(JPEG/GIF).\n" - " 1.0 = visually lossless. Default for other input.\n" - " Recommended range: 0.5 .. 3.0."), - ¶ms.butteraugli_distance, &ParseFloat); - opt_target_size_id = cmdline->AddOptionValue( - '\0', "target_size", "N", - ("Aim at file size of N bytes.\n" - " Compresses to 1 % of the target size in ideal conditions.\n" - " Runs the same algorithm as --target_bpp"), - ¶ms.target_size, &ParseUnsigned, 1); - opt_target_bpp_id = cmdline->AddOptionValue( - '\0', "target_bpp", "BPP", - ("Aim at file size that has N bits per pixel.\n" - " Compresses to 1 % of the target BPP in ideal conditions."), - ¶ms.target_bitrate, &ParseFloat, 1); - - // High-level options - opt_quality_id = cmdline->AddOptionValue( - 'q', "quality", "QUALITY", - "Quality setting (is remapped to --distance). Range: -inf .. 100.\n" - " 100 = mathematically lossless. Default for already-lossy input " - "(JPEG/GIF).\n Positive quality values roughly match libjpeg quality.", - &quality, &ParseFloat); - - cmdline->AddOptionValue( - 'e', "effort", "EFFORT", - "Encoder effort setting. Range: 1 .. 9.\n" - " Default: 7. Higher number is more effort (slower).", - ¶ms.speed_tier, &ParseSpeedTier); - - cmdline->AddOptionValue('\0', "brotli_effort", "B_EFFORT", - "Brotli effort setting. Range: 0 .. 11.\n" - " Default: -1 (based on EFFORT). Higher number is " - "more effort (slower).", - ¶ms.brotli_effort, &ParseSigned, -1); - - cmdline->AddOptionValue( - 's', "speed", "ANIMAL", - "Deprecated synonym for --effort. Valid values are:\n" - " lightning (1), thunder, falcon, cheetah, hare, wombat, squirrel, " - "kitten, tortoise (9)\n" - " Default: squirrel. Values are in order from faster to slower.\n", - ¶ms.speed_tier, &ParseSpeedTier, 2); - - cmdline->AddOptionValue('\0', "faster_decoding", "AMOUNT", - "Favour higher decoding speed. 0 = default, higher " - "values give higher speed at the expense of quality", - ¶ms.decoding_speed_tier, &ParseUnsigned, 2); - - cmdline->AddOptionFlag('p', "progressive", - "Enable progressive/responsive decoding.", - &progressive, &SetBooleanTrue); - - cmdline->AddOptionFlag('\0', "premultiply", - "Force premultiplied (associated) alpha.", - &force_premultiplied, &SetBooleanTrue, 1); - cmdline->AddOptionValue('\0', "keep_invisible", "0|1", - "force disable/enable preserving color of invisible " - "pixels (default: 1 if lossless, 0 if lossy).", - ¶ms.keep_invisible, &ParseOverride, 1); - - cmdline->AddOptionFlag('\0', "centerfirst", - "Put center groups first in the compressed file.", - ¶ms.centerfirst, &SetBooleanTrue, 1); - - cmdline->AddOptionValue('\0', "center_x", "0..XSIZE", - "Put center groups first in the compressed file.", - ¶ms.center_x, &ParseUnsigned, 1); - cmdline->AddOptionValue('\0', "center_y", "0..YSIZE", - "Put center groups first in the compressed file.", - ¶ms.center_y, &ParseUnsigned, 1); - - // Flags. - cmdline->AddOptionFlag('\0', "progressive_ac", - "Use the progressive mode for AC.", - ¶ms.progressive_mode, &SetBooleanTrue, 1); - cmdline->AddOptionFlag('\0', "qprogressive_ac", - "Use the progressive mode for AC.", - ¶ms.qprogressive_mode, &SetBooleanTrue, 1); - cmdline->AddOptionValue('\0', "progressive_dc", "num_dc_frames", - "Use progressive mode for DC.", - ¶ms.progressive_dc, &ParseSigned, 1); - cmdline->AddOptionFlag('m', "modular", - "Use the modular mode (lossy / lossless).", - ¶ms.modular_mode, &SetBooleanTrue, 1); - cmdline->AddOptionFlag('\0', "use_new_heuristics", - "use new and not yet ready encoder heuristics", - ¶ms.use_new_heuristics, &SetBooleanTrue, 2); - - // JPEG modes: parallel Brunsli, pixels to JPEG, or JPEG to Brunsli - cmdline->AddOptionFlag('j', "jpeg_transcode", - "Do lossy transcode of input JPEG file (decode to " - "pixels instead of doing lossless transcode).", - &jpeg_transcode, &SetBooleanFalse, 1); - cmdline->AddOptionFlag('\0', "jpeg_transcode_disable_cfl", - "Disable CFL for lossless JPEG recompression", - ¶ms.force_cfl_jpeg_recompression, &SetBooleanFalse, - 2); - - cmdline->AddOptionValue('\0', "num_threads", "N", - "number of worker threads (zero = none).", - &num_threads, &ParseUnsigned, 1); - cmdline->AddOptionValue('\0', "num_reps", "N", "how many times to compress.", - &num_reps, &ParseUnsigned, 1); - - cmdline->AddOptionValue('\0', "noise", "0|1", - "force disable/enable noise generation.", - ¶ms.noise, &ParseOverride, 1); - cmdline->AddOptionValue( - '\0', "photon_noise", "ISO3200", - "Set the noise to approximately what it would be at a given nominal " - "exposure on a 35mm camera. For formats other than 35mm, or when the " - "whole sensor was not used, you can multiply the ISO value by the " - "equivalence ratio squared, for example by 2.25 for an APS-C camera.", - ¶ms.photon_noise_iso, &ParsePhotonNoiseParameter, 1); - cmdline->AddOptionValue('\0', "dots", "0|1", - "force disable/enable dots generation.", ¶ms.dots, - &ParseOverride, 1); - cmdline->AddOptionValue('\0', "patches", "0|1", - "force disable/enable patches generation.", - ¶ms.patches, &ParseOverride, 1); - cmdline->AddOptionValue( - '\0', "resampling", "-1|1|2|4|8", - "Subsample all color channels by this factor, or use -1 to choose the " - "resampling factor based on distance.", - ¶ms.resampling, &ParseSigned, 0); - cmdline->AddOptionValue( - '\0', "ec_resampling", "1|2|4|8", - "Subsample all extra channels by this factor. If this value is smaller " - "than the resampling of color channels, it will be increased to match.", - ¶ms.ec_resampling, &ParseSigned, 2); - cmdline->AddOptionFlag('\0', "already_downsampled", - "Do not downsample the given input before encoding, " - "but still signal that the decoder should upsample.", - ¶ms.already_downsampled, &SetBooleanTrue, 2); - - cmdline->AddOptionValue( - '\0', "epf", "-1..3", - "Edge preserving filter level (-1 = choose based on quality, default)", - ¶ms.epf, &ParseSigned, 1); - - cmdline->AddOptionValue('\0', "gaborish", "0|1", - "force disable/enable gaborish.", ¶ms.gaborish, - &ParseOverride, 1); - - opt_intensity_target_id = cmdline->AddOptionValue( - '\0', "intensity_target", "N", - ("Intensity target of monitor in nits, higher\n" - " results in higher quality image. Must be strictly positive.\n" - " Default is 255 for standard images, 4000 for input images known to\n" - " to have PQ or HLG transfer function."), - &intensity_target, &ParseIntensityTarget, 1); - - cmdline->AddOptionValue('\0', "saliency_num_progressive_steps", "N", nullptr, - ¶ms.saliency_num_progressive_steps, - &ParseUnsigned, 2); - cmdline->AddOptionValue('\0', "saliency_map_filename", "STRING", nullptr, - &saliency_map_filename, &ParseString, 2); - cmdline->AddOptionValue('\0', "saliency_threshold", "0..1", nullptr, - ¶ms.saliency_threshold, &ParseFloat, 2); - - cmdline->AddOptionValue( - 'x', "dec-hints", "key=value", - "color_space indicates the ColorEncoding, see Description();\n" - "icc_pathname refers to a binary file containing an ICC profile.", - &color_hints, &ParseAndAppendKeyValue, 1); - - cmdline->AddOptionValue( - '\0', "override_bitdepth", "0=use from image, 1-32=override", - "If nonzero, store the given bit depth in the JPEG XL file metadata" - " (1-32), instead of using the bit depth from the original input" - " image.", - &override_bitdepth, &ParseUnsigned, 2); - - opt_color_id = cmdline->AddOptionValue( - 'c', "colortransform", "0..2", "0=XYB, 1=None, 2=YCbCr", - ¶ms.color_transform, &ParseColorTransform, 2); - - // modular mode options - cmdline->AddOptionValue( - 'I', "iterations", "F", - "[modular encoding] fraction of pixels used to learn MA trees " - "(default=0.5, try 0 for no MA and fast decode)", - ¶ms.options.nb_repeats, &ParseFloat, 2); - - cmdline->AddOptionValue( - 'C', "colorspace", "K", - ("[modular encoding] color transform: 0=RGB, 1=YCoCg, " - "2-37=RCT (default: try several, depending on speed)"), - ¶ms.colorspace, &ParseSigned, 1); - - opt_m_group_size_id = cmdline->AddOptionValue( - 'g', "group-size", "K", - ("[modular encoding] set group size to 128 << K " - "(default: 1 or 2)"), - ¶ms.modular_group_size_shift, &ParseUnsigned, 1); - - cmdline->AddOptionValue( - 'P', "predictor", "K", - "[modular encoding] predictor(s) to use: 0=zero, " - "1=left, 2=top, 3=avg0, 4=select, 5=gradient, 6=weighted, " - "7=topright, 8=topleft, 9=leftleft, 10=avg1, 11=avg2, 12=avg3, " - "13=toptop predictive average " - "14=mix 5 and 6, 15=mix everything. Default 14, at slowest speed " - "default 15", - ¶ms.options.predictor, &ParsePredictor, 1); - - cmdline->AddOptionValue( - 'E', "extra-properties", "K", - "[modular encoding] number of extra MA tree properties to use", - ¶ms.options.max_properties, &ParseSigned, 2); - - cmdline->AddOptionValue('\0', "palette", "K", - "[modular encoding] use a palette if image has at " - "most K colors (default: 1024)", - ¶ms.palette_colors, &ParseSigned, 1); - - cmdline->AddOptionFlag( - '\0', "lossy-palette", - "[modular encoding] quantize to a palette that has fewer entries than " - "would be necessary for perfect preservation; for the time being, it is " - "recommended to set --palette=0 with this option to use the default " - "palette only", - ¶ms.lossy_palette, &SetBooleanTrue, 1); - - cmdline->AddOptionValue( - 'X', "pre-compact", "PERCENT", - ("[modular encoding] compact channels (globally) if ratio " - "used/range is below this (default: 80%)"), - ¶ms.channel_colors_pre_transform_percent, &ParseFloat, 2); - - cmdline->AddOptionValue( - 'Y', "post-compact", "PERCENT", - ("[modular encoding] compact channels (per-group) if ratio " - "used/range is below this (default: 80%)"), - ¶ms.channel_colors_percent, &ParseFloat, 2); - - cmdline->AddOptionValue('R', "responsive", "K", - "[modular encoding] do Squeeze transform, 0=false, " - "1=true (default: true if lossy, false if lossless)", - ¶ms.responsive, &ParseSigned, 1); - - cmdline->AddOptionFlag('V', "version", "Print version number and exit", - &version, &SetBooleanTrue, 1); - cmdline->AddOptionFlag('\0', "quiet", "Be more silent", &quiet, - &SetBooleanTrue, 1); - cmdline->AddOptionValue('\0', "print_profile", "0|1", - "Print timing information before exiting", - &print_profile, &ParseOverride, 1); - - cmdline->AddOptionFlag( - 'v', "verbose", - "Verbose output; can be repeated, also applies to help (!).", - ¶ms.verbose, &SetBooleanTrue); -} - -jxl::Status CompressArgs::ValidateArgs(const CommandLineParser& cmdline) { - params.file_in = file_in; - params.file_out = file_out; - - if (file_in == nullptr) { - fprintf(stderr, "Missing INPUT filename.\n"); - return false; - } - - bool got_distance = cmdline.GetOption(opt_distance_id)->matched(); - bool got_target_size = cmdline.GetOption(opt_target_size_id)->matched(); - bool got_target_bpp = cmdline.GetOption(opt_target_bpp_id)->matched(); - bool got_quality = cmdline.GetOption(opt_quality_id)->matched(); - bool got_intensity_target = - cmdline.GetOption(opt_intensity_target_id)->matched(); - - if (got_quality) { - default_settings = false; - if (quality < 100) jpeg_transcode = false; - if (quality < 7 || quality == 100 || params.modular_mode) { - if (jpeg_transcode == false) params.modular_mode = true; - } - // Quality settings roughly match libjpeg qualities. - if (quality >= 100) { - params.butteraugli_distance = 0; - } else if (quality >= 30) { - params.butteraugli_distance = 0.1 + (100 - quality) * 0.09; - } else { - params.butteraugli_distance = - 6.4 + pow(2.5, (30 - quality) / 5.0f) / 6.25f; - } - } - - if (params.resampling > 1 && !params.already_downsampled) - jpeg_transcode = false; - - if (progressive) { - params.qprogressive_mode = true; - params.responsive = 1; - default_settings = false; - } - if (got_target_size || got_target_bpp || got_intensity_target) { - default_settings = false; - } - - if (params.progressive_dc < -1 || params.progressive_dc > 2) { - fprintf(stderr, "Invalid/out of range progressive_dc (%d), try -1 to 2.\n", - params.progressive_dc); - return false; - } - - if (got_distance) { - constexpr float butteraugli_min_dist = 0.1f; - constexpr float butteraugli_max_dist = 25.0f; - if (!(0 <= params.butteraugli_distance && - params.butteraugli_distance <= butteraugli_max_dist)) { - fprintf(stderr, "Invalid/out of range distance, try 0 to %g.\n", - butteraugli_max_dist); - return false; - } - if (params.butteraugli_distance > 0) jpeg_transcode = false; - if (params.butteraugli_distance == 0) { - // Use modular for lossless. - if (jpeg_transcode == false) params.modular_mode = true; - } else if (params.butteraugli_distance < butteraugli_min_dist) { - params.butteraugli_distance = butteraugli_min_dist; - } - default_settings = false; - } - - if (got_target_bpp || got_target_size) { - jpeg_transcode = false; - } - if (params.brotli_effort > 11) { - fprintf(stderr, "Invalid --brotli_effort value\n"); - return false; - } - if (got_target_bpp + got_target_size + got_distance + got_quality > 1) { - fprintf(stderr, - "You can specify only one of '--distance', '-q', " - "'--target_bpp' and '--target_size'. They are all different ways" - " to specify the image quality. When in doubt, use --distance." - " It gives the most visually consistent results.\n"); - return false; - } - - if (!saliency_map_filename.empty()) { - if (!params.progressive_mode) { - saliency_map_filename.clear(); - fprintf(stderr, - "Warning: Specifying --saliency_map_filename only makes sense " - "for --progressive_ac mode.\n"); - } - } - - if (!params.file_in) { - fprintf(stderr, "Missing input filename.\n"); - return false; - } - - if (!cmdline.GetOption(opt_color_id)->matched()) { - // default to RGB for lossless modular - if (params.modular_mode) { - if (params.butteraugli_distance > 0.f) { - params.color_transform = jxl::ColorTransform::kXYB; - } else { - params.color_transform = jxl::ColorTransform::kNone; - } - } - } - - if (override_bitdepth > 32) { - fprintf(stderr, "override_bitdepth must be <= 32\n"); - return false; - } - - if (params.epf > 3) { - fprintf(stderr, "--epf must be in the 0..3 range\n"); - return false; - } - - return true; -} - -jxl::Status CompressArgs::ValidateArgsAfterLoad( - const CommandLineParser& cmdline, const jxl::CodecInOut& io) { - if (!ValidateArgs(cmdline)) return false; - bool got_m_group_size = cmdline.GetOption(opt_m_group_size_id)->matched(); - if (params.modular_mode && !got_m_group_size) { - // Default modular group size: set to 512 if 256 would be silly - const size_t kThinImageThr = 256 + 64; - const size_t kSmallImageThr = 256 + 128; - if (io.xsize() < kThinImageThr || io.ysize() < kThinImageThr || - (io.xsize() < kSmallImageThr && io.ysize() < kSmallImageThr)) { - params.modular_group_size_shift = 2; - } - } - if (!io.blobs.exif.empty() || !io.blobs.xmp.empty() || - !io.blobs.jumbf.empty() || !io.blobs.iptc.empty() || - (jpeg_transcode && store_jpeg_metadata)) { - use_container = true; - } - if (no_container) use_container = false; - if (jpeg_transcode && params.modular_mode) { - fprintf(stderr, - "Error: cannot do lossless JPEG transcode in modular mode.\n"); - return false; - } - if (jpeg_transcode) { - if (params.progressive_mode || params.qprogressive_mode || - params.progressive_dc > 0) { - fprintf(stderr, - "Error: progressive lossless JPEG transcode is not yet " - "implemented.\n"); - return false; - } - } - return true; -} - -jxl::Status LoadAll(CompressArgs& args, jxl::ThreadPoolInternal* pool, - jxl::CodecInOut* io, double* decode_mps) { - const double t0 = jxl::Now(); - - jxl::PaddedBytes encoded; - JXL_RETURN_IF_ERROR(jxl::ReadFile(args.params.file_in, &encoded)); - jxl::extras::Codec input_codec; - bool ok; - if (args.jpeg_transcode && encoded.size() >= 2 && encoded[0] == 0xFF && - encoded[1] == 0xD8) { - input_codec = jxl::extras::Codec::kJPG; - ok = jxl::jpeg::DecodeImageJPG(jxl::Span<const uint8_t>(encoded), io); - } else { - ok = jxl::SetFromBytes(jxl::Span<const uint8_t>(encoded), args.color_hints, - io, nullptr, &input_codec); - } - if (!ok) { - fprintf(stderr, "Failed to read image %s.\n", args.params.file_in); - return false; - } - if (args.intensity_target != 0) { - io->metadata.m.SetIntensityTarget(args.intensity_target); - } - if (input_codec != jxl::extras::Codec::kJPG) args.jpeg_transcode = false; - if (args.jpeg_transcode) args.params.butteraugli_distance = 0; - - if (input_codec == jxl::extras::Codec::kGIF && args.default_settings) { - args.params.modular_mode = true; - args.params.butteraugli_distance = 0; - } - if (args.override_bitdepth != 0) { - if (args.override_bitdepth == 32) { - io->metadata.m.SetFloat32Samples(); - } else { - io->metadata.m.SetUintSamples(args.override_bitdepth); - } - } - if (args.force_premultiplied) { - io->PremultiplyAlpha(); - } - - jxl::ImageF saliency_map; - if (!args.saliency_map_filename.empty()) { - if (!LoadSaliencyMap(args.saliency_map_filename, pool, &saliency_map)) { - fprintf(stderr, "Failed to read saliency map %s.\n", - args.saliency_map_filename.c_str()); - return false; - } - args.params.saliency_map = &saliency_map; - } - - const double t1 = jxl::Now(); - const size_t pixels = io->xsize() * io->ysize(); - *decode_mps = pixels * io->frames.size() * 1E-6 / (t1 - t0); - - return true; -} - -jxl::Status CompressJxl(jxl::CodecInOut& io, double decode_mps, - jxl::ThreadPoolInternal* pool, CompressArgs& args, - jxl::PaddedBytes* compressed, bool print_stats) { - JXL_CHECK(pool); - - const size_t pixels = io.xsize() * io.ysize(); - - if (args.params.target_size > 0 || args.params.target_bitrate > 0) { - // Slow iterative search for parameters that reach target bpp / size. - SetParametersForSizeOrBitrate(pool, pixels, &args); - } - - if (print_stats) PrintMode(pool, io, decode_mps, args); - - // Final/actual compression run (possibly repeated for benchmarking). - jxl::AuxOut aux_out; - if (args.inspector_image3f) { - aux_out.SetInspectorImage3F(args.inspector_image3f); - } - SpeedStats stats; - jxl::PassesEncoderState passes_encoder_state; - if (args.params.use_new_heuristics) { - passes_encoder_state.heuristics = - jxl::make_unique<jxl::FastEncoderHeuristics>(); - } - for (size_t i = 0; i < args.num_reps; ++i) { - const double t0 = jxl::Now(); - jxl::Status ok = false; - if (io.Main().IsJPEG()) { - // TODO(lode): automate this in the encoder. The encoder must in the - // beginning choose to either do all in xyb, or all in non-xyb, write - // that in the xyb_encoded header flag, and persistently keep that state - // to check if every frame uses an allowed color transform. - args.params.color_transform = io.Main().color_transform; - } - ok = EncodeFile(args.params, &io, &passes_encoder_state, compressed, - jxl::GetJxlCms(), &aux_out, pool); - if (!ok) { - fprintf(stderr, "Failed to compress to %s.\n", ModeFromArgs(args)); - return false; - } - const double t1 = jxl::Now(); - stats.NotifyElapsed(t1 - t0); - stats.SetImageSize(io.xsize(), io.ysize()); - } - - if (print_stats) { - const double bpp = - static_cast<double>(compressed->size() * jxl::kBitsPerByte) / pixels; - fprintf(stderr, "Compressed to %" PRIuS " bytes (%.3f bpp%s).\n", - compressed->size(), bpp / io.frames.size(), - io.frames.size() == 1 ? "" : "/frame"); - JXL_CHECK(stats.Print(args.num_threads)); - if (args.params.verbose) { - aux_out.Print(1); - } - } - - return true; -} - -} // namespace tools -} // namespace jpegxl diff --git a/media/libjxl/src/tools/cjxl.h b/media/libjxl/src/tools/cjxl.h deleted file mode 100644 index 1be3500dfd..0000000000 --- a/media/libjxl/src/tools/cjxl.h +++ /dev/null @@ -1,109 +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. - -#ifndef TOOLS_CJXL_H_ -#define TOOLS_CJXL_H_ - -#include <stddef.h> - -#include <string> -#include <thread> -#include <utility> - -#include "lib/extras/dec/color_hints.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/override.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/base/thread_pool_internal.h" -#include "lib/jxl/codec_in_out.h" -#include "lib/jxl/enc_params.h" -#include "lib/jxl/jxl_inspection.h" -#include "tools/cmdline.h" - -namespace jpegxl { -namespace tools { - -struct CompressArgs { - void SetInspectorImage3F(const jxl::InspectorImage3F& inspector) { - inspector_image3f = inspector; - } - - // Add all the command line options to the CommandLineParser. Note that the - // options are tied to the instance that this was called on. - void AddCommandLineOptions(CommandLineParser* cmdline); - - // Post-processes and validates the passed arguments, checking whether all - // passed options are compatible. Returns whether the validation was - // successful. - jxl::Status ValidateArgs(const CommandLineParser& cmdline); - - // Validates the arguments again, having loaded the input so sensible defaults - // can be chosen based on e.g. dimensions. - jxl::Status ValidateArgsAfterLoad(const CommandLineParser& cmdline, - const jxl::CodecInOut& io); - - // Common flags. - bool version = false; - bool use_container = false; - bool no_container = false; - bool quiet = false; - - const char* file_in = nullptr; - const char* file_out = nullptr; - jxl::Override print_profile = jxl::Override::kDefault; - - // Decoding source image flags - jxl::extras::ColorHints color_hints; - - // JXL flags - size_t override_bitdepth = 0; - jxl::CompressParams params; - size_t num_threads = std::thread::hardware_concurrency(); - size_t num_reps = 1; - float intensity_target = 0; - - // Filename for the user provided saliency-map. - std::string saliency_map_filename; - - // Whether to perform lossless transcoding with kVarDCT or kJPEG encoding. - // If true, attempts to load JPEG coefficients instead of pixels. - // Reset to false if input image is not a JPEG. - bool jpeg_transcode = true; - - bool store_jpeg_metadata = true; - - float quality = -1001.f; // Default to lossless if input is already lossy, - // or to VarDCT otherwise. - bool progressive = false; - bool default_settings = true; - bool force_premultiplied = false; - - // Will get passed on to AuxOut. - jxl::InspectorImage3F inspector_image3f; - - // References (ids) of specific options to check if they were matched. - CommandLineParser::OptionId opt_distance_id = -1; - CommandLineParser::OptionId opt_target_size_id = -1; - CommandLineParser::OptionId opt_target_bpp_id = -1; - CommandLineParser::OptionId opt_quality_id = -1; - CommandLineParser::OptionId opt_near_lossless_id = -1; - CommandLineParser::OptionId opt_intensity_target_id = -1; - CommandLineParser::OptionId opt_color_id = -1; - CommandLineParser::OptionId opt_m_group_size_id = -1; -}; - -jxl::Status LoadAll(CompressArgs& args, jxl::ThreadPoolInternal* pool, - jxl::CodecInOut* io, double* decode_mps); - -// The input image must already have been loaded into io using LoadAll. -jxl::Status CompressJxl(jxl::CodecInOut& io, double decode_mps, - jxl::ThreadPoolInternal* pool, CompressArgs& args, - jxl::PaddedBytes* compressed, bool print_stats = true); - -} // namespace tools -} // namespace jpegxl - -#endif // TOOLS_CJXL_H_ diff --git a/media/libjxl/src/tools/cjxl_fuzzer.cc b/media/libjxl/src/tools/cjxl_fuzzer.cc new file mode 100644 index 0000000000..f3a1d9f9d8 --- /dev/null +++ b/media/libjxl/src/tools/cjxl_fuzzer.cc @@ -0,0 +1,231 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <algorithm> +#include <functional> +#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" + +namespace { + +#define TRY(expr) \ + do { \ + if (JXL_ENC_SUCCESS != (expr)) return false; \ + } while (0) + +struct FuzzSpec { + size_t xsize; + size_t ysize; + struct OptionSpec { + JxlEncoderFrameSettingId id; + int32_t value; + }; + std::vector<OptionSpec> options; + bool is_jpeg = false; + bool lossless = false; + bool have_alpha = false; + bool premultiply = false; + bool orig_profile = true; + uint16_t pixels_seed = 0; + uint16_t alpha_seed = 0; + size_t bit_depth = 8; + size_t alpha_bit_depth = 8; + int32_t codestream_level = -1; + std::vector<uint8_t> icc; + JxlColorEncoding color_encoding; + size_t num_frames = 1; + size_t output_buffer_size = 1; +}; + +bool EncodeJpegXl(const FuzzSpec& spec) { + // Multi-threaded parallel runner. Limit to max 2 threads since the fuzzer + // itself is already multithreaded. + size_t num_threads = + std::min<size_t>(2, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + auto runner = JxlThreadParallelRunnerMake(nullptr, num_threads); + JxlEncoderPtr enc_ptr = JxlEncoderMake(/*memory_manager=*/nullptr); + JxlEncoder* enc = enc_ptr.get(); + for (size_t num_rep = 0; num_rep < 2; ++num_rep) { + JxlEncoderReset(enc); + TRY(JxlEncoderSetParallelRunner(enc, JxlThreadParallelRunner, + runner.get())); + JxlEncoderFrameSettings* frame_settings = + JxlEncoderFrameSettingsCreate(enc, nullptr); + + for (auto option : spec.options) { + TRY(JxlEncoderFrameSettingsSetOption(frame_settings, option.id, + option.value)); + } + + TRY(JxlEncoderSetCodestreamLevel(enc, spec.codestream_level)); + JxlBasicInfo basic_info; + JxlEncoderInitBasicInfo(&basic_info); + basic_info.xsize = spec.xsize; + basic_info.ysize = spec.ysize; + basic_info.bits_per_sample = spec.bit_depth; + basic_info.uses_original_profile = spec.orig_profile; + if (spec.have_alpha) { + basic_info.alpha_bits = spec.alpha_bit_depth; + basic_info.num_extra_channels = 1; + } + TRY(JxlEncoderSetBasicInfo(enc, &basic_info)); + if (spec.lossless) { + TRY(JxlEncoderSetFrameLossless(frame_settings, JXL_TRUE)); + } + + // TODO(szabadka) Add icc color profiles. + TRY(JxlEncoderSetColorEncoding(enc, &spec.color_encoding)); + + // TODO(szabadka) Add jpeg frames. + for (size_t i = 0; i < spec.num_frames; ++i) { + JxlFrameHeader frame_header; + JxlEncoderInitFrameHeader(&frame_header); + // TODO(szabadka) Add more frame header options. + TRY(JxlEncoderSetFrameHeader(frame_settings, &frame_header)); + if (spec.have_alpha) { + JxlExtraChannelInfo extra_channel_info; + JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &extra_channel_info); + TRY(JxlEncoderSetExtraChannelInfo(enc, 0, &extra_channel_info)); + extra_channel_info.alpha_premultiplied = spec.premultiply; + } + JxlPixelFormat pixelformat = {3, JXL_TYPE_UINT16, JXL_LITTLE_ENDIAN, 0}; + std::vector<uint8_t> pixels = jxl::test::GetSomeTestImage( + spec.xsize, spec.ysize, 3, spec.pixels_seed); + TRY(JxlEncoderAddImageFrame(frame_settings, &pixelformat, pixels.data(), + pixels.size())); + if (spec.have_alpha) { + std::vector<uint8_t> alpha_pixels = jxl::test::GetSomeTestImage( + spec.xsize, spec.ysize, 1, spec.alpha_seed); + TRY(JxlEncoderSetExtraChannelBuffer(frame_settings, &pixelformat, + alpha_pixels.data(), + alpha_pixels.size(), 0)); + } + } + // 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); + uint8_t* next_out = buf.data(); + size_t avail_out = buf.size(); + process_result = JxlEncoderProcessOutput(enc, &next_out, &avail_out); + } + if (JXL_ENC_SUCCESS != process_result) { + return false; + } + } + return true; +} + +template <typename T> +T Select(const std::vector<T>& vec, std::function<uint32_t(size_t)> get_index) { + return vec[get_index(vec.size() - 1)]; +} + +int TestOneInput(const uint8_t* data, size_t size) { + uint64_t flags = 0; + size_t flag_bits = 0; + + const auto consume_data = [&]() { + if (size < 4) abort(); + uint32_t buf = 0; + memcpy(&buf, data, 4); + data += 4; + size -= 4; + flags = (flags << 32) | buf; + flag_bits += 32; + }; + + const auto get_flag = [&](size_t max_value) { + size_t limit = 1; + while (limit <= max_value) { + limit <<= 1; + --flag_bits; + if (flag_bits <= 16) { + consume_data(); + } + } + uint32_t result = flags % limit; + flags /= limit; + return result % (max_value + 1); + }; + + std::vector<JxlColorSpace> colorspaces = { + JXL_COLOR_SPACE_RGB, JXL_COLOR_SPACE_GRAY, JXL_COLOR_SPACE_XYB, + JXL_COLOR_SPACE_UNKNOWN}; + std::vector<JxlWhitePoint> whitepoints = { + JXL_WHITE_POINT_D65, JXL_WHITE_POINT_CUSTOM, JXL_WHITE_POINT_E, + JXL_WHITE_POINT_DCI}; + std::vector<JxlPrimaries> primaries = {JXL_PRIMARIES_SRGB, + JXL_PRIMARIES_CUSTOM, + JXL_PRIMARIES_2100, JXL_PRIMARIES_P3}; + std::vector<JxlTransferFunction> transfer_functions = { + JXL_TRANSFER_FUNCTION_709, JXL_TRANSFER_FUNCTION_UNKNOWN, + JXL_TRANSFER_FUNCTION_LINEAR, JXL_TRANSFER_FUNCTION_SRGB, + JXL_TRANSFER_FUNCTION_PQ, JXL_TRANSFER_FUNCTION_DCI, + JXL_TRANSFER_FUNCTION_HLG, JXL_TRANSFER_FUNCTION_GAMMA}; + std::vector<JxlRenderingIntent> rendering_intents = { + JXL_RENDERING_INTENT_PERCEPTUAL, + JXL_RENDERING_INTENT_RELATIVE, + JXL_RENDERING_INTENT_SATURATION, + JXL_RENDERING_INTENT_ABSOLUTE, + }; + + FuzzSpec spec; + // Randomly set some options. + // TODO(szabadka) Make value bounds option specific. + size_t num_options = get_flag(32); + for (size_t i = 0; i < num_options; ++i) { + FuzzSpec::OptionSpec option; + option.id = static_cast<JxlEncoderFrameSettingId>(get_flag(32)); + option.value = static_cast<int32_t>(get_flag(16)) - 1; + spec.options.push_back(option); + } + + spec.xsize = get_flag(4095) + 1; + spec.ysize = get_flag(4095) + 1; + spec.lossless = get_flag(1); + if (!spec.lossless) { + spec.orig_profile = get_flag(1); + } + spec.have_alpha = get_flag(1); + spec.premultiply = get_flag(1); + spec.pixels_seed = get_flag((1 << 16) - 1); + spec.alpha_seed = get_flag((1 << 16) - 1); + spec.bit_depth = get_flag(15) + 1; + spec.alpha_bit_depth = get_flag(15) + 1; + spec.color_encoding.color_space = Select(colorspaces, get_flag); + spec.color_encoding.white_point = Select(whitepoints, get_flag); + spec.color_encoding.primaries = Select(primaries, get_flag); + spec.color_encoding.transfer_function = Select(transfer_functions, get_flag); + spec.color_encoding.rendering_intent = Select(rendering_intents, get_flag); + spec.output_buffer_size = get_flag(4095) + 1; + + const auto targets = hwy::SupportedAndGeneratedTargets(); + hwy::SetSupportedTargetsForTest(Select(targets, get_flag)); + EncodeJpegXl(spec); + hwy::SetSupportedTargetsForTest(0); + + return 0; +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return TestOneInput(data, size); +} diff --git a/media/libjxl/src/tools/cjxl_main.cc b/media/libjxl/src/tools/cjxl_main.cc index 157400a13d..e43bb27075 100644 --- a/media/libjxl/src/tools/cjxl_main.cc +++ b/media/libjxl/src/tools/cjxl_main.cc @@ -3,19 +3,63 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -#include <stdio.h> +// Note: This encoder binary does extensive flag-validity checking (in +// order to produce meaningful error messages), and on top of that +// checks all libjxl C API call return values. The downside of this +// vs. libjxl providing meaningful error messages is that a change to +// the accepted range of a flag-specified parameter in libjxl will +// also require a change to the range-check here. The advantage is +// that this minimizes the size of libjxl. +#include <stdint.h> + +#include <cmath> +#include <cstdlib> +#include <functional> +#include <iostream> +#include <sstream> +#include <string> +#include <thread> +#include <type_traits> +#include <vector> + +#include "jxl/codestream_header.h" #include "jxl/encode.h" -#include "lib/jxl/base/file_io.h" -#include "lib/jxl/base/profiler.h" -#include "lib/jxl/jpeg/enc_jpeg_data.h" -#include "tools/box/box.h" -#include "tools/cjxl.h" +#include "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/pnm.h" +#include "lib/extras/time.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/printf_macros.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" +#include "tools/file_io.h" +#include "tools/speed_stats.h" namespace jpegxl { namespace tools { +namespace { +inline bool ParsePhotonNoiseParameter(const char* arg, float* out) { + return strncmp(arg, "ISO", 3) == 0 && ParseFloat(arg + 3, out) && *out > 0; +} +inline bool ParseIntensityTarget(const char* arg, float* out) { + return ParseFloat(arg, out) && *out > 0; +} + +} // namespace + enum CjxlRetCode : int { OK = 0, ERR_PARSE, @@ -28,124 +72,1144 @@ enum CjxlRetCode : int { DROPPED_JBRD, }; -int CompressJpegXlMain(int argc, const char* argv[]) { - CommandLineParser cmdline; - CompressArgs args; - args.AddCommandLineOptions(&cmdline); +struct CompressArgs { + // CompressArgs() = default; + void AddCommandLineOptions(CommandLineParser* cmdline) { + // Positional arguments. + cmdline->AddPositionalOption("INPUT", /* required = */ true, + "the input can be " +#if JPEGXL_ENABLE_APNG + "PNG, APNG, " +#endif +#if JPEGXL_ENABLE_GIF + "GIF, " +#endif +#if JPEGXL_ENABLE_JPEG + "JPEG, " +#else + "JPEG (lossless recompression only), " +#endif +#if JPEGXL_ENABLE_EXR + "EXR, " +#endif + "PPM, PFM, or PGX", + &file_in); + cmdline->AddPositionalOption( + "OUTPUT", /* required = */ true, + "the compressed JXL output file (can be omitted for benchmarking)", + &file_out); - if (!cmdline.Parse(argc, argv)) { - // Parse already printed the actual error cause. - fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); - return CjxlRetCode::ERR_PARSE; + // 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); + + // Target distance/size/bpp + opt_distance_id = cmdline->AddOptionValue( + 'd', "distance", "maxError", + "Max. butteraugli distance, lower = higher quality.\n" + " 0.0 = mathematically lossless. Default for already-lossy input " + "(JPEG/GIF).\n" + " 1.0 = visually lossless. Default for other input.\n" + " Recommended range: 0.5 .. 3.0. Mutually exclusive with --quality.", + &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, &ParseFloat); + + cmdline->AddOptionValue( + 'e', "effort", "EFFORT", + "Encoder effort setting. Range: 1 .. 9.\n" + " Default: 7. Higher number is more effort (slower).", + &effort, &ParseUnsigned, -1); + + 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('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); + + 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'.", + &group_order, &ParseOverride, 1); + + cmdline->AddOptionValue( + '\0', "center_x", "0..XSIZE", + "Determines the horizontal position of center for the center-first " + "group order. The value -1 means 'use the middle of the image', " + "other values [0..xsize) set this to a particular coordinate.", + ¢er_x, &ParseInt64, 1); + + cmdline->AddOptionValue( + '\0', "center_y", "0..YSIZE", + "Determines the vertical position of center for the center-first " + "group order. The value -1 means 'use the middle of the image', " + "other values [0..ysize) set this to a particular coordinate.", + ¢er_y, &ParseInt64, 1); + + // Flags. + cmdline->AddOptionFlag('\0', "progressive_ac", + "Use the progressive mode for AC.", &progressive_ac, + &SetBooleanTrue, 1); + + opt_qprogressive_ac_id = cmdline->AddOptionFlag( + '\0', "qprogressive_ac", + "Use the progressive mode for AC with shift quantization.", + &qprogressive_ac, &SetBooleanTrue, 1); + + cmdline->AddOptionValue( + '\0', "progressive_dc", "num_dc_frames", + "Progressive-DC setting. Valid values are: -1, 0, 1, 2.", + &progressive_dc, &ParseSigned, 1); + + cmdline->AddOptionValue( + 'm', "modular", "0|1", + "Use modular mode (not provided = encoder chooses, 0 = enforce VarDCT, " + "1 = enforce modular mode).", + &modular, &ParseOverride, 1); + + // JPEG modes: parallel Brunsli, pixels to JPEG, or JPEG to Brunsli + opt_lossless_jpeg_id = cmdline->AddOptionValue( + 'j', "lossless_jpeg", "0|1", + "If the input is JPEG, losslessly transcode JPEG, " + "rather than using reencode pixels.", + &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.", + &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); + + cmdline->AddOptionValue( + '\0', "patches", "0|1", + "Force disable/enable patches generation. " + "(not provided = default, 0 = disable, 1 = enable).", + &patches, &ParseOverride, 1); + + 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); + + 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); + + cmdline->AddOptionFlag('\0', "already_downsampled", + "Do not downsample the given input before encoding, " + "but still signal that the decoder should upsample.", + &already_downsampled, &SetBooleanTrue, 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); + + cmdline->AddOptionValue( + '\0', "gaborish", "0|1", + "Force disable/enable the gaborish filter. " + "(not provided = default, 0 = disable, 1 = enable).", + &gaborish, &ParseOverride, 1); + + 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); + + cmdline->AddOptionValue( + 'x', "dec-hints", "key=value", + "color_space indicates the ColorEncoding, see Description();\n" + "icc_pathname refers to a binary file containing an ICC profile.", + &color_hints, &ParseAndAppendKeyValue, 1); + + cmdline->AddOptionValue( + '\0', "override_bitdepth", "0=use from image, 1-32=override", + "If nonzero, store the given bit depth in the JPEG XL file metadata" + " (1-32), instead of using the bit depth from the original input" + " image.", + &override_bitdepth, &ParseUnsigned, 2); + + // 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); + + 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); + + 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); + + 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); + + cmdline->AddOptionValue( + 'E', "modular_nb_prev_channels", "K", + "[modular encoding] number of extra MA tree properties to use", + &modular_nb_prev_channels, &ParseSigned, 2); + + 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); + + 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); + + 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); } - if (args.version) { - fprintf(stdout, "cjxl %s\n", - CodecConfigString(JxlEncoderVersion()).c_str()); - fprintf(stdout, "Copyright (c) the JPEG XL Project\n"); - return CjxlRetCode::OK; + // Common flags. + bool version = false; + jxl::Override container = jxl::Override::kDefault; + bool quiet = false; + + const char* file_in = nullptr; + const char* file_out = nullptr; + jxl::Override print_profile = jxl::Override::kDefault; + + // Decoding source image flags + jxl::extras::ColorHints color_hints; + + // JXL flags + size_t override_bitdepth = 0; + int32_t num_threads = -1; + 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. + size_t lossless_jpeg = 1; + + size_t jpeg_store_metadata = 1; + + float quality = -1001.f; // Default to lossless if input is already lossy, + // or to VarDCT otherwise. + bool verbose = false; + bool progressive = false; + bool progressive_ac = false; + bool qprogressive_ac = false; + int32_t progressive_dc = -1; + bool modular_lossy_palette = false; + int32_t premultiply = -1; + bool already_downsampled = false; + jxl::Override jpeg_reconstruction_cfl = jxl::Override::kDefault; + jxl::Override modular = jxl::Override::kDefault; + jxl::Override keep_invisible = jxl::Override::kDefault; + jxl::Override dots = jxl::Override::kDefault; + jxl::Override patches = jxl::Override::kDefault; + jxl::Override gaborish = jxl::Override::kDefault; + jxl::Override group_order = jxl::Override::kDefault; + + size_t faster_decoding = 0; + int32_t resampling = -1; + int32_t ec_resampling = -1; + int32_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; + 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; + float modular_ma_tree_learning_percent = -1.f; + float photon_noise_iso = 0; + int32_t codestream_level = -1; + int32_t responsive = -1; + float 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; + + // 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_quality_id = -1; + CommandLineParser::OptionId opt_qprogressive_ac_id = -1; + CommandLineParser::OptionId opt_modular_group_size_id = -1; +}; + +const char* ModeFromArgs(const CompressArgs& args) { + if (args.lossless_jpeg) return "JPEG"; + if (args.modular == jxl::Override::kOn || args.distance == 0) + return "Modular"; + return "VarDCT"; +} + +std::string DistanceFromArgs(const CompressArgs& args) { + char buf[100]; + if (args.lossless_jpeg) { + snprintf(buf, sizeof(buf), "lossless transcode"); + } else if (args.distance == 0) { + snprintf(buf, sizeof(buf), "lossless"); + } else { + snprintf(buf, sizeof(buf), "d%.3f", args.distance); } + return buf; +} - if (!args.quiet) { - fprintf(stderr, "JPEG XL encoder %s\n", - CodecConfigString(JxlEncoderVersion()).c_str()); +void PrintMode(jxl::extras::PackedPixelFile& ppf, const double decode_mps, + size_t num_bytes, const CompressArgs& args) { + 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); + } 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); } + fprintf(stderr, "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"); + if (!ppf.metadata.exif.empty()) + fprintf(stderr, " | %" PRIuS "-byte Exif", ppf.metadata.exif.size()); + if (!ppf.metadata.xmp.empty()) + fprintf(stderr, " | %" PRIuS "-byte XMP", ppf.metadata.xmp.size()); + if (!ppf.metadata.jumbf.empty()) + fprintf(stderr, " | %" PRIuS "-byte JUMBF", ppf.metadata.jumbf.size()); + } + fprintf(stderr, "], \n"); +} - if (cmdline.HelpFlagPassed()) { - cmdline.PrintHelp(); - return CjxlRetCode::OK; +} // namespace tools +} // namespace jpegxl + +namespace { + +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; + exit(EXIT_FAILURE); } +} - if (!args.ValidateArgs(cmdline)) { - // ValidateArgs already printed the actual error cause. - fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); - return CjxlRetCode::ERR_INVALID_ARG; +void SetDistanceFromFlags(JxlEncoderFrameSettings* jxl_encoder_frame_settings, + jpegxl::tools::CommandLineParser* cmdline, + jpegxl::tools::CompressArgs* args, + const jxl::extras::Codec& codec) { + bool distance_set = cmdline->GetOption(args->opt_distance_id)->matched(); + bool quality_set = cmdline->GetOption(args->opt_quality_id)->matched(); + 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; + 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; + } + if (JXL_ENC_SUCCESS != + JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, args->distance)) { + std::cerr << "Setting frame distance failed." << std::endl; + exit(EXIT_FAILURE); } +} - jxl::PaddedBytes compressed; +using flag_check_fn = std::function<std::string(int64_t)>; +using flag_check_float_fn = std::function<std::string(float)>; - jxl::ThreadPoolInternal pool(args.num_threads); - jxl::CodecInOut io; - double decode_mps = 0; - if (!LoadAll(args, &pool, &io, &decode_mps)) { - return CjxlRetCode::ERR_LOAD_INPUT; +bool IsJPG(const std::vector<uint8_t>& image_data) { + return (image_data.size() >= 2 && image_data[0] == 0xFF && + image_data[1] == 0xD8); +} + +// 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; + } +#if JPEGXL_ENABLE_GIF + if (jxl::extras::DecodeImageGIF(encoded, color_hints, size_constraints, + &ppf)) { + return jxl::extras::Codec::kGIF; + } +#endif +#if JPEGXL_ENABLE_JPEG + if (jxl::extras::DecodeImageJPG(encoded, color_hints, size_constraints, + &ppf)) { + return jxl::extras::Codec::kJPG; + } +#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 + +int main(int argc, char** argv) { + std::string version = jpegxl::tools::CodecConfigString(JxlEncoderVersion()); + jpegxl::tools::CompressArgs args; + jpegxl::tools::CommandLineParser cmdline; + args.AddCommandLineOptions(&cmdline); - // need to validate again because now we know the input - if (!args.ValidateArgsAfterLoad(cmdline, io)) { + 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 CjxlRetCode::ERR_INVALID_INPUT; + return jpegxl::tools::CjxlRetCode::ERR_PARSE; } + + if (args.version) { + fprintf(stdout, "cjxl %s\n", version.c_str()); + fprintf(stdout, "Copyright (c) the JPEG XL Project\n"); + return jpegxl::tools::CjxlRetCode::OK; + } + + if (!args.quiet) { + fprintf(stderr, "JPEG XL encoder %s\n", version.c_str()); + } + + if (cmdline.HelpFlagPassed() || !args.file_in) { + cmdline.PrintHelp(); + return jpegxl::tools::CjxlRetCode::OK; + } + if (!args.file_out && !args.quiet) { fprintf(stderr, "No output file specified.\n" "Encoding will be performed, but the result will be discarded.\n"); } - if (!CompressJxl(io, decode_mps, &pool, args, &compressed, !args.quiet)) { - return CjxlRetCode::ERR_ENCODING; + + // Loading the input. + // Depending on flags-settings, we want to either load a JPEG and + // faithfully convert it to JPEG XL, or load (JPEG or non-JPEG) + // pixel data. + 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)) { + 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; + exit(EXIT_FAILURE); + } + if (ppf.frames.empty()) { + std::cerr << "No frames on input file." << std::endl; + exit(EXIT_FAILURE); + } + + const double t1 = jxl::Now(); + pixels = ppf.info.xsize * ppf.info.ysize; + decode_mps = pixels * ppf.info.num_color_channels * 1E-6 / (t1 - t0); } - int ret = CjxlRetCode::OK; - if (args.use_container) { - JpegXlContainer container; - container.codestream = std::move(compressed); - if (!io.blobs.exif.empty()) { - container.exif = io.blobs.exif.data(); - container.exif_size = io.blobs.exif.size(); + 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; + } } - auto append_xml = [&container](const std::vector<uint8_t>& bytes) { - if (bytes.empty()) return; - container.xml.emplace_back(bytes.data(), bytes.size()); + 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; + 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; + 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); + } }; - append_xml(io.blobs.xmp); - if (!io.blobs.jumbf.empty()) { - container.jumb = io.blobs.jumbf.data(); - container.jumb_size = io.blobs.jumbf.size(); - } - jxl::PaddedBytes jpeg_data; - if (args.store_jpeg_metadata && io.Main().IsJPEG()) { - jxl::jpeg::JPEGData data_in = *io.Main().jpeg_data; - if (EncodeJPEGData(data_in, &jpeg_data, args.params)) { - container.jpeg_reconstruction = jpeg_data.data(); - container.jpeg_reconstruction_size = jpeg_data.size(); + + { // 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 ""; + }); + } + { // 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 (num_rep == 0 && !args.quiet) + PrintMode(ppf, decode_mps, image_data.size(), args); + + if (args.lossless_jpeg && 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; + } + } + + 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 { - fprintf(stderr, "Warning: failed to create JPEG reconstruction data\n"); - ret = CjxlRetCode::DROPPED_JBRD; + if (JXL_ENC_SUCCESS != + JxlEncoderSetColorEncoding(jxl_encoder, &ppf.color_encoding)) { + std::cerr << "JxlEncoderSetColorEncoding() failed." << std::endl; + return EXIT_FAILURE; + } + } + + for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) { + const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame]; + const jxl::extras::PackedImage& pimage = pframe.color; + JxlPixelFormat ppixelformat = pimage.format; + { + if (JXL_ENC_SUCCESS != + JxlEncoderSetFrameHeader(jxl_encoder_frame_settings, + &pframe.frame_info)) { + std::cerr << "JxlEncoderSetFrameHeader() failed." << std::endl; + return EXIT_FAILURE; + } + } + if (num_frame < 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; + } + } + } } } - compressed = {}; - if (!EncodeJpegXlContainerOneShot(container, &compressed)) { - fprintf(stderr, "Failed to encode container format\n"); - return CjxlRetCode::ERR_CONTAINER; + 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; + } } - if (!args.quiet) { - const size_t pixels = io.xsize() * io.ysize(); - const double bpp = - static_cast<double>(compressed.size() * jxl::kBitsPerByte) / pixels; - fprintf(stderr, "Including container: %llu bytes (%.3f bpp%s).\n", - static_cast<long long unsigned>(compressed.size()), - bpp / io.frames.size(), io.frames.size() == 1 ? "" : "/frame"); + compressed.resize(next_out - compressed.data()); + if (JXL_ENC_SUCCESS != process_result) { + std::cerr << "JxlEncoderProcessOutput failed." << std::endl; + return EXIT_FAILURE; } + + const double t1 = jxl::Now(); + stats.NotifyElapsed(t1 - t0); + stats.SetImageSize(ppf.info.xsize, ppf.info.ysize); } + if (args.file_out) { - if (!jxl::WriteFile(compressed, args.file_out)) { - fprintf(stderr, "Failed to write to \"%s\"\n", args.file_out); - return CjxlRetCode::ERR_WRITE; + if (!jpegxl::tools::WriteFile(args.file_out, compressed)) { + std::cerr << "Could not write jxl file." << std::endl; + return EXIT_FAILURE; } } - - if (args.print_profile == jxl::Override::kOn) { - PROFILER_PRINT_RESULTS(); - } - if (!args.quiet && cmdline.verbosity > 0) { - jxl::CacheAligned::PrintStats(); + if (!args.quiet) { + const double bpp = + static_cast<double>(compressed.size() * jxl::kBitsPerByte) / pixels; + fprintf(stderr, "Compressed to %" PRIuS " bytes ", compressed.size()); + // 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 "); + } + if (!args.lossless_jpeg) { + fprintf(stderr, "(%.3f bpp%s).\n", bpp / ppf.frames.size(), + ppf.frames.size() == 1 ? "" : "/frame"); + JXL_CHECK(stats.Print(num_worker_threads)); + } else { + fprintf(stderr, "\n"); + } } - return ret; -} - -} // namespace tools -} // namespace jpegxl - -int main(int argc, const char** argv) { - return jpegxl::tools::CompressJpegXlMain(argc, argv); + return EXIT_SUCCESS; } diff --git a/media/libjxl/src/tools/cjxl_ng_main.cc b/media/libjxl/src/tools/cjxl_ng_main.cc deleted file mode 100644 index 403ee82cfd..0000000000 --- a/media/libjxl/src/tools/cjxl_ng_main.cc +++ /dev/null @@ -1,922 +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. - -// Note: This encoder binary does extensive flag-validity checking (in -// order to produce meaningful error messages), and on top of that -// checks all libjxl C API call return values. The downside of this -// vs. libjxl providing meaningful error messages is that a change to -// the accepted range of a flag-specified parameter in libjxl will -// also require a change to the range-check here. The advantage is -// that this minimizes the size of libjxl. - -#include <stdint.h> - -#include <cmath> -#include <cstdlib> -#include <functional> -#include <iostream> -#include <sstream> -#include <string> -#include <vector> - -#include "gflags/gflags.h" -#include "jxl/codestream_header.h" -#include "jxl/encode.h" -#include "jxl/encode_cxx.h" -#include "jxl/thread_parallel_runner.h" -#include "jxl/thread_parallel_runner_cxx.h" -#include "jxl/types.h" -#include "lib/extras/codec.h" -#include "lib/extras/dec/apng.h" -#include "lib/extras/dec/color_hints.h" -#include "lib/extras/dec/gif.h" -#include "lib/extras/dec/jpg.h" -#include "lib/extras/dec/pgx.h" -#include "lib/extras/dec/pnm.h" -#include "lib/jxl/base/file_io.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/size_constraints.h" - -DECLARE_bool(help); -DECLARE_bool(helpshort); -// The flag --version is owned by gflags itself. -DEFINE_bool(encoder_version, false, - "Print encoder library version number and exit."); - -DEFINE_bool(lossless_jpeg, true, - "If the input is JPEG, use JxlEncoderAddJPEGFrame " - "to add a JPEG frame (i.e. losslessly transcoding JPEG), " - "rather than using JxlEncoderAddImageFrame to reencode pixels."); - -DEFINE_bool(jpeg_store_metadata, true, - "If --lossless_jpeg is set, store JPEG reconstruction " - "metadata in the JPEG XL container " - "(for lossless reconstruction of the JPEG codestream)."); - -DEFINE_bool(jpeg_reconstruction_cfl, true, - "Enable/disable chroma-from-luma (CFL) for lossless " - "JPEG reconstruction."); - -DEFINE_bool(container, false, - "Force using container format (default: use only if needed)."); - -DEFINE_bool(strip, false, - "Do not encode using container format (strips " - "Exif/XMP/JPEG bitstream reconstruction data)."); - -DEFINE_bool(responsive, false, "[modular encoding] Do Squeeze transform"); - -DEFINE_bool(progressive, false, "Enable progressive/responsive decoding."); - -DEFINE_bool(progressive_ac, false, "Use progressive mode for AC."); - -DEFINE_bool(qprogressive_ac, false, "Use progressive mode for AC."); - -DEFINE_bool(modular_lossy_palette, false, "Use delta-palette."); - -DEFINE_int32(premultiply, -1, - "Force premultiplied (associated) alpha. " - "-1 = Do what the input does, 0 = Do not premultiply, " - "1 = force premultiply."); - -DEFINE_bool(already_downsampled, false, - "Do not downsample the given input before encoding, " - "but still signal that the decoder should upsample."); - -DEFINE_bool( - modular, false, - "Use modular mode (not provided = encoder chooses, 0 = enforce VarDCT, " - "1 = enforce modular mode)."); - -DEFINE_bool(keep_invisible, false, - "Force disable/enable preserving color of invisible " - "pixels. (not provided = default, 0 = disable, 1 = enable)."); - -DEFINE_bool(dots, false, - "Force disable/enable dots generation. " - "(not provided = default, 0 = disable, 1 = enable)."); - -DEFINE_bool(patches, false, - "Force disable/enable patches generation. " - "(not provided = default, 0 = disable, 1 = enable)."); - -DEFINE_bool(gaborish, false, - "Force disable/enable the gaborish filter. " - "(not provided = default, 0 = disable, 1 = enable)."); - -DEFINE_bool( - group_order, false, - "Order in which 256x256 regions are stored " - "in the codestream for progressive rendering. " - "Value not provided means 'encoder default', 0 means 'scanline order', " - "1 means 'center-first order'."); - -DEFINE_double( - intensity_target, 0.0, - "Upper bound on the intensity level present in the image in nits. " - "Leaving this set to its default of 0 lets libjxl choose a sensible " - "default " - "value based on the color encoding."); - -// TODO(tfish): -// --dec-hints, -- NEED (passed to image decoders, via extras, tweaks decoding) -// --override_bitdepth, -- NEED - -DEFINE_int32(progressive_dc, -1, - "Progressive-DC setting. Valid values are: -1, 0, 1, 2."); - -DEFINE_int32(faster_decoding, 0, - "Favour higher decoding speed. 0 = default, higher " - "values give higher speed at the expense of quality"); - -DEFINE_int32( - resampling, -1, - "Resampling. Default of -1 applies resampling only for low quality. " - "Value 1 does no downsampling (1x1), 2 does 2x2 downsampling, " - "4 is for 4x4 downsampling, and 8 for 8x8 downsampling."); - -DEFINE_int32( - ec_resampling, -1, - "Resampling for extra channels. Default of -1 applies resampling only " - "for low quality. Value 1 does no downsampling (1x1), 2 does 2x2 " - "downsampling, 4 is for 4x4 downsampling, and 8 for 8x8 downsampling."); - -DEFINE_int32( - epf, -1, - "Edge preserving filter level, -1 to 3. " - "Value -1 means: default (encoder chooses), 0 to 3 set a strength."); - -DEFINE_int64( - center_x, -1, - "Determines the horizontal position of center for the center-first " - "group order. The value -1 means 'use the middle of the image', " - "other values [0..xsize) set this to a particular coordinate."); - -DEFINE_int64(center_y, -1, - "Determines the vertical position of center for the center-first " - "group order. The value -1 means 'use the middle of the image', " - "other values [0..ysize) set this to a particular coordinate."); - -DEFINE_int64(num_threads, -1, - "Number of worker threads (-1 == use machine default, " - "0 == do not use multithreading)."); - -DEFINE_int64(num_reps, 1, "How many times to compress. (For benchmarking)."); - -DEFINE_int32(modular_group_size, -1, - "[modular encoding] group size: -1 == default. 0 => 128, " - "1 => 256, 2 => 512, 3 => 1024"); - -DEFINE_int32(modular_predictor, -1, - "[modular encoding] predictor(s) to use: 0=zero, " - "1=left, 2=top, 3=avg0, 4=select, 5=gradient, 6=weighted, " - "7=topright, 8=topleft, 9=leftleft, 10=avg1, 11=avg2, 12=avg3, " - "13=toptop predictive average " - "14=mix 5 and 6, 15=mix everything. If unset, uses default 14, " - "at slowest speed default 15."); - -DEFINE_int32(modular_colorspace, -1, - "[modular encoding] color transform: 0=RGB, 1=YCoCg, " - "2-37=RCT (default: try several, depending on speed)"); - -DEFINE_int32( - modular_channel_colors_global_percent, -1, - "[modular encoding] Use Global channel palette if the number of " - "colors is smaller than this percentage of range. " - "Use 0-100 to set an explicit percentage, -1 to use the encoder default."); - -DEFINE_int32( - modular_channel_colors_group_percent, -1, - "[modular encoding] Use Local (per-group) channel palette if the number " - "of colors is smaller than this percentage of range. Use 0-100 to set " - "an explicit percentage, -1 to use the encoder default."); - -DEFINE_int32( - modular_palette_colors, -1, - "[modular encoding] Use color palette if number of colors is smaller " - "than or equal to this, or -1 to use the encoder default."); - -DEFINE_int32(modular_nb_prev_channels, -1, - "[modular encoding] number of extra MA tree properties to use"); - -DEFINE_int32(modular_ma_tree_learning_percent, -1, - "[modular encoding] Fraction of pixels used to learn MA trees as " - "a percentage. -1 = default, 0 = no MA and fast decode, 50 = " - "default value, 100 = all, values above 100 are also permitted. " - "Higher values use more encoder memory."); - -DEFINE_int32(photon_noise_iso, 0, - "Adds noise to the image emulating photographic film noise. " - "The higher the given number, the grainier the image will be. " - "As an example, a value of 100 gives low noise whereas a value " - "of 3200 gives a lot of noise. The default value is 0."); - -DEFINE_int32(codestream_level, 5, "The codestream level. Either `5` or `10`."); - -DEFINE_double( - distance, 1.0, - "Max. butteraugli distance, lower = higher quality.\n" - " 0.0 = mathematically lossless. Default for already-lossy input " - "(JPEG/GIF).\n" - " 1.0 = visually lossless. Default for other input.\n" - " Recommended range: 0.5 .. 3.0. Mutually exclusive with --quality."); - -DEFINE_double( - quality, 100.0, - "Quality setting (is remapped to --distance). Range: -inf .. 100.\n" - " 100 = mathematically lossless. Default for already-lossy input " - "(JPEG/GIF).\n" - " Other input gets encoded as per --distance default.\n" - " Positive quality values roughly match libjpeg quality.\n" - " Mutually exclusive with --distance."); - -DEFINE_int64(effort, 3, - "Encoder effort setting. Range: 1 .. 9.\n" - " Higher number is more effort (slower)."); - -DEFINE_int32(brotli_effort, 9, - "Brotli effort setting. Range: 0 .. 11.\n" - " Default: 9. Higher number is more effort (slower)."); - -DEFINE_string(frame_indexing, "", - // TODO(tfish): Add a more convenient vanilla alternative. - "If non-empty, a string matching '^[01]*$'. If this string has a " - "'1' in i-th position, then the i-th frame will be indexed in " - "the frame index box."); - -namespace { -/** - * Writes bytes to file. - */ -bool WriteFile(const std::vector<uint8_t>& bytes, const char* filename) { - FILE* file = fopen(filename, "wb"); - if (!file) { - std::cerr << "Could not open file: " << filename << " for writing" - << std::endl - << "Error: " << strerror(errno) << std::endl; - return false; - } - if (fwrite(bytes.data(), sizeof(uint8_t), bytes.size(), file) != - bytes.size()) { - std::cerr << "Could not write bytes to file: " << filename << std::endl - << "Error: " << strerror(errno) << std::endl; - return false; - } - if (fclose(file) != 0) { - std::cerr << "Could not close file: " << filename << std::endl - << "Error: " << strerror(errno) << std::endl; - return false; - } - return true; -} - -void SetFlagFrameOptionOrDie(const char* flag_name, int32_t flag_value, - JxlEncoderFrameSettings* frame_settings, - JxlEncoderFrameSettingId encoder_option) { - if (JXL_ENC_SUCCESS != JxlEncoderFrameSettingsSetOption( - frame_settings, encoder_option, flag_value)) { - std::cerr << "Setting encoder option from flag -- " << flag_name - << "failed." << std::endl; - exit(EXIT_FAILURE); - } -} - -void SetDistanceFromFlags(JxlEncoderFrameSettings* jxl_encoder_frame_settings, - const jxl::extras::Codec& codec) { - bool distance_set = - !gflags::GetCommandLineFlagInfoOrDie("distance").is_default; - bool quality_set = !gflags::GetCommandLineFlagInfoOrDie("quality").is_default; - - if (distance_set && quality_set) { - std::cerr << "Must not set both --distance and --quality." << std::endl; - exit(EXIT_FAILURE); - } - if (distance_set) { - if (JXL_ENC_SUCCESS != JxlEncoderSetFrameDistance( - jxl_encoder_frame_settings, FLAGS_distance)) { - std::cerr << "Setting --distance parameter failed." << std::endl; - exit(EXIT_FAILURE); - } - return; - } - if (quality_set) { - double distance = FLAGS_quality >= 100 ? 0.0 - : FLAGS_quality >= 30 - ? 0.1 + (100 - FLAGS_quality) * 0.09 - : 6.4 + pow(2.5, (30 - FLAGS_quality) / 5.0) / 6.25; - if (JXL_ENC_SUCCESS != - JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, distance)) { - std::cerr << "Setting --quality parameter failed." << std::endl; - exit(EXIT_FAILURE); - } - return; - } - // No flag set, but input is JPG or GIF: Use distance 0 default. - if (codec == jxl::extras::Codec::kJPG || codec == jxl::extras::Codec::kGIF) { - if (JXL_ENC_SUCCESS == - JxlEncoderSetFrameDistance(jxl_encoder_frame_settings, 0.0)) { - std::cerr << "Setting 'lossless' default for GIF or JPEG input." - << std::endl; - } - } -} - -typedef std::function<std::string(int32_t)> flag_check_fn; - -bool IsJPG(const jxl::PaddedBytes& image_data) { - return (image_data.size() >= 2 && image_data[0] == 0xFF && - image_data[1] == 0xD8); -} - -void SetCodestreamLevel(JxlEncoder* jxl_encoder, bool for_lossless_jpeg) { - bool flag_set = - !gflags::GetCommandLineFlagInfoOrDie("codestream_level").is_default; - int32_t codestream_level = FLAGS_codestream_level; - auto set_codestream_level = [&jxl_encoder, &codestream_level]() { - if (JXL_ENC_SUCCESS != - JxlEncoderSetCodestreamLevel(jxl_encoder, codestream_level)) { - std::cerr << "Setting --codestream_level failed." << std::endl; - exit(EXIT_FAILURE); - } - }; - if (for_lossless_jpeg) { - if (!flag_set) { - set_codestream_level(); - } - } else { - if (!flag_set) { - codestream_level = static_cast<int32_t>( - JxlEncoderGetRequiredCodestreamLevel(jxl_encoder)); - if (codestream_level == -1) { - std::cerr << "No codestream_level supports the given image parameters." - << std::endl; - exit(EXIT_FAILURE); - } - } - set_codestream_level(); - } -} - -// TODO(tfish): Replace with non-C-API library function. -// Implementation is in extras/. -jxl::Status GetPixeldata(const jxl::PaddedBytes& image_data, - jxl::extras::PackedPixelFile& ppf, - jxl::extras::Codec& codec) { - // Any valid encoding is larger (ensures codecs can read the first few bytes). - constexpr size_t kMinBytes = 9; - - if (image_data.size() < kMinBytes) return JXL_FAILURE("Input too small."); - jxl::Span<const uint8_t> encoded(image_data); - - ppf.info.orientation = JXL_ORIENT_IDENTITY; - jxl::extras::ColorHints color_hints; - jxl::SizeConstraints size_constraints; - -#if JPEGXL_ENABLE_APNG - if (jxl::extras::DecodeImageAPNG(encoded, color_hints, size_constraints, - &ppf)) { - codec = jxl::extras::Codec::kPNG; - } else -#endif - if (jxl::extras::DecodeImagePGX(encoded, color_hints, size_constraints, - &ppf)) { - codec = jxl::extras::Codec::kPGX; - } else if (jxl::extras::DecodeImagePNM(encoded, color_hints, size_constraints, - &ppf)) { - codec = jxl::extras::Codec::kPNM; - } -#if JPEGXL_ENABLE_GIF - else if (jxl::extras::DecodeImageGIF(encoded, color_hints, size_constraints, - &ppf)) { - codec = jxl::extras::Codec::kGIF; - } -#endif -#if JPEGXL_ENABLE_JPEG - else if (jxl::extras::DecodeImageJPG(encoded, color_hints, size_constraints, - &ppf)) { - codec = jxl::extras::Codec::kJPG; - } -#endif - else { // TODO(tfish): Bring back EXR and PSD. - return JXL_FAILURE("Codecs failed to decode input."); - } - // TODO(tfish): Migrate this: - // if (!skip_ppf_conversion) { - // JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); - // } - return true; -} - -void set_usage_message_and_version(const char* argv0) { - gflags::SetUsageMessage( - "JPEG XL-encodes an image.\n" - " Input format can be one of: " -#if JPEGXL_ENABLE_APNG - "PNG, APNG, " -#endif -#if JPEGXL_ENABLE_GIF - "GIF, " -#endif -#if JPEGXL_ENABLE_JPEG - "JPEG, " -#endif - "PPM, PFM, PGX.\n Sample usage:\n" + - std::string(argv0) + " <source_image_filename> <target_image_filename>"); - uint32_t version = JxlEncoderVersion(); - - gflags::SetVersionString(std::to_string(version / 1000000) + "." + - std::to_string((version / 1000) % 1000) + "." + - std::to_string(version % 1000)); -} - -} // namespace - -int main(int argc, char** argv) { - std::cerr << "Warning: This is work in progress, consider using cjxl instead!" - << std::endl; - set_usage_message_and_version(argv[0]); - // TODO(firsching): rethink --help handling - gflags::ParseCommandLineNonHelpFlags(&argc, &argv, /*remove_flags=*/true); - if (FLAGS_help) { - FLAGS_help = false; - FLAGS_helpshort = true; - } - gflags::HandleCommandLineHelpFlags(); - - if (argc != 3) { - FLAGS_help = false; - FLAGS_helpshort = true; - gflags::HandleCommandLineHelpFlags(); - return EXIT_FAILURE; - } - const char* filename_in = argv[1]; - const char* filename_out = argv[2]; - - // Loading the input. - // Depending on flags-settings, we want to either load a JPEG and - // faithfully convert it to JPEG XL, or load (JPEG or non-JPEG) - // pixel data. For benchmarking, we want to be able to do - // N repetitions of image-compression, but the input should - // not get reloaded as part of that. - // Since we do not want to load the input before we decided that - // flag-settings are valid, we need a mechanism to lazy-load the image. - bool input_image_loaded = false; - jxl::PaddedBytes image_data; - jxl::extras::PackedPixelFile ppf; - jxl::extras::Codec codec = jxl::extras::Codec::kUnknown; - auto ensure_image_loaded = [&filename_in, &input_image_loaded, &image_data, - &ppf, &codec]() { - if (input_image_loaded) return; - if (!ReadFile(filename_in, &image_data)) { - std::cerr << "Reading image data failed." << std::endl; - exit(EXIT_FAILURE); - } - if (!(FLAGS_lossless_jpeg && IsJPG(image_data))) { - jxl::Status status = GetPixeldata(image_data, ppf, codec); - if (!status) { - std::cerr << "Getting pixel data." << std::endl; - exit(EXIT_FAILURE); - } - if (ppf.frames.size() < 1) { - std::cerr << "No frames on input file." << std::endl; - exit(EXIT_FAILURE); - } - } - input_image_loaded = true; - }; - - JxlEncoderPtr enc = JxlEncoderMake(/*memory_manager=*/nullptr); - JxlEncoder* jxl_encoder = enc.get(); - JxlThreadParallelRunnerPtr runner; - for (int num_rep = 0; num_rep < FLAGS_num_reps; ++num_rep) { - JxlEncoderReset(jxl_encoder); - if (FLAGS_num_threads != 0) { - size_t num_worker_threads = - JxlThreadParallelRunnerDefaultNumWorkerThreads(); - { - int64_t flag_num_worker_threads = FLAGS_num_threads; - if (flag_num_worker_threads != -1) { - num_worker_threads = flag_num_worker_threads; - } - } - if (runner == nullptr) { - runner = JxlThreadParallelRunnerMake( - /*memory_manager=*/nullptr, num_worker_threads); - } - if (JXL_ENC_SUCCESS != - JxlEncoderSetParallelRunner(jxl_encoder, JxlThreadParallelRunner, - runner.get())) { - std::cerr << "JxlEncoderSetParallelRunner failed." << std::endl; - return EXIT_FAILURE; - } - } - - JxlEncoderFrameSettings* jxl_encoder_frame_settings = - JxlEncoderFrameSettingsCreate(jxl_encoder, nullptr); - - auto process_flag = [&jxl_encoder_frame_settings]( - const char* flag_name, int32_t flag_value, - JxlEncoderFrameSettingId encoder_option, - flag_check_fn flag_check) { - gflags::CommandLineFlagInfo flag_info = - gflags::GetCommandLineFlagInfoOrDie(flag_name); - if (!flag_info.is_default) { - std::string error = flag_check(flag_value); - if (!error.empty()) { - std::cerr << "Invalid flag value for --" << flag_name << ": " << error - << std::endl; - exit(EXIT_FAILURE); - } - SetFlagFrameOptionOrDie(flag_name, flag_value, - jxl_encoder_frame_settings, encoder_option); - } - }; - - auto process_bool_flag = [&process_flag]( - const char* flag_name, int32_t flag_value, - JxlEncoderFrameSettingId encoder_option) { - process_flag(flag_name, static_cast<int32_t>(flag_value), encoder_option, - [](int32_t x) { return ""; }); - }; - - { // Processing tuning flags. - bool use_container = FLAGS_container; - // TODO(tfish): Set use_container according to need of encoded data. - // This will likely require moving this piece out of flags-processing. - if (FLAGS_strip) { - use_container = false; - } - JxlEncoderUseContainer(jxl_encoder, use_container); - - process_bool_flag("modular", FLAGS_modular, - JXL_ENC_FRAME_SETTING_MODULAR); - process_bool_flag("keep_invisible", FLAGS_keep_invisible, - JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE); - process_bool_flag("dots", FLAGS_dots, JXL_ENC_FRAME_SETTING_DOTS); - process_bool_flag("patches", FLAGS_patches, - JXL_ENC_FRAME_SETTING_PATCHES); - process_bool_flag("gaborish", FLAGS_gaborish, - JXL_ENC_FRAME_SETTING_GABORISH); - process_bool_flag("group_order", FLAGS_group_order, - JXL_ENC_FRAME_SETTING_GROUP_ORDER); - - if (!FLAGS_frame_indexing.empty()) { - bool must_be_all_zeros = FLAGS_frame_indexing[0] != '1'; - for (char c : FLAGS_frame_indexing) { - if (c == '1') { - if (must_be_all_zeros) { - std::cerr - << "Invalid --frame_indexing. If the first character is " - "'0', all must be '0'." - << std::endl; - return EXIT_FAILURE; - } - } else if (c != '0') { - std::cerr << "Invalid --frame_indexing. Must match the pattern " - "'^(0*|1[01]*)$'." - << std::endl; - return EXIT_FAILURE; - } - } - } - - process_flag( - "effort", FLAGS_effort, JXL_ENC_FRAME_SETTING_EFFORT, - [](int32_t x) -> std::string { - return (1 <= x && x <= 9) ? "" : "Valid range is {1, 2, ..., 9}."; - }); - process_flag( - "brotli_effort", FLAGS_brotli_effort, - JXL_ENC_FRAME_SETTING_BROTLI_EFFORT, [](int32_t x) -> std::string { - return (-1 <= x && x <= 11) ? "" - : "Valid range is {-1, 0, 1, ..., 11}."; - }); - process_flag("epf", FLAGS_epf, JXL_ENC_FRAME_SETTING_EPF, - [](int32_t x) -> std::string { - return (-1 <= x && x <= 3) - ? "" - : "Valid range is {-1, 0, 1, 2, 3}.\n"; - }); - process_flag( - "faster_decoding", FLAGS_faster_decoding, - JXL_ENC_FRAME_SETTING_DECODING_SPEED, [](int32_t x) -> std::string { - return (0 <= x && x <= 4) ? "" - : "Valid range is {0, 1, 2, 3, 4}.\n"; - }); - process_flag("resampling", FLAGS_resampling, - JXL_ENC_FRAME_SETTING_RESAMPLING, - [](int32_t x) -> std::string { - return (x == -1 || x == 1 || x == 4 || x == 8) - ? "" - : "Valid values are {-1, 1, 2, 4, 8}.\n"; - }); - process_flag("ec_resampling", FLAGS_ec_resampling, - JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, - [](int32_t x) -> std::string { - return (x == -1 || x == 1 || x == 4 || x == 8) - ? "" - : "Valid values are {-1, 1, 2, 4, 8}.\n"; - }); - process_flag("photon_noise_iso", FLAGS_photon_noise_iso, - JXL_ENC_FRAME_SETTING_PHOTON_NOISE, - [](int32_t x) -> std::string { - return x >= 0 ? "" : "Must be >= 0."; - }); - process_bool_flag("already_downsampled", FLAGS_already_downsampled, - JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED); - SetDistanceFromFlags(jxl_encoder_frame_settings, codec); - - if (!FLAGS_group_order && - (FLAGS_center_x != -1 || FLAGS_center_y != -1)) { - std::cerr - << "Invalid flag combination. Setting --center_x or --center_y " - << "requires setting --group_order=1" << std::endl; - return EXIT_FAILURE; - } - process_flag("center_x", FLAGS_center_x, - JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X, - [](int32_t x) -> std::string { - if (x < -1) { - return "Valid values are: -1 or [0 .. xsize)."; - } - return ""; - }); - process_flag("center_y", FLAGS_center_y, - JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y, - [](int32_t x) -> std::string { - if (x < -1) { - return "Valid values are: -1 or [0 .. ysize)."; - } - return ""; - }); - } - { // Progressive/responsive mode settings. - bool qprogressive_ac_set = - !gflags::GetCommandLineFlagInfoOrDie("qprogressive_ac").is_default; - int32_t qprogressive_ac = FLAGS_qprogressive_ac ? 1 : 0; - bool responsive_set = - !gflags::GetCommandLineFlagInfoOrDie("responsive").is_default; - int32_t responsive = FLAGS_responsive ? 1 : 0; - - process_flag( - "progressive_dc", FLAGS_progressive_dc, - JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, [](int32_t x) -> std::string { - return (-1 <= x && x <= 2) ? "" : "Valid range is {-1, 0, 1, 2}.\n"; - }); - process_bool_flag("progressive_ac", FLAGS_progressive_ac, - JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC); - - if (FLAGS_progressive) { - qprogressive_ac = 1; - qprogressive_ac_set = true; - responsive = 1; - responsive_set = true; - } - if (responsive_set) { - SetFlagFrameOptionOrDie("responsive", responsive, - jxl_encoder_frame_settings, - JXL_ENC_FRAME_SETTING_RESPONSIVE); - } - if (qprogressive_ac_set) { - SetFlagFrameOptionOrDie("qprogressive_ac", qprogressive_ac, - jxl_encoder_frame_settings, - JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC); - } - } - { // Modular mode related. - process_flag("modular_group_size", FLAGS_modular_group_size, - JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, - [](int32_t x) -> std::string { - return (-1 <= x && x <= 3) - ? "" - : "Invalid --modular_group_size. Valid " - "range is {-1, 0, 1, 2, 3}.\n"; - }); - process_flag("modular_predictor", FLAGS_modular_predictor, - JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, - [](int32_t x) -> std::string { - return (0 <= x && x <= 15) - ? "" - : "Invalid --modular_predictor. Valid " - "range is {-1, 0, 1, ..., 15}.\n"; - }); - process_flag( - "modular_colorspace", FLAGS_modular_colorspace, - JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, - [](int32_t x) -> std::string { - return (0 <= x && x <= 15) - ? "" - : "Invalid --modular_colorspace. Valid range is " - "{-1, 0, 1, ..., 37}.\n"; - }); - process_flag("modular_ma_tree_learning_percent", - FLAGS_modular_ma_tree_learning_percent, - JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, - [](int32_t x) -> std::string { - return (-1 <= x && x <= 100) - ? "" - : "Invalid --modular_ma_tree_learning_percent. " - "Valid range is {-1, 0, 1, ..., 100}.\n"; - }); - process_flag("modular_nb_prev_channels", FLAGS_modular_nb_prev_channels, - JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, - [](int32_t x) -> std::string { - return (-1 <= x && x <= 11) - ? "" - : "Invalid --modular_nb_prev_channels. Valid " - "range is {-1, 0, 1, ..., 11}.\n"; - }); - process_bool_flag("modular_lossy_palette", FLAGS_modular_lossy_palette, - JXL_ENC_FRAME_SETTING_LOSSY_PALETTE); - process_flag("modular_palette_colors", FLAGS_modular_palette_colors, - JXL_ENC_FRAME_SETTING_PALETTE_COLORS, - [](int32_t x) -> std::string { return ""; }); - process_flag( - "modular_channel_colors_global_percent", - FLAGS_modular_channel_colors_global_percent, - JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, - [](int32_t x) -> std::string { - return (-1 <= x && x <= 100) - ? "" - : "Invalid --modular_channel_colors_global_percent. " - "Valid " - "range is {-1, 0, 1, ..., 100}.\n"; - }); - process_flag( - "modular_channel_colors_group_percent", - FLAGS_modular_channel_colors_group_percent, - JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, - [](int32_t x) -> std::string { - return (-1 <= x && x <= 100) - ? "" - : "Invalid --modular_channel_colors_group_percent. " - "Valid " - "range is {-1, 0, 1, ..., 100}.\n"; - }); - } - ensure_image_loaded(); - if (FLAGS_lossless_jpeg && IsJPG(image_data)) { - if (gflags::GetCommandLineFlagInfoOrDie("lossless_jpeg").is_default) { - std::cerr << "Note: Implicit-default for JPEG is lossless-transcoding. " - << "To silence this message, set --lossless_jpeg=(1|0)." - << std::endl; - } - if (FLAGS_jpeg_store_metadata) { - if (JXL_ENC_SUCCESS != JxlEncoderStoreJPEGMetadata(jxl_encoder, true)) { - std::cerr << "Storing JPEG metadata failed. " << std::endl; - return EXIT_FAILURE; - } - } - process_bool_flag("jpeg_reconstruction_cfl", - FLAGS_jpeg_reconstruction_cfl, - JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL); - SetCodestreamLevel(jxl_encoder, /*for_lossless_jpeg=*/true); - if (JXL_ENC_SUCCESS != JxlEncoderAddJPEGFrame(jxl_encoder_frame_settings, - image_data.data(), - image_data.size())) { - std::cerr << "JxlEncoderAddJPEGFrame() failed." << std::endl; - return EXIT_FAILURE; - } - } else { // Do JxlEncoderAddImageFrame(). - size_t num_alpha_channels = 0; // Adjusted below. - { - JxlBasicInfo basic_info = ppf.info; - if (basic_info.alpha_bits > 0) num_alpha_channels = 1; - basic_info.intensity_target = - static_cast<float>(FLAGS_intensity_target); - basic_info.num_extra_channels = num_alpha_channels; - basic_info.num_color_channels = ppf.info.num_color_channels; - basic_info.uses_original_profile = JXL_FALSE; - if (JXL_ENC_SUCCESS != - JxlEncoderSetBasicInfo(jxl_encoder, &basic_info)) { - std::cerr << "JxlEncoderSetBasicInfo() failed." << std::endl; - return EXIT_FAILURE; - } - SetCodestreamLevel(jxl_encoder, /*for_lossless_jpeg=*/false); - } - - if (!ppf.icc.empty()) { - JxlEncoderSetICCProfile(jxl_encoder, ppf.icc.data(), ppf.icc.size()); - if (JXL_ENC_SUCCESS != JxlEncoderSetICCProfile(jxl_encoder, - ppf.icc.data(), - ppf.icc.size())) { - std::cerr << "JxlEncoderSetICCProfile() failed." << std::endl; - return EXIT_FAILURE; - } - } else { - if (JXL_ENC_SUCCESS != - JxlEncoderSetColorEncoding(jxl_encoder, &ppf.color_encoding)) { - std::cerr << "JxlEncoderSetColorEncoding() failed." << std::endl; - return EXIT_FAILURE; - } - } - - for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) { - const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame]; - const jxl::extras::PackedImage& pimage = pframe.color; - JxlPixelFormat ppixelformat = pimage.format; - { - if (JXL_ENC_SUCCESS != - JxlEncoderSetFrameHeader(jxl_encoder_frame_settings, - &pframe.frame_info)) { - std::cerr << "JxlEncoderSetFrameHeader() failed." << std::endl; - return EXIT_FAILURE; - } - } - if (num_frame < FLAGS_frame_indexing.size() && - FLAGS_frame_indexing[num_frame] == '1') { - if (JXL_ENC_SUCCESS != - JxlEncoderFrameSettingsSetOption(jxl_encoder_frame_settings, - JXL_ENC_FRAME_INDEX_BOX, 1)) { - std::cerr << "Setting option JXL_ENC_FRAME_INDEX_BOX failed." - << std::endl; - return EXIT_FAILURE; - } - } - jxl::Status enc_status(true); - { - if (num_alpha_channels > 0) { - JxlExtraChannelInfo extra_channel_info; - JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, - &extra_channel_info); - enc_status = JxlEncoderSetExtraChannelInfo(jxl_encoder, 0, - &extra_channel_info); - if (JXL_ENC_SUCCESS != enc_status) { - std::cerr << "JxlEncoderSetExtraChannelInfo() failed." - << std::endl; - return EXIT_FAILURE; - } - if (FLAGS_premultiply != -1) { - if (!(FLAGS_premultiply == 0 || FLAGS_premultiply == 1)) { - std::cerr << "Flag --premultiply must be one of: -1, 0, 1." - << std::endl; - return EXIT_FAILURE; - } - extra_channel_info.alpha_premultiplied = FLAGS_premultiply; - } - // We take the extra channel blend info frame_info, but don't do - // clamping. - JxlBlendInfo extra_channel_blend_info = - pframe.frame_info.layer_info.blend_info; - extra_channel_blend_info.clamp = JXL_FALSE; - JxlEncoderSetExtraChannelBlendInfo(jxl_encoder_frame_settings, 0, - &extra_channel_blend_info); - } - enc_status = - JxlEncoderAddImageFrame(jxl_encoder_frame_settings, &ppixelformat, - pimage.pixels(), pimage.pixels_size); - if (JXL_ENC_SUCCESS != enc_status) { - std::cerr << "JxlEncoderAddImageFrame() failed." << std::endl; - return EXIT_FAILURE; - } - // Only set extra channel buffer if is is provided non-interleaved. - if (!pframe.extra_channels.empty()) { - enc_status = JxlEncoderSetExtraChannelBuffer( - jxl_encoder_frame_settings, &ppixelformat, - pframe.extra_channels[0].pixels(), - pframe.extra_channels[0].stride * - pframe.extra_channels[0].ysize, - 0); - if (JXL_ENC_SUCCESS != enc_status) { - std::cerr << "JxlEncoderSetExtraChannelBuffer() failed." - << std::endl; - return EXIT_FAILURE; - } - } - } - } - } - JxlEncoderCloseInput(jxl_encoder); - } - // Reading compressed output - std::vector<uint8_t> compressed; - compressed.resize(4096); - uint8_t* next_out = compressed.data(); - size_t avail_out = compressed.size() - (next_out - compressed.data()); - JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; - while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { - process_result = - JxlEncoderProcessOutput(jxl_encoder, &next_out, &avail_out); - if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { - size_t offset = next_out - compressed.data(); - compressed.resize(compressed.size() * 2); - next_out = compressed.data() + offset; - avail_out = compressed.size() - offset; - } - } - compressed.resize(next_out - compressed.data()); - if (JXL_ENC_SUCCESS != process_result) { - std::cerr << "JxlEncoderProcessOutput failed." << std::endl; - return EXIT_FAILURE; - } - - // TODO(firsching): print info about compressed size and other image stats - // here and in the beginning, like is done in current cjxl. - if (!WriteFile(compressed, filename_out)) { - std::cerr << "Could not write jxl file." << std::endl; - return EXIT_FAILURE; - } - return EXIT_SUCCESS; -} diff --git a/media/libjxl/src/tools/cmdline.h b/media/libjxl/src/tools/cmdline.h index ed5d847050..9b730e67e3 100644 --- a/media/libjxl/src/tools/cmdline.h +++ b/media/libjxl/src/tools/cmdline.h @@ -9,12 +9,11 @@ #include <stdio.h> #include <string.h> +#include <cstdint> #include <memory> #include <string> #include <vector> -#include "lib/jxl/base/status.h" - namespace jpegxl { namespace tools { @@ -97,7 +96,6 @@ class CommandLineParser { } const CmdOptionInterface* GetOption(OptionId id) const { - JXL_ASSERT(id < options_.size()); return options_[id].get(); } @@ -316,6 +314,82 @@ class CommandLineParser { bool help_ = false; }; +// +// Common parsers for AddOptionValue and AddOptionFlag +// + +static inline bool ParseSigned(const char* arg, int* out) { + char* end; + *out = static_cast<int>(strtol(arg, &end, 0)); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as signed integer: %s.\n", arg); + return false; + } + return true; +} + +static inline bool ParseUnsigned(const char* arg, size_t* out) { + char* end; + *out = static_cast<size_t>(strtoull(arg, &end, 0)); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as unsigned integer: %s.\n", arg); + return false; + } + return true; +} + +static inline bool ParseInt64(const char* arg, int64_t* out) { + char* end; + *out = strtol(arg, &end, 0); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as signed integer: %s.\n", arg); + return false; + } + return true; +} + +static inline bool ParseUint32(const char* arg, uint32_t* out) { + size_t value = 0; + bool ret = ParseUnsigned(arg, &value); + if (ret) *out = value; + return ret; +} + +static inline bool ParseFloat(const char* arg, float* out) { + char* end; + *out = static_cast<float>(strtod(arg, &end)); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as float: %s.\n", arg); + return false; + } + return true; +} + +static inline bool ParseDouble(const char* arg, double* out) { + char* end; + *out = static_cast<double>(strtod(arg, &end)); + if (end[0] != '\0') { + fprintf(stderr, "Unable to interpret as double: %s.\n", arg); + return false; + } + return true; +} + +static inline bool ParseString(const char* arg, std::string* out) { + out->assign(arg); + return true; +} + +static inline bool SetBooleanTrue(bool* out) { + *out = true; + return true; +} + +static inline bool SetBooleanFalse(bool* out) { + *out = false; + return true; +} + } // namespace tools } // namespace jpegxl diff --git a/media/libjxl/src/tools/codec_config.cc b/media/libjxl/src/tools/codec_config.cc index 9ed64a23c8..8efc26c221 100644 --- a/media/libjxl/src/tools/codec_config.cc +++ b/media/libjxl/src/tools/codec_config.cc @@ -7,7 +7,6 @@ #include <hwy/targets.h> -#include "lib/jxl/base/status.h" #include "tools/tool_version.h" namespace jpegxl { @@ -45,8 +44,9 @@ std::string CodecConfigString(uint32_t lib_version) { config += ','; saw_target = true; } - JXL_ASSERT(saw_target); - (void)saw_target; + if (!saw_target) { + config += "no targets found,"; + } config.resize(config.size() - 1); // remove trailing comma config += "]"; diff --git a/media/libjxl/src/tools/codec_config.h b/media/libjxl/src/tools/codec_config.h index 729c96d4a8..a4f79a66b8 100644 --- a/media/libjxl/src/tools/codec_config.h +++ b/media/libjxl/src/tools/codec_config.h @@ -6,6 +6,7 @@ #ifndef TOOLS_CODEC_CONFIG_H_ #define TOOLS_CODEC_CONFIG_H_ +#include <stdint.h> #include <string> namespace jpegxl { diff --git a/media/libjxl/src/tools/conformance/CMakeLists.txt b/media/libjxl/src/tools/conformance/CMakeLists.txt index 9bc7e317f7..5766612abf 100644 --- a/media/libjxl/src/tools/conformance/CMakeLists.txt +++ b/media/libjxl/src/tools/conformance/CMakeLists.txt @@ -3,9 +3,6 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -add_executable(djxl_conformance djxl_conformance.cc) -target_link_libraries(djxl_conformance jxl_dec) - if(BUILD_TESTING AND CMAKE_EXECUTABLE_SUFFIX STREQUAL "") # Script to validate the tooling. find_program (BASH_PROGRAM bash) diff --git a/media/libjxl/src/tools/conformance/conformance.py b/media/libjxl/src/tools/conformance/conformance.py index b012716d2e..15158bcc37 100755 --- a/media/libjxl/src/tools/conformance/conformance.py +++ b/media/libjxl/src/tools/conformance/conformance.py @@ -18,16 +18,14 @@ import tempfile import lcms2 +def Failure(message): + print(f"\033[91m{message}\033[0m", flush=True) + return False -class ConformanceTestError(Exception): - """General conformance test error.""" - - -def CompareNPY(ref, ref_icc, dec, dec_icc, frame_idx, rmse, peak_error): +def CompareNPY(ref, ref_icc, dec, dec_icc, frame_idx, rmse_limit, peak_error): """Compare a decoded numpy against the reference one.""" if ref.shape != dec.shape: - raise ConformanceTestError( - f'Expected shape {ref.shape} but found {dec.shape}') + return Failure(f'Expected shape {ref.shape} but found {dec.shape}') ref_frame = ref[frame_idx] dec_frame = dec[frame_idx] num_channels = ref_frame.shape[2] @@ -35,23 +33,25 @@ def CompareNPY(ref, ref_icc, dec, dec_icc, frame_idx, rmse, peak_error): if ref_icc != dec_icc: # Transform colors before comparison. if num_channels < 3: - raise ConformanceTestError(f"Only RGB images are supported") - ref_clr = ref_frame[:, :, 0:3] + return Failure(f"Only RGB images are supported") dec_clr = dec_frame[:, :, 0:3] dec_frame[:, :, 0:3] = lcms2.convert_pixels(dec_icc, ref_icc, dec_clr) error = numpy.abs(ref_frame - dec_frame) - for ch in range(num_channels): - error_ch = error[:, :, ch] - actual_rmse = numpy.sqrt(numpy.mean(error_ch * error_ch)) - if actual_rmse > rmse: - raise ConformanceTestError( - f"RMSE too large: {actual_rmse} > {rmse}") - actual_peak_error = error.max() + error_by_channel = [error[:, :, ch] for ch in range(num_channels)] + actual_rmses = [numpy.sqrt(numpy.mean(error_ch * error_ch)) for error_ch in error_by_channel] + actual_rmse = max(actual_rmses) + + print(f"RMSE: {actual_rmses}, peak error: {actual_peak_error}", flush=True) + + if actual_rmse > rmse_limit: + return Failure(f"RMSE too large: {actual_rmse} > {rmse_limit}") + if actual_peak_error > peak_error: - raise ConformanceTestError( + return Failure( f"Peak error too large: {actual_peak_error} > {peak_error}") + return True def CompareBinaries(ref_bin, dec_bin): @@ -63,8 +63,9 @@ def CompareBinaries(ref_bin, dec_bin): dec_data = decf.read() if ref_data != dec_data: - raise ConformanceTestError( + return Failure( f'Binary files mismatch: {ref_bin} {dec_bin}') + return True TEST_KEYS = set( @@ -74,31 +75,33 @@ TEST_KEYS = set( def CheckMeta(dec, ref): if isinstance(ref, dict): if not isinstance(dec, dict): - raise ConformanceTestError("Malformed metadata file") + return Failure("Malformed metadata file") for k, v in ref.items(): if k in TEST_KEYS: continue if k not in dec: - raise ConformanceTestError( + return Failure( f"Malformed metadata file: key {k} not found") vv = dec[k] - CheckMeta(vv, v) + return CheckMeta(vv, v) elif isinstance(ref, list): if not isinstance(dec, list) or len(dec) != len(ref): - raise ConformanceTestError("Malformed metadata file") + return Failure("Malformed metadata file") for vv, v in zip(dec, ref): - CheckMeta(vv, v) + return CheckMeta(vv, v) elif isinstance(ref, float): if not isinstance(dec, float): - raise ConformanceTestError("Malformed metadata file") + return Failure("Malformed metadata file") if abs(dec - ref) > 0.0001: - raise ConformanceTestError( + return Failure( f"Metadata: Expected {ref}, found {dec}") elif dec != ref: - raise ConformanceTestError(f"Metadata: Expected {ref}, found {dec}") + return Failure(f"Metadata: Expected {ref}, found {dec}") + return True def ConformanceTestRunner(args): + ok = True # We can pass either the .txt file or the directory which defaults to the # full corpus. This is useful to run a subset of the corpus in other .txt # files. @@ -112,7 +115,7 @@ def ConformanceTestRunner(args): with open(corpus_txt, 'r') as f: for test_id in f: test_id = test_id.rstrip('\n') - print('Testing %s' % test_id) + print(f"\033[94m\033[1mTesting {test_id}\033[0m", flush=True) test_dir = os.path.join(corpus_dir, test_id) with open(os.path.join(test_dir, 'test.json'), 'r') as f: @@ -123,38 +126,53 @@ def ConformanceTestRunner(args): exact_tests = [] with tempfile.TemporaryDirectory(prefix=test_id) as work_dir: - cmd = [args.decoder, os.path.join(test_dir, 'input.jxl')] - # Select the parameters to run. + input_filename = os.path.join(test_dir, 'input.jxl') pixel_prefix = os.path.join(work_dir, 'decoded') - cmd.extend(['-p', pixel_prefix]) + output_filename = pixel_prefix + '_image.npy' + cmd = [args.decoder, input_filename, output_filename] + cmd_jpeg = [] + if 'preview' in descriptor: + preview_filename = os.path.join(work_dir, + 'decoded_preview.npy') + cmd.extend(['--preview_out', preview_filename]) if 'reconstructed_jpeg' in descriptor: jpeg_filename = os.path.join(work_dir, 'reconstructed.jpg') - cmd.extend(['-j', jpeg_filename]) + cmd_jpeg = [args.decoder, input_filename, jpeg_filename] exact_tests.append(('reconstructed.jpg', jpeg_filename)) if 'original_icc' in descriptor: decoded_original_icc = os.path.join( work_dir, 'decoded_org.icc') - cmd.extend(['-i', decoded_original_icc]) + cmd.extend(['--orig_icc_out', decoded_original_icc]) exact_tests.append(('original.icc', decoded_original_icc)) meta_filename = os.path.join(work_dir, 'meta.json') - cmd.extend(['-m', meta_filename]) + cmd.extend(['--metadata_out', meta_filename]) + cmd.extend(['--icc_out', pixel_prefix + '.icc']) + cmd.extend(['--norender_spotcolors']) + print(f"Running: {cmd}", flush=True) if subprocess.call(cmd) != 0: - raise ConformanceTestError( - 'Running the decoder (%s) returned error' % - ' '.join(cmd)) + ok = Failure('Running the decoder (%s) returned error' % + ' '.join(cmd)) + continue + if cmd_jpeg: + print(f"Running: {cmd_jpeg}", flush=True) + if subprocess.call(cmd_jpeg) != 0: + ok = Failure( + 'Running the decoder (%s) returned error' % + ' '.join(cmd_jpeg)) + continue # Run validation of exact files. for reference_basename, decoded_filename in exact_tests: reference_filename = os.path.join(test_dir, reference_basename) - CompareBinaries(reference_filename, decoded_filename) + ok = ok & CompareBinaries(reference_filename, decoded_filename) # Validate metadata. with open(meta_filename, 'r') as f: meta = json.load(f) - CheckMeta(meta, descriptor) + ok = ok & CheckMeta(meta, descriptor) # Pixel data. decoded_icc = pixel_prefix + '.icc' @@ -168,16 +186,16 @@ def ConformanceTestRunner(args): decoded_npy = os.path.join(work_dir, 'decoded_image.npy') if not os.path.exists(decoded_npy): - raise ConformanceTestError( - 'File not decoded: decoded_image.npy') + ok = Failure('File not decoded: decoded_image.npy') + continue reference_npy = numpy.load(reference_npy) decoded_npy = numpy.load(decoded_npy) for i, fd in enumerate(descriptor['frames']): - CompareNPY(reference_npy, reference_icc, decoded_npy, - decoded_icc, i, fd['rms_error'], - fd['peak_error']) + ok = ok & CompareNPY(reference_npy, reference_icc, decoded_npy, + decoded_icc, i, fd['rms_error'], + fd['peak_error']) if 'preview' in descriptor: reference_npy = os.path.join(test_dir, @@ -185,17 +203,17 @@ def ConformanceTestRunner(args): decoded_npy = os.path.join(work_dir, 'decoded_preview.npy') if not os.path.exists(decoded_npy): - raise ConformanceTestError( + ok = Failure( 'File not decoded: decoded_preview.npy') reference_npy = numpy.load(reference_npy) decoded_npy = numpy.load(decoded_npy) - CompareNPY(reference_npy, reference_icc, decoded_npy, - decoded_icc, 0, - descriptor['preview']['rms_error'], - descriptor['preview']['peak_error']) + ok = ok & CompareNPY(reference_npy, reference_icc, decoded_npy, + decoded_icc, 0, + descriptor['preview']['rms_error'], + descriptor['preview']['peak_error']) - return True + return ok def main(): diff --git a/media/libjxl/src/tools/conformance/djxl_conformance.cc b/media/libjxl/src/tools/conformance/djxl_conformance.cc deleted file mode 100644 index 77d0c68722..0000000000 --- a/media/libjxl/src/tools/conformance/djxl_conformance.cc +++ /dev/null @@ -1,669 +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 <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -#include <algorithm> -#include <fstream> -#include <iostream> -#include <iterator> -#include <memory> -#include <sstream> -#include <vector> - -#include "jxl/decode.h" -#include "jxl/decode_cxx.h" -#include "jxl/thread_parallel_runner.h" -#include "jxl/thread_parallel_runner_cxx.h" - -namespace { - -struct DecodeOptions { - // Path to the input .jxl file. - const char* input = nullptr; - - // Prefix of the output path where to generate the pixel data or nullptr if - // no pixel data should be save to disk. - const char* pixel_prefix = nullptr; - - // Path to the original ICC profile to be generated, if requested. - const char* icc_path = nullptr; - - // Path to JPEG reconstruction file to be generated, if requested. - const char* jpeg_path = nullptr; - - // Path to the metadata text file to be generated, if requested. - const char* metadata_path = nullptr; -}; - -bool LoadFile(const char* filename, std::vector<uint8_t>* data) { - std::ifstream ifs(filename, std::ios::binary); - std::vector<uint8_t> contents((std::istreambuf_iterator<char>(ifs)), - std::istreambuf_iterator<char>()); - ifs.close(); - *data = std::move(contents); - return ifs.good(); -} - -bool SaveFile(const char* filename, std::vector<uint8_t> data) { - std::ofstream ofs(filename, std::ios::binary); - ofs.write(reinterpret_cast<const char*>(data.data()), data.size()); - ofs.close(); - return ofs.good(); -} - -struct ImageArray { - uint32_t xsize, ysize; - // amount of color channels: 1 for grayscale, 3 for RGB - uint32_t num_color_channels; - // amount of extra channels, including alpha channels, spot colors, ... - uint32_t num_extra_channels; - - // Both frames and ec_frames are filled in by the JXL decoder, and will be - // converted into a numpy array of the form (frame, ysize, xsize, channel) - - // Array of the color channels of the frames. This is an array of frames, - // where each frame is an array of pixels. The pixels in a frame are laid - // out per scanline, then per channel, and finally individual pixels as - // little endian 32-bit floating point. - std::vector<std::vector<uint8_t>> frames; - - // Array of the extra channels of the frames. This is an array of frames, - // where each frame is an array of extra channels. The pixels in an extra - // channel are laid out per scanline, then individual pixels as - // little endian 32-bit floating point. - std::vector<std::vector<std::vector<uint8_t>>> ec_frames; -}; - -// Saves an ImageArray as a numpy 4D ndarray in binary format. -bool SaveNPYArray(const char* filename, const ImageArray& arr) { - size_t image_size = - sizeof(float) * arr.xsize * arr.ysize * arr.num_color_channels; - size_t ec_size = sizeof(float) * arr.xsize * arr.ysize; - for (const auto& frame : arr.frames) { - if (frame.size() != image_size) { - fprintf(stderr, "Invalid frame size\n"); - return false; - } - } - for (const auto& frame : arr.ec_frames) { - if (frame.size() != arr.num_extra_channels) { - fprintf(stderr, "Invalid extra channel count\n"); - return false; - } - for (const auto& ch : frame) { - if (ch.size() != ec_size) { - fprintf(stderr, "Invalid extra channel size\n"); - return false; - } - } - } - - FILE* file = fopen(filename, "wb"); - if (!file) { - fprintf(stderr, "Could not open %s for writing", filename); - return false; - } -#define WRITE_TO_FILE(ptr, len) \ - do { \ - if (fwrite((ptr), (len), 1, file) != 1) { \ - fprintf(stderr, "Error writing " #ptr " to file %s\n", filename); \ - fclose(file); \ - return false; \ - } \ - } while (0) - - const uint8_t header[] = "\x93NUMPY\x01\x00"; - WRITE_TO_FILE(header, 8); - - { - uint32_t num_channels = arr.num_color_channels + arr.num_extra_channels; - std::stringstream ss; - ss << "{'descr': '<f4', 'fortran_order': False, 'shape': (" - << arr.frames.size() << ", " << arr.ysize << ", " << arr.xsize << ", " - << num_channels << "), }\n"; - // 16-bit little endian header length. - uint8_t header_len[2] = {static_cast<uint8_t>(ss.str().size() % 256), - static_cast<uint8_t>(ss.str().size() / 256)}; - WRITE_TO_FILE(header_len, 2); - WRITE_TO_FILE(ss.str().data(), ss.str().size()); - } - - // interleave the samples from color and extra channels - for (size_t f = 0; f < arr.frames.size(); ++f) { - size_t pos = 0; - for (size_t y = 0; y < arr.ysize; ++y) { - for (size_t x = 0; x < arr.xsize; ++x, pos += sizeof(float)) { - WRITE_TO_FILE(arr.frames[f].data() + pos * arr.num_color_channels, - arr.num_color_channels * sizeof(float)); - for (size_t i = 0; i < arr.num_extra_channels; i++) { - WRITE_TO_FILE(arr.ec_frames[f][i].data() + pos, sizeof(float)); - } - } - } - } - - return fclose(file) == 0; -#undef WRITE_TO_FILE -} - -// JSON value writing - -class JSONField { - public: - virtual ~JSONField() = default; - virtual void Write(std::ostream& o, uint32_t indent) const = 0; - - protected: - JSONField() = default; -}; - -class JSONValue : public JSONField { - public: - template <typename T> - explicit JSONValue(const T& value) : value_(std::to_string(value)) {} - - explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {} - - explicit JSONValue(bool value) : value_(value ? "true" : "false") {} - - void Write(std::ostream& o, uint32_t indent) const override { o << value_; } - - private: - std::string value_; -}; - -class JSONDict : public JSONField { - public: - JSONDict() = default; - - template <typename T> - T* AddEmpty(const std::string& key) { - static_assert(std::is_convertible<T*, JSONField*>::value, - "T must be a JSONField"); - T* ret = new T(); - values_.emplace_back( - key, std::unique_ptr<JSONField>(static_cast<JSONField*>(ret))); - return ret; - } - - template <typename T> - void Add(const std::string& key, const T& value) { - values_.emplace_back(key, std::unique_ptr<JSONField>(new JSONValue(value))); - } - - void Write(std::ostream& o, uint32_t indent) const override { - std::string indent_str(indent, ' '); - o << "{"; - bool is_first = true; - for (const auto& key_value : values_) { - if (!is_first) { - o << ","; - } - is_first = false; - o << std::endl << indent_str << " \"" << key_value.first << "\": "; - key_value.second->Write(o, indent + 2); - } - if (!values_.empty()) { - o << std::endl << indent_str; - } - o << "}"; - } - - private: - // Dictionary with order. - std::vector<std::pair<std::string, std::unique_ptr<JSONField>>> values_; -}; - -class JSONArray : public JSONField { - public: - JSONArray() = default; - - template <typename T> - T* AddEmpty() { - static_assert(std::is_convertible<T*, JSONField*>::value, - "T must be a JSONField"); - T* ret = new T(); - values_.emplace_back(ret); - return ret; - } - - template <typename T> - void Add(const T& value) { - values_.emplace_back(new JSONValue(value)); - } - - void Write(std::ostream& o, uint32_t indent) const override { - std::string indent_str(indent, ' '); - o << "["; - bool is_first = true; - for (const auto& value : values_) { - if (!is_first) { - o << ","; - } - is_first = false; - o << std::endl << indent_str << " "; - value->Write(o, indent + 2); - } - if (!values_.empty()) { - o << std::endl << indent_str; - } - o << "]"; - } - - private: - std::vector<std::unique_ptr<JSONField>> values_; -}; - -#define EXPECT_TRUE(X) \ - do { \ - if (!(X)) { \ - fprintf(stderr, "Failed: %s\n", #X); \ - return false; \ - } \ - } while (false) - -// Helper macro for decoder error checking. -#define EXPECT_SUCCESS(X) EXPECT_TRUE((X) == JXL_DEC_SUCCESS) - -// TODO(veluca): merge this back in DecodeJXL once/if the API supports decoding -// to JPEG and to pixels at the same time. -bool DecodeJXLToJpeg(const char* input_path, const char* output_path) { - // JPEG output buffer when reconstructing a JPEG file. - std::vector<uint8_t> jpeg_data; - std::vector<uint8_t> jpeg_data_chunk(16 * 1024); - auto dec = JxlDecoderMake(nullptr); - - uint32_t events = JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE; - EXPECT_SUCCESS(JxlDecoderSubscribeEvents(dec.get(), events)); - - // TODO(deymo): Consider using a multi-threading decoder for conformance - // testing as well. - - // Load and set input all at oncee. - std::vector<uint8_t> jxl_input; - EXPECT_TRUE(LoadFile(input_path, &jxl_input)); - EXPECT_SUCCESS( - JxlDecoderSetInput(dec.get(), jxl_input.data(), jxl_input.size())); - - bool has_jpeg_reconstruction = false; - - while (true) { - JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); - if (status == JXL_DEC_ERROR) { - fprintf(stderr, "Error decoding.\n"); - return false; - } else if (status == JXL_DEC_NEED_MORE_INPUT) { - fprintf(stderr, "Error decoding: expected more input.\n"); - return false; - } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { - has_jpeg_reconstruction = true; - // Decoding to JPEG. - EXPECT_SUCCESS(JxlDecoderSetJPEGBuffer(dec.get(), jpeg_data_chunk.data(), - jpeg_data_chunk.size())); - } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { - // Decoded a chunk to JPEG. - size_t used_jpeg_output = - jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); - jpeg_data.insert(jpeg_data.end(), jpeg_data_chunk.data(), - jpeg_data_chunk.data() + used_jpeg_output); - if (used_jpeg_output == 0) { - // Chunk is too small. - jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2); - } - EXPECT_SUCCESS(JxlDecoderSetJPEGBuffer(dec.get(), jpeg_data_chunk.data(), - jpeg_data_chunk.size())); - } else if (status == JXL_DEC_SUCCESS) { - break; - } else if (status == JXL_DEC_FULL_IMAGE) { - } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { - return true; - } else { - fprintf(stderr, "Error: unexpected status: %d\n", - static_cast<int>(status)); - return false; - } - } - - if (has_jpeg_reconstruction) { - size_t used_jpeg_output = - jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); - jpeg_data.insert(jpeg_data.end(), jpeg_data_chunk.data(), - jpeg_data_chunk.data() + used_jpeg_output); - EXPECT_TRUE(SaveFile(output_path, jpeg_data)); - } - return true; -} - -bool DecodeJXL(const DecodeOptions& opts) { - auto dec = JxlDecoderMake(nullptr); - - uint32_t events = - JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_PREVIEW_IMAGE; - if (opts.pixel_prefix) events |= JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE; - // We need to output the frame header info in the metadata. - if (opts.metadata_path) events |= JXL_DEC_FRAME; - - if (opts.jpeg_path) { - EXPECT_TRUE(DecodeJXLToJpeg(opts.input, opts.jpeg_path)); - } - - EXPECT_SUCCESS(JxlDecoderSubscribeEvents(dec.get(), events)); - EXPECT_SUCCESS(JxlDecoderSetRenderSpotcolors(dec.get(), JXL_FALSE)); - - // TODO(deymo): Consider using a multi-threading decoder for conformance - // testing as well. - - // Load and set input all at oncee. - std::vector<uint8_t> jxl_input; - EXPECT_TRUE(LoadFile(opts.input, &jxl_input)); - EXPECT_SUCCESS( - JxlDecoderSetInput(dec.get(), jxl_input.data(), jxl_input.size())); - - JxlBasicInfo info{}; - - // Pixel data when decoding a frame or a preview frame. - std::vector<uint8_t> pixels; - std::vector<uint8_t> preview_pixels; - - std::vector<JxlExtraChannelInfo> extra_channels; - std::vector<std::vector<uint8_t>> extra_channel_pixels; - - std::vector<JxlFrameHeader> frame_headers; - std::vector<std::string> frame_names; - - JxlPixelFormat format; - - ImageArray image, preview; - - while (true) { - JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); - if (status == JXL_DEC_ERROR) { - fprintf(stderr, "Error decoding.\n"); - return false; - } else if (status == JXL_DEC_NEED_MORE_INPUT) { - fprintf(stderr, "Error decoding: expected more input.\n"); - return false; - } else if (status == JXL_DEC_BASIC_INFO) { - // Basic info. - EXPECT_SUCCESS(JxlDecoderGetBasicInfo(dec.get(), &info)); - extra_channels.resize(info.num_extra_channels); - for (uint32_t i = 0; i < info.num_extra_channels; ++i) { - EXPECT_SUCCESS( - JxlDecoderGetExtraChannelInfo(dec.get(), i, &extra_channels[i])); - std::vector<char> name(extra_channels[i].name_length + 1); - EXPECT_SUCCESS(JxlDecoderGetExtraChannelName(dec.get(), i, name.data(), - name.size())); - } - - // Select the output pixel format based on the basic info. - format = JxlPixelFormat{info.num_color_channels, JXL_TYPE_FLOAT, - JXL_LITTLE_ENDIAN, 0}; - image.num_color_channels = info.num_color_channels; - image.num_extra_channels = info.num_extra_channels; - image.xsize = info.xsize; - image.ysize = info.ysize; - - if (info.have_preview) { - preview.num_color_channels = info.num_color_channels; - preview.num_extra_channels = info.num_extra_channels; - preview.xsize = info.preview.xsize; - preview.ysize = info.preview.ysize; - } - } else if (status == JXL_DEC_COLOR_ENCODING) { - // ICC profiles. - if (opts.icc_path) { - // Store the original ICC if requested. - size_t icc_size; - EXPECT_SUCCESS(JxlDecoderGetICCProfileSize( - dec.get(), nullptr, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &icc_size)); - std::vector<uint8_t> icc_original(icc_size); - EXPECT_SUCCESS(JxlDecoderGetColorAsICCProfile( - dec.get(), nullptr, JXL_COLOR_PROFILE_TARGET_ORIGINAL, - icc_original.data(), icc_original.size())); - EXPECT_TRUE(SaveFile(opts.icc_path, icc_original)); - } - - if (opts.pixel_prefix) { - // Get the ICC color profile of the pixel data and store it. - size_t icc_size; - EXPECT_SUCCESS(JxlDecoderGetICCProfileSize( - dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)); - std::vector<uint8_t> icc_data(icc_size); - EXPECT_SUCCESS(JxlDecoderGetColorAsICCProfile( - dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, icc_data.data(), - icc_data.size())); - std::string icc_data_filename = std::string(opts.pixel_prefix) + ".icc"; - EXPECT_TRUE(SaveFile(icc_data_filename.c_str(), icc_data)); - } - } else if (status == JXL_DEC_FRAME) { - // Capture the frame header information. - JxlFrameHeader frame_header; - EXPECT_SUCCESS(JxlDecoderGetFrameHeader(dec.get(), &frame_header)); - std::vector<char> frame_name(frame_header.name_length + 1); - EXPECT_SUCCESS(JxlDecoderGetFrameName(dec.get(), frame_name.data(), - frame_name.size())); - EXPECT_TRUE(frame_name[frame_name.size() - 1] == '\0'); - frame_headers.emplace_back(frame_header); - frame_names.emplace_back(frame_name.begin(), frame_name.end() - 1); - } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { - // Set pixel output buffer. - size_t buffer_size; - EXPECT_SUCCESS( - JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)); - pixels.resize(buffer_size); - memset(pixels.data(), 0, pixels.size()); - EXPECT_SUCCESS(JxlDecoderSetImageOutBuffer(dec.get(), &format, - pixels.data(), pixels.size())); - extra_channel_pixels.resize(info.num_extra_channels); - for (uint32_t i = 0; i < info.num_extra_channels; ++i) { - EXPECT_SUCCESS(JxlDecoderExtraChannelBufferSize(dec.get(), &format, - &buffer_size, i)); - extra_channel_pixels[i].resize(buffer_size); - memset(extra_channel_pixels[i].data(), 0, - extra_channel_pixels[i].size()); - EXPECT_SUCCESS(JxlDecoderSetExtraChannelBuffer( - dec.get(), &format, extra_channel_pixels[i].data(), - extra_channel_pixels[i].size(), i)); - } - } else if (status == JXL_DEC_NEED_PREVIEW_OUT_BUFFER) { - // Set preview pixel output buffer. - size_t buffer_size; - EXPECT_SUCCESS( - JxlDecoderPreviewOutBufferSize(dec.get(), &format, &buffer_size)); - preview_pixels.resize(buffer_size); - memset(preview_pixels.data(), 0, preview_pixels.size()); - EXPECT_SUCCESS(JxlDecoderSetPreviewOutBuffer( - dec.get(), &format, preview_pixels.data(), preview_pixels.size())); - } else if (status == JXL_DEC_FULL_IMAGE) { - // Pixel output buffer is set. - if (opts.pixel_prefix) { - image.frames.emplace_back(); - swap(image.frames.back(), pixels); - image.ec_frames.emplace_back(); - for (uint32_t i = 0; i < info.num_extra_channels; ++i) { - image.ec_frames.back().emplace_back(); - swap(image.ec_frames.back().back(), extra_channel_pixels[i]); - } - } - - // TODO(deymo): Get the extra channel pixel data an store it. - } else if (status == JXL_DEC_PREVIEW_IMAGE) { - // Preview pixel output buffer is set. - if (opts.pixel_prefix && info.have_preview) { - preview.frames.emplace_back(); - swap(preview.frames.back(), preview_pixels); - } - } else if (status == JXL_DEC_SUCCESS) { - break; - } else { - fprintf(stderr, "Error: unexpected status: %d\n", - static_cast<int>(status)); - return false; - } - } - - if (opts.pixel_prefix) { - std::string name = std::string(opts.pixel_prefix) + "_image.npy"; - EXPECT_TRUE(SaveNPYArray(name.c_str(), image)); - } - - if (opts.pixel_prefix && info.have_preview) { - std::string name = std::string(opts.pixel_prefix) + "_preview.npy"; - EXPECT_TRUE(SaveNPYArray(name.c_str(), preview)); - } - - if (opts.metadata_path) { - JSONDict meta; - // Same order as in 18181-3 CD. - - // Frames. - auto* meta_frames = meta.AddEmpty<JSONArray>("frames"); - for (size_t i = 0; i < frame_headers.size(); i++) { - auto* frame_i = meta_frames->AddEmpty<JSONDict>(); - if (info.have_animation) { - frame_i->Add("duration", JSONValue(frame_headers[i].duration * 1.0f * - info.animation.tps_denominator / - info.animation.tps_numerator)); - } - - frame_i->Add("name", JSONValue(frame_names[i])); - - if (info.animation.have_timecodes) { - frame_i->Add("timecode", JSONValue(frame_headers[i].timecode)); - } - } - -#define METADATA(FIELD) meta.Add(#FIELD, info.FIELD) - - METADATA(intensity_target); - METADATA(min_nits); - METADATA(relative_to_max_display); - METADATA(linear_below); - - if (info.have_preview) { - meta.AddEmpty<JSONDict>("preview"); - // TODO(veluca): can we have duration/name/timecode here? - } - - { - auto ectype = meta.AddEmpty<JSONArray>("extra_channel_type"); - auto bps = meta.AddEmpty<JSONArray>("bits_per_sample"); - auto ebps = meta.AddEmpty<JSONArray>("exp_bits_per_sample"); - bps->Add(info.bits_per_sample); - ebps->Add(info.exponent_bits_per_sample); - for (size_t i = 0; i < extra_channels.size(); i++) { - switch (extra_channels[i].type) { - case JXL_CHANNEL_ALPHA: { - ectype->Add(std::string("Alpha")); - break; - } - case JXL_CHANNEL_DEPTH: { - ectype->Add(std::string("Depth")); - break; - } - case JXL_CHANNEL_SPOT_COLOR: { - ectype->Add(std::string("SpotColor")); - break; - } - case JXL_CHANNEL_SELECTION_MASK: { - ectype->Add(std::string("SelectionMask")); - break; - } - case JXL_CHANNEL_BLACK: { - ectype->Add(std::string("Black")); - break; - } - case JXL_CHANNEL_CFA: { - ectype->Add(std::string("CFA")); - break; - } - case JXL_CHANNEL_THERMAL: { - ectype->Add(std::string("Thermal")); - break; - } - default: { - ectype->Add(std::string("UNKNOWN")); - break; - } - } - bps->Add(extra_channels[i].bits_per_sample); - ebps->Add(extra_channels[i].exponent_bits_per_sample); - } - } - - std::ofstream ofs(opts.metadata_path); - meta.Write(ofs, 0); - ofs << std::endl; - ofs.close(); - EXPECT_TRUE(ofs.good()); - } - return true; -} - -int Usage(const char* program) { - fprintf( - stderr, - "Usage: %s INPUT_JXL [-i ORG_ICC] [-p PREFIX] [-m METADATA]\n" - "\n" - " INPUT_JXL: Path to the input .jxl file.\n" - " -i ORG_ICC: Path to the output \"original\" ICC profile.\n" - " -p PREFIX: Prefix path to generate the pixel numpy data image (with\n" - " suffix \".npy\") and ICC profile (with suffix \".icc\"). The \n" - " image data will be a 4D numpy array with dimensions (number of \n" - " frames, height, width, number of channels).\n" - " -j JPEG: Path to the output reconstructed JPEG file.\n" - " -m METADATA: Path to the output JSON text metadata file.\n", - program); - return 1; -} - -} // namespace - -// Helper macro to check that an extra argument was passed to ARG. -#define EXPECT_ARG(ARG) \ - if (optind >= argc) { \ - fprintf(stderr, "%s needs an argument value.\n", ARG); \ - return Usage(argv[0]); \ - } - -int main(int argc, char* argv[]) { - DecodeOptions opts; - - for (int optind = 1; optind < argc;) { - if (!strcmp(argv[optind], "-i")) { - optind++; - EXPECT_ARG("-i"); - opts.icc_path = argv[optind++]; - } else if (!strcmp(argv[optind], "-p")) { - optind++; - EXPECT_ARG("-p"); - opts.pixel_prefix = argv[optind++]; - } else if (!strcmp(argv[optind], "-j")) { - optind++; - EXPECT_ARG("-j"); - opts.jpeg_path = argv[optind++]; - } else if (!strcmp(argv[optind], "-m")) { - optind++; - EXPECT_ARG("-m"); - opts.metadata_path = argv[optind++]; - } else if (opts.input == nullptr) { - opts.input = argv[optind++]; - } else { - fprintf(stderr, "Unknown parameter: \"%s\".\n", argv[optind]); - return Usage(argv[0]); - } - } - if (!opts.input) { - fprintf(stderr, "JXL decoder for conformance testing.\n"); - return Usage(argv[0]); - } - - return DecodeJXL(opts) ? 0 : 1; -} diff --git a/media/libjxl/src/tools/conformance/generator.py b/media/libjxl/src/tools/conformance/generator.py index e230d48b3c..e2a9b2e66a 100755 --- a/media/libjxl/src/tools/conformance/generator.py +++ b/media/libjxl/src/tools/conformance/generator.py @@ -57,15 +57,14 @@ def GenerateConformanceCorpus(args): descriptor = {} descriptor['jxl'] = 'input.jxl' - cmd = [args.decoder, input_file] original_icc_filename = os.path.join(test_dir, 'original.icc') reconstructed_filename = os.path.join(test_dir, 'reconstructed.jpg') pixel_prefix = os.path.join(test_dir, 'reference') - cmd.extend(['-p', pixel_prefix]) - cmd.extend(['-i', original_icc_filename]) - cmd.extend(['-j', reconstructed_filename]) + output_file = pixel_prefix + '_image.npy' + cmd = [args.decoder, input_file, output_file] metadata_filename = os.path.join(test_dir, 'test.json') - cmd.extend(['-m', metadata_filename]) + cmd.extend(['--metadata_out', metadata_filename]) + cmd.extend(['--icc_out', pixel_prefix + '.icc']) # Decode and generate the reference files. subprocess.check_call(cmd) diff --git a/media/libjxl/src/tools/conformance/tooling_test.sh b/media/libjxl/src/tools/conformance/tooling_test.sh index 4366599838..95adefb1eb 100755 --- a/media/libjxl/src/tools/conformance/tooling_test.sh +++ b/media/libjxl/src/tools/conformance/tooling_test.sh @@ -13,7 +13,7 @@ MYDIR=$(dirname $(realpath "$0")) if [[ $# -eq 2 ]]; then JPEGXL_TEST_DATA_PATH="$2" else - JPEGXL_TEST_DATA_PATH="${MYDIR}/../../third_party/testdata" + JPEGXL_TEST_DATA_PATH="${MYDIR}/../../testdata" fi set -eux @@ -41,7 +41,7 @@ main() { build_dir=$(realpath "${MYDIR}/../../build") fi - local decoder="${build_dir}/tools/conformance/djxl_conformance" + local decoder="${build_dir}/tools/djxl" "${MYDIR}/generator.py" \ --decoder="${decoder}" \ --output="${tmpdir}" \ diff --git a/media/libjxl/src/tools/djxl.cc b/media/libjxl/src/tools/djxl.cc deleted file mode 100644 index 8487fcdf09..0000000000 --- a/media/libjxl/src/tools/djxl.cc +++ /dev/null @@ -1,329 +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/djxl.h" - -#include <stdio.h> - -#include "lib/extras/codec.h" -#include "lib/extras/dec/color_description.h" -#include "lib/extras/time.h" -#include "lib/extras/tone_mapping.h" -#include "lib/jxl/alpha.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/file_io.h" -#include "lib/jxl/base/override.h" -#include "lib/jxl/base/printf_macros.h" -#include "lib/jxl/color_encoding_internal.h" -#include "lib/jxl/color_management.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/frame_header.h" -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" -#include "lib/jxl/image_ops.h" -#include "lib/jxl/jpeg/dec_jpeg_data_writer.h" -#include "tools/args.h" -#include "tools/box/box.h" - -namespace jpegxl { -namespace tools { - -static inline bool ParseLuminanceRange(const char* arg, - std::pair<float, float>* out) { - char* end; - out->first = static_cast<float>(strtod(arg, &end)); - if (*end == '\0') { - // That was actually the upper bound. - out->second = out->first; - out->first = 0; - return true; - } - if (*end != '-') { - fprintf(stderr, "Unable to interpret as luminance range: %s.\n", arg); - return JXL_FAILURE("Args"); - } - const char* second = end + 1; - out->second = static_cast<float>(strtod(second, &end)); - if (*end != '\0') { - fprintf(stderr, "Unable to interpret as luminance range: %s.\n", arg); - return JXL_FAILURE("Args"); - } - return true; -} - -void DecompressArgs::AddCommandLineOptions(CommandLineParser* cmdline) { - // Positional arguments. - cmdline->AddPositionalOption("INPUT", /* required = */ true, - "the compressed input file", &file_in); - - cmdline->AddPositionalOption( - "OUTPUT", /* required = */ true, - "the output can be PNG with ICC, JPG, or PPM/PFM.", &file_out); - - cmdline->AddOptionFlag('V', "version", "print version number and exit", - &version, &SetBooleanTrue); - - cmdline->AddOptionValue('\0', "num_reps", "N", nullptr, &num_reps, - &ParseUnsigned); - - cmdline->AddOptionValue('\0', "num_threads", "N", - "The number of threads to use", &num_threads, - &ParseUnsigned); - - cmdline->AddOptionValue('\0', "print_profile", "0|1", - "print timing information before exiting", - &print_profile, &ParseOverride); - - cmdline->AddOptionValue('\0', "bits_per_sample", "N", - "defaults to original (input) bit depth", - &bits_per_sample, &ParseUnsigned); - - cmdline->AddOptionFlag( - '\0', "tone_map", - "tone map the image to the luminance range indicated by --display_nits " - "instead of performing a naive 0-1 -> 0-1 conversion", - &tone_map, &SetBooleanTrue); - - cmdline->AddOptionValue('\0', "display_nits", "0.3-250", - "luminance range of the display to which to " - "tone-map; the lower bound can be omitted", - &display_nits, &ParseLuminanceRange); - cmdline->AddOptionValue( - '\0', "preserve_saturation", "0..1", - "with --tone_map, how much to favor saturation over luminance", - &preserve_saturation, &ParseFloat); - - cmdline->AddOptionValue('\0', "color_space", "RGB_D65_SRG_Rel_Lin", - "defaults to original (input) color space", - &color_space, &ParseString); - - cmdline->AddOptionValue('s', "downsampling", "1,2,4,8,16", - "maximum permissible downsampling factor (values " - "greater than 16 will return the LQIP if available)", - ¶ms.max_downsampling, &ParseUnsigned); - - cmdline->AddOptionFlag('\0', "allow_partial_files", - "allow decoding of truncated files", - ¶ms.allow_partial_files, &SetBooleanTrue); - - cmdline->AddOptionFlag('\0', "allow_more_progressive_steps", - "allow decoding more progressive steps in truncated " - "files. No effect without --allow_partial_files", - ¶ms.allow_more_progressive_steps, &SetBooleanTrue); - -#if JPEGXL_ENABLE_JPEG - cmdline->AddOptionFlag( - 'j', "pixels_to_jpeg", - "By default, if the input JPEG XL contains a recompressed JPEG file, " - "djxl " - "reconstructs the exact original JPEG file. This flag causes the decoder " - "to instead decode the image to pixels and encode a new (lossy) JPEG. " - "The output file if provided must be a .jpg or .jpeg file.", - &decode_to_pixels, &SetBooleanTrue); - - opt_jpeg_quality_id = - cmdline->AddOptionValue('q', "jpeg_quality", "N", - "JPEG output quality. Setting an output quality " - "implies --pixels_to_jpeg.", - &jpeg_quality, &ParseUnsigned); -#endif - -#if JPEGXL_ENABLE_SJPEG - cmdline->AddOptionFlag('\0', "use_sjpeg", - "use sjpeg instead of libjpeg for JPEG output", - &use_sjpeg, &SetBooleanTrue); -#endif - - cmdline->AddOptionFlag('\0', "print_read_bytes", - "print total number of decoded bytes", - &print_read_bytes, &SetBooleanTrue); - - cmdline->AddOptionFlag('\0', "quiet", "silence output (except for errors)", - &quiet, &SetBooleanTrue); -} - -jxl::Status DecompressArgs::ValidateArgs(const CommandLineParser& cmdline) { - if (file_in == nullptr) { - fprintf(stderr, "Missing INPUT filename.\n"); - return false; - } - -#if JPEGXL_ENABLE_JPEG - if (cmdline.GetOption(opt_jpeg_quality_id)->matched()) { - decode_to_pixels = true; - } -#endif - if (file_out) { - const std::string extension = jxl::Extension(file_out); - const jxl::extras::Codec codec = - jxl::extras::CodecFromExtension(extension, &bits_per_sample); - if (codec != jxl::extras::Codec::kJPG) { - // when decoding to anything-but-JPEG, we'll need pixels - decode_to_pixels = true; - } - } else { - decode_to_pixels = true; - } - return true; -} - -jxl::Status DecompressJxlToPixels(const jxl::Span<const uint8_t> compressed, - const jxl::DecompressParams& params, - jxl::ThreadPool* pool, - jxl::CodecInOut* JXL_RESTRICT io, - SpeedStats* JXL_RESTRICT stats) { - const double t0 = jxl::Now(); - if (!jxl::DecodeFile(params, compressed, io, pool)) { - fprintf(stderr, "Failed to decompress to pixels.\n"); - return false; - } - const double t1 = jxl::Now(); - stats->NotifyElapsed(t1 - t0); - stats->SetImageSize(io->xsize(), io->ysize()); - return true; -} - -jxl::Status DecompressJxlToJPEG(const JpegXlContainer& container, - const DecompressArgs& args, - jxl::ThreadPool* pool, jxl::PaddedBytes* output, - SpeedStats* JXL_RESTRICT stats) { - output->clear(); - const double t0 = jxl::Now(); - - jxl::Span<const uint8_t> compressed(container.codestream); - - JXL_RETURN_IF_ERROR(compressed.size() >= 2); - - // JXL case - // Decode to DCT when possible and generate a JPG file. - jxl::CodecInOut io; - // Set JPEG quality. - // TODO(deymo): We should probably fail to give a JPEG file if the - // original image can't be transcoded to a JPEG file without passing - // through pixels, or at least signal this to the user. - io.use_sjpeg = args.use_sjpeg; - io.jpeg_quality = args.jpeg_quality; - - if (!DecodeJpegXlToJpeg(args.params, container, &io, pool)) { - return JXL_FAILURE("Failed to decode JXL to JPEG"); - } - if (!jxl::jpeg::EncodeImageJPGCoefficients(&io, output)) { - return JXL_FAILURE("Failed to generate JPEG"); - } - stats->SetImageSize(io.xsize(), io.ysize()); - - const double t1 = jxl::Now(); - stats->NotifyElapsed(t1 - t0); - stats->SetFileSize(output->size()); - return true; -} - -jxl::Status WriteJxlOutput(const DecompressArgs& args, const char* file_out, - jxl::CodecInOut& io, jxl::ThreadPool* pool) { - // Can only write if we decoded and have an output filename. - // (Writing large PNGs is slow, so allow skipping it for benchmarks.) - if (file_out == nullptr) return true; - - // Stay in original color space unless something else is needed. - jxl::ColorEncoding c_out = io.metadata.m.color_encoding; - // Override original color space with sRGB if input is CMYK. - if (io.Main().HasBlack()) c_out = jxl::ColorEncoding::SRGB(false); - // Override original color space with arg if specified. - if (!args.color_space.empty()) { - bool color_space_applied = false; - JxlColorEncoding c_out_external; - if (jxl::ParseDescription(args.color_space, &c_out_external) && - ConvertExternalToInternalColorEncoding(c_out_external, &c_out) && - c_out.CreateICC()) { - color_space_applied = true; - } else { - jxl::PaddedBytes icc; - if (jxl::ReadFile(args.color_space, &icc) && - c_out.SetICC(std::move(icc))) { - color_space_applied = true; - } - } - - if (!color_space_applied) { - fprintf(stderr, "Failed to apply color_space.\n"); - return false; - } - } - - // Override original #bits with arg if specified. - size_t bits_per_sample = io.metadata.m.bit_depth.bits_per_sample; - if (args.bits_per_sample != 0) bits_per_sample = args.bits_per_sample; - - if (args.tone_map) { - jxl::Status status = jxl::ToneMapTo(args.display_nits, &io, pool); - if (!status) fprintf(stderr, "Failed to map tones.\n"); - JXL_RETURN_IF_ERROR(status); - status = jxl::GamutMap(&io, args.preserve_saturation, pool); - if (!status) fprintf(stderr, "Failed to map gamut.\n"); - JXL_RETURN_IF_ERROR(status); - if (c_out.tf.IsPQ() && args.color_space.empty()) { - // Prevent writing the tone-mapped image to PQ output unless explicitly - // requested. The result would look even dimmer than it would have without - // tone mapping. - c_out.tf.SetTransferFunction(jxl::TransferFunction::k709); - status = c_out.CreateICC(); - if (!status) fprintf(stderr, "Failed to create ICC\n"); - JXL_RETURN_IF_ERROR(c_out.CreateICC()); - } - } - - const char* extension = strrchr(file_out, '.'); - std::string base = extension == nullptr - ? std::string(file_out) - : std::string(file_out, extension - file_out); - if (extension == nullptr) extension = ""; - const jxl::extras::Codec codec = jxl::extras::CodecFromExtension(extension); - if (!io.metadata.m.have_animation || codec == jxl::extras::Codec::kPNG) { - bool ok; - if (io.Main().IsJPEG() && codec == jxl::extras::Codec::kJPG) { - jxl::PaddedBytes encoded; - ok = jxl::jpeg::EncodeImageJPGCoefficients(&io, &encoded) && - jxl::WriteFile(encoded, file_out); - } else { - ok = jxl::EncodeToFile(io, c_out, bits_per_sample, file_out, pool); - } - if (!ok) { - fprintf(stderr, "Failed to write decoded image.\n"); - return false; - } - } else { - const int digits = 1 + static_cast<int>(std::log10(std::max( - 1, static_cast<int>(io.frames.size() - 1)))); - std::vector<char> output_filename; - output_filename.resize(base.size() + 1 + digits + strlen(extension) + 1); - - for (size_t i = 0; i < io.frames.size(); ++i) { - jxl::CodecInOut frame_io; - frame_io.SetFromImage(jxl::CopyImage(*io.frames[i].color()), - io.frames[i].c_current()); - frame_io.metadata.m = *io.frames[i].metadata(); - frame_io.jpeg_quality = io.jpeg_quality; - frame_io.use_sjpeg = io.use_sjpeg; - if (io.frames[i].HasAlpha()) { - frame_io.Main().SetAlpha( - jxl::CopyImage(*io.frames[i].alpha()), - /*alpha_is_premultiplied=*/io.frames[i].AlphaIsPremultiplied()); - } - snprintf(output_filename.data(), output_filename.size(), "%s-%0*zu%s", - base.c_str(), digits, i, extension); - if (!EncodeToFile(frame_io, c_out, bits_per_sample, - output_filename.data(), pool)) { - fprintf(stderr, - "Failed to write decoded image for frame %" PRIuS "/%" PRIuS - ".\n", - i + 1, io.frames.size()); - } - } - } - return true; -} - -} // namespace tools -} // namespace jpegxl diff --git a/media/libjxl/src/tools/djxl.h b/media/libjxl/src/tools/djxl.h deleted file mode 100644 index d091ed7a3f..0000000000 --- a/media/libjxl/src/tools/djxl.h +++ /dev/null @@ -1,91 +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. - -#ifndef TOOLS_DJXL_H_ -#define TOOLS_DJXL_H_ - -#include <stddef.h> - -#include <thread> - -#include "jxl/decode.h" -#include "lib/jxl/aux_out.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/override.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/codec_in_out.h" -#include "lib/jxl/dec_params.h" -#include "tools/args.h" -#include "tools/box/box.h" -#include "tools/cmdline.h" -#include "tools/speed_stats.h" - -namespace jpegxl { -namespace tools { - -// Common JPEG XL decompress arguments. -struct DecompressArgs { - // Initialize non-static default options. - DecompressArgs() = default; - - // Add all the command line options to the CommandLineParser. Note that the - // options are tied to the instance that this was called on. - void AddCommandLineOptions(CommandLineParser* cmdline); - - // Validate the passed arguments, checking whether all passed options are - // compatible. Returns whether the validation was successful. - jxl::Status ValidateArgs(const CommandLineParser& cmdline); - - // Common djxl parameters. - const char* file_in = nullptr; - const char* file_out = nullptr; - size_t num_threads = std::thread::hardware_concurrency(); - bool use_sjpeg = false; - size_t jpeg_quality = 95; - bool decode_to_pixels = false; - bool version = false; - jxl::Override print_profile = jxl::Override::kDefault; - - size_t num_reps = 1; - - // Format parameters: - - size_t bits_per_sample = 0; - bool tone_map = false; - std::pair<float, float> display_nits = {0.f, jxl::kDefaultIntensityTarget}; - float preserve_saturation = .1f; - std::string color_space; // description or path to ICC profile - - jxl::DecompressParams params; - - // If true, print the effective amount of bytes read from the bitstream. - bool print_read_bytes = false; - bool quiet = false; - - // References (ids) of specific options to check if they were matched. - CommandLineParser::OptionId opt_jpeg_quality_id = -1; -}; - -// Decompresses and notifies SpeedStats of elapsed time. -jxl::Status DecompressJxlToPixels(const jxl::Span<const uint8_t> compressed, - const jxl::DecompressParams& params, - jxl::ThreadPool* pool, - jxl::CodecInOut* JXL_RESTRICT io, - SpeedStats* JXL_RESTRICT stats); - -jxl::Status DecompressJxlToJPEG(const JpegXlContainer& container, - const DecompressArgs& args, - jxl::ThreadPool* pool, jxl::PaddedBytes* output, - SpeedStats* JXL_RESTRICT stats); - -jxl::Status WriteJxlOutput(const DecompressArgs& args, const char* file_out, - jxl::CodecInOut& io, - jxl::ThreadPool* pool = nullptr); - -} // namespace tools -} // namespace jpegxl - -#endif // TOOLS_DJXL_H_ diff --git a/media/libjxl/src/tools/djxl_fuzzer_test.cc b/media/libjxl/src/tools/djxl_fuzzer_test.cc new file mode 100644 index 0000000000..e5b35c9cdf --- /dev/null +++ b/media/libjxl/src/tools/djxl_fuzzer_test.cc @@ -0,0 +1,44 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#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" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size); + +std::vector<uint64_t> AllTestIds() { + return { + 4546077333782528, 4716049045520384, 4718378999218176, 4729306868219904, + 4787817341911040, 4816304719134720, 4848606801166336, 4859247059402752, + 4887504894951424, 4984529666834432, 5014934495297536, 5112097090961408, + 5189497920290816, 5381727462227968, 5382562858532864, 5392074930782208, + 5467620336336896, 5473482434019328, 5489367788945408, 5556400888086528, + 5582808628723712, 5631220790198272, 5685623166468096, 5737500246671360, + 5785438255710208, 5800733037953024, 5849986531721216, 5858549672050688, + 5899664422993920, 5900921718046720, 5906295376445440, 5914266367557632, + 6013780411154432, 6165169006313472, 6277573962760192, 6329817929220096, + 6355777170833408, 6375307931680768, 6448658097242112, 6515680276512768, + 6569981946494976, 6735607318052864, 6737321070821376, 6748486320652288, + }; +} + +class DjxlFuzzerTest : public ::testing::TestWithParam<uint64_t> {}; +JXL_GTEST_INSTANTIATE_TEST_SUITE_P(DjxlFuzzerTestInstantiation, DjxlFuzzerTest, + ::testing::ValuesIn(AllTestIds())); +TEST_P(DjxlFuzzerTest, TestOne) { + uint64_t id = GetParam(); + 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()); + LLVMFuzzerTestOneInput(input.data(), input.size()); +} diff --git a/media/libjxl/src/tools/djxl_main.cc b/media/libjxl/src/tools/djxl_main.cc index d1442d949c..44971c08eb 100644 --- a/media/libjxl/src/tools/djxl_main.cc +++ b/media/libjxl/src/tools/djxl_main.cc @@ -3,193 +3,463 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -#include <stdint.h> -#include <stdio.h> -#include <string.h> - +#include <climits> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <sstream> +#include <string> #include <vector> #include "jxl/decode.h" -#include "lib/jxl/aux_out.h" -#include "lib/jxl/base/cache_aligned.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/file_io.h" -#include "lib/jxl/base/override.h" -#include "lib/jxl/base/padded_bytes.h" +#include "jxl/thread_parallel_runner.h" +#include "jxl/thread_parallel_runner_cxx.h" +#include "jxl/types.h" +#include "lib/extras/dec/decode.h" +#include "lib/extras/dec/jxl.h" +#include "lib/extras/enc/encode.h" +#include "lib/extras/enc/pnm.h" +#include "lib/extras/packed_image.h" +#include "lib/extras/time.h" #include "lib/jxl/base/printf_macros.h" -#include "lib/jxl/base/profiler.h" -#include "lib/jxl/base/span.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/base/thread_pool_internal.h" -#include "lib/jxl/codec_in_out.h" -#include "tools/box/box.h" #include "tools/cmdline.h" #include "tools/codec_config.h" -#include "tools/djxl.h" +#include "tools/file_io.h" #include "tools/speed_stats.h" namespace jpegxl { namespace tools { + +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.", + &file_out); + + cmdline->AddOptionFlag('V', "version", "Print version number and exit.", + &version, &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->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); + + 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); + + cmdline->AddOptionFlag('\0', "allow_partial_files", + "Allow decoding of truncated files.", + &allow_partial_files, &SetBooleanTrue); + +#if JPEGXL_ENABLE_JPEG + cmdline->AddOptionFlag( + 'j', "pixels_to_jpeg", + "By default, if the input JPEG XL contains a recompressed JPEG file, " + "djxl reconstructs the exact original JPEG file. This flag causes the " + "decoder to instead decode the image to pixels and encode a new " + "(lossy) JPEG. The output file if provided must be a .jpg or .jpeg " + "file.", + &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 + cmdline->AddOptionFlag('\0', "use_sjpeg", + "Use sjpeg instead of libjpeg for JPEG output.", + &use_sjpeg, &SetBooleanTrue); +#endif + + cmdline->AddOptionFlag('\0', "norender_spotcolors", + "Disables rendering spot colors.", + &render_spotcolors, &SetBooleanFalse); + + cmdline->AddOptionValue('\0', "preview_out", "FILENAME", + "If specified, writes the preview image to this " + "file.", + &preview_out, &ParseString); + + cmdline->AddOptionValue( + '\0', "icc_out", "FILENAME", + "If specified, writes the ICC profile of the decoded image to " + "this file.", + &icc_out, &ParseString); + + 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); + + 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); + } + + // Validate the passed arguments, checking whether all passed options are + // compatible. Returns whether the validation was successful. + bool ValidateArgs(const CommandLineParser& cmdline) { + if (file_in == nullptr) { + fprintf(stderr, "Missing INPUT filename.\n"); + return false; + } + return true; + } + + const char* file_in = nullptr; + const char* file_out = nullptr; + bool version = false; + size_t num_reps = 1; + size_t num_threads = 0; + size_t bits_per_sample = 0; + double display_nits = 0.0; + std::string color_space; + uint32_t downsampling = 0; + bool allow_partial_files = false; + bool pixels_to_jpeg = false; + size_t jpeg_quality = 95; + bool use_sjpeg = false; + bool render_spotcolors = true; + std::string preview_out; + std::string icc_out; + std::string orig_icc_out; + std::string metadata_out; + bool print_read_bytes = false; + bool quiet = false; + // References (ids) of specific options to check if they were matched. + CommandLineParser::OptionId opt_jpeg_quality_id = -1; +}; + +} // namespace tools +} // namespace jpegxl + namespace { -int DecompressMain(int argc, const char* argv[]) { - DecompressArgs args; - CommandLineParser cmdline; +bool WriteOptionalOutput(const std::string& filename, + const std::vector<uint8_t>& bytes) { + if (filename.empty() || bytes.empty()) { + return true; + } + return jpegxl::tools::WriteFile(filename.data(), bytes); +} + +std::string Filename(const std::string& base, const std::string& extension, + int layer_index, int frame_index, int num_layers, + int num_frames) { + auto digits = [](int n) { return 1 + static_cast<int>(std::log10(n)); }; + std::string out = base; + 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) { + std::vector<char> buf(4 + digits(num_layers)); + snprintf(buf.data(), buf.size(), "-ec%0*d", digits(num_layers), + layer_index); + out.append(buf.data()); + } + if (extension == ".ppm" && layer_index > 0) { + out.append(".pgm"); + } else { + out.append(extension); + } + return out; +} + +bool DecompressJxlReconstructJPEG(const jpegxl::tools::DecompressArgs& args, + const std::vector<uint8_t>& compressed, + void* runner, + std::vector<uint8_t>* jpeg_bytes, + jpegxl::tools::SpeedStats* stats) { + const double t0 = jxl::Now(); + jxl::extras::PackedPixelFile ppf; // for JxlBasicInfo + jxl::extras::JXLDecompressParams dparams; + dparams.runner = JxlThreadParallelRunner; + dparams.runner_opaque = runner; + if (!jxl::extras::DecodeImageJXL(compressed.data(), compressed.size(), + dparams, nullptr, &ppf, jpeg_bytes)) { + return false; + } + const double t1 = jxl::Now(); + if (stats) { + stats->NotifyElapsed(t1 - t0); + stats->SetImageSize(ppf.info.xsize, ppf.info.ysize); + stats->SetFileSize(jpeg_bytes->size()); + } + return true; +} + +bool DecompressJxlToPackedPixelFile( + const jpegxl::tools::DecompressArgs& args, + const std::vector<uint8_t>& compressed, + const std::vector<JxlPixelFormat>& accepted_formats, void* runner, + jxl::extras::PackedPixelFile* ppf, size_t* decoded_bytes, + jpegxl::tools::SpeedStats* stats) { + jxl::extras::JXLDecompressParams dparams; + dparams.max_downsampling = args.downsampling; + dparams.accepted_formats = accepted_formats; + dparams.display_nits = args.display_nits; + dparams.color_space = args.color_space; + dparams.render_spotcolors = args.render_spotcolors; + dparams.runner = JxlThreadParallelRunner; + dparams.runner_opaque = runner; + dparams.allow_partial_input = args.allow_partial_files; + const double t0 = jxl::Now(); + if (!jxl::extras::DecodeImageJXL(compressed.data(), compressed.size(), + dparams, decoded_bytes, ppf)) { + return false; + } + const double t1 = jxl::Now(); + if (stats) { + stats->NotifyElapsed(t1 - t0); + stats->SetImageSize(ppf->info.xsize, ppf->info.ysize); + } + return true; +} + +} // namespace + +int main(int argc, const char* argv[]) { + std::string version = jpegxl::tools::CodecConfigString(JxlDecoderVersion()); + jpegxl::tools::DecompressArgs args; + jpegxl::tools::CommandLineParser cmdline; args.AddCommandLineOptions(&cmdline); if (!cmdline.Parse(argc, argv)) { - // ValidateArgs already printed the actual error cause. + // Parse already printed the actual error cause. fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); - return 1; + return EXIT_FAILURE; } if (args.version) { - fprintf(stdout, "djxl %s\n", - CodecConfigString(JxlDecoderVersion()).c_str()); + fprintf(stdout, "djxl %s\n", version.c_str()); fprintf(stdout, "Copyright (c) the JPEG XL Project\n"); - return 0; + return EXIT_SUCCESS; } if (!args.quiet) { - fprintf(stderr, "JPEG XL decoder %s\n", - CodecConfigString(JxlDecoderVersion()).c_str()); + fprintf(stderr, "JPEG XL decoder %s\n", version.c_str()); } if (cmdline.HelpFlagPassed()) { cmdline.PrintHelp(); - return 0; + return EXIT_SUCCESS; } if (!args.ValidateArgs(cmdline)) { // ValidateArgs already printed the actual error cause. fprintf(stderr, "Use '%s -h' for more information\n", argv[0]); - return 1; + return EXIT_FAILURE; } - jxl::PaddedBytes compressed; - if (!jxl::ReadFile(args.file_in, &compressed)) { - fprintf(stderr, "Failed to read file: %s.\n", args.file_in); - return 1; + std::vector<uint8_t> compressed; + // Reading compressed JPEG XL input + if (!jpegxl::tools::ReadFile(args.file_in, &compressed)) { + fprintf(stderr, "couldn't load %s\n", args.file_in); + return EXIT_FAILURE; } if (!args.quiet) { fprintf(stderr, "Read %" PRIuS " compressed bytes.\n", compressed.size()); } - // If the file uses the box format container, unpack the boxes into - // `container`. Otherwise, fill `container.codestream` accordingly. - JpegXlContainer container; - if (IsContainerHeader(compressed.data(), compressed.size())) { - if (!DecodeJpegXlContainerOneShot(compressed.data(), compressed.size(), - &container)) { - fprintf(stderr, "Decoding container format failed.\n"); - return 1; - } - } else { - container.codestream = std::move(compressed); - } - - jxl::ThreadPoolInternal pool(args.num_threads); - SpeedStats stats; - - // Quick test that this looks like a valid JXL file. - JxlSignature signature = JxlSignatureCheck(container.codestream.data(), - container.codestream.size()); - if (signature == JXL_SIG_NOT_ENOUGH_BYTES || signature == JXL_SIG_INVALID) { - fprintf(stderr, "Unknown compressed image format (%u)\n", signature); - return 1; - } - if (!args.file_out && !args.quiet) { fprintf(stderr, "No output file specified.\n" "Decoding will be performed, but the result will be discarded.\n"); } - jxl::AuxOut aux_out; - - if (!args.decode_to_pixels) { - args.params.keep_dct = true; + std::string filename_out; + std::string base; + 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()) { + base = filename_out.substr(0, pos); + extension = filename_out.substr(pos); + } else { + base = filename_out; + } + } + 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) { + fprintf(stderr, "Warning: colorspace ignored for EXR output\n"); + } + args.color_space = force_colorspace; + } - jxl::PaddedBytes jpg_output; - bool success = true; - for (size_t i = 0; i < args.num_reps; ++i) { - success = success && DecompressJxlToJPEG(container, args, &pool, - &jpg_output, &stats); + 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) { + num_worker_threads = flag_num_worker_threads; } - if (!args.quiet && success) fprintf(stderr, "Reconstructed to JPEG.\n"); + } + auto runner = JxlThreadParallelRunnerMake( + /*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()) { + decode_to_pixels = true; + } +#endif - if (success && args.file_out != nullptr) { - if (!jxl::WriteFile(jpg_output, args.file_out)) { - fprintf(stderr, "Failed to write to \"%s\"\n", args.file_out); - return 1; + size_t num_reps = args.num_reps; + if (!decode_to_pixels) { + std::vector<uint8_t> bytes; + for (size_t i = 0; i < num_reps; ++i) { + if (!DecompressJxlReconstructJPEG(args, compressed, runner.get(), &bytes, + &stats)) { + if (bytes.empty()) { + if (!args.quiet) { + fprintf(stderr, + "Warning: could not decode losslessly to JPEG. Retrying " + "with --pixels_to_jpeg...\n"); + } + decode_to_pixels = true; + break; + } + return EXIT_FAILURE; } } - if (!success) { - if (!args.quiet) { - fprintf(stderr, - "Warning: could not decode losslessly to JPEG. Retrying with " - "--pixels_to_jpeg...\n"); + if (!bytes.empty()) { + if (!args.quiet) fprintf(stderr, "Reconstructed to JPEG.\n"); + if (!filename_out.empty() && + !jpegxl::tools::WriteFile(filename_out.c_str(), bytes)) { + return EXIT_FAILURE; } - args.decode_to_pixels = true; } } - if (args.decode_to_pixels) { - args.params.keep_dct = false; - jxl::CodecInOut io; - auto assign = [](const uint8_t* bytes, size_t size, - std::vector<uint8_t>& target) { - target.assign(bytes, bytes + size); - }; - if (container.exif_size) { - assign(container.exif, container.exif_size, io.blobs.exif); + if (decode_to_pixels) { + std::vector<JxlPixelFormat> accepted_formats; + std::unique_ptr<jxl::extras::Encoder> encoder; + if (!filename_out.empty()) { + 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; + } + accepted_formats = encoder->AcceptedFormats(); + } + jxl::extras::PackedPixelFile ppf; + size_t decoded_bytes = 0; + for (size_t i = 0; i < num_reps; ++i) { + if (!DecompressJxlToPackedPixelFile(args, compressed, accepted_formats, + runner.get(), &ppf, &decoded_bytes, + &stats)) { + fprintf(stderr, "DecompressJxlToPackedPixelFile failed\n"); + return EXIT_FAILURE; + } } - if (!container.xml.empty()) { - assign(container.xml[0].first, container.xml[0].second, io.blobs.xmp); + if (!args.quiet) fprintf(stderr, "Decoded to pixels.\n"); + if (args.print_read_bytes) { + fprintf(stderr, "Decoded bytes: %" PRIuS "\n", decoded_bytes); } - if (container.xml.size() > 1) { - fprintf(stderr, - "Warning: more than one XML box found, assuming first one is XMP " - "and ignoring others\n"); + 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; } - // Set JPEG quality. - // TODO(veluca): the decoder should set this value, and the argument should - // be an override. - // TODO(veluca): the decoder should directly produce a JPEG file, and this - // should not be necessary. - io.use_sjpeg = args.use_sjpeg; - io.jpeg_quality = args.jpeg_quality; - - // Decode to pixels. - for (size_t i = 0; i < args.num_reps; ++i) { - if (!DecompressJxlToPixels(jxl::Span<const uint8_t>(container.codestream), - args.params, &pool, &io, &stats)) { - // Error is already reported by DecompressJxlToPixels. - return 1; +#if JPEGXL_ENABLE_JPEG + if (encoder) { + 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 (!encoder->Encode(ppf, &encoded_image)) { + fprintf(stderr, "Encode failed\n"); + return EXIT_FAILURE; } } - if (!args.quiet) fprintf(stderr, "Decoded to pixels.\n"); - if (!WriteJxlOutput(args, args.file_out, io, &pool)) { - // Error is already reported by WriteJxlOutput. - return 1; + 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; + } + } } - - if (args.print_read_bytes) { - fprintf(stderr, "Decoded bytes: %" PRIuS "\n", io.Main().decoded_bytes()); + 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) JXL_CHECK(stats.Print(pool.NumWorkerThreads())); - - if (args.print_profile == jxl::Override::kOn) { - PROFILER_PRINT_RESULTS(); + if (!args.quiet) { + stats.Print(num_worker_threads); } - if (!args.quiet) jxl::CacheAligned::PrintStats(); - return 0; -} - -} // namespace -} // namespace tools -} // namespace jpegxl - -int main(int argc, const char* argv[]) { - return jpegxl::tools::DecompressMain(argc, argv); + return EXIT_SUCCESS; } diff --git a/media/libjxl/src/tools/djxl_ng_main.cc b/media/libjxl/src/tools/djxl_ng_main.cc deleted file mode 100644 index b2eec302a8..0000000000 --- a/media/libjxl/src/tools/djxl_ng_main.cc +++ /dev/null @@ -1,476 +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 <climits> -#include <cstddef> -#include <cstdint> -#include <cstdio> -#include <cstdlib> -#include <cstring> -#include <iostream> -#include <string> -#include <vector> - -#include "gflags/gflags.h" -#include "jxl/codestream_header.h" -#include "jxl/decode.h" -#include "jxl/decode_cxx.h" -#include "jxl/resizable_parallel_runner_cxx.h" -#include "jxl/thread_parallel_runner.h" -#include "jxl/thread_parallel_runner_cxx.h" -#include "jxl/types.h" -#include "lib/extras/dec/decode.h" -#include "lib/extras/enc/pnm.h" -#include "lib/extras/packed_image.h" -#include "lib/jxl/base/printf_macros.h" -#include "lib/jxl/base/status.h" - -DECLARE_bool(help); -DECLARE_bool(helpshort); - -DEFINE_int64(num_reps, 1, "How many times to decompress."); - -DEFINE_int64(num_threads, 0, - // TODO(firsching): Sync with team about changed meaning of 0 - - // was: No multithreaded workers. Is: use default number. - "Number of worker threads (0 == use machine default)."); - -// TODO(firsching): wire this up. -DEFINE_int32(bits_per_sample, 0, "0 = original (input) bit depth"); - -// TODO(firsching): wire this up. -DEFINE_bool( - tone_map, true, - "tone map the image to the luminance range indicated by --display_nits " - "instead of performing a naive 0-1 -> 0-1 conversion"); - -// TODO(firsching): wire this up. -DEFINE_string(display_nits, "0.f-255.", - "luminance range of the display to which to " - "tone-map; the lower bound can be omitted"); - -// TODO(firsching): wire this up. -DEFINE_double(preserve_saturation, 0.1, - "with --tone_map, how much to favor saturation over luminance"); - -// TODO(firsching): wire this up; consider making empty string the default. -DEFINE_string(color_space, "RGB_D65_SRG_Rel_Lin", - "defaults to original (input) color space"); - -// TODO(firsching): wire this up. -DEFINE_uint32(downsampling, 0, - "maximum permissible downsampling factor (values " - "greater than 16 will return the LQIP if available"); - -// TODO(firsching): wire this up. -DEFINE_bool(allow_partial_files, false, "allow decoding of truncated files"); - -// TODO(firsching): wire this up. -DEFINE_bool(allow_more_progressive_steps, false, - "allow decoding more progressive steps in truncated " - "files. No effect without --allow_partial_files"); - -#if JPEGXL_ENABLE_JPEG -// TODO(firsching): wire this up. -DEFINE_bool( - pixels_to_jpeg, false, - "By default, if the input JPEG XL contains a recompressed JPEG file, djxl " - "reconstructs the exact original JPEG file. This flag causes the decoder " - "to instead decode the image to pixels and encode a new (lossy) JPEG. " - "The output file if provided must be a .jpg or .jpeg file."); - -// TODO(firsching): wire this up. -DEFINE_uint32(jpeg_quality, 95, - "JPEG output quality. Setting an output quality " - "implies --pixels_to_jpeg."); -#endif - -#if JPEGXL_ENABLE_SJPEG -// TODO(firsching): wire this up. -DEFINE_bool(use_sjpeg, false, "use sjpeg instead of libjpeg for JPEG output"); -#endif - -// TODO(firsching): wire this up. -DEFINE_bool(print_read_bytes, false, "print total number of decoded bytes"); - -// TODO(firsching): wire this up. -DEFINE_bool(quiet, false, "silence output (except for errors)"); - -bool ReadFile(const char* filename, std::vector<uint8_t>* out) { - FILE* file = fopen(filename, "rb"); - if (!file) { - return false; - } - - if (fseek(file, 0, SEEK_END) != 0) { - fclose(file); - return false; - } - - long size = ftell(file); - // Avoid invalid file or directory. - if (size >= LONG_MAX || size < 0) { - fclose(file); - return false; - } - - if (fseek(file, 0, SEEK_SET) != 0) { - fclose(file); - return false; - } - - out->resize(size); - size_t readsize = fread(out->data(), 1, size, file); - if (fclose(file) != 0) { - return false; - } - - return readsize == static_cast<size_t>(size); -} - -bool WriteFile(const char* filename, const std::vector<uint8_t>& bytes) { - FILE* file = fopen(filename, "wb"); - if (!file) { - fprintf(stderr, - "Could not open %s for writing\n" - "Error: %s", - filename, strerror(errno)); - return false; - } - if (fwrite(bytes.data(), 1, bytes.size(), file) != bytes.size()) { - fprintf(stderr, - "Could not write to file\n" - "Error: %s", - strerror(errno)); - return false; - } - if (fclose(file) != 0) { - fprintf(stderr, - "Could not close file\n" - "Error: %s", - strerror(errno)); - return false; - } - return true; -} - -int DecompressJxlReconstructJPEG(const std::vector<uint8_t>& compressed, - std::vector<uint8_t>& jpeg_bytes, - JxlDecoderPtr dec, - JxlThreadParallelRunnerPtr runner) { - if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), - JxlThreadParallelRunner, - runner.get())) { - fprintf(stderr, "JxlEncoderSetParallelRunner failed\n"); - return EXIT_FAILURE; - } - - if (JXL_DEC_SUCCESS != - JxlDecoderSubscribeEvents( - dec.get(), JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE)) { - fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); - return EXIT_FAILURE; - } - bool can_reconstruct_jpeg = false; - std::vector<uint8_t> jpeg_data_chunk(16384); - jpeg_bytes.resize(0); - if (JXL_DEC_SUCCESS != - JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size())) { - fprintf(stderr, "Decoder failed to set input\n"); - return EXIT_FAILURE; - } - JxlDecoderCloseInput(dec.get()); - - for (;;) { - JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); - if (status == JXL_DEC_ERROR) { - fprintf(stderr, "Failed to decode image\n"); - return EXIT_FAILURE; - } else if (status == JXL_DEC_NEED_MORE_INPUT) { - fprintf(stderr, "Error, already provided all input\n"); - return EXIT_FAILURE; - } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { - can_reconstruct_jpeg = true; - // Decoding to JPEG. - if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec.get(), - jpeg_data_chunk.data(), - jpeg_data_chunk.size())) { - fprintf(stderr, "Decoder failed to set JPEG Buffer\n"); - return EXIT_FAILURE; - } - } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { - // Decoded a chunk to JPEG. - size_t used_jpeg_output = - jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); - jpeg_bytes.insert(jpeg_bytes.end(), jpeg_data_chunk.data(), - jpeg_data_chunk.data() + used_jpeg_output); - if (used_jpeg_output == 0) { - // Chunk is too small. - jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2); - } - if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec.get(), - jpeg_data_chunk.data(), - jpeg_data_chunk.size())) { - fprintf(stderr, "Decoder failed to set JPEG Buffer\n"); - return EXIT_FAILURE; - } - } else if (status == JXL_DEC_SUCCESS) { - // Decoding finished successfully. - break; - } else if (status == JXL_DEC_FULL_IMAGE) { - } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { - break; - } else { - fprintf(stderr, "Error: unexpected status: %d\n", - static_cast<int>(status)); - return EXIT_FAILURE; - } - } - if (!can_reconstruct_jpeg) return EXIT_FAILURE; - size_t used_jpeg_output = - jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); - jpeg_bytes.insert(jpeg_bytes.end(), jpeg_data_chunk.data(), - jpeg_data_chunk.data() + used_jpeg_output); - return EXIT_SUCCESS; -} - -int DecompressJxlToPackedPixelFile(const std::vector<uint8_t>& compressed, - jxl::extras::PackedPixelFile& ppf, - JxlPixelFormat& format, JxlDecoderPtr dec, - JxlThreadParallelRunnerPtr runner) { - if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), - JxlThreadParallelRunner, - runner.get())) { - fprintf(stderr, "JxlEncoderSetParallelRunner failed\n"); - return EXIT_FAILURE; - } - if (JXL_DEC_SUCCESS != - JxlDecoderSubscribeEvents(dec.get(), - JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | - JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)) { - fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); - return EXIT_FAILURE; - } - - // Reading compressed JPEG XL input and decoding to pixels - if (JXL_DEC_SUCCESS != - JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size())) { - fprintf(stderr, "Decoder failed to set input\n"); - return EXIT_FAILURE; - } - // TODO(firsching): handle boxes as well (exif, iptc, jumbf and xmp). - for (;;) { - JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); - if (status == JXL_DEC_ERROR) { - fprintf(stderr, "Failed to decode image\n"); - return EXIT_FAILURE; - } else if (status == JXL_DEC_NEED_MORE_INPUT) { - fprintf(stderr, "Error, already provided all input\n"); - return EXIT_FAILURE; - } else if (status == JXL_DEC_BASIC_INFO) { - if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &ppf.info)) { - fprintf(stderr, "JxlDecoderGetBasicInfo failed\n"); - return EXIT_FAILURE; - } - // Make some modifications to the format if the decoded data requires it. - if (ppf.info.num_color_channels != format.num_channels) { - format.num_channels = ppf.info.num_color_channels; - } - if (ppf.info.bits_per_sample > 8 && - ppf.info.exponent_bits_per_sample == 0) { - format.data_type = JXL_TYPE_UINT16; - } - // TODO(firsching): handle extra channels - } else if (status == JXL_DEC_COLOR_ENCODING) { - size_t icc_size = 0; - // TODO(firsching) handle other targets as well. - JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_ORIGINAL; - if (JXL_DEC_SUCCESS != - JxlDecoderGetICCProfileSize(dec.get(), &format, target, &icc_size)) { - fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); - } - if (icc_size != 0) { - ppf.icc.resize(icc_size); - if (JXL_DEC_SUCCESS != - JxlDecoderGetColorAsICCProfile(dec.get(), &format, target, - ppf.icc.data(), icc_size)) { - fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); - return EXIT_FAILURE; - } - } else { - if (JXL_DEC_SUCCESS != - JxlDecoderGetColorAsEncodedProfile(dec.get(), &format, target, - &ppf.color_encoding)) { - fprintf(stderr, "JxlDecoderGetColorAsEncodedProfile failed\n"); - return EXIT_FAILURE; - } - } - } else if (status == JXL_DEC_FRAME) { - jxl::extras::PackedFrame frame(ppf.info.xsize, ppf.info.ysize, format); - ppf.frames.emplace_back(std::move(frame)); - } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { - size_t buffer_size; - if (JXL_DEC_SUCCESS != - JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) { - fprintf(stderr, "JxlDecoderImageOutBufferSize failed\n"); - return EXIT_FAILURE; - } - if (buffer_size != ppf.frames.back().color.pixels_size) { - fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n", - buffer_size, ppf.frames.back().color.pixels_size); - return EXIT_FAILURE; - } - - auto callback = [](void* opaque, size_t x, size_t y, size_t num_pixels, - const void* pixels) { - jxl::extras::PackedPixelFile* ppf = - reinterpret_cast<jxl::extras::PackedPixelFile*>(opaque); - uint8_t* pixels_buffer = - reinterpret_cast<uint8_t*>(ppf->frames.back().color.pixels()); - size_t sample_size = ppf->frames.back().color.format.num_channels * - ppf->frames.back().color.BitsPerChannel( - ppf->frames.back().color.format.data_type) / - 8; - // TODO(firsching): take color profile into account and transform if - // needed here. - memcpy(pixels_buffer + - (ppf->frames.back().color.stride * y + sample_size * x), - pixels, num_pixels * sample_size); - }; - if (JXL_DEC_SUCCESS != - JxlDecoderSetImageOutCallback(dec.get(), &format, callback, &ppf)) { - fprintf(stderr, "JxlDecoderSetImageOutCallback failed\n"); - return EXIT_FAILURE; - } - } else if (status == JXL_DEC_SUCCESS) { - // Decoding finished successfully. - break; - } else if (status == JXL_DEC_FULL_IMAGE) { - } else { - fprintf(stderr, "Error: unexpected status: %d\n", - static_cast<int>(status)); - return EXIT_FAILURE; - } - } - return EXIT_SUCCESS; -} - -int main(int argc, char** argv) { - std::cerr << "Warning: This is work in progress, consider using djxl " - "instead!\n"; - - gflags::SetUsageMessage("JPEG XL decoder"); - uint32_t version = JxlDecoderVersion(); - gflags::SetVersionString(std::to_string(version / 1000000) + "." + - std::to_string((version / 1000) % 1000) + "." + - std::to_string(version % 1000)); - // TODO(firsching): rethink --help handling - gflags::ParseCommandLineNonHelpFlags(&argc, &argv, /*remove_flags=*/true); - if (FLAGS_help) { - FLAGS_help = false; - FLAGS_helpshort = true; - } - gflags::HandleCommandLineHelpFlags(); - - if (argc != 3) { - FLAGS_help = false; - FLAGS_helpshort = true; - gflags::HandleCommandLineHelpFlags(); - return EXIT_FAILURE; - } - const char* filename_in = argv[1]; - const char* filename_out = argv[2]; - size_t num_reps = FLAGS_num_reps; - - const char* extension = strrchr(filename_out, '.'); - std::string base = extension == nullptr - ? std::string(filename_out) - : std::string(filename_out, extension - filename_out); - if (extension == nullptr) extension = ""; - const jxl::extras::Codec codec = jxl::extras::CodecFromExtension(extension); - - std::vector<uint8_t> compressed; - // Reading compressed JPEG XL input - if (!ReadFile(filename_in, &compressed)) { - fprintf(stderr, "couldn't load %s\n", filename_in); - return EXIT_FAILURE; - } - - size_t num_worker_threads = JxlThreadParallelRunnerDefaultNumWorkerThreads(); - { - int64_t flag_num_worker_threads = FLAGS_num_threads; - if (flag_num_worker_threads != 0) { - num_worker_threads = flag_num_worker_threads; - } - } - auto dec = JxlDecoderMake(/*memory_manager=*/nullptr); - auto runner = JxlThreadParallelRunnerMake( - /*memory_manager=*/nullptr, num_worker_threads); - std::vector<uint8_t> bytes; - if (codec == jxl::extras::Codec::kJPG -#if JPEGXL_ENABLE_JPEG - && !FLAGS_pixels_to_jpeg -#endif - ) { - std::vector<uint8_t> bytes; - for (size_t i = 0; i < num_reps; ++i) { - if (DecompressJxlReconstructJPEG(compressed, bytes, std::move(dec), - std::move(runner)) != 0) { - return EXIT_FAILURE; - } - } - if (WriteFile(filename_out, bytes)) { - return EXIT_FAILURE; - }; - // TODO(firsching): handle non-reconstruct JPEG - } else if (codec == jxl::extras::Codec::kPNM) { - JxlDataType datatype = JXL_TYPE_UINT8; - uint32_t num_channels = 3; - if (std::string(extension) == ".pfm") { - datatype = JXL_TYPE_FLOAT; - } - if (std::string(extension) == ".pgm") { - num_channels = 1; - } - - JxlPixelFormat format = {num_channels, datatype, JXL_NATIVE_ENDIAN, 0}; - jxl::extras::PackedPixelFile ppf; - if (DecompressJxlToPackedPixelFile(compressed, ppf, format, std::move(dec), - std::move(runner)) != 0) { - return EXIT_FAILURE; - } - if (ppf.info.exponent_bits_per_sample != 0) { - if (num_channels == 1 && ppf.info.num_color_channels == 3) { - JXL_WARNING("For color images, the filename should end with .ppm.\n"); - } - if (num_channels == 3 && ppf.info.num_color_channels == 1) { - JXL_WARNING( - "For grayscale images, the filename should end with .pgm.\n"); - } - if (ppf.info.bits_per_sample > 16) { - JXL_WARNING("PPM only supports up to 16 bits per sample"); - } - } - const int digits = 1 + static_cast<int>(std::log10(std::max( - 1, static_cast<int>(ppf.frames.size() - 1)))); - std::vector<char> output_filename; - output_filename.resize(base.size() + 1 + digits + strlen(extension) + 1); - for (size_t i = 0; i < ppf.frames.size(); i++) { - JXL_RETURN_IF_ERROR(jxl::extras::EncodeImagePNM( - ppf, ppf.frames[i].color.BitsPerChannel(format.data_type), nullptr, i, - &bytes)); - snprintf(output_filename.data(), output_filename.size(), "%s-%0*zu%s", - base.c_str(), digits, i, extension); - if (!WriteFile( - ppf.frames.size() > 1 ? output_filename.data() : filename_out, - bytes)) { - return EXIT_FAILURE; - } - } - } else { - // TODO(firsching): handle other formats - } - return EXIT_SUCCESS; -} diff --git a/media/libjxl/src/tools/file_io.cc b/media/libjxl/src/tools/file_io.cc new file mode 100644 index 0000000000..bc7f3b16fb --- /dev/null +++ b/media/libjxl/src/tools/file_io.cc @@ -0,0 +1,75 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "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/media/libjxl/src/tools/file_io.h b/media/libjxl/src/tools/file_io.h new file mode 100644 index 0000000000..959b79d492 --- /dev/null +++ b/media/libjxl/src/tools/file_io.h @@ -0,0 +1,23 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef TOOLS_FILE_IO_H_ +#define TOOLS_FILE_IO_H_ + +#include <stdint.h> + +#include <vector> + +namespace jpegxl { +namespace tools { + +bool ReadFile(const char* filename, std::vector<uint8_t>* out); + +bool WriteFile(const char* filename, const std::vector<uint8_t>& bytes); + +} // namespace tools +} // namespace jpegxl + +#endif // TOOLS_FILE_IO_H_ diff --git a/media/libjxl/src/tools/fuzzer_corpus.cc b/media/libjxl/src/tools/fuzzer_corpus.cc index 0d66bd816e..159256cb6f 100644 --- a/media/libjxl/src/tools/fuzzer_corpus.cc +++ b/media/libjxl/src/tools/fuzzer_corpus.cc @@ -23,7 +23,7 @@ #include <vector> #if JPEGXL_ENABLE_JPEG -#include "lib/extras/enc/jpg.h" +#include "lib/extras/codec.h" #endif #include "lib/jxl/aux_out.h" #include "lib/jxl/base/data_parallel.h" @@ -220,8 +220,8 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec, span, spec.width, spec.height, io.metadata.m.color_encoding, bytes_per_pixel / bytes_per_sample, /*alpha_is_premultiplied=*/spec.alpha_is_premultiplied, - io.metadata.m.bit_depth.bits_per_sample, JXL_LITTLE_ENDIAN, - false /* flipped_y */, nullptr, &ib, /*float_in=*/false, /*align=*/0)); + io.metadata.m.bit_depth.bits_per_sample, JXL_LITTLE_ENDIAN, nullptr, + &ib, /*float_in=*/false, /*align=*/0)); io.frames.push_back(std::move(ib)); } @@ -232,10 +232,12 @@ bool GenerateFile(const char* output_dir, const ImageSpec& spec, if (spec.is_reconstructible_jpeg) { // If this image is supposed to be a reconstructible JPEG, collect the JPEG // metadata and encode it in the beginning of the compressed bytes. - jxl::PaddedBytes jpeg_bytes; - JXL_RETURN_IF_ERROR(EncodeImageJPG( - &io, jxl::extras::JpegEncoder::kLibJpeg, /*quality=*/70, - jxl::YCbCrChromaSubsampling(), /*pool=*/nullptr, &jpeg_bytes)); + 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_RETURN_IF_ERROR(jxl::jpeg::DecodeImageJPG( jxl::Span<const uint8_t>(jpeg_bytes.data(), jpeg_bytes.size()), &io)); jxl::PaddedBytes jpeg_data; diff --git a/media/libjxl/src/tools/hdr/generate_lut_template.cc b/media/libjxl/src/tools/hdr/generate_lut_template.cc index 45fc27ab56..626d54fd2e 100644 --- a/media/libjxl/src/tools/hdr/generate_lut_template.cc +++ b/media/libjxl/src/tools/hdr/generate_lut_template.cc @@ -53,6 +53,7 @@ int main(int argc, const char** argv) { "GenerateTemplate")); 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)); diff --git a/media/libjxl/src/tools/jxl_emcc.cc b/media/libjxl/src/tools/jxl_emcc.cc index 1951b30a5a..c4c855a1d8 100644 --- a/media/libjxl/src/tools/jxl_emcc.cc +++ b/media/libjxl/src/tools/jxl_emcc.cc @@ -4,63 +4,240 @@ // license that can be found in the LICENSE file. #include <cstring> +#include <memory> +#include <vector> -#include "lib/extras/codec.h" -#include "lib/jxl/base/span.h" -#include "lib/jxl/codec_in_out.h" -#include "lib/jxl/dec_file.h" -#include "lib/jxl/enc_cache.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_file.h" +#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 + +namespace { + +struct DecoderInstance { + uint32_t width = 0; + uint32_t height = 0; + uint8_t* pixels = nullptr; + uint32_t color_space = 0; + + size_t pixels_size = 0; + bool want_sdr; + uint32_t display_nits; + JxlPixelFormat format; + JxlDecoderPtr decoder; + JxlThreadParallelRunnerPtr thread_pool; + + std::vector<uint8_t> tail; +}; + +} // namespace extern "C" { -/* NOTA BENE: see file history to uncover how to decode HDR JPEGs to pixels. */ +void* jxlCreateInstance(bool want_sdr, uint32_t display_nits) { + DecoderInstance* instance = new DecoderInstance(); + instance->want_sdr = want_sdr; + instance->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); + + JxlDecoder* dec = instance->decoder.get(); + + auto report_error = [&](uint32_t code, const char* text) { + fprintf(stderr, "%s\n", text); + // instance->result = code; + return instance; + }; + + instance->thread_pool = JxlThreadParallelRunnerMake(nullptr, 4); + void* runner = instance->thread_pool.get(); + + auto status = + JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner); + + if (status != JXL_DEC_SUCCESS) { + return report_error(1, "JxlDecoderSetParallelRunner failed"); + } + + status = JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE | + JXL_DEC_FRAME_PROGRESSION); + if (JXL_DEC_SUCCESS != status) { + return report_error(2, "JxlDecoderSubscribeEvents failed"); + } + + status = JxlDecoderSetProgressiveDetail(dec, kPasses); + if (JXL_DEC_SUCCESS != status) { + return report_error(3, "JxlDecoderSetProgressiveDetail failed"); + } + return instance; +} + +void jxlDestroyInstance(void* opaque_instance) { + if (opaque_instance == nullptr) return; + DecoderInstance* instance = + reinterpret_cast<DecoderInstance*>(opaque_instance); + if (instance->pixels) { + free(instance->pixels); + } + delete instance; +} + +uint32_t jxlProcessInput(void* opaque_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(); + + auto report_error = [&](int code, const char* text) { + fprintf(stderr, "%s\n", text); + // instance->result = code; + return static_cast<uint32_t>(code); + }; -/** Result: uint32_t 'size' followed by compressed image (JXL). */ -uint8_t* jxlCompress(const uint8_t* data, size_t size) { - jxl::PaddedBytes compressed; - jxl::CodecInOut io; - jxl::extras::Codec input_codec; - if (!jxl::SetFromBytes(jxl::Span<const uint8_t>(data, size), &io, nullptr, - &input_codec)) { - return nullptr; + std::vector<uint8_t>& tail = instance->tail; + if (!tail.empty()) { + tail.reserve(tail.size() + input_size); + tail.insert(tail.end(), input, input + input_size); + input = tail.data(); + input_size = tail.size(); } - jxl::CompressParams params; - jxl::PassesEncoderState passes_encoder_state; - if (!jxl::EncodeFile(params, &io, &passes_encoder_state, &compressed, - jxl::GetJxlCms(), nullptr, nullptr)) { - return nullptr; + + auto status = JxlDecoderSetInput(dec, input, input_size); + if (JXL_DEC_SUCCESS != status) { + return report_error(-2, "JxlDecoderSetInput failed"); + } + + auto release_input = [&]() { + size_t unused_input = JxlDecoderReleaseInput(dec); + if (unused_input == 0) { + tail.clear(); + return; + } + if (tail.empty()) { + tail.insert(tail.end(), input + input_size - unused_input, + input + input_size); + } else { + memmove(tail.data(), tail.data() + tail.size() - unused_input, + unused_input); + tail.resize(unused_input); + } + }; + + while (true) { + status = JxlDecoderProcessInput(dec); + if (JXL_DEC_SUCCESS == status) { + release_input(); + return 0; // ¯\_(ツ)_/¯ + } else if (JXL_DEC_FRAME_PROGRESSION == status) { + release_input(); + return 1; // ready to flush; client will decide whether it is necessary + } else if (JXL_DEC_NEED_MORE_INPUT == status) { + release_input(); + return 2; + } else if (JXL_DEC_FULL_IMAGE == status) { + release_input(); + return 0; // final image is ready + } else if (JXL_DEC_BASIC_INFO == status) { + JxlBasicInfo info; + status = JxlDecoderGetBasicInfo(dec, &info); + if (status != JXL_DEC_SUCCESS) { + release_input(); + return report_error(-4, "JxlDecoderGetBasicInfo failed"); + } + instance->width = info.xsize; + instance->height = info.ysize; + status = JxlDecoderImageOutBufferSize(dec, &instance->format, + &instance->pixels_size); + if (status != JXL_DEC_SUCCESS) { + release_input(); + return report_error(-6, "JxlDecoderImageOutBufferSize failed"); + } + if (instance->pixels) { + release_input(); + return report_error(-7, "Tried to realloc pixels"); + } + instance->pixels = + reinterpret_cast<uint8_t*>(malloc(instance->pixels_size)); + } else if (JXL_DEC_NEED_IMAGE_OUT_BUFFER == status) { + if (!instance->pixels) { + release_input(); + return report_error(-8, "Out buffer not allocated"); + } + status = JxlDecoderSetImageOutBuffer( + dec, &instance->format, instance->pixels, instance->pixels_size); + if (status != JXL_DEC_SUCCESS) { + release_input(); + return report_error(-9, "JxlDecoderSetImageOutBuffer failed"); + } + } else if (JXL_DEC_COLOR_ENCODING == status) { + JxlColorEncoding color_encoding; + 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 + ? JXL_TRANSFER_FUNCTION_SRGB + : JXL_TRANSFER_FUNCTION_PQ; + color_encoding.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL; + status = JxlDecoderSetPreferredColorProfile(dec, &color_encoding); + if (status != JXL_DEC_SUCCESS) { + release_input(); + return report_error(-5, "JxlDecoderSetPreferredColorProfile failed"); + } + } else { + release_input(); + return report_error(-3, "Unexpected decoder status"); + } } - size_t compressed_size = compressed.size(); - uint8_t* result = reinterpret_cast<uint8_t*>(malloc(compressed_size + 4)); - uint32_t* meta = reinterpret_cast<uint32_t*>(result); - meta[0] = compressed_size; - memcpy(result + 4, compressed.data(), compressed_size); - return result; + + release_input(); + return 0; } -/** Result: uint32_t 'size' followed by decompressed image (JPG). */ -uint8_t* jxlDecompress(const uint8_t* data, size_t size) { - jxl::PaddedBytes decompressed; - jxl::CodecInOut io; - jxl::DecompressParams params; - if (!jxl::DecodeFile(params, jxl::Span<const uint8_t>(data, size), &io, - nullptr)) { - return nullptr; +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(); + + auto report_error = [&](int code, const char* text) { + fprintf(stderr, "%s\n", text); + // instance->result = code; + return static_cast<uint32_t>(code); + }; + + if (!instance->pixels) { + return report_error(-2, "Not ready to flush"); } - io.use_sjpeg = false; - io.jpeg_quality = 100; - if (!jxl::Encode(io, jxl::extras::Codec::kJPG, io.Main().c_current(), 8, - &decompressed, nullptr)) { - return nullptr; + + auto status = JxlDecoderFlushImage(dec); + if (status != JXL_DEC_SUCCESS) { + return report_error(-3, "Failed to flush"); } - size_t decompressed_size = decompressed.size(); - uint8_t* result = reinterpret_cast<uint8_t*>(malloc(decompressed_size + 4)); - uint32_t* meta = reinterpret_cast<uint32_t*>(result); - meta[0] = decompressed_size; - memcpy(result + 4, decompressed.data(), decompressed_size); - return result; + + 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/media/libjxl/src/tools/jxlinfo.c b/media/libjxl/src/tools/jxlinfo.c index 0cced1740c..d8d67e7fad 100644 --- a/media/libjxl/src/tools/jxlinfo.c +++ b/media/libjxl/src/tools/jxlinfo.c @@ -91,9 +91,13 @@ int PrintBasicInfo(FILE* file, int verbose) { printf("float (%d exponent bits) ", info.exponent_bits_per_sample); } int cmyk = 0, alpha = 0; - const char* const ec_type_names[7] = {"Alpha", "Depth", "Spotcolor", - "Selection", "Black", "CFA", - "Thermal"}; + const char* const ec_type_names[] = { + "Alpha", "Depth", "Spotcolor", "Selection", "Black", + "CFA", "Thermal", "Reserved0", "Reserved1", "Reserved2", + "Reserved3", "Reserved4", "Reserved5", "Reserved6", "Reserved7", + "Unknown", "Optional"}; + const size_t ec_type_names_size = + sizeof(ec_type_names) / sizeof(ec_type_names[0]); for (uint32_t i = 0; i < info.num_extra_channels; i++) { JxlExtraChannelInfo extra; if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &extra)) { @@ -131,10 +135,9 @@ int PrintBasicInfo(FILE* file, int verbose) { continue; } - printf("+%s", (extra.type < 7 ? ec_type_names[extra.type] - : (extra.type == JXL_CHANNEL_OPTIONAL - ? "UnknownOptional" - : "Unknown(OUTDATED libjxl!)"))); + printf("+%s", (extra.type < ec_type_names_size + ? ec_type_names[extra.type] + : "Unknown, please update your libjxl")); } printf("\n"); if (verbose) { @@ -149,12 +152,9 @@ int PrintBasicInfo(FILE* file, int verbose) { break; } printf("extra channel %u:\n", i); - printf( - " type: %s\n", - (extra.type < 7 ? ec_type_names[extra.type] - : (extra.type == JXL_CHANNEL_OPTIONAL - ? "Unknown but can be ignored" - : "Unknown, please update your libjxl"))); + printf(" type: %s\n", (extra.type < ec_type_names_size + ? ec_type_names[extra.type] + : "Unknown, please update your libjxl")); printf(" bits_per_sample: %u\n", extra.bits_per_sample); if (extra.exponent_bits_per_sample > 0) { printf(" float, with exponent_bits_per_sample: %u\n", diff --git a/media/libjxl/src/tools/reference_zip.sh b/media/libjxl/src/tools/reference_zip.sh index 6a87344d2d..6a284b43f7 100755 --- a/media/libjxl/src/tools/reference_zip.sh +++ b/media/libjxl/src/tools/reference_zip.sh @@ -61,8 +61,8 @@ cd build cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_SJPEG=OFF .. cmake --build . -- -j\$(nproc) -tools/djxl ../third_party/testdata/jxl/blending/cropped_traffic_light.jxl test.png -tools/cjxl ../third_party/testdata/third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg test.jxl +tools/djxl ../testdata/jxl/blending/cropped_traffic_light.jxl test.png +tools/cjxl ../testdata/jxl/flower/flower.png.im_q85_444.jpg test.jxl tools/djxl test.jxl test.jpg EOF set +x diff --git a/media/libjxl/src/tools/roundtrip_test.sh b/media/libjxl/src/tools/roundtrip_test.sh new file mode 100644 index 0000000000..46b775645a --- /dev/null +++ b/media/libjxl/src/tools/roundtrip_test.sh @@ -0,0 +1,91 @@ +#!/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 cjxl and djxl tools. + +MYDIR=$(dirname $(realpath "$0")) +JPEGXL_TEST_DATA_PATH="${MYDIR}/../testdata" + +set -eux + +EMULATOR=${EMULATOR:-} + +# 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 + +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 + + if [ "${infn: -3}" == "jpg" ]; then + local outfn="$(mktemp -p "$tmpdir").jpg" + + # Test losless jpeg reconstruction. + "${decoder}" "${jxlfn}" "${outfn}" --num_reps 2 + diff "${infn}" "${outfn}" + + # Test decoding to pixels. + "${decoder}" "${jxlfn}" "${outfn}" --num_reps 2 --pixels_to_jpeg + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} > 0.0)" + python3 -c "import sys; sys.exit(not ${dist} < 0.005)" + + # Test decoding to pixels by setting the --jpeg_quality flag. + "${decoder}" "${jxlfn}" "${outfn}" --num_reps 2 --jpeg_quality 100 + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} > 0.0)" + python3 -c "import sys; sys.exit(not ${dist} < 0.005)" + + # Test decoding to pixels by writing to a png. + outfn="$(mktemp -p "$tmpdir").png" + "${decoder}" "${jxlfn}" "${outfn}" --num_reps 2 + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} > 0.0)" + python3 -c "import sys; sys.exit(not ${dist} < 0.005)" + else + # Test decoding to png. + local outfn="$(mktemp -p "$tmpdir").png" + "${decoder}" "${jxlfn}" "${outfn}" --num_reps 2 + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist})" + + # Test decoding to jpg. + outfn="$(mktemp -p "$tmpdir").jpg" + "${decoder}" "${jxlfn}" "${outfn}" --num_reps 2 + local dist="$("${comparator}" "${infn}" "${outfn}")" + python3 -c "import sys; sys.exit(not ${dist} <= ${maxdist} + 0.05)" + fi +} + +main() { + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + + local build_dir="${1:-}" + if [[ -z "${build_dir}" ]]; then + 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 + roundtrip_test "jxl/flower/flower_cropped.jpg" "-e 1" 0.0 +} + +main "$@" diff --git a/media/libjxl/src/tools/speed_stats.cc b/media/libjxl/src/tools/speed_stats.cc index 3ab271f964..cdef814df4 100644 --- a/media/libjxl/src/tools/speed_stats.cc +++ b/media/libjxl/src/tools/speed_stats.cc @@ -17,12 +17,13 @@ namespace jpegxl { namespace tools { void SpeedStats::NotifyElapsed(double elapsed_seconds) { - JXL_ASSERT(elapsed_seconds > 0.0); - elapsed_.push_back(elapsed_seconds); + if (elapsed_seconds > 0.0) { + elapsed_.push_back(elapsed_seconds); + } } -jxl::Status SpeedStats::GetSummary(SpeedStats::Summary* s) { - if (elapsed_.empty()) return JXL_FAILURE("Didn't call NotifyElapsed"); +bool SpeedStats::GetSummary(SpeedStats::Summary* s) { + if (elapsed_.empty()) return false; s->min = *std::min_element(elapsed_.begin(), elapsed_.end()); s->max = *std::max_element(elapsed_.begin(), elapsed_.end()); @@ -83,18 +84,18 @@ std::string SummaryStat(double value, const char* unit, const double value_min = value / s.max; const double value_max = value / s.min; - int ret = snprintf(stat_str, sizeof(stat_str), ",%s %.2f %s/s [%.2f, %.2f]", - s.type, value_tendency, unit, value_min, value_max); - (void)ret; // ret is unused when JXL_ASSERT is disabled. - JXL_ASSERT(ret < static_cast<int>(sizeof(stat_str))); + snprintf(stat_str, sizeof(stat_str), ",%s %.2f %s/s [%.2f, %.2f]", s.type, + value_tendency, unit, value_min, value_max); return stat_str; } } // namespace -jxl::Status SpeedStats::Print(size_t worker_threads) { +bool SpeedStats::Print(size_t worker_threads) { Summary s; - JXL_RETURN_IF_ERROR(GetSummary(&s)); + if (!GetSummary(&s)) { + return false; + } std::string mps_stats = SummaryStat(xsize_ * ysize_ * 1e-6, "MP", s); std::string mbs_stats = SummaryStat(file_size_ * 1e-6, "MB", s); diff --git a/media/libjxl/src/tools/speed_stats.h b/media/libjxl/src/tools/speed_stats.h index eec8a58586..870523f6f1 100644 --- a/media/libjxl/src/tools/speed_stats.h +++ b/media/libjxl/src/tools/speed_stats.h @@ -11,8 +11,6 @@ #include <vector> -#include "lib/jxl/base/status.h" - namespace jpegxl { namespace tools { @@ -32,7 +30,7 @@ class SpeedStats { }; // Non-const, may sort elapsed_. - jxl::Status GetSummary(Summary* summary); + bool GetSummary(Summary* summary); // Sets the image size to allow computing MP/s values. void SetImageSize(size_t xsize, size_t ysize) { @@ -45,7 +43,7 @@ class SpeedStats { // Calls GetSummary and prints megapixels/sec. SetImageSize() must be called // once before this can be used. - jxl::Status Print(size_t worker_threads); + bool Print(size_t worker_threads); private: std::vector<double> elapsed_; |