diff --git a/media/libjxl/moz.build b/media/libjxl/moz.build --- a/media/libjxl/moz.build +++ b/media/libjxl/moz.build @@ -44,10 +44,11 @@ "/third_party/jpeg-xl/lib/jxl/decode.cc", "/third_party/jpeg-xl/lib/jxl/decode_to_jpeg.cc", "/third_party/jpeg-xl/lib/jxl/enc_bit_writer.cc", "/third_party/jpeg-xl/lib/jxl/entropy_coder.cc", "/third_party/jpeg-xl/lib/jxl/epf.cc", + "/third_party/jpeg-xl/lib/jxl/exif.cc", "/third_party/jpeg-xl/lib/jxl/fast_dct.cc", "/third_party/jpeg-xl/lib/jxl/fields.cc", "/third_party/jpeg-xl/lib/jxl/frame_header.cc", "/third_party/jpeg-xl/lib/jxl/gauss_blur.cc", "/third_party/jpeg-xl/lib/jxl/headers.cc", 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: commit dbbe2e2663f9e156d6a8f5e547f4b37c337732bc (2022-03-28T14:05:43Z). - revision: 318c592d98b97d103941b90d47107f06a10c71da + revision: dbbe2e2663f9e156d6a8f5e547f4b37c337732bc license: Apache-2.0 license-file: LICENSE 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,10 +23,11 @@ CanadianBaconBoi Daniel Novomeský David Burnett Dirk Lemstra Don Olmstead +Heiko Becker Jon Sneyers Kleis Auke Wolthuizen Leo Izen Lovell Fuller Marcin Konicki 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 @@ -189,10 +189,19 @@ # Threads set(THREADS_PREFER_PTHREAD_FLAG YES) find_package(Threads REQUIRED) +# These settings are important to drive check_cxx_source_compiles +# See CMP0067 (min cmake version is 3.10 anyway) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED YES) + +# Atomics +find_package(Atomics REQUIRED) + if(JPEGXL_STATIC) if (MINGW) # In MINGW libstdc++ uses pthreads directly. When building statically a # program (regardless of whether the source code uses pthread or not) the # toolchain will add stdc++ and pthread to the linking step but stdc++ will @@ -296,14 +305,10 @@ endif () endif () # !MSVC include(GNUInstallDirs) -set(CMAKE_CXX_STANDARD 11) -set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD_REQUIRED YES) - add_subdirectory(third_party) # Copy the JXL license file to the output build directory. configure_file("${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" ${PROJECT_BINARY_DIR}/LICENSE.jpeg-xl COPYONLY) @@ -417,11 +422,11 @@ MAIN_DEPENDENCY "${CMAKE_CURRENT_SOURCE_DIR}/doc/man/${PAGE}.txt") list(APPEND MANPAGE_FILES "${CMAKE_CURRENT_BINARY_DIR}/${PAGE}.1") list(APPEND MANPAGES "${PAGE}.1") endforeach() add_custom_target(manpages ALL DEPENDS ${MANPAGES}) - install(FILES ${MANPAGE_FILES} DESTINATION share/man/man1) + install(FILES ${MANPAGE_FILES} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) endif() # ASCIIDOC_PY_FOUND else() message(WARNING "asciidoc was not found, the man pages will not be installed.") endif() # ASCIIDOC != "ASCIIDOC-NOTFOUND" endif() # JPEGXL_ENABLE_MANPAGES diff --git a/third_party/jpeg-xl/cmake/FindAtomics.cmake b/third_party/jpeg-xl/cmake/FindAtomics.cmake new file mode 100644 --- /dev/null +++ b/third_party/jpeg-xl/cmake/FindAtomics.cmake @@ -0,0 +1,53 @@ +# Original issue: +# * https://gitlab.kitware.com/cmake/cmake/-/issues/23021#note_1098733 +# +# For reference: +# * https://gcc.gnu.org/wiki/Atomic/GCCMM +# +# riscv64 specific: +# * https://lists.debian.org/debian-riscv/2022/01/msg00009.html +# +# ATOMICS_FOUND - system has c++ atomics +# ATOMICS_LIBRARIES - libraries needed to use c++ atomics + +include(CheckCXXSourceCompiles) + +# RISC-V only has 32-bit and 64-bit atomic instructions. GCC is supposed +# to convert smaller atomics to those larger ones via masking and +# shifting like LLVM, but it’s a known bug that it does not. This means +# anything that wants to use atomics on 1-byte or 2-byte types needs +# -latomic, but not 4-byte or 8-byte (though it does no harm). +set(atomic_code + " + #include + #include + std::atomic n8 (0); // riscv64 + std::atomic n64 (0); // armel, mipsel, powerpc + int main() { + ++n8; + ++n64; + return 0; + }") + +check_cxx_source_compiles("${atomic_code}" ATOMICS_LOCK_FREE_INSTRUCTIONS) + +if(ATOMICS_LOCK_FREE_INSTRUCTIONS) + set(ATOMICS_FOUND TRUE) + set(ATOMICS_LIBRARIES) +else() + set(CMAKE_REQUIRED_LIBRARIES "-latomic") + check_cxx_source_compiles("${atomic_code}" ATOMICS_IN_LIBRARY) + set(CMAKE_REQUIRED_LIBRARIES) + if(ATOMICS_IN_LIBRARY) + set(ATOMICS_LIBRARY atomic) + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Atomics DEFAULT_MSG ATOMICS_LIBRARY) + set(ATOMICS_LIBRARIES ${ATOMICS_LIBRARY}) + unset(ATOMICS_LIBRARY) + else() + if(Atomics_FIND_REQUIRED) + message(FATAL_ERROR "Neither lock free instructions nor -latomic found.") + endif() + endif() +endif() +unset(atomic_code) 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 @@ -19,11 +19,10 @@ #endif #if JPEGXL_ENABLE_EXR #include "lib/extras/enc/exr.h" #endif -#include "lib/extras/codec_psd.h" #include "lib/extras/dec/decode.h" #include "lib/extras/enc/pgx.h" #include "lib/extras/enc/pnm.h" #include "lib/extras/packed_image_convert.h" #include "lib/jxl/base/file_io.h" @@ -44,14 +43,10 @@ extras::PackedPixelFile ppf; if (extras::DecodeBytes(bytes, color_hints, io->constraints, &ppf, pool, orig_codec)) { return ConvertPackedPixelFileToCodecInOut(ppf, pool, io); - } else if (extras::DecodeImagePSD(bytes, color_hints, pool, io)) { - // TODO(deymo): Migrate PSD codec too. - if (orig_codec) *orig_codec = extras::Codec::kPSD; - return true; } return JXL_FAILURE("Codecs failed to decode"); } Status SetFromFile(const std::string& pathname, @@ -122,13 +117,10 @@ case extras::Codec::kPGX: return extras::EncodeImagePGX(&io, c_desired, bits_per_sample, pool, bytes); case extras::Codec::kGIF: return JXL_FAILURE("Encoding to GIF is not implemented"); - case extras::Codec::kPSD: - return extras::EncodeImagePSD(&io, c_desired, bits_per_sample, pool, - bytes); case extras::Codec::kEXR: #if JPEGXL_ENABLE_EXR return extras::EncodeImageEXR(&io, c_desired, pool, bytes); #else return JXL_FAILURE("JPEG XL was built without OpenEXR support"); diff --git a/third_party/jpeg-xl/lib/extras/codec_psd.h b/third_party/jpeg-xl/lib/extras/codec_psd.h deleted file mode 100644 --- a/third_party/jpeg-xl/lib/extras/codec_psd.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) the JPEG XL Project Authors. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#ifndef LIB_EXTRAS_CODEC_PSD_H_ -#define LIB_EXTRAS_CODEC_PSD_H_ - -// Decodes Photoshop PSD/PSB, preserving the layers - -#include -#include - -#include "lib/extras/dec/color_hints.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/span.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/codec_in_out.h" -#include "lib/jxl/color_encoding_internal.h" - -namespace jxl { -namespace extras { - -// Decodes `bytes` into `io`. -Status DecodeImagePSD(const Span bytes, - const extras::ColorHints& color_hints, ThreadPool* pool, - CodecInOut* io); - -// Not implemented yet -Status EncodeImagePSD(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes); - -} // namespace extras -} // namespace jxl - -#endif // LIB_EXTRAS_CODEC_PSD_H_ diff --git a/third_party/jpeg-xl/lib/extras/codec_psd.cc b/third_party/jpeg-xl/lib/extras/codec_psd.cc deleted file mode 100644 --- a/third_party/jpeg-xl/lib/extras/codec_psd.cc +++ /dev/null @@ -1,621 +0,0 @@ -// Copyright (c) the JPEG XL Project Authors. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#include "lib/extras/codec_psd.h" - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "lib/jxl/base/bits.h" -#include "lib/jxl/base/byte_order.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" -#include "lib/jxl/common.h" -#include "lib/jxl/fields.h" // AllDefault -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" -#include "lib/jxl/image_ops.h" -#include "lib/jxl/luminance.h" - -namespace jxl { -namespace extras { -namespace { - -uint64_t get_be_int(int bytes, const uint8_t*& pos, const uint8_t* maxpos) { - uint64_t r = 0; - if (pos + bytes <= maxpos) { - if (bytes == 1) { - r = *pos; - } else if (bytes == 2) { - r = LoadBE16(pos); - } else if (bytes == 4) { - r = LoadBE32(pos); - } else if (bytes == 8) { - r = LoadBE64(pos); - } - } - pos += bytes; - return r; -} - -// Copies up to n bytes, without reading from maxpos (the STL-style end). -void safe_copy(const uint8_t* JXL_RESTRICT pos, - const uint8_t* JXL_RESTRICT maxpos, char* JXL_RESTRICT out, - size_t n) { - for (size_t i = 0; i < n; ++i) { - if (pos + i >= maxpos) return; - out[i] = pos[i]; - } -} - -// maxpos is the STL-style end! The valid range is up to [pos, maxpos). -int safe_strncmp(const uint8_t* pos, const uint8_t* maxpos, const char* s2, - size_t n) { - if (pos + n > maxpos) return 1; - return strncmp((const char*)pos, s2, n); -} -constexpr int PSD_VERBOSITY = 1; - -Status decode_layer(const uint8_t*& pos, const uint8_t* maxpos, - ImageBundle& layer, std::vector chans, - std::vector invert, int w, int h, int version, - int colormodel, bool is_layer, int depth) { - int compression_method = 2; - int nb_channels = chans.size(); - JXL_DEBUG_V(PSD_VERBOSITY, - "Trying to decode layer with dimensions %ix%i and %i channels", w, - h, nb_channels); - if (w <= 0 || h <= 0) return JXL_FAILURE("PSD: empty layer"); - for (int c = 0; c < nb_channels; c++) { - // skip nop byte padding - while (pos < maxpos && *pos == 128) pos++; - JXL_DEBUG_V(PSD_VERBOSITY, "Channel %i (pos %" PRIuS ")", c, (size_t)pos); - // Merged image stores all channels together (same compression method) - // Layers store channel per channel - if (is_layer || c == 0) { - compression_method = get_be_int(2, pos, maxpos); - JXL_DEBUG_V(PSD_VERBOSITY, "compression method: %i", compression_method); - if (compression_method > 1 || compression_method < 0) { - return JXL_FAILURE("PSD: can't handle compression method %i", - compression_method); - } - } - - if (!is_layer && c < colormodel) { - // skip to the extra channels - if (compression_method == 0) { - pos += w * h * (depth >> 3) * colormodel; - c = colormodel - 1; - continue; - } - size_t skip_amount = 0; - for (int i = 0; i < nb_channels; i++) { - if (i < colormodel) { - for (int y = 0; y < h; y++) { - skip_amount += get_be_int(2 * version, pos, maxpos); - } - } else { - pos += h * 2 * version; - } - } - pos += skip_amount; - c = colormodel - 1; - continue; - } - if (is_layer || c == 0) { - // skip the line-counts, we don't need them - if (compression_method == 1) { - pos += h * (is_layer ? 1 : nb_channels) * 2 * - version; // PSB uses 4 bytes per rowsize instead of 2 - } - } - int c_id = chans[c]; - if (c_id < 0) continue; // skip - if (static_cast(c_id) >= 3 + layer.extra_channels().size()) - return JXL_FAILURE("PSD: can't handle channel id %i", c_id); - ImageF& ch = (c_id < 3 ? layer.color()->Plane(c_id) - : layer.extra_channels()[c_id - 3]); - - for (int y = 0; y < h; y++) { - if (pos > maxpos) return JXL_FAILURE("PSD: premature end of input"); - float* const JXL_RESTRICT row = ch.Row(y); - if (compression_method == 0) { - // uncompressed is easy - if (depth == 8) { - for (int x = 0; x < w; x++) { - row[x] = get_be_int(1, pos, maxpos) * (1.f / 255.f); - } - } else if (depth == 16) { - for (int x = 0; x < w; x++) { - row[x] = get_be_int(2, pos, maxpos) * (1.f / 65535.f); - } - } else if (depth == 32) { - for (int x = 0; x < w; x++) { - uint32_t f = get_be_int(4, pos, maxpos); - memcpy(&row[x], &f, 4); - } - } - } else { - // RLE is not that hard - if (depth != 8) - return JXL_FAILURE("PSD: did not expect RLE with depth>1"); - for (int x = 0; x < w;) { - if (pos >= maxpos) return JXL_FAILURE("PSD: out of bounds"); - int8_t rle = *pos++; - if (rle <= 0) { - if (rle == -128) continue; // nop - int count = 1 - rle; - float v = get_be_int(1, pos, maxpos) * (1.f / 255.f); - while (count && x < w) { - row[x] = v; - count--; - x++; - } - if (count) return JXL_FAILURE("PSD: row overflow"); - } else { - int count = 1 + rle; - while (count && x < w) { - row[x] = get_be_int(1, pos, maxpos) * (1.f / 255.f); - count--; - x++; - } - if (count) return JXL_FAILURE("PSD: row overflow"); - } - } - } - if (invert[c]) { - // sometimes 0 means full ink - for (int x = 0; x < w; x++) { - row[x] = 1.f - row[x]; - } - } - } - JXL_DEBUG_V(PSD_VERBOSITY, "Channel %i read.", c); - } - - return true; -} - -} // namespace - -Status DecodeImagePSD(const Span bytes, - const ColorHints& /*color_hints*/, ThreadPool* pool, - CodecInOut* io) { - const uint8_t* pos = bytes.data(); - const uint8_t* maxpos = bytes.data() + bytes.size(); - if (safe_strncmp(pos, maxpos, "8BPS", 4)) return false; // not a PSD file - JXL_DEBUG_V(PSD_VERBOSITY, "trying psd decode"); - pos += 4; - int version = get_be_int(2, pos, maxpos); - JXL_DEBUG_V(PSD_VERBOSITY, "Version=%i", version); - if (version < 1 || version > 2) - return JXL_FAILURE("PSD: unknown format version"); - // PSD = version 1, PSB = version 2 - pos += 6; - int nb_channels = get_be_int(2, pos, maxpos); - size_t ysize = get_be_int(4, pos, maxpos); - size_t xsize = get_be_int(4, pos, maxpos); - const SizeConstraints* constraints = &io->constraints; - JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, xsize, ysize)); - uint64_t total_pixel_count = static_cast(xsize) * ysize; - int bitdepth = get_be_int(2, pos, maxpos); - if (bitdepth != 8 && bitdepth != 16 && bitdepth != 32) { - return JXL_FAILURE("PSD: bit depth %i invalid or not supported", bitdepth); - } - if (bitdepth == 32) { - io->metadata.m.SetFloat32Samples(); - } else { - io->metadata.m.SetUintSamples(bitdepth); - } - int colormodel = get_be_int(2, pos, maxpos); - // 1 = Grayscale, 3 = RGB, 4 = CMYK - if (colormodel != 1 && colormodel != 3 && colormodel != 4) - return JXL_FAILURE("PSD: unsupported color model"); - - int real_nb_channels = colormodel; - std::vector> spotcolor; - - if (get_be_int(4, pos, maxpos)) - return JXL_FAILURE("PSD: Unsupported color mode section"); - - bool hasmergeddata = true; - bool have_alpha = false; - bool merged_has_alpha = false; - bool color_already_set = false; - size_t metalength = get_be_int(4, pos, maxpos); - const uint8_t* metaoffset = pos; - while (pos < metaoffset + metalength) { - char header[5] = "????"; - safe_copy(pos, maxpos, header, 4); - if (memcmp(header, "8BIM", 4) != 0) { - return JXL_FAILURE("PSD: Unexpected image resource header: %s", header); - } - pos += 4; - int id = get_be_int(2, pos, maxpos); - int namelength = get_be_int(1, pos, maxpos); - pos += namelength; - if (!(namelength & 1)) pos++; // padding to even length - size_t blocklength = get_be_int(4, pos, maxpos); - // JXL_DEBUG_V(PSD_VERBOSITY, "block id: %i | block length: %" PRIuS,id, - // blocklength); - if (pos > maxpos) return JXL_FAILURE("PSD: Unexpected end of file"); - if (id == 1039) { // ICC profile - size_t delta = maxpos - pos; - if (delta < blocklength) { - return JXL_FAILURE("PSD: Invalid block length"); - } - PaddedBytes icc; - icc.resize(blocklength); - memcpy(icc.data(), pos, blocklength); - if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) { - return JXL_FAILURE("PSD: Invalid color profile"); - } - color_already_set = true; - } else if (id == 1057) { // compatibility mode or not? - if (get_be_int(4, pos, maxpos) != 1) { - return JXL_FAILURE("PSD: expected version=1 in id=1057 resource block"); - } - hasmergeddata = get_be_int(1, pos, maxpos); - pos++; - blocklength -= 6; // already skipped these bytes - } else if (id == 1077) { // spot colors - int version = get_be_int(4, pos, maxpos); - if (version != 1) { - return JXL_FAILURE( - "PSD: expected DisplayInfo version 1, got version %i", version); - } - int spotcolorcount = nb_channels - colormodel; - JXL_DEBUG_V(PSD_VERBOSITY, "Reading %i spot colors. %" PRIuS, - spotcolorcount, blocklength); - for (int k = 0; k < spotcolorcount; k++) { - int colorspace = get_be_int(2, pos, maxpos); - if ((colormodel == 3 && colorspace != 0) || - (colormodel == 4 && colorspace != 2)) { - return JXL_FAILURE( - "PSD: cannot handle spot colors in different color spaces than " - "image itself"); - } - if (colorspace == 2) JXL_WARNING("PSD: K ignored in CMYK spot color"); - std::vector color; - color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // R or C - color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // G or M - color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // B or Y - color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // ignored or K - color.push_back(get_be_int(2, pos, maxpos) / - 100.f); // solidity (alpha, basically) - int kind = get_be_int(1, pos, maxpos); - JXL_DEBUG_V(PSD_VERBOSITY, "Kind=%i", kind); - color.push_back(kind); - spotcolor.push_back(color); - if (kind == 2) { - JXL_DEBUG_V(PSD_VERBOSITY, "Actual spot color"); - } else if (kind == 1) { - JXL_DEBUG_V(PSD_VERBOSITY, "Mask (alpha) channel"); - } else if (kind == 0) { - JXL_DEBUG_V(PSD_VERBOSITY, "Selection (alpha) channel"); - } else { - return JXL_FAILURE("PSD: Unknown extra channel type"); - } - } - if (blocklength & 1) pos++; - blocklength = 0; - } - pos += blocklength; - if (blocklength & 1) pos++; // padding again - } - - // TODO(deymo): Apply color hints when PSD is converted to PackedPixelFile. - (void)color_already_set; - // JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, color_already_set, - // /*is_gray=*/false, io)); - - size_t layerlength = get_be_int(4 * version, pos, maxpos); - const uint8_t* after_layers_pos = pos + layerlength; - if (after_layers_pos < pos) return JXL_FAILURE("PSD: invalid layer length"); - if (layerlength) { - pos += 4 * version; // don't care about layerinfolength - JXL_DEBUG_V(PSD_VERBOSITY, "Layer section length: %" PRIuS, layerlength); - int layercount = static_cast(get_be_int(2, pos, maxpos)); - JXL_DEBUG_V(PSD_VERBOSITY, "Layer count: %i", layercount); - io->frames.clear(); - - if (layercount == 0) { - if (get_be_int(2, pos, maxpos) != 0) { - return JXL_FAILURE( - "PSD: Expected zero padding before additional layer info"); - } - while (pos < after_layers_pos) { - if (safe_strncmp(pos, maxpos, "8BIM", 4) && - safe_strncmp(pos, maxpos, "8B64", 4)) - return JXL_FAILURE("PSD: Unexpected layer info signature"); - pos += 4; - const uint8_t* tpos = pos; - pos += 4; - size_t blocklength = get_be_int(4 * version, pos, maxpos); - JXL_DEBUG_V(PSD_VERBOSITY, "Length=%" PRIuS, blocklength); - if (blocklength > 0) { - if (pos >= maxpos) return JXL_FAILURE("PSD: Unexpected end of file"); - size_t delta = maxpos - pos; - if (delta < blocklength) { - return JXL_FAILURE("PSD: Invalid block length"); - } - } - if (!safe_strncmp(tpos, maxpos, "Layr", 4) || - !safe_strncmp(tpos, maxpos, "Lr16", 4) || - !safe_strncmp(tpos, maxpos, "Lr32", 4)) { - layercount = static_cast(get_be_int(2, pos, maxpos)); - if (layercount < 0) { - return JXL_FAILURE("PSD: Invalid layer count"); - } - JXL_DEBUG_V(PSD_VERBOSITY, "Real layer count: %i", layercount); - if (layercount > 1) have_alpha = true; - break; - } - if (!safe_strncmp(tpos, maxpos, "Mtrn", 4) || - !safe_strncmp(tpos, maxpos, "Mt16", 4) || - !safe_strncmp(tpos, maxpos, "Mt32", 4)) { - JXL_DEBUG_V(PSD_VERBOSITY, "Merged layer has transparency channel"); - if (nb_channels > real_nb_channels) { - have_alpha = true; - merged_has_alpha = true; - } - } - pos += blocklength; - } - } else if (layercount < 0) { - // negative layer count indicates merged has alpha and it is to be shown - if (nb_channels > real_nb_channels) { - have_alpha = true; - merged_has_alpha = true; - } - layercount = -layercount; - } else { - // multiple layers implies there is alpha - have_alpha = true; - } - - ExtraChannelInfo info; - info.bit_depth.bits_per_sample = bitdepth; - info.dim_shift = 0; - - if (colormodel == 4) { // cmyk - info.type = ExtraChannel::kBlack; - io->metadata.m.extra_channel_info.push_back(info); - } - if (have_alpha) { - JXL_DEBUG_V(PSD_VERBOSITY, "Have alpha"); - real_nb_channels++; - info.type = ExtraChannel::kAlpha; - info.alpha_associated = - false; // true? PSD is not consistent with this, need to check - io->metadata.m.extra_channel_info.push_back(info); - } - if (merged_has_alpha && !spotcolor.empty() && spotcolor[0][5] == 1) { - // first alpha channel - spotcolor.erase(spotcolor.begin()); - } - for (size_t i = 0; i < spotcolor.size(); i++) { - real_nb_channels++; - if (spotcolor[i][5] == 2) { - info.type = ExtraChannel::kSpotColor; - info.spot_color[0] = spotcolor[i][0]; - info.spot_color[1] = spotcolor[i][1]; - info.spot_color[2] = spotcolor[i][2]; - info.spot_color[3] = spotcolor[i][4]; - } else if (spotcolor[i][5] == 1) { - info.type = ExtraChannel::kAlpha; - } else if (spotcolor[i][5] == 0) { - info.type = ExtraChannel::kSelectionMask; - } else - return JXL_FAILURE("PSD: unhandled extra channel"); - io->metadata.m.extra_channel_info.push_back(info); - } - std::vector> layer_chan_id; - std::vector layer_offsets(layercount + 1, 0); - std::vector is_real_layer(layercount, false); - for (int l = 0; l < layercount; l++) { - ImageBundle layer(&io->metadata.m); - layer.duration = 0; - layer.blend = (l > 0); - - layer.use_for_next_frame = (l + 1 < layercount); - layer.origin.y0 = get_be_int(4, pos, maxpos); - layer.origin.x0 = get_be_int(4, pos, maxpos); - size_t height = get_be_int(4, pos, maxpos) - layer.origin.y0; - size_t width = get_be_int(4, pos, maxpos) - layer.origin.x0; - JXL_DEBUG_V(PSD_VERBOSITY, - "Layer %i: %" PRIuS " x %" PRIuS " at origin (%i, %i)", l, - width, height, layer.origin.x0, layer.origin.y0); - int nb_chs = get_be_int(2, pos, maxpos); - JXL_DEBUG_V(PSD_VERBOSITY, " channels: %i", nb_chs); - std::vector chan_ids; - layer_offsets[l + 1] = layer_offsets[l]; - for (int lc = 0; lc < nb_chs; lc++) { - int id = get_be_int(2, pos, maxpos); - JXL_DEBUG_V(PSD_VERBOSITY, " id=%i", id); - if (id == 65535) { - chan_ids.push_back(colormodel); // alpha - } else if (id == 65534) { - chan_ids.push_back(-1); // layer mask, ignored - } else { - chan_ids.push_back(id); // color channel - } - layer_offsets[l + 1] += get_be_int(4 * version, pos, maxpos); - } - layer_chan_id.push_back(chan_ids); - if (safe_strncmp(pos, maxpos, "8BIM", 4)) - return JXL_FAILURE("PSD: Layer %i: Unexpected signature (not 8BIM)", l); - pos += 4; - if (safe_strncmp(pos, maxpos, "norm", 4)) { - return JXL_FAILURE( - "PSD: Layer %i: Cannot handle non-default blend mode", l); - } - pos += 4; - int opacity = get_be_int(1, pos, maxpos); - if (opacity < 100) { - JXL_WARNING( - "PSD: ignoring opacity of semi-transparent layer %i (opacity=%i)", - l, opacity); - } - pos++; // clipping - int flags = get_be_int(1, pos, maxpos); - pos++; - bool invisible = (flags & 2); - if (invisible) { - if (l + 1 < layercount) { - layer.blend = false; - layer.use_for_next_frame = false; - } else { - // TODO: instead add dummy last frame? - JXL_WARNING("PSD: invisible top layer was made visible"); - } - } - size_t extradata = get_be_int(4, pos, maxpos); - JXL_DEBUG_V(PSD_VERBOSITY, " extradata: %" PRIuS " bytes", extradata); - const uint8_t* after_extra = pos + extradata; - // TODO: deal with non-empty layer masks - pos += get_be_int(4, pos, maxpos); // skip layer mask data - pos += get_be_int(4, pos, maxpos); // skip layer blend range data - size_t namelength = get_be_int(1, pos, maxpos); - size_t delta = maxpos - pos; - if (delta < namelength) return JXL_FAILURE("PSD: Invalid block length"); - char lname[256] = {}; - memcpy(lname, pos, namelength); - lname[namelength] = 0; - JXL_DEBUG_V(PSD_VERBOSITY, " name: %s", lname); - pos = after_extra; - if (width == 0 || height == 0) { - JXL_DEBUG_V(PSD_VERBOSITY, - " NOT A REAL LAYER"); // probably layer group - continue; - } - is_real_layer[l] = true; - JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, width, height)); - uint64_t pixel_count = static_cast(width) * height; - if (!SafeAdd(total_pixel_count, pixel_count, total_pixel_count)) { - return JXL_FAILURE("Image too big"); - } - if (total_pixel_count > constraints->dec_max_pixels) { - return JXL_FAILURE("Image too big"); - } - Image3F rgb(width, height); - layer.SetFromImage(std::move(rgb), io->metadata.m.color_encoding); - std::vector ec; - for (const auto& ec_meta : layer.metadata()->extra_channel_info) { - ImageF extra(width, height); - if (ec_meta.type == ExtraChannel::kAlpha) { - FillPlane(1.0f, &extra, Rect(extra)); // opaque - } else { - ZeroFillPlane(&extra, Rect(extra)); // zeroes - } - ec.push_back(std::move(extra)); - } - if (!ec.empty()) layer.SetExtraChannels(std::move(ec)); - layer.name = lname; - io->dec_pixels += layer.xsize() * layer.ysize(); - io->frames.push_back(std::move(layer)); - } - - std::vector invert(real_nb_channels, false); - int il = 0; - const uint8_t* bpos = pos; - for (int l = 0; l < layercount; l++) { - if (!is_real_layer[l]) continue; - pos = bpos + layer_offsets[l]; - if (pos < bpos) return JXL_FAILURE("PSD: invalid layer offset"); - JXL_DEBUG_V(PSD_VERBOSITY, "At position %i (%" PRIuS ")", - (int)(pos - bytes.data()), (size_t)pos); - ImageBundle& layer = io->frames[il++]; - std::vector& chan_id = layer_chan_id[l]; - if (chan_id.size() > invert.size()) invert.resize(chan_id.size(), false); - JXL_RETURN_IF_ERROR(decode_layer(pos, maxpos, layer, chan_id, invert, - layer.xsize(), layer.ysize(), version, - colormodel, true, bitdepth)); - } - } else - return JXL_FAILURE("PSD: no layer data found"); - - if (!hasmergeddata && !spotcolor.empty()) { - return JXL_FAILURE("PSD: extra channel data declared but not found"); - } - - if (!spotcolor.empty() || (hasmergeddata && io->frames.empty())) { - // PSD only has spot colors / extra alpha/mask data in the merged image - // We don't redundantly store the merged image, so we put it in the first - // layer (the next layers will kAdd zeroes to it) - pos = after_layers_pos; - bool have_only_merged = false; - if (io->frames.empty()) { - // There is only the merged image, no layers - ImageBundle nlayer(&io->metadata.m); - Image3F rgb(xsize, ysize); - nlayer.SetFromImage(std::move(rgb), io->metadata.m.color_encoding); - std::vector ec; - for (const auto& ec_meta : nlayer.metadata()->extra_channel_info) { - ImageF extra(xsize, ysize); - if (ec_meta.type == ExtraChannel::kAlpha) { - FillPlane(1.0f, &extra, Rect(extra)); // opaque - } else { - ZeroFillPlane(&extra, Rect(extra)); // zeroes - } - ec.push_back(std::move(extra)); - } - if (!ec.empty()) nlayer.SetExtraChannels(std::move(ec)); - io->dec_pixels += nlayer.xsize() * nlayer.ysize(); - io->frames.push_back(std::move(nlayer)); - have_only_merged = true; - } - ImageBundle& layer = io->frames[0]; - std::vector chan_id(real_nb_channels); - std::iota(chan_id.begin(), chan_id.end(), 0); - std::vector invert(real_nb_channels, false); - if (static_cast(spotcolor.size()) + colormodel + 1 < - real_nb_channels) { - return JXL_FAILURE("Inconsistent layer configuration"); - } - if (!merged_has_alpha) { - if (colormodel >= real_nb_channels) { - return JXL_FAILURE("Inconsistent layer configuration"); - } - chan_id.erase(chan_id.begin() + colormodel); - invert.erase(invert.begin() + colormodel); - } else { - colormodel++; - } - for (size_t i = colormodel; i < invert.size(); i++) { - if (spotcolor[i - colormodel][5] == 2) invert[i] = true; - if (spotcolor[i - colormodel][5] == 0) invert[i] = true; - } - JXL_RETURN_IF_ERROR(decode_layer( - pos, maxpos, layer, chan_id, invert, layer.xsize(), layer.ysize(), - version, (have_only_merged ? 0 : colormodel), false, bitdepth)); - } - - if (io->frames.empty()) return JXL_FAILURE("PSD: no layers"); - - io->SetSize(xsize, ysize); - - SetIntensityTarget(io); - - return true; -} - -Status EncodeImagePSD(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes) { - return JXL_FAILURE("PSD encoding not yet implemented"); -} - -} // namespace extras -} // namespace jxl diff --git a/third_party/jpeg-xl/lib/extras/dec/apng.cc b/third_party/jpeg-xl/lib/extras/dec/apng.cc --- a/third_party/jpeg-xl/lib/extras/dec/apng.cc +++ b/third_party/jpeg-xl/lib/extras/dec/apng.cc @@ -41,10 +41,11 @@ #include #include #include +#include "jxl/codestream_header.h" #include "jxl/encode.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/base/scope_guard.h" #include "lib/jxl/common.h" @@ -668,11 +669,12 @@ bool should_blend = frame.blend_op == BLEND_OP_OVER; bool use_for_next_frame = has_nontrivial_background && frame.dispose_op != DISPOSE_OP_PREVIOUS; size_t x0 = frame.x0; size_t y0 = frame.y0; - + size_t xsize = frame.data.xsize; + size_t ysize = frame.data.ysize; if (previous_frame_should_be_cleared) { size_t xs = frame.data.xsize; size_t ys = frame.data.ysize; size_t px0 = frames[i - 1].x0; size_t py0 = frames[i - 1].y0; @@ -708,102 +710,54 @@ xs * bytes_per_pixel); } x0 = px0; y0 = py0; + xsize = pxs; + ysize = pys; should_blend = false; ppf->frames.emplace_back(std::move(new_data)); } else { // If all else fails, insert a dummy blank frame with kReplace. PackedImage blank(pxs, pys, frame.data.format); memset(blank.pixels(), 0, blank.pixels_size); ppf->frames.emplace_back(std::move(blank)); auto& pframe = ppf->frames.back(); - pframe.x0 = px0; - pframe.y0 = py0; + pframe.frame_info.layer_info.crop_x0 = px0; + pframe.frame_info.layer_info.crop_y0 = py0; + pframe.frame_info.layer_info.xsize = frame.xsize; + pframe.frame_info.layer_info.ysize = frame.ysize; pframe.frame_info.duration = 0; - pframe.blend = false; - pframe.use_for_next_frame = true; - + pframe.frame_info.layer_info.have_crop = 0; + pframe.frame_info.layer_info.blend_info.blendmode = JXL_BLEND_REPLACE; + pframe.frame_info.layer_info.blend_info.source = 0; + pframe.frame_info.layer_info.save_as_reference = 1; ppf->frames.emplace_back(std::move(frame.data)); } } else { ppf->frames.emplace_back(std::move(frame.data)); } auto& pframe = ppf->frames.back(); - pframe.x0 = x0; - pframe.y0 = y0; + pframe.frame_info.layer_info.crop_x0 = x0; + pframe.frame_info.layer_info.crop_y0 = y0; + pframe.frame_info.layer_info.xsize = xsize; + pframe.frame_info.layer_info.ysize = ysize; pframe.frame_info.duration = frame.duration; - pframe.blend = should_blend; - pframe.use_for_next_frame = use_for_next_frame; + pframe.frame_info.layer_info.blend_info.blendmode = + should_blend ? JXL_BLEND_BLEND : JXL_BLEND_REPLACE; + pframe.frame_info.layer_info.have_crop = 1; + pframe.frame_info.layer_info.blend_info.source = should_blend ? 1 : 0; + pframe.frame_info.layer_info.blend_info.alpha = 0; + pframe.frame_info.layer_info.save_as_reference = use_for_next_frame ? 1 : 0; - if (has_nontrivial_background && - frame.dispose_op == DISPOSE_OP_BACKGROUND) { - previous_frame_should_be_cleared = true; - } else { - previous_frame_should_be_cleared = false; - } + previous_frame_should_be_cleared = + has_nontrivial_background && frame.dispose_op == DISPOSE_OP_BACKGROUND; } if (ppf->frames.empty()) return JXL_FAILURE("No frames decoded"); ppf->frames.back().frame_info.is_last = true; return true; } -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); -} - -// Stores XMP and EXIF/IPTC into key/value strings for PNG -class BlobsWriterPNG { - public: - static Status Encode(const Blobs& blobs, std::vector* strings) { - if (!blobs.exif.empty()) { - JXL_RETURN_IF_ERROR(EncodeBase16("exif", blobs.exif, strings)); - } - if (!blobs.iptc.empty()) { - JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings)); - } - if (!blobs.xmp.empty()) { - JXL_RETURN_IF_ERROR(EncodeBase16("xmp", blobs.xmp, strings)); - } - return true; - } - - private: - static JXL_INLINE char EncodeNibble(const uint8_t nibble) { - JXL_ASSERT(nibble < 16); - return (nibble < 10) ? '0' + nibble : 'a' + nibble - 10; - } - - static Status EncodeBase16(const std::string& type, const PaddedBytes& bytes, - std::vector* strings) { - // Encoding: base16 with newline after 72 chars. - const size_t base16_size = - 2 * bytes.size() + DivCeil(bytes.size(), size_t(36)) + 1; - std::string base16; - base16.reserve(base16_size); - for (size_t i = 0; i < bytes.size(); ++i) { - if (i % 36 == 0) base16.push_back('\n'); - base16.push_back(EncodeNibble(bytes[i] >> 4)); - base16.push_back(EncodeNibble(bytes[i] & 0x0F)); - } - base16.push_back('\n'); - JXL_ASSERT(base16.length() == base16_size); - - char key[30]; - snprintf(key, sizeof(key), "Raw profile type %s", type.c_str()); - - char header[30]; - snprintf(header, sizeof(header), "\n%s\n%8" PRIuS, type.c_str(), - bytes.size()); - - strings->push_back(std::string(key)); - strings->push_back(std::string(header) + base16); - return true; - } -}; - } // namespace extras } // namespace jxl diff --git a/third_party/jpeg-xl/lib/extras/dec/decode.h b/third_party/jpeg-xl/lib/extras/dec/decode.h --- a/third_party/jpeg-xl/lib/extras/dec/decode.h +++ b/third_party/jpeg-xl/lib/extras/dec/decode.h @@ -31,12 +31,11 @@ kPNG, kPNM, kPGX, kJPG, kGIF, - kEXR, - kPSD + kEXR }; static inline constexpr uint64_t EnumBits(Codec /*unused*/) { // Return only fully-supported codecs (kGIF is decode-only). return MakeBit(Codec::kPNM) @@ -47,11 +46,11 @@ | MakeBit(Codec::kJPG) #endif #if JPEGXL_ENABLE_EXR | MakeBit(Codec::kEXR) #endif - | MakeBit(Codec::kPSD); + ; } // Lower case ASCII including dot, e.g. ".png". std::string ExtensionFromCodec(Codec codec, bool is_gray, size_t bits_per_sample); diff --git a/third_party/jpeg-xl/lib/extras/dec/decode.cc b/third_party/jpeg-xl/lib/extras/dec/decode.cc --- a/third_party/jpeg-xl/lib/extras/dec/decode.cc +++ b/third_party/jpeg-xl/lib/extras/dec/decode.cc @@ -45,12 +45,10 @@ return (bits_per_sample == 32) ? ".pfm" : ".ppm"; case Codec::kGIF: return ".gif"; case Codec::kEXR: return ".exr"; - case Codec::kPSD: - return ".psd"; case Codec::kUnknown: return std::string(); } JXL_UNREACHABLE; return std::string(); @@ -81,12 +79,10 @@ if (extension == ".gif") return Codec::kGIF; if (extension == ".exr") return Codec::kEXR; - if (extension == ".psd") return Codec::kPSD; - return Codec::kUnknown; } Status DecodeBytes(const Span bytes, const ColorHints& color_hints, diff --git a/third_party/jpeg-xl/lib/extras/dec/gif.cc b/third_party/jpeg-xl/lib/extras/dec/gif.cc --- a/third_party/jpeg-xl/lib/extras/dec/gif.cc +++ b/third_party/jpeg-xl/lib/extras/dec/gif.cc @@ -10,10 +10,11 @@ #include #include #include +#include "jxl/codestream_header.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/sanitizers.h" namespace jxl { namespace extras { @@ -255,19 +256,30 @@ msan::UnpoisonMemory(color_map->Colors, sizeof(*color_map->Colors) * color_map->ColorCount); GraphicsControlBlock gcb; DGifSavedExtensionToGCB(gif.get(), i, &gcb); msan::UnpoisonMemory(&gcb, sizeof(gcb)); - + bool is_full_size = total_rect.x0() == 0 && total_rect.y0() == 0 && + total_rect.xsize() == canvas.color.xsize && + total_rect.ysize() == canvas.color.ysize; if (ppf->info.have_animation) { frame->frame_info.duration = gcb.DelayTime; - frame->x0 = total_rect.x0(); - frame->y0 = total_rect.y0(); + frame->frame_info.layer_info.have_crop = static_cast(!is_full_size); + frame->frame_info.layer_info.crop_x0 = total_rect.x0(); + frame->frame_info.layer_info.crop_y0 = total_rect.y0(); + frame->frame_info.layer_info.xsize = frame->color.xsize; + frame->frame_info.layer_info.ysize = frame->color.ysize; if (last_base_was_none) { replace = true; } - frame->blend = !replace; + frame->frame_info.layer_info.blend_info.blendmode = + replace ? JXL_BLEND_REPLACE : JXL_BLEND_BLEND; + // We always only reference at most the last frame + frame->frame_info.layer_info.blend_info.source = + last_base_was_none ? 0u : 1u; + frame->frame_info.layer_info.blend_info.clamp = 1; + frame->frame_info.layer_info.blend_info.alpha = 0; // TODO(veluca): this could in principle be implemented. if (last_base_was_none && (total_rect.x0() != 0 || total_rect.y0() != 0 || total_rect.xsize() != canvas.color.xsize || total_rect.ysize() != canvas.color.ysize || !replace)) { @@ -276,18 +288,18 @@ "blended frames"); } switch (gcb.DisposalMode) { case DISPOSE_DO_NOT: case DISPOSE_BACKGROUND: - frame->use_for_next_frame = true; + frame->frame_info.layer_info.save_as_reference = 1u; last_base_was_none = false; break; case DISPOSE_PREVIOUS: - frame->use_for_next_frame = false; + frame->frame_info.layer_info.save_as_reference = 0u; break; default: - frame->use_for_next_frame = false; + frame->frame_info.layer_info.save_as_reference = 0u; last_base_was_none = true; } } // Update the canvas by creating a copy first. diff --git a/third_party/jpeg-xl/lib/extras/dec/pnm.cc b/third_party/jpeg-xl/lib/extras/dec/pnm.cc --- a/third_party/jpeg-xl/lib/extras/dec/pnm.cc +++ b/third_party/jpeg-xl/lib/extras/dec/pnm.cc @@ -274,11 +274,16 @@ JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); // The scale has no meaning as multiplier, only its sign is used to // indicate endianness. All software expects nominal range 0..1. double scale; JXL_RETURN_IF_ERROR(ParseSigned(&scale)); - header->big_endian = scale >= 0.0; + if (scale == 0.0) { + return JXL_FAILURE("PFM: bad scale factor value."); + } else if (std::abs(scale) != 1.0) { + JXL_WARNING("PFM: Discarding non-unit scale factor"); + } + header->big_endian = scale > 0.0; header->bits_per_sample = 32; header->floating_point = true; JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); @@ -361,10 +366,11 @@ }; ppf->frames.clear(); ppf->frames.emplace_back(header.xsize, header.ysize, format); auto* frame = &ppf->frames.back(); + frame->color.bitdepth_from_format = false; frame->color.flipped_y = header.bits_per_sample == 32; // PFMs are flipped size_t pnm_remaining_size = bytes.data() + bytes.size() - pos; if (pnm_remaining_size < frame->color.pixels_size) { return JXL_FAILURE("PNM file too small"); } 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 @@ -47,10 +47,11 @@ #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/dec_external_image.h" #include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_image_bundle.h" +#include "lib/jxl/exif.h" #include "lib/jxl/frame_header.h" #include "lib/jxl/headers.h" #include "lib/jxl/image.h" #include "lib/jxl/image_bundle.h" #include "png.h" /* original (unpatched) libpng is ok */ @@ -68,11 +69,16 @@ // Stores XMP and EXIF/IPTC into key/value strings for PNG class BlobsWriterPNG { public: static Status Encode(const Blobs& blobs, std::vector* strings) { if (!blobs.exif.empty()) { - JXL_RETURN_IF_ERROR(EncodeBase16("exif", blobs.exif, strings)); + // PNG viewers typically ignore Exif orientation but not all of them do + // (and e.g. cjxl doesn't), so we overwrite the Exif orientation to the + // identity to avoid repeated orientation. + PaddedBytes exif(blobs.exif); + ResetExifOrientation(exif); + JXL_RETURN_IF_ERROR(EncodeBase16("exif", exif, strings)); } if (!blobs.iptc.empty()) { JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings)); } if (!blobs.xmp.empty()) { 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 @@ -17,12 +17,14 @@ #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" #include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_image_bundle.h" +#include "lib/jxl/exif.h" #include "lib/jxl/image.h" #include "lib/jxl/image_bundle.h" #include "lib/jxl/sanitizers.h" #if JPEGXL_ENABLE_SJPEG #include "sjpeg.h" @@ -31,27 +33,19 @@ namespace jxl { namespace extras { namespace { -constexpr float kJPEGSampleMultiplier = MAXJSAMPLE; constexpr unsigned char kICCSignature[12] = { 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00}; constexpr int kICCMarker = JPEG_APP0 + 2; constexpr size_t kMaxBytesInMarker = 65533; constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, 0x66, 0x00, 0x00}; constexpr int kExifMarker = JPEG_APP0 + 1; -constexpr float kJPEGSampleMin = 0; -constexpr float kJPEGSampleMax = MAXJSAMPLE; - -// TODO (jon): take orientation into account when writing jpeg output -// TODO (jon): write Exif blob also in sjpeg encoding -// TODO (jon): overwrite orientation in Exif blob to avoid double orientation - void WriteICCProfile(jpeg_compress_struct* const cinfo, const PaddedBytes& icc) { constexpr size_t kMaxIccBytesInMarker = kMaxBytesInMarker - sizeof kICCSignature - 2; const int num_markers = @@ -72,18 +66,17 @@ ++begin; } } } void WriteExif(jpeg_compress_struct* const cinfo, const PaddedBytes& exif) { - if (exif.size() < 4) return; jpeg_write_m_header( cinfo, kExifMarker, - static_cast(exif.size() - 4 + sizeof kExifSignature)); + static_cast(exif.size() + sizeof kExifSignature)); for (const unsigned char c : kExifSignature) { jpeg_write_m_byte(cinfo, c); } - for (size_t i = 4; i < exif.size(); ++i) { + for (size_t i = 0; i < exif.size(); ++i) { jpeg_write_m_byte(cinfo, exif[i]); } } Status SetChromaSubsampling(const YCbCrChromaSubsampling& chroma_subsampling, @@ -113,12 +106,12 @@ 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->xsize(); - cinfo.image_height = ib->ysize(); + 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; @@ -132,31 +125,31 @@ jpeg_set_quality(&cinfo, quality, TRUE); jpeg_start_compress(&cinfo, TRUE); if (!ib->IsSRGB()) { WriteICCProfile(&cinfo, ib->c_current().ICC()); } - WriteExif(&cinfo, io->blobs.exif); + if (!io->blobs.exif.empty()) { + PaddedBytes exif(io->blobs.exif); + ResetExifOrientation(exif); + WriteExif(&cinfo, exif); + } if (cinfo.input_components > 3 || cinfo.input_components < 0) return JXL_FAILURE("invalid numbers of components"); - std::unique_ptr row( - new JSAMPLE[cinfo.input_components * cinfo.image_width]); - for (size_t y = 0; y < ib->ysize(); ++y) { - const float* const JXL_RESTRICT input_row[3] = { - ib->color().ConstPlaneRow(0, y), ib->color().ConstPlaneRow(1, y), - ib->color().ConstPlaneRow(2, y)}; - for (size_t x = 0; x < ib->xsize(); ++x) { - for (size_t c = 0; c < static_cast(cinfo.input_components); ++c) { - JXL_RETURN_IF_ERROR(c < 3); - row[cinfo.input_components * x + c] = static_cast( - std::max(std::min(kJPEGSampleMultiplier * input_row[c][x] + .5f, - kJPEGSampleMax), - kJPEGSampleMin)); - } - } - JSAMPROW rows[] = {row.get()}; - jpeg_write_scanlines(&cinfo, rows, 1); + size_t stride = + ib->oriented_xsize() * cinfo.input_components * sizeof(JSAMPLE); + PaddedBytes raw_bytes(stride * ib->oriented_ysize()); + JXL_RETURN_IF_ERROR(ConvertToExternal( + *ib, BITS_IN_JSAMPLE, /*float_out=*/false, cinfo.input_components, + JXL_BIG_ENDIAN, stride, nullptr, raw_bytes.data(), raw_bytes.size(), + /*out_callback=*/nullptr, + /*out_opaque=*/nullptr, ib->metadata()->GetOrientation())); + + 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); jpeg_destroy_compress(&cinfo); bytes->resize(size); // Compressed image data is initialized by libjpeg, which we are not @@ -165,46 +158,45 @@ std::copy_n(buffer, size, bytes->data()); std::free(buffer); return true; } -Status EncodeWithSJpeg(const ImageBundle* ib, size_t quality, +Status EncodeWithSJpeg(const ImageBundle* ib, const CodecInOut* io, + size_t quality, const YCbCrChromaSubsampling& chroma_subsampling, PaddedBytes* 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()); } + PaddedBytes exif(io->blobs.exif); + if (!exif.empty()) { + ResetExifOrientation(exif); + param.exif.assign(exif.begin(), exif.end()); + } if (chroma_subsampling.Is444()) { param.yuv_mode = SJPEG_YUV_444; } else if (chroma_subsampling.Is420()) { param.yuv_mode = SJPEG_YUV_SHARP; } else { return JXL_FAILURE("sjpeg does not support this chroma subsampling mode"); } - std::vector rgb; - rgb.reserve(ib->xsize() * ib->ysize() * 3); - for (size_t y = 0; y < ib->ysize(); ++y) { - const float* const rows[] = { - ib->color().ConstPlaneRow(0, y), - ib->color().ConstPlaneRow(1, y), - ib->color().ConstPlaneRow(2, y), - }; - for (size_t x = 0; x < ib->xsize(); ++x) { - for (const float* const row : rows) { - rgb.push_back(static_cast( - std::max(0.f, std::min(255.f, roundf(255.f * row[x]))))); - } - } - } + size_t stride = ib->oriented_xsize() * 3; + PaddedBytes rgb(ib->xsize() * ib->ysize() * 3); + JXL_RETURN_IF_ERROR(ConvertToExternal( + *ib, 8, /*float_out=*/false, 3, JXL_BIG_ENDIAN, stride, nullptr, + rgb.data(), rgb.size(), /*out_callback=*/nullptr, + /*out_opaque=*/nullptr, ib->metadata()->GetOrientation())); + std::string output; - JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib->xsize(), ib->ysize(), - ib->xsize() * 3, param, &output)); + 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 @@ -232,11 +224,11 @@ JXL_RETURN_IF_ERROR( EncodeWithLibJpeg(ib, io, quality, chroma_subsampling, bytes)); break; case JpegEncoder::kSJpeg: JXL_RETURN_IF_ERROR( - EncodeWithSJpeg(ib, quality, chroma_subsampling, bytes)); + EncodeWithSJpeg(ib, io, quality, chroma_subsampling, bytes)); break; default: return JXL_FAILURE("tried to use an unknown JPEG encoder"); } diff --git a/third_party/jpeg-xl/lib/extras/packed_image.h b/third_party/jpeg-xl/lib/extras/packed_image.h --- a/third_party/jpeg-xl/lib/extras/packed_image.h +++ b/third_party/jpeg-xl/lib/extras/packed_image.h @@ -62,10 +62,16 @@ size_t ysize; // Whether the y coordinate is flipped (y=0 is the last row). bool flipped_y = false; + // Whether the range is determined by format or by JxlBasicInfo + // e.g. if format is UINT16 and JxlBasicInfo bits_per_sample is 10, + // then if bitdepth_from_format == true, the range is 0..65535 + // while if bitdepth_from_format == true, the range is 0..1023. + bool bitdepth_from_format = true; + // The number of bytes per row. size_t stride; // Pixel storage format and buffer size of the pixels_ pointer. JxlPixelFormat format; @@ -114,20 +120,10 @@ // The Frame metadata. JxlFrameHeader frame_info = {}; std::string name; - // Offset of the frame in the image. - // TODO(deymo): Add support in the API for this. - size_t x0 = 0; - size_t y0 = 0; - - // Whether this frame should be blended with the previous one. - // TODO(deymo): Maybe add support for this in the API. - bool blend = false; - bool use_for_next_frame = false; - // The pixel data for the color (or grayscale) channels. PackedImage color; // Extra channel image data. std::vector extra_channels; }; diff --git a/third_party/jpeg-xl/lib/extras/packed_image_convert.cc b/third_party/jpeg-xl/lib/extras/packed_image_convert.cc --- a/third_party/jpeg-xl/lib/extras/packed_image_convert.cc +++ b/third_party/jpeg-xl/lib/extras/packed_image_convert.cc @@ -105,34 +105,37 @@ io->dec_pixels = 0; io->frames.clear(); for (const auto& frame : ppf.frames) { JXL_ASSERT(frame.color.pixels() != nullptr); size_t frame_bits_per_sample = - frame.color.BitsPerChannel(frame.color.format.data_type); + (frame.color.bitdepth_from_format + ? frame.color.BitsPerChannel(frame.color.format.data_type) + : ppf.info.bits_per_sample); JXL_ASSERT(frame_bits_per_sample != 0); // It is ok for the frame.color.format.num_channels to not match the // number of channels on the image. JXL_ASSERT(1 <= frame.color.format.num_channels && frame.color.format.num_channels <= 4); const Span span( static_cast(frame.color.pixels()), frame.color.pixels_size); - Rect frame_rect = - Rect(frame.x0, frame.y0, frame.color.xsize, frame.color.ysize); + Rect frame_rect = Rect(frame.frame_info.layer_info.crop_x0, + frame.frame_info.layer_info.crop_y0, + frame.frame_info.layer_info.xsize, + frame.frame_info.layer_info.ysize); JXL_ASSERT(frame_rect.IsInside(Rect(0, 0, ppf.info.xsize, ppf.info.ysize))); - ImageBundle bundle(&io->metadata.m); if (ppf.info.have_animation) { bundle.duration = frame.frame_info.duration; - bundle.blend = frame.blend; - bundle.use_for_next_frame = frame.use_for_next_frame; + bundle.blend = frame.frame_info.layer_info.blend_info.blendmode > 0; + bundle.use_for_next_frame = + frame.frame_info.layer_info.save_as_reference > 0; + bundle.origin.x0 = frame.frame_info.layer_info.crop_x0; + bundle.origin.y0 = frame.frame_info.layer_info.crop_y0; } bundle.name = frame.name; // frame.frame_info.name_length is ignored here. - bundle.origin.x0 = frame.x0; - bundle.origin.y0 = frame.y0; - JXL_ASSERT(io->metadata.m.color_encoding.IsGray() == (frame.color.format.num_channels <= 2)); const bool float_in = frame.color.format.data_type == JXL_TYPE_FLOAT16 || frame.color.format.data_type == JXL_TYPE_FLOAT; @@ -142,15 +145,17 @@ /*alpha_is_premultiplied=*/ppf.info.alpha_premultiplied, frame_bits_per_sample, frame.color.format.endianness, /*flipped_y=*/frame.color.flipped_y, pool, &bundle, /*float_in=*/float_in, /*align=*/0)); - for (const auto& ppf_ec : frame.extra_channels) { - bundle.extra_channels().emplace_back(ppf_ec.xsize, ppf_ec.ysize); + bundle.extra_channels().resize(io->metadata.m.extra_channel_info.size()); + for (size_t i = 0; i < frame.extra_channels.size(); i++) { + const auto& ppf_ec = frame.extra_channels[i]; + bundle.extra_channels()[i] = ImageF(ppf_ec.xsize, ppf_ec.ysize); JXL_CHECK(BufferToImageF(ppf_ec.format, ppf_ec.xsize, ppf_ec.ysize, ppf_ec.pixels(), ppf_ec.pixels_size, pool, - &bundle.extra_channels().back())); + &bundle.extra_channels()[i])); } io->frames.push_back(std::move(bundle)); io->dec_pixels += frame.color.xsize * frame.color.ysize; } 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 @@ -281,10 +281,17 @@ * If non-keyframes, i.e., frames with cropping, blending or patches are * attempted to be indexed, JXL_ENC_ERROR will occur. */ JXL_ENC_FRAME_INDEX_BOX = 31, + /** Sets brotli encode effort for use in JPEG recompression and compressed + * metadata boxes (brob). Can be -1 (default) or 0 (fastest) to 11 (slowest). + * Default is based on the general encode effort in case of JPEG + * recompression, and 4 for brob boxes. + */ + JXL_ENC_FRAME_SETTING_BROTLI_EFFORT = 32, + /** Enum value not to be used as an option. This value is added to force the * C compiler to have the enum to take a known size. */ JXL_ENC_FRAME_SETTING_FILL_ENUM = 65535, @@ -507,10 +514,12 @@ * - trichromatic, e.g. RGB * - trichromatic + alpha * * Extra channels not handled here need to be set by @ref * JxlEncoderSetExtraChannelBuffer. + * If the image has alpha, and alpha is not passed here, it will implicitly be + * set to all-opaque (an alpha value of 1.0 everywhere). * * The color profile of the pixels depends on the value of uses_original_profile * in the JxlBasicInfo. If true, the pixels are assumed to be encoded in the * original profile that is set with JxlEncoderSetColorEncoding or * JxlEncoderSetICCProfile. If false, the pixels are assumed to be nonlinear 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 @@ -104,10 +104,12 @@ jxl/enc_bit_writer.h jxl/entropy_coder.cc jxl/entropy_coder.h jxl/epf.cc jxl/epf.h + jxl/exif.cc + jxl/exif.h jxl/fast_dct-inl.h jxl/fast_dct.cc jxl/fast_dct.h jxl/fast_dct128-inl.h jxl/fast_dct16-inl.h @@ -352,10 +354,11 @@ set(JPEGXL_INTERNAL_LIBS ${JPEGXL_DEC_INTERNAL_LIBS} brotlienc-static Threads::Threads + ${ATOMICS_LIBRARIES} ) # strips the -static suffix from all the elements in LIST function(strip_static OUTPUT_VAR LIB_LIST) foreach(lib IN LISTS ${LIB_LIST}) diff --git a/third_party/jpeg-xl/lib/jxl/dec_file.cc b/third_party/jpeg-xl/lib/jxl/dec_file.cc --- a/third_party/jpeg-xl/lib/jxl/dec_file.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_file.cc @@ -64,11 +64,27 @@ Status ret = true; { BitReader reader(file); BitReaderScopedCloser reader_closer(&reader, &ret); - (void)reader.ReadFixedBits<16>(); // skip marker + if (reader.ReadFixedBits<16>() != 0x0AFF) { + // We don't have a naked codestream. Make a quick & dirty attempt to find + // the codestream. + // TODO(jon): get rid of this whole function + const unsigned char* begin = file.data(); + const unsigned char* end = file.data() + file.size() - 4; + while (begin < end) { + if (!memcmp(begin, "jxlc", 4)) break; + begin++; + } + if (begin >= end) return JXL_FAILURE("Couldn't find jxl codestream"); + reader.SkipBits(8 * (begin - file.data() + 2)); + unsigned int firstbytes = reader.ReadFixedBits<16>(); + if (firstbytes != 0x0AFF) + return JXL_FAILURE("Codestream didn't start with FF0A but with %X", + firstbytes); + } { JXL_RETURN_IF_ERROR(DecodeHeaders(&reader, io)); size_t xsize = io->metadata.xsize(); size_t ysize = io->metadata.ysize(); 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 @@ -263,10 +263,11 @@ decoded->ClearExtraChannels(); decoded->duration = frame_header_.animation_frame.duration; if (!frame_header_.nonserialized_is_preview && + (frame_header_.is_last || frame_header_.animation_frame.duration > 0) && (frame_header_.frame_type == kRegularFrame || frame_header_.frame_type == kSkipProgressive)) { ++dec_state_->visible_frame_index; dec_state_->nonvisible_frame_index = 0; } else { 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 @@ -87,10 +87,20 @@ namespace jxl { HWY_EXPORT(MultiplySum); // Local function HWY_EXPORT(RgbFromSingle); // Local function HWY_EXPORT(SingleFromSingle); // Local function +// Slow conversion using double precision multiplication, only +// needed when the bit depth is too high for single precision +void SingleFromSingleAccurate(const size_t xsize, + const pixel_type* const JXL_RESTRICT row_in, + const double factor, float* row_out) { + for (size_t x = 0; x < xsize; x++) { + row_out[x] = row_in[x] * factor; + } +} + // convert custom [bits]-bit float (with [exp_bits] exponent bits) stored as int // back to binary32 float void int_to_float(const pixel_type* const JXL_RESTRICT row_in, float* const JXL_RESTRICT row_out, const size_t xsize, const int bits, const int exp_bits) { @@ -468,13 +478,13 @@ metadata->m.color_encoding.IsGray() && frame_header.color_transform == ColorTransform::kNone; const bool fp = metadata->m.bit_depth.floating_point_sample && frame_header.color_transform != ColorTransform::kXYB; for (; c < 3; c++) { - float factor = full_image.bitdepth < 32 - ? 1.f / ((1u << full_image.bitdepth) - 1) - : 0; + double factor = full_image.bitdepth < 32 + ? 1.0 / ((1u << full_image.bitdepth) - 1) + : 0; size_t c_in = c; if (frame_header.color_transform == ColorTransform::kXYB) { factor = dec_state->shared->matrices.DCQuants()[c]; // XYB is encoded as YX(B-Y) if (c < 2) c_in = 1 - c; @@ -544,17 +554,31 @@ [&](const uint32_t task, size_t /* thread */) { const size_t y = task; const pixel_type* const JXL_RESTRICT row_in = mr.Row(&ch_in.plane, y); if (rgb_from_gray) { - HWY_DYNAMIC_DISPATCH(RgbFromSingle) - (xsize_shifted, row_in, factor, get_row(r, 0, y), - get_row(r, 1, y), get_row(r, 2, y)); + if (full_image.bitdepth < 23) { + HWY_DYNAMIC_DISPATCH(RgbFromSingle) + (xsize_shifted, row_in, factor, get_row(r, 0, y), + get_row(r, 1, y), get_row(r, 2, y)); + } else { + SingleFromSingleAccurate(xsize_shifted, row_in, factor, + get_row(r, 0, y)); + SingleFromSingleAccurate(xsize_shifted, row_in, factor, + get_row(r, 0, y)); + SingleFromSingleAccurate(xsize_shifted, row_in, factor, + get_row(r, 0, y)); + } } else { float* const JXL_RESTRICT row_out = get_row(r, c, y); - HWY_DYNAMIC_DISPATCH(SingleFromSingle) - (xsize_shifted, row_in, factor, row_out); + if (full_image.bitdepth < 23) { + HWY_DYNAMIC_DISPATCH(SingleFromSingle) + (xsize_shifted, row_in, factor, row_out); + } else { + SingleFromSingleAccurate(xsize_shifted, row_in, factor, + row_out); + } } }, "ModularIntToFloat")); } if (rgb_from_gray) { @@ -570,11 +594,11 @@ const ExtraChannelInfo& eci = metadata->m.extra_channel_info[ec]; int bits = eci.bit_depth.bits_per_sample; int exp_bits = eci.bit_depth.exponent_bits_per_sample; bool fp = eci.bit_depth.floating_point_sample; JXL_ASSERT(fp || bits < 32); - const float mul = fp ? 0 : (1.0f / ((1u << bits) - 1)); + const double factor = fp ? 0 : (1.0 / ((1u << bits) - 1)); JXL_ASSERT(c < gi.channel.size()); Channel& ch_in = gi.channel[c]; Rect r = render_pipeline_input.GetBuffer(3 + ec).second; Rect mr(modular_rect.x0() >> ch_in.hshift, modular_rect.y0() >> ch_in.vshift, @@ -587,12 +611,15 @@ r.Row(render_pipeline_input.GetBuffer(3 + ec).first, y); const pixel_type* const JXL_RESTRICT row_in = mr.Row(&ch_in.plane, y); if (fp) { int_to_float(row_in, row_out, r.xsize(), bits, exp_bits); } else { - for (size_t x = 0; x < r.xsize(); ++x) { - row_out[x] = row_in[x] * mul; + if (full_image.bitdepth < 23) { + HWY_DYNAMIC_DISPATCH(SingleFromSingle) + (r.xsize(), row_in, factor, row_out); + } else { + SingleFromSingleAccurate(r.xsize(), row_in, factor, row_out); } } } } return true; 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 @@ -607,10 +607,15 @@ namespace { bool CheckSizeLimit(JxlDecoder* dec, size_t xsize, size_t ysize) { if (!dec->memory_limit_base) return true; if (xsize == 0 || ysize == 0) return true; + if (xsize >= dec->memory_limit_base || ysize >= dec->memory_limit_base) { + return false; + } + // Rough estimate of real row length. + xsize = jxl::DivCeil(xsize, 32) * 32; size_t num_pixels = xsize * ysize; if (num_pixels / xsize != ysize) return false; // overflow if (num_pixels > dec->memory_limit_base) return false; return true; } @@ -729,11 +734,11 @@ JxlDecoder* dec = new (alloc) JxlDecoder(); dec->memory_manager = local_memory_manager; #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION if (!memory_manager) { - dec->memory_limit_base = 1 << 21; + dec->memory_limit_base = 53 << 16; // Allow 5 x max_image_size processing units; every frame is accounted // as W x H CPU processing units, so there could be numerous small frames // or few larger ones. dec->cpu_limit_base = 5 * dec->memory_limit_base; } @@ -1063,10 +1068,11 @@ ? jxl::Orientation::kIdentity : dec->metadata.m.GetOrientation(); jxl::Status status(true); if (want_extra_channel) { + JXL_ASSERT(extra_channel_index < frame.extra_channels().size()); status = jxl::ConvertToExternal( frame.extra_channels()[extra_channel_index], BitsPerChannel(format.data_type), float_format, format.endianness, stride, dec->thread_pool.get(), out_image, out_size, /*out_callback=*/out_callback, @@ -1561,18 +1567,24 @@ dec->image_out_opaque); if (status != JXL_DEC_SUCCESS) return status; } dec->image_out_buffer_set = false; + bool has_ec = !dec->ib->extra_channels().empty(); for (size_t i = 0; i < dec->extra_channel_output.size(); ++i) { void* buffer = dec->extra_channel_output[i].buffer; // buffer nullptr indicates this extra channel is not requested if (!buffer) continue; + if (!has_ec) { + JXL_WARNING( + "Extra channels are not supported when callback is used"); + return JXL_DEC_ERROR; + } const JxlPixelFormat* format = &dec->extra_channel_output[i].format; JxlDecoderStatus status = ConvertImageInternal( dec, *dec->ib, *format, - /*want_extra_channel=*/true, i, buffer, + /*want_extra_channel=*/true, /*extra_channel_index=*/i, buffer, dec->extra_channel_output[i].buffer_size, nullptr, nullptr); if (status != JXL_DEC_SUCCESS) return status; } dec->extra_channel_output.clear(); 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 @@ -209,11 +209,11 @@ &pool, &jpeg_bytes)); jpeg_codestream->append(jpeg_bytes.data(), jpeg_bytes.data() + jpeg_bytes.size()); EXPECT_TRUE(jxl::jpeg::DecodeImageJPG( jxl::Span(jpeg_bytes.data(), jpeg_bytes.size()), &io)); - EXPECT_TRUE(EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data)); + EXPECT_TRUE(EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data, cparams)); io.metadata.m.xyb_encoded = false; #else // JPEGXL_ENABLE_JPEG JXL_ABORT( "unable to create reconstructible JPEG without JPEG support enabled"); #endif // JPEGXL_ENABLE_JPEG @@ -3584,11 +3584,12 @@ orig_io.Main(), &enc_state, jxl::GetJxlCms(), /*pool=*/nullptr, &writer, /*aux_out=*/nullptr)); jxl::PaddedBytes jpeg_data; - ASSERT_TRUE(EncodeJPEGData(*orig_io.Main().jpeg_data.get(), &jpeg_data)); + ASSERT_TRUE( + EncodeJPEGData(*orig_io.Main().jpeg_data.get(), &jpeg_data, cparams)); jxl::PaddedBytes container; container.append(jxl::kContainerHeader, jxl::kContainerHeader + sizeof(jxl::kContainerHeader)); jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false, &container); @@ -4098,11 +4099,11 @@ jxl::CompressParams cparams; cparams.speed_tier = jxl::SpeedTier::kLightning; cparams.modular_mode = true; cparams.color_transform = jxl::ColorTransform::kNone; - cparams.quality_pair = {100, 100}; + cparams.butteraugli_distance = 0.f; jxl::PaddedBytes compressed; std::unique_ptr enc_state = jxl::make_unique(); EXPECT_TRUE(jxl::EncodeFile(cparams, &io, enc_state.get(), &compressed, diff --git a/third_party/jpeg-xl/lib/jxl/enc_cache.cc b/third_party/jpeg-xl/lib/jxl/enc_cache.cc --- a/third_party/jpeg-xl/lib/jxl/enc_cache.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_cache.cc @@ -96,12 +96,14 @@ cparams.gaborish = Override::kOff; // Use kVarDCT in max_error_mode for intermediate progressive DC, // and kModular for the smallest DC (first in the bitstream) if (cparams.progressive_dc == 0) { cparams.modular_mode = true; - cparams.quality_pair.first = cparams.quality_pair.second = - 99.f - enc_state->cparams.butteraugli_distance * 0.2f; + // TODO(jon): tweak mapping from image dist to dist for modular DC + cparams.butteraugli_distance = + std::max(kMinButteraugliDistance, + enc_state->cparams.butteraugli_distance * 0.03f); } ImageBundle ib(&shared.metadata->m); // This is a lie - dc is in XYB // (but EncodeFrame will skip RGB->XYB conversion anyway) ib.SetFromImage( diff --git a/third_party/jpeg-xl/lib/jxl/enc_external_image.cc b/third_party/jpeg-xl/lib/jxl/enc_external_image.cc --- a/third_party/jpeg-xl/lib/jxl/enc_external_image.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_external_image.cc @@ -430,10 +430,16 @@ }, "ConvertAlphaUint")); } ib->SetAlpha(std::move(alpha), alpha_is_premultiplied); + } else if (!has_alpha && ib->HasAlpha()) { + // if alpha is not passed, but it is expected, then assume + // it is all-opaque + ImageF alpha(xsize, ysize); + FillImage(1.0f, &alpha); + ib->SetAlpha(std::move(alpha), alpha_is_premultiplied); } return true; } diff --git a/third_party/jpeg-xl/lib/jxl/enc_file.cc b/third_party/jpeg-xl/lib/jxl/enc_file.cc --- a/third_party/jpeg-xl/lib/jxl/enc_file.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_file.cc @@ -18,10 +18,11 @@ #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/enc_bit_writer.h" #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_frame.h" #include "lib/jxl/enc_icc_codec.h" +#include "lib/jxl/exif.h" #include "lib/jxl/frame_header.h" #include "lib/jxl/headers.h" #include "lib/jxl/image_bundle.h" namespace jxl { @@ -62,55 +63,10 @@ /*suitable_for_downsampling_of_at_least=*/2}, {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/false, /*suitable_for_downsampling_of_at_least=*/0}, }; -constexpr uint16_t kExifOrientationTag = 274; - -// Parses the Exif data just enough to extract any render-impacting info. -// If the Exif data is invalid or could not be parsed, then it is treated -// as a no-op. -// TODO (jon): tag 1 can be used to represent Adobe RGB 1998 if it has value -// "R03" -// TODO (jon): set intrinsic dimensions according to -// https://discourse.wicg.io/t/proposal-exif-image-resolution-auto-and-from-image/4326/24 -void InterpretExif(const PaddedBytes& exif, CodecMetadata* metadata) { - if (exif.size() < 12) return; // not enough bytes for a valid exif blob - const uint8_t* t = exif.data(); - bool bigendian = false; - if (LoadLE32(t) == 0x2A004D4D) { - bigendian = true; - } else if (LoadLE32(t) != 0x002A4949) { - return; // not a valid tiff header - } - t += 4; - uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t)); - if (exif.size() < 12 + offset + 2 || offset < 8) return; - t += offset - 4; - uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 2; - while (nb_tags > 0) { - if (t + 12 >= exif.data() + exif.size()) return; - uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 2; - uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 2; - uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t)); - t += 4; - uint16_t value = (bigendian ? LoadBE16(t) : LoadLE16(t)); - t += 4; - if (tag == kExifOrientationTag) { - if (type == 3 && count == 1) { - if (value >= 1 && value <= 8) { - metadata->m.orientation = value; - } - } - } - nb_tags--; - } -} - Status PrepareCodecMetadataFromIO(const CompressParams& cparams, const CodecInOut* io, CodecMetadata* metadata) { *metadata = io->metadata; size_t ups = 1; @@ -119,13 +75,11 @@ JXL_RETURN_IF_ERROR(metadata->size.Set(io->xsize() * ups, io->ysize() * ups)); // Keep ICC profile in lossless modes because a reconstructed profile may be // slightly different (quantization). // Also keep ICC in JPEG reconstruction mode as we need byte-exact profiles. - const bool lossless_modular = - cparams.modular_mode && cparams.quality_pair.first == 100.0f; - if (!lossless_modular && !io->Main().IsJPEG()) { + if (!cparams.IsLossless() && !io->Main().IsJPEG()) { metadata->m.color_encoding.DecideIfWantICC(); } metadata->m.xyb_encoded = cparams.color_transform == ColorTransform::kXYB ? true : false; diff --git a/third_party/jpeg-xl/lib/jxl/enc_frame.cc b/third_party/jpeg-xl/lib/jxl/enc_frame.cc --- a/third_party/jpeg-xl/lib/jxl/enc_frame.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_frame.cc @@ -252,14 +252,13 @@ } } } // Strength of EPF in modular mode. if (frame_header->encoding == FrameEncoding::kModular && - cparams.quality_pair.first < 100) { + !cparams.IsLossless()) { // TODO(veluca): this formula is nonsense. - loop_filter->epf_sigma_for_modular = - 20.0f * (1.0f - cparams.quality_pair.first / 100); + loop_filter->epf_sigma_for_modular = cparams.butteraugli_distance; } if (frame_header->encoding == FrameEncoding::kModular && cparams.lossy_palette) { loop_filter->epf_sigma_for_modular = 1.0f; } @@ -1103,16 +1102,10 @@ if (cparams.butteraugli_distance != 0 && cparams.butteraugli_distance < kMinButteraugliDistance) { return JXL_FAILURE("Butteraugli distance is too low (%f)", cparams.butteraugli_distance); } - if (cparams.butteraugli_distance > 0.9f && cparams.modular_mode == false && - cparams.quality_pair.first == 100) { - // in case the color image is lossy, make the alpha slightly lossy too - cparams.quality_pair.first = - std::max(90.f, 99.f - 0.3f * cparams.butteraugli_distance); - } if (ib.IsJPEG()) { cparams.gaborish = Override::kOff; cparams.epf = 0; cparams.modular_mode = false; @@ -1226,12 +1219,11 @@ // YCbCr) // If encoding a special DC or reference frame, don't do anything: // input is already in XYB. CopyImageTo(ib.color(), &opsin); } - bool lossless = (frame_header->encoding == FrameEncoding::kModular && - cparams.quality_pair.first == 100); + bool lossless = cparams.IsLossless(); if (ib.HasAlpha() && !ib.AlphaIsPremultiplied() && frame_header->frame_type == FrameType::kRegularFrame && !ApplyOverride(cparams.keep_invisible, lossless) && cparams.ec_resampling == cparams.resampling) { // simplify invisible pixels diff --git a/third_party/jpeg-xl/lib/jxl/enc_modular.h b/third_party/jpeg-xl/lib/jxl/enc_modular.h --- a/third_party/jpeg-xl/lib/jxl/enc_modular.h +++ b/third_party/jpeg-xl/lib/jxl/enc_modular.h @@ -78,12 +78,11 @@ std::vector> tokens; EntropyEncodingData code; std::vector context_map; FrameDimensions frame_dim; CompressParams cparams; - float quality = cparams.quality_pair.first; - float cquality = cparams.quality_pair.second; + float quality = 100.f; std::vector tree_splits; std::vector multiplier_info; std::vector> gi_channel; std::vector image_widths; Predictor delta_pred = Predictor::Average4; 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 @@ -226,15 +226,22 @@ // convert binary32 float that corresponds to custom [bits]-bit float (with // [exp_bits] exponent bits) to a [bits]-bit integer representation that should // fit in pixel_type Status float_to_int(const float* const row_in, pixel_type* const row_out, size_t xsize, unsigned int bits, unsigned int exp_bits, - bool fp, float factor) { + bool fp, double dfactor) { JXL_ASSERT(sizeof(pixel_type) * 8 >= bits); if (!fp) { - for (size_t x = 0; x < xsize; ++x) { - row_out[x] = row_in[x] * factor + (row_in[x] < 0 ? -0.5f : 0.5f); + if (bits > 22) { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row_in[x] * dfactor + (row_in[x] < 0 ? -0.5 : 0.5); + } + } else { + float factor = dfactor; + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row_in[x] * factor + (row_in[x] < 0 ? -0.5f : 0.5f); + } } return true; } if (bits == 32 && fp) { JXL_ASSERT(exp_bits == 8); @@ -294,12 +301,11 @@ ModularFrameEncoder::ModularFrameEncoder(const FrameHeader& frame_header, const CompressParams& cparams_orig) : frame_dim(frame_header.ToFrameDimensions()), cparams(cparams_orig) { size_t num_streams = ModularStreamId::Num(frame_dim, frame_header.passes.num_passes); - if (cparams.modular_mode && - cparams.quality_pair == std::pair{100.0, 100.0}) { + if (cparams.IsLossless()) { switch (cparams.decoding_speed_tier) { case 0: break; case 1: cparams.options.wp_tree_mode = ModularOptions::TreeMode::kWPOnly; @@ -320,22 +326,21 @@ break; } } } if (cparams.decoding_speed_tier >= 1 && cparams.responsive && - cparams.quality_pair == std::make_pair(100.f, 100.f)) { + cparams.IsLossless()) { cparams.options.tree_kind = ModularOptions::TreeKind::kTrivialTreeNoPredictor; cparams.options.nb_repeats = 0; } stream_images.resize(num_streams); - if (cquality > 100) cquality = quality; // use a sensible default if nothing explicit is specified: // Squeeze for lossy, no squeeze for lossless if (cparams.responsive < 0) { - if (quality == 100) { + if (cparams.IsLossless()) { cparams.responsive = 0; } else { cparams.responsive = 1; } } @@ -394,18 +399,18 @@ if (cparams.options.predictor == static_cast(-1)) { // no explicit predictor(s) given, set a good default if ((cparams.speed_tier <= SpeedTier::kTortoise || cparams.modular_mode == false) && - quality == 100 && cparams.responsive == false) { + cparams.IsLossless() && cparams.responsive == false) { // TODO(veluca): allow all predictors that don't break residual // multipliers in lossy mode. cparams.options.predictor = Predictor::Variable; } else if (cparams.responsive || cparams.lossy_palette) { // zero predictor for Squeeze residues and lossy palette cparams.options.predictor = Predictor::Zero; - } else if (quality < 100) { + } else if (!cparams.IsLossless()) { // If not responsive and lossy. TODO(veluca): use near_lossless instead? cparams.options.predictor = Predictor::Gradient; } else if (cparams.speed_tier < SpeedTier::kFalcon) { // try median and weighted predictor for anything else cparams.options.predictor = Predictor::Best; @@ -418,10 +423,16 @@ } } else { delta_pred = cparams.options.predictor; if (cparams.lossy_palette) cparams.options.predictor = Predictor::Zero; } + if (!cparams.IsLossless()) { + if (cparams.options.predictor == Predictor::Weighted || + cparams.options.predictor == Predictor::Variable || + cparams.options.predictor == Predictor::Best) + cparams.options.predictor = Predictor::Zero; + } tree_splits.push_back(0); if (cparams.modular_mode == false) { cparams.options.fast_decode_multiplier = 1.0f; tree_splits.push_back(ModularStreamId::VarDCTDC(0).ID(frame_dim)); tree_splits.push_back(ModularStreamId::ModularDC(0).ID(frame_dim)); @@ -497,21 +508,26 @@ } else if (metadata.bit_depth.bits_per_sample > 32) { return JXL_FAILURE("bits_per_sample > 32 not supported"); } } + // in the non-float case, there is an implicit 0 sign bit + int max_bitdepth = + do_color ? metadata.bit_depth.bits_per_sample + (fp ? 0 : 1) : 0; Image& gi = stream_images[0]; gi = Image(xsize, ysize, metadata.bit_depth.bits_per_sample, nb_chans); int c = 0; if (cparams.color_transform == ColorTransform::kXYB && cparams.modular_mode == true) { static const float enc_factors[3] = {32768.0f, 2048.0f, 2048.0f}; if (cparams.manual_xyb_factors.size() == 3) { DequantMatricesSetCustomDC(&enc_state->shared.matrices, cparams.manual_xyb_factors.data()); + // TODO(jon): update max_bitdepth in this case } else { DequantMatricesSetCustomDC(&enc_state->shared.matrices, enc_factors); + max_bitdepth = 12; } } pixel_type maxval = gi.bitdepth < 32 ? (1u << gi.bitdepth) - 1 : 0; if (do_color) { for (; c < 3; c++) { @@ -521,11 +537,11 @@ continue; int c_out = c; // XYB is encoded as YX(B-Y) if (cparams.color_transform == ColorTransform::kXYB && c < 2) c_out = 1 - c_out; - float factor = maxval; + double factor = maxval; if (cparams.color_transform == ColorTransform::kXYB) factor = enc_state->shared.matrices.InvDCQuant(c); if (c == 2 && cparams.color_transform == ColorTransform::kXYB) { JXL_ASSERT(!fp); for (size_t y = 0; y < ysize; ++y) { @@ -579,11 +595,12 @@ CeilLog2Nonzero(ecups) - CeilLog2Nonzero(frame_header.upsampling); int bits = eci.bit_depth.bits_per_sample; int exp_bits = eci.bit_depth.exponent_bits_per_sample; bool fp = eci.bit_depth.floating_point_sample; - float factor = (fp ? 1 : ((1u << eci.bit_depth.bits_per_sample) - 1)); + double factor = (fp ? 1 : ((1u << eci.bit_depth.bits_per_sample) - 1)); + if (bits + (fp ? 0 : 1) > max_bitdepth) max_bitdepth = bits + (fp ? 0 : 1); std::atomic has_error{false}; JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, gi.channel[c].plane.ysize(), ThreadPool::NoInit, [&](const int task, const int thread) { const size_t y = task; @@ -597,13 +614,20 @@ "float2int")); if (has_error) return JXL_FAILURE("Error in float to integer conversion"); } JXL_ASSERT(c == nb_chans); + int level_max_bitdepth = (cparams.level == 5 ? 16 : 32); + if (max_bitdepth > level_max_bitdepth) + return JXL_FAILURE( + "Bitdepth too high for level %i (need %i bits, have only %i in this " + "level)", + cparams.level, max_bitdepth, level_max_bitdepth); + // Set options and apply transformations - if (quality < 100) { + if (!cparams.IsLossless()) { if (cparams.palette_colors != 0) { JXL_DEBUG_V(3, "Lossy encode, not doing palette transforms"); } if (cparams.color_transform == ColorTransform::kXYB) { cparams.channel_colors_pre_transform_percent = 0; @@ -702,67 +726,63 @@ do_transform(gi, maybe_palette_3, weighted::Header(), pool, cparams.options.zero_tokens); } } + // don't do an RCT if we're short on bits if (cparams.color_transform == ColorTransform::kNone && do_color && !fp && - gi.channel.size() - gi.nb_meta_channels >= 3) { + gi.channel.size() - gi.nb_meta_channels >= 3 && + max_bitdepth + 1 < level_max_bitdepth) { if (cparams.colorspace == 1 || (cparams.colorspace < 0 && - (quality < 100 || cparams.speed_tier > SpeedTier::kHare))) { + (!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) { Transform sg(TransformId::kRCT); sg.begin_c = gi.nb_meta_channels; sg.rct_type = cparams.colorspace - 2; do_transform(gi, sg, weighted::Header(), pool); + max_bitdepth++; } } - if (cparams.responsive && !gi.channel.empty()) { + // don't do squeeze if we don't have some spare bits + if (cparams.responsive && !gi.channel.empty() && + max_bitdepth + 2 < level_max_bitdepth) { Transform t(TransformId::kSqueeze); t.squeezes = cparams.squeezes; do_transform(gi, t, weighted::Header(), pool); + max_bitdepth += 2; } + if (max_bitdepth + 1 > level_max_bitdepth) { + // force no group RCTs if we don't have a spare bit + cparams.colorspace = 0; + } + JXL_ASSERT(max_bitdepth <= level_max_bitdepth); + std::vector quants; - if (quality < 100 || cquality < 100) { + if (!cparams.IsLossless()) { quants.resize(gi.channel.size(), 1); - JXL_DEBUG_V( - 2, - "Adding quantization constants corresponding to luma quality %.2f " - "and chroma quality %.2f", - quality, cquality); + float quality = 0.25f * cparams.butteraugli_distance; + JXL_DEBUG_V(2, + "Adding quantization constants corresponding to distance %.3f ", + quality); if (!cparams.responsive) { JXL_DEBUG_V(1, "Warning: lossy compression without Squeeze " "transform is just color quantization."); - quality = (400 + quality) / 5; - cquality = (400 + cquality) / 5; + quality *= 0.2f; } - // convert 'quality' to quantization scaling factor - if (quality > 50) { - quality = 200.0 - quality * 2.0; - } else { - quality = 900.0 - quality * 16.0; - } - if (cquality > 50) { - cquality = 200.0 - cquality * 2.0; - } else { - cquality = 900.0 - cquality * 16.0; - } if (cparams.color_transform != ColorTransform::kXYB) { - quality *= 0.01f * maxval / 255.f; - cquality *= 0.01f * maxval / 255.f; - } else { - quality *= 0.01f; - cquality *= 0.01f; + quality *= maxval / 255.f; } if (cparams.options.nb_repeats == 0) { return JXL_FAILURE("nb_repeats = 0 not supported with modular lossy!"); } @@ -771,21 +791,22 @@ int shift = ch.hshift + ch.vshift; // number of pixel halvings if (shift > 16) shift = 16; if (shift > 0) shift--; int q; // assuming default Squeeze here - int component = ((i - gi.nb_meta_channels) % nb_chans); + int component = + (do_color ? 0 : 3) + ((i - gi.nb_meta_channels) % nb_chans); // last 4 channels are final chroma residuals if (nb_chans > 2 && i >= gi.channel.size() - 4 && cparams.responsive) { component = 1; } if (cparams.color_transform == ColorTransform::kXYB && component < 3) { - q = (component == 0 ? quality : cquality) * squeeze_quality_factor_xyb * + q = quality * squeeze_quality_factor_xyb * squeeze_xyb_qtable[component][shift]; } else { if (cparams.colorspace != 0 && component > 0 && component < 3) { - q = cquality * squeeze_quality_factor * squeeze_chroma_qtable[shift]; + q = quality * squeeze_quality_factor * squeeze_chroma_qtable[shift]; } else { q = quality * squeeze_quality_factor * squeeze_luma_factor * squeeze_luma_qtable[shift]; } } @@ -1286,16 +1307,14 @@ } if (gi.channel.empty()) return true; // Do some per-group transforms - float quality = cparams.quality_pair.first; - // Local palette // TODO(veluca): make this work with quantize-after-prediction in lossy // mode. - if (quality == 100 && cparams.palette_colors != 0 && + if (cparams.IsLossless() && cparams.palette_colors != 0 && cparams.speed_tier < SpeedTier::kCheetah) { // all-channel palette (e.g. RGBA) if (gi.channel.size() - gi.nb_meta_channels > 1) { Transform maybe_palette(TransformId::kPalette); maybe_palette.begin_c = gi.nb_meta_channels; @@ -1319,11 +1338,11 @@ do_transform(gi, maybe_palette_3, weighted::Header()); } } // Local channel palette - if (cparams.channel_colors_percent > 0 && quality == 100 && + if (cparams.channel_colors_percent > 0 && cparams.IsLossless() && !cparams.lossy_palette && cparams.speed_tier < SpeedTier::kCheetah && !(cparams.responsive && cparams.decoding_speed_tier >= 1)) { // single channel palette (like FLIF's ChannelCompact) size_t nb_channels = gi.channel.size() - gi.nb_meta_channels; for (size_t i = 0; i < nb_channels; i++) { @@ -1346,12 +1365,13 @@ } } // lossless and no specific color transform specified: try Nothing, YCoCg, // and 17 RCTs - if (cparams.color_transform == ColorTransform::kNone && quality == 100 && - cparams.colorspace < 0 && gi.channel.size() - gi.nb_meta_channels >= 3 && + if (cparams.color_transform == ColorTransform::kNone && + cparams.IsLossless() && cparams.colorspace < 0 && + gi.channel.size() - gi.nb_meta_channels >= 3 && cparams.responsive == false && do_color && cparams.speed_tier <= SpeedTier::kHare) { Transform sg(TransformId::kRCT); sg.begin_c = gi.nb_meta_channels; size_t nb_rcts_to_try = 0; diff --git a/third_party/jpeg-xl/lib/jxl/enc_params.h b/third_party/jpeg-xl/lib/jxl/enc_params.h --- a/third_party/jpeg-xl/lib/jxl/enc_params.h +++ b/third_party/jpeg-xl/lib/jxl/enc_params.h @@ -125,10 +125,11 @@ // Try to achieve a maximum pixel-by-pixel error on each channel. bool max_error_mode = false; float max_error[3] = {0.0, 0.0, 0.0}; SpeedTier speed_tier = SpeedTier::kSquirrel; + int brotli_effort = -1; // 0 = default. // 1 = slightly worse quality. // 4 = fastest speed, lowest quality // TODO(veluca): hook this up to the C API. @@ -216,32 +217,30 @@ // modular mode options below ModularOptions options; int responsive = -1; // empty for default squeeze std::vector squeezes; - // A pair of . - std::pair quality_pair{100.f, 100.f}; int colorspace = -1; // Use Global channel palette if #colors < this percentage of range float channel_colors_pre_transform_percent = 95.f; // Use Local channel palette if #colors < this percentage of range float channel_colors_percent = 80.f; int palette_colors = 1 << 10; // up to 10-bit palette is probably worthwhile bool lossy_palette = false; // Returns whether these params are lossless as defined by SetLossless(); bool IsLossless() const { - return modular_mode && quality_pair.first == 100 && - quality_pair.second == 100 && - color_transform == jxl::ColorTransform::kNone; + // YCbCr is also considered lossless here since it's intended for + // source material that is already YCbCr (we don't do the fwd transform) + return modular_mode && butteraugli_distance == 0.0f && + color_transform != jxl::ColorTransform::kXYB; } // Sets the parameters required to make the codec lossless. void SetLossless() { modular_mode = true; - quality_pair.first = 100; - quality_pair.second = 100; + butteraugli_distance = 0.0f; color_transform = jxl::ColorTransform::kNone; } bool use_new_heuristics = false; @@ -253,10 +252,14 @@ int resampling = -1; int ec_resampling = -1; // Skip the downsampling before encoding if this is true. bool already_downsampled = false; + // Codestream level to conform to. + // -1: don't care + int level = -1; + std::vector manual_noise; std::vector manual_xyb_factors; }; static constexpr float kMinButteraugliForDynamicAR = 0.5f; 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 @@ -710,12 +710,11 @@ CompressParams cparams = state->cparams; // Recursive application of patches could create very weird issues. cparams.patches = Override::kOff; // TODO(veluca): possibly change heuristics here. if (!cparams.modular_mode) { - cparams.quality_pair.first = cparams.quality_pair.second = - 90.f - cparams.butteraugli_distance * 5.f; + cparams.butteraugli_distance *= 0.5f; } RoundtripPatchFrame(&reference_frame, state, 0, cparams, cms, pool, true); // TODO(veluca): this assumes that applying patches is commutative, which is 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 @@ -144,10 +144,14 @@ return -1; } // Level 5 checks + if (!m.modular_16_bit_buffer_sufficient) { + if (debug_string) *debug_string = "Too high modular bit depth"; + return 10; + } if (xsize > (1ull << 18ull) || ysize > (1ull << 18ull) || xsize * ysize > (1ull << 28ull)) { if (debug_string) *debug_string = "Too large image dimensions"; return 10; } @@ -361,22 +365,24 @@ input_frame->option_values.header.layer_info.blend_info.clamp; frame_info.alpha_channel = input_frame->option_values.header.layer_info.blend_info.alpha; frame_info.extra_channel_blending_info.resize( metadata.m.num_extra_channels); - // If extra channel blend info has not been set, use the default values. - JxlBlendInfo default_blend_info; - JxlEncoderInitBlendInfo(&default_blend_info); + // If extra channel blend info has not been set, use the blend mode from the + // layer_info. + JxlBlendInfo default_blend_info = + input_frame->option_values.header.layer_info.blend_info; for (size_t i = 0; i < metadata.m.num_extra_channels; ++i) { auto& to = frame_info.extra_channel_blending_info[i]; const auto& from = i < input_frame->option_values.extra_channel_blend_info.size() ? input_frame->option_values.extra_channel_blend_info[i] : default_blend_info; to.mode = static_cast(from.blendmode); to.source = from.source; to.alpha_channel = from.alpha; + to.clamp = (from.clamp != 0); } if (input_frame->option_values.header.layer_info.have_crop) { ib.origin.x0 = input_frame->option_values.header.layer_info.crop_x0; ib.origin.y0 = input_frame->option_values.header.layer_info.crop_y0; @@ -421,13 +427,14 @@ jxl::PaddedBytes compressed(4); // Prepend the original box type in the brob box contents for (size_t i = 0; i < 4; i++) { compressed[i] = static_cast(box->type[i]); } - if (JXL_ENC_SUCCESS != BrotliCompress(9, box->contents.data(), - box->contents.size(), - &compressed)) { + 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"); } jxl::AppendBoxHeader(jxl::MakeBoxType("brob"), compressed.size(), false, &output_byte_queue); output_byte_queue.insert(output_byte_queue.end(), compressed.data(), @@ -559,11 +566,12 @@ enc->metadata.m.bit_depth.exponent_bits_per_sample = info->exponent_bits_per_sample; enc->metadata.m.bit_depth.floating_point_sample = (info->exponent_bits_per_sample != 0u); enc->metadata.m.modular_16_bit_buffer_sufficient = - (info->exponent_bits_per_sample == 0u) && info->bits_per_sample <= 12; + (!info->uses_original_profile || info->bits_per_sample <= 12) && + info->alpha_bits <= 12; // The number of extra channels includes the alpha channel, so for example and // 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); @@ -618,11 +626,19 @@ 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_ENC_SUCCESS; } void JxlEncoderInitExtraChannelInfo(JxlExtraChannelType type, JxlExtraChannelInfo* info) { @@ -650,20 +666,31 @@ } 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; + enc->metadata.m.modular_16_bit_buffer_sufficient &= + info->bits_per_sample <= 12; channel.bit_depth.exponent_bits_per_sample = info->exponent_bits_per_sample; channel.bit_depth.floating_point_sample = info->exponent_bits_per_sample != 0; channel.dim_shift = info->dim_shift; channel.name = ""; channel.alpha_associated = (info->alpha_premultiplied != 0); channel.cfa_channel = info->cfa_channel; channel.spot_color[0] = info->spot_color[0]; channel.spot_color[1] = info->spot_color[1]; channel.spot_color[2] = info->spot_color[2]; 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_ENC_SUCCESS; } JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelName(JxlEncoder* enc, size_t index, @@ -686,10 +713,11 @@ if (source != nullptr) { opts->values = source->values; } else { opts->values.lossless = false; } + opts->values.cparams.level = enc->codestream_level; JxlEncoderFrameSettings* ret = opts.get(); enc->encoder_options.emplace_back(std::move(opts)); return ret; } @@ -699,10 +727,14 @@ return JxlEncoderFrameSettingsCreate(enc, source); } 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"); + } frame_settings->values.lossless = lossless; return JXL_ENC_SUCCESS; } JxlEncoderStatus JxlEncoderOptionsSetLossless( @@ -724,33 +756,10 @@ } if (distance > 0.f && distance < 0.01f) { distance = 0.01f; } frame_settings->values.cparams.butteraugli_distance = distance; - float jpeg_quality; - // Formula to translate butteraugli distance roughly into JPEG 0-100 quality. - // This is the inverse of the formula in cjxl.cc to translate JPEG quality - // into butteraugli distance. - if (distance > 6.56f) { - jpeg_quality = -5.456783f * std::log(0.0256f * distance - 0.16384f); - } else { - jpeg_quality = -11.11111f * distance + 101.11111f; - } - // Translate JPEG quality into the quality_pair setting for modular encoding. - // This is the formula also used in cjxl.cc to convert the command line JPEG - // quality parameter to the quality_pair setting. - // TODO(lode): combine the distance -> quality_pair conversion into a single - // formula, possibly altering it to a more suitable heuristic. - float quality; - if (jpeg_quality < 7.f) { - quality = std::min(35.f + (jpeg_quality - 7.f) * 3.0f, 100.0f); - } else { - quality = - std::min(35.f + (jpeg_quality - 7.f) * 65.f / 93.f, 100.0f); - } - frame_settings->values.cparams.quality_pair.first = - frame_settings->values.cparams.quality_pair.second = quality; return JXL_ENC_SUCCESS; } JxlEncoderStatus JxlEncoderOptionsSetDistance( JxlEncoderFrameSettings* frame_settings, float distance) { @@ -773,10 +782,19 @@ return JXL_ENC_ERROR; } 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; + } + // 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; } frame_settings->values.cparams.decoding_speed_tier = value; @@ -1136,11 +1154,12 @@ } 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)) { + if (!jxl::jpeg::EncodeJPEGData(data_in, &jpeg_data, + frame_settings->values.cparams)) { return JXL_ENC_ERROR; } frame_settings->enc->jpeg_metadata = std::vector( jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); } @@ -1245,19 +1264,35 @@ has_interleaved_alpha = 0; // only first Alpha is initialized } else { queued_frame->ec_initialized.push_back(0); } } + queued_frame->frame.origin.x0 = + frame_settings->values.header.layer_info.crop_x0; + queued_frame->frame.origin.y0 = + frame_settings->values.header.layer_info.crop_y0; + queued_frame->frame.use_for_next_frame = + (frame_settings->values.header.layer_info.save_as_reference != 0u); + queued_frame->frame.blendmode = + frame_settings->values.header.layer_info.blend_info.blendmode == + JXL_BLEND_REPLACE + ? jxl::BlendMode::kReplace + : jxl::BlendMode::kBlend; + queued_frame->frame.blend = + 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; } - if (frame_settings->values.lossless) { - queued_frame->option_values.cparams.SetLossless(); + if (frame_settings->values.lossless && + frame_settings->enc->metadata.m.xyb_encoded) { + return JXL_API_ERROR("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; } 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 @@ -151,10 +151,11 @@ bool frames_closed; bool boxes_closed; bool basic_info_set; bool color_encoding_set; bool intensity_target_set; + int brotli_effort = -1; // Takes the first frame in the input_queue, encodes it, and appends // the bytes to the output_byte_queue. JxlEncoderStatus RefillOutputByteQueue(); 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 @@ -36,10 +36,11 @@ JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/pixel_format.num_channels < 3); EXPECT_EQ(JXL_ENC_SUCCESS, @@ -83,10 +84,11 @@ JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = true; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderAddImageFrame(frame_settings, &pixel_format, @@ -171,10 +173,12 @@ if (frame_settings->values.lossless) { basic_info.uses_original_profile = true; } else { basic_info.uses_original_profile = false; } + // 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); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc, &color_encoding)); @@ -196,11 +200,10 @@ avail_out = compressed.size() - offset; } } compressed.resize(next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); - jxl::DecompressParams dparams; jxl::CodecInOut decoded_io; EXPECT_TRUE(jxl::DecodeFile( dparams, jxl::Span(compressed.data(), compressed.size()), &decoded_io, /*pool=*/nullptr)); @@ -628,10 +631,11 @@ JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, @@ -737,11 +741,11 @@ EXPECT_EQ(0u, encoded_span.size()); EXPECT_EQ(0, memcmp("jxll", container.boxes[0].type, 4)); } TEST(EncodeTest, CodestreamLevelVerificationTest) { - JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0}; JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = 64; basic_info.ysize = 64; @@ -753,20 +757,19 @@ EXPECT_EQ(5, JxlEncoderGetRequiredCodestreamLevel(enc.get())); // Set an image dimension that is too large for level 5, but fits in level 10 basic_info.xsize = 1ull << 30ull; + EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); - EXPECT_EQ(10, JxlEncoderGetRequiredCodestreamLevel(enc.get())); // Set an image dimension that is too large even for level 10 basic_info.xsize = 1ull << 31ull; - EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); - - EXPECT_EQ(-1, JxlEncoderGetRequiredCodestreamLevel(enc.get())); + EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); } TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) { const std::string jpeg_path = "imagecompression.info/flower_foveon.png.im_q85_420.jpg"; @@ -898,10 +901,11 @@ basic_info.orientation = JXL_ORIENT_ROTATE_90_CW; basic_info.animation.tps_numerator = 55; basic_info.animation.tps_denominator = 77; basic_info.animation.num_loops = 10; basic_info.animation.have_timecodes = JXL_TRUE; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); @@ -999,10 +1003,11 @@ basic_info.ysize = ysize; basic_info.have_animation = true; basic_info.animation.tps_numerator = 1000; basic_info.animation.tps_denominator = 1; basic_info.animation.have_timecodes = JXL_TRUE; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); @@ -1100,10 +1105,11 @@ jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); // Encoding a 300x300 frame in an image that is only 100x100 basic_info.xsize = 100; basic_info.ysize = 100; basic_info.uses_original_profile = JXL_TRUE; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); @@ -1192,10 +1198,11 @@ JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, diff --git a/third_party/jpeg-xl/lib/jxl/exif.h b/third_party/jpeg-xl/lib/jxl/exif.h new file mode 100644 --- /dev/null +++ b/third_party/jpeg-xl/lib/jxl/exif.h @@ -0,0 +1,27 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_JXL_EXIF_H_ +#define LIB_JXL_EXIF_H_ + +// Basic parsing of Exif (just enough for the render-impacting things +// like orientation) + +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/image_metadata.h" + +namespace jxl { + +// Parses the Exif data just enough to extract any render-impacting info. +// If the Exif data is invalid or could not be parsed, then it is treated +// as a no-op. +void InterpretExif(const PaddedBytes& exif, CodecMetadata* metadata); + +// Sets the Exif orientation to the identity, to avoid repeated orientation +void ResetExifOrientation(PaddedBytes& exif); + +} // namespace jxl + +#endif // LIB_JXL_EXIF_H_ diff --git a/third_party/jpeg-xl/lib/jxl/exif.cc b/third_party/jpeg-xl/lib/jxl/exif.cc new file mode 100644 --- /dev/null +++ b/third_party/jpeg-xl/lib/jxl/exif.cc @@ -0,0 +1,89 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/jxl/exif.h" + +namespace jxl { + +constexpr uint16_t kExifOrientationTag = 274; + +// Checks if a blob looks like Exif, and if so, sets bigendian +// according to the tiff endianness +bool IsExif(const PaddedBytes& exif, bool* bigendian) { + if (exif.size() < 12) return false; // not enough bytes for a valid exif blob + const uint8_t* t = exif.data(); + if (LoadLE32(t) == 0x2A004D4D) { + *bigendian = true; + return true; + } else if (LoadLE32(t) == 0x002A4949) { + *bigendian = false; + return true; + } + return false; // not a valid tiff header +} + +// Finds the position of an Exif tag, or 0 if it is not found +size_t FindExifTagPosition(const PaddedBytes& exif, uint16_t tagname) { + bool bigendian; + if (!IsExif(exif, &bigendian)) return 0; + const uint8_t* t = exif.data() + 4; + uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t)); + if (exif.size() < 12 + offset + 2 || offset < 8) return 0; + t += offset - 4; + uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + while (nb_tags > 0) { + if (t + 12 >= exif.data() + exif.size()) return 0; + uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + if (tag == tagname) return static_cast(t - exif.data()); + t += 10; + nb_tags--; + } + return 0; +} + +// TODO (jon): tag 1 can be used to represent Adobe RGB 1998 if it has value +// "R03" +// TODO (jon): set intrinsic dimensions according to +// https://discourse.wicg.io/t/proposal-exif-image-resolution-auto-and-from-image/4326/24 +void InterpretExif(const PaddedBytes& exif, CodecMetadata* metadata) { + bool bigendian; + if (!IsExif(exif, &bigendian)) return; + size_t o_pos = FindExifTagPosition(exif, kExifOrientationTag); + if (o_pos) { + const uint8_t* t = exif.data() + o_pos; + uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t)); + t += 4; + uint16_t value = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 4; + if (type == 3 && count == 1 && value >= 1 && value <= 8) { + metadata->m.orientation = value; + } + } +} + +void ResetExifOrientation(PaddedBytes& exif) { + bool bigendian; + if (!IsExif(exif, &bigendian)) return; + size_t o_pos = FindExifTagPosition(exif, kExifOrientationTag); + if (o_pos) { + uint8_t* t = exif.data() + o_pos; + uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t)); + t += 4; + if (type == 3 && count == 1) { + if (bigendian) + StoreBE16(1, t); + else + StoreLE16(1, t); + } + } +} + +} // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data.h b/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data.h --- a/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data.h +++ b/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data.h @@ -6,15 +6,17 @@ #ifndef LIB_JXL_JPEG_ENC_JPEG_DATA_H_ #define LIB_JXL_JPEG_ENC_JPEG_DATA_H_ #include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/codec_in_out.h" +#include "lib/jxl/enc_params.h" #include "lib/jxl/jpeg/jpeg_data.h" namespace jxl { namespace jpeg { -Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes); +Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes, + const CompressParams& cparams); Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg, ColorEncoding* color_encoding); /** diff --git a/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data.cc b/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data.cc --- a/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data.cc +++ b/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data.cc @@ -228,22 +228,25 @@ } return color_encoding->SetICC(std::move(icc_profile)); } -Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes) { +Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes, + const CompressParams& cparams) { jpeg_data.app_marker_type.resize(jpeg_data.app_data.size(), AppMarkerType::kUnknown); JXL_RETURN_IF_ERROR(DetectIccProfile(jpeg_data)); JXL_RETURN_IF_ERROR(DetectBlobs(jpeg_data)); BitWriter writer; JXL_RETURN_IF_ERROR(Bundle::Write(jpeg_data, &writer, 0, nullptr)); writer.ZeroPadToByte(); *bytes = std::move(writer).TakeBytes(); BrotliEncoderState* brotli_enc = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); - BrotliEncoderSetParameter(brotli_enc, BROTLI_PARAM_QUALITY, 11); + int effort = cparams.brotli_effort; + if (effort < 0) effort = 11 - static_cast(cparams.speed_tier); + BrotliEncoderSetParameter(brotli_enc, BROTLI_PARAM_QUALITY, effort); size_t total_data = 0; for (size_t i = 0; i < jpeg_data.app_data.size(); i++) { if (jpeg_data.app_marker_type[i] != AppMarkerType::kUnknown) { continue; } diff --git a/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data_reader.cc b/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data_reader.cc --- a/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data_reader.cc +++ b/third_party/jpeg-xl/lib/jxl/jpeg/enc_jpeg_data_reader.cc @@ -93,10 +93,11 @@ size_t marker_len = ReadUint16(data, pos); int precision = ReadUint8(data, pos); int height = ReadUint16(data, pos); int width = ReadUint16(data, pos); int num_components = ReadUint8(data, pos); + // 'jbrd' is hardcoded for 8bits: JXL_JPEG_VERIFY_INPUT(precision, 8, 8, PRECISION); JXL_JPEG_VERIFY_INPUT(height, 1, kMaxDimPixels, HEIGHT); JXL_JPEG_VERIFY_INPUT(width, 1, kMaxDimPixels, WIDTH); JXL_JPEG_VERIFY_INPUT(num_components, 1, kMaxComponents, NUMCOMP); JXL_JPEG_VERIFY_LEN(3 * num_components); 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 @@ -593,11 +593,11 @@ CodecInOut io2; EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(0.2f)); + IsSlightlyBelow(0.12f)); } TEST(JxlTest, RoundtripSmallPatches) { ThreadPool* pool = nullptr; CodecInOut io; @@ -621,11 +621,11 @@ CodecInOut io2; EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(0.2f)); + IsSlightlyBelow(0.12f)); } // Test header encoding of original bits per sample TEST(JxlTest, RoundtripImageBundleOriginalBits) { ThreadPool* pool = nullptr; @@ -824,11 +824,11 @@ IsSlightlyBelow(1.4)); io2.Main().UnpremultiplyAlpha(); EXPECT_THAT( ButteraugliDistance(io_nopremul, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(1.8)); + IsSlightlyBelow(1.4)); } TEST(JxlTest, RoundtripAlphaResampling) { ThreadPool* pool = nullptr; const PaddedBytes orig = @@ -920,19 +920,19 @@ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, pool)); CodecInOut io2; EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); - EXPECT_LE(compressed.size(), 200u); + EXPECT_LE(compressed.size(), 180u); // TODO(robryk): Fix the following line in presence of different alpha_bits in // the two contexts. // EXPECT_TRUE(SamePixels(io.Main().alpha(), io2.Main().alpha())); // TODO(robryk): Fix the distance estimate used in the encoder. EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(0.8)); + IsSlightlyBelow(0.9)); } TEST(JxlTest, RoundtripAlpha16) { ThreadPoolInternal pool(4); @@ -963,12 +963,11 @@ ASSERT_TRUE(io.metadata.m.HasAlpha()); ASSERT_TRUE(io.Main().HasAlpha()); CompressParams cparams; cparams.butteraugli_distance = 0.5; - // Prevent the test to be too slow, does not affect alpha - cparams.speed_tier = SpeedTier::kSquirrel; + cparams.speed_tier = SpeedTier::kWombat; DecompressParams dparams; io.metadata.m.SetUintSamples(16); EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB()); PassesEncoderState enc_state; @@ -976,20 +975,21 @@ PaddedBytes compressed; EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(), aux_out, &pool)); CodecInOut io2; EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, &pool)); - - EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha())); + EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), + /*distmap=*/nullptr, &pool), + IsSlightlyBelow(0.8)); } namespace { CompressParams CParamsForLossless() { CompressParams cparams; cparams.modular_mode = true; cparams.color_transform = jxl::ColorTransform::kNone; - cparams.quality_pair = {100, 100}; + cparams.butteraugli_distance = 0.f; cparams.options.predictor = {Predictor::Weighted}; return cparams; } } // namespace @@ -1446,11 +1446,11 @@ /*aux_out=*/nullptr, pool)); jpegxl::tools::JpegXlContainer enc_container; enc_container.codestream = std::move(codestream); jpeg::JPEGData data_in = *io.Main().jpeg_data; jxl::PaddedBytes jpeg_data; - EXPECT_TRUE(EncodeJPEGData(data_in, &jpeg_data)); + EXPECT_TRUE(EncodeJPEGData(data_in, &jpeg_data, cparams)); enc_container.jpeg_reconstruction = jpeg_data.data(); enc_container.jpeg_reconstruction_size = jpeg_data.size(); EXPECT_TRUE(EncodeJpegXlContainerOneShot(enc_container, &compressed)); jpegxl::tools::JpegXlContainer container; @@ -1684,10 +1684,10 @@ cparams.progressive_mode = true; CodecInOut io2; EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u); EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, &pool), - IsSlightlyBelow(2.5f)); + IsSlightlyBelow(1.1f)); } } // namespace } // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl/modular/encoding/context_predict.h b/third_party/jpeg-xl/lib/jxl/modular/encoding/context_predict.h --- a/third_party/jpeg-xl/lib/jxl/modular/encoding/context_predict.h +++ b/third_party/jpeg-xl/lib/jxl/modular/encoding/context_predict.h @@ -302,11 +302,11 @@ uint32_t context; Predictor predictor; int64_t offset; int32_t multiplier; }; - LookupResult Lookup(const Properties &properties) const { + JXL_INLINE LookupResult Lookup(const Properties &properties) const { uint32_t pos = 0; while (true) { const FlatDecisionNode &node = nodes_[pos]; if (node.property0 < 0) { return {node.childID, node.predictor, node.predictor_offset, @@ -414,10 +414,11 @@ enum PredictorMode { kUseTree = 1, kUseWP = 2, kForceComputeProperties = 4, kAllPredictions = 8, + kNoEdgeCases = 16 }; JXL_INLINE pixel_type_w PredictOne(Predictor p, pixel_type_w left, pixel_type_w top, pixel_type_w toptop, pixel_type_w topleft, pixel_type_w topright, @@ -459,26 +460,28 @@ return pixel_type_w{0}; } } template -inline PredictionResult Predict( +JXL_INLINE PredictionResult Predict( Properties *p, size_t w, const pixel_type *JXL_RESTRICT pp, const intptr_t onerow, const size_t x, const size_t y, Predictor predictor, const MATreeLookup *lookup, const Channel *references, weighted::State *wp_state, pixel_type_w *predictions) { // We start in position 3 because of 2 static properties + y. size_t offset = 3; constexpr bool compute_properties = mode & kUseTree || mode & kForceComputeProperties; - pixel_type_w left = (x ? pp[-1] : (y ? pp[-onerow] : 0)); - pixel_type_w top = (y ? pp[-onerow] : left); - pixel_type_w topleft = (x && y ? pp[-1 - onerow] : left); - pixel_type_w topright = (x + 1 < w && y ? pp[1 - onerow] : top); - pixel_type_w leftleft = (x > 1 ? pp[-2] : left); - pixel_type_w toptop = (y > 1 ? pp[-onerow - onerow] : top); - pixel_type_w toprightright = (x + 2 < w && y ? pp[2 - onerow] : topright); + constexpr bool nec = mode & kNoEdgeCases; + pixel_type_w left = (nec || x ? pp[-1] : (y ? pp[-onerow] : 0)); + pixel_type_w top = (nec || y ? pp[-onerow] : left); + pixel_type_w topleft = (nec || (x && y) ? pp[-1 - onerow] : left); + pixel_type_w topright = (nec || (x + 1 < w && y) ? pp[1 - onerow] : top); + pixel_type_w leftleft = (nec || x > 1 ? pp[-2] : left); + pixel_type_w toptop = (nec || y > 1 ? pp[-onerow - onerow] : top); + pixel_type_w toprightright = + (nec || (x + 2 < w && y) ? pp[2 - onerow] : topright); if (compute_properties) { // location (*p)[offset++] = x; // neighbors @@ -504,11 +507,11 @@ pixel_type_w wp_pred = 0; if (mode & kUseWP) { wp_pred = wp_state->Predict( x, y, w, top, left, topright, topleft, toptop, p, offset); } - if (compute_properties) { + if (!nec && compute_properties) { offset += weighted::kNumProperties; // Extra properties. const pixel_type *JXL_RESTRICT rp = references->Row(x); for (size_t i = 0; i < references->w; i++) { (*p)[offset++] = rp[i]; @@ -563,10 +566,19 @@ const Channel &references) { return detail::Predict( p, w, pp, onerow, x, y, Predictor::Zero, &tree_lookup, &references, /*wp_state=*/nullptr, /*predictions=*/nullptr); } +// Only use for y > 1, x > 1, x < w-2, and empty references +JXL_INLINE PredictionResult +PredictTreeNoWPNEC(Properties *p, size_t w, const pixel_type *JXL_RESTRICT pp, + const intptr_t onerow, const int x, const int y, + const MATreeLookup &tree_lookup, const Channel &references) { + return detail::Predict( + p, w, pp, onerow, x, y, Predictor::Zero, &tree_lookup, &references, + /*wp_state=*/nullptr, /*predictions=*/nullptr); +} inline PredictionResult PredictTreeWP(Properties *p, size_t w, const pixel_type *JXL_RESTRICT pp, const intptr_t onerow, const int x, const int y, diff --git a/third_party/jpeg-xl/lib/jxl/modular/encoding/encoding.cc b/third_party/jpeg-xl/lib/jxl/modular/encoding/encoding.cc --- a/third_party/jpeg-xl/lib/jxl/modular/encoding/encoding.cc +++ b/third_party/jpeg-xl/lib/jxl/modular/encoding/encoding.cc @@ -184,18 +184,27 @@ pixel_type v = make_pixel(value, multiplier, offset); for (size_t y = 0; y < channel.h; y++) { pixel_type *JXL_RESTRICT r = channel.Row(y); std::fill(r, r + channel.w, v); } - } else { JXL_DEBUG_V(8, "Fast track."); - for (size_t y = 0; y < channel.h; y++) { - pixel_type *JXL_RESTRICT r = channel.Row(y); - for (size_t x = 0; x < channel.w; x++) { - uint32_t v = reader->ReadHybridUintClustered(ctx_id, br); - r[x] = make_pixel(v, multiplier, offset); + if (multiplier == 1 && offset == 0) { + for (size_t y = 0; y < channel.h; y++) { + pixel_type *JXL_RESTRICT r = channel.Row(y); + for (size_t x = 0; x < channel.w; x++) { + uint32_t v = reader->ReadHybridUintClustered(ctx_id, br); + r[x] = UnpackSigned(v); + } + } + } else { + for (size_t y = 0; y < channel.h; y++) { + pixel_type *JXL_RESTRICT r = channel.Row(y); + for (size_t x = 0; x < channel.w; x++) { + uint32_t v = reader->ReadHybridUintClustered(ctx_id, br); + r[x] = make_pixel(v, multiplier, offset); + } } } } } else if (predictor == Predictor::Gradient && offset == 0 && multiplier == 1 && reader->HuffRleOnly()) { @@ -353,16 +362,40 @@ Channel references(properties.size() - kNumNonrefProperties, channel.w); for (size_t y = 0; y < channel.h; y++) { pixel_type *JXL_RESTRICT p = channel.Row(y); PrecomputeReferences(channel, y, *image, chan, &references); InitPropsRow(&properties, static_props, y); - for (size_t x = 0; x < channel.w; x++) { - PredictionResult res = - PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y, - tree_lookup, references); - uint64_t v = reader->ReadHybridUintClustered(res.context, br); - p[x] = make_pixel(v, res.multiplier, res.guess); + if (y > 1 && channel.w > 8 && references.w == 0) { + for (size_t x = 0; x < 2; x++) { + PredictionResult res = + PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y, + tree_lookup, references); + uint64_t v = reader->ReadHybridUintClustered(res.context, br); + p[x] = make_pixel(v, res.multiplier, res.guess); + } + for (size_t x = 2; x < channel.w - 2; x++) { + PredictionResult res = + PredictTreeNoWPNEC(&properties, channel.w, p + x, onerow, x, y, + tree_lookup, references); + uint64_t v = reader->ReadHybridUintClustered(res.context, br); + p[x] = make_pixel(v, res.multiplier, res.guess); + } + for (size_t x = channel.w - 2; x < channel.w; x++) { + PredictionResult res = + PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y, + tree_lookup, references); + uint64_t v = reader->ReadHybridUintClustered(res.context, br); + p[x] = make_pixel(v, res.multiplier, res.guess); + } + } else { + for (size_t x = 0; x < channel.w; x++) { + PredictionResult res = + PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y, + tree_lookup, references); + uint64_t v = reader->ReadHybridUintClustered(res.context, br); + p[x] = make_pixel(v, res.multiplier, res.guess); + } } } } else { JXL_DEBUG_V(8, "Slowest track."); MATreeLookup tree_lookup(tree); 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 @@ -548,22 +548,19 @@ Status FwdPalette(Image &input, uint32_t begin_c, uint32_t end_c, uint32_t &nb_colors, uint32_t &nb_deltas, bool ordered, bool lossy, Predictor &predictor, const weighted::Header &wp_header) { PaletteIterationData palette_iteration_data; - uint32_t nb = end_c - begin_c + 1; uint32_t nb_colors_orig = nb_colors; uint32_t nb_deltas_orig = nb_deltas; - // if no channel palette special case - if ((lossy || nb != 1) && input.bitdepth >= 8) { + // preprocessing pass in case of lossy palette + if (lossy && input.bitdepth >= 8) { JXL_RETURN_IF_ERROR(FwdPaletteIteration( - input, begin_c, end_c, nb_colors, nb_deltas, ordered, lossy, predictor, - wp_header, palette_iteration_data)); + input, begin_c, end_c, nb_colors_orig, nb_deltas_orig, ordered, lossy, + predictor, wp_header, palette_iteration_data)); } palette_iteration_data.final_run = true; - nb_colors = nb_colors_orig; - nb_deltas = nb_deltas_orig; return FwdPaletteIteration(input, begin_c, end_c, nb_colors, nb_deltas, ordered, lossy, predictor, wp_header, palette_iteration_data); } diff --git a/third_party/jpeg-xl/lib/jxl/modular/transform/squeeze.h b/third_party/jpeg-xl/lib/jxl/modular/transform/squeeze.h --- a/third_party/jpeg-xl/lib/jxl/modular/transform/squeeze.h +++ b/third_party/jpeg-xl/lib/jxl/modular/transform/squeeze.h @@ -73,14 +73,10 @@ if (diff - (diff & 1) < 2 * (a - n)) diff = 2 * (a - n); } return diff; } -void InvHSqueeze(Image &input, int c, int rc, ThreadPool *pool); - -void InvVSqueeze(Image &input, int c, int rc, ThreadPool *pool); - void DefaultSqueezeParameters(std::vector *parameters, const Image &image); Status CheckMetaSqueezeParams(const SqueezeParams ¶meter, int num_channels); diff --git a/third_party/jpeg-xl/lib/jxl/modular/transform/squeeze.cc b/third_party/jpeg-xl/lib/jxl/modular/transform/squeeze.cc --- a/third_party/jpeg-xl/lib/jxl/modular/transform/squeeze.cc +++ b/third_party/jpeg-xl/lib/jxl/modular/transform/squeeze.cc @@ -10,17 +10,83 @@ #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/common.h" #include "lib/jxl/modular/modular_image.h" #include "lib/jxl/modular/transform/transform.h" +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/modular/transform/squeeze.cc" +#include +#include +#include "lib/jxl/simd_util-inl.h" + +HWY_BEFORE_NAMESPACE(); namespace jxl { +namespace HWY_NAMESPACE { + +using hwy::HWY_NAMESPACE::ShiftLeft; +using hwy::HWY_NAMESPACE::ShiftRight; + +#if HWY_TARGET != HWY_SCALAR + +JXL_INLINE void FastUnsqueeze(const pixel_type *JXL_RESTRICT p_residual, + const pixel_type *JXL_RESTRICT p_avg, + const pixel_type *JXL_RESTRICT p_navg, + const pixel_type *p_pout, + pixel_type *JXL_RESTRICT p_out, + pixel_type *p_nout) { + const HWY_CAPPED(pixel_type, 8) d; + const size_t N = Lanes(d); + auto onethird = Set(d, 0x55555556); + for (size_t x = 0; x < 8; x += N) { + auto avg = Load(d, p_avg + x); + auto next_avg = Load(d, p_navg + x); + auto top = Load(d, p_pout + x); + // Equivalent to SmoothTendency(top,avg,next_avg), but without branches + auto Ba = top - avg; + auto an = avg - next_avg; + auto nonmono = Ba ^ an; + auto absBa = Abs(Ba); + auto absan = Abs(an); + auto absBn = Abs(top - next_avg); + // Compute a3 = absBa / 3 + auto a3e = BitCast(d, ShiftRight<32>(MulEven(absBa, onethird))); + auto a3oi = MulEven(Reverse(d, absBa), onethird); + auto a3o = BitCast( + d, Reverse(hwy::HWY_NAMESPACE::Repartition(), + a3oi)); + auto a3 = OddEven(a3o, a3e); + a3 += absBn + Set(d, 2); + auto absdiff = ShiftRight<2>(a3); + auto skipdiff = Ba != Zero(d); + skipdiff = And(skipdiff, an != Zero(d)); + skipdiff = And(skipdiff, nonmono < Zero(d)); + auto absBa2 = ShiftLeft<1>(absBa) + (absdiff & Set(d, 1)); + absdiff = + IfThenElse(absdiff > absBa2, ShiftLeft<1>(absBa) + Set(d, 1), absdiff); + auto absan2 = ShiftLeft<1>(absan); + absdiff = + IfThenElse(absdiff + (absdiff & Set(d, 1)) > absan2, absan2, absdiff); + auto diff1 = IfThenElse(top < next_avg, Neg(absdiff), absdiff); + auto tendency = IfThenZeroElse(skipdiff, diff1); + + auto diff_minus_tendency = Load(d, p_residual + x); + auto diff = diff_minus_tendency + tendency; + auto out = ShiftRight<1>( + ShiftLeft<1>(avg) + diff + + IfThenElse(diff < Zero(d), (diff & Set(d, 1)), Neg(diff & Set(d, 1)))); + Store(out, d, p_out + x); + Store(out - diff, d, p_nout + x); + } +} + +#endif Status InvHSqueeze(Image &input, uint32_t c, uint32_t rc, ThreadPool *pool) { JXL_ASSERT(c < input.channel.size()); JXL_ASSERT(rc < input.channel.size()); - const Channel &chin = input.channel[c]; + Channel &chin = input.channel[c]; const Channel &chin_residual = input.channel[rc]; // These must be valid since we ran MetaApply already. JXL_ASSERT(chin.w == DivCeil(chin.w + chin_residual.w, 2)); JXL_ASSERT(chin.h == chin_residual.h); @@ -40,44 +106,88 @@ if (chin_residual.h == 0) { // Short-circuit: channel with no pixels. input.channel[c] = std::move(chout); return true; } + auto unsqueeze_row = [&](size_t y, size_t x0) { + const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y); + const pixel_type *JXL_RESTRICT p_avg = chin.Row(y); + pixel_type *JXL_RESTRICT p_out = chout.Row(y); + for (size_t x = x0; x < chin_residual.w; x++) { + pixel_type diff_minus_tendency = p_residual[x]; + pixel_type avg = p_avg[x]; + pixel_type next_avg = (x + 1 < chin.w ? p_avg[x + 1] : avg); + pixel_type left = (x ? p_out[(x << 1) - 1] : avg); + pixel_type tendency = SmoothTendency(left, avg, next_avg); + pixel_type diff = diff_minus_tendency + tendency; + pixel_type A = + ((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1; + p_out[(x << 1)] = A; + pixel_type B = A - diff; + p_out[(x << 1) + 1] = B; + } + if (chout.w & 1) p_out[chout.w - 1] = p_avg[chin.w - 1]; + }; + // somewhat complicated trickery just to be able to SIMD this. + // Horizontal unsqueeze has horizontal data dependencies, so we do + // 8 rows at a time and treat it as a vertical unsqueeze of a + // transposed 8x8 block (or 9x8 for one input). + constexpr size_t kRowsPerThread = 8; JXL_RETURN_IF_ERROR(RunOnPool( - pool, 0, chin.h, ThreadPool::NoInit, + pool, 0, DivCeil(chin.h, kRowsPerThread), ThreadPool::NoInit, [&](const uint32_t task, size_t /* thread */) { - const size_t y = task; - const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y); - const pixel_type *JXL_RESTRICT p_avg = chin.Row(y); - pixel_type *JXL_RESTRICT p_out = chout.Row(y); + const size_t y0 = task * kRowsPerThread; + const size_t rows = std::min(kRowsPerThread, chin.h - y0); + size_t x = 0; - // special case for x=0 so we don't have to check x>0 - pixel_type_w avg = p_avg[0]; - pixel_type_w next_avg = (1 < chin.w ? p_avg[1] : avg); - pixel_type_w tendency = SmoothTendency(avg, avg, next_avg); - pixel_type_w diff = p_residual[0] + tendency; - pixel_type_w A = - ((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1; - pixel_type_w B = A - diff; - p_out[0] = A; - p_out[1] = B; +#if HWY_TARGET != HWY_SCALAR + intptr_t onerow_in = chin.plane.PixelsPerRow(); + intptr_t onerow_inr = chin_residual.plane.PixelsPerRow(); + intptr_t onerow_out = chout.plane.PixelsPerRow(); + const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y0); + const pixel_type *JXL_RESTRICT p_avg = chin.Row(y0); + pixel_type *JXL_RESTRICT p_out = chout.Row(y0); + HWY_ALIGN pixel_type b_p_avg[9 * kRowsPerThread]; + HWY_ALIGN pixel_type b_p_residual[8 * kRowsPerThread]; + HWY_ALIGN pixel_type b_p_out_even[8 * kRowsPerThread]; + HWY_ALIGN pixel_type b_p_out_odd[8 * kRowsPerThread]; + HWY_ALIGN pixel_type b_p_out_evenT[8 * kRowsPerThread]; + HWY_ALIGN pixel_type b_p_out_oddT[8 * kRowsPerThread]; + const HWY_CAPPED(pixel_type, 8) d; + const size_t N = Lanes(d); + if (chin_residual.w > 16 && rows == kRowsPerThread) { + for (; x < chin_residual.w - 9; x += 8) { + Transpose8x8Block(p_residual + x, b_p_residual, onerow_inr); + Transpose8x8Block(p_avg + x, b_p_avg, onerow_in); + for (size_t y = 0; y < kRowsPerThread; y++) { + b_p_avg[8 * 8 + y] = p_avg[x + 8 + onerow_in * y]; + } + for (size_t i = 0; i < 8; i++) { + FastUnsqueeze(b_p_residual + 8 * i, b_p_avg + 8 * i, + b_p_avg + 8 * (i + 1), + (x + i ? b_p_out_odd + 8 * ((x + i - 1) & 7) + : b_p_avg + 8 * i), + b_p_out_even + 8 * i, b_p_out_odd + 8 * i); + } - for (size_t x = 1; x < chin_residual.w; x++) { - pixel_type_w diff_minus_tendency = p_residual[x]; - pixel_type_w avg = p_avg[x]; - pixel_type_w next_avg = (x + 1 < chin.w ? p_avg[x + 1] : avg); - pixel_type_w left = p_out[(x << 1) - 1]; - pixel_type_w tendency = SmoothTendency(left, avg, next_avg); - pixel_type_w diff = diff_minus_tendency + tendency; - pixel_type_w A = - ((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1; - p_out[x << 1] = A; - pixel_type_w B = A - diff; - p_out[(x << 1) + 1] = B; + Transpose8x8Block(b_p_out_even, b_p_out_evenT, 8); + Transpose8x8Block(b_p_out_odd, b_p_out_oddT, 8); + for (size_t y = 0; y < kRowsPerThread; y++) { + for (size_t i = 0; i < kRowsPerThread; i += N) { + auto even = Load(d, b_p_out_evenT + 8 * y + i); + auto odd = Load(d, b_p_out_oddT + 8 * y + i); + StoreInterleaved(d, even, odd, + p_out + ((x + i) << 1) + onerow_out * y); + } + } + } } - if (chout.w & 1) p_out[chout.w - 1] = p_avg[chin.w - 1]; +#endif + for (size_t y = 0; y < rows; y++) { + unsqueeze_row(y0 + y, x); + } }, "InvHorizontalSqueeze")); input.channel[c] = std::move(chout); return true; } @@ -109,42 +219,50 @@ // Short-circuit: channel with no pixels. input.channel[c] = std::move(chout); return true; } - intptr_t onerow_in = chin.plane.PixelsPerRow(); - intptr_t onerow_out = chout.plane.PixelsPerRow(); constexpr int kColsPerThread = 64; JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, DivCeil(chin.w, kColsPerThread), ThreadPool::NoInit, [&](const uint32_t task, size_t /* thread */) { const size_t x0 = task * kColsPerThread; const size_t x1 = std::min((size_t)(task + 1) * kColsPerThread, chin.w); + const size_t w = x1 - x0; // We only iterate up to std::min(chin_residual.h, chin.h) which is // always chin_residual.h. for (size_t y = 0; y < chin_residual.h; y++) { - const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y); - const pixel_type *JXL_RESTRICT p_avg = chin.Row(y); - pixel_type *JXL_RESTRICT p_out = chout.Row(y << 1); - for (size_t x = x0; x < x1; x++) { - pixel_type_w diff_minus_tendency = p_residual[x]; - pixel_type_w avg = p_avg[x]; - - pixel_type_w next_avg = avg; - if (y + 1 < chin.h) next_avg = p_avg[x + onerow_in]; - pixel_type_w top = - (y > 0 ? p_out[static_cast(x) - onerow_out] : avg); - pixel_type_w tendency = SmoothTendency(top, avg, next_avg); - pixel_type_w diff = diff_minus_tendency + tendency; - pixel_type_w out = - ((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1; + const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y) + x0; + const pixel_type *JXL_RESTRICT p_avg = chin.Row(y) + x0; + const pixel_type *JXL_RESTRICT p_navg = + chin.Row(y + 1 < chin.h ? y + 1 : y) + x0; + pixel_type *JXL_RESTRICT p_out = chout.Row(y << 1) + x0; + pixel_type *JXL_RESTRICT p_nout = chout.Row((y << 1) + 1) + x0; + const pixel_type *p_pout = + y > 0 ? chout.Row((y << 1) - 1) + x0 : p_avg; + size_t x = 0; +#if HWY_TARGET != HWY_SCALAR + for (; x + 7 < w; x += 8) { + FastUnsqueeze(p_residual + x, p_avg + x, p_navg + x, p_pout + x, + p_out + x, p_nout + x); + } +#endif + for (; x < w; x++) { + pixel_type avg = p_avg[x]; + pixel_type next_avg = p_navg[x]; + pixel_type top = p_pout[x]; + pixel_type tendency = SmoothTendency(top, avg, next_avg); + pixel_type diff_minus_tendency = p_residual[x]; + pixel_type diff = diff_minus_tendency + tendency; + pixel_type out = + ((avg * 2) + diff + (diff < 0 ? (diff & 1) : -(diff & 1))) >> 1; p_out[x] = out; // If the chin_residual.h == chin.h, the output has an even number // of rows so the next line is fine. Otherwise, this loop won't // write to the last output row which is handled separately. - p_out[x + onerow_out] = p_out[x] - diff; + p_nout[x] = out - diff; } } }, "InvVertSqueeze")); @@ -158,10 +276,66 @@ } input.channel[c] = std::move(chout); return true; } +Status InvSqueeze(Image &input, std::vector parameters, + ThreadPool *pool) { + for (int i = parameters.size() - 1; i >= 0; i--) { + JXL_RETURN_IF_ERROR( + CheckMetaSqueezeParams(parameters[i], input.channel.size())); + bool horizontal = parameters[i].horizontal; + bool in_place = parameters[i].in_place; + uint32_t beginc = parameters[i].begin_c; + uint32_t endc = parameters[i].begin_c + parameters[i].num_c - 1; + uint32_t offset; + if (in_place) { + offset = endc + 1; + } else { + offset = input.channel.size() + beginc - endc - 1; + } + if (beginc < input.nb_meta_channels) { + // This is checked in MetaSqueeze. + JXL_ASSERT(input.nb_meta_channels > parameters[i].num_c); + input.nb_meta_channels -= parameters[i].num_c; + } + + for (uint32_t c = beginc; c <= endc; c++) { + uint32_t rc = offset + c - beginc; + // MetaApply should imply that `rc` is within range, otherwise there's a + // programming bug. + JXL_ASSERT(rc < input.channel.size()); + if ((input.channel[c].w < input.channel[rc].w) || + (input.channel[c].h < input.channel[rc].h)) { + return JXL_FAILURE("Corrupted squeeze transform"); + } + if (horizontal) { + JXL_RETURN_IF_ERROR(InvHSqueeze(input, c, rc, pool)); + } else { + JXL_RETURN_IF_ERROR(InvVSqueeze(input, c, rc, pool)); + } + } + input.channel.erase(input.channel.begin() + offset, + input.channel.begin() + offset + (endc - beginc + 1)); + } + return true; +} + +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE + +namespace jxl { + +HWY_EXPORT(InvSqueeze); +Status InvSqueeze(Image &input, std::vector parameters, + ThreadPool *pool) { + return HWY_DYNAMIC_DISPATCH(InvSqueeze)(input, parameters, pool); +} + void DefaultSqueezeParameters(std::vector *parameters, const Image &image) { int nb_channels = image.channel.size() - image.nb_meta_channels; parameters->clear(); @@ -283,48 +457,8 @@ } } return true; } -Status InvSqueeze(Image &input, std::vector parameters, - ThreadPool *pool) { - for (int i = parameters.size() - 1; i >= 0; i--) { - JXL_RETURN_IF_ERROR( - CheckMetaSqueezeParams(parameters[i], input.channel.size())); - bool horizontal = parameters[i].horizontal; - bool in_place = parameters[i].in_place; - uint32_t beginc = parameters[i].begin_c; - uint32_t endc = parameters[i].begin_c + parameters[i].num_c - 1; - uint32_t offset; - if (in_place) { - offset = endc + 1; - } else { - offset = input.channel.size() + beginc - endc - 1; - } - if (beginc < input.nb_meta_channels) { - // This is checked in MetaSqueeze. - JXL_ASSERT(input.nb_meta_channels > parameters[i].num_c); - input.nb_meta_channels -= parameters[i].num_c; - } +} // namespace jxl - for (uint32_t c = beginc; c <= endc; c++) { - uint32_t rc = offset + c - beginc; - // MetaApply should imply that `rc` is within range, otherwise there's a - // programming bug. - JXL_ASSERT(rc < input.channel.size()); - if ((input.channel[c].w < input.channel[rc].w) || - (input.channel[c].h < input.channel[rc].h)) { - return JXL_FAILURE("Corrupted squeeze transform"); - } - if (horizontal) { - JXL_RETURN_IF_ERROR(InvHSqueeze(input, c, rc, pool)); - } else { - JXL_RETURN_IF_ERROR(InvVSqueeze(input, c, rc, pool)); - } - } - input.channel.erase(input.channel.begin() + offset, - input.channel.begin() + offset + (endc - beginc + 1)); - } - return true; -} - -} // namespace jxl +#endif \ No newline at end of file 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 @@ -23,10 +23,11 @@ #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/color_management.h" #include "lib/jxl/dec_file.h" #include "lib/jxl/dec_params.h" #include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_butteraugli_pnorm.h" #include "lib/jxl/enc_cache.h" #include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_file.h" #include "lib/jxl/enc_params.h" #include "lib/jxl/image.h" @@ -45,13 +46,12 @@ void TestLosslessGroups(size_t group_size_shift) { ThreadPool* pool = nullptr; const PaddedBytes orig = ReadTestData("imagecompression.info/flower_foveon.png"); CompressParams cparams; - cparams.modular_mode = true; + cparams.SetLossless(); cparams.modular_group_size_shift = group_size_shift; - cparams.color_transform = jxl::ColorTransform::kNone; DecompressParams dparams; CodecInOut io_out; size_t compressed_size; @@ -79,17 +79,16 @@ TEST(ModularTest, RoundtripLosslessCustomWP_PermuteRCT) { ThreadPool* pool = nullptr; const PaddedBytes orig = ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CompressParams cparams; - cparams.modular_mode = true; + cparams.SetLossless(); // 9 = permute to GBR, to test the special case of permutation-only cparams.colorspace = 9; // slowest speed so different WP modes are tried cparams.speed_tier = SpeedTier::kTortoise; cparams.options.predictor = {Predictor::Weighted}; - cparams.color_transform = jxl::ColorTransform::kNone; DecompressParams dparams; CodecInOut io_out; size_t compressed_size; @@ -133,12 +132,11 @@ TEST(ModularTest, RoundtripLossyDeltaPaletteWP) { ThreadPool* pool = nullptr; const PaddedBytes orig = ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CompressParams cparams; - cparams.modular_mode = true; - cparams.color_transform = jxl::ColorTransform::kNone; + cparams.SetLossless(); cparams.lossy_palette = true; cparams.palette_colors = 0; cparams.options.predictor = jxl::Predictor::Weighted; DecompressParams dparams; @@ -162,34 +160,34 @@ ThreadPool* pool = nullptr; const PaddedBytes orig = ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); CompressParams cparams; cparams.modular_mode = true; - cparams.quality_pair = {80.0f, 80.0f}; + cparams.butteraugli_distance = 2.f; DecompressParams dparams; CodecInOut io_out; size_t compressed_size; CodecInOut io; ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); - EXPECT_LE(compressed_size, 40000u); + EXPECT_LE(compressed_size, 30000u); cparams.ba_params.intensity_target = 80.0f; EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(2.0)); + IsSlightlyBelow(2.3)); } TEST(ModularTest, RoundtripLossy16) { ThreadPool* pool = nullptr; const PaddedBytes orig = ReadTestData("raw.pixls/DJI-FC6310-16bit_709_v4_krita.png"); CompressParams cparams; cparams.modular_mode = true; - cparams.quality_pair = {80.0f, 80.0f}; + cparams.butteraugli_distance = 2.f; DecompressParams dparams; CodecInOut io_out; size_t compressed_size; @@ -197,15 +195,15 @@ ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); JXL_CHECK(io.TransformTo(ColorEncoding::SRGB(), GetJxlCms(), pool)); io.metadata.m.color_encoding = ColorEncoding::SRGB(); compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); - EXPECT_LE(compressed_size, 400u); + EXPECT_LE(compressed_size, 300u); cparams.ba_params.intensity_target = 80.0f; EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool), - IsSlightlyBelow(1.5)); + IsSlightlyBelow(1.6)); } TEST(ModularTest, RoundtripExtraProperties) { constexpr size_t kSize = 250; Image image(kSize, kSize, /*bitdepth=*/8, 3); @@ -256,11 +254,11 @@ ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); CompressParams cparams; cparams.modular_mode = true; cparams.color_transform = jxl::ColorTransform::kNone; - cparams.quality_pair = {100, 100}; + cparams.butteraugli_distance = 0.f; cparams.options.predictor = {Predictor::Zero}; cparams.speed_tier = SpeedTier::kThunder; cparams.responsive = 1; // Custom squeeze params, atm just for testing SqueezeParams p; @@ -279,10 +277,109 @@ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 265000u); EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(), /*distmap=*/nullptr, pool)); } +struct RoundtripLosslessConfig { + int bitdepth; + int responsive; +}; +class ModularTestParam + : public ::testing::TestWithParam {}; + +std::vector GenerateLosslessTests() { + std::vector all; + for (int responsive = 0; responsive <= 1; responsive++) { + for (int bitdepth = 1; bitdepth < 32; bitdepth++) { + if (responsive && bitdepth > 30) continue; + all.push_back({bitdepth, responsive}); + } + } + return all; +} +std::string LosslessTestDescription( + const testing::TestParamInfo& info) { + std::stringstream name; + name << info.param.bitdepth << "bit"; + if (info.param.responsive) name << "Squeeze"; + return name.str(); +} + +JXL_GTEST_INSTANTIATE_TEST_SUITE_P(RoundtripLossless, ModularTestParam, + testing::ValuesIn(GenerateLosslessTests()), + LosslessTestDescription); + +TEST_P(ModularTestParam, RoundtripLossless) { + RoundtripLosslessConfig config = GetParam(); + int bitdepth = config.bitdepth; + int responsive = config.responsive; + + ThreadPool* pool = nullptr; + Rng generator(123); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io1; + ASSERT_TRUE(SetFromBytes(Span(orig), &io1, pool)); + + // vary the dimensions a bit, in case of bugs related to + // even vs odd width or height. + size_t xsize = 423 + bitdepth; + size_t ysize = 467 + bitdepth; + + CodecInOut io; + io.SetSize(xsize, ysize); + io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB(false); + io.metadata.m.SetUintSamples(bitdepth); + + double factor = ((1lu << bitdepth) - 1lu); + double ifactor = 1.0 / factor; + Image3F noise_added(xsize, ysize); + + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < ysize; y++) { + const float* in = io1.Main().color()->PlaneRow(c, y); + float* out = noise_added.PlaneRow(c, y); + for (size_t x = 0; x < xsize; x++) { + // make the least significant bits random + float f = in[x] + generator.UniformF(0.0f, 1.f / 255.f); + if (f > 1.f) f = 1.f; + // quantize to the bitdepth we're testing + unsigned int u = f * factor + 0.5; + out[x] = u * ifactor; + } + } + } + io.SetFromImage(std::move(noise_added), jxl::ColorEncoding::SRGB(false)); + + CompressParams cparams; + cparams.modular_mode = true; + cparams.color_transform = jxl::ColorTransform::kNone; + cparams.butteraugli_distance = 0.f; + cparams.options.predictor = {Predictor::Zero}; + cparams.speed_tier = SpeedTier::kThunder; + cparams.responsive = responsive; + DecompressParams dparams; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), + bitdepth * xsize * ysize / 3); + EXPECT_LE(0, ComputeDistance2(io.Main(), io2.Main(), GetJxlCms())); + size_t different = 0; + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < ysize; y++) { + const float* in = io.Main().color()->PlaneRow(c, y); + const float* out = io2.Main().color()->PlaneRow(c, y); + for (size_t x = 0; x < xsize; x++) { + uint32_t uin = in[x] * factor + 0.5; + uint32_t uout = out[x] * factor + 0.5; + // check that the integer values are identical + if (uin != uout) different++; + } + } + } + EXPECT_EQ(different, 0); +} + TEST(ModularTest, RoundtripLosslessCustomFloat) { ThreadPool* pool = nullptr; CodecInOut io; size_t xsize = 100, ysize = 300; io.SetSize(xsize, ysize); @@ -308,11 +405,11 @@ io.metadata.m.SetIntensityTarget(255); CompressParams cparams; cparams.modular_mode = true; cparams.color_transform = jxl::ColorTransform::kNone; - cparams.quality_pair = {100, 100}; + cparams.butteraugli_distance = 0.f; cparams.options.predictor = {Predictor::Zero}; cparams.speed_tier = SpeedTier::kThunder; cparams.decoding_speed_tier = 2; DecompressParams dparams; diff --git a/third_party/jpeg-xl/lib/jxl/patch_dictionary_test.cc b/third_party/jpeg-xl/lib/jxl/patch_dictionary_test.cc --- a/third_party/jpeg-xl/lib/jxl/patch_dictionary_test.cc +++ b/third_party/jpeg-xl/lib/jxl/patch_dictionary_test.cc @@ -22,12 +22,11 @@ const PaddedBytes orig = ReadTestData("jxl/grayscale_patches.png"); CodecInOut io; ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); CompressParams cparams; - cparams.color_transform = jxl::ColorTransform::kNone; - cparams.modular_mode = true; + cparams.SetLossless(); cparams.patches = jxl::Override::kOn; DecompressParams dparams; CodecInOut io2; // Without patches: ~25k diff --git a/third_party/jpeg-xl/lib/jxl/render_pipeline/low_memory_render_pipeline.h b/third_party/jpeg-xl/lib/jxl/render_pipeline/low_memory_render_pipeline.h --- a/third_party/jpeg-xl/lib/jxl/render_pipeline/low_memory_render_pipeline.h +++ b/third_party/jpeg-xl/lib/jxl/render_pipeline/low_memory_render_pipeline.h @@ -44,15 +44,13 @@ std::pair BorderToStore(size_t c) const; bool use_group_ids_; - // Storage for borders between groups. - // We also store the extremal borders for simplicity. Horizontal borders are - // stored in an image as wide as the main frame, in top-to-bottom order (top - // border of a group first, followed by the bottom border, followed by top - // border of the next group). Vertical borders are similarly stored. + // Storage for borders between groups. Borders of adjacent groups are stacked + // together, e.g. bottom border of current group is followed by top border + // of next group. std::vector borders_horizontal_; std::vector borders_vertical_; // Manages the status of borders. GroupBorderAssigner group_border_assigner_; 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 @@ -48,28 +48,35 @@ auto borders = BorderToStore(c); size_t borderx_write = borders.first; size_t bordery_write = borders.second; - CopyImageTo( - Rect(group_data_x_border_, group_data_y_border_, x1 - x0, bordery_write), - in, Rect(x0, (gy * 2) * bordery_write, x1 - x0, bordery_write), - &borders_horizontal_[c]); - CopyImageTo( - Rect(group_data_x_border_, group_data_y_border_ + y1 - y0 - bordery_write, - x1 - x0, bordery_write), - in, Rect(x0, (gy * 2 + 1) * bordery_write, x1 - x0, bordery_write), - &borders_horizontal_[c]); - CopyImageTo( - Rect(group_data_x_border_, group_data_y_border_, borderx_write, y1 - y0), - in, Rect((gx * 2) * borderx_write, y0, borderx_write, y1 - y0), - &borders_vertical_[c]); - CopyImageTo(Rect(group_data_x_border_ + x1 - x0 - borderx_write, - group_data_y_border_, borderx_write, y1 - y0), - in, - Rect((gx * 2 + 1) * borderx_write, y0, borderx_write, y1 - y0), - &borders_vertical_[c]); + if (gy > 0) { + Rect from(group_data_x_border_, group_data_y_border_, x1 - x0, + bordery_write); + Rect to(x0, (gy * 2 - 1) * bordery_write, x1 - x0, bordery_write); + CopyImageTo(from, in, to, &borders_horizontal_[c]); + } + if (gy + 1 < frame_dimensions_.ysize_groups) { + Rect from(group_data_x_border_, + group_data_y_border_ + y1 - y0 - bordery_write, x1 - x0, + bordery_write); + Rect to(x0, (gy * 2) * bordery_write, x1 - x0, bordery_write); + CopyImageTo(from, in, to, &borders_horizontal_[c]); + } + if (gx > 0) { + Rect from(group_data_x_border_, group_data_y_border_, borderx_write, + y1 - y0); + Rect to((gx * 2 - 1) * borderx_write, y0, borderx_write, y1 - y0); + CopyImageTo(from, in, to, &borders_vertical_[c]); + } + if (gx + 1 < frame_dimensions_.xsize_groups) { + Rect from(group_data_x_border_ + x1 - x0 - borderx_write, + group_data_y_border_, borderx_write, y1 - y0); + Rect to((gx * 2) * borderx_write, y0, borderx_write, y1 - y0); + CopyImageTo(from, in, to, &borders_vertical_[c]); + } } void LowMemoryRenderPipeline::LoadBorders(size_t group_id, size_t c, const Rect& r, ImageF* out) { size_t gy = group_id / frame_dimensions_.xsize_groups; @@ -117,41 +124,41 @@ // Copy other groups' borders from the border storage. if (y0src < y0) { JXL_DASSERT(gy > 0); CopyImageTo( - Rect(x0src, (gy * 2 - 1) * bordery_write, x1src - x0src, bordery_write), + Rect(x0src, (gy * 2 - 2) * bordery_write, x1src - x0src, bordery_write), borders_horizontal_[c], Rect(group_data_x_border_ + x0src - x0, group_data_y_border_ - bordery_write, x1src - x0src, bordery_write), out); } if (y1src > y1) { // When copying the bottom border we must not be on the bottom groups. JXL_DASSERT(gy + 1 < frame_dimensions_.ysize_groups); CopyImageTo( - Rect(x0src, (gy * 2 + 2) * bordery_write, x1src - x0src, bordery_write), + Rect(x0src, (gy * 2 + 1) * bordery_write, x1src - x0src, bordery_write), borders_horizontal_[c], Rect(group_data_x_border_ + x0src - x0, group_data_y_border_ + y1 - y0, x1src - x0src, bordery_write), out); } if (x0src < x0) { JXL_DASSERT(gx > 0); CopyImageTo( - Rect((gx * 2 - 1) * borderx_write, y0src, borderx_write, y1src - y0src), + Rect((gx * 2 - 2) * borderx_write, y0src, borderx_write, y1src - y0src), borders_vertical_[c], Rect(group_data_x_border_ - borderx_write, group_data_y_border_ + y0src - y0, borderx_write, y1src - y0src), out); } if (x1src > x1) { // When copying the right border we must not be on the rightmost groups. JXL_DASSERT(gx + 1 < frame_dimensions_.xsize_groups); CopyImageTo( - Rect((gx * 2 + 2) * borderx_write, y0src, borderx_write, y1src - y0src), + Rect((gx * 2 + 1) * borderx_write, y0src, borderx_write, y1src - y0src), borders_vertical_[c], Rect(group_data_x_border_ + x1 - x0, group_data_y_border_ + y0src - y0, borderx_write, y1src - y0src), out); } @@ -175,20 +182,23 @@ } for (size_t c = 0; c < shifts.size(); c++) { auto borders = BorderToStore(c); size_t borderx = borders.first; size_t bordery = borders.second; - Rect horizontal = Rect( - 0, 0, - DivCeil(frame_dimensions_.xsize_upsampled_padded, 1 << shifts[c].first), - bordery * frame_dimensions_.ysize_groups * 2); + JXL_DASSERT(frame_dimensions_.xsize_groups > 0); + size_t num_xborders = (frame_dimensions_.xsize_groups - 1) * 2; + JXL_DASSERT(frame_dimensions_.ysize_groups > 0); + size_t num_yborders = (frame_dimensions_.ysize_groups - 1) * 2; + size_t downsampled_xsize = + DivCeil(frame_dimensions_.xsize_upsampled_padded, 1 << shifts[c].first); + size_t downsampled_ysize = DivCeil(frame_dimensions_.ysize_upsampled_padded, + 1 << shifts[c].second); + Rect horizontal = Rect(0, 0, downsampled_xsize, bordery * num_yborders); if (!SameSize(horizontal, borders_horizontal_[c])) { borders_horizontal_[c] = ImageF(horizontal.xsize(), horizontal.ysize()); } - Rect vertical = Rect(0, 0, borderx * frame_dimensions_.xsize_groups * 2, - DivCeil(frame_dimensions_.ysize_upsampled_padded, - 1 << shifts[c].second)); + Rect vertical = Rect(0, 0, borderx * num_xborders, downsampled_ysize); if (!SameSize(vertical, borders_vertical_[c])) { borders_vertical_[c] = ImageF(vertical.xsize(), vertical.ysize()); } } } @@ -361,14 +371,15 @@ GroupInputYSize(c) + group_data_y_border_ * 2); } } // TODO(veluca): avoid reallocating buffers if not needed. stage_data_.resize(num); + size_t upsampling = 1u << base_color_shift_; size_t stage_buffer_xsize = - (frame_dimensions_.group_dim << base_color_shift_) + - 2 * group_data_x_border_ + // maximum size of a rect - 2 * kRenderPipelineXOffset; // extra padding for processing + frame_dimensions_.group_dim * upsampling + + 2 * group_data_x_border_ * upsampling + // maximum size of a rect + 2 * kRenderPipelineXOffset; // extra padding for processing for (size_t t = 0; t < num; t++) { stage_data_[t].resize(shifts.size()); for (size_t c = 0; c < shifts.size(); c++) { stage_data_[t][c].resize(stages_.size()); size_t next_y_border = 0; 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 @@ -346,11 +346,11 @@ { auto s = settings; s.cparams_descr = "ModularLossy"; s.cparams.modular_mode = true; - s.cparams.quality_pair = {90, 90}; + s.cparams.butteraugli_distance = 1.f; all_tests.push_back(s); } { auto s = settings; diff --git a/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_write.cc b/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_write.cc --- a/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_write.cc +++ b/third_party/jpeg-xl/lib/jxl/render_pipeline/stage_write.cc @@ -188,10 +188,11 @@ for (size_t c = 1; c < input_sizes.size(); c++) { JXL_ASSERT(input_sizes[c].first == input_sizes[0].first); JXL_ASSERT(input_sizes[c].second == input_sizes[0].second); } #endif + // TODO(eustas): what should we do in the case of "want only ECs"? image_bundle_->SetFromImage( Image3F(input_sizes[0].first, input_sizes[0].second), color_encoding_); // TODO(veluca): consider not reallocating ECs if not needed. image_bundle_->extra_channels().clear(); for (size_t c = 3; c < input_sizes.size(); c++) { 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 @@ -265,12 +265,13 @@ std::vector channel_infos; for (const auto& extra_channel : extra_channels) { auto channel_type = extra_channel.first; JxlExtraChannelInfo channel_info; JxlEncoderInitExtraChannelInfo(channel_type, &channel_info); - channel_info.bits_per_sample = basic_info.bits_per_sample; - channel_info.exponent_bits_per_sample = basic_info.exponent_bits_per_sample; + channel_info.bits_per_sample = (lossless ? basic_info.bits_per_sample : 8); + channel_info.exponent_bits_per_sample = + (lossless ? basic_info.exponent_bits_per_sample : 0); channel_infos.push_back(channel_info); } for (size_t index = 0; index < channel_infos.size(); index++) { EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetExtraChannelInfo(enc, index + has_interleaved_alpha, @@ -444,16 +445,10 @@ extra_channel_bytes.data(), xsize, ysize, extra_channel_pixel_format, extra_channel_output_pixel_format), 0u); EXPECT_EQ(extra_channel, extra_channel_bytes); - } else { - EXPECT_EQ(jxl::test::ComparePixels( - extra_channel.data(), extra_channel_bytes.data(), xsize, - ysize, extra_channel_pixel_format, - extra_channel_output_pixel_format, 16.0), - 0u); } } } } // namespace @@ -586,10 +581,11 @@ JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc, 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info)); JxlColorEncoding color_encoding; if (pixel_format.data_type == JXL_TYPE_FLOAT) { JxlColorEncodingSetToLinearSRGB(&color_encoding, /*is_gray=*/pixel_format.num_channels < 3); diff --git a/third_party/jpeg-xl/lib/jxl/simd_util-inl.h b/third_party/jpeg-xl/lib/jxl/simd_util-inl.h --- a/third_party/jpeg-xl/lib/jxl/simd_util-inl.h +++ b/third_party/jpeg-xl/lib/jxl/simd_util-inl.h @@ -12,10 +12,12 @@ #define LIB_JXL_SIMD_UTIL_INL_H_ #endif #include +#include "lib/jxl/base/compiler_specific.h" + HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { #if HWY_CAP_GE512 @@ -37,39 +39,45 @@ #endif // Stores v0[0], v1[0], v0[1], v1[1], ... to mem, in this order. Mem must be // aligned. -template -void StoreInterleaved(const DF df, V v0, V v1, float* mem) { +template +void StoreInterleaved(const DF df, V v0, V v1, T* mem) { + static_assert(sizeof(T) == 4, "only use StoreInterleaved for 4-byte types"); #if HWY_TARGET == HWY_SCALAR Store(v0, df, mem); Store(v1, df, mem + 1); #elif !HWY_CAP_GE256 Store(InterleaveLower(df, v0, v1), df, mem); Store(InterleaveUpper(df, v0, v1), df, mem + Lanes(df)); -#elif !HWY_CAP_GE512 - auto t0 = InterleaveLower(df, v0, v1); - auto t1 = InterleaveUpper(df, v0, v1); - Store(ConcatLowerLower(df, t1, t0), df, mem); - Store(ConcatUpperUpper(df, t1, t0), df, mem + Lanes(df)); #else - auto t0 = InterleaveLower(df, v0, v1); - auto t1 = InterleaveUpper(df, v0, v1); - Store(Concat4(df, Quarter<0>(df, t0), Quarter<0>(df, t1), Quarter<1>(df, t0), - Quarter<1>(df, t1)), - df, mem); - Store(Concat4(df, Quarter<2>(df, t0), Quarter<2>(df, t1), Quarter<3>(df, t0), - Quarter<3>(df, t1)), - df, mem + Lanes(df)); + if (!HWY_CAP_GE512 || Lanes(df) == 8) { + auto t0 = InterleaveLower(df, v0, v1); + auto t1 = InterleaveUpper(df, v0, v1); + Store(ConcatLowerLower(df, t1, t0), df, mem); + Store(ConcatUpperUpper(df, t1, t0), df, mem + Lanes(df)); + } else { +#if HWY_CAP_GE512 + auto t0 = InterleaveLower(df, v0, v1); + auto t1 = InterleaveUpper(df, v0, v1); + Store(Concat4(df, Quarter<0>(df, t0), Quarter<0>(df, t1), + Quarter<1>(df, t0), Quarter<1>(df, t1)), + df, mem); + Store(Concat4(df, Quarter<2>(df, t0), Quarter<2>(df, t1), + Quarter<3>(df, t0), Quarter<3>(df, t1)), + df, mem + Lanes(df)); +#endif + } #endif } // Stores v0[0], v1[0], v2[0], v3[0], v0[1] ... to mem, in this order. Mem must // be aligned. -template -void StoreInterleaved(const DF df, V v0, V v1, V v2, V v3, float* mem) { +template +void StoreInterleaved(const DF df, V v0, V v1, V v2, V v3, T* mem) { + static_assert(sizeof(T) == 4, "only use StoreInterleaved for 4-byte types"); #if HWY_TARGET == HWY_SCALAR Store(v0, df, mem); Store(v1, df, mem + 1); Store(v2, df, mem + 2); Store(v3, df, mem + 3); @@ -253,10 +261,88 @@ Quarter<3>(df, m7)), df, mem + 7 * Lanes(df)); #endif } +#if HWY_CAP_GE256 +JXL_INLINE void Transpose8x8Block(const int32_t* JXL_RESTRICT from, + int32_t* JXL_RESTRICT to, size_t fromstride) { + const HWY_CAPPED(int32_t, 8) d; + auto i0 = Load(d, from); + auto i1 = Load(d, from + 1 * fromstride); + auto i2 = Load(d, from + 2 * fromstride); + auto i3 = Load(d, from + 3 * fromstride); + auto i4 = Load(d, from + 4 * fromstride); + auto i5 = Load(d, from + 5 * fromstride); + auto i6 = Load(d, from + 6 * fromstride); + auto i7 = Load(d, from + 7 * fromstride); + + const auto q0 = InterleaveLower(d, i0, i2); + const auto q1 = InterleaveLower(d, i1, i3); + const auto q2 = InterleaveUpper(d, i0, i2); + const auto q3 = InterleaveUpper(d, i1, i3); + const auto q4 = InterleaveLower(d, i4, i6); + const auto q5 = InterleaveLower(d, i5, i7); + const auto q6 = InterleaveUpper(d, i4, i6); + const auto q7 = InterleaveUpper(d, i5, i7); + + const auto r0 = InterleaveLower(d, q0, q1); + const auto r1 = InterleaveUpper(d, q0, q1); + const auto r2 = InterleaveLower(d, q2, q3); + const auto r3 = InterleaveUpper(d, q2, q3); + const auto r4 = InterleaveLower(d, q4, q5); + const auto r5 = InterleaveUpper(d, q4, q5); + const auto r6 = InterleaveLower(d, q6, q7); + const auto r7 = InterleaveUpper(d, q6, q7); + + i0 = ConcatLowerLower(d, r4, r0); + i1 = ConcatLowerLower(d, r5, r1); + i2 = ConcatLowerLower(d, r6, r2); + i3 = ConcatLowerLower(d, r7, r3); + i4 = ConcatUpperUpper(d, r4, r0); + i5 = ConcatUpperUpper(d, r5, r1); + i6 = ConcatUpperUpper(d, r6, r2); + i7 = ConcatUpperUpper(d, r7, r3); + + Store(i0, d, to); + Store(i1, d, to + 1 * 8); + Store(i2, d, to + 2 * 8); + Store(i3, d, to + 3 * 8); + Store(i4, d, to + 4 * 8); + Store(i5, d, to + 5 * 8); + Store(i6, d, to + 6 * 8); + Store(i7, d, to + 7 * 8); +} +#elif HWY_TARGET != HWY_SCALAR +JXL_INLINE void Transpose8x8Block(const int32_t* JXL_RESTRICT from, + int32_t* JXL_RESTRICT to, size_t fromstride) { + const HWY_CAPPED(int32_t, 4) d; + for (size_t n = 0; n < 8; n += 4) { + for (size_t m = 0; m < 8; m += 4) { + auto p0 = Load(d, from + n * fromstride + m); + auto p1 = Load(d, from + (n + 1) * fromstride + m); + auto p2 = Load(d, from + (n + 2) * fromstride + m); + auto p3 = Load(d, from + (n + 3) * fromstride + m); + const auto q0 = InterleaveLower(d, p0, p2); + const auto q1 = InterleaveLower(d, p1, p3); + const auto q2 = InterleaveUpper(d, p0, p2); + const auto q3 = InterleaveUpper(d, p1, p3); + + const auto r0 = InterleaveLower(d, q0, q1); + const auto r1 = InterleaveUpper(d, q0, q1); + const auto r2 = InterleaveLower(d, q2, q3); + const auto r3 = InterleaveUpper(d, q2, q3); + Store(r0, d, to + m * 8 + n); + Store(r1, d, to + (1 + m) * 8 + n); + Store(r2, d, to + (2 + m) * 8 + n); + Store(r3, d, to + (3 + m) * 8 + n); + } + } +} + +#endif + // NOLINTNEXTLINE(google-readability-namespace-comments) } // namespace HWY_NAMESPACE } // namespace jxl HWY_AFTER_NAMESPACE(); 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 @@ -4,12 +4,10 @@ # license that can be found in the LICENSE file. set(JPEGXL_EXTRAS_SOURCES extras/codec.cc extras/codec.h - extras/codec_psd.cc - extras/codec_psd.h extras/dec/color_description.cc extras/dec/color_description.h extras/dec/color_hints.cc extras/dec/color_hints.h extras/dec/decode.cc 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 @@ -122,10 +122,12 @@ "jxl/enc_bit_writer.h", "jxl/entropy_coder.cc", "jxl/entropy_coder.h", "jxl/epf.cc", "jxl/epf.h", + "jxl/exif.cc", + "jxl/exif.h", "jxl/fast_dct-inl.h", "jxl/fast_dct.cc", "jxl/fast_dct.h", "jxl/fast_dct128-inl.h", "jxl/fast_dct16-inl.h", @@ -434,12 +436,10 @@ ] libjxl_extras_sources = [ "extras/codec.cc", "extras/codec.h", - "extras/codec_psd.cc", - "extras/codec_psd.h", "extras/dec/color_description.cc", "extras/dec/color_description.h", "extras/dec/color_hints.cc", "extras/dec/color_hints.h", "extras/dec/decode.cc",