diff --git a/media/libjxl/moz.yaml b/media/libjxl/moz.yaml --- a/media/libjxl/moz.yaml +++ b/media/libjxl/moz.yaml @@ -8,13 +8,13 @@ name: libjxl description: JPEG XL image format reference implementation url: https://github.com/libjxl/libjxl - release: commit 318c592d98b97d103941b90d47107f06a10c71da (2022-03-21T20:44:46Z). + release: c0249504f1d343a1e0415c2d7af4578cd9ef3320 (2022-05-13T16:23:26Z). - revision: 318c592d98b97d103941b90d47107f06a10c71da + revision: c0249504f1d343a1e0415c2d7af4578cd9ef3320 license: Apache-2.0 license-file: LICENSE diff --git a/third_party/jpeg-xl/.github/workflows/build_test.yml b/third_party/jpeg-xl/.github/workflows/build_test.yml --- a/third_party/jpeg-xl/.github/workflows/build_test.yml +++ b/third_party/jpeg-xl/.github/workflows/build_test.yml @@ -453,14 +453,13 @@ wasm32_build: name: WASM wasm32/${{ matrix.variant }} runs-on: ubuntu-latest env: CCACHE_DIR: ${{ github.workspace }}/.ccache - EM_VERSION: 3.1.4 - V8_VERSION: 9.8.177 - V8: ${{ github.workspace }}/.jsvu/v8 BUILD_TARGET: wasm32 + EM_VERSION: 3.1.1 + NODE_VERSION: 18 strategy: matrix: include: - variant: scalar @@ -497,39 +496,43 @@ path: ${{ env.CCACHE_DIR }} key: ${{ runner.os }}-${{ github.sha }}-${{ matrix.variant }} restore-keys: | ${{ runner.os }}-${{ steps.git-env.outputs.parent }}-${{ matrix.variant }} + - name: Install node + uses: actions/setup-node@v3 + with: + node-version: ${{env.NODE_VERSION}} + + - name: Get non-EMSDK node path + run: which node >> $HOME/.base_node_path + - name: Install emsdk uses: mymindstorm/setup-emsdk@v11 # TODO(deymo): We could cache this action but it doesn't work when running # in a matrix. with: version: ${{env.EM_VERSION}} no-cache: true - - name: Install v8 + + - name: Set EMSDK node version run: | - npm install jsvu -g - HOME="${{ github.workspace }}" jsvu --os=linux64 "v8@${V8_VERSION}" - rm -f "${{ github.workspace }}/.jsvu/v8" - ln -s "${{ github.workspace }}/.jsvu/v8-${V8_VERSION}" \ - "${{ github.workspace }}/.jsvu/v8" + echo "NODE_JS='$(cat $HOME/.base_node_path)'" >> $EM_CONFIG + emsdk construct_env + # TODO(deymo): Build and install other dependencies like libpng, libjpeg, # etc. - name: Build run: | mkdir -p ${CCACHE_DIR} echo "max_size = 200M" > ${CCACHE_DIR}/ccache.conf if [[ "${{ matrix.variant }}" == "simd" ]]; then export ENABLE_WASM_SIMD=1 fi - emconfigure ./ci.sh release \ + ./ci.sh release \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \ - -DBUILD_SHARED_LIBS=OFF \ - -DJPEGXL_ENABLE_JNI=OFF + -DCMAKE_C_COMPILER_LAUNCHER=ccache env: SKIP_TEST: 1 - name: ccache stats run: ccache --show-stats @@ -537,6 +540,6 @@ if: | github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.names, 'CI:full')) run: | - emconfigure ./ci.sh test + ./ci.sh test diff --git a/third_party/jpeg-xl/AUTHORS b/third_party/jpeg-xl/AUTHORS --- a/third_party/jpeg-xl/AUTHORS +++ b/third_party/jpeg-xl/AUTHORS @@ -23,16 +23,18 @@ CanadianBaconBoi Daniel Novomeský David Burnett Dirk Lemstra Don Olmstead +Even Rouault Heiko Becker Jon Sneyers Kleis Auke Wolthuizen L. E. Segovia Leo Izen Lovell Fuller +Maarten DB Marcin Konicki Martin Strunz Mathieu Malaterre Misaki Kasumi Petr Diblík diff --git a/third_party/jpeg-xl/CMakeLists.txt b/third_party/jpeg-xl/CMakeLists.txt --- a/third_party/jpeg-xl/CMakeLists.txt +++ b/third_party/jpeg-xl/CMakeLists.txt @@ -232,10 +232,16 @@ "-Wl,--whole-archive;-lpthread;-Wl,--no-whole-archive") endif() endif() endif() # JPEGXL_STATIC +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() if (CXX_NO_RTTI_SUPPORTED) @@ -316,11 +322,11 @@ # Enable tests regardless of where they are defined. 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. add_subdirectory(lib) diff --git a/third_party/jpeg-xl/bash_test.sh b/third_party/jpeg-xl/bash_test.sh --- a/third_party/jpeg-xl/bash_test.sh +++ b/third_party/jpeg-xl/bash_test.sh @@ -101,10 +101,16 @@ echo "Don't use '%zu' or '%zd' in a format string, instead use " \ "'%\" PRIuS \"' or '%\" PRIdS \"'." >&2 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 if [ ! -e "$f" ]; then continue @@ -114,11 +120,11 @@ "types for code outside lib/jxl library." >&2 ret=1 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. # those may re-define PRIuS unconditionally causing a compile error. echo "$f: Don't use PRI.S in header files. Sorry." diff --git a/third_party/jpeg-xl/ci.sh b/third_party/jpeg-xl/ci.sh --- a/third_party/jpeg-xl/ci.sh +++ b/third_party/jpeg-xl/ci.sh @@ -39,26 +39,10 @@ # Time limit for the "fuzz" command in seconds (0 means no limit). 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} @@ -348,11 +332,10 @@ -B"${BUILD_DIR}" -H"${MYDIR}" -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" -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}" -DJPEGXL_VERSION="${JPEGXL_VERSION}" -DSANITIZER="${SANITIZER}" @@ -390,15 +373,18 @@ -DCMAKE_C_COMPILER_TARGET="${BUILD_TARGET}" -DCMAKE_CXX_COMPILER_TARGET="${BUILD_TARGET}" # 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+=( # These are needed to make googletest work when cross-compiling. -DCMAKE_CROSSCOMPILING=1 @@ -455,11 +441,15 @@ if [[ -n "${CMAKE_MAKE_PROGRAM}" ]]; then args+=( -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() { # gtest_discover_tests() runs the test binaries to discover the list of tests # at build time, which fails under qemu. @@ -492,11 +482,11 @@ # Configure the build to strip unused functions. This considerably reduces the # output size, specially for tests which only use a small part of the whole # 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 # discard all unreachable code, reducing the code size. For this to work, we # need to also pass --no-export-dynamic to prevent it from exporting all the @@ -1018,15 +1008,15 @@ local jxl_jpeg_benchmarks=( "--num_reps=3" ) 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 # Use a single cpu config in this case. local cpu_confs=("?") diff --git a/third_party/jpeg-xl/debian/copyright b/third_party/jpeg-xl/debian/copyright --- a/third_party/jpeg-xl/debian/copyright +++ b/third_party/jpeg-xl/debian/copyright @@ -36,47 +36,27 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (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/third_party/pngsuite/* Copyright: Willem van Schaik, 1996, 2011 License: PngSuite License See http://www.schaik.com/pngsuite/ for details. . 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/raw.pixls/* Copyright: their respective owners listed in https://raw.pixls.us/ License: CC0-1.0 -Files: third_party/testdata/raw.pixls/* +Files: testdata/raw.pixls/* 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/third_party/wide-gamut-tests/ Copyright: github.com/codelogic/wide-gamut-tests authors. License: Apache-2.0 License: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/third_party/jpeg-xl/js-wasm-wrapper.sh b/third_party/jpeg-xl/js-wasm-wrapper.sh deleted file mode 100755 --- a/third_party/jpeg-xl/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/third_party/jpeg-xl/lib/extras/codec.h b/third_party/jpeg-xl/lib/extras/codec.h --- a/third_party/jpeg-xl/lib/extras/codec.h +++ b/third_party/jpeg-xl/lib/extras/codec.h @@ -47,11 +47,11 @@ // Replaces "bytes" with an encoding of pixels transformed from c_current // 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* bytes, ThreadPool* pool = nullptr); // Deduces codec, calls Encode and writes to file. Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, size_t bits_per_sample, const std::string& pathname, ThreadPool* pool = nullptr); diff --git a/third_party/jpeg-xl/lib/extras/codec.cc b/third_party/jpeg-xl/lib/extras/codec.cc --- a/third_party/jpeg-xl/lib/extras/codec.cc +++ b/third_party/jpeg-xl/lib/extras/codec.cc @@ -50,20 +50,20 @@ } Status SetFromFile(const std::string& pathname, const extras::ColorHints& color_hints, CodecInOut* io, ThreadPool* pool, extras::Codec* orig_codec) { - PaddedBytes encoded; + std::vector encoded; JXL_RETURN_IF_ERROR(ReadFile(pathname, &encoded)); JXL_RETURN_IF_ERROR(SetFromBytes(Span(encoded), color_hints, io, pool, orig_codec)); return true; } Status Encode(const CodecInOut& io, const extras::Codec codec, const ColorEncoding& c_desired, size_t bits_per_sample, - PaddedBytes* bytes, ThreadPool* pool) { + std::vector* 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"); @@ -161,11 +161,11 @@ if (bits_per_sample > 16 && codec == extras::Codec::kPNG) { JXL_WARNING("PNG only supports up to 16 bits per sample"); bits_per_sample = 16; } - PaddedBytes encoded; + std::vector encoded; return Encode(io, codec, c_desired, bits_per_sample, &encoded, pool) && WriteFile(encoded, pathname); } Status EncodeToFile(const CodecInOut& io, const std::string& pathname, diff --git a/third_party/jpeg-xl/lib/extras/codec_test.cc b/third_party/jpeg-xl/lib/extras/codec_test.cc --- a/third_party/jpeg-xl/lib/extras/codec_test.cc +++ b/third_party/jpeg-xl/lib/extras/codec_test.cc @@ -10,27 +10,36 @@ #include #include #include -#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" #include "lib/jxl/color_management.h" #include "lib/jxl/enc_color_management.h" #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) { switch (codec) { case Codec::kJPG: @@ -124,11 +133,11 @@ const CodecInOut io = CreateTestImage(xsize, ysize, is_gray, add_alpha, bits_per_sample, c_native); const ImageBundle& ib1 = io.Main(); - PaddedBytes encoded; + std::vector encoded; JXL_CHECK(Encode(io, codec, c_external, bits_per_sample, &encoded, pool)); CodecInOut io2; ColorHints color_hints; // Only for PNM because PNG will warn about ignoring them. @@ -215,11 +224,11 @@ JXL_CHECK( SetFromBytes(Span(orig), color_hints, &io, pool, nullptr)); const ImageBundle& ib1 = io.Main(); // Encode/Decode again to make sure Encode carries through all metadata. - PaddedBytes encoded; + std::vector encoded; JXL_CHECK(Encode(io, Codec::kPNG, io.metadata.m.color_encoding, io.metadata.m.bit_depth.bits_per_sample, &encoded, pool)); CodecInOut io2; JXL_CHECK(SetFromBytes(Span(encoded), color_hints, &io2, pool, @@ -394,8 +403,77 @@ // Primaries::k2100, &pool); } TEST(CodecTest, TestPNM) { TestCodecPNM(); } +TEST(CodecTest, FormatNegotiation) { + const std::vector 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 png_encoder = Encoder::FromExtension(".png"); + ASSERT_THAT(png_encoder, NotNull()); + + const PaddedBytes original_png = ReadTestData( + "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + PackedPixelFile ppf; + ASSERT_TRUE(extras::DecodeBytes(Span(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(encoded_png.bitstreams.front()), + ColorHints(), SizeConstraints(), &decoded_ppf)); + + CodecInOut io1, io2; + ASSERT_TRUE(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io1)); + ASSERT_TRUE(ConvertPackedPixelFileToCodecInOut(decoded_ppf, pool, &io2)); + VerifyEqual(*io1.Main().color(), *io2.Main().color()); +} + } // namespace } // namespace extras } // namespace jxl diff --git a/third_party/jpeg-xl/lib/extras/enc/apng.h b/third_party/jpeg-xl/lib/extras/enc/apng.h --- a/third_party/jpeg-xl/lib/extras/enc/apng.h +++ b/third_party/jpeg-xl/lib/extras/enc/apng.h @@ -6,23 +6,26 @@ #ifndef LIB_EXTRAS_ENC_APNG_H_ #define LIB_EXTRAS_ENC_APNG_H_ // Encodes APNG images in memory. +#include "lib/extras/enc/encode.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" namespace jxl { namespace extras { +std::unique_ptr GetAPNGEncoder(); + // Encodes `io` into `bytes`. Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes); + std::vector* bytes); } // namespace extras } // namespace jxl #endif // LIB_EXTRAS_ENC_APNG_H_ diff --git a/third_party/jpeg-xl/lib/extras/enc/apng.cc b/third_party/jpeg-xl/lib/extras/enc/apng.cc --- a/third_party/jpeg-xl/lib/extras/enc/apng.cc +++ b/third_party/jpeg-xl/lib/extras/enc/apng.cc @@ -41,10 +41,11 @@ #include #include #include "jxl/encode.h" +#include "lib/extras/packed_image_convert.h" #include "lib/jxl/base/compiler_specific.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" @@ -59,13 +60,42 @@ namespace jxl { namespace extras { namespace { +class APNGEncoder : public Encoder { + public: + std::vector AcceptedFormats() const override { + std::vector 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 Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool) const override { + encoded_image->icc.clear(); + encoded_image->bitstreams.resize(1); + CodecInOut io; + JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io)); + return EncodeImageAPNG(&io, io.metadata.m.color_encoding, + ppf.info.bits_per_sample, pool, + &encoded_image->bitstreams.front()); + } +}; + static void PngWrite(png_structp png_ptr, png_bytep data, png_size_t length) { - PaddedBytes* bytes = static_cast(png_get_io_ptr(png_ptr)); - bytes->append(data, data + length); + std::vector* bytes = + static_cast*>(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: @@ -122,13 +152,17 @@ } }; } // namespace +std::unique_ptr GetAPNGEncoder() { + return jxl::make_unique(); +} + Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes) { + std::vector* 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; diff --git a/third_party/jpeg-xl/lib/extras/enc/encode.h b/third_party/jpeg-xl/lib/extras/enc/encode.h new file mode 100644 --- /dev/null +++ b/third_party/jpeg-xl/lib/extras/enc/encode.h @@ -0,0 +1,69 @@ +// Copyright (c) the JPEG XL Project 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 +#include + +#include "lib/extras/dec/decode.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 { + +struct EncodedImage { + // One (if the format supports animations or the image has only one frame) or + // more sequential bitstreams. + std::vector> bitstreams; + + // 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 icc; +}; + +class Encoder { + public: + static std::unique_ptr FromExtension(std::string extension); + + virtual ~Encoder() = default; + + virtual std::vector 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& options() const { + return options_; + } + + private: + std::unordered_map options_; +}; + +// TODO(sboukortt): consider exposing this as part of the C API. +Status SelectFormat(const std::vector& accepted_formats, + const JxlBasicInfo& basic_info, JxlPixelFormat* format); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ENC_ENCODE_H_ diff --git a/third_party/jpeg-xl/lib/extras/enc/encode.cc b/third_party/jpeg-xl/lib/extras/enc/encode.cc new file mode 100644 --- /dev/null +++ b/third_party/jpeg-xl/lib/extras/enc/encode.cc @@ -0,0 +1,84 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/enc/encode.h" + +#include + +#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/pgx.h" +#include "lib/extras/enc/pnm.h" +#include "lib/jxl/base/printf_macros.h" + +namespace jxl { +namespace extras { + +Status SelectFormat(const std::vector& accepted_formats, + const JxlBasicInfo& basic_info, JxlPixelFormat* format) { + const size_t original_bit_depth = basic_info.bits_per_sample; + const auto num_channels = + basic_info.num_color_channels + (basic_info.alpha_bits != 0 ? 1 : 0); + size_t current_bit_depth = 0; + 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) { + return JXL_FAILURE("no appropriate format found"); + } + 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::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") return GetAPNGEncoder(); +#endif + +#if JPEGXL_ENABLE_JPEG + if (extension == ".jpg") return GetJPEGEncoder(); + if (extension == ".jpeg") return GetJPEGEncoder(); +#endif + + if (extension == ".pgx") return GetPGXEncoder(); + + 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/third_party/jpeg-xl/lib/extras/enc/exr.h b/third_party/jpeg-xl/lib/extras/enc/exr.h --- a/third_party/jpeg-xl/lib/extras/enc/exr.h +++ b/third_party/jpeg-xl/lib/extras/enc/exr.h @@ -6,10 +6,11 @@ #ifndef LIB_EXTRAS_ENC_EXR_H_ #define LIB_EXTRAS_ENC_EXR_H_ // Encodes OpenEXR images in memory. +#include "lib/extras/enc/encode.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" @@ -17,14 +18,16 @@ #include "lib/jxl/color_encoding_internal.h" namespace jxl { namespace extras { +std::unique_ptr GetEXREncoder(); + // 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); + ThreadPool* pool, std::vector* bytes); } // namespace extras } // namespace jxl #endif // LIB_EXTRAS_ENC_EXR_H_ diff --git a/third_party/jpeg-xl/lib/extras/enc/exr.cc b/third_party/jpeg-xl/lib/extras/enc/exr.cc --- a/third_party/jpeg-xl/lib/extras/enc/exr.cc +++ b/third_party/jpeg-xl/lib/extras/enc/exr.cc @@ -10,10 +10,11 @@ #include #include #include +#include "lib/extras/packed_image_convert.h" #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" @@ -45,11 +46,11 @@ } class InMemoryOStream : public OpenEXR::OStream { public: // `bytes` must outlive the InMemoryOStream. - explicit InMemoryOStream(PaddedBytes* const bytes) + explicit InMemoryOStream(std::vector* const bytes) : OStream(/*fileName=*/""), bytes_(*bytes) {} void write(const char c[], const int n) override { if (bytes_.size() < pos_ + n) { bytes_.resize(pos_ + n); @@ -65,93 +66,90 @@ } pos_ = pos; } private: - PaddedBytes& bytes_; + std::vector& bytes_; size_t pos_ = 0; }; -} // namespace - -Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, - ThreadPool* pool, PaddedBytes* bytes) { +Status EncodeImageEXR(const ImageBundle& ib, const ColorEncoding& c_desired, + ThreadPool* pool, std::vector* 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; + ImageMetadata metadata = *ib.metadata(); ImageBundle store(&metadata); const ImageBundle* linear; - JXL_RETURN_IF_ERROR(TransformIfNeeded(io->Main(), c_linear, GetJxlCms(), pool, - &store, &linear)); + JXL_RETURN_IF_ERROR( + TransformIfNeeded(ib, c_linear, GetJxlCms(), pool, &store, &linear)); - const bool has_alpha = io->Main().HasAlpha(); - const bool alpha_is_premultiplied = io->Main().AlphaIsPremultiplied(); + const bool has_alpha = ib.HasAlpha(); + const bool alpha_is_premultiplied = ib.AlphaIsPremultiplied(); - OpenEXR::Header header(io->xsize(), io->ysize()); + OpenEXR::Header header(ib.xsize(), ib.ysize()); const PrimariesCIExy& primaries = c_linear.HasPrimaries() ? c_linear.GetPrimaries() : ColorEncoding::SRGB().GetPrimaries(); 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.white = Imath::V2f(c_linear.GetWhitePoint().x, c_linear.GetWhitePoint().y); OpenEXR::addChromaticities(header, chromaticities); - OpenEXR::addWhiteLuminance(header, io->metadata.m.IntensityTarget()); + OpenEXR::addWhiteLuminance(header, ib.metadata()->IntensityTarget()); // Ensure that the destructor of RgbaOutputFile has run before we look at the // size of `bytes`. { InMemoryOStream os(bytes); OpenEXR::RgbaOutputFile output( 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 output_rows(io->xsize() * y_chunk_size); + const int y_chunk_size = ib.ysize(); + std::vector output_rows(ib.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 < ib.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()); + const size_t end_y = std::min(start_y + y_chunk_size - 1, ib.ysize() - 1); + output.setFrameBuffer(output_rows.data() - start_y * ib.xsize(), + /*xStride=*/1, /*yStride=*/ib.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()]; + &output_rows[(y - start_y) * ib.xsize()]; if (has_alpha) { const float* const JXL_RESTRICT alpha_row = - io->Main().alpha().ConstRow(y); + ib.alpha().ConstRow(y); if (alpha_is_premultiplied) { - for (size_t x = 0; x < io->xsize(); ++x) { + for (size_t x = 0; x < ib.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) { + for (size_t x = 0; x < ib.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) { + for (size_t x = 0; x < ib.xsize(); ++x) { row_data[x] = OpenEXR::Rgba(input_rows[0][x], input_rows[1][x], input_rows[2][x], 1.f); } } }, @@ -161,7 +159,49 @@ } return true; } +class EXREncoder : public Encoder { + std::vector AcceptedFormats() const override { + std::vector 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 { + encoded_image->icc.clear(); + CodecInOut io; + JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io)); + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(io.frames.size()); + for (const ImageBundle& ib : io.frames) { + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImageEXR(ib, io.metadata.m.color_encoding, pool, + &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr GetEXREncoder() { + return jxl::make_unique(); +} + +Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, + ThreadPool* pool, std::vector* bytes) { + return EncodeImageEXR(io->Main(), c_desired, pool, bytes); +} + } // namespace extras } // namespace jxl diff --git a/third_party/jpeg-xl/lib/extras/enc/jpg.h b/third_party/jpeg-xl/lib/extras/enc/jpg.h --- a/third_party/jpeg-xl/lib/extras/enc/jpg.h +++ b/third_party/jpeg-xl/lib/extras/enc/jpg.h @@ -8,11 +8,11 @@ // Encodes JPG pixels and metadata in memory. #include -#include "lib/extras/codec.h" +#include "lib/extras/enc/encode.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" @@ -23,14 +23,16 @@ enum class JpegEncoder { kLibJpeg, kSJpeg, }; +std::unique_ptr GetJPEGEncoder(); + // Encodes into `bytes`. Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, YCbCrChromaSubsampling chroma_subsampling, - ThreadPool* pool, PaddedBytes* bytes); + ThreadPool* pool, std::vector* bytes); } // namespace extras } // namespace jxl #endif // LIB_EXTRAS_ENC_JPG_H_ diff --git a/third_party/jpeg-xl/lib/extras/enc/jpg.cc b/third_party/jpeg-xl/lib/extras/enc/jpg.cc --- a/third_party/jpeg-xl/lib/extras/enc/jpg.cc +++ b/third_party/jpeg-xl/lib/extras/enc/jpg.cc @@ -10,13 +10,15 @@ #include #include #include #include +#include #include #include +#include "lib/extras/packed_image_convert.h" #include "lib/jxl/base/compiler_specific.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" @@ -91,29 +93,27 @@ chroma_subsampling.VShift(i < 2 ? i ^ 1 : i)); } return true; } -} // namespace - -Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, +Status EncodeWithLibJpeg(const ImageBundle& ib, std::vector exif, size_t quality, const YCbCrChromaSubsampling& chroma_subsampling, - PaddedBytes* bytes) { + std::vector* bytes) { jpeg_compress_struct cinfo; // cinfo is initialized by libjpeg, which we are not instrumenting with // msan. msan::UnpoisonMemory(&cinfo, sizeof(cinfo)); jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); 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.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; @@ -123,30 +123,29 @@ if (cinfo.input_components == 3) { JXL_RETURN_IF_ERROR(SetChromaSubsampling(chroma_subsampling, &cinfo)); } jpeg_set_quality(&cinfo, quality, TRUE); jpeg_start_compress(&cinfo, TRUE); - if (!ib->IsSRGB()) { - WriteICCProfile(&cinfo, ib->c_current().ICC()); + if (!ib.IsSRGB()) { + WriteICCProfile(&cinfo, ib.c_current().ICC()); } - if (!io->blobs.exif.empty()) { - std::vector 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()); + ib.oriented_xsize() * cinfo.input_components * sizeof(JSAMPLE); + std::vector raw_bytes(stride * ib.oriented_ysize()); JXL_RETURN_IF_ERROR(ConvertToExternal( - *ib, BITS_IN_JSAMPLE, /*float_out=*/false, cinfo.input_components, + 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())); + /*out_callback=*/{}, ib.metadata()->GetOrientation())); - for (size_t y = 0; y < ib->oriented_ysize(); ++y) { + for (size_t y = 0; y < ib.oriented_ysize(); ++y) { JSAMPROW row[] = {raw_bytes.data() + y * stride}; jpeg_write_scanlines(&cinfo, row, 1); } jpeg_finish_compress(&cinfo); @@ -158,23 +157,22 @@ std::copy_n(buffer, size, bytes->data()); std::free(buffer); return true; } -Status EncodeWithSJpeg(const ImageBundle* ib, const CodecInOut* io, +Status EncodeWithSJpeg(const ImageBundle& ib, std::vector exif, size_t quality, const YCbCrChromaSubsampling& chroma_subsampling, - PaddedBytes* bytes) { + std::vector* 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 (!ib.IsSRGB()) { + param.iccp.assign(ib.metadata()->color_encoding.ICC().begin(), + ib.metadata()->color_encoding.ICC().end()); } - std::vector exif = io->blobs.exif; if (!exif.empty()) { ResetExifOrientation(exif); param.exif.assign(exif.begin(), exif.end()); } if (chroma_subsampling.Is444()) { @@ -182,58 +180,143 @@ } else if (chroma_subsampling.Is420()) { 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); + size_t stride = ib.oriented_xsize() * 3; + std::vector rgb(ib.xsize() * ib.ysize() * 3); JXL_RETURN_IF_ERROR( - ConvertToExternal(*ib, 8, /*float_out=*/false, 3, JXL_BIG_ENDIAN, stride, + ConvertToExternal(ib, 8, /*float_out=*/false, 3, JXL_BIG_ENDIAN, stride, nullptr, rgb.data(), rgb.size(), - /*out_callback=*/{}, ib->metadata()->GetOrientation())); + /*out_callback=*/{}, ib.metadata()->GetOrientation())); std::string output; - JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib->oriented_xsize(), - ib->oriented_ysize(), stride, param, + JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib.oriented_xsize(), + ib.oriented_ysize(), stride, param, &output)); bytes->assign( reinterpret_cast(output.data()), reinterpret_cast(output.data() + output.size())); return true; #endif } -Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, +Status EncodeImageJPG(const ImageBundle& ib, std::vector exif, + JpegEncoder encoder, size_t quality, YCbCrChromaSubsampling chroma_subsampling, - ThreadPool* pool, PaddedBytes* bytes) { - if (io->Main().HasAlpha()) { + ThreadPool* pool, std::vector* bytes) { + if (ib.HasAlpha()) { 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; + const ImageBundle* transformed; + ImageMetadata metadata = *ib.metadata(); ImageBundle ib_store(&metadata); - JXL_RETURN_IF_ERROR(TransformIfNeeded(io->Main(), - io->metadata.m.color_encoding, - GetJxlCms(), pool, &ib_store, &ib)); + JXL_RETURN_IF_ERROR(TransformIfNeeded( + ib, metadata.color_encoding, GetJxlCms(), pool, &ib_store, &transformed)); switch (encoder) { case JpegEncoder::kLibJpeg: - JXL_RETURN_IF_ERROR( - EncodeWithLibJpeg(ib, io, quality, chroma_subsampling, bytes)); + JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(ib, 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(ib, std::move(exif), quality, + chroma_subsampling, bytes)); break; default: return JXL_FAILURE("tried to use an unknown JPEG encoder"); } return true; } +Status ParseChromaSubsampling(const std::string& param, + YCbCrChromaSubsampling* subsampling) { + const std::pair, std::array>> + 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) { + return subsampling->Set(option.second.first.data(), + option.second.second.data()); + } + } + return false; +} + +class JPEGEncoder : public Encoder { + std::vector AcceptedFormats() const override { + std::vector 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 { + 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(is >> quality)); + } + YCbCrChromaSubsampling chroma_subsampling; + auto it_chroma_subsampling = options.find("chroma_subsampling"); + if (it_chroma_subsampling != options.end()) { + JXL_RETURN_IF_ERROR(ParseChromaSubsampling(it_chroma_subsampling->second, + &chroma_subsampling)); + } + 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()); + } + } + CodecInOut io; + JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io)); + encoded_image->icc = ppf.icc; + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(io.frames.size()); + for (const ImageBundle& ib : io.frames) { + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImageJPG(ib, ppf.metadata.exif, jpeg_encoder, + quality, chroma_subsampling, pool, + &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr GetJPEGEncoder() { + return jxl::make_unique(); +} + +Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, + YCbCrChromaSubsampling chroma_subsampling, + ThreadPool* pool, std::vector* bytes) { + return EncodeImageJPG(io->Main(), io->blobs.exif, encoder, quality, + chroma_subsampling, pool, bytes); +} + } // namespace extras } // namespace jxl diff --git a/third_party/jpeg-xl/lib/extras/enc/pgx.h b/third_party/jpeg-xl/lib/extras/enc/pgx.h --- a/third_party/jpeg-xl/lib/extras/enc/pgx.h +++ b/third_party/jpeg-xl/lib/extras/enc/pgx.h @@ -9,10 +9,11 @@ // Encodes PGX pixels in memory. #include #include +#include "lib/extras/enc/encode.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" @@ -20,14 +21,16 @@ #include "lib/jxl/color_encoding_internal.h" namespace jxl { namespace extras { +std::unique_ptr GetPGXEncoder(); + // 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::vector* bytes); } // namespace extras } // namespace jxl #endif // LIB_EXTRAS_ENC_PGX_H_ diff --git a/third_party/jpeg-xl/lib/extras/enc/pgx.cc b/third_party/jpeg-xl/lib/extras/enc/pgx.cc --- a/third_party/jpeg-xl/lib/extras/enc/pgx.cc +++ b/third_party/jpeg-xl/lib/extras/enc/pgx.cc @@ -6,10 +6,11 @@ #include "lib/extras/enc/pgx.h" #include #include +#include "lib/extras/packed_image_convert.h" #include "lib/jxl/base/bits.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/file_io.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/color_management.h" @@ -45,33 +46,29 @@ JXL_RETURN_IF_ERROR(static_cast(*chars_written) < kMaxHeaderSize); return true; } -} // namespace - -Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, +Status EncodeImagePGX(const ImageBundle& ib, const ColorEncoding& c_desired, size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes) { - if (!Bundle::AllDefault(io->metadata.m)) { + std::vector* bytes) { + if (!Bundle::AllDefault(*ib.metadata())) { 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"); } - ImageBundle ib = io->Main().Copy(); - - ImageMetadata metadata = io->metadata.m; + ImageMetadata metadata = *ib.metadata(); 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)); + std::vector 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, @@ -87,7 +84,51 @@ memcpy(bytes->data() + header_size, pixels.data(), pixels.size()); return true; } +class PGXEncoder : public Encoder { + public: + std::vector AcceptedFormats() const override { + std::vector 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 { + CodecInOut io; + JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io)); + const PaddedBytes& icc = io.Main().c_current().ICC(); + encoded_image->icc.assign(icc.begin(), icc.end()); + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(io.frames.size()); + for (const ImageBundle& ib : io.frames) { + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImagePGX(ib, io.metadata.m.color_encoding, + ppf.info.bits_per_sample, pool, + &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr GetPGXEncoder() { + return jxl::make_unique(); +} + +Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, + size_t bits_per_sample, ThreadPool* pool, + std::vector* bytes) { + return EncodeImagePGX(io->Main(), c_desired, bits_per_sample, pool, bytes); +} + } // namespace extras } // namespace jxl diff --git a/third_party/jpeg-xl/lib/extras/enc/pnm.h b/third_party/jpeg-xl/lib/extras/enc/pnm.h --- a/third_party/jpeg-xl/lib/extras/enc/pnm.h +++ b/third_party/jpeg-xl/lib/extras/enc/pnm.h @@ -9,20 +9,25 @@ // Encodes/decodes PBM/PGM/PPM/PFM pixels in memory. // TODO(janwas): workaround for incorrect Win64 codegen (cause unknown) #include +#include "lib/extras/enc/encode.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/status.h" #include "lib/jxl/codec_in_out.h" #include "lib/jxl/color_encoding_internal.h" namespace jxl { namespace extras { +std::unique_ptr GetPGMEncoder(); +std::unique_ptr GetPPMEncoder(); +std::unique_ptr GetPFMEncoder(); + // 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* bytes); diff --git a/third_party/jpeg-xl/lib/extras/enc/pnm.cc b/third_party/jpeg-xl/lib/extras/enc/pnm.cc --- a/third_party/jpeg-xl/lib/extras/enc/pnm.cc +++ b/third_party/jpeg-xl/lib/extras/enc/pnm.cc @@ -30,10 +30,78 @@ namespace extras { namespace { constexpr size_t kMaxHeaderSize = 200; +class PNMEncoder : public Encoder { + public: + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + encoded_image->icc = ppf.icc; + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (size_t i = 0; i < ppf.frames.size(); ++i) { + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImagePNM(ppf, ppf.info.bits_per_sample, pool, + /*frame_index=*/i, + &encoded_image->bitstreams.back())); + } + return true; + } +}; + +class PPMEncoder : public PNMEncoder { + public: + std::vector AcceptedFormats() const override { + std::vector 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; + } +}; + +class PFMEncoder : public PNMEncoder { + public: + std::vector AcceptedFormats() const override { + std::vector 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; + } +}; + +class PGMEncoder : public PPMEncoder { + public: + std::vector AcceptedFormats() const override { + std::vector formats = PPMEncoder::AcceptedFormats(); + for (auto it = formats.begin(); it != formats.end();) { + if (it->num_channels > 2) { + it = formats.erase(it); + } else { + ++it; + } + } + return formats; + } +}; + 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 = @@ -94,10 +162,22 @@ } } } // namespace +std::unique_ptr GetPPMEncoder() { + return jxl::make_unique(); +} + +std::unique_ptr GetPFMEncoder() { + return jxl::make_unique(); +} + +std::unique_ptr GetPGMEncoder() { + return jxl::make_unique(); +} + Status EncodeImagePNM(const PackedPixelFile& ppf, size_t bits_per_sample, ThreadPool* pool, size_t frame_index, std::vector* bytes) { const bool floating_point = bits_per_sample > 16; // Choose native for PFM; PGM/PPM require big-endian diff --git a/third_party/jpeg-xl/lib/extras/tone_mapping_gbench.cc b/third_party/jpeg-xl/lib/extras/tone_mapping_gbench.cc --- a/third_party/jpeg-xl/lib/extras/tone_mapping_gbench.cc +++ b/third_party/jpeg-xl/lib/extras/tone_mapping_gbench.cc @@ -11,12 +11,11 @@ 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(image_bytes), &image)); // Convert to linear Rec. 2020 so that `ToneMapTo` doesn't have to and we // mainly measure the tone mapping itself. ColorEncoding linear_rec2020; diff --git a/third_party/jpeg-xl/lib/include/jxl/color_encoding.h b/third_party/jpeg-xl/lib/include/jxl/color_encoding.h --- a/third_party/jpeg-xl/lib/include/jxl/color_encoding.h +++ b/third_party/jpeg-xl/lib/include/jxl/color_encoding.h @@ -88,11 +88,11 @@ JXL_TRANSFER_FUNCTION_UNKNOWN = 2, /** The gamma exponent is 1 */ 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, /** As specified in Rec. ITU-R BT.2100-1 (HLG) */ JXL_TRANSFER_FUNCTION_HLG = 18, diff --git a/third_party/jpeg-xl/lib/include/jxl/decode.h b/third_party/jpeg-xl/lib/include/jxl/decode.h --- a/third_party/jpeg-xl/lib/include/jxl/decode.h +++ b/third_party/jpeg-xl/lib/include/jxl/decode.h @@ -1277,21 +1277,20 @@ 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. + * 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 JXL_DEC_FRAME_PROGRESSION + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as an + * invalid value for the progressive detail. */ -JXL_EXPORT void JxlDecoderSetProgressiveDetail(JxlDecoder* dec, - uint32_t detail); +JXL_EXPORT JxlDecoderStatus +JxlDecoderSetProgressiveDetail(JxlDecoder* dec, JxlProgressiveDetail detail); /** * Outputs progressive step towards the decoded image so far when only partial * input was received. If the flush was successful, the buffer set with * JxlDecoderSetImageOutBuffer will contain partial image data. diff --git a/third_party/jpeg-xl/lib/include/jxl/encode.h b/third_party/jpeg-xl/lib/include/jxl/encode.h --- a/third_party/jpeg-xl/lib/include/jxl/encode.h +++ b/third_party/jpeg-xl/lib/include/jxl/encode.h @@ -71,19 +71,64 @@ */ JXL_ENC_NEED_MORE_OUTPUT = 2, /** 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 * values are, from faster to slower speed: 1:lightning 2:thunder 3:falcon * 4:cheetah 5:hare 6:wombat 7:squirrel 8:kitten 9:tortoise. @@ -234,13 +279,16 @@ * YCbCr both perform no transform, but YCbCr is used to indicate that the * encoded data losslessly represents YCbCr values. */ 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-48=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, /** Group size for modular encoding: -1=default, 0=128, 1=256, 2=512, 3=1024. */ @@ -353,10 +401,19 @@ JXL_EXPORT JxlEncoderStatus 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 * processed by the encoder and *next_out will be incremented by the same * amount, so *next_out will now point at the amount of *avail_out unprocessed @@ -713,10 +770,11 @@ /** * Sets the original color encoding of the image encoded by this encoder. This * 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 * copied internally. * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR or @@ -728,10 +786,11 @@ /** * Sets the original color encoding of the image encoded by this encoder as an * 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 * @param size size of the icc_profile buffer in bytes * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR or diff --git a/third_party/jpeg-xl/lib/include/jxl/types.h b/third_party/jpeg-xl/lib/include/jxl/types.h --- a/third_party/jpeg-xl/lib/include/jxl/types.h +++ b/third_party/jpeg-xl/lib/include/jxl/types.h @@ -113,10 +113,36 @@ /** Data type holding the 4-character type name of an ISOBMFF box. */ 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) + * - 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 #endif /* JXL_TYPES_H_ */ diff --git a/third_party/jpeg-xl/lib/jxl.cmake b/third_party/jpeg-xl/lib/jxl.cmake --- a/third_party/jpeg-xl/lib/jxl.cmake +++ b/third_party/jpeg-xl/lib/jxl.cmake @@ -398,14 +398,14 @@ add_library(jxl_dec-obj OBJECT ${JPEGXL_INTERNAL_SOURCES_DEC}) 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_compile_definitions(jxl_dec-obj PUBLIC ${OBJ_COMPILE_DEFINITIONS} ) if (JPEGXL_ENABLE_PROFILER) @@ -468,13 +468,13 @@ $ ) 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") + "$" + "$" + "$") # The list of objects in the static and shared libraries. set(JPEGXL_INTERNAL_OBJECTS $ $ @@ -489,13 +489,13 @@ # to do, remove $ here and depend on jxl_dec-static 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") + "$" + "$" + "$") # JXL_EXPORT is defined to "__declspec(dllimport)" automatically by CMake # in Windows builds when including headers from the C API and compiling from # outside the jxl library. This is required when using the shared library, # however in windows this causes the function to not be found when linking @@ -541,12 +541,12 @@ strip_static(JPEGXL_INTERNAL_SHARED_LIBS JPEGXL_INTERNAL_LIBS) 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") + "$" + "$") set_target_properties(jxl PROPERTIES VERSION ${JPEGXL_LIBRARY_VERSION} SOVERSION ${JPEGXL_LIBRARY_SOVERSION} LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") @@ -614,9 +614,23 @@ set(JPEGXL_LIBRARY_REQUIRES "libhwy libbrotlicommon libbrotlienc libbrotlidec") 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" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") diff --git a/third_party/jpeg-xl/lib/jxl/alpha_test.cc b/third_party/jpeg-xl/lib/jxl/alpha_test.cc --- a/third_party/jpeg-xl/lib/jxl/alpha_test.cc +++ b/third_party/jpeg-xl/lib/jxl/alpha_test.cc @@ -3,12 +3,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #include "lib/jxl/alpha.h" -#include "gmock/gmock.h" -#include "gtest/gtest.h" +#include "lib/jxl/test_utils.h" namespace jxl { namespace { using ::testing::_; diff --git a/third_party/jpeg-xl/lib/jxl/base/file_io.h b/third_party/jpeg-xl/lib/jxl/base/file_io.h --- a/third_party/jpeg-xl/lib/jxl/base/file_io.h +++ b/third_party/jpeg-xl/lib/jxl/base/file_io.h @@ -75,11 +75,12 @@ template 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(); if (size < 0) { // Size is unknown, loop reading chunks until EOF. diff --git a/third_party/jpeg-xl/lib/jxl/blending_test.cc b/third_party/jpeg-xl/lib/jxl/blending_test.cc --- a/third_party/jpeg-xl/lib/jxl/blending_test.cc +++ b/third_party/jpeg-xl/lib/jxl/blending_test.cc @@ -1,15 +1,14 @@ // Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -#include "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 { namespace { diff --git a/third_party/jpeg-xl/lib/jxl/color_management_test.cc b/third_party/jpeg-xl/lib/jxl/color_management_test.cc --- a/third_party/jpeg-xl/lib/jxl/color_management_test.cc +++ b/third_party/jpeg-xl/lib/jxl/color_management_test.cc @@ -11,12 +11,10 @@ #include #include #include #include -#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" #include "lib/jxl/base/thread_pool_internal.h" #include "lib/jxl/enc_color_management.h" diff --git a/third_party/jpeg-xl/lib/jxl/dec_cache.h b/third_party/jpeg-xl/lib/jxl/dec_cache.h --- a/third_party/jpeg-xl/lib/jxl/dec_cache.h +++ b/third_party/jpeg-xl/lib/jxl/dec_cache.h @@ -6,10 +6,11 @@ #ifndef LIB_JXL_DEC_CACHE_H_ #define LIB_JXL_DEC_CACHE_H_ #include +#include #include // HWY_ALIGN_MAX #include "jxl/decode.h" #include "lib/jxl/ac_strategy.h" #include "lib/jxl/base/profiler.h" diff --git a/third_party/jpeg-xl/lib/jxl/dec_cache.cc b/third_party/jpeg-xl/lib/jxl/dec_cache.cc --- a/third_party/jpeg-xl/lib/jxl/dec_cache.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_cache.cc @@ -87,11 +87,13 @@ } } } 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)); } diff --git a/third_party/jpeg-xl/lib/jxl/dec_frame.h b/third_party/jpeg-xl/lib/jxl/dec_frame.h --- a/third_party/jpeg-xl/lib/jxl/dec_frame.h +++ b/third_party/jpeg-xl/lib/jxl/dec_frame.h @@ -7,10 +7,11 @@ #define LIB_JXL_DEC_FRAME_H_ #include #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" #include "lib/jxl/base/status.h" #include "lib/jxl/blending.h" @@ -133,17 +134,25 @@ // 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_; } + 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 // will only occur if supported (that is, flushing will produce a valid // 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; } + void SetPauseAtProgressive(JxlProgressiveDetail prog_detail) { + pause_at_progressive_ = true; + progressive_detail_ = prog_detail; + } // 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. // If it does not succeed, the image is decoded to the ImageBundle passed to // InitFrame instead. @@ -284,10 +293,11 @@ // Testing setting: whether or not to use the slow rendering pipeline. bool use_slow_rendering_pipeline_; bool pause_at_progressive_ = false; + JxlProgressiveDetail progressive_detail_ = kDC; }; } // namespace jxl #endif // LIB_JXL_DEC_FRAME_H_ diff --git a/third_party/jpeg-xl/lib/jxl/dec_frame.cc b/third_party/jpeg-xl/lib/jxl/dec_frame.cc --- a/third_party/jpeg-xl/lib/jxl/dec_frame.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_frame.cc @@ -13,10 +13,11 @@ #include #include #include #include +#include "jxl/types.h" #include "lib/jxl/ac_context.h" #include "lib/jxl/ac_strategy.h" #include "lib/jxl/ans_params.h" #include "lib/jxl/base/bits.h" #include "lib/jxl/base/compiler_specific.h" @@ -654,21 +655,23 @@ size_t ac_global_sec = num; std::vector dc_group_sec(frame_dim_.num_dc_groups, num); std::vector> ac_group_sec( frame_dim_.num_groups, std::vector(frame_header_.passes.num_passes, num)); - std::vector 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 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) { JXL_ASSERT(num == 1); JXL_ASSERT(sections[0].id == 0); if (processed_section_[0] == false) { 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; } } else { size_t ac_global_index = frame_dim_.num_dc_groups + 1; @@ -704,11 +707,11 @@ for (; j + decoded_passes_per_ac_group_[g] < max_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) { Status dc_global_status = ProcessDCGlobal(sections[dc_global_sec].br); if (dc_global_status.IsFatalError()) return dc_global_status; @@ -735,59 +738,72 @@ }, "DecodeDCGroup")); } if (has_error) return JXL_FAILURE("Error in DC group"); + bool do_progressive = false; + if (pause_at_progressive_ && (frame_header_.frame_type != kSkipProgressive)) { + do_progressive = 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. + do_progressive = 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) + // TOOD(firsching): Check if this is still the case. + do_progressive = 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. + do_progressive = false; + } + } if (*std::min_element(decoded_dc_groups_.begin(), decoded_dc_groups_.end()) && !finalized_dc_) { PassesDecoderState::PipelineOptions pipeline_options; pipeline_options.use_slow_render_pipeline = use_slow_rendering_pipeline_; pipeline_options.coalescing = coalescing_; pipeline_options.render_spotcolors = render_spotcolors_; JXL_RETURN_IF_ERROR( 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 (do_progressive && progressive_detail_ >= JxlProgressiveDetail::kDC) { + MarkSections(sections, num, section_status); + return true; } } if (finalized_dc_ && ac_global_sec != num && !decoded_ac_global_) { JXL_RETURN_IF_ERROR(ProcessACGlobal(sections[ac_global_sec].br)); section_status[ac_global_sec] = SectionStatus::kDone; } + if (do_progressive && progressive_detail_ >= JxlProgressiveDetail::kPasses) { + // Mark that we only want the next pass. + size_t num_complete_passes = NumCompletePasses(); + for (size_t i = 0; i < ac_group_sec.size(); i++) { + desired_num_ac_passes[i] = + std::min(desired_num_ac_passes[i], + 1 + num_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()) { + if (desired_num_ac_passes[i] == 0 && + !modular_frame_decoder_.UsesFullImage()) { continue; } dec_state_->render_pipeline->ClearDone(i); } @@ -795,28 +811,29 @@ pool_, 0, ac_group_sec.size(), [this](size_t num_threads) { 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; } } }, diff --git a/third_party/jpeg-xl/lib/jxl/dec_modular.cc b/third_party/jpeg-xl/lib/jxl/dec_modular.cc --- a/third_party/jpeg-xl/lib/jxl/dec_modular.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_modular.cc @@ -5,10 +5,11 @@ #include "lib/jxl/dec_modular.h" #include +#include #include #include "lib/jxl/frame_header.h" #undef HWY_TARGET_INCLUDE diff --git a/third_party/jpeg-xl/lib/jxl/decode.cc b/third_party/jpeg-xl/lib/jxl/decode.cc --- a/third_party/jpeg-xl/lib/jxl/decode.cc +++ b/third_party/jpeg-xl/lib/jxl/decode.cc @@ -3,19 +3,21 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #include "jxl/decode.h" +#include "jxl/types.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/span.h" #include "lib/jxl/base/status.h" #include "lib/jxl/box_content_decoder.h" #include "lib/jxl/dec_external_image.h" #include "lib/jxl/dec_frame.h" #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" #include "lib/jxl/loop_filter.h" #include "lib/jxl/memory_manager_internal.h" @@ -449,10 +451,13 @@ // Fields for reading the basic info from the header. size_t basic_info_size_hint; bool have_container; size_t box_count; + // The level of progressive detail in frame decoding. + JxlProgressiveDetail prog_detail = kDC; + // 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. @@ -1393,11 +1398,11 @@ 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(); + dec->frame_dec->SetPauseAtProgressive(dec->prog_detail); } // If JPEG reconstruction is wanted and possible, set the jpeg_data of // the ImageBundle. if (!dec->jpeg_decoder.SetImageBundleJpegData(dec->ib.get())) @@ -1427,10 +1432,11 @@ } bool return_full_image = false; if (dec->frame_stage == FrameStage::kFull) { + size_t num_complete_passes = 0; if (dec->events_wanted & JXL_DEC_FULL_IMAGE) { if (!dec->image_out_buffer_set && (!dec->jpeg_decoder.IsOutputSet() || dec->ib->jpeg_data == nullptr) && dec->is_last_of_still) { @@ -1510,11 +1516,32 @@ bool got_dc_only = !!status && !all_sections_done && dec->frame_dec->HasDecodedDC(); if ((dec->events_wanted & JXL_DEC_FRAME_PROGRESSION) && got_dc_only) { - dec->events_wanted &= ~JXL_DEC_FRAME_PROGRESSION; + if (dec->prog_detail <= JxlProgressiveDetail::kDC) { + dec->events_wanted &= ~JXL_DEC_FRAME_PROGRESSION; + } + if (dec->frame_header->frame_type != kSkipProgressive) { + return JXL_DEC_FRAME_PROGRESSION; + } + } + + size_t new_num_complete_passes = dec->frame_dec->NumCompletePasses(); + + bool new_group_done = num_complete_passes < new_num_complete_passes; + if (new_group_done) { + num_complete_passes = new_num_complete_passes; + } + + bool got_complete_group_only = + !!status && !all_sections_done && new_group_done; + + if ((dec->events_wanted & JXL_DEC_FRAME_PROGRESSION) && + got_complete_group_only && + (dec->frame_header->frame_type != kSkipProgressive) && + (dec->prog_detail <= JxlProgressiveDetail::kPasses)) { return JXL_DEC_FRAME_PROGRESSION; } if (!all_sections_done) { // Not all sections have been processed yet @@ -2179,10 +2206,12 @@ JxlDecoderStatus JxlDecoderGetBasicInfo(const JxlDecoder* dec, JxlBasicInfo* info) { 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; info->xsize = dec->metadata.size.xsize(); info->ysize = dec->metadata.size.ysize(); @@ -2699,10 +2728,11 @@ JxlFrameHeader* header) { if (!dec->frame_header || dec->frame_stage == FrameStage::kHeader) { 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) { header->timecode = dec->frame_header->animation_frame.timecode; } @@ -2880,5 +2910,17 @@ if (size) { *size = dec->box_size; } return JXL_DEC_SUCCESS; } + +JxlDecoderStatus JxlDecoderSetProgressiveDetail(JxlDecoder* dec, + JxlProgressiveDetail detail) { + if (detail != kDC && detail != kPasses) { + return JXL_API_ERROR( + "Values other than kDC (%d) and kPasses (%d), like %d are not " + "implemented.", + kDC, kPasses, detail); + } + dec->prog_detail = detail; + return JXL_DEC_SUCCESS; +} diff --git a/third_party/jpeg-xl/lib/jxl/decode_test.cc b/third_party/jpeg-xl/lib/jxl/decode_test.cc --- a/third_party/jpeg-xl/lib/jxl/decode_test.cc +++ b/third_party/jpeg-xl/lib/jxl/decode_test.cc @@ -15,10 +15,11 @@ #include "gtest/gtest.h" #include "jxl/decode_cxx.h" #include "jxl/resizable_parallel_runner_cxx.h" #include "jxl/thread_parallel_runner_cxx.h" +#include "jxl/types.h" #include "lib/extras/enc/jpg.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/file_io.h" #include "lib/jxl/base/span.h" #include "lib/jxl/base/status.h" @@ -201,11 +202,11 @@ /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN, /*flipped_y=*/false, &pool, &io.Main(), /*float_in=*/false, /*align=*/0)); jxl::PaddedBytes jpeg_data; if (jpeg_codestream != nullptr) { #if JPEGXL_ENABLE_JPEG - jxl::PaddedBytes jpeg_bytes; + std::vector 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()); @@ -3391,126 +3392,163 @@ JxlDecoderDestroy(dec); } TEST(DecodeTest, ProgressiveEventTest) { + std::set progressive_details = {kDC, kPasses}; 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 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(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}; - - std::vector pixels2; - pixels2.resize(pixels.size()); - std::vector dc; - dc.resize(pixels.size()); - - 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_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(info.xsize, xsize); - EXPECT_EQ(info.ysize, ysize); - - EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(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 (expect_dc) { - EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, JxlDecoderProcessInput(dec)); - EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderFlushImage(dec)); - dc = pixels2; - } - - EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); - EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); - - 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; + 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 pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + jxl::CompressParams cparams; + if (lossless) { + cparams.SetLossless(); + } else { + cparams.butteraugli_distance = 0.5f; + } + cparams.qprogressive_mode = true; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span(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}; + + std::vector pixels2; + pixels2.resize(pixels.size()); + std::vector dc; + dc.resize(pixels.size()); + std::vector pass; + pass.resize(pixels.size()); + + 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, kLastPasses)); + 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, + 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(info.xsize, xsize); + EXPECT_EQ(info.ysize, ysize); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(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 (expect_flush) { + EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderFlushImage(dec)); + dc = pixels2; + if (prog_detail == kPasses) { + EXPECT_EQ(JXL_DEC_FRAME_PROGRESSION, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderFlushImage(dec)); + pass = pixels2; } } - } - - jxl::CodecInOut io_pixels; - io_pixels.SetFromImage(std::move(imagef_pixels), + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + + jxl::Image3F imagef_pixels(xsize, ysize); + jxl::Image3F imagef_pixels2(xsize, ysize); + jxl::Image3F imagef_dc(xsize, ysize); + jxl::Image3F imagef_pass(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); + float* row_pass = imagef_pass.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; + row_pass[x] = pass[pixel_index] / 255.0f; + } + } + } + + 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::CodecInOut io_pass; + io_pass.SetFromImage(std::move(imagef_pass), 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(), + + jxl::ButteraugliParams ba; + float distance_full = + ButteraugliDistance(io_pixels, io_pixels2, 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); + + if (expect_flush) { + float distance_dc = + ButteraugliDistance(io_pixels, io_dc, ba, jxl::GetJxlCms(), + /*distmap=*/nullptr, nullptr); + EXPECT_LT(distance_dc, 30.0f); + // Verify that the returned DC image is actually DC, by checking + // that it has much worse butteraugli score than the full image. + EXPECT_LT(distance_full * 5, distance_dc); + if (prog_detail == kPasses) { + float distance_pass = + ButteraugliDistance(io_pixels, io_pass, ba, jxl::GetJxlCms(), + /*distmap=*/nullptr, nullptr); + EXPECT_LT(distance_pass, 3.0f); + // Verify that the returned pass image is actually not the full + // image, by checking that it has a bit worse butteraugli score + // than the full image. + EXPECT_LT(distance_full * 1.1, distance_pass); + } + } + JxlDecoderDestroy(dec); } - JxlDecoderDestroy(dec); } } } } @@ -3562,12 +3600,11 @@ 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( jxl::jpeg::DecodeImageJPG(jxl::Span(orig), &orig_io)); orig_io.metadata.m.xyb_encoded = false; diff --git a/third_party/jpeg-xl/lib/jxl/enc_image_bundle.cc b/third_party/jpeg-xl/lib/jxl/enc_image_bundle.cc --- a/third_party/jpeg-xl/lib/jxl/enc_image_bundle.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_image_bundle.cc @@ -3,10 +3,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #include "lib/jxl/enc_image_bundle.h" +#include #include #include #include "lib/jxl/alpha.h" #include "lib/jxl/base/byte_order.h" diff --git a/third_party/jpeg-xl/lib/jxl/enc_modular.cc b/third_party/jpeg-xl/lib/jxl/enc_modular.cc --- a/third_party/jpeg-xl/lib/jxl/enc_modular.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_modular.cc @@ -7,10 +7,11 @@ #include #include #include +#include #include #include #include #include @@ -673,10 +674,12 @@ !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; 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); @@ -692,11 +695,15 @@ (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(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) && @@ -736,25 +743,24 @@ 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++; } } diff --git a/third_party/jpeg-xl/lib/jxl/enc_patch_dictionary.cc b/third_party/jpeg-xl/lib/jxl/enc_patch_dictionary.cc --- a/third_party/jpeg-xl/lib/jxl/enc_patch_dictionary.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_patch_dictionary.cc @@ -8,10 +8,11 @@ #include #include #include #include +#include #include #include #include #include diff --git a/third_party/jpeg-xl/lib/jxl/enc_photon_noise_test.cc b/third_party/jpeg-xl/lib/jxl/enc_photon_noise_test.cc --- a/third_party/jpeg-xl/lib/jxl/enc_photon_noise_test.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_photon_noise_test.cc @@ -3,11 +3,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #include "lib/jxl/enc_photon_noise.h" -#include "gmock/gmock.h" +#include "lib/jxl/test_utils.h" namespace jxl { namespace { using ::testing::FloatNear; diff --git a/third_party/jpeg-xl/lib/jxl/encode.cc b/third_party/jpeg-xl/lib/jxl/encode.cc --- a/third_party/jpeg-xl/lib/jxl/encode.cc +++ b/third_party/jpeg-xl/lib/jxl/encode.cc @@ -25,18 +25,26 @@ #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 uint32_t JxlEncoderVersion(void) { @@ -78,11 +86,11 @@ JxlEncoderStatus BrotliCompress(int quality, const uint8_t* in, size_t in_size, jxl::PaddedBytes* out) { std::unique_ptr 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); constexpr size_t kBufferSize = 128 * 1024; @@ -98,11 +106,11 @@ uint8_t* next_out = temp_buffer.data(); jxl::msan::MemoryIsInitialized(next_in, avail_in); 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); out->resize(out->size() + out_size); memcpy(out->data() + out->size() - out_size, temp_buffer.data(), out_size); @@ -191,16 +199,16 @@ uint32_t exponent_bits_per_sample) { if (!exponent_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; } } // namespace @@ -225,30 +233,33 @@ 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) { 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 writer.ZeroPadToByte(); @@ -309,11 +320,12 @@ std::move(input.frame); input_queue.erase(input_queue.begin()); 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); } } // TODO(zond): If the input queue is empty and the frames_closed is true, // then mark this frame as the last. @@ -396,11 +408,11 @@ JXL_ASSERT(writer.BitsWritten() == 0); if (!jxl::EncodeFrame(input_frame->option_values.cparams, frame_info, &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; codestream_bytes_written_end_of_frame += jxl::DivCeil(writer.BitsWritten(), 8); @@ -441,11 +453,12 @@ } if (JXL_ENC_SUCCESS != 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); output_byte_queue.insert(output_byte_queue.end(), compressed.data(), compressed.data() + compressed.size()); @@ -460,17 +473,32 @@ return JXL_ENC_SUCCESS; } 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) { jxl::SetIntensityTarget(&enc->metadata.m); } @@ -478,18 +506,36 @@ } 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); } @@ -564,15 +610,15 @@ } 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 = info->exponent_bits_per_sample; enc->metadata.m.bit_depth.floating_point_sample = @@ -585,10 +631,11 @@ // RGBA with no other extra channels, has exactly num_extra_channels == 1 enc->metadata.m.num_extra_channels = info->num_extra_channels; 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"); } // If the user provides non-zero alpha_bits, we make the channel info at index // zero the appropriate alpha channel. @@ -596,19 +643,25 @@ JxlExtraChannelInfo channel_info; JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &channel_info); 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"); } } enc->metadata.m.xyb_encoded = !info->uses_original_profile; 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); enc->intensity_target_set = true; } else if (enc->color_encoding_set || enc->metadata.m.xyb_encoded) { @@ -620,34 +673,38 @@ } enc->metadata.m.tone_mapping.min_nits = info->min_nits; 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; enc->metadata.m.animation.num_loops = info->animation.num_loops; enc->metadata.m.animation.have_timecodes = info->animation.have_timecodes; } std::string level_message; int required_level = VerifyLevelSettings(enc, &level_message); if (required_level == -1 || static_cast(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()); + 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; } void JxlEncoderInitExtraChannelInfo(JxlExtraChannelType type, @@ -666,15 +723,16 @@ } 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]; channel.type = static_cast(info->type); channel.bit_depth.bits_per_sample = info->bits_per_sample; @@ -692,24 +750,26 @@ channel.spot_color[3] = info->spot_color[3]; std::string level_message; int required_level = VerifyLevelSettings(enc, &level_message); if (required_level == -1 || static_cast(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()); + 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; } JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelName(JxlEncoder* enc, size_t index, 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); return JXL_ENC_SUCCESS; } @@ -739,11 +799,12 @@ 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; } @@ -760,11 +821,12 @@ } 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; } frame_settings->values.cparams.butteraugli_distance = distance; @@ -784,36 +846,64 @@ } JxlEncoderStatus JxlEncoderFrameSettingsSetOption( JxlEncoderFrameSettings* frame_settings, JxlEncoderFrameSettingId option, int32_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(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; // set enc option for brotli use in brob boxes frame_settings->enc->brotli_effort = value; 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; case JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING: // TOOD(lode): the jxl codestream allows choosing a different resampling @@ -821,11 +911,12 @@ // option to a JxlEncoderFrameSettings-option that can be set per extra // channel, so needs its own function rather than // 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; case JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED: if (value < 0 || value > 1) { @@ -839,124 +930,138 @@ // the noise synthesis parameters per frame for more fine grained control. frame_settings->values.cparams.photon_noise_iso = static_cast(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(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(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(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(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(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(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(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(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; + 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_CHANNEL_COLORS_GLOBAL_PERCENT: - if (value < -1 || value > 100) return JXL_ENC_ERROR; + if (value < -1 || value > 100) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..100]"); + } 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(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 || value > 100) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..100]"); + } if (value == -1) { frame_settings->values.cparams.channel_colors_percent = 80.0f; } else { frame_settings->values.cparams.channel_colors_percent = static_cast(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 { frame_settings->values.cparams.palette_colors = value; } 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, // alternatively, in the cjxl binary like now) 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; } else { frame_settings->values.cparams.color_transform = static_cast(value); } 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 > 48) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..48]"); + } + 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 // kept in cjxl and set it to 1 or 2 using this API. if (value == -1) { @@ -964,16 +1069,22 @@ } else { frame_settings->values.cparams.modular_group_size_shift = value; } 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(value); return JXL_ENC_SUCCESS; case JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT: - if (value < -1) return JXL_ENC_ERROR; + if (value < -1) { + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, + "Option value has to be in [-1..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 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 @@ -987,27 +1098,31 @@ 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 { frame_settings->values.cparams.options.max_properties = value; } 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; default: - return JXL_ENC_ERROR; + return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, + "Unknown option"); } } JxlEncoder* JxlEncoderCreate(const JxlMemoryManager* memory_manager) { JxlMemoryManager local_memory_manager; @@ -1048,10 +1163,11 @@ enc->color_encoding_set = false; enc->intensity_target_set = false; enc->use_container = false; enc->use_boxes = false; enc->codestream_level = 5; + JxlEncoderInitBasicInfo(&enc->basic_info); } void JxlEncoderDestroy(JxlEncoder* enc) { if (enc) { JxlMemoryManager local_memory_manager = enc->memory_manager; @@ -1059,32 +1175,39 @@ enc->~JxlEncoder(); jxl::MemoryManagerFree(&local_memory_manager, 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(use_container); return JXL_ENC_SUCCESS; } 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(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 != 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; } @@ -1098,15 +1221,19 @@ } 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( &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; } namespace { @@ -1123,33 +1250,37 @@ size_t factor = frame_settings->values.cparams.resampling; xsize = jxl::DivCeil(xsize, factor); 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; } } // namespace 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(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"); } } if (!frame_settings->enc->basic_info_set) { JxlBasicInfo basic_info; @@ -1157,25 +1288,51 @@ basic_info.xsize = io.Main().jpeg_data->width; basic_info.ysize = io.Main().jpeg_data->height; 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()) { + 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 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( jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); } @@ -1186,21 +1343,25 @@ jxl::JxlEncoderQueuedFrame{ frame_settings->values, 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(io.Main().jpeg_data->width) || ysize != static_cast(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 extra_channels( frame_settings->enc->metadata.m.num_extra_channels); for (auto& extra_channel : extra_channels) { extra_channel = jxl::ImageF(xsize, ysize); @@ -1222,15 +1383,28 @@ (!frame_settings->enc->color_encoding_set && !frame_settings->enc->metadata.m.xyb_encoded)) { // 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( &frame_settings->enc->memory_manager, // JxlEncoderQueuedFrame is a struct with no constructors, so we use the @@ -1239,11 +1413,13 @@ frame_settings->values, 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?"); } jxl::ColorEncoding c_current; if (!frame_settings->enc->color_encoding_set) { if ((pixel_format->data_type == JXL_TYPE_FLOAT) || @@ -1259,15 +1435,18 @@ uint32_t num_channels = pixel_format->num_channels; size_t has_interleaved_alpha = static_cast(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 extra_channels( frame_settings->enc->metadata.m.num_extra_channels); for (auto& extra_channel : extra_channels) { extra_channel = jxl::ImageF(xsize, ysize); @@ -1296,51 +1475,58 @@ frame_settings->values.header.layer_info.blend_info.source > 0; 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; QueueFrame(frame_settings, queued_frame); return JXL_ENC_SUCCESS; } 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; } JxlEncoderStatus JxlEncoderAddBox(JxlEncoder* enc, const JxlBoxType type, const uint8_t* contents, size_t size, 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"); } } auto box = jxl::MemoryManagerMakeUnique( &enc->memory_manager); @@ -1354,31 +1540,37 @@ 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; return JXL_ENC_SUCCESS; } @@ -1416,17 +1608,19 @@ } 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; // Setting the frame header resets the frame name, it must be set again with // JxlEncoderSetFrameName if desired. @@ -1437,11 +1631,12 @@ 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() != frame_settings->enc->metadata.m.num_extra_channels) { JxlBlendInfo default_blend_info; @@ -1455,11 +1650,12 @@ 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(); return JXL_ENC_SUCCESS; } diff --git a/third_party/jpeg-xl/lib/jxl/encode_internal.h b/third_party/jpeg-xl/lib/jxl/encode_internal.h --- a/third_party/jpeg-xl/lib/jxl/encode_internal.h +++ b/third_party/jpeg-xl/lib/jxl/encode_internal.h @@ -104,10 +104,11 @@ } // namespace jxl // 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 thread_pool{ nullptr, jxl::MemoryManagerDeleteHelper(&memory_manager)}; JxlCmsInterface cms; std::vector> @@ -143,10 +144,11 @@ // Wrote any output at all, so wrote the data before the first user added // frame or box, such as signature, basic info, ICC profile or jpeg // 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 // used. The counter is used for the 4-byte jxlp box index header. size_t jxlp_counter; diff --git a/third_party/jpeg-xl/lib/jxl/encode_test.cc b/third_party/jpeg-xl/lib/jxl/encode_test.cc --- a/third_party/jpeg-xl/lib/jxl/encode_test.cc +++ b/third_party/jpeg-xl/lib/jxl/encode_test.cc @@ -56,12 +56,11 @@ JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); 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 = JxlEncoderFrameSettingsCreate(enc.get(), NULL); @@ -108,11 +107,11 @@ jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); 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); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderAddImageFrame(frame_settings, &pixel_format, @@ -177,13 +176,19 @@ } // 16-bit alpha means this requires level 10 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())); JxlEncoderCloseInput(enc); @@ -449,14 +454,11 @@ 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_EQ(7, enc->last_used_cparams.options.max_properties); } @@ -769,12 +771,11 @@ basic_info.xsize = 1ull << 31ull; EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); } 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); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); @@ -1304,12 +1305,13 @@ #if JPEGXL_ENABLE_JPEG // Loading .jpg files requires libjpeg support. 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(orig), &orig_io, /*pool=*/nullptr)); diff --git a/third_party/jpeg-xl/lib/jxl/image.h b/third_party/jpeg-xl/lib/jxl/image.h --- a/third_party/jpeg-xl/lib/jxl/image.h +++ b/third_party/jpeg-xl/lib/jxl/image.h @@ -298,10 +298,36 @@ size_t xsize() const { return xsize_; } size_t ysize() const { return ysize_; } T x1() const { return x0_ + xsize_; } T y1() const { return y0_ + ysize_; } + RectT ShiftLeft(size_t shiftx, size_t shifty) const { + return RectT(x0_ * (1 << shiftx), y0_ * (1 << shifty), xsize_ << shiftx, + ysize_ << shifty); + } + RectT ShiftLeft(size_t shift) const { return ShiftLeft(shift, shift); } + + // Requires x0(), y0() to be multiples of 1< CeilShiftRight(size_t shiftx, size_t shifty) const { + JXL_ASSERT(x0_ % (1 << shiftx) == 0); + JXL_ASSERT(y0_ % (1 << shifty) == 0); + return RectT(x0_ / (1 << shiftx), y0_ / (1 << shifty), + DivCeil(xsize_, T{1} << shiftx), + DivCeil(ysize_, T{1} << shifty)); + } + RectT CeilShiftRight(std::pair shift) const { + return CeilShiftRight(shift.first, shift.second); + } + RectT CeilShiftRight(size_t shift) const { + return CeilShiftRight(shift, shift); + } + + template + RectT As() const { + return RectT(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) { return (static_cast(begin + size_max) <= end) ? size_max diff --git a/third_party/jpeg-xl/lib/jxl/jpeg/jpeg_data.h b/third_party/jpeg-xl/lib/jxl/jpeg/jpeg_data.h --- a/third_party/jpeg-xl/lib/jxl/jpeg/jpeg_data.h +++ b/third_party/jpeg-xl/lib/jxl/jpeg/jpeg_data.h @@ -103,10 +103,11 @@ EMPTY_DQT, OUT_OF_BAND_COEFF, EOB_RUN_TOO_LONG, IMAGE_TOO_LARGE, INVALID_QUANT_TBL_PRECISION, + TAIL_DATA_TOO_LARGE }; // Quantization values for an 8x8 pixel block. struct JPEGQuantTable { std::array values; diff --git a/third_party/jpeg-xl/lib/jxl/jpeg/jpeg_data.cc b/third_party/jpeg-xl/lib/jxl/jpeg/jpeg_data.cc --- a/third_party/jpeg-xl/lib/jxl/jpeg/jpeg_data.cc +++ b/third_party/jpeg-xl/lib/jxl/jpeg/jpeg_data.cc @@ -363,10 +363,15 @@ uint32_t len = visitor->IsReading() ? 0 : inter_marker_data[i].size(); JXL_RETURN_IF_ERROR(visitor->Bits(16, 0, &len)); 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)); JXL_RETURN_IF_ERROR(visitor->Bool(false, &has_zero_padding_bit)); diff --git a/third_party/jpeg-xl/lib/jxl/jxl_test.cc b/third_party/jpeg-xl/lib/jxl/jxl_test.cc --- a/third_party/jpeg-xl/lib/jxl/jxl_test.cc +++ b/third_party/jpeg-xl/lib/jxl/jxl_test.cc @@ -229,34 +229,32 @@ IsSlightlyBelow(90)); } 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(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, dparams, &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 // in random order. 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(orig), &io, &pool)); // Image size is selected so that the block border needed is larger than the // amount of pixels available on the next block. io.ShrinkTo(513, 515); @@ -274,12 +272,11 @@ } 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(orig), &io, &pool)); // Image size is selected so that the block border needed is larger than the // amount of pixels available on the next block. io.ShrinkTo(513, 515); @@ -352,12 +349,11 @@ #if JXL_TEST_NL 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(orig), &io, &pool)); io.ShrinkTo(600, 1024); // partial X, full Y group CompressParams cparams; @@ -381,47 +377,45 @@ #endif TEST(JxlTest, RoundtripMultiGroup) { 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(orig), &io, &pool)); io.ShrinkTo(600, 1024); CompressParams cparams; DecompressParams dparams; cparams.butteraugli_distance = 1.0f; cparams.speed_tier = SpeedTier::kKitten; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u); + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 55000u); EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), - IsSlightlyBelow(15)); + IsSlightlyBelow(11)); cparams.butteraugli_distance = 2.0f; cparams.speed_tier = SpeedTier::kWombat; CodecInOut io3; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 22100u); + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 34000u); EXPECT_THAT(ComputeDistance2(io.Main(), io3.Main(), GetJxlCms()), - IsSlightlyBelow(24)); + IsSlightlyBelow(18)); } 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(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, dparams, &pool, &io2), 450000u); } TEST(JxlTest, RoundtripDotsForceEpf) { ThreadPoolInternal pool(8); const PaddedBytes orig = @@ -434,20 +428,19 @@ 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, dparams, &pool, &io2), 450000u); } // Checks for differing size/distance in two consecutive runs of distance 2, // which involves additional processing including adaptive reconstruction. // 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(orig), &io, &pool)); CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; @@ -475,12 +468,11 @@ } // 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(orig), &io, &pool)); CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; @@ -1218,16 +1210,14 @@ EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha())); } 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(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(yuv420), &io2)); CompressParams cparams = CParamsForLossless(); cparams.speed_tier = SpeedTier::kThunder; @@ -1239,15 +1229,15 @@ EXPECT_TRUE(EncodeFile(cparams, &io2, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io3; EXPECT_TRUE(DecodeFile(dparams, 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 = @@ -1472,22 +1462,20 @@ return compressed.size(); } 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(orig), &io)); CodecInOut io2; ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); @@ -1506,12 +1494,11 @@ IsSlightlyBelow(12)); } 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(orig), &io)); CodecInOut io2; ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); @@ -1529,12 +1516,11 @@ } 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(orig), &io)); CodecInOut io2; ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); @@ -1547,18 +1533,17 @@ 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(orig), &io)); CodecInOut io2; ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); @@ -1576,13 +1561,12 @@ } 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(orig), &io)); CodecInOut io2; ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); @@ -1601,82 +1585,76 @@ #endif 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(orig), &io, &pool)); io.ShrinkTo(600, 1024); CompressParams cparams; @@ -1685,13 +1663,13 @@ cparams.butteraugli_distance = 1.0f; cparams.progressive_dc = true; cparams.responsive = true; cparams.progressive_mode = true; CodecInOut io2; - EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u); + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 61000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool), - IsSlightlyBelow(1.1f)); + IsSlightlyBelow(1.2f)); } } // namespace } // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl/libjxl.pc.in b/third_party/jpeg-xl/lib/jxl/libjxl.pc.in --- a/third_party/jpeg-xl/lib/jxl/libjxl.pc.in +++ b/third_party/jpeg-xl/lib/jxl/libjxl.pc.in @@ -1,9 +1,9 @@ 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 Version: @JPEGXL_LIBRARY_VERSION@ Requires.private: @JPEGXL_LIBRARY_REQUIRES@ diff --git a/third_party/jpeg-xl/lib/jxl/modular/transform/enc_palette.cc b/third_party/jpeg-xl/lib/jxl/modular/transform/enc_palette.cc --- a/third_party/jpeg-xl/lib/jxl/modular/transform/enc_palette.cc +++ b/third_party/jpeg-xl/lib/jxl/modular/transform/enc_palette.cc @@ -177,11 +177,47 @@ pixel_type minval, maxval; compute_minmax(input.channel[begin_c], &minval, &maxval); size_t lookup_table_size = static_cast(maxval) - static_cast(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 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; for (size_t y = 0; y < h; y++) { const pixel_type *p = input.channel[begin_c].Row(y); diff --git a/third_party/jpeg-xl/lib/jxl/modular/transform/palette.h b/third_party/jpeg-xl/lib/jxl/modular/transform/palette.h --- a/third_party/jpeg-xl/lib/jxl/modular/transform/palette.h +++ b/third_party/jpeg-xl/lib/jxl/modular/transform/palette.h @@ -4,10 +4,12 @@ // license that can be found in the LICENSE file. #ifndef LIB_JXL_MODULAR_TRANSFORM_PALETTE_H_ #define LIB_JXL_MODULAR_TRANSFORM_PALETTE_H_ +#include + #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/status.h" #include "lib/jxl/common.h" #include "lib/jxl/modular/encoding/context_predict.h" #include "lib/jxl/modular/modular_image.h" @@ -82,11 +84,11 @@ index %= 1 + 2 * (kDeltaPalette.size() - 1); static constexpr int kMultiplier[] = {-1, 1}; pixel_type result = kDeltaPalette[((index + 1) >> 1)][c] * kMultiplier[index & 1]; if (bit_depth > 8) { - result *= static_cast(1) << (bit_depth - 8); + result *= static_cast(1) << (std::min(bit_depth, 24) - 8); } return result; } else if (palette_size <= index && index < palette_size + kLargeCubeOffset) { if (c >= kRgbChannels) return 0; index -= palette_size; diff --git a/third_party/jpeg-xl/lib/jxl/modular/transform/transform.cc b/third_party/jpeg-xl/lib/jxl/modular/transform/transform.cc --- a/third_party/jpeg-xl/lib/jxl/modular/transform/transform.cc +++ b/third_party/jpeg-xl/lib/jxl/modular/transform/transform.cc @@ -66,13 +66,10 @@ case TransformId::kPalette: JXL_DEBUG_V(2, "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: return JXL_FAILURE("Unknown transformation (ID=%u)", static_cast(id)); diff --git a/third_party/jpeg-xl/lib/jxl/modular_test.cc b/third_party/jpeg-xl/lib/jxl/modular_test.cc --- a/third_party/jpeg-xl/lib/jxl/modular_test.cc +++ b/third_party/jpeg-xl/lib/jxl/modular_test.cc @@ -43,12 +43,11 @@ namespace { 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; diff --git a/third_party/jpeg-xl/lib/jxl/passes_test.cc b/third_party/jpeg-xl/lib/jxl/passes_test.cc --- a/third_party/jpeg-xl/lib/jxl/passes_test.cc +++ b/third_party/jpeg-xl/lib/jxl/passes_test.cc @@ -74,12 +74,11 @@ 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(orig), &io, &pool)); io.ShrinkTo(600, 1024); // partial X, full Y group CompressParams cparams; @@ -89,24 +88,23 @@ 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)); + IsSlightlyBelow(1.3f)); 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)); + IsSlightlyBelow(2.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(orig), &io, &pool)); CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; @@ -120,12 +118,11 @@ // Checks for differing size/distance in two consecutive runs of distance 2, // which involves additional processing including adaptive reconstruction. // 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(orig), &io, &pool)); CompressParams cparams; cparams.speed_tier = SpeedTier::kSquirrel; @@ -299,12 +296,11 @@ EXPECT_GE(butteraugli_distance_down2_full, 1.0f); } 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(orig), &io_orig, &pool)); Rect rect(0, 0, io_orig.xsize(), 128); // need 2 DC groups for the DC frame to actually be progressive. Image3F large(4242, rect.ysize()); @@ -346,12 +342,11 @@ EXPECT_GE(butteraugli_distance_down2_full, 1.0f); } 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(orig), &io, &pool)); PaddedBytes compressed; AuxOut aux; diff --git a/third_party/jpeg-xl/lib/jxl/render_pipeline/low_memory_render_pipeline.cc b/third_party/jpeg-xl/lib/jxl/render_pipeline/low_memory_render_pipeline.cc --- a/third_party/jpeg-xl/lib/jxl/render_pipeline/low_memory_render_pipeline.cc +++ b/third_party/jpeg-xl/lib/jxl/render_pipeline/low_memory_render_pipeline.cc @@ -301,10 +301,21 @@ input_sizes[c] = {full_image_xsize_, full_image_ysize_}; } stages_[i]->SetInputSizes(input_sizes); } + // Check that kInput stages appear after the switch to image dimensions (if + // any). + for (size_t i = 0; i < stages_.size(); i++) { + for (size_t c = 0; c < shifts.size(); c++) { + if (stages_[i]->GetChannelMode(c) == RenderPipelineChannelMode::kInput) { + JXL_ASSERT(first_image_dim_stage_ == stages_.size() || + i >= first_image_dim_stage_); + } + } + } + anyc_.resize(stages_.size()); for (size_t i = 0; i < stages_.size(); i++) { for (size_t c = 0; c < shifts.size(); c++) { if (stages_[i]->GetChannelMode(c) != RenderPipelineChannelMode::kIgnored) { @@ -476,76 +487,135 @@ } } } } +// Information about where the *output* of each stage is stored. +class Rows { + public: + Rows(const std::vector>& stages, + const Rect data_max_color_channel_rect, int group_data_x_border, + int group_data_y_border, + const std::vector>& group_data_shift, + size_t base_color_shift, std::vector>& thread_data, + std::vector& 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 + + rows_.resize(num_stages + 1, std::vector(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() + .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> rows_; +}; + } // namespace void LowMemoryRenderPipeline::RenderRect(size_t thread_id, std::vector& 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. + // processed, in the coordinates of that stage (i.e. with the scaling factor + // that that stage has). std::vector 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++) { - 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); + group_rect[i] = + image_area_rect.CeilShiftRight(channel_shifts_[i][anyc_[i]]); } - struct RowInfo { - float* base_ptr; - int ymod_minus_1; - size_t stride; - }; - - std::vector> rows( - stages_.size() + 1, std::vector(input_data.size())); - - 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(); - } - } - } + 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_; - 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; + // 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(full_image_x1, full_image_xsize); - 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); - }; + // 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 input_rows(first_trailing_stage_ + 1); for (size_t i = 0; i < first_trailing_stage_; i++) { input_rows[i].resize(input_data.size()); @@ -555,133 +625,133 @@ // Maximum possible shift is 3. RenderPipelineStage::RowInfo output_rows(input_data.size(), std::vector(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 // to an actual row of the current processing stage; actual processing happens // when vy % (1<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; // Do not produce rows in out-of-bounds areas. if (image_y < 0 || image_y >= ssize_t(image_rect_[i].ysize())) { 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); + // 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; } - 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); - } - - 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(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); } } } void LowMemoryRenderPipeline::RenderPadding(size_t thread_id, Rect rect) { diff --git a/third_party/jpeg-xl/lib/jxl/render_pipeline/render_pipeline_stage.h b/third_party/jpeg-xl/lib/jxl/render_pipeline/render_pipeline_stage.h --- a/third_party/jpeg-xl/lib/jxl/render_pipeline/render_pipeline_stage.h +++ b/third_party/jpeg-xl/lib/jxl/render_pipeline/render_pipeline_stage.h @@ -29,11 +29,14 @@ kIgnored = 0, // This channel is modified in-place. 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, }; class RenderPipeline; @@ -98,10 +101,14 @@ // of floats; concurrent calls will have different buffers. virtual 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 = 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) {} virtual Status IsInitialized() const { return true; } @@ -110,14 +117,10 @@ virtual void SetInputSizes( const std::vector>& input_sizes) {} 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 // offset-ed row (i.e. kRenderPipelineXOffset has been applied). float* GetInputRow(const RowInfo& input_rows, size_t c, int offset) const { @@ -138,10 +141,12 @@ } // 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 // ysize to the image size, and frame_origin to the location of the frame // within the image. Otherwise, this is not called at all. diff --git a/third_party/jpeg-xl/lib/jxl/render_pipeline/render_pipeline_test.cc b/third_party/jpeg-xl/lib/jxl/render_pipeline/render_pipeline_test.cc --- a/third_party/jpeg-xl/lib/jxl/render_pipeline/render_pipeline_test.cc +++ b/third_party/jpeg-xl/lib/jxl/render_pipeline/render_pipeline_test.cc @@ -220,11 +220,11 @@ {3, 8}, {128, 128}, {256, 256}, {258, 258}, {533, 401}, {777, 777}, }; 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; // Base settings. settings.cparams.butteraugli_distance = 1.0; @@ -352,40 +352,36 @@ 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 = "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; all_tests.push_back(s); } { 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); } @@ -396,15 +392,14 @@ all_tests.push_back(s); } } #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; settings.xsize = 2268; settings.ysize = 1512; @@ -421,10 +416,30 @@ settings.ysize = 277; settings.cparams_descr = "Patches"; 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; } std::ostream& operator<<(std::ostream& os, const RenderPipelineTestInputSettings& c) { diff --git a/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_patches.h b/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_patches.h --- a/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_patches.h +++ b/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_patches.h @@ -13,10 +13,10 @@ namespace jxl { // Draws patches if applicable. std::unique_ptr GetPatchesStage( - const PatchDictionary* patches); + const PatchDictionary* patches, size_t num_channels); } // namespace jxl #endif // LIB_JXL_RENDER_PIPELINE_STAGE_PATCHES_H_ diff --git a/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_patches.cc b/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_patches.cc --- a/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_patches.cc +++ b/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_patches.cc @@ -7,38 +7,42 @@ 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 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 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 GetPatchesStage( - const PatchDictionary* patches) { - return jxl::make_unique(patches); + const PatchDictionary* patches, size_t num_channels) { + return jxl::make_unique(patches, num_channels); } } // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl/roundtrip_test.cc b/third_party/jpeg-xl/lib/jxl/roundtrip_test.cc --- a/third_party/jpeg-xl/lib/jxl/roundtrip_test.cc +++ b/third_party/jpeg-xl/lib/jxl/roundtrip_test.cc @@ -798,12 +798,11 @@ JxlDecoderDestroy(dec); } #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( SetFromBytes(jxl::Span(orig), &orig_io, /*pool=*/nullptr)); diff --git a/third_party/jpeg-xl/lib/jxl/splines_test.cc b/third_party/jpeg-xl/lib/jxl/splines_test.cc --- a/third_party/jpeg-xl/lib/jxl/splines_test.cc +++ b/third_party/jpeg-xl/lib/jxl/splines_test.cc @@ -3,19 +3,18 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #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 { std::ostream& operator<<(std::ostream& os, const Spline::Point& p) { diff --git a/third_party/jpeg-xl/lib/jxl/test_utils.h b/third_party/jpeg-xl/lib/jxl/test_utils.h --- a/third_party/jpeg-xl/lib/jxl/test_utils.h +++ b/third_party/jpeg-xl/lib/jxl/test_utils.h @@ -6,11 +6,18 @@ #ifndef LIB_JXL_TEST_UTILS_H_ #define LIB_JXL_TEST_UTILS_H_ // 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/jxl/aux_out_fwd.h" #include "lib/jxl/base/data_parallel.h" @@ -79,10 +86,15 @@ basic_info->exponent_bits_per_sample = 0; break; 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; basic_info->num_extra_channels = 1; } else { diff --git a/third_party/jpeg-xl/lib/jxl/testdata.h b/third_party/jpeg-xl/lib/jxl/testdata.h --- a/third_party/jpeg-xl/lib/jxl/testdata.h +++ b/third_party/jpeg-xl/lib/jxl/testdata.h @@ -17,43 +17,11 @@ 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(size_bytes[1]); - if (size) { - data.append(bytes, bytes + size); - free(reinterpret_cast(bytes)); - ok = true; - } - } -#endif - JXL_CHECK(ok); + JXL_CHECK(ReadFile(full_path, &data)); return data; } } // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl_extras.cmake b/third_party/jpeg-xl/lib/jxl_extras.cmake --- a/third_party/jpeg-xl/lib/jxl_extras.cmake +++ b/third_party/jpeg-xl/lib/jxl_extras.cmake @@ -14,10 +14,12 @@ extras/dec/decode.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/pgx.cc extras/enc/pgx.h extras/enc/pnm.cc extras/enc/pnm.h extras/hlg.cc diff --git a/third_party/jpeg-xl/lib/jxl_tests.cmake b/third_party/jpeg-xl/lib/jxl_tests.cmake --- a/third_party/jpeg-xl/lib/jxl_tests.cmake +++ b/third_party/jpeg-xl/lib/jxl_tests.cmake @@ -103,10 +103,14 @@ set_target_properties(${TESTNAME} PROPERTIES LINK_FLAGS "\ -O1 \ -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 ${JPEGXL_INTERNAL_FLAGS} # Add coverage flags to the test binary so code in the private headers of diff --git a/third_party/jpeg-xl/lib/jxl_threads.cmake b/third_party/jpeg-xl/lib/jxl_threads.cmake --- a/third_party/jpeg-xl/lib/jxl_threads.cmake +++ b/third_party/jpeg-xl/lib/jxl_threads.cmake @@ -107,10 +107,24 @@ endif() # TARGET_SUPPORTS_SHARED_LIBS AND NOT JPEGXL_STATIC AND # 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) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libjxl_threads.pc" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") diff --git a/third_party/jpeg-xl/lib/lib.gni b/third_party/jpeg-xl/lib/lib.gni --- a/third_party/jpeg-xl/lib/lib.gni +++ b/third_party/jpeg-xl/lib/lib.gni @@ -446,10 +446,12 @@ "extras/dec/decode.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/pgx.cc", "extras/enc/pgx.h", "extras/enc/pnm.cc", "extras/enc/pnm.h", "extras/hlg.cc", diff --git a/third_party/jpeg-xl/lib/threads/libjxl_threads.pc.in b/third_party/jpeg-xl/lib/threads/libjxl_threads.pc.in --- a/third_party/jpeg-xl/lib/threads/libjxl_threads.pc.in +++ b/third_party/jpeg-xl/lib/threads/libjxl_threads.pc.in @@ -1,9 +1,9 @@ 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. Version: @JPEGXL_LIBRARY_VERSION@ Requires.private: @JPEGXL_THREADS_LIBRARY_REQUIRES@ diff --git a/third_party/jpeg-xl/lib/threads/thread_parallel_runner_internal.cc b/third_party/jpeg-xl/lib/threads/thread_parallel_runner_internal.cc --- a/third_party/jpeg-xl/lib/threads/thread_parallel_runner_internal.cc +++ b/third_party/jpeg-xl/lib/threads/thread_parallel_runner_internal.cc @@ -171,18 +171,12 @@ } } } 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_); // Suppress "unused-private-field" warning. diff --git a/third_party/jpeg-xl/lib/threads/thread_parallel_runner_test.cc b/third_party/jpeg-xl/lib/threads/thread_parallel_runner_test.cc --- a/third_party/jpeg-xl/lib/threads/thread_parallel_runner_test.cc +++ b/third_party/jpeg-xl/lib/threads/thread_parallel_runner_test.cc @@ -1,10 +1,12 @@ // Copyright (c) the JPEG XL Project 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 + #include "gtest/gtest.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/thread_pool_internal.h" namespace jpegxl { diff --git a/third_party/jpeg-xl/plugins/gdk-pixbuf/CMakeLists.txt b/third_party/jpeg-xl/plugins/gdk-pixbuf/CMakeLists.txt --- a/third_party/jpeg-xl/plugins/gdk-pixbuf/CMakeLists.txt +++ b/third_party/jpeg-xl/plugins/gdk-pixbuf/CMakeLists.txt @@ -69,11 +69,11 @@ add_test( NAME pixbufloader_test_jxl COMMAND ${XVFB_PROGRAM_PREFIX} $ "${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) endif() endif() # Gdk_FOUND