summaryrefslogtreecommitdiffstats
path: root/third_party/jpeg-xl/lib/extras
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/jpeg-xl/lib/extras
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/jpeg-xl/lib/extras')
-rw-r--r--third_party/jpeg-xl/lib/extras/LICENSE.apngdis27
-rw-r--r--third_party/jpeg-xl/lib/extras/README.md5
-rw-r--r--third_party/jpeg-xl/lib/extras/codec.cc191
-rw-r--r--third_party/jpeg-xl/lib/extras/codec.h70
-rw-r--r--third_party/jpeg-xl/lib/extras/codec_test.cc645
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/apng.cc962
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/apng.h34
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/color_description.cc218
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/color_description.h23
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/color_description_test.cc38
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/color_hints.cc66
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/color_hints.h72
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/decode.cc132
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/decode.h55
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/exr.cc184
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/exr.h32
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/gif.cc400
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/gif.h33
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jpegli.cc271
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jpegli.h41
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jpg.cc322
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jpg.h44
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jxl.cc561
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jxl.h69
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pgx.cc202
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pgx.h35
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pgx_test.cc79
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pnm.cc474
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pnm.h41
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/apng.cc371
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/apng.h23
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/encode.cc170
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/encode.h83
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/exr.cc200
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/exr.h23
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/jpegli.cc503
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/jpegli.h47
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/jpg.cc427
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/jpg.h23
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/jxl.cc276
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/jxl.h78
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/npy.cc322
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/npy.h23
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/pgx.cc123
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/pgx.h24
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/pnm.cc303
-rw-r--r--third_party/jpeg-xl/lib/extras/enc/pnm.h28
-rw-r--r--third_party/jpeg-xl/lib/extras/exif.cc55
-rw-r--r--third_party/jpeg-xl/lib/extras/exif.h20
-rw-r--r--third_party/jpeg-xl/lib/extras/hlg.cc56
-rw-r--r--third_party/jpeg-xl/lib/extras/hlg.h21
-rw-r--r--third_party/jpeg-xl/lib/extras/jpegli_test.cc405
-rw-r--r--third_party/jpeg-xl/lib/extras/packed_image.h170
-rw-r--r--third_party/jpeg-xl/lib/extras/packed_image_convert.cc300
-rw-r--r--third_party/jpeg-xl/lib/extras/packed_image_convert.h36
-rw-r--r--third_party/jpeg-xl/lib/extras/size_constraints.h43
-rw-r--r--third_party/jpeg-xl/lib/extras/time.cc60
-rw-r--r--third_party/jpeg-xl/lib/extras/time.h19
-rw-r--r--third_party/jpeg-xl/lib/extras/tone_mapping.cc132
-rw-r--r--third_party/jpeg-xl/lib/extras/tone_mapping.h30
-rw-r--r--third_party/jpeg-xl/lib/extras/tone_mapping_gbench.cc40
61 files changed, 9760 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/lib/extras/LICENSE.apngdis b/third_party/jpeg-xl/lib/extras/LICENSE.apngdis
new file mode 100644
index 0000000000..eb0ba7c07b
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/LICENSE.apngdis
@@ -0,0 +1,27 @@
+APNG Disassembler 2.8
+
+Deconstructs APNG files into individual frames.
+
+http://apngdis.sourceforge.net
+
+Copyright (c) 2010-2015 Max Stepin
+maxst at users.sourceforge.net
+
+zlib license
+------------
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
diff --git a/third_party/jpeg-xl/lib/extras/README.md b/third_party/jpeg-xl/lib/extras/README.md
new file mode 100644
index 0000000000..06a9b5ea07
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/README.md
@@ -0,0 +1,5 @@
+## JPEG XL "extras"
+
+The files in this directory do not form part of the library or codec and are
+only used by tests or specific internal tools that have access to the internals
+of the library.
diff --git a/third_party/jpeg-xl/lib/extras/codec.cc b/third_party/jpeg-xl/lib/extras/codec.cc
new file mode 100644
index 0000000000..5d3f00706e
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/codec.cc
@@ -0,0 +1,191 @@
+// Copyright (c) the JPEG XL Project 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.h"
+
+#include <jxl/decode.h>
+#include <jxl/types.h>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/status.h"
+
+#if JPEGXL_ENABLE_APNG
+#include "lib/extras/enc/apng.h"
+#endif
+#if JPEGXL_ENABLE_JPEG
+#include "lib/extras/enc/jpg.h"
+#endif
+#if JPEGXL_ENABLE_EXR
+#include "lib/extras/enc/exr.h"
+#endif
+
+#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"
+#include "lib/jxl/image_bundle.h"
+
+namespace jxl {
+namespace {
+
+// Any valid encoding is larger (ensures codecs can read the first few bytes)
+constexpr size_t kMinBytes = 9;
+
+} // namespace
+
+Status SetFromBytes(const Span<const uint8_t> bytes,
+ const extras::ColorHints& color_hints, CodecInOut* io,
+ ThreadPool* pool, const SizeConstraints* constraints,
+ extras::Codec* orig_codec) {
+ if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes");
+
+ extras::PackedPixelFile ppf;
+ if (extras::DecodeBytes(bytes, color_hints, &ppf, constraints, orig_codec)) {
+ return ConvertPackedPixelFileToCodecInOut(ppf, pool, io);
+ }
+ return JXL_FAILURE("Codecs failed to decode");
+}
+
+Status SetFromFile(const std::string& pathname,
+ const extras::ColorHints& color_hints, CodecInOut* io,
+ ThreadPool* pool, const SizeConstraints* constraints,
+ extras::Codec* orig_codec) {
+ std::vector<uint8_t> encoded;
+ JXL_RETURN_IF_ERROR(ReadFile(pathname, &encoded));
+ JXL_RETURN_IF_ERROR(SetFromBytes(Span<const uint8_t>(encoded), color_hints,
+ io, pool, constraints, orig_codec));
+ return true;
+}
+
+Status Encode(const CodecInOut& io, const extras::Codec codec,
+ const ColorEncoding& c_desired, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes, ThreadPool* pool) {
+ JXL_CHECK(!io.Main().c_current().ICC().empty());
+ JXL_CHECK(!c_desired.ICC().empty());
+ io.CheckMetadata();
+ if (io.Main().IsJPEG()) {
+ JXL_WARNING("Writing JPEG data as pixels");
+ }
+ JxlPixelFormat format = {
+ 0, // num_channels is ignored by the converter
+ bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, JXL_BIG_ENDIAN,
+ 0};
+ const bool floating_point = bits_per_sample > 16;
+ std::unique_ptr<extras::Encoder> encoder;
+ std::ostringstream os;
+ switch (codec) {
+ case extras::Codec::kPNG:
+#if JPEGXL_ENABLE_APNG
+ encoder = extras::GetAPNGEncoder();
+ break;
+#else
+ return JXL_FAILURE("JPEG XL was built without (A)PNG support");
+#endif
+ case extras::Codec::kJPG:
+#if JPEGXL_ENABLE_JPEG
+ format.data_type = JXL_TYPE_UINT8;
+ encoder = extras::GetJPEGEncoder();
+ os << io.jpeg_quality;
+ encoder->SetOption("q", os.str());
+ break;
+#else
+ return JXL_FAILURE("JPEG XL was built without JPEG support");
+#endif
+ case extras::Codec::kPNM:
+ if (io.Main().HasAlpha()) {
+ encoder = extras::GetPAMEncoder();
+ } else if (io.Main().IsGray()) {
+ encoder = extras::GetPGMEncoder();
+ } else if (!floating_point) {
+ encoder = extras::GetPPMEncoder();
+ } else {
+ format.data_type = JXL_TYPE_FLOAT;
+ format.endianness = JXL_LITTLE_ENDIAN;
+ encoder = extras::GetPFMEncoder();
+ }
+ break;
+ case extras::Codec::kPGX:
+ encoder = extras::GetPGXEncoder();
+ break;
+ case extras::Codec::kGIF:
+ return JXL_FAILURE("Encoding to GIF is not implemented");
+ case extras::Codec::kEXR:
+#if JPEGXL_ENABLE_EXR
+ format.data_type = JXL_TYPE_FLOAT;
+ encoder = extras::GetEXREncoder();
+ break;
+#else
+ return JXL_FAILURE("JPEG XL was built without OpenEXR support");
+#endif
+ case extras::Codec::kUnknown:
+ return JXL_FAILURE("Cannot encode using Codec::kUnknown");
+ }
+
+ if (!encoder) {
+ return JXL_FAILURE("Invalid codec.");
+ }
+
+ extras::PackedPixelFile ppf;
+ JXL_RETURN_IF_ERROR(
+ ConvertCodecInOutToPackedPixelFile(io, format, c_desired, pool, &ppf));
+ ppf.info.bits_per_sample = bits_per_sample;
+ if (format.data_type == JXL_TYPE_FLOAT) {
+ ppf.info.bits_per_sample = 32;
+ ppf.info.exponent_bits_per_sample = 8;
+ }
+ extras::EncodedImage encoded_image;
+ JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded_image, pool));
+ JXL_ASSERT(encoded_image.bitstreams.size() == 1);
+ *bytes = encoded_image.bitstreams[0];
+
+ return true;
+}
+
+Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired,
+ size_t bits_per_sample, const std::string& pathname,
+ ThreadPool* pool) {
+ const std::string extension = Extension(pathname);
+ const extras::Codec codec =
+ extras::CodecFromExtension(extension, &bits_per_sample);
+
+ // Warn about incorrect usage of PGM/PGX/PPM - only the latter supports
+ // color, but CodecFromExtension lumps them all together.
+ if (codec == extras::Codec::kPNM && extension != ".pfm") {
+ if (io.Main().HasAlpha() && extension != ".pam") {
+ JXL_WARNING(
+ "For images with alpha, the filename should end with .pam.\n");
+ } else if (!io.Main().IsGray() && extension == ".pgm") {
+ JXL_WARNING("For color images, the filename should end with .ppm.\n");
+ } else if (io.Main().IsGray() && extension == ".ppm") {
+ JXL_WARNING(
+ "For grayscale images, the filename should not end with .ppm.\n");
+ }
+ if (bits_per_sample > 16) {
+ JXL_WARNING("PPM only supports up to 16 bits per sample");
+ bits_per_sample = 16;
+ }
+ } else if (codec == extras::Codec::kPGX && !io.Main().IsGray()) {
+ JXL_WARNING("Storing color image to PGX - use .ppm extension instead.\n");
+ }
+ if (bits_per_sample > 16 && codec == extras::Codec::kPNG) {
+ JXL_WARNING("PNG only supports up to 16 bits per sample");
+ bits_per_sample = 16;
+ }
+
+ std::vector<uint8_t> encoded;
+ return Encode(io, codec, c_desired, bits_per_sample, &encoded, pool) &&
+ WriteFile(encoded, pathname);
+}
+
+Status EncodeToFile(const CodecInOut& io, const std::string& pathname,
+ ThreadPool* pool) {
+ // TODO(lode): need to take the floating_point_sample field into account
+ return EncodeToFile(io, io.metadata.m.color_encoding,
+ io.metadata.m.bit_depth.bits_per_sample, pathname, pool);
+}
+
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/codec.h b/third_party/jpeg-xl/lib/extras/codec.h
new file mode 100644
index 0000000000..80a42f926c
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/codec.h
@@ -0,0 +1,70 @@
+// Copyright (c) the JPEG XL Project 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_H_
+#define LIB_EXTRAS_CODEC_H_
+
+// Facade for image encoders/decoders (PNG, PNM, ...).
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/dec/decode.h"
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/field_encodings.h" // MakeBit
+
+namespace jxl {
+
+struct SizeConstraints;
+
+// Decodes "bytes" and sets io->metadata.m.
+// color_space_hint may specify the color space, otherwise, defaults to sRGB.
+Status SetFromBytes(Span<const uint8_t> bytes,
+ const extras::ColorHints& color_hints, CodecInOut* io,
+ ThreadPool* pool = nullptr,
+ const SizeConstraints* constraints = nullptr,
+ extras::Codec* orig_codec = nullptr);
+// Helper function to use no color_space_hint.
+JXL_INLINE Status SetFromBytes(const Span<const uint8_t> bytes, CodecInOut* io,
+ ThreadPool* pool = nullptr,
+ const SizeConstraints* constraints = nullptr,
+ extras::Codec* orig_codec = nullptr) {
+ return SetFromBytes(bytes, extras::ColorHints(), io, pool, constraints,
+ orig_codec);
+}
+
+// Reads from file and calls SetFromBytes.
+Status SetFromFile(const std::string& pathname,
+ const extras::ColorHints& color_hints, CodecInOut* io,
+ ThreadPool* pool = nullptr,
+ const SizeConstraints* constraints = nullptr,
+ extras::Codec* orig_codec = nullptr);
+
+// Replaces "bytes" with an encoding of pixels transformed from c_current
+// color space to c_desired.
+Status Encode(const CodecInOut& io, extras::Codec codec,
+ const ColorEncoding& c_desired, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes, ThreadPool* pool = nullptr);
+
+// Deduces codec, calls Encode and writes to file.
+Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired,
+ size_t bits_per_sample, const std::string& pathname,
+ ThreadPool* pool = nullptr);
+// Same, but defaults to metadata.original color_encoding and bits_per_sample.
+Status EncodeToFile(const CodecInOut& io, const std::string& pathname,
+ ThreadPool* pool = nullptr);
+
+} // namespace jxl
+
+#endif // LIB_EXTRAS_CODEC_H_
diff --git a/third_party/jpeg-xl/lib/extras/codec_test.cc b/third_party/jpeg-xl/lib/extras/codec_test.cc
new file mode 100644
index 0000000000..66a8563639
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/codec_test.cc
@@ -0,0 +1,645 @@
+// Copyright (c) the JPEG XL Project 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.h"
+
+#include <stddef.h>
+#include <stdio.h>
+
+#include <algorithm>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "lib/extras/dec/decode.h"
+#include "lib/extras/dec/pgx.h"
+#include "lib/extras/dec/pnm.h"
+#include "lib/extras/enc/encode.h"
+#include "lib/extras/packed_image_convert.h"
+#include "lib/jxl/base/random.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/enc_butteraugli_comparator.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_test_utils.h"
+#include "lib/jxl/test_utils.h"
+#include "lib/jxl/testing.h"
+
+namespace jxl {
+
+using test::ThreadPoolForTests;
+
+namespace extras {
+namespace {
+
+using ::testing::AllOf;
+using ::testing::Contains;
+using ::testing::Field;
+using ::testing::IsEmpty;
+using ::testing::NotNull;
+using ::testing::SizeIs;
+
+std::string ExtensionFromCodec(Codec codec, const bool is_gray,
+ const bool has_alpha,
+ const size_t bits_per_sample) {
+ switch (codec) {
+ case Codec::kJPG:
+ return ".jpg";
+ case Codec::kPGX:
+ return ".pgx";
+ case Codec::kPNG:
+ return ".png";
+ case Codec::kPNM:
+ if (bits_per_sample == 32) return ".pfm";
+ if (has_alpha) return ".pam";
+ return is_gray ? ".pgm" : ".ppm";
+ case Codec::kGIF:
+ return ".gif";
+ case Codec::kEXR:
+ return ".exr";
+ case Codec::kUnknown:
+ return std::string();
+ }
+ JXL_UNREACHABLE;
+ return std::string();
+}
+
+void VerifySameImage(const PackedImage& im0, size_t bits_per_sample0,
+ const PackedImage& im1, size_t bits_per_sample1,
+ bool lossless = true) {
+ ASSERT_EQ(im0.xsize, im1.xsize);
+ ASSERT_EQ(im0.ysize, im1.ysize);
+ ASSERT_EQ(im0.format.num_channels, im1.format.num_channels);
+ auto get_factor = [](JxlPixelFormat f, size_t bits) -> double {
+ return 1.0 / ((1u << std::min(test::GetPrecision(f.data_type), bits)) - 1);
+ };
+ double factor0 = get_factor(im0.format, bits_per_sample0);
+ double factor1 = get_factor(im1.format, bits_per_sample1);
+ auto pixels0 = static_cast<const uint8_t*>(im0.pixels());
+ auto pixels1 = static_cast<const uint8_t*>(im1.pixels());
+ auto rgba0 =
+ test::ConvertToRGBA32(pixels0, im0.xsize, im0.ysize, im0.format, factor0);
+ auto rgba1 =
+ test::ConvertToRGBA32(pixels1, im1.xsize, im1.ysize, im1.format, factor1);
+ double tolerance =
+ lossless ? 0.5 * std::min(factor0, factor1) : 3.0f / 255.0f;
+ if (bits_per_sample0 == 32 || bits_per_sample1 == 32) {
+ tolerance = 0.5 * std::max(factor0, factor1);
+ }
+ for (size_t y = 0; y < im0.ysize; ++y) {
+ for (size_t x = 0; x < im0.xsize; ++x) {
+ for (size_t c = 0; c < im0.format.num_channels; ++c) {
+ size_t ix = (y * im0.xsize + x) * 4 + c;
+ double val0 = rgba0[ix];
+ double val1 = rgba1[ix];
+ ASSERT_NEAR(val1, val0, tolerance)
+ << "y = " << y << " x = " << x << " c = " << c;
+ }
+ }
+ }
+}
+
+JxlColorEncoding CreateTestColorEncoding(bool is_gray) {
+ JxlColorEncoding c;
+ c.color_space = is_gray ? JXL_COLOR_SPACE_GRAY : JXL_COLOR_SPACE_RGB;
+ c.white_point = JXL_WHITE_POINT_D65;
+ c.primaries = JXL_PRIMARIES_P3;
+ c.rendering_intent = JXL_RENDERING_INTENT_RELATIVE;
+ c.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR;
+ // Roundtrip through internal color encoding to fill in primaries and white
+ // point CIE xy coordinates.
+ ColorEncoding c_internal;
+ JXL_CHECK(ConvertExternalToInternalColorEncoding(c, &c_internal));
+ ConvertInternalToExternalColorEncoding(c_internal, &c);
+ return c;
+}
+
+std::vector<uint8_t> GenerateICC(JxlColorEncoding color_encoding) {
+ ColorEncoding c;
+ JXL_CHECK(ConvertExternalToInternalColorEncoding(color_encoding, &c));
+ JXL_CHECK(c.CreateICC());
+ PaddedBytes icc = c.ICC();
+ return std::vector<uint8_t>(icc.begin(), icc.end());
+}
+
+void StoreRandomValue(uint8_t* out, Rng* rng, JxlPixelFormat format,
+ size_t bits_per_sample) {
+ uint64_t max_val = (1ull << bits_per_sample) - 1;
+ if (format.data_type == JXL_TYPE_UINT8) {
+ *out = rng->UniformU(0, max_val);
+ } else if (format.data_type == JXL_TYPE_UINT16) {
+ uint32_t val = rng->UniformU(0, max_val);
+ if (format.endianness == JXL_BIG_ENDIAN) {
+ StoreBE16(val, out);
+ } else {
+ StoreLE16(val, out);
+ }
+ } else {
+ ASSERT_EQ(format.data_type, JXL_TYPE_FLOAT);
+ float val = rng->UniformF(0.0, 1.0);
+ uint32_t uval;
+ memcpy(&uval, &val, 4);
+ if (format.endianness == JXL_BIG_ENDIAN) {
+ StoreBE32(uval, out);
+ } else {
+ StoreLE32(uval, out);
+ }
+ }
+}
+
+void FillPackedImage(size_t bits_per_sample, PackedImage* image) {
+ JxlPixelFormat format = image->format;
+ size_t bytes_per_channel = PackedImage::BitsPerChannel(format.data_type) / 8;
+ uint8_t* out = static_cast<uint8_t*>(image->pixels());
+ size_t stride = image->xsize * format.num_channels * bytes_per_channel;
+ ASSERT_EQ(image->pixels_size, image->ysize * stride);
+ Rng rng(129);
+ for (size_t y = 0; y < image->ysize; ++y) {
+ for (size_t x = 0; x < image->xsize; ++x) {
+ for (size_t c = 0; c < format.num_channels; ++c) {
+ StoreRandomValue(out, &rng, format, bits_per_sample);
+ out += bytes_per_channel;
+ }
+ }
+ }
+}
+
+struct TestImageParams {
+ Codec codec;
+ size_t xsize;
+ size_t ysize;
+ size_t bits_per_sample;
+ bool is_gray;
+ bool add_alpha;
+ bool big_endian;
+ bool add_extra_channels;
+
+ bool ShouldTestRoundtrip() const {
+ if (codec == Codec::kPNG) {
+ return bits_per_sample <= 16;
+ } else if (codec == Codec::kPNM) {
+ // TODO(szabadka) Make PNM encoder endianness-aware.
+ return ((bits_per_sample <= 16 && big_endian) ||
+ (bits_per_sample == 32 && !add_alpha && !big_endian));
+ } else if (codec == Codec::kPGX) {
+ return ((bits_per_sample == 8 || bits_per_sample == 16) && is_gray &&
+ !add_alpha);
+ } else if (codec == Codec::kEXR) {
+#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \
+ defined(THREAD_SANITIZER)
+ // OpenEXR 2.3 has a memory leak in IlmThread_2_3::ThreadPool
+ return false;
+#else
+ return bits_per_sample == 32 && !is_gray;
+#endif
+ } else if (codec == Codec::kJPG) {
+ return bits_per_sample == 8 && !add_alpha;
+ } else {
+ return false;
+ }
+ }
+
+ JxlPixelFormat PixelFormat() const {
+ JxlPixelFormat format;
+ format.num_channels = (is_gray ? 1 : 3) + (add_alpha ? 1 : 0);
+ format.data_type = (bits_per_sample == 32 ? JXL_TYPE_FLOAT
+ : bits_per_sample > 8 ? JXL_TYPE_UINT16
+ : JXL_TYPE_UINT8);
+ format.endianness = big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN;
+ format.align = 0;
+ return format;
+ }
+
+ std::string DebugString() const {
+ std::ostringstream os;
+ os << "bps:" << bits_per_sample << " gr:" << is_gray << " al:" << add_alpha
+ << " be: " << big_endian << " ec: " << add_extra_channels;
+ return os.str();
+ }
+};
+
+void CreateTestImage(const TestImageParams& params, PackedPixelFile* ppf) {
+ ppf->info.xsize = params.xsize;
+ ppf->info.ysize = params.ysize;
+ ppf->info.bits_per_sample = params.bits_per_sample;
+ ppf->info.exponent_bits_per_sample = params.bits_per_sample == 32 ? 8 : 0;
+ ppf->info.num_color_channels = params.is_gray ? 1 : 3;
+ ppf->info.alpha_bits = params.add_alpha ? params.bits_per_sample : 0;
+ ppf->info.alpha_premultiplied = (params.codec == Codec::kEXR);
+
+ JxlColorEncoding color_encoding = CreateTestColorEncoding(params.is_gray);
+ ppf->icc = GenerateICC(color_encoding);
+ ppf->color_encoding = color_encoding;
+
+ PackedFrame frame(params.xsize, params.ysize, params.PixelFormat());
+ FillPackedImage(params.bits_per_sample, &frame.color);
+ if (params.add_extra_channels) {
+ for (size_t i = 0; i < 7; ++i) {
+ JxlPixelFormat ec_format = params.PixelFormat();
+ ec_format.num_channels = 1;
+ PackedImage ec(params.xsize, params.ysize, ec_format);
+ FillPackedImage(params.bits_per_sample, &ec);
+ frame.extra_channels.emplace_back(std::move(ec));
+ PackedExtraChannel pec;
+ pec.ec_info.bits_per_sample = params.bits_per_sample;
+ pec.ec_info.type = static_cast<JxlExtraChannelType>(i);
+ ppf->extra_channels_info.emplace_back(std::move(pec));
+ }
+ }
+ ppf->frames.emplace_back(std::move(frame));
+}
+
+// Ensures reading a newly written file leads to the same image pixels.
+void TestRoundTrip(const TestImageParams& params, ThreadPool* pool) {
+ if (!params.ShouldTestRoundtrip()) return;
+
+ std::string extension = ExtensionFromCodec(
+ params.codec, params.is_gray, params.add_alpha, params.bits_per_sample);
+ printf("Codec %s %s\n", extension.c_str(), params.DebugString().c_str());
+
+ PackedPixelFile ppf_in;
+ CreateTestImage(params, &ppf_in);
+
+ EncodedImage encoded;
+ auto encoder = Encoder::FromExtension(extension);
+ ASSERT_TRUE(encoder.get());
+ ASSERT_TRUE(encoder->Encode(ppf_in, &encoded, pool));
+ ASSERT_EQ(encoded.bitstreams.size(), 1);
+
+ PackedPixelFile ppf_out;
+ ColorHints color_hints;
+ if (params.codec == Codec::kPNM || params.codec == Codec::kPGX) {
+ color_hints.Add("color_space",
+ params.is_gray ? "Gra_D65_Rel_SRG" : "RGB_D65_SRG_Rel_SRG");
+ }
+ ASSERT_TRUE(DecodeBytes(Span<const uint8_t>(encoded.bitstreams[0]),
+ color_hints, &ppf_out));
+ if (params.codec == Codec::kPNG && ppf_out.icc.empty()) {
+ // Decoding a PNG may drop the ICC profile if there's a valid cICP chunk.
+ // Rendering intent is not preserved in this case.
+ EXPECT_EQ(ppf_in.color_encoding.color_space,
+ ppf_out.color_encoding.color_space);
+ EXPECT_EQ(ppf_in.color_encoding.white_point,
+ ppf_out.color_encoding.white_point);
+ if (ppf_in.color_encoding.color_space != JXL_COLOR_SPACE_GRAY) {
+ EXPECT_EQ(ppf_in.color_encoding.primaries,
+ ppf_out.color_encoding.primaries);
+ }
+ EXPECT_EQ(ppf_in.color_encoding.transfer_function,
+ ppf_out.color_encoding.transfer_function);
+ EXPECT_EQ(ppf_out.color_encoding.rendering_intent,
+ JXL_RENDERING_INTENT_RELATIVE);
+ } else if (params.codec != Codec::kPNM && params.codec != Codec::kPGX &&
+ params.codec != Codec::kEXR) {
+ EXPECT_EQ(ppf_in.icc, ppf_out.icc);
+ }
+
+ ASSERT_EQ(ppf_out.frames.size(), 1);
+ const auto& frame_in = ppf_in.frames[0];
+ const auto& frame_out = ppf_out.frames[0];
+ VerifySameImage(frame_in.color, ppf_in.info.bits_per_sample, frame_out.color,
+ ppf_out.info.bits_per_sample,
+ /*lossless=*/params.codec != Codec::kJPG);
+ ASSERT_EQ(frame_in.extra_channels.size(), frame_out.extra_channels.size());
+ ASSERT_EQ(ppf_out.extra_channels_info.size(),
+ frame_out.extra_channels.size());
+ for (size_t i = 0; i < frame_in.extra_channels.size(); ++i) {
+ VerifySameImage(frame_in.extra_channels[i], ppf_in.info.bits_per_sample,
+ frame_out.extra_channels[i], ppf_out.info.bits_per_sample,
+ /*lossless=*/true);
+ EXPECT_EQ(ppf_out.extra_channels_info[i].ec_info.type,
+ ppf_in.extra_channels_info[i].ec_info.type);
+ }
+}
+
+TEST(CodecTest, TestRoundTrip) {
+ ThreadPoolForTests pool(12);
+
+ TestImageParams params;
+ params.xsize = 7;
+ params.ysize = 4;
+
+ for (Codec codec : AvailableCodecs()) {
+ for (int bits_per_sample : {4, 8, 10, 12, 16, 32}) {
+ for (bool is_gray : {false, true}) {
+ for (bool add_alpha : {false, true}) {
+ for (bool big_endian : {false, true}) {
+ params.codec = codec;
+ params.bits_per_sample = static_cast<size_t>(bits_per_sample);
+ params.is_gray = is_gray;
+ params.add_alpha = add_alpha;
+ params.big_endian = big_endian;
+ params.add_extra_channels = false;
+ TestRoundTrip(params, &pool);
+ if (codec == Codec::kPNM && add_alpha) {
+ params.add_extra_channels = true;
+ TestRoundTrip(params, &pool);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+TEST(CodecTest, LosslessPNMRoundtrip) {
+ ThreadPoolForTests pool(12);
+
+ static const char* kChannels[] = {"", "g", "ga", "rgb", "rgba"};
+ static const char* kExtension[] = {"", ".pgm", ".pam", ".ppm", ".pam"};
+ for (size_t bit_depth = 1; bit_depth <= 16; ++bit_depth) {
+ for (size_t channels = 1; channels <= 4; ++channels) {
+ if (bit_depth == 1 && (channels == 2 || channels == 4)) continue;
+ std::string extension(kExtension[channels]);
+ std::string filename = "jxl/flower/flower_small." +
+ std::string(kChannels[channels]) + ".depth" +
+ std::to_string(bit_depth) + extension;
+ const PaddedBytes orig = jxl::test::ReadTestData(filename);
+
+ PackedPixelFile ppf;
+ ColorHints color_hints;
+ color_hints.Add("color_space",
+ channels < 3 ? "Gra_D65_Rel_SRG" : "RGB_D65_SRG_Rel_SRG");
+ ASSERT_TRUE(DecodeBytes(Span<const uint8_t>(orig.data(), orig.size()),
+ color_hints, &ppf));
+
+ EncodedImage encoded;
+ auto encoder = Encoder::FromExtension(extension);
+ ASSERT_TRUE(encoder.get());
+ ASSERT_TRUE(encoder->Encode(ppf, &encoded, &pool));
+ ASSERT_EQ(encoded.bitstreams.size(), 1);
+ ASSERT_EQ(orig.size(), encoded.bitstreams[0].size());
+ EXPECT_EQ(0,
+ memcmp(orig.data(), encoded.bitstreams[0].data(), orig.size()));
+ }
+ }
+}
+
+void DecodeRoundtrip(const std::string& pathname, ThreadPool* pool,
+ CodecInOut& io,
+ const ColorHints& color_hints = ColorHints()) {
+ const PaddedBytes orig = jxl::test::ReadTestData(pathname);
+ JXL_CHECK(SetFromBytes(Span<const uint8_t>(orig), color_hints, &io, pool));
+ const ImageBundle& ib1 = io.Main();
+
+ // Encode/Decode again to make sure Encode carries through all metadata.
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(Encode(io, Codec::kPNG, io.metadata.m.color_encoding,
+ io.metadata.m.bit_depth.bits_per_sample, &encoded, pool));
+
+ CodecInOut io2;
+ JXL_CHECK(
+ SetFromBytes(Span<const uint8_t>(encoded), color_hints, &io2, pool));
+ const ImageBundle& ib2 = io2.Main();
+ EXPECT_EQ(Description(ib1.metadata()->color_encoding),
+ Description(ib2.metadata()->color_encoding));
+ EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current()));
+
+ size_t bits_per_sample = io2.metadata.m.bit_depth.bits_per_sample;
+
+ // "Same" pixels?
+ double max_l1 = bits_per_sample <= 12 ? 1.3 : 2E-3;
+ double max_rel = bits_per_sample <= 12 ? 6E-3 : 1E-4;
+ if (ib1.metadata()->color_encoding.IsGray()) {
+ max_rel *= 2.0;
+ } else if (ib1.metadata()->color_encoding.primaries != Primaries::kSRGB) {
+ // Need more tolerance for large gamuts (anything but sRGB)
+ max_l1 *= 1.5;
+ max_rel *= 3.0;
+ }
+ JXL_ASSERT_OK(
+ VerifyRelativeError(ib1.color(), ib2.color(), max_l1, max_rel, _));
+
+ // Simulate the encoder removing profile and decoder restoring it.
+ if (!ib2.metadata()->color_encoding.WantICC()) {
+ io2.metadata.m.color_encoding.InternalRemoveICC();
+ EXPECT_TRUE(io2.metadata.m.color_encoding.CreateICC());
+ }
+}
+
+#if 0
+TEST(CodecTest, TestMetadataSRGB) {
+ ThreadPoolForTests pool(12);
+
+ const char* paths[] = {"external/raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png",
+ "external/raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png",
+ "external/raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png",
+ "external/raw.pixls/Nikon-D300-12bit_srgb8_dt.png",
+ "external/raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"};
+ for (const char* relative_pathname : paths) {
+ CodecInOut io;
+ DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool, io);
+ EXPECT_EQ(8, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample);
+
+ EXPECT_EQ(64, io.xsize());
+ EXPECT_EQ(64, io.ysize());
+ EXPECT_FALSE(io.metadata.m.HasAlpha());
+
+ const ColorEncoding& c_original = io.metadata.m.color_encoding;
+ EXPECT_FALSE(c_original.ICC().empty());
+ EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
+ EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
+ EXPECT_EQ(Primaries::kSRGB, c_original.primaries);
+ EXPECT_TRUE(c_original.tf.IsSRGB());
+ }
+}
+
+TEST(CodecTest, TestMetadataLinear) {
+ ThreadPoolForTests pool(12);
+
+ const char* paths[3] = {
+ "external/raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png",
+ "external/raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png",
+ "external/raw.pixls/Nikon-D300-12bit_2020_g1_dt.png",
+ };
+ const WhitePoint white_points[3] = {WhitePoint::kCustom, WhitePoint::kD65,
+ WhitePoint::kD65};
+ const Primaries primaries[3] = {Primaries::kCustom, Primaries::kSRGB,
+ Primaries::k2100};
+
+ for (size_t i = 0; i < 3; ++i) {
+ CodecInOut io;
+ DecodeRoundtrip(paths[i], Codec::kPNG, &pool, io);
+ EXPECT_EQ(16, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample);
+
+ EXPECT_EQ(64, io.xsize());
+ EXPECT_EQ(64, io.ysize());
+ EXPECT_FALSE(io.metadata.m.HasAlpha());
+
+ const ColorEncoding& c_original = io.metadata.m.color_encoding;
+ EXPECT_FALSE(c_original.ICC().empty());
+ EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
+ EXPECT_EQ(white_points[i], c_original.white_point);
+ EXPECT_EQ(primaries[i], c_original.primaries);
+ EXPECT_TRUE(c_original.tf.IsLinear());
+ }
+}
+
+TEST(CodecTest, TestMetadataICC) {
+ ThreadPoolForTests pool(12);
+
+ const char* paths[] = {
+ "external/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png",
+ "external/raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png",
+ };
+ for (const char* relative_pathname : paths) {
+ CodecInOut io;
+ DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool, io);
+ EXPECT_GE(16, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_LE(14, io.metadata.m.bit_depth.bits_per_sample);
+
+ EXPECT_EQ(64, io.xsize());
+ EXPECT_EQ(64, io.ysize());
+ EXPECT_FALSE(io.metadata.m.HasAlpha());
+
+ const ColorEncoding& c_original = io.metadata.m.color_encoding;
+ EXPECT_FALSE(c_original.ICC().empty());
+ EXPECT_EQ(RenderingIntent::kPerceptual, c_original.rendering_intent);
+ EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
+ EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
+ EXPECT_EQ(Primaries::kSRGB, c_original.primaries);
+ EXPECT_EQ(TransferFunction::k709, c_original.tf.GetTransferFunction());
+ }
+}
+
+TEST(CodecTest, Testexternal/pngsuite) {
+ ThreadPoolForTests pool(12);
+
+ // Ensure we can load PNG with text, japanese UTF-8, compressed text.
+ CodecInOut tmp1;
+ DecodeRoundtrip("external/pngsuite/ct1n0g04.png", Codec::kPNG, &pool, tmp1);
+ CodecInOut tmp2;
+ DecodeRoundtrip("external/pngsuite/ctjn0g04.png", Codec::kPNG, &pool, tmp2);
+ CodecInOut tmp3;
+ DecodeRoundtrip("external/pngsuite/ctzn0g04.png", Codec::kPNG, &pool, tmp3);
+
+ // Extract gAMA
+ CodecInOut b1;
+ DecodeRoundtrip("external/pngsuite/g10n3p04.png", Codec::kPNG, &pool, b1);
+ EXPECT_TRUE(b1.metadata.color_encoding.tf.IsLinear());
+
+ // Extract cHRM
+ CodecInOut b_p;
+ DecodeRoundtrip("external/pngsuite/ccwn2c08.png", Codec::kPNG, &pool, b_p);
+ EXPECT_EQ(Primaries::kSRGB, b_p.metadata.color_encoding.primaries);
+ EXPECT_EQ(WhitePoint::kD65, b_p.metadata.color_encoding.white_point);
+
+ // Extract EXIF from (new-style) dedicated chunk
+ CodecInOut b_exif;
+ DecodeRoundtrip("external/pngsuite/exif2c08.png", Codec::kPNG, &pool, b_exif);
+ EXPECT_EQ(978, b_exif.blobs.exif.size());
+}
+#endif
+
+void VerifyWideGamutMetadata(const std::string& relative_pathname,
+ const Primaries primaries, ThreadPool* pool) {
+ CodecInOut io;
+ DecodeRoundtrip(relative_pathname, pool, io);
+
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+
+ const ColorEncoding& c_original = io.metadata.m.color_encoding;
+ EXPECT_FALSE(c_original.ICC().empty());
+ EXPECT_EQ(RenderingIntent::kAbsolute, c_original.rendering_intent);
+ EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
+ EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
+ EXPECT_EQ(primaries, c_original.primaries);
+}
+
+TEST(CodecTest, TestWideGamut) {
+ ThreadPoolForTests pool(12);
+ // VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-bars.png",
+ // Primaries::kP3, &pool);
+ VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-ring.png",
+ Primaries::kP3, &pool);
+ // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-bars.png",
+ // Primaries::k2100, &pool);
+ // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-ring.png",
+ // Primaries::k2100, &pool);
+}
+
+TEST(CodecTest, TestPNM) { TestCodecPNM(); }
+
+TEST(CodecTest, FormatNegotiation) {
+ const std::vector<JxlPixelFormat> accepted_formats = {
+ {/*num_channels=*/4,
+ /*data_type=*/JXL_TYPE_UINT16,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0},
+ {/*num_channels=*/3,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0},
+ {/*num_channels=*/3,
+ /*data_type=*/JXL_TYPE_UINT16,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0},
+ {/*num_channels=*/1,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0},
+ };
+
+ JxlBasicInfo info;
+ JxlEncoderInitBasicInfo(&info);
+ info.bits_per_sample = 12;
+ info.num_color_channels = 2;
+
+ JxlPixelFormat format;
+ EXPECT_FALSE(SelectFormat(accepted_formats, info, &format));
+
+ info.num_color_channels = 3;
+ ASSERT_TRUE(SelectFormat(accepted_formats, info, &format));
+ EXPECT_EQ(format.num_channels, info.num_color_channels);
+ // 16 is the smallest accepted format that can accommodate the 12-bit data.
+ EXPECT_EQ(format.data_type, JXL_TYPE_UINT16);
+}
+
+TEST(CodecTest, EncodeToPNG) {
+ ThreadPool* const pool = nullptr;
+
+ std::unique_ptr<Encoder> png_encoder = Encoder::FromExtension(".png");
+ ASSERT_THAT(png_encoder, NotNull());
+
+ const PaddedBytes original_png = jxl::test::ReadTestData(
+ "external/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
+ PackedPixelFile ppf;
+ ASSERT_TRUE(extras::DecodeBytes(Span<const uint8_t>(original_png),
+ ColorHints(), &ppf));
+
+ const JxlPixelFormat& format = ppf.frames.front().color.format;
+ ASSERT_THAT(
+ png_encoder->AcceptedFormats(),
+ Contains(AllOf(Field(&JxlPixelFormat::num_channels, format.num_channels),
+ Field(&JxlPixelFormat::data_type, format.data_type),
+ Field(&JxlPixelFormat::endianness, format.endianness))));
+ EncodedImage encoded_png;
+ ASSERT_TRUE(png_encoder->Encode(ppf, &encoded_png, pool));
+ EXPECT_THAT(encoded_png.icc, IsEmpty());
+ ASSERT_THAT(encoded_png.bitstreams, SizeIs(1));
+
+ PackedPixelFile decoded_ppf;
+ ASSERT_TRUE(
+ extras::DecodeBytes(Span<const uint8_t>(encoded_png.bitstreams.front()),
+ ColorHints(), &decoded_ppf));
+
+ ASSERT_EQ(decoded_ppf.info.bits_per_sample, ppf.info.bits_per_sample);
+ ASSERT_EQ(decoded_ppf.frames.size(), 1);
+ VerifySameImage(ppf.frames[0].color, ppf.info.bits_per_sample,
+ decoded_ppf.frames[0].color,
+ decoded_ppf.info.bits_per_sample);
+}
+
+} // namespace
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/apng.cc b/third_party/jpeg-xl/lib/extras/dec/apng.cc
new file mode 100644
index 0000000000..c16fb5c81f
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/apng.cc
@@ -0,0 +1,962 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/apng.h"
+
+// Parts of this code are taken from apngdis, which has the following license:
+/* APNG Disassembler 2.8
+ *
+ * Deconstructs APNG files into individual frames.
+ *
+ * http://apngdis.sourceforge.net
+ *
+ * Copyright (c) 2010-2015 Max Stepin
+ * maxst at users.sourceforge.net
+ *
+ * zlib license
+ * ------------
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty. In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ * claim that you wrote the original software. If you use this software
+ * in a product, an acknowledgment in the product documentation would be
+ * appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ * misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ *
+ */
+
+#include <jxl/codestream_header.h>
+#include <jxl/encode.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "lib/extras/size_constraints.h"
+#include "lib/jxl/base/byte_order.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"
+#include "lib/jxl/sanitizers.h"
+#include "png.h" /* original (unpatched) libpng is ok */
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
+ 0x66, 0x00, 0x00};
+
+/* hIST chunk tail is not proccesed properly; skip this chunk completely;
+ see https://github.com/glennrp/libpng/pull/413 */
+const png_byte kIgnoredPngChunks[] = {
+ 104, 73, 83, 84, '\0' /* hIST */
+};
+
+// Returns floating-point value from the PNG encoding (times 10^5).
+static double F64FromU32(const uint32_t x) {
+ return static_cast<int32_t>(x) * 1E-5;
+}
+
+Status DecodeSRGB(const unsigned char* payload, const size_t payload_size,
+ JxlColorEncoding* color_encoding) {
+ if (payload_size != 1) return JXL_FAILURE("Wrong sRGB size");
+ // (PNG uses the same values as ICC.)
+ if (payload[0] >= 4) return JXL_FAILURE("Invalid Rendering Intent");
+ color_encoding->white_point = JXL_WHITE_POINT_D65;
+ color_encoding->primaries = JXL_PRIMARIES_SRGB;
+ color_encoding->transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
+ color_encoding->rendering_intent =
+ static_cast<JxlRenderingIntent>(payload[0]);
+ return true;
+}
+
+// If the cICP profile is not fully supported, return false and leave
+// color_encoding unmodified.
+Status DecodeCICP(const unsigned char* payload, const size_t payload_size,
+ JxlColorEncoding* color_encoding) {
+ if (payload_size != 4) return JXL_FAILURE("Wrong cICP size");
+ JxlColorEncoding color_enc = *color_encoding;
+
+ // From https://www.itu.int/rec/T-REC-H.273-202107-I/en
+ if (payload[0] == 1) {
+ // IEC 61966-2-1 sRGB
+ color_enc.primaries = JXL_PRIMARIES_SRGB;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 4) {
+ // Rec. ITU-R BT.470-6 System M
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.67;
+ color_enc.primaries_red_xy[1] = 0.33;
+ color_enc.primaries_green_xy[0] = 0.21;
+ color_enc.primaries_green_xy[1] = 0.71;
+ color_enc.primaries_blue_xy[0] = 0.14;
+ color_enc.primaries_blue_xy[1] = 0.08;
+ color_enc.white_point = JXL_WHITE_POINT_CUSTOM;
+ color_enc.white_point_xy[0] = 0.310;
+ color_enc.white_point_xy[1] = 0.316;
+ } else if (payload[0] == 5) {
+ // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.64;
+ color_enc.primaries_red_xy[1] = 0.33;
+ color_enc.primaries_green_xy[0] = 0.29;
+ color_enc.primaries_green_xy[1] = 0.60;
+ color_enc.primaries_blue_xy[0] = 0.15;
+ color_enc.primaries_blue_xy[1] = 0.06;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 6 || payload[0] == 7) {
+ // SMPTE ST 170 (2004) / SMPTE ST 240 (1999)
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.630;
+ color_enc.primaries_red_xy[1] = 0.340;
+ color_enc.primaries_green_xy[0] = 0.310;
+ color_enc.primaries_green_xy[1] = 0.595;
+ color_enc.primaries_blue_xy[0] = 0.155;
+ color_enc.primaries_blue_xy[1] = 0.070;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 8) {
+ // Generic film (colour filters using Illuminant C)
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.681;
+ color_enc.primaries_red_xy[1] = 0.319;
+ color_enc.primaries_green_xy[0] = 0.243;
+ color_enc.primaries_green_xy[1] = 0.692;
+ color_enc.primaries_blue_xy[0] = 0.145;
+ color_enc.primaries_blue_xy[1] = 0.049;
+ color_enc.white_point = JXL_WHITE_POINT_CUSTOM;
+ color_enc.white_point_xy[0] = 0.310;
+ color_enc.white_point_xy[1] = 0.316;
+ } else if (payload[0] == 9) {
+ // Rec. ITU-R BT.2100-2
+ color_enc.primaries = JXL_PRIMARIES_2100;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 10) {
+ // CIE 1931 XYZ
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 1;
+ color_enc.primaries_red_xy[1] = 0;
+ color_enc.primaries_green_xy[0] = 0;
+ color_enc.primaries_green_xy[1] = 1;
+ color_enc.primaries_blue_xy[0] = 0;
+ color_enc.primaries_blue_xy[1] = 0;
+ color_enc.white_point = JXL_WHITE_POINT_E;
+ } else if (payload[0] == 11) {
+ // SMPTE RP 431-2 (2011)
+ color_enc.primaries = JXL_PRIMARIES_P3;
+ color_enc.white_point = JXL_WHITE_POINT_DCI;
+ } else if (payload[0] == 12) {
+ // SMPTE EG 432-1 (2010)
+ color_enc.primaries = JXL_PRIMARIES_P3;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 22) {
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.630;
+ color_enc.primaries_red_xy[1] = 0.340;
+ color_enc.primaries_green_xy[0] = 0.295;
+ color_enc.primaries_green_xy[1] = 0.605;
+ color_enc.primaries_blue_xy[0] = 0.155;
+ color_enc.primaries_blue_xy[1] = 0.077;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else {
+ JXL_WARNING("Unsupported primaries specified in cICP chunk: %d",
+ static_cast<int>(payload[0]));
+ return false;
+ }
+
+ if (payload[1] == 1 || payload[1] == 6 || payload[1] == 14 ||
+ payload[1] == 15) {
+ // Rec. ITU-R BT.709-6
+ color_enc.transfer_function = JXL_TRANSFER_FUNCTION_709;
+ } else if (payload[1] == 4) {
+ // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
+ color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
+ color_enc.gamma = 1 / 2.2;
+ } else if (payload[1] == 5) {
+ // Rec. ITU-R BT.470-6 System B, G
+ color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
+ color_enc.gamma = 1 / 2.8;
+ } else if (payload[1] == 8 || payload[1] == 13 || payload[1] == 16 ||
+ payload[1] == 17 || payload[1] == 18) {
+ // These codes all match the corresponding JXL enum values
+ color_enc.transfer_function = static_cast<JxlTransferFunction>(payload[1]);
+ } else {
+ JXL_WARNING("Unsupported transfer function specified in cICP chunk: %d",
+ static_cast<int>(payload[1]));
+ return false;
+ }
+
+ if (payload[2] != 0) {
+ JXL_WARNING("Unsupported color space specified in cICP chunk: %d",
+ static_cast<int>(payload[2]));
+ return false;
+ }
+ if (payload[3] != 1) {
+ JXL_WARNING("Unsupported full-range flag specified in cICP chunk: %d",
+ static_cast<int>(payload[3]));
+ return false;
+ }
+ // cICP has no rendering intent, so use the default
+ color_enc.rendering_intent = JXL_RENDERING_INTENT_RELATIVE;
+ *color_encoding = color_enc;
+ return true;
+}
+
+Status DecodeGAMA(const unsigned char* payload, const size_t payload_size,
+ JxlColorEncoding* color_encoding) {
+ if (payload_size != 4) return JXL_FAILURE("Wrong gAMA size");
+ color_encoding->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
+ color_encoding->gamma = F64FromU32(LoadBE32(payload));
+ return true;
+}
+
+Status DecodeCHRM(const unsigned char* payload, const size_t payload_size,
+ JxlColorEncoding* color_encoding) {
+ if (payload_size != 32) return JXL_FAILURE("Wrong cHRM size");
+
+ color_encoding->white_point = JXL_WHITE_POINT_CUSTOM;
+ color_encoding->white_point_xy[0] = F64FromU32(LoadBE32(payload + 0));
+ color_encoding->white_point_xy[1] = F64FromU32(LoadBE32(payload + 4));
+
+ color_encoding->primaries = JXL_PRIMARIES_CUSTOM;
+ color_encoding->primaries_red_xy[0] = F64FromU32(LoadBE32(payload + 8));
+ color_encoding->primaries_red_xy[1] = F64FromU32(LoadBE32(payload + 12));
+ color_encoding->primaries_green_xy[0] = F64FromU32(LoadBE32(payload + 16));
+ color_encoding->primaries_green_xy[1] = F64FromU32(LoadBE32(payload + 20));
+ color_encoding->primaries_blue_xy[0] = F64FromU32(LoadBE32(payload + 24));
+ color_encoding->primaries_blue_xy[1] = F64FromU32(LoadBE32(payload + 28));
+ return true;
+}
+
+// Retrieves XMP and EXIF/IPTC from itext and text.
+class BlobsReaderPNG {
+ public:
+ static Status Decode(const png_text_struct& info, PackedMetadata* metadata) {
+ // We trust these are properly null-terminated by libpng.
+ const char* key = info.key;
+ const char* value = info.text;
+ if (strstr(key, "XML:com.adobe.xmp")) {
+ metadata->xmp.resize(strlen(value)); // safe, see above
+ memcpy(metadata->xmp.data(), value, metadata->xmp.size());
+ }
+
+ std::string type;
+ std::vector<uint8_t> bytes;
+
+ // Handle text chunks annotated with key "Raw profile type ####", with
+ // #### a type, which may contain metadata.
+ const char* kKey = "Raw profile type ";
+ if (strncmp(key, kKey, strlen(kKey)) != 0) return false;
+
+ if (!MaybeDecodeBase16(key, value, &type, &bytes)) {
+ JXL_WARNING("Couldn't parse 'Raw format type' text chunk");
+ return false;
+ }
+ if (type == "exif") {
+ // Remove "Exif\0\0" prefix if present
+ if (bytes.size() >= sizeof kExifSignature &&
+ memcmp(bytes.data(), kExifSignature, sizeof kExifSignature) == 0) {
+ bytes.erase(bytes.begin(), bytes.begin() + sizeof kExifSignature);
+ }
+ if (!metadata->exif.empty()) {
+ JXL_WARNING("overwriting EXIF (%" PRIuS " bytes) with base16 (%" PRIuS
+ " bytes)",
+ metadata->exif.size(), bytes.size());
+ }
+ metadata->exif = std::move(bytes);
+ } else if (type == "iptc") {
+ // TODO (jon): Deal with IPTC in some way
+ } else if (type == "8bim") {
+ // TODO (jon): Deal with 8bim in some way
+ } else if (type == "xmp") {
+ if (!metadata->xmp.empty()) {
+ JXL_WARNING("overwriting XMP (%" PRIuS " bytes) with base16 (%" PRIuS
+ " bytes)",
+ metadata->xmp.size(), bytes.size());
+ }
+ metadata->xmp = std::move(bytes);
+ } else {
+ JXL_WARNING("Unknown type in 'Raw format type' text chunk: %s: %" PRIuS
+ " bytes",
+ type.c_str(), bytes.size());
+ }
+ return true;
+ }
+
+ private:
+ // Returns false if invalid.
+ static JXL_INLINE Status DecodeNibble(const char c,
+ uint32_t* JXL_RESTRICT nibble) {
+ if ('a' <= c && c <= 'f') {
+ *nibble = 10 + c - 'a';
+ } else if ('0' <= c && c <= '9') {
+ *nibble = c - '0';
+ } else {
+ *nibble = 0;
+ return JXL_FAILURE("Invalid metadata nibble");
+ }
+ JXL_ASSERT(*nibble < 16);
+ return true;
+ }
+
+ // Returns false if invalid.
+ static JXL_INLINE Status DecodeDecimal(const char** pos, const char* end,
+ uint32_t* JXL_RESTRICT value) {
+ size_t len = 0;
+ *value = 0;
+ while (*pos < end) {
+ char next = **pos;
+ if (next >= '0' && next <= '9') {
+ *value = (*value * 10) + static_cast<uint32_t>(next - '0');
+ len++;
+ if (len > 8) {
+ break;
+ }
+ } else {
+ // Do not consume terminator (non-decimal digit).
+ break;
+ }
+ (*pos)++;
+ }
+ if (len == 0 || len > 8) {
+ return JXL_FAILURE("Failed to parse decimal");
+ }
+ return true;
+ }
+
+ // Parses a PNG text chunk with key of the form "Raw profile type ####", with
+ // #### a type.
+ // Returns whether it could successfully parse the content.
+ // We trust key and encoded are null-terminated because they come from
+ // libpng.
+ static Status MaybeDecodeBase16(const char* key, const char* encoded,
+ std::string* type,
+ std::vector<uint8_t>* bytes) {
+ const char* encoded_end = encoded + strlen(encoded);
+
+ const char* kKey = "Raw profile type ";
+ if (strncmp(key, kKey, strlen(kKey)) != 0) return false;
+ *type = key + strlen(kKey);
+ const size_t kMaxTypeLen = 20;
+ if (type->length() > kMaxTypeLen) return false; // Type too long
+
+ // Header: freeform string and number of bytes
+ // Expected format is:
+ // \n
+ // profile name/description\n
+ // 40\n (the number of bytes after hex-decoding)
+ // 01234566789abcdef....\n (72 bytes per line max).
+ // 012345667\n (last line)
+ const char* pos = encoded;
+
+ if (*(pos++) != '\n') return false;
+ while (pos < encoded_end && *pos != '\n') {
+ pos++;
+ }
+ if (pos == encoded_end) return false;
+ // We parsed so far a \n, some number of non \n characters and are now
+ // pointing at a \n.
+ if (*(pos++) != '\n') return false;
+ // Skip leading spaces
+ while (pos < encoded_end && *pos == ' ') {
+ pos++;
+ }
+ uint32_t bytes_to_decode = 0;
+ JXL_RETURN_IF_ERROR(DecodeDecimal(&pos, encoded_end, &bytes_to_decode));
+
+ // We need 2*bytes for the hex values plus 1 byte every 36 values,
+ // plus terminal \n for length.
+ const unsigned long needed_bytes =
+ bytes_to_decode * 2 + 1 + DivCeil(bytes_to_decode, 36);
+ if (needed_bytes != static_cast<size_t>(encoded_end - pos)) {
+ return JXL_FAILURE("Not enough bytes to parse %d bytes in hex",
+ bytes_to_decode);
+ }
+ JXL_ASSERT(bytes->empty());
+ bytes->reserve(bytes_to_decode);
+
+ // Encoding: base16 with newline after 72 chars.
+ // pos points to the \n before the first line of hex values.
+ for (size_t i = 0; i < bytes_to_decode; ++i) {
+ if (i % 36 == 0) {
+ if (pos + 1 >= encoded_end) return false; // Truncated base16 1
+ if (*pos != '\n') return false; // Expected newline
+ ++pos;
+ }
+
+ if (pos + 2 >= encoded_end) return false; // Truncated base16 2;
+ uint32_t nibble0, nibble1;
+ JXL_RETURN_IF_ERROR(DecodeNibble(pos[0], &nibble0));
+ JXL_RETURN_IF_ERROR(DecodeNibble(pos[1], &nibble1));
+ bytes->push_back(static_cast<uint8_t>((nibble0 << 4) + nibble1));
+ pos += 2;
+ }
+ if (pos + 1 != encoded_end) return false; // Too many encoded bytes
+ if (pos[0] != '\n') return false; // Incorrect metadata terminator
+ return true;
+ }
+};
+
+constexpr bool isAbc(char c) {
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
+}
+
+constexpr uint32_t kId_IHDR = 0x52444849;
+constexpr uint32_t kId_acTL = 0x4C546361;
+constexpr uint32_t kId_fcTL = 0x4C546366;
+constexpr uint32_t kId_IDAT = 0x54414449;
+constexpr uint32_t kId_fdAT = 0x54416466;
+constexpr uint32_t kId_IEND = 0x444E4549;
+constexpr uint32_t kId_cICP = 0x50434963;
+constexpr uint32_t kId_iCCP = 0x50434369;
+constexpr uint32_t kId_sRGB = 0x42475273;
+constexpr uint32_t kId_gAMA = 0x414D4167;
+constexpr uint32_t kId_cHRM = 0x4D524863;
+constexpr uint32_t kId_eXIf = 0x66495865;
+
+struct APNGFrame {
+ std::vector<uint8_t> pixels;
+ std::vector<uint8_t*> rows;
+ unsigned int w, h, delay_num, delay_den;
+};
+
+struct Reader {
+ const uint8_t* next;
+ const uint8_t* last;
+ bool Read(void* data, size_t len) {
+ size_t cap = last - next;
+ size_t to_copy = std::min(cap, len);
+ memcpy(data, next, to_copy);
+ next += to_copy;
+ return (len == to_copy);
+ }
+ bool Eof() { return next == last; }
+};
+
+const unsigned long cMaxPNGSize = 1000000UL;
+const size_t kMaxPNGChunkSize = 1lu << 30; // 1 GB
+
+void info_fn(png_structp png_ptr, png_infop info_ptr) {
+ png_set_expand(png_ptr);
+ png_set_palette_to_rgb(png_ptr);
+ png_set_tRNS_to_alpha(png_ptr);
+ (void)png_set_interlace_handling(png_ptr);
+ png_read_update_info(png_ptr, info_ptr);
+}
+
+void row_fn(png_structp png_ptr, png_bytep new_row, png_uint_32 row_num,
+ int pass) {
+ APNGFrame* frame = (APNGFrame*)png_get_progressive_ptr(png_ptr);
+ JXL_CHECK(frame);
+ JXL_CHECK(row_num < frame->rows.size());
+ JXL_CHECK(frame->rows[row_num] < frame->pixels.data() + frame->pixels.size());
+ png_progressive_combine_row(png_ptr, frame->rows[row_num], new_row);
+}
+
+inline unsigned int read_chunk(Reader* r, std::vector<uint8_t>* pChunk) {
+ unsigned char len[4];
+ if (r->Read(&len, 4)) {
+ const auto size = png_get_uint_32(len);
+ // Check first, to avoid overflow.
+ if (size > kMaxPNGChunkSize) {
+ JXL_WARNING("APNG chunk size is too big");
+ return 0;
+ }
+ pChunk->resize(size + 12);
+ memcpy(pChunk->data(), len, 4);
+ if (r->Read(pChunk->data() + 4, pChunk->size() - 4)) {
+ return LoadLE32(pChunk->data() + 4);
+ }
+ }
+ return 0;
+}
+
+int processing_start(png_structp& png_ptr, png_infop& info_ptr, void* frame_ptr,
+ bool hasInfo, std::vector<uint8_t>& chunkIHDR,
+ std::vector<std::vector<uint8_t>>& chunksInfo) {
+ unsigned char header[8] = {137, 80, 78, 71, 13, 10, 26, 10};
+
+ // Cleanup prior decoder, if any.
+ png_destroy_read_struct(&png_ptr, &info_ptr, 0);
+ // Just in case. Not all versions on libpng wipe-out the pointers.
+ png_ptr = nullptr;
+ info_ptr = nullptr;
+
+ png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+ info_ptr = png_create_info_struct(png_ptr);
+ if (!png_ptr || !info_ptr) return 1;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ return 1;
+ }
+
+ png_set_keep_unknown_chunks(png_ptr, 1, kIgnoredPngChunks,
+ (int)sizeof(kIgnoredPngChunks) / 5);
+
+ png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE);
+ png_set_progressive_read_fn(png_ptr, frame_ptr, info_fn, row_fn, NULL);
+
+ png_process_data(png_ptr, info_ptr, header, 8);
+ png_process_data(png_ptr, info_ptr, chunkIHDR.data(), chunkIHDR.size());
+
+ if (hasInfo) {
+ for (unsigned int i = 0; i < chunksInfo.size(); i++) {
+ png_process_data(png_ptr, info_ptr, chunksInfo[i].data(),
+ chunksInfo[i].size());
+ }
+ }
+ return 0;
+}
+
+int processing_data(png_structp png_ptr, png_infop info_ptr, unsigned char* p,
+ unsigned int size) {
+ if (!png_ptr || !info_ptr) return 1;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ return 1;
+ }
+
+ png_process_data(png_ptr, info_ptr, p, size);
+ return 0;
+}
+
+int processing_finish(png_structp png_ptr, png_infop info_ptr,
+ PackedMetadata* metadata) {
+ unsigned char footer[12] = {0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130};
+
+ if (!png_ptr || !info_ptr) return 1;
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ return 1;
+ }
+
+ png_process_data(png_ptr, info_ptr, footer, 12);
+ // before destroying: check if we encountered any metadata chunks
+ png_textp text_ptr;
+ int num_text;
+ png_get_text(png_ptr, info_ptr, &text_ptr, &num_text);
+ for (int i = 0; i < num_text; i++) {
+ (void)BlobsReaderPNG::Decode(text_ptr[i], metadata);
+ }
+
+ return 0;
+}
+
+} // namespace
+
+Status DecodeImageAPNG(const Span<const uint8_t> bytes,
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+ Reader r;
+ unsigned int id, j, w, h, w0, h0, x0, y0;
+ unsigned int delay_num, delay_den, dop, bop, rowbytes, imagesize;
+ unsigned char sig[8];
+ png_structp png_ptr = nullptr;
+ png_infop info_ptr = nullptr;
+ std::vector<uint8_t> chunk;
+ std::vector<uint8_t> chunkIHDR;
+ std::vector<std::vector<uint8_t>> chunksInfo;
+ bool isAnimated = false;
+ bool hasInfo = false;
+ APNGFrame frameRaw = {};
+ uint32_t num_channels;
+ JxlPixelFormat format;
+ unsigned int bytes_per_pixel = 0;
+
+ struct FrameInfo {
+ PackedImage data;
+ uint32_t duration;
+ size_t x0, xsize;
+ size_t y0, ysize;
+ uint32_t dispose_op;
+ uint32_t blend_op;
+ };
+
+ std::vector<FrameInfo> frames;
+
+ // Make sure png memory is released in any case.
+ auto scope_guard = MakeScopeGuard([&]() {
+ png_destroy_read_struct(&png_ptr, &info_ptr, 0);
+ // Just in case. Not all versions on libpng wipe-out the pointers.
+ png_ptr = nullptr;
+ info_ptr = nullptr;
+ });
+
+ r = {bytes.data(), bytes.data() + bytes.size()};
+ // Not a PNG => not an error
+ unsigned char png_signature[8] = {137, 80, 78, 71, 13, 10, 26, 10};
+ if (!r.Read(sig, 8) || memcmp(sig, png_signature, 8) != 0) {
+ return false;
+ }
+ id = read_chunk(&r, &chunkIHDR);
+
+ ppf->info.exponent_bits_per_sample = 0;
+ ppf->info.alpha_exponent_bits = 0;
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+
+ ppf->frames.clear();
+
+ bool have_color = false;
+ bool have_cicp = false, have_iccp = false, have_srgb = false;
+ bool errorstate = true;
+ if (id == kId_IHDR && chunkIHDR.size() == 25) {
+ x0 = 0;
+ y0 = 0;
+ delay_num = 1;
+ delay_den = 10;
+ dop = 0;
+ bop = 0;
+
+ w0 = w = png_get_uint_32(chunkIHDR.data() + 8);
+ h0 = h = png_get_uint_32(chunkIHDR.data() + 12);
+ if (w > cMaxPNGSize || h > cMaxPNGSize) {
+ return false;
+ }
+
+ // default settings in case e.g. only gAMA is given
+ ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB;
+ ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
+ ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
+ ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
+ ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_RELATIVE;
+
+ if (!processing_start(png_ptr, info_ptr, (void*)&frameRaw, hasInfo,
+ chunkIHDR, chunksInfo)) {
+ while (!r.Eof()) {
+ id = read_chunk(&r, &chunk);
+ if (!id) break;
+
+ if (id == kId_acTL && !hasInfo && !isAnimated) {
+ isAnimated = true;
+ ppf->info.have_animation = true;
+ ppf->info.animation.tps_numerator = 1000;
+ ppf->info.animation.tps_denominator = 1;
+ } else if (id == kId_IEND ||
+ (id == kId_fcTL && (!hasInfo || isAnimated))) {
+ if (hasInfo) {
+ if (!processing_finish(png_ptr, info_ptr, &ppf->metadata)) {
+ // Allocates the frame buffer.
+ uint32_t duration = delay_num * 1000 / delay_den;
+ frames.push_back(FrameInfo{PackedImage(w0, h0, format), duration,
+ x0, w0, y0, h0, dop, bop});
+ auto& frame = frames.back().data;
+ for (size_t y = 0; y < h0; ++y) {
+ memcpy(static_cast<uint8_t*>(frame.pixels()) + frame.stride * y,
+ frameRaw.rows[y], bytes_per_pixel * w0);
+ }
+ } else {
+ break;
+ }
+ }
+
+ if (id == kId_IEND) {
+ errorstate = false;
+ break;
+ }
+ if (chunk.size() < 34) {
+ return JXL_FAILURE("Received a chunk that is too small (%" PRIuS
+ "B)",
+ chunk.size());
+ }
+ // At this point the old frame is done. Let's start a new one.
+ w0 = png_get_uint_32(chunk.data() + 12);
+ h0 = png_get_uint_32(chunk.data() + 16);
+ x0 = png_get_uint_32(chunk.data() + 20);
+ y0 = png_get_uint_32(chunk.data() + 24);
+ delay_num = png_get_uint_16(chunk.data() + 28);
+ delay_den = png_get_uint_16(chunk.data() + 30);
+ dop = chunk[32];
+ bop = chunk[33];
+
+ if (!delay_den) delay_den = 100;
+
+ if (w0 > cMaxPNGSize || h0 > cMaxPNGSize || x0 > cMaxPNGSize ||
+ y0 > cMaxPNGSize || x0 + w0 > w || y0 + h0 > h || dop > 2 ||
+ bop > 1) {
+ break;
+ }
+
+ if (hasInfo) {
+ memcpy(chunkIHDR.data() + 8, chunk.data() + 12, 8);
+ if (processing_start(png_ptr, info_ptr, (void*)&frameRaw, hasInfo,
+ chunkIHDR, chunksInfo)) {
+ break;
+ }
+ }
+ } else if (id == kId_IDAT) {
+ // First IDAT chunk means we now have all header info
+ hasInfo = true;
+ JXL_CHECK(w == png_get_image_width(png_ptr, info_ptr));
+ JXL_CHECK(h == png_get_image_height(png_ptr, info_ptr));
+ int colortype = png_get_color_type(png_ptr, info_ptr);
+ int png_bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+ ppf->info.bits_per_sample = png_bit_depth;
+ png_color_8p sigbits = NULL;
+ png_get_sBIT(png_ptr, info_ptr, &sigbits);
+ if (colortype & 1) {
+ // palette will actually be 8-bit regardless of the index bitdepth
+ ppf->info.bits_per_sample = 8;
+ }
+ if (colortype & 2) {
+ ppf->info.num_color_channels = 3;
+ ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB;
+ if (sigbits && sigbits->red == sigbits->green &&
+ sigbits->green == sigbits->blue)
+ ppf->info.bits_per_sample = sigbits->red;
+ } else {
+ ppf->info.num_color_channels = 1;
+ ppf->color_encoding.color_space = JXL_COLOR_SPACE_GRAY;
+ if (sigbits) ppf->info.bits_per_sample = sigbits->gray;
+ }
+ if (colortype & 4 ||
+ png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
+ ppf->info.alpha_bits = ppf->info.bits_per_sample;
+ if (sigbits) {
+ if (sigbits->alpha &&
+ sigbits->alpha != ppf->info.bits_per_sample) {
+ return JXL_FAILURE("Unsupported alpha bit-depth");
+ }
+ ppf->info.alpha_bits = sigbits->alpha;
+ }
+ } else {
+ ppf->info.alpha_bits = 0;
+ }
+ ppf->color_encoding.color_space =
+ (ppf->info.num_color_channels == 1 ? JXL_COLOR_SPACE_GRAY
+ : JXL_COLOR_SPACE_RGB);
+ ppf->info.xsize = w;
+ ppf->info.ysize = h;
+ JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, w, h));
+ num_channels =
+ ppf->info.num_color_channels + (ppf->info.alpha_bits ? 1 : 0);
+ format = {
+ /*num_channels=*/num_channels,
+ /*data_type=*/ppf->info.bits_per_sample > 8 ? JXL_TYPE_UINT16
+ : JXL_TYPE_UINT8,
+ /*endianness=*/JXL_BIG_ENDIAN,
+ /*align=*/0,
+ };
+ if (png_bit_depth > 8 && format.data_type == JXL_TYPE_UINT8) {
+ png_set_strip_16(png_ptr);
+ }
+ bytes_per_pixel =
+ num_channels * (format.data_type == JXL_TYPE_UINT16 ? 2 : 1);
+ rowbytes = w * bytes_per_pixel;
+ imagesize = h * rowbytes;
+ frameRaw.pixels.resize(imagesize);
+ frameRaw.rows.resize(h);
+ for (j = 0; j < h; j++)
+ frameRaw.rows[j] = frameRaw.pixels.data() + j * rowbytes;
+
+ if (processing_data(png_ptr, info_ptr, chunk.data(), chunk.size())) {
+ break;
+ }
+ } else if (id == kId_fdAT && isAnimated) {
+ png_save_uint_32(chunk.data() + 4, chunk.size() - 16);
+ memcpy(chunk.data() + 8, "IDAT", 4);
+ if (processing_data(png_ptr, info_ptr, chunk.data() + 4,
+ chunk.size() - 4)) {
+ break;
+ }
+ } else if (id == kId_cICP) {
+ // Color profile chunks: cICP has the highest priority, followed by
+ // iCCP and sRGB (which shouldn't co-exist, but if they do, we use
+ // iCCP), followed finally by gAMA and cHRM.
+ if (DecodeCICP(chunk.data() + 8, chunk.size() - 12,
+ &ppf->color_encoding)) {
+ have_cicp = true;
+ have_color = true;
+ ppf->icc.clear();
+ }
+ } else if (!have_cicp && id == kId_iCCP) {
+ if (processing_data(png_ptr, info_ptr, chunk.data(), chunk.size())) {
+ JXL_WARNING("Corrupt iCCP chunk");
+ break;
+ }
+
+ // TODO(jon): catch special case of PQ and synthesize color encoding
+ // in that case
+ int compression_type;
+ png_bytep profile;
+ png_charp name;
+ png_uint_32 proflen = 0;
+ auto ok = png_get_iCCP(png_ptr, info_ptr, &name, &compression_type,
+ &profile, &proflen);
+ if (ok && proflen) {
+ ppf->icc.assign(profile, profile + proflen);
+ have_color = true;
+ have_iccp = true;
+ } else {
+ // TODO(eustas): JXL_WARNING?
+ }
+ } else if (!have_cicp && !have_iccp && id == kId_sRGB) {
+ JXL_RETURN_IF_ERROR(DecodeSRGB(chunk.data() + 8, chunk.size() - 12,
+ &ppf->color_encoding));
+ have_srgb = true;
+ have_color = true;
+ } else if (!have_cicp && !have_srgb && !have_iccp && id == kId_gAMA) {
+ JXL_RETURN_IF_ERROR(DecodeGAMA(chunk.data() + 8, chunk.size() - 12,
+ &ppf->color_encoding));
+ have_color = true;
+ } else if (!have_cicp && !have_srgb && !have_iccp && id == kId_cHRM) {
+ JXL_RETURN_IF_ERROR(DecodeCHRM(chunk.data() + 8, chunk.size() - 12,
+ &ppf->color_encoding));
+ have_color = true;
+ } else if (id == kId_eXIf) {
+ ppf->metadata.exif.resize(chunk.size() - 12);
+ memcpy(ppf->metadata.exif.data(), chunk.data() + 8,
+ chunk.size() - 12);
+ } else if (!isAbc(chunk[4]) || !isAbc(chunk[5]) || !isAbc(chunk[6]) ||
+ !isAbc(chunk[7])) {
+ break;
+ } else {
+ if (processing_data(png_ptr, info_ptr, chunk.data(), chunk.size())) {
+ break;
+ }
+ if (!hasInfo) {
+ chunksInfo.push_back(chunk);
+ continue;
+ }
+ }
+ }
+ }
+
+ JXL_RETURN_IF_ERROR(ApplyColorHints(
+ color_hints, have_color, ppf->info.num_color_channels == 1, ppf));
+ }
+
+ if (errorstate) return false;
+
+ bool has_nontrivial_background = false;
+ bool previous_frame_should_be_cleared = false;
+ enum {
+ DISPOSE_OP_NONE = 0,
+ DISPOSE_OP_BACKGROUND = 1,
+ DISPOSE_OP_PREVIOUS = 2,
+ };
+ enum {
+ BLEND_OP_SOURCE = 0,
+ BLEND_OP_OVER = 1,
+ };
+ for (size_t i = 0; i < frames.size(); i++) {
+ auto& frame = frames[i];
+ JXL_ASSERT(frame.data.xsize == frame.xsize);
+ JXL_ASSERT(frame.data.ysize == frame.ysize);
+
+ // Before encountering a DISPOSE_OP_NONE frame, the canvas is filled with 0,
+ // so DISPOSE_OP_BACKGROUND and DISPOSE_OP_PREVIOUS are equivalent.
+ if (frame.dispose_op == DISPOSE_OP_NONE) {
+ has_nontrivial_background = true;
+ }
+ 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 px0 = frames[i - 1].x0;
+ size_t py0 = frames[i - 1].y0;
+ size_t pxs = frames[i - 1].xsize;
+ size_t pys = frames[i - 1].ysize;
+ if (px0 >= x0 && py0 >= y0 && px0 + pxs <= x0 + xsize &&
+ py0 + pys <= y0 + ysize && frame.blend_op == BLEND_OP_SOURCE &&
+ use_for_next_frame) {
+ // If the previous frame is entirely contained in the current frame and
+ // we are using BLEND_OP_SOURCE, nothing special needs to be done.
+ ppf->frames.emplace_back(std::move(frame.data));
+ } else if (px0 == x0 && py0 == y0 && px0 + pxs == x0 + xsize &&
+ py0 + pys == y0 + ysize && use_for_next_frame) {
+ // If the new frame has the same size as the old one, but we are
+ // blending, we can instead just not blend.
+ should_blend = false;
+ ppf->frames.emplace_back(std::move(frame.data));
+ } else if (px0 <= x0 && py0 <= y0 && px0 + pxs >= x0 + xsize &&
+ py0 + pys >= y0 + ysize && use_for_next_frame) {
+ // If the new frame is contained within the old frame, we can pad the
+ // new frame with zeros and not blend.
+ PackedImage new_data(pxs, pys, frame.data.format);
+ memset(new_data.pixels(), 0, new_data.pixels_size);
+ for (size_t y = 0; y < ysize; y++) {
+ size_t bytes_per_pixel =
+ PackedImage::BitsPerChannel(new_data.format.data_type) *
+ new_data.format.num_channels / 8;
+ memcpy(static_cast<uint8_t*>(new_data.pixels()) +
+ new_data.stride * (y + y0 - py0) +
+ bytes_per_pixel * (x0 - px0),
+ static_cast<const uint8_t*>(frame.data.pixels()) +
+ frame.data.stride * y,
+ xsize * 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.frame_info.layer_info.crop_x0 = px0;
+ pframe.frame_info.layer_info.crop_y0 = py0;
+ pframe.frame_info.layer_info.xsize = pxs;
+ pframe.frame_info.layer_info.ysize = pys;
+ pframe.frame_info.duration = 0;
+ bool is_full_size = px0 == 0 && py0 == 0 && pxs == ppf->info.xsize &&
+ pys == ppf->info.ysize;
+ pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1;
+ pframe.frame_info.layer_info.blend_info.blendmode = JXL_BLEND_REPLACE;
+ pframe.frame_info.layer_info.blend_info.source = 1;
+ 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.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.frame_info.layer_info.blend_info.blendmode =
+ should_blend ? JXL_BLEND_BLEND : JXL_BLEND_REPLACE;
+ bool is_full_size = x0 == 0 && y0 == 0 && xsize == ppf->info.xsize &&
+ ysize == ppf->info.ysize;
+ pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1;
+ pframe.frame_info.layer_info.blend_info.source = 1;
+ pframe.frame_info.layer_info.blend_info.alpha = 0;
+ pframe.frame_info.layer_info.save_as_reference = use_for_next_frame ? 1 : 0;
+
+ 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;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/apng.h b/third_party/jpeg-xl/lib/extras/dec/apng.h
new file mode 100644
index 0000000000..6502ac80c0
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/apng.h
@@ -0,0 +1,34 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_APNG_H_
+#define LIB_EXTRAS_DEC_APNG_H_
+
+// Decodes APNG images in memory.
+
+#include <stdint.h>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+// Decodes `bytes` into `ppf`.
+Status DecodeImageAPNG(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_APNG_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/color_description.cc b/third_party/jpeg-xl/lib/extras/dec/color_description.cc
new file mode 100644
index 0000000000..54f6aa4206
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/color_description.cc
@@ -0,0 +1,218 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/color_description.h"
+
+#include <errno.h>
+
+#include <cmath>
+
+namespace jxl {
+
+namespace {
+
+template <typename T>
+struct EnumName {
+ const char* name;
+ T value;
+};
+
+const EnumName<JxlColorSpace> kJxlColorSpaceNames[] = {
+ {"RGB", JXL_COLOR_SPACE_RGB},
+ {"Gra", JXL_COLOR_SPACE_GRAY},
+ {"XYB", JXL_COLOR_SPACE_XYB},
+ {"CS?", JXL_COLOR_SPACE_UNKNOWN},
+};
+
+const EnumName<JxlWhitePoint> kJxlWhitePointNames[] = {
+ {"D65", JXL_WHITE_POINT_D65},
+ {"Cst", JXL_WHITE_POINT_CUSTOM},
+ {"EER", JXL_WHITE_POINT_E},
+ {"DCI", JXL_WHITE_POINT_DCI},
+};
+
+const EnumName<JxlPrimaries> kJxlPrimariesNames[] = {
+ {"SRG", JXL_PRIMARIES_SRGB},
+ {"Cst", JXL_PRIMARIES_CUSTOM},
+ {"202", JXL_PRIMARIES_2100},
+ {"DCI", JXL_PRIMARIES_P3},
+};
+
+const EnumName<JxlTransferFunction> kJxlTransferFunctionNames[] = {
+ {"709", JXL_TRANSFER_FUNCTION_709},
+ {"TF?", JXL_TRANSFER_FUNCTION_UNKNOWN},
+ {"Lin", JXL_TRANSFER_FUNCTION_LINEAR},
+ {"SRG", JXL_TRANSFER_FUNCTION_SRGB},
+ {"PeQ", JXL_TRANSFER_FUNCTION_PQ},
+ {"DCI", JXL_TRANSFER_FUNCTION_DCI},
+ {"HLG", JXL_TRANSFER_FUNCTION_HLG},
+ {"", JXL_TRANSFER_FUNCTION_GAMMA},
+};
+
+const EnumName<JxlRenderingIntent> kJxlRenderingIntentNames[] = {
+ {"Per", JXL_RENDERING_INTENT_PERCEPTUAL},
+ {"Rel", JXL_RENDERING_INTENT_RELATIVE},
+ {"Sat", JXL_RENDERING_INTENT_SATURATION},
+ {"Abs", JXL_RENDERING_INTENT_ABSOLUTE},
+};
+
+template <typename T>
+Status ParseEnum(const std::string& token, const EnumName<T>* enum_values,
+ size_t enum_len, T* value) {
+ for (size_t i = 0; i < enum_len; i++) {
+ if (enum_values[i].name == token) {
+ *value = enum_values[i].value;
+ return true;
+ }
+ }
+ return false;
+}
+#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0]))
+#define PARSE_ENUM(type, token, value) \
+ ParseEnum<type>(token, k##type##Names, ARRAY_SIZE(k##type##Names), value)
+
+class Tokenizer {
+ public:
+ Tokenizer(const std::string* input, char separator)
+ : input_(input), separator_(separator) {}
+
+ Status Next(std::string* next) {
+ const size_t end = input_->find(separator_, start_);
+ if (end == std::string::npos) {
+ *next = input_->substr(start_); // rest of string
+ } else {
+ *next = input_->substr(start_, end - start_);
+ }
+ if (next->empty()) return JXL_FAILURE("Missing token");
+ start_ = end + 1;
+ return true;
+ }
+
+ private:
+ const std::string* const input_; // not owned
+ const char separator_;
+ size_t start_ = 0; // of next token
+};
+
+Status ParseDouble(const std::string& num, double* d) {
+ char* end;
+ errno = 0;
+ *d = strtod(num.c_str(), &end);
+ if (*d == 0.0 && end == num.c_str()) {
+ return JXL_FAILURE("Invalid double: %s", num.c_str());
+ }
+ if (std::isnan(*d)) {
+ return JXL_FAILURE("Invalid double: %s", num.c_str());
+ }
+ if (errno == ERANGE) {
+ return JXL_FAILURE("Double out of range: %s", num.c_str());
+ }
+ return true;
+}
+
+Status ParseDouble(Tokenizer* tokenizer, double* d) {
+ std::string num;
+ JXL_RETURN_IF_ERROR(tokenizer->Next(&num));
+ return ParseDouble(num, d);
+}
+
+Status ParseColorSpace(Tokenizer* tokenizer, JxlColorEncoding* c) {
+ std::string str;
+ JXL_RETURN_IF_ERROR(tokenizer->Next(&str));
+ JxlColorSpace cs;
+ if (PARSE_ENUM(JxlColorSpace, str, &cs)) {
+ c->color_space = cs;
+ return true;
+ }
+
+ return JXL_FAILURE("Unknown ColorSpace %s", str.c_str());
+}
+
+Status ParseWhitePoint(Tokenizer* tokenizer, JxlColorEncoding* c) {
+ if (c->color_space == JXL_COLOR_SPACE_XYB) {
+ // Implicit white point.
+ c->white_point = JXL_WHITE_POINT_D65;
+ return true;
+ }
+
+ std::string str;
+ JXL_RETURN_IF_ERROR(tokenizer->Next(&str));
+ if (PARSE_ENUM(JxlWhitePoint, str, &c->white_point)) return true;
+
+ Tokenizer xy_tokenizer(&str, ';');
+ c->white_point = JXL_WHITE_POINT_CUSTOM;
+ JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->white_point_xy + 0));
+ JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->white_point_xy + 1));
+ return true;
+}
+
+Status ParsePrimaries(Tokenizer* tokenizer, JxlColorEncoding* c) {
+ if (c->color_space == JXL_COLOR_SPACE_GRAY ||
+ c->color_space == JXL_COLOR_SPACE_XYB) {
+ // No primaries case.
+ return true;
+ }
+
+ std::string str;
+ JXL_RETURN_IF_ERROR(tokenizer->Next(&str));
+ if (PARSE_ENUM(JxlPrimaries, str, &c->primaries)) return true;
+
+ Tokenizer xy_tokenizer(&str, ';');
+ JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_red_xy + 0));
+ JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_red_xy + 1));
+ JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_green_xy + 0));
+ JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_green_xy + 1));
+ JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_blue_xy + 0));
+ JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_blue_xy + 1));
+ c->primaries = JXL_PRIMARIES_CUSTOM;
+
+ return JXL_FAILURE("Invalid primaries %s", str.c_str());
+}
+
+Status ParseRenderingIntent(Tokenizer* tokenizer, JxlColorEncoding* c) {
+ std::string str;
+ JXL_RETURN_IF_ERROR(tokenizer->Next(&str));
+ if (PARSE_ENUM(JxlRenderingIntent, str, &c->rendering_intent)) return true;
+
+ return JXL_FAILURE("Invalid RenderingIntent %s\n", str.c_str());
+}
+
+Status ParseTransferFunction(Tokenizer* tokenizer, JxlColorEncoding* c) {
+ if (c->color_space == JXL_COLOR_SPACE_XYB) {
+ // Implicit TF.
+ c->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
+ c->gamma = 1 / 3.;
+ return true;
+ }
+
+ std::string str;
+ JXL_RETURN_IF_ERROR(tokenizer->Next(&str));
+ if (PARSE_ENUM(JxlTransferFunction, str, &c->transfer_function)) {
+ return true;
+ }
+
+ if (str[0] == 'g') {
+ JXL_RETURN_IF_ERROR(ParseDouble(str.substr(1), &c->gamma));
+ c->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
+ return true;
+ }
+
+ return JXL_FAILURE("Invalid gamma %s", str.c_str());
+}
+
+} // namespace
+
+Status ParseDescription(const std::string& description, JxlColorEncoding* c) {
+ *c = {};
+ Tokenizer tokenizer(&description, '_');
+ JXL_RETURN_IF_ERROR(ParseColorSpace(&tokenizer, c));
+ JXL_RETURN_IF_ERROR(ParseWhitePoint(&tokenizer, c));
+ JXL_RETURN_IF_ERROR(ParsePrimaries(&tokenizer, c));
+ JXL_RETURN_IF_ERROR(ParseRenderingIntent(&tokenizer, c));
+ JXL_RETURN_IF_ERROR(ParseTransferFunction(&tokenizer, c));
+ return true;
+}
+
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/color_description.h b/third_party/jpeg-xl/lib/extras/dec/color_description.h
new file mode 100644
index 0000000000..23680ff7c6
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/color_description.h
@@ -0,0 +1,23 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_COLOR_DESCRIPTION_H_
+#define LIB_EXTRAS_COLOR_DESCRIPTION_H_
+
+#include <jxl/color_encoding.h>
+
+#include <string>
+
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+// Parse the color description into a JxlColorEncoding "RGB_D65_SRG_Rel_Lin".
+Status ParseDescription(const std::string& description,
+ JxlColorEncoding* JXL_RESTRICT c);
+
+} // namespace jxl
+
+#endif // LIB_EXTRAS_COLOR_DESCRIPTION_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/color_description_test.cc b/third_party/jpeg-xl/lib/extras/dec/color_description_test.cc
new file mode 100644
index 0000000000..a1c04a94e4
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/color_description_test.cc
@@ -0,0 +1,38 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/color_description.h"
+
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/test_utils.h"
+#include "lib/jxl/testing.h"
+
+namespace jxl {
+
+// Verify ParseDescription(Description) yields the same ColorEncoding
+TEST(ColorDescriptionTest, RoundTripAll) {
+ for (const auto& cdesc : test::AllEncodings()) {
+ const ColorEncoding c_original = test::ColorEncodingFromDescriptor(cdesc);
+ const std::string description = Description(c_original);
+ printf("%s\n", description.c_str());
+
+ JxlColorEncoding c_external = {};
+ EXPECT_TRUE(ParseDescription(description, &c_external));
+ ColorEncoding c_internal;
+ EXPECT_TRUE(
+ ConvertExternalToInternalColorEncoding(c_external, &c_internal));
+ EXPECT_TRUE(c_original.SameColorEncoding(c_internal))
+ << "Where c_original=" << c_original
+ << " and c_internal=" << c_internal;
+ }
+}
+
+TEST(ColorDescriptionTest, NanGamma) {
+ const std::string description = "Gra_2_Per_gnan";
+ JxlColorEncoding c;
+ EXPECT_FALSE(ParseDescription(description, &c));
+}
+
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/color_hints.cc b/third_party/jpeg-xl/lib/extras/dec/color_hints.cc
new file mode 100644
index 0000000000..53f7cd0543
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/color_hints.cc
@@ -0,0 +1,66 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/color_hints.h"
+
+#include <jxl/encode.h>
+
+#include "lib/extras/dec/color_description.h"
+#include "lib/jxl/base/file_io.h"
+
+namespace jxl {
+namespace extras {
+
+Status ApplyColorHints(const ColorHints& color_hints,
+ const bool color_already_set, const bool is_gray,
+ PackedPixelFile* ppf) {
+ if (color_already_set) {
+ return color_hints.Foreach(
+ [](const std::string& key, const std::string& /*value*/) {
+ JXL_WARNING("Decoder ignoring %s hint", key.c_str());
+ return true;
+ });
+ }
+
+ bool got_color_space = false;
+
+ JXL_RETURN_IF_ERROR(color_hints.Foreach(
+ [is_gray, ppf, &got_color_space](const std::string& key,
+ const std::string& value) -> Status {
+ if (key == "color_space") {
+ JxlColorEncoding c_original_external;
+ if (!ParseDescription(value, &c_original_external)) {
+ return JXL_FAILURE("Failed to apply color_space");
+ }
+ ppf->color_encoding = c_original_external;
+
+ if (is_gray !=
+ (ppf->color_encoding.color_space == JXL_COLOR_SPACE_GRAY)) {
+ return JXL_FAILURE("mismatch between file and color_space hint");
+ }
+
+ got_color_space = true;
+ } else if (key == "icc_pathname") {
+ JXL_RETURN_IF_ERROR(ReadFile(value, &ppf->icc));
+ got_color_space = true;
+ } else {
+ JXL_WARNING("Ignoring %s hint", key.c_str());
+ }
+ return true;
+ }));
+
+ if (!got_color_space) {
+ ppf->color_encoding.color_space =
+ is_gray ? JXL_COLOR_SPACE_GRAY : JXL_COLOR_SPACE_RGB;
+ ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
+ ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
+ ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
+ }
+
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/color_hints.h b/third_party/jpeg-xl/lib/extras/dec/color_hints.h
new file mode 100644
index 0000000000..9c7de884f9
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/color_hints.h
@@ -0,0 +1,72 @@
+// Copyright (c) the JPEG XL Project 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_COLOR_HINTS_H_
+#define LIB_EXTRAS_COLOR_HINTS_H_
+
+// Not all the formats implemented in the extras lib support bundling color
+// information into the file, and those that support it may not have it.
+// To allow attaching color information to those file formats the caller can
+// define these color hints.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+class ColorHints {
+ public:
+ // key=color_space, value=Description(c/pp): specify the ColorEncoding of
+ // the pixels for decoding. Otherwise, if the codec did not obtain an ICC
+ // profile from the image, assume sRGB.
+ //
+ // Strings are taken from the command line, so avoid spaces for convenience.
+ void Add(const std::string& key, const std::string& value) {
+ kv_.emplace_back(key, value);
+ }
+
+ // Calls `func(key, value)` for each key/value in the order they were added,
+ // returning false immediately if `func` returns false.
+ template <class Func>
+ Status Foreach(const Func& func) const {
+ for (const KeyValue& kv : kv_) {
+ Status ok = func(kv.key, kv.value);
+ if (!ok) {
+ return JXL_FAILURE("ColorHints::Foreach returned false");
+ }
+ }
+ return true;
+ }
+
+ private:
+ // Splitting into key/value avoids parsing in each codec.
+ struct KeyValue {
+ KeyValue(std::string key, std::string value)
+ : key(std::move(key)), value(std::move(value)) {}
+
+ std::string key;
+ std::string value;
+ };
+
+ std::vector<KeyValue> kv_;
+};
+
+// Apply the color hints to the decoded image in PackedPixelFile if any.
+// color_already_set tells whether the color encoding was already set, in which
+// case the hints are ignored if any hint is passed.
+Status ApplyColorHints(const ColorHints& color_hints, bool color_already_set,
+ bool is_gray, PackedPixelFile* ppf);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_COLOR_HINTS_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/decode.cc b/third_party/jpeg-xl/lib/extras/dec/decode.cc
new file mode 100644
index 0000000000..e1b0365274
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/decode.cc
@@ -0,0 +1,132 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/decode.h"
+
+#include <locale>
+
+#if JPEGXL_ENABLE_APNG
+#include "lib/extras/dec/apng.h"
+#endif
+#if JPEGXL_ENABLE_EXR
+#include "lib/extras/dec/exr.h"
+#endif
+#if JPEGXL_ENABLE_GIF
+#include "lib/extras/dec/gif.h"
+#endif
+#if JPEGXL_ENABLE_JPEG
+#include "lib/extras/dec/jpg.h"
+#endif
+#include "lib/extras/dec/pgx.h"
+#include "lib/extras/dec/pnm.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+// Any valid encoding is larger (ensures codecs can read the first few bytes)
+constexpr size_t kMinBytes = 9;
+
+} // namespace
+
+std::vector<Codec> AvailableCodecs() {
+ std::vector<Codec> out;
+#if JPEGXL_ENABLE_APNG
+ out.push_back(Codec::kPNG);
+#endif
+#if JPEGXL_ENABLE_EXR
+ out.push_back(Codec::kEXR);
+#endif
+#if JPEGXL_ENABLE_GIF
+ out.push_back(Codec::kGIF);
+#endif
+#if JPEGXL_ENABLE_JPEG
+ out.push_back(Codec::kJPG);
+#endif
+ out.push_back(Codec::kPGX);
+ out.push_back(Codec::kPNM);
+ return out;
+}
+
+Codec CodecFromExtension(std::string extension,
+ size_t* JXL_RESTRICT bits_per_sample) {
+ std::transform(
+ extension.begin(), extension.end(), extension.begin(),
+ [](char c) { return std::tolower(c, std::locale::classic()); });
+ if (extension == ".png") return Codec::kPNG;
+
+ if (extension == ".jpg") return Codec::kJPG;
+ if (extension == ".jpeg") return Codec::kJPG;
+
+ if (extension == ".pgx") return Codec::kPGX;
+
+ if (extension == ".pam") return Codec::kPNM;
+ if (extension == ".pnm") return Codec::kPNM;
+ if (extension == ".pgm") return Codec::kPNM;
+ if (extension == ".ppm") return Codec::kPNM;
+ if (extension == ".pfm") {
+ if (bits_per_sample != nullptr) *bits_per_sample = 32;
+ return Codec::kPNM;
+ }
+
+ if (extension == ".gif") return Codec::kGIF;
+
+ if (extension == ".exr") return Codec::kEXR;
+
+ return Codec::kUnknown;
+}
+
+Status DecodeBytes(const Span<const uint8_t> bytes,
+ const ColorHints& color_hints, extras::PackedPixelFile* ppf,
+ const SizeConstraints* constraints, Codec* orig_codec) {
+ if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes");
+
+ *ppf = extras::PackedPixelFile();
+
+ // Default values when not set by decoders.
+ ppf->info.uses_original_profile = true;
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+
+ const auto choose_codec = [&]() -> Codec {
+#if JPEGXL_ENABLE_APNG
+ if (DecodeImageAPNG(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPNG;
+ }
+#endif
+ if (DecodeImagePGX(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPGX;
+ }
+ if (DecodeImagePNM(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPNM;
+ }
+#if JPEGXL_ENABLE_GIF
+ if (DecodeImageGIF(bytes, color_hints, ppf, constraints)) {
+ return Codec::kGIF;
+ }
+#endif
+#if JPEGXL_ENABLE_JPEG
+ if (DecodeImageJPG(bytes, color_hints, ppf, constraints)) {
+ return Codec::kJPG;
+ }
+#endif
+#if JPEGXL_ENABLE_EXR
+ if (DecodeImageEXR(bytes, color_hints, ppf, constraints)) {
+ return Codec::kEXR;
+ }
+#endif
+ return Codec::kUnknown;
+ };
+
+ Codec codec = choose_codec();
+ if (codec == Codec::kUnknown) {
+ return JXL_FAILURE("Codecs failed to decode");
+ }
+ if (orig_codec) *orig_codec = codec;
+
+ 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
new file mode 100644
index 0000000000..f802041026
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/decode.h
@@ -0,0 +1,55 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_DECODE_H_
+#define LIB_EXTRAS_DEC_DECODE_H_
+
+// Facade for image decoders (PNG, PNM, ...).
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+// Codecs supported by CodecInOut::Encode.
+enum class Codec : uint32_t {
+ kUnknown, // for CodecFromExtension
+ kPNG,
+ kPNM,
+ kPGX,
+ kJPG,
+ kGIF,
+ kEXR
+};
+
+std::vector<Codec> AvailableCodecs();
+
+// If and only if extension is ".pfm", *bits_per_sample is updated to 32 so
+// that Encode() would encode to PFM instead of PPM.
+Codec CodecFromExtension(std::string extension,
+ size_t* JXL_RESTRICT bits_per_sample = nullptr);
+
+// Decodes "bytes" info *ppf.
+// color_space_hint may specify the color space, otherwise, defaults to sRGB.
+Status DecodeBytes(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ extras::PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr,
+ Codec* orig_codec = nullptr);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_DECODE_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/exr.cc b/third_party/jpeg-xl/lib/extras/dec/exr.cc
new file mode 100644
index 0000000000..f174ccd0c9
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/exr.cc
@@ -0,0 +1,184 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/exr.h"
+
+#include <ImfChromaticitiesAttribute.h>
+#include <ImfIO.h>
+#include <ImfRgbaFile.h>
+#include <ImfStandardAttributes.h>
+
+#include <vector>
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+namespace OpenEXR = OPENEXR_IMF_NAMESPACE;
+namespace Imath = IMATH_NAMESPACE;
+
+// OpenEXR::Int64 is deprecated in favor of using uint64_t directly, but using
+// uint64_t as recommended causes build failures with previous OpenEXR versions
+// on macOS, where the definition for OpenEXR::Int64 was actually not equivalent
+// to uint64_t. This alternative should work in all cases.
+using ExrInt64 = decltype(std::declval<OpenEXR::IStream>().tellg());
+
+constexpr int kExrBitsPerSample = 16;
+constexpr int kExrAlphaBits = 16;
+
+class InMemoryIStream : public OpenEXR::IStream {
+ public:
+ // The data pointed to by `bytes` must outlive the InMemoryIStream.
+ explicit InMemoryIStream(const Span<const uint8_t> bytes)
+ : IStream(/*fileName=*/""), bytes_(bytes) {}
+
+ bool isMemoryMapped() const override { return true; }
+ char* readMemoryMapped(const int n) override {
+ JXL_ASSERT(pos_ + n <= bytes_.size());
+ char* const result =
+ const_cast<char*>(reinterpret_cast<const char*>(bytes_.data() + pos_));
+ pos_ += n;
+ return result;
+ }
+ bool read(char c[], const int n) override {
+ std::copy_n(readMemoryMapped(n), n, c);
+ return pos_ < bytes_.size();
+ }
+
+ ExrInt64 tellg() override { return pos_; }
+ void seekg(const ExrInt64 pos) override {
+ JXL_ASSERT(pos + 1 <= bytes_.size());
+ pos_ = pos;
+ }
+
+ private:
+ const Span<const uint8_t> bytes_;
+ size_t pos_ = 0;
+};
+
+} // namespace
+
+Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+ InMemoryIStream is(bytes);
+
+#ifdef __EXCEPTIONS
+ std::unique_ptr<OpenEXR::RgbaInputFile> input_ptr;
+ try {
+ input_ptr.reset(new OpenEXR::RgbaInputFile(is));
+ } catch (...) {
+ return JXL_FAILURE("OpenEXR failed to parse input");
+ }
+ OpenEXR::RgbaInputFile& input = *input_ptr;
+#else
+ OpenEXR::RgbaInputFile input(is);
+#endif
+
+ if ((input.channels() & OpenEXR::RgbaChannels::WRITE_RGB) !=
+ OpenEXR::RgbaChannels::WRITE_RGB) {
+ return JXL_FAILURE("only RGB OpenEXR files are supported");
+ }
+ const bool has_alpha = (input.channels() & OpenEXR::RgbaChannels::WRITE_A) ==
+ OpenEXR::RgbaChannels::WRITE_A;
+
+ const float intensity_target = OpenEXR::hasWhiteLuminance(input.header())
+ ? OpenEXR::whiteLuminance(input.header())
+ : 0;
+
+ auto image_size = input.displayWindow().size();
+ // Size is computed as max - min, but both bounds are inclusive.
+ ++image_size.x;
+ ++image_size.y;
+
+ ppf->info.xsize = image_size.x;
+ ppf->info.ysize = image_size.y;
+ ppf->info.num_color_channels = 3;
+
+ const JxlDataType data_type =
+ kExrBitsPerSample == 16 ? JXL_TYPE_FLOAT16 : JXL_TYPE_FLOAT;
+ const JxlPixelFormat format{
+ /*num_channels=*/3u + (has_alpha ? 1u : 0u),
+ /*data_type=*/data_type,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0,
+ };
+ ppf->frames.clear();
+ // Allocates the frame buffer.
+ ppf->frames.emplace_back(image_size.x, image_size.y, format);
+ const auto& frame = ppf->frames.back();
+
+ const int row_size = input.dataWindow().size().x + 1;
+ // Number of rows to read at a time.
+ // https://www.openexr.com/documentation/ReadingAndWritingImageFiles.pdf
+ // recommends reading the whole file at once.
+ const int y_chunk_size = input.displayWindow().size().y + 1;
+ std::vector<OpenEXR::Rgba> input_rows(row_size * y_chunk_size);
+ for (int start_y =
+ std::max(input.dataWindow().min.y, input.displayWindow().min.y);
+ start_y <=
+ std::min(input.dataWindow().max.y, input.displayWindow().max.y);
+ start_y += y_chunk_size) {
+ // Inclusive.
+ const int end_y = std::min(
+ start_y + y_chunk_size - 1,
+ std::min(input.dataWindow().max.y, input.displayWindow().max.y));
+ input.setFrameBuffer(
+ input_rows.data() - input.dataWindow().min.x - start_y * row_size,
+ /*xStride=*/1, /*yStride=*/row_size);
+ input.readPixels(start_y, end_y);
+ for (int exr_y = start_y; exr_y <= end_y; ++exr_y) {
+ const int image_y = exr_y - input.displayWindow().min.y;
+ const OpenEXR::Rgba* const JXL_RESTRICT input_row =
+ &input_rows[(exr_y - start_y) * row_size];
+ uint8_t* row = static_cast<uint8_t*>(frame.color.pixels()) +
+ frame.color.stride * image_y;
+ const uint32_t pixel_size =
+ (3 + (has_alpha ? 1 : 0)) * kExrBitsPerSample / 8;
+ for (int exr_x =
+ std::max(input.dataWindow().min.x, input.displayWindow().min.x);
+ exr_x <=
+ std::min(input.dataWindow().max.x, input.displayWindow().max.x);
+ ++exr_x) {
+ const int image_x = exr_x - input.displayWindow().min.x;
+ memcpy(row + image_x * pixel_size,
+ input_row + (exr_x - input.dataWindow().min.x), pixel_size);
+ }
+ }
+ }
+
+ ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR;
+ ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB;
+ ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
+ ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
+ if (OpenEXR::hasChromaticities(input.header())) {
+ ppf->color_encoding.primaries = JXL_PRIMARIES_CUSTOM;
+ ppf->color_encoding.white_point = JXL_WHITE_POINT_CUSTOM;
+ const auto& chromaticities = OpenEXR::chromaticities(input.header());
+ ppf->color_encoding.primaries_red_xy[0] = chromaticities.red.x;
+ ppf->color_encoding.primaries_red_xy[1] = chromaticities.red.y;
+ ppf->color_encoding.primaries_green_xy[0] = chromaticities.green.x;
+ ppf->color_encoding.primaries_green_xy[1] = chromaticities.green.y;
+ ppf->color_encoding.primaries_blue_xy[0] = chromaticities.blue.x;
+ ppf->color_encoding.primaries_blue_xy[1] = chromaticities.blue.y;
+ ppf->color_encoding.white_point_xy[0] = chromaticities.white.x;
+ ppf->color_encoding.white_point_xy[1] = chromaticities.white.y;
+ }
+
+ // EXR uses binary16 or binary32 floating point format.
+ ppf->info.bits_per_sample = kExrBitsPerSample;
+ ppf->info.exponent_bits_per_sample = kExrBitsPerSample == 16 ? 5 : 8;
+ if (has_alpha) {
+ ppf->info.alpha_bits = kExrAlphaBits;
+ ppf->info.alpha_exponent_bits = ppf->info.exponent_bits_per_sample;
+ ppf->info.alpha_premultiplied = true;
+ }
+ ppf->info.intensity_target = intensity_target;
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/exr.h b/third_party/jpeg-xl/lib/extras/dec/exr.h
new file mode 100644
index 0000000000..6b7c5b714d
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/exr.h
@@ -0,0 +1,32 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_EXR_H_
+#define LIB_EXTRAS_DEC_EXR_H_
+
+// Decodes OpenEXR images in memory.
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+// Decodes `bytes` into `ppf`. color_hints are ignored.
+Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_EXR_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/gif.cc b/third_party/jpeg-xl/lib/extras/dec/gif.cc
new file mode 100644
index 0000000000..4593382b92
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/gif.cc
@@ -0,0 +1,400 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/gif.h"
+
+#include <gif_lib.h>
+#include <jxl/codestream_header.h>
+#include <string.h>
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "lib/extras/size_constraints.h"
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/sanitizers.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+struct ReadState {
+ Span<const uint8_t> bytes;
+};
+
+struct DGifCloser {
+ void operator()(GifFileType* const ptr) const { DGifCloseFile(ptr, nullptr); }
+};
+using GifUniquePtr = std::unique_ptr<GifFileType, DGifCloser>;
+
+struct PackedRgba {
+ uint8_t r, g, b, a;
+};
+
+struct PackedRgb {
+ uint8_t r, g, b;
+};
+
+void ensure_have_alpha(PackedFrame* frame) {
+ if (!frame->extra_channels.empty()) return;
+ const JxlPixelFormat alpha_format{
+ /*num_channels=*/1u,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0,
+ };
+ frame->extra_channels.emplace_back(frame->color.xsize, frame->color.ysize,
+ alpha_format);
+ // We need to set opaque-by-default.
+ std::fill_n(static_cast<uint8_t*>(frame->extra_channels[0].pixels()),
+ frame->color.xsize * frame->color.ysize, 255u);
+}
+
+} // namespace
+
+Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+ int error = GIF_OK;
+ ReadState state = {bytes};
+ const auto ReadFromSpan = [](GifFileType* const gif, GifByteType* const bytes,
+ int n) {
+ ReadState* const state = reinterpret_cast<ReadState*>(gif->UserData);
+ // giflib API requires the input size `n` to be signed int.
+ if (static_cast<size_t>(n) > state->bytes.size()) {
+ n = state->bytes.size();
+ }
+ memcpy(bytes, state->bytes.data(), n);
+ state->bytes.remove_prefix(n);
+ return n;
+ };
+ GifUniquePtr gif(DGifOpen(&state, ReadFromSpan, &error));
+ if (gif == nullptr) {
+ if (error == D_GIF_ERR_NOT_GIF_FILE) {
+ // Not an error.
+ return false;
+ } else {
+ return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(error));
+ }
+ }
+ error = DGifSlurp(gif.get());
+ if (error != GIF_OK) {
+ return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(gif->Error));
+ }
+
+ msan::UnpoisonMemory(gif.get(), sizeof(*gif));
+ if (gif->SColorMap) {
+ msan::UnpoisonMemory(gif->SColorMap, sizeof(*gif->SColorMap));
+ msan::UnpoisonMemory(
+ gif->SColorMap->Colors,
+ sizeof(*gif->SColorMap->Colors) * gif->SColorMap->ColorCount);
+ }
+ msan::UnpoisonMemory(gif->SavedImages,
+ sizeof(*gif->SavedImages) * gif->ImageCount);
+
+ JXL_RETURN_IF_ERROR(
+ VerifyDimensions<uint32_t>(constraints, gif->SWidth, gif->SHeight));
+ uint64_t total_pixel_count =
+ static_cast<uint64_t>(gif->SWidth) * gif->SHeight;
+ for (int i = 0; i < gif->ImageCount; ++i) {
+ const SavedImage& image = gif->SavedImages[i];
+ uint32_t w = image.ImageDesc.Width;
+ uint32_t h = image.ImageDesc.Height;
+ JXL_RETURN_IF_ERROR(VerifyDimensions<uint32_t>(constraints, w, h));
+ uint64_t pixel_count = static_cast<uint64_t>(w) * h;
+ if (total_pixel_count + pixel_count < total_pixel_count) {
+ return JXL_FAILURE("Image too big");
+ }
+ total_pixel_count += pixel_count;
+ if (constraints && (total_pixel_count > constraints->dec_max_pixels)) {
+ return JXL_FAILURE("Image too big");
+ }
+ }
+
+ if (!gif->SColorMap) {
+ for (int i = 0; i < gif->ImageCount; ++i) {
+ if (!gif->SavedImages[i].ImageDesc.ColorMap) {
+ return JXL_FAILURE("Missing GIF color map");
+ }
+ }
+ }
+
+ if (gif->ImageCount > 1) {
+ ppf->info.have_animation = true;
+ // Delays in GIF are specified in 100ths of a second.
+ ppf->info.animation.tps_numerator = 100;
+ ppf->info.animation.tps_denominator = 1;
+ }
+
+ ppf->frames.clear();
+ ppf->frames.reserve(gif->ImageCount);
+
+ ppf->info.xsize = gif->SWidth;
+ ppf->info.ysize = gif->SHeight;
+ ppf->info.bits_per_sample = 8;
+ ppf->info.exponent_bits_per_sample = 0;
+ // alpha_bits is later set to 8 if we find a frame with transparent pixels.
+ ppf->info.alpha_bits = 0;
+ ppf->info.alpha_exponent_bits = 0;
+ JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
+ /*is_gray=*/false, ppf));
+
+ ppf->info.num_color_channels = 3;
+
+ // Pixel format for the 'canvas' onto which we paint
+ // the (potentially individually cropped) GIF frames
+ // of an animation.
+ const JxlPixelFormat canvas_format{
+ /*num_channels=*/4u,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0,
+ };
+
+ // Pixel format for the JXL PackedFrame that goes into the
+ // PackedPixelFile. Here, we use 3 color channels, and provide
+ // the alpha channel as an extra_channel wherever it is used.
+ const JxlPixelFormat packed_frame_format{
+ /*num_channels=*/3u,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0,
+ };
+
+ GifColorType background_color;
+ if (gif->SColorMap == nullptr ||
+ gif->SBackGroundColor >= gif->SColorMap->ColorCount) {
+ background_color = {0, 0, 0};
+ } else {
+ background_color = gif->SColorMap->Colors[gif->SBackGroundColor];
+ }
+ const PackedRgba background_rgba{background_color.Red, background_color.Green,
+ background_color.Blue, 0};
+ PackedFrame canvas(gif->SWidth, gif->SHeight, canvas_format);
+ std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
+ canvas.color.xsize * canvas.color.ysize, background_rgba);
+ Rect canvas_rect{0, 0, canvas.color.xsize, canvas.color.ysize};
+
+ Rect previous_rect_if_restore_to_background;
+
+ bool replace = true;
+ bool last_base_was_none = true;
+ for (int i = 0; i < gif->ImageCount; ++i) {
+ const SavedImage& image = gif->SavedImages[i];
+ msan::UnpoisonMemory(image.RasterBits, sizeof(*image.RasterBits) *
+ image.ImageDesc.Width *
+ image.ImageDesc.Height);
+ const Rect image_rect(image.ImageDesc.Left, image.ImageDesc.Top,
+ image.ImageDesc.Width, image.ImageDesc.Height);
+
+ Rect total_rect;
+ if (previous_rect_if_restore_to_background.xsize() != 0 ||
+ previous_rect_if_restore_to_background.ysize() != 0) {
+ const size_t xbegin = std::min(
+ image_rect.x0(), previous_rect_if_restore_to_background.x0());
+ const size_t ybegin = std::min(
+ image_rect.y0(), previous_rect_if_restore_to_background.y0());
+ const size_t xend =
+ std::max(image_rect.x0() + image_rect.xsize(),
+ previous_rect_if_restore_to_background.x0() +
+ previous_rect_if_restore_to_background.xsize());
+ const size_t yend =
+ std::max(image_rect.y0() + image_rect.ysize(),
+ previous_rect_if_restore_to_background.y0() +
+ previous_rect_if_restore_to_background.ysize());
+ total_rect = Rect(xbegin, ybegin, xend - xbegin, yend - ybegin);
+ previous_rect_if_restore_to_background = Rect();
+ replace = true;
+ } else {
+ total_rect = image_rect;
+ replace = false;
+ }
+ if (!image_rect.IsInside(canvas_rect)) {
+ return JXL_FAILURE("GIF frame extends outside of the canvas");
+ }
+
+ // Allocates the frame buffer.
+ ppf->frames.emplace_back(total_rect.xsize(), total_rect.ysize(),
+ packed_frame_format);
+ PackedFrame* frame = &ppf->frames.back();
+
+ // We cannot tell right from the start whether there will be a
+ // need for an alpha channel. This is discovered only as soon as
+ // we see a transparent pixel. We hence initialize alpha lazily.
+ auto set_pixel_alpha = [&frame](size_t x, size_t y, uint8_t a) {
+ // If we do not have an alpha-channel and a==255 (fully opaque),
+ // we can skip setting this pixel-value and rely on
+ // "no alpha channel = no transparency".
+ if (a == 255 && !frame->extra_channels.empty()) return;
+ ensure_have_alpha(frame);
+ static_cast<uint8_t*>(
+ frame->extra_channels[0].pixels())[y * frame->color.xsize + x] = a;
+ };
+
+ const ColorMapObject* const color_map =
+ image.ImageDesc.ColorMap ? image.ImageDesc.ColorMap : gif->SColorMap;
+ JXL_CHECK(color_map);
+ msan::UnpoisonMemory(color_map, sizeof(*color_map));
+ 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->frame_info.layer_info.have_crop = static_cast<int>(!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->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)) {
+ return JXL_FAILURE(
+ "GIF with dispose-to-0 is not supported for non-full or "
+ "blended frames");
+ }
+ switch (gcb.DisposalMode) {
+ case DISPOSE_DO_NOT:
+ case DISPOSE_BACKGROUND:
+ frame->frame_info.layer_info.save_as_reference = 1u;
+ last_base_was_none = false;
+ break;
+ case DISPOSE_PREVIOUS:
+ frame->frame_info.layer_info.save_as_reference = 0u;
+ break;
+ default:
+ frame->frame_info.layer_info.save_as_reference = 0u;
+ last_base_was_none = true;
+ }
+ }
+
+ // Update the canvas by creating a copy first.
+ PackedImage new_canvas_image(canvas.color.xsize, canvas.color.ysize,
+ canvas.color.format);
+ memcpy(new_canvas_image.pixels(), canvas.color.pixels(),
+ new_canvas_image.pixels_size);
+ for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) {
+ // Assumes format.align == 0. row points to the beginning of the y row in
+ // the image_rect.
+ PackedRgba* row = static_cast<PackedRgba*>(new_canvas_image.pixels()) +
+ (y + image_rect.y0()) * new_canvas_image.xsize +
+ image_rect.x0();
+ for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) {
+ const GifByteType byte = image.RasterBits[byte_index];
+ if (byte >= color_map->ColorCount) {
+ return JXL_FAILURE("GIF color is out of bounds");
+ }
+
+ if (byte == gcb.TransparentColor) continue;
+ GifColorType color = color_map->Colors[byte];
+ row[x].r = color.Red;
+ row[x].g = color.Green;
+ row[x].b = color.Blue;
+ row[x].a = 255;
+ }
+ }
+ const PackedImage& sub_frame_image = frame->color;
+ if (replace) {
+ // Copy from the new canvas image to the subframe
+ for (size_t y = 0; y < total_rect.ysize(); ++y) {
+ const PackedRgba* row_in =
+ static_cast<const PackedRgba*>(new_canvas_image.pixels()) +
+ (y + total_rect.y0()) * new_canvas_image.xsize + total_rect.x0();
+ PackedRgb* row_out = static_cast<PackedRgb*>(sub_frame_image.pixels()) +
+ y * sub_frame_image.xsize;
+ for (size_t x = 0; x < sub_frame_image.xsize; ++x) {
+ row_out[x].r = row_in[x].r;
+ row_out[x].g = row_in[x].g;
+ row_out[x].b = row_in[x].b;
+ set_pixel_alpha(x, y, row_in[x].a);
+ }
+ }
+ } else {
+ for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) {
+ // Assumes format.align == 0
+ PackedRgb* row = static_cast<PackedRgb*>(sub_frame_image.pixels()) +
+ y * sub_frame_image.xsize;
+ for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) {
+ const GifByteType byte = image.RasterBits[byte_index];
+ if (byte > color_map->ColorCount) {
+ return JXL_FAILURE("GIF color is out of bounds");
+ }
+ if (byte == gcb.TransparentColor) {
+ row[x].r = 0;
+ row[x].g = 0;
+ row[x].b = 0;
+ set_pixel_alpha(x, y, 0);
+ continue;
+ }
+ GifColorType color = color_map->Colors[byte];
+ row[x].r = color.Red;
+ row[x].g = color.Green;
+ row[x].b = color.Blue;
+ set_pixel_alpha(x, y, 255);
+ }
+ }
+ }
+
+ if (!frame->extra_channels.empty()) {
+ ppf->info.alpha_bits = 8;
+ }
+
+ switch (gcb.DisposalMode) {
+ case DISPOSE_DO_NOT:
+ canvas.color = std::move(new_canvas_image);
+ break;
+
+ case DISPOSE_BACKGROUND:
+ std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
+ canvas.color.xsize * canvas.color.ysize, background_rgba);
+ previous_rect_if_restore_to_background = image_rect;
+ break;
+
+ case DISPOSE_PREVIOUS:
+ break;
+
+ case DISPOSAL_UNSPECIFIED:
+ default:
+ std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
+ canvas.color.xsize * canvas.color.ysize, background_rgba);
+ }
+ }
+ // Finally, if any frame has an alpha-channel, every frame will need
+ // to have an alpha-channel.
+ bool seen_alpha = false;
+ for (const PackedFrame& frame : ppf->frames) {
+ if (!frame.extra_channels.empty()) {
+ seen_alpha = true;
+ break;
+ }
+ }
+ if (seen_alpha) {
+ for (PackedFrame& frame : ppf->frames) {
+ ensure_have_alpha(&frame);
+ }
+ }
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/gif.h b/third_party/jpeg-xl/lib/extras/dec/gif.h
new file mode 100644
index 0000000000..e217d617a7
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/gif.h
@@ -0,0 +1,33 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_GIF_H_
+#define LIB_EXTRAS_DEC_GIF_H_
+
+// Decodes GIF images in memory.
+
+#include <stdint.h>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+// Decodes `bytes` into `ppf`. color_hints are ignored.
+Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_GIF_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/jpegli.cc b/third_party/jpeg-xl/lib/extras/dec/jpegli.cc
new file mode 100644
index 0000000000..ffa1b79c25
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jpegli.cc
@@ -0,0 +1,271 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/jpegli.h"
+
+#include <setjmp.h>
+#include <stdint.h>
+
+#include <algorithm>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+#include "lib/jpegli/decode.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/sanitizers.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
+ 0x66, 0x00, 0x00};
+constexpr int kExifMarker = JPEG_APP0 + 1;
+constexpr int kICCMarker = JPEG_APP0 + 2;
+
+static inline bool IsJPG(const std::vector<uint8_t>& bytes) {
+ if (bytes.size() < 2) return false;
+ if (bytes[0] != 0xFF || bytes[1] != 0xD8) return false;
+ return true;
+}
+
+bool MarkerIsExif(const jpeg_saved_marker_ptr marker) {
+ return marker->marker == kExifMarker &&
+ marker->data_length >= sizeof kExifSignature + 2 &&
+ std::equal(std::begin(kExifSignature), std::end(kExifSignature),
+ marker->data);
+}
+
+Status ReadICCProfile(jpeg_decompress_struct* const cinfo,
+ std::vector<uint8_t>* const icc) {
+ uint8_t* icc_data_ptr;
+ unsigned int icc_data_len;
+ if (jpegli_read_icc_profile(cinfo, &icc_data_ptr, &icc_data_len)) {
+ icc->assign(icc_data_ptr, icc_data_ptr + icc_data_len);
+ free(icc_data_ptr);
+ return true;
+ }
+ return false;
+}
+
+void ReadExif(jpeg_decompress_struct* const cinfo,
+ std::vector<uint8_t>* const exif) {
+ constexpr size_t kExifSignatureSize = sizeof kExifSignature;
+ for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr;
+ marker = marker->next) {
+ // marker is initialized by libjpeg, which we are not instrumenting with
+ // msan.
+ msan::UnpoisonMemory(marker, sizeof(*marker));
+ msan::UnpoisonMemory(marker->data, marker->data_length);
+ if (!MarkerIsExif(marker)) continue;
+ size_t marker_length = marker->data_length - kExifSignatureSize;
+ exif->resize(marker_length);
+ std::copy_n(marker->data + kExifSignatureSize, marker_length, exif->data());
+ return;
+ }
+}
+
+JpegliDataType ConvertDataType(JxlDataType type) {
+ switch (type) {
+ case JXL_TYPE_UINT8:
+ return JPEGLI_TYPE_UINT8;
+ case JXL_TYPE_UINT16:
+ return JPEGLI_TYPE_UINT16;
+ case JXL_TYPE_FLOAT:
+ return JPEGLI_TYPE_FLOAT;
+ default:
+ return JPEGLI_TYPE_UINT8;
+ }
+}
+
+JpegliEndianness ConvertEndianness(JxlEndianness type) {
+ switch (type) {
+ case JXL_NATIVE_ENDIAN:
+ return JPEGLI_NATIVE_ENDIAN;
+ case JXL_BIG_ENDIAN:
+ return JPEGLI_BIG_ENDIAN;
+ case JXL_LITTLE_ENDIAN:
+ return JPEGLI_LITTLE_ENDIAN;
+ default:
+ return JPEGLI_NATIVE_ENDIAN;
+ }
+}
+
+JxlColorSpace ConvertColorSpace(J_COLOR_SPACE colorspace) {
+ switch (colorspace) {
+ case JCS_GRAYSCALE:
+ return JXL_COLOR_SPACE_GRAY;
+ case JCS_RGB:
+ return JXL_COLOR_SPACE_RGB;
+ default:
+ return JXL_COLOR_SPACE_UNKNOWN;
+ }
+}
+
+void MyErrorExit(j_common_ptr cinfo) {
+ jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data);
+ (*cinfo->err->output_message)(cinfo);
+ jpegli_destroy_decompress(reinterpret_cast<j_decompress_ptr>(cinfo));
+ longjmp(*env, 1);
+}
+
+void MyOutputMessage(j_common_ptr cinfo) {
+#if JXL_DEBUG_WARNING == 1
+ char buf[JMSG_LENGTH_MAX + 1];
+ (*cinfo->err->format_message)(cinfo, buf);
+ buf[JMSG_LENGTH_MAX] = 0;
+ JXL_WARNING("%s", buf);
+#endif
+}
+
+void UnmapColors(uint8_t* row, size_t xsize, int components,
+ JSAMPARRAY colormap, size_t num_colors) {
+ JXL_CHECK(colormap != nullptr);
+ std::vector<uint8_t> tmp(xsize * components);
+ for (size_t x = 0; x < xsize; ++x) {
+ JXL_CHECK(row[x] < num_colors);
+ for (int c = 0; c < components; ++c) {
+ tmp[x * components + c] = colormap[c][row[x]];
+ }
+ }
+ memcpy(row, tmp.data(), tmp.size());
+}
+
+} // namespace
+
+Status DecodeJpeg(const std::vector<uint8_t>& compressed,
+ const JpegDecompressParams& dparams, ThreadPool* pool,
+ PackedPixelFile* ppf) {
+ // Don't do anything for non-JPEG files (no need to report an error)
+ if (!IsJPG(compressed)) return false;
+
+ // TODO(veluca): use JPEGData also for pixels?
+
+ // We need to declare all the non-trivial destructor local variables before
+ // the call to setjmp().
+ std::unique_ptr<JSAMPLE[]> row;
+
+ jpeg_decompress_struct cinfo;
+ const auto try_catch_block = [&]() -> bool {
+ // Setup error handling in jpeg library so we can deal with broken jpegs in
+ // the fuzzer.
+ jpeg_error_mgr jerr;
+ jmp_buf env;
+ cinfo.err = jpegli_std_error(&jerr);
+ jerr.error_exit = &MyErrorExit;
+ jerr.output_message = &MyOutputMessage;
+ if (setjmp(env)) {
+ return false;
+ }
+ cinfo.client_data = static_cast<void*>(&env);
+
+ jpegli_create_decompress(&cinfo);
+ jpegli_mem_src(&cinfo,
+ reinterpret_cast<const unsigned char*>(compressed.data()),
+ compressed.size());
+ jpegli_save_markers(&cinfo, kICCMarker, 0xFFFF);
+ jpegli_save_markers(&cinfo, kExifMarker, 0xFFFF);
+ const auto failure = [&cinfo](const char* str) -> Status {
+ jpegli_abort_decompress(&cinfo);
+ jpegli_destroy_decompress(&cinfo);
+ return JXL_FAILURE("%s", str);
+ };
+ jpegli_read_header(&cinfo, TRUE);
+ // Might cause CPU-zip bomb.
+ if (cinfo.arith_code) {
+ return failure("arithmetic code JPEGs are not supported");
+ }
+ int nbcomp = cinfo.num_components;
+ if (nbcomp != 1 && nbcomp != 3) {
+ return failure("unsupported number of components in JPEG");
+ }
+ if (dparams.force_rgb) {
+ cinfo.out_color_space = JCS_RGB;
+ } else if (dparams.force_grayscale) {
+ cinfo.out_color_space = JCS_GRAYSCALE;
+ }
+ if (!ReadICCProfile(&cinfo, &ppf->icc)) {
+ ppf->icc.clear();
+ // Default to SRGB
+ ppf->color_encoding.color_space =
+ ConvertColorSpace(cinfo.out_color_space);
+ ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
+ ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
+ ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
+ ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL;
+ }
+ ReadExif(&cinfo, &ppf->metadata.exif);
+
+ ppf->info.xsize = cinfo.image_width;
+ ppf->info.ysize = cinfo.image_height;
+ if (dparams.output_data_type == JXL_TYPE_UINT8) {
+ ppf->info.bits_per_sample = 8;
+ ppf->info.exponent_bits_per_sample = 0;
+ } else if (dparams.output_data_type == JXL_TYPE_UINT16) {
+ ppf->info.bits_per_sample = 16;
+ ppf->info.exponent_bits_per_sample = 0;
+ } else if (dparams.output_data_type == JXL_TYPE_FLOAT) {
+ ppf->info.bits_per_sample = 32;
+ ppf->info.exponent_bits_per_sample = 8;
+ } else {
+ return failure("unsupported data type");
+ }
+ ppf->info.uses_original_profile = true;
+
+ // No alpha in JPG
+ ppf->info.alpha_bits = 0;
+ ppf->info.alpha_exponent_bits = 0;
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+
+ jpegli_set_output_format(&cinfo, ConvertDataType(dparams.output_data_type),
+ ConvertEndianness(dparams.output_endianness));
+
+ if (dparams.num_colors > 0) {
+ cinfo.quantize_colors = TRUE;
+ cinfo.desired_number_of_colors = dparams.num_colors;
+ cinfo.two_pass_quantize = dparams.two_pass_quant;
+ cinfo.dither_mode = (J_DITHER_MODE)dparams.dither_mode;
+ }
+
+ jpegli_start_decompress(&cinfo);
+
+ ppf->info.num_color_channels = cinfo.out_color_components;
+ const JxlPixelFormat format{
+ /*num_channels=*/static_cast<uint32_t>(cinfo.out_color_components),
+ dparams.output_data_type,
+ dparams.output_endianness,
+ /*align=*/0,
+ };
+ ppf->frames.clear();
+ // Allocates the frame buffer.
+ ppf->frames.emplace_back(cinfo.image_width, cinfo.image_height, format);
+ const auto& frame = ppf->frames.back();
+ JXL_ASSERT(sizeof(JSAMPLE) * cinfo.out_color_components *
+ cinfo.image_width <=
+ frame.color.stride);
+
+ for (size_t y = 0; y < cinfo.image_height; ++y) {
+ JSAMPROW rows[] = {reinterpret_cast<JSAMPLE*>(
+ static_cast<uint8_t*>(frame.color.pixels()) +
+ frame.color.stride * y)};
+ jpegli_read_scanlines(&cinfo, rows, 1);
+ if (dparams.num_colors > 0) {
+ UnmapColors(rows[0], cinfo.output_width, cinfo.out_color_components,
+ cinfo.colormap, cinfo.actual_number_of_colors);
+ }
+ }
+
+ jpegli_finish_decompress(&cinfo);
+ return true;
+ };
+ bool success = try_catch_block();
+ jpegli_destroy_decompress(&cinfo);
+ return success;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/jpegli.h b/third_party/jpeg-xl/lib/extras/dec/jpegli.h
new file mode 100644
index 0000000000..574df54c8e
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jpegli.h
@@ -0,0 +1,41 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_JPEGLI_H_
+#define LIB_EXTRAS_DEC_JPEGLI_H_
+
+// Decodes JPG pixels and metadata in memory using the libjpegli library.
+
+#include <jxl/types.h>
+#include <stdint.h>
+
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+struct JpegDecompressParams {
+ JxlDataType output_data_type = JXL_TYPE_UINT8;
+ JxlEndianness output_endianness = JXL_NATIVE_ENDIAN;
+ bool force_rgb = false;
+ bool force_grayscale = false;
+ int num_colors = 0;
+ bool two_pass_quant = true;
+ // 0 = none, 1 = ordered, 2 = Floyd-Steinberg
+ int dither_mode = 2;
+};
+
+Status DecodeJpeg(const std::vector<uint8_t>& compressed,
+ const JpegDecompressParams& dparams, ThreadPool* pool,
+ PackedPixelFile* ppf);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_JPEGLI_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/jpg.cc b/third_party/jpeg-xl/lib/extras/dec/jpg.cc
new file mode 100644
index 0000000000..b3c568b87b
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jpg.cc
@@ -0,0 +1,322 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/jpg.h"
+
+#include <jpeglib.h>
+#include <setjmp.h>
+#include <stdint.h>
+
+#include <algorithm>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+#include "lib/extras/size_constraints.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/sanitizers.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+constexpr unsigned char kICCSignature[12] = {
+ 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00};
+constexpr int kICCMarker = JPEG_APP0 + 2;
+
+constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
+ 0x66, 0x00, 0x00};
+constexpr int kExifMarker = JPEG_APP0 + 1;
+
+static inline bool IsJPG(const Span<const uint8_t> bytes) {
+ if (bytes.size() < 2) return false;
+ if (bytes[0] != 0xFF || bytes[1] != 0xD8) return false;
+ return true;
+}
+
+bool MarkerIsICC(const jpeg_saved_marker_ptr marker) {
+ return marker->marker == kICCMarker &&
+ marker->data_length >= sizeof kICCSignature + 2 &&
+ std::equal(std::begin(kICCSignature), std::end(kICCSignature),
+ marker->data);
+}
+bool MarkerIsExif(const jpeg_saved_marker_ptr marker) {
+ return marker->marker == kExifMarker &&
+ marker->data_length >= sizeof kExifSignature + 2 &&
+ std::equal(std::begin(kExifSignature), std::end(kExifSignature),
+ marker->data);
+}
+
+Status ReadICCProfile(jpeg_decompress_struct* const cinfo,
+ std::vector<uint8_t>* const icc) {
+ constexpr size_t kICCSignatureSize = sizeof kICCSignature;
+ // ICC signature + uint8_t index + uint8_t max_index.
+ constexpr size_t kICCHeadSize = kICCSignatureSize + 2;
+ // Markers are 1-indexed, and we keep them that way in this vector to get a
+ // convenient 0 at the front for when we compute the offsets later.
+ std::vector<size_t> marker_lengths;
+ int num_markers = 0;
+ int seen_markers_count = 0;
+ bool has_num_markers = false;
+ for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr;
+ marker = marker->next) {
+ // marker is initialized by libjpeg, which we are not instrumenting with
+ // msan.
+ msan::UnpoisonMemory(marker, sizeof(*marker));
+ msan::UnpoisonMemory(marker->data, marker->data_length);
+ if (!MarkerIsICC(marker)) continue;
+
+ const int current_marker = marker->data[kICCSignatureSize];
+ if (current_marker == 0) {
+ return JXL_FAILURE("inconsistent JPEG ICC marker numbering");
+ }
+ const int current_num_markers = marker->data[kICCSignatureSize + 1];
+ if (current_marker > current_num_markers) {
+ return JXL_FAILURE("inconsistent JPEG ICC marker numbering");
+ }
+ if (has_num_markers) {
+ if (current_num_markers != num_markers) {
+ return JXL_FAILURE("inconsistent numbers of JPEG ICC markers");
+ }
+ } else {
+ num_markers = current_num_markers;
+ has_num_markers = true;
+ marker_lengths.resize(num_markers + 1);
+ }
+
+ size_t marker_length = marker->data_length - kICCHeadSize;
+
+ if (marker_length == 0) {
+ // NB: if we allow empty chunks, then the next check is incorrect.
+ return JXL_FAILURE("Empty ICC chunk");
+ }
+
+ if (marker_lengths[current_marker] != 0) {
+ return JXL_FAILURE("duplicate JPEG ICC marker number");
+ }
+ marker_lengths[current_marker] = marker_length;
+ seen_markers_count++;
+ }
+
+ if (marker_lengths.empty()) {
+ // Not an error.
+ return false;
+ }
+
+ if (seen_markers_count != num_markers) {
+ JXL_DASSERT(has_num_markers);
+ return JXL_FAILURE("Incomplete set of ICC chunks");
+ }
+
+ std::vector<size_t> offsets = std::move(marker_lengths);
+ std::partial_sum(offsets.begin(), offsets.end(), offsets.begin());
+ icc->resize(offsets.back());
+
+ for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr;
+ marker = marker->next) {
+ if (!MarkerIsICC(marker)) continue;
+ const uint8_t* first = marker->data + kICCHeadSize;
+ uint8_t current_marker = marker->data[kICCSignatureSize];
+ size_t offset = offsets[current_marker - 1];
+ size_t marker_length = offsets[current_marker] - offset;
+ std::copy_n(first, marker_length, icc->data() + offset);
+ }
+
+ return true;
+}
+
+void ReadExif(jpeg_decompress_struct* const cinfo,
+ std::vector<uint8_t>* const exif) {
+ constexpr size_t kExifSignatureSize = sizeof kExifSignature;
+ for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr;
+ marker = marker->next) {
+ // marker is initialized by libjpeg, which we are not instrumenting with
+ // msan.
+ msan::UnpoisonMemory(marker, sizeof(*marker));
+ msan::UnpoisonMemory(marker->data, marker->data_length);
+ if (!MarkerIsExif(marker)) continue;
+ size_t marker_length = marker->data_length - kExifSignatureSize;
+ exif->resize(marker_length);
+ std::copy_n(marker->data + kExifSignatureSize, marker_length, exif->data());
+ return;
+ }
+}
+
+void MyErrorExit(j_common_ptr cinfo) {
+ jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data);
+ (*cinfo->err->output_message)(cinfo);
+ jpeg_destroy_decompress(reinterpret_cast<j_decompress_ptr>(cinfo));
+ longjmp(*env, 1);
+}
+
+void MyOutputMessage(j_common_ptr cinfo) {
+#if JXL_DEBUG_WARNING == 1
+ char buf[JMSG_LENGTH_MAX + 1];
+ (*cinfo->err->format_message)(cinfo, buf);
+ buf[JMSG_LENGTH_MAX] = 0;
+ JXL_WARNING("%s", buf);
+#endif
+}
+
+void UnmapColors(uint8_t* row, size_t xsize, int components,
+ JSAMPARRAY colormap, size_t num_colors) {
+ JXL_CHECK(colormap != nullptr);
+ std::vector<uint8_t> tmp(xsize * components);
+ for (size_t x = 0; x < xsize; ++x) {
+ JXL_CHECK(row[x] < num_colors);
+ for (int c = 0; c < components; ++c) {
+ tmp[x * components + c] = colormap[c][row[x]];
+ }
+ }
+ memcpy(row, tmp.data(), tmp.size());
+}
+
+} // namespace
+
+Status DecodeImageJPG(const Span<const uint8_t> bytes,
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints,
+ const JPGDecompressParams* dparams) {
+ // Don't do anything for non-JPEG files (no need to report an error)
+ if (!IsJPG(bytes)) return false;
+
+ // TODO(veluca): use JPEGData also for pixels?
+
+ // We need to declare all the non-trivial destructor local variables before
+ // the call to setjmp().
+ std::unique_ptr<JSAMPLE[]> row;
+
+ const auto try_catch_block = [&]() -> bool {
+ jpeg_decompress_struct cinfo = {};
+ // Setup error handling in jpeg library so we can deal with broken jpegs in
+ // the fuzzer.
+ jpeg_error_mgr jerr;
+ jmp_buf env;
+ cinfo.err = jpeg_std_error(&jerr);
+ jerr.error_exit = &MyErrorExit;
+ jerr.output_message = &MyOutputMessage;
+ if (setjmp(env)) {
+ return false;
+ }
+ cinfo.client_data = static_cast<void*>(&env);
+
+ jpeg_create_decompress(&cinfo);
+ jpeg_mem_src(&cinfo, reinterpret_cast<const unsigned char*>(bytes.data()),
+ bytes.size());
+ jpeg_save_markers(&cinfo, kICCMarker, 0xFFFF);
+ jpeg_save_markers(&cinfo, kExifMarker, 0xFFFF);
+ const auto failure = [&cinfo](const char* str) -> Status {
+ jpeg_abort_decompress(&cinfo);
+ jpeg_destroy_decompress(&cinfo);
+ return JXL_FAILURE("%s", str);
+ };
+ int read_header_result = jpeg_read_header(&cinfo, TRUE);
+ // TODO(eustas): what about JPEG_HEADER_TABLES_ONLY?
+ if (read_header_result == JPEG_SUSPENDED) {
+ return failure("truncated JPEG input");
+ }
+ if (!VerifyDimensions(constraints, cinfo.image_width, cinfo.image_height)) {
+ return failure("image too big");
+ }
+ // Might cause CPU-zip bomb.
+ if (cinfo.arith_code) {
+ return failure("arithmetic code JPEGs are not supported");
+ }
+ int nbcomp = cinfo.num_components;
+ if (nbcomp != 1 && nbcomp != 3) {
+ return failure("unsupported number of components in JPEG");
+ }
+ if (!ReadICCProfile(&cinfo, &ppf->icc)) {
+ ppf->icc.clear();
+ // Default to SRGB
+ // Actually, (cinfo.output_components == nbcomp) will be checked after
+ // `jpeg_start_decompress`.
+ ppf->color_encoding.color_space =
+ (nbcomp == 1) ? JXL_COLOR_SPACE_GRAY : JXL_COLOR_SPACE_RGB;
+ ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
+ ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
+ ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
+ ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL;
+ }
+ ReadExif(&cinfo, &ppf->metadata.exif);
+ if (!ApplyColorHints(color_hints, /*color_already_set=*/true,
+ /*is_gray=*/false, ppf)) {
+ return failure("ApplyColorHints failed");
+ }
+
+ ppf->info.xsize = cinfo.image_width;
+ ppf->info.ysize = cinfo.image_height;
+ // Original data is uint, so exponent_bits_per_sample = 0.
+ ppf->info.bits_per_sample = BITS_IN_JSAMPLE;
+ JXL_ASSERT(BITS_IN_JSAMPLE == 8 || BITS_IN_JSAMPLE == 16);
+ ppf->info.exponent_bits_per_sample = 0;
+ ppf->info.uses_original_profile = true;
+
+ // No alpha in JPG
+ ppf->info.alpha_bits = 0;
+ ppf->info.alpha_exponent_bits = 0;
+
+ ppf->info.num_color_channels = nbcomp;
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+
+ if (dparams && dparams->num_colors > 0) {
+ cinfo.quantize_colors = TRUE;
+ cinfo.desired_number_of_colors = dparams->num_colors;
+ cinfo.two_pass_quantize = dparams->two_pass_quant;
+ cinfo.dither_mode = (J_DITHER_MODE)dparams->dither_mode;
+ }
+
+ jpeg_start_decompress(&cinfo);
+ JXL_ASSERT(cinfo.out_color_components == nbcomp);
+ JxlDataType data_type =
+ ppf->info.bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16;
+
+ const JxlPixelFormat format{
+ /*num_channels=*/static_cast<uint32_t>(nbcomp),
+ data_type,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0,
+ };
+ ppf->frames.clear();
+ // Allocates the frame buffer.
+ ppf->frames.emplace_back(cinfo.image_width, cinfo.image_height, format);
+ const auto& frame = ppf->frames.back();
+ JXL_ASSERT(sizeof(JSAMPLE) * cinfo.out_color_components *
+ cinfo.image_width <=
+ frame.color.stride);
+
+ if (cinfo.quantize_colors) {
+ jxl::msan::UnpoisonMemory(cinfo.colormap, cinfo.out_color_components *
+ sizeof(cinfo.colormap[0]));
+ for (int c = 0; c < cinfo.out_color_components; ++c) {
+ jxl::msan::UnpoisonMemory(
+ cinfo.colormap[c],
+ cinfo.actual_number_of_colors * sizeof(cinfo.colormap[c][0]));
+ }
+ }
+ for (size_t y = 0; y < cinfo.image_height; ++y) {
+ JSAMPROW rows[] = {reinterpret_cast<JSAMPLE*>(
+ static_cast<uint8_t*>(frame.color.pixels()) +
+ frame.color.stride * y)};
+ jpeg_read_scanlines(&cinfo, rows, 1);
+ msan::UnpoisonMemory(rows[0], sizeof(JSAMPLE) * cinfo.output_components *
+ cinfo.image_width);
+ if (dparams && dparams->num_colors > 0) {
+ UnmapColors(rows[0], cinfo.output_width, cinfo.out_color_components,
+ cinfo.colormap, cinfo.actual_number_of_colors);
+ }
+ }
+
+ jpeg_finish_decompress(&cinfo);
+ jpeg_destroy_decompress(&cinfo);
+ return true;
+ };
+
+ return try_catch_block();
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/jpg.h b/third_party/jpeg-xl/lib/extras/dec/jpg.h
new file mode 100644
index 0000000000..e3de2536ac
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jpg.h
@@ -0,0 +1,44 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_JPG_H_
+#define LIB_EXTRAS_DEC_JPG_H_
+
+// Decodes JPG pixels and metadata in memory.
+
+#include <stdint.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/color_hints.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+struct JPGDecompressParams {
+ int num_colors = 0;
+ bool two_pass_quant = false;
+ // 0 = none, 1 = ordered, 2 = Floyd-Steinberg
+ int dither_mode = 0;
+};
+
+// Decodes `bytes` into `ppf`. color_hints are ignored.
+// `elapsed_deinterleave`, if non-null, will be set to the time (in seconds)
+// that it took to deinterleave the raw JSAMPLEs to planar floats.
+Status DecodeImageJPG(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr,
+ const JPGDecompressParams* dparams = nullptr);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_JPG_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/jxl.cc b/third_party/jpeg-xl/lib/extras/dec/jxl.cc
new file mode 100644
index 0000000000..224f7c7bf9
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jxl.cc
@@ -0,0 +1,561 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/jxl.h"
+
+#include <jxl/decode.h>
+#include <jxl/decode_cxx.h>
+#include <jxl/types.h>
+
+#include "lib/extras/dec/color_description.h"
+#include "lib/extras/enc/encode.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/exif.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+struct BoxProcessor {
+ BoxProcessor(JxlDecoder* dec) : dec_(dec) { Reset(); }
+
+ void InitializeOutput(std::vector<uint8_t>* out) {
+ box_data_ = out;
+ AddMoreOutput();
+ }
+
+ bool AddMoreOutput() {
+ Flush();
+ static const size_t kBoxOutputChunkSize = 1 << 16;
+ box_data_->resize(box_data_->size() + kBoxOutputChunkSize);
+ next_out_ = box_data_->data() + total_size_;
+ avail_out_ = box_data_->size() - total_size_;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetBoxBuffer(dec_, next_out_, avail_out_)) {
+ fprintf(stderr, "JxlDecoderSetBoxBuffer failed\n");
+ return false;
+ }
+ return true;
+ }
+
+ void FinalizeOutput() {
+ if (box_data_ == nullptr) return;
+ Flush();
+ box_data_->resize(total_size_);
+ Reset();
+ }
+
+ private:
+ JxlDecoder* dec_;
+ std::vector<uint8_t>* box_data_;
+ uint8_t* next_out_;
+ size_t avail_out_;
+ size_t total_size_;
+
+ void Reset() {
+ box_data_ = nullptr;
+ next_out_ = nullptr;
+ avail_out_ = 0;
+ total_size_ = 0;
+ }
+ void Flush() {
+ if (box_data_ == nullptr) return;
+ size_t remaining = JxlDecoderReleaseBoxBuffer(dec_);
+ size_t bytes_written = avail_out_ - remaining;
+ next_out_ += bytes_written;
+ avail_out_ -= bytes_written;
+ total_size_ += bytes_written;
+ }
+};
+
+void SetBitDepthFromDataType(JxlDataType data_type, uint32_t* bits_per_sample,
+ uint32_t* exponent_bits_per_sample) {
+ switch (data_type) {
+ case JXL_TYPE_UINT8:
+ *bits_per_sample = 8;
+ *exponent_bits_per_sample = 0;
+ break;
+ case JXL_TYPE_UINT16:
+ *bits_per_sample = 16;
+ *exponent_bits_per_sample = 0;
+ break;
+ case JXL_TYPE_FLOAT16:
+ *bits_per_sample = 16;
+ *exponent_bits_per_sample = 5;
+ break;
+ case JXL_TYPE_FLOAT:
+ *bits_per_sample = 32;
+ *exponent_bits_per_sample = 8;
+ break;
+ }
+}
+
+template <typename T>
+void UpdateBitDepth(JxlBitDepth bit_depth, JxlDataType data_type, T* info) {
+ if (bit_depth.type == JXL_BIT_DEPTH_FROM_PIXEL_FORMAT) {
+ SetBitDepthFromDataType(data_type, &info->bits_per_sample,
+ &info->exponent_bits_per_sample);
+ } else if (bit_depth.type == JXL_BIT_DEPTH_CUSTOM) {
+ info->bits_per_sample = bit_depth.bits_per_sample;
+ info->exponent_bits_per_sample = bit_depth.exponent_bits_per_sample;
+ }
+}
+
+} // namespace
+
+bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
+ const JXLDecompressParams& dparams, size_t* decoded_bytes,
+ PackedPixelFile* ppf, std::vector<uint8_t>* jpeg_bytes) {
+ auto decoder = JxlDecoderMake(/*memory_manager=*/nullptr);
+ JxlDecoder* dec = decoder.get();
+ ppf->frames.clear();
+
+ if (dparams.runner_opaque != nullptr &&
+ JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec, dparams.runner,
+ dparams.runner_opaque)) {
+ fprintf(stderr, "JxlEncoderSetParallelRunner failed\n");
+ return false;
+ }
+
+ JxlPixelFormat format;
+ std::vector<JxlPixelFormat> accepted_formats = dparams.accepted_formats;
+ if (accepted_formats.empty()) {
+ for (const uint32_t num_channels : {1, 2, 3, 4}) {
+ accepted_formats.push_back(
+ {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0});
+ }
+ }
+ JxlColorEncoding color_encoding;
+ size_t num_color_channels = 0;
+ if (!dparams.color_space.empty()) {
+ if (!jxl::ParseDescription(dparams.color_space, &color_encoding)) {
+ fprintf(stderr, "Failed to parse color space %s.\n",
+ dparams.color_space.c_str());
+ return false;
+ }
+ num_color_channels =
+ color_encoding.color_space == JXL_COLOR_SPACE_GRAY ? 1 : 3;
+ }
+
+ bool can_reconstruct_jpeg = false;
+ std::vector<uint8_t> jpeg_data_chunk;
+ if (jpeg_bytes != nullptr) {
+ jpeg_data_chunk.resize(16384);
+ jpeg_bytes->resize(0);
+ }
+
+ int events = (JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE);
+
+ bool max_passes_defined =
+ (dparams.max_passes < std::numeric_limits<uint32_t>::max());
+ if (max_passes_defined || dparams.max_downsampling > 1) {
+ events |= JXL_DEC_FRAME_PROGRESSION;
+ if (max_passes_defined) {
+ JxlDecoderSetProgressiveDetail(dec, JxlProgressiveDetail::kPasses);
+ } else {
+ JxlDecoderSetProgressiveDetail(dec, JxlProgressiveDetail::kLastPasses);
+ }
+ }
+ if (jpeg_bytes != nullptr) {
+ events |= JXL_DEC_JPEG_RECONSTRUCTION;
+ } else {
+ events |= (JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_PREVIEW_IMAGE |
+ JXL_DEC_BOX);
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec, events)) {
+ fprintf(stderr, "JxlDecoderSubscribeEvents failed\n");
+ return false;
+ }
+ if (jpeg_bytes == nullptr) {
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetRenderSpotcolors(dec, dparams.render_spotcolors)) {
+ fprintf(stderr, "JxlDecoderSetRenderSpotColors failed\n");
+ return false;
+ }
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetKeepOrientation(dec, dparams.keep_orientation)) {
+ fprintf(stderr, "JxlDecoderSetKeepOrientation failed\n");
+ return false;
+ }
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetUnpremultiplyAlpha(dec, dparams.unpremultiply_alpha)) {
+ fprintf(stderr, "JxlDecoderSetUnpremultiplyAlpha failed\n");
+ return false;
+ }
+ if (dparams.display_nits > 0 &&
+ JXL_DEC_SUCCESS !=
+ JxlDecoderSetDesiredIntensityTarget(dec, dparams.display_nits)) {
+ fprintf(stderr, "Decoder failed to set desired intensity target\n");
+ return false;
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(dec, JXL_TRUE)) {
+ fprintf(stderr, "JxlDecoderSetDecompressBoxes failed\n");
+ return false;
+ }
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderSetInput(dec, bytes, bytes_size)) {
+ fprintf(stderr, "Decoder failed to set input\n");
+ return false;
+ }
+ uint32_t progression_index = 0;
+ bool codestream_done = false;
+ BoxProcessor boxes(dec);
+ for (;;) {
+ JxlDecoderStatus status = JxlDecoderProcessInput(dec);
+ if (status == JXL_DEC_ERROR) {
+ fprintf(stderr, "Failed to decode image\n");
+ return false;
+ } else if (status == JXL_DEC_NEED_MORE_INPUT) {
+ if (codestream_done) {
+ break;
+ }
+ if (dparams.allow_partial_input) {
+ if (JXL_DEC_SUCCESS != JxlDecoderFlushImage(dec)) {
+ fprintf(stderr,
+ "Input file is truncated and there is no preview "
+ "available yet.\n");
+ return false;
+ }
+ break;
+ }
+ size_t released_size = JxlDecoderReleaseInput(dec);
+ fprintf(stderr,
+ "Input file is truncated (total bytes: %" PRIuS
+ ", processed bytes: %" PRIuS
+ ") and --allow_partial_files is not present.\n",
+ bytes_size, bytes_size - released_size);
+ return false;
+ } else if (status == JXL_DEC_BOX) {
+ boxes.FinalizeOutput();
+ JxlBoxType box_type;
+ if (JXL_DEC_SUCCESS != JxlDecoderGetBoxType(dec, box_type, JXL_TRUE)) {
+ fprintf(stderr, "JxlDecoderGetBoxType failed\n");
+ return false;
+ }
+ std::vector<uint8_t>* box_data = nullptr;
+ if (memcmp(box_type, "Exif", 4) == 0) {
+ box_data = &ppf->metadata.exif;
+ } else if (memcmp(box_type, "iptc", 4) == 0) {
+ box_data = &ppf->metadata.iptc;
+ } else if (memcmp(box_type, "jumb", 4) == 0) {
+ box_data = &ppf->metadata.jumbf;
+ } else if (memcmp(box_type, "xml ", 4) == 0) {
+ box_data = &ppf->metadata.xmp;
+ }
+ if (box_data) {
+ boxes.InitializeOutput(box_data);
+ }
+ } else if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
+ boxes.AddMoreOutput();
+ } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) {
+ can_reconstruct_jpeg = true;
+ // Decoding to JPEG.
+ if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec,
+ jpeg_data_chunk.data(),
+ jpeg_data_chunk.size())) {
+ fprintf(stderr, "Decoder failed to set JPEG Buffer\n");
+ return false;
+ }
+ } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) {
+ // Decoded a chunk to JPEG.
+ size_t used_jpeg_output =
+ jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec);
+ jpeg_bytes->insert(jpeg_bytes->end(), jpeg_data_chunk.data(),
+ jpeg_data_chunk.data() + used_jpeg_output);
+ if (used_jpeg_output == 0) {
+ // Chunk is too small.
+ jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2);
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec,
+ jpeg_data_chunk.data(),
+ jpeg_data_chunk.size())) {
+ fprintf(stderr, "Decoder failed to set JPEG Buffer\n");
+ return false;
+ }
+ } else if (status == JXL_DEC_BASIC_INFO) {
+ if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec, &ppf->info)) {
+ fprintf(stderr, "JxlDecoderGetBasicInfo failed\n");
+ return false;
+ }
+ if (num_color_channels != 0) {
+ // Mark the change in number of color channels due to the requested
+ // color space.
+ ppf->info.num_color_channels = num_color_channels;
+ }
+ if (dparams.output_bitdepth.type == JXL_BIT_DEPTH_CUSTOM) {
+ // Select format based on custom bits per sample.
+ ppf->info.bits_per_sample = dparams.output_bitdepth.bits_per_sample;
+ }
+ // Select format according to accepted formats.
+ if (!jxl::extras::SelectFormat(accepted_formats, ppf->info, &format)) {
+ fprintf(stderr, "SelectFormat failed\n");
+ return false;
+ }
+ bool have_alpha = (format.num_channels == 2 || format.num_channels == 4);
+ if (!have_alpha) {
+ // Mark in the basic info that alpha channel was dropped.
+ ppf->info.alpha_bits = 0;
+ } else {
+ if (dparams.unpremultiply_alpha) {
+ // Mark in the basic info that alpha was unpremultiplied.
+ ppf->info.alpha_premultiplied = false;
+ }
+ }
+ bool alpha_found = false;
+ for (uint32_t i = 0; i < ppf->info.num_extra_channels; ++i) {
+ JxlExtraChannelInfo eci;
+ if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &eci)) {
+ fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n");
+ return false;
+ }
+ if (eci.type == JXL_CHANNEL_ALPHA && have_alpha && !alpha_found) {
+ // Skip the first alpha channels because it is already present in the
+ // interleaved image.
+ alpha_found = true;
+ continue;
+ }
+ std::string name(eci.name_length + 1, 0);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetExtraChannelName(dec, i, &name[0], name.size())) {
+ fprintf(stderr, "JxlDecoderGetExtraChannelName failed\n");
+ return false;
+ }
+ name.resize(eci.name_length);
+ ppf->extra_channels_info.push_back({eci, i, name});
+ }
+ } else if (status == JXL_DEC_COLOR_ENCODING) {
+ if (!dparams.color_space.empty()) {
+ if (ppf->info.uses_original_profile) {
+ fprintf(stderr,
+ "Warning: --color_space ignored because the image is "
+ "not XYB encoded.\n");
+ } else {
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetPreferredColorProfile(dec, &color_encoding)) {
+ fprintf(stderr, "Failed to set color space.\n");
+ return false;
+ }
+ }
+ }
+ size_t icc_size = 0;
+ JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_DATA;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) {
+ fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
+ }
+ if (icc_size != 0) {
+ ppf->icc.resize(icc_size);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetColorAsICCProfile(dec, nullptr, target,
+ ppf->icc.data(), icc_size)) {
+ fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
+ return false;
+ }
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsEncodedProfile(
+ dec, nullptr, target, &ppf->color_encoding)) {
+ ppf->color_encoding.color_space = JXL_COLOR_SPACE_UNKNOWN;
+ }
+ icc_size = 0;
+ target = JXL_COLOR_PROFILE_TARGET_ORIGINAL;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) {
+ fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
+ }
+ if (icc_size != 0) {
+ ppf->orig_icc.resize(icc_size);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetColorAsICCProfile(dec, nullptr, target,
+ ppf->orig_icc.data(), icc_size)) {
+ fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
+ return false;
+ }
+ }
+ } else if (status == JXL_DEC_FRAME) {
+ jxl::extras::PackedFrame frame(ppf->info.xsize, ppf->info.ysize, format);
+ if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &frame.frame_info)) {
+ fprintf(stderr, "JxlDecoderGetFrameHeader failed\n");
+ return false;
+ }
+ frame.name.resize(frame.frame_info.name_length + 1, 0);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetFrameName(dec, &frame.name[0], frame.name.size())) {
+ fprintf(stderr, "JxlDecoderGetFrameName failed\n");
+ return false;
+ }
+ frame.name.resize(frame.frame_info.name_length);
+ ppf->frames.emplace_back(std::move(frame));
+ progression_index = 0;
+ } else if (status == JXL_DEC_FRAME_PROGRESSION) {
+ size_t downsampling = JxlDecoderGetIntendedDownsamplingRatio(dec);
+ if ((max_passes_defined && progression_index >= dparams.max_passes) ||
+ (!max_passes_defined && downsampling <= dparams.max_downsampling)) {
+ if (JXL_DEC_SUCCESS != JxlDecoderFlushImage(dec)) {
+ fprintf(stderr, "JxlDecoderFlushImage failed\n");
+ return false;
+ }
+ if (ppf->frames.back().frame_info.is_last) {
+ break;
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderSkipCurrentFrame(dec)) {
+ fprintf(stderr, "JxlDecoderSkipCurrentFrame failed\n");
+ return false;
+ }
+ }
+ ++progression_index;
+ } else if (status == JXL_DEC_NEED_PREVIEW_OUT_BUFFER) {
+ size_t buffer_size;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderPreviewOutBufferSize(dec, &format, &buffer_size)) {
+ fprintf(stderr, "JxlDecoderPreviewOutBufferSize failed\n");
+ return false;
+ }
+ ppf->preview_frame = std::unique_ptr<jxl::extras::PackedFrame>(
+ new jxl::extras::PackedFrame(ppf->info.preview.xsize,
+ ppf->info.preview.ysize, format));
+ if (buffer_size != ppf->preview_frame->color.pixels_size) {
+ fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n",
+ buffer_size, ppf->preview_frame->color.pixels_size);
+ return false;
+ }
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetPreviewOutBuffer(
+ dec, &format, ppf->preview_frame->color.pixels(), buffer_size)) {
+ fprintf(stderr, "JxlDecoderSetPreviewOutBuffer failed\n");
+ return false;
+ }
+ } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
+ if (jpeg_bytes != nullptr) {
+ break;
+ }
+ size_t buffer_size;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)) {
+ fprintf(stderr, "JxlDecoderImageOutBufferSize failed\n");
+ return false;
+ }
+ jxl::extras::PackedFrame& frame = ppf->frames.back();
+ if (buffer_size != frame.color.pixels_size) {
+ fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n",
+ buffer_size, frame.color.pixels_size);
+ return false;
+ }
+
+ if (dparams.use_image_callback) {
+ auto callback = [](void* opaque, size_t x, size_t y, size_t num_pixels,
+ const void* pixels) {
+ auto* ppf = reinterpret_cast<jxl::extras::PackedPixelFile*>(opaque);
+ jxl::extras::PackedImage& color = ppf->frames.back().color;
+ uint8_t* pixels_buffer = reinterpret_cast<uint8_t*>(color.pixels());
+ size_t sample_size = color.pixel_stride();
+ memcpy(pixels_buffer + (color.stride * y + sample_size * x), pixels,
+ num_pixels * sample_size);
+ };
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetImageOutCallback(dec, &format, callback, ppf)) {
+ fprintf(stderr, "JxlDecoderSetImageOutCallback failed\n");
+ return false;
+ }
+ } else {
+ if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec, &format,
+ frame.color.pixels(),
+ buffer_size)) {
+ fprintf(stderr, "JxlDecoderSetImageOutBuffer failed\n");
+ return false;
+ }
+ }
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetImageOutBitDepth(dec, &dparams.output_bitdepth)) {
+ fprintf(stderr, "JxlDecoderSetImageOutBitDepth failed\n");
+ return false;
+ }
+ UpdateBitDepth(dparams.output_bitdepth, format.data_type, &ppf->info);
+ bool have_alpha = (format.num_channels == 2 || format.num_channels == 4);
+ if (have_alpha) {
+ // Interleaved alpha channels has the same bit depth as color channels.
+ ppf->info.alpha_bits = ppf->info.bits_per_sample;
+ ppf->info.alpha_exponent_bits = ppf->info.exponent_bits_per_sample;
+ }
+ JxlPixelFormat ec_format = format;
+ ec_format.num_channels = 1;
+ for (auto& eci : ppf->extra_channels_info) {
+ frame.extra_channels.emplace_back(jxl::extras::PackedImage(
+ ppf->info.xsize, ppf->info.ysize, ec_format));
+ auto& ec = frame.extra_channels.back();
+ size_t buffer_size;
+ if (JXL_DEC_SUCCESS != JxlDecoderExtraChannelBufferSize(
+ dec, &ec_format, &buffer_size, eci.index)) {
+ fprintf(stderr, "JxlDecoderExtraChannelBufferSize failed\n");
+ return false;
+ }
+ if (buffer_size != ec.pixels_size) {
+ fprintf(stderr,
+ "Invalid extra channel buffer size"
+ " %" PRIuS " %" PRIuS "\n",
+ buffer_size, ec.pixels_size);
+ return false;
+ }
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetExtraChannelBuffer(dec, &ec_format, ec.pixels(),
+ buffer_size, eci.index)) {
+ fprintf(stderr, "JxlDecoderSetExtraChannelBuffer failed\n");
+ return false;
+ }
+ UpdateBitDepth(dparams.output_bitdepth, ec_format.data_type,
+ &eci.ec_info);
+ }
+ } else if (status == JXL_DEC_SUCCESS) {
+ // Decoding finished successfully.
+ break;
+ } else if (status == JXL_DEC_PREVIEW_IMAGE) {
+ // Nothing to do.
+ } else if (status == JXL_DEC_FULL_IMAGE) {
+ if (jpeg_bytes != nullptr || ppf->frames.back().frame_info.is_last) {
+ codestream_done = true;
+ }
+ } else {
+ fprintf(stderr, "Error: unexpected status: %d\n",
+ static_cast<int>(status));
+ return false;
+ }
+ }
+ boxes.FinalizeOutput();
+ if (!ppf->metadata.exif.empty()) {
+ // Verify that Exif box has a valid TIFF header at the specified offset.
+ // Discard bytes preceding the header.
+ if (ppf->metadata.exif.size() >= 4) {
+ uint32_t offset = LoadBE32(ppf->metadata.exif.data());
+ if (offset <= ppf->metadata.exif.size() - 8) {
+ std::vector<uint8_t> exif(ppf->metadata.exif.begin() + 4 + offset,
+ ppf->metadata.exif.end());
+ bool bigendian;
+ if (IsExif(exif, &bigendian)) {
+ ppf->metadata.exif = std::move(exif);
+ } else {
+ fprintf(stderr, "Warning: invalid TIFF header in Exif\n");
+ }
+ } else {
+ fprintf(stderr, "Warning: invalid Exif offset: %" PRIu32 "\n", offset);
+ }
+ } else {
+ fprintf(stderr, "Warning: invalid Exif length: %" PRIuS "\n",
+ ppf->metadata.exif.size());
+ }
+ }
+ if (jpeg_bytes != nullptr) {
+ if (!can_reconstruct_jpeg) return false;
+ size_t used_jpeg_output =
+ jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec);
+ jpeg_bytes->insert(jpeg_bytes->end(), jpeg_data_chunk.data(),
+ jpeg_data_chunk.data() + used_jpeg_output);
+ }
+ if (decoded_bytes) {
+ *decoded_bytes = bytes_size - JxlDecoderReleaseInput(dec);
+ }
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/jxl.h b/third_party/jpeg-xl/lib/extras/dec/jxl.h
new file mode 100644
index 0000000000..5f4ed7f683
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jxl.h
@@ -0,0 +1,69 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_JXL_H_
+#define LIB_EXTRAS_DEC_JXL_H_
+
+// Decodes JPEG XL images in memory.
+
+#include <jxl/parallel_runner.h>
+#include <jxl/types.h>
+#include <stdint.h>
+
+#include <limits>
+#include <string>
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+
+namespace jxl {
+namespace extras {
+
+struct JXLDecompressParams {
+ // If empty, little endian float formats will be accepted.
+ std::vector<JxlPixelFormat> accepted_formats;
+
+ // Requested output color space description.
+ std::string color_space;
+ // If set, performs tone mapping to this intensity target luminance.
+ float display_nits = 0.0;
+ // Whether spot colors are rendered on the image.
+ bool render_spotcolors = true;
+ // Whether to keep or undo the orientation given in the header.
+ bool keep_orientation = false;
+
+ // If runner_opaque is set, the decoder uses this parallel runner.
+ JxlParallelRunner runner;
+ void* runner_opaque = nullptr;
+
+ // Whether truncated input should be treated as an error.
+ bool allow_partial_input = false;
+
+ // How many passes to decode at most. By default, decode everything.
+ uint32_t max_passes = std::numeric_limits<uint32_t>::max();
+
+ // Alternatively, one can specify the maximum tolerable downscaling factor
+ // with respect to the full size of the image. By default, nothing less than
+ // the full size is requested.
+ size_t max_downsampling = 1;
+
+ // Whether to use the image callback or the image buffer to get the output.
+ bool use_image_callback = true;
+ // Whether to unpremultiply colors for associated alpha channels.
+ bool unpremultiply_alpha = false;
+
+ // Controls the effective bit depth of the output pixels.
+ JxlBitDepth output_bitdepth = {JXL_BIT_DEPTH_FROM_PIXEL_FORMAT, 0, 0};
+};
+
+bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
+ const JXLDecompressParams& dparams, size_t* decoded_bytes,
+ PackedPixelFile* ppf,
+ std::vector<uint8_t>* jpeg_bytes = nullptr);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_JXL_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/pgx.cc b/third_party/jpeg-xl/lib/extras/dec/pgx.cc
new file mode 100644
index 0000000000..a99eb0f4ee
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pgx.cc
@@ -0,0 +1,202 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/pgx.h"
+
+#include <string.h>
+
+#include "lib/extras/size_constraints.h"
+#include "lib/jxl/base/bits.h"
+#include "lib/jxl/base/compiler_specific.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+struct HeaderPGX {
+ // NOTE: PGX is always grayscale
+ size_t xsize;
+ size_t ysize;
+ size_t bits_per_sample;
+ bool big_endian;
+ bool is_signed;
+};
+
+class Parser {
+ public:
+ explicit Parser(const Span<const uint8_t> bytes)
+ : pos_(bytes.data()), end_(pos_ + bytes.size()) {}
+
+ // Sets "pos" to the first non-header byte/pixel on success.
+ Status ParseHeader(HeaderPGX* header, const uint8_t** pos) {
+ // codec.cc ensures we have at least two bytes => no range check here.
+ if (pos_[0] != 'P' || pos_[1] != 'G') return false;
+ pos_ += 2;
+ return ParseHeaderPGX(header, pos);
+ }
+
+ // Exposed for testing
+ Status ParseUnsigned(size_t* number) {
+ if (pos_ == end_) return JXL_FAILURE("PGX: reached end before number");
+ if (!IsDigit(*pos_)) return JXL_FAILURE("PGX: expected unsigned number");
+
+ *number = 0;
+ while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
+ *number *= 10;
+ *number += *pos_ - '0';
+ ++pos_;
+ }
+
+ return true;
+ }
+
+ private:
+ static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; }
+ static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; }
+ static bool IsWhitespace(const uint8_t c) {
+ return IsLineBreak(c) || c == '\t' || c == ' ';
+ }
+
+ Status SkipSpace() {
+ if (pos_ == end_) return JXL_FAILURE("PGX: reached end before space");
+ const uint8_t c = *pos_;
+ if (c != ' ') return JXL_FAILURE("PGX: expected space");
+ ++pos_;
+ return true;
+ }
+
+ Status SkipLineBreak() {
+ if (pos_ == end_) return JXL_FAILURE("PGX: reached end before line break");
+ // Line break can be either "\n" (0a) or "\r\n" (0d 0a).
+ if (*pos_ == '\n') {
+ pos_++;
+ return true;
+ } else if (*pos_ == '\r' && pos_ + 1 != end_ && *(pos_ + 1) == '\n') {
+ pos_ += 2;
+ return true;
+ }
+ return JXL_FAILURE("PGX: expected line break");
+ }
+
+ Status SkipSingleWhitespace() {
+ if (pos_ == end_) return JXL_FAILURE("PGX: reached end before whitespace");
+ if (!IsWhitespace(*pos_)) return JXL_FAILURE("PGX: expected whitespace");
+ ++pos_;
+ return true;
+ }
+
+ Status ParseHeaderPGX(HeaderPGX* header, const uint8_t** pos) {
+ JXL_RETURN_IF_ERROR(SkipSpace());
+ if (pos_ + 2 > end_) return JXL_FAILURE("PGX: header too small");
+ if (*pos_ == 'M' && *(pos_ + 1) == 'L') {
+ header->big_endian = true;
+ } else if (*pos_ == 'L' && *(pos_ + 1) == 'M') {
+ header->big_endian = false;
+ } else {
+ return JXL_FAILURE("PGX: invalid endianness");
+ }
+ pos_ += 2;
+ JXL_RETURN_IF_ERROR(SkipSpace());
+ if (pos_ == end_) return JXL_FAILURE("PGX: header too small");
+ if (*pos_ == '+') {
+ header->is_signed = false;
+ } else if (*pos_ == '-') {
+ header->is_signed = true;
+ } else {
+ return JXL_FAILURE("PGX: invalid signedness");
+ }
+ pos_++;
+ // Skip optional space
+ if (pos_ < end_ && *pos_ == ' ') pos_++;
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->bits_per_sample));
+ JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
+ JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
+ // 0xa, or 0xd 0xa.
+ JXL_RETURN_IF_ERROR(SkipLineBreak());
+
+ // TODO(jon): could do up to 24-bit by converting the values to
+ // JXL_TYPE_FLOAT.
+ if (header->bits_per_sample > 16) {
+ return JXL_FAILURE("PGX: >16 bits not yet supported");
+ }
+ // TODO(lode): support signed integers. This may require changing the way
+ // external_image works.
+ if (header->is_signed) {
+ return JXL_FAILURE("PGX: signed not yet supported");
+ }
+
+ size_t numpixels = header->xsize * header->ysize;
+ size_t bytes_per_pixel = header->bits_per_sample <= 8 ? 1 : 2;
+ if (pos_ + numpixels * bytes_per_pixel > end_) {
+ return JXL_FAILURE("PGX: data too small");
+ }
+
+ *pos = pos_;
+ return true;
+ }
+
+ const uint8_t* pos_;
+ const uint8_t* const end_;
+};
+
+} // namespace
+
+Status DecodeImagePGX(const Span<const uint8_t> bytes,
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+ Parser parser(bytes);
+ HeaderPGX header = {};
+ const uint8_t* pos;
+ if (!parser.ParseHeader(&header, &pos)) return false;
+ JXL_RETURN_IF_ERROR(
+ VerifyDimensions(constraints, header.xsize, header.ysize));
+ if (header.bits_per_sample == 0 || header.bits_per_sample > 32) {
+ return JXL_FAILURE("PGX: bits_per_sample invalid");
+ }
+
+ JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
+ /*is_gray=*/true, ppf));
+ ppf->info.xsize = header.xsize;
+ ppf->info.ysize = header.ysize;
+ // Original data is uint, so exponent_bits_per_sample = 0.
+ ppf->info.bits_per_sample = header.bits_per_sample;
+ ppf->info.exponent_bits_per_sample = 0;
+ ppf->info.uses_original_profile = true;
+
+ // No alpha in PGX
+ ppf->info.alpha_bits = 0;
+ ppf->info.alpha_exponent_bits = 0;
+ ppf->info.num_color_channels = 1; // Always grayscale
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+
+ JxlDataType data_type;
+ if (header.bits_per_sample > 8) {
+ data_type = JXL_TYPE_UINT16;
+ } else {
+ data_type = JXL_TYPE_UINT8;
+ }
+
+ const JxlPixelFormat format{
+ /*num_channels=*/1,
+ /*data_type=*/data_type,
+ /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
+ /*align=*/0,
+ };
+ ppf->frames.clear();
+ // Allocates the frame buffer.
+ ppf->frames.emplace_back(header.xsize, header.ysize, format);
+ const auto& frame = ppf->frames.back();
+ size_t pgx_remaining_size = bytes.data() + bytes.size() - pos;
+ if (pgx_remaining_size < frame.color.pixels_size) {
+ return JXL_FAILURE("PGX file too small");
+ }
+ memcpy(frame.color.pixels(), pos, frame.color.pixels_size);
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/pgx.h b/third_party/jpeg-xl/lib/extras/dec/pgx.h
new file mode 100644
index 0000000000..2cbd3b4dcf
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pgx.h
@@ -0,0 +1,35 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_PGX_H_
+#define LIB_EXTRAS_DEC_PGX_H_
+
+// Decodes PGX pixels in memory.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+// Decodes `bytes` into `ppf`.
+Status DecodeImagePGX(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_PGX_H_
diff --git a/third_party/jpeg-xl/lib/extras/dec/pgx_test.cc b/third_party/jpeg-xl/lib/extras/dec/pgx_test.cc
new file mode 100644
index 0000000000..78ed689d07
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pgx_test.cc
@@ -0,0 +1,79 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/pgx.h"
+
+#include "lib/extras/packed_image_convert.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/testing.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+Span<const uint8_t> MakeSpan(const char* str) {
+ return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str),
+ strlen(str));
+}
+
+TEST(CodecPGXTest, Test8bits) {
+ std::string pgx = "PG ML + 8 2 3\npixels";
+
+ PackedPixelFile ppf;
+ ThreadPool* pool = nullptr;
+
+ EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), &ppf));
+ CodecInOut io;
+ EXPECT_TRUE(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io));
+
+ ScaleImage(255.f, io.Main().color());
+
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_TRUE(io.metadata.m.color_encoding.IsGray());
+ EXPECT_EQ(2u, io.xsize());
+ EXPECT_EQ(3u, io.ysize());
+
+ float eps = 1e-5;
+ EXPECT_NEAR('p', io.Main().color()->Plane(0).Row(0)[0], eps);
+ EXPECT_NEAR('i', io.Main().color()->Plane(0).Row(0)[1], eps);
+ EXPECT_NEAR('x', io.Main().color()->Plane(0).Row(1)[0], eps);
+ EXPECT_NEAR('e', io.Main().color()->Plane(0).Row(1)[1], eps);
+ EXPECT_NEAR('l', io.Main().color()->Plane(0).Row(2)[0], eps);
+ EXPECT_NEAR('s', io.Main().color()->Plane(0).Row(2)[1], eps);
+}
+
+TEST(CodecPGXTest, Test16bits) {
+ std::string pgx = "PG ML + 16 2 3\np_i_x_e_l_s_";
+
+ PackedPixelFile ppf;
+ ThreadPool* pool = nullptr;
+
+ EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), &ppf));
+ CodecInOut io;
+ EXPECT_TRUE(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io));
+
+ ScaleImage(255.f, io.Main().color());
+
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(16u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_TRUE(io.metadata.m.color_encoding.IsGray());
+ EXPECT_EQ(2u, io.xsize());
+ EXPECT_EQ(3u, io.ysize());
+
+ // Comparing ~16-bit numbers in floats, only ~7 bits left.
+ float eps = 1e-3;
+ const auto& plane = io.Main().color()->Plane(0);
+ EXPECT_NEAR(256.0f * 'p' + '_', plane.Row(0)[0] * 257, eps);
+ EXPECT_NEAR(256.0f * 'i' + '_', plane.Row(0)[1] * 257, eps);
+ EXPECT_NEAR(256.0f * 'x' + '_', plane.Row(1)[0] * 257, eps);
+ EXPECT_NEAR(256.0f * 'e' + '_', plane.Row(1)[1] * 257, eps);
+ EXPECT_NEAR(256.0f * 'l' + '_', plane.Row(2)[0] * 257, eps);
+ EXPECT_NEAR(256.0f * 's' + '_', plane.Row(2)[1] * 257, eps);
+}
+
+} // namespace
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/pnm.cc b/third_party/jpeg-xl/lib/extras/dec/pnm.cc
new file mode 100644
index 0000000000..c3c2247769
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pnm.cc
@@ -0,0 +1,474 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/pnm.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <cmath>
+
+#include "lib/extras/size_constraints.h"
+#include "lib/jxl/base/bits.h"
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+struct HeaderPNM {
+ size_t xsize;
+ size_t ysize;
+ bool is_gray; // PGM
+ bool has_alpha; // PAM
+ size_t bits_per_sample;
+ bool floating_point;
+ bool big_endian;
+ std::vector<JxlExtraChannelType> ec_types; // PAM
+};
+
+class Parser {
+ public:
+ explicit Parser(const Span<const uint8_t> bytes)
+ : pos_(bytes.data()), end_(pos_ + bytes.size()) {}
+
+ // Sets "pos" to the first non-header byte/pixel on success.
+ Status ParseHeader(HeaderPNM* header, const uint8_t** pos) {
+ // codec.cc ensures we have at least two bytes => no range check here.
+ if (pos_[0] != 'P') return false;
+ const uint8_t type = pos_[1];
+ pos_ += 2;
+
+ switch (type) {
+ case '4':
+ return JXL_FAILURE("pbm not supported");
+
+ case '5':
+ header->is_gray = true;
+ return ParseHeaderPNM(header, pos);
+
+ case '6':
+ header->is_gray = false;
+ return ParseHeaderPNM(header, pos);
+
+ case '7':
+ return ParseHeaderPAM(header, pos);
+
+ case 'F':
+ header->is_gray = false;
+ return ParseHeaderPFM(header, pos);
+
+ case 'f':
+ header->is_gray = true;
+ return ParseHeaderPFM(header, pos);
+ }
+ return false;
+ }
+
+ // Exposed for testing
+ Status ParseUnsigned(size_t* number) {
+ if (pos_ == end_) return JXL_FAILURE("PNM: reached end before number");
+ if (!IsDigit(*pos_)) return JXL_FAILURE("PNM: expected unsigned number");
+
+ *number = 0;
+ while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
+ *number *= 10;
+ *number += *pos_ - '0';
+ ++pos_;
+ }
+
+ return true;
+ }
+
+ Status ParseSigned(double* number) {
+ if (pos_ == end_) return JXL_FAILURE("PNM: reached end before signed");
+
+ if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) {
+ return JXL_FAILURE("PNM: expected signed number");
+ }
+
+ // Skip sign
+ const bool is_neg = *pos_ == '-';
+ if (is_neg || *pos_ == '+') {
+ ++pos_;
+ if (pos_ == end_) return JXL_FAILURE("PNM: reached end before digits");
+ }
+
+ // Leading digits
+ *number = 0.0;
+ while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
+ *number *= 10;
+ *number += *pos_ - '0';
+ ++pos_;
+ }
+
+ // Decimal places?
+ if (pos_ < end_ && *pos_ == '.') {
+ ++pos_;
+ double place = 0.1;
+ while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') {
+ *number += (*pos_ - '0') * place;
+ place *= 0.1;
+ ++pos_;
+ }
+ }
+
+ if (is_neg) *number = -*number;
+ return true;
+ }
+
+ private:
+ static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; }
+ static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; }
+ static bool IsWhitespace(const uint8_t c) {
+ return IsLineBreak(c) || c == '\t' || c == ' ';
+ }
+
+ Status SkipBlank() {
+ if (pos_ == end_) return JXL_FAILURE("PNM: reached end before blank");
+ const uint8_t c = *pos_;
+ if (c != ' ' && c != '\n') return JXL_FAILURE("PNM: expected blank");
+ ++pos_;
+ return true;
+ }
+
+ Status SkipSingleWhitespace() {
+ if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace");
+ if (!IsWhitespace(*pos_)) return JXL_FAILURE("PNM: expected whitespace");
+ ++pos_;
+ return true;
+ }
+
+ Status SkipWhitespace() {
+ if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace");
+ if (!IsWhitespace(*pos_) && *pos_ != '#') {
+ return JXL_FAILURE("PNM: expected whitespace/comment");
+ }
+
+ while (pos_ < end_ && IsWhitespace(*pos_)) {
+ ++pos_;
+ }
+
+ // Comment(s)
+ while (pos_ != end_ && *pos_ == '#') {
+ while (pos_ != end_ && !IsLineBreak(*pos_)) {
+ ++pos_;
+ }
+ // Newline(s)
+ while (pos_ != end_ && IsLineBreak(*pos_)) pos_++;
+ }
+
+ while (pos_ < end_ && IsWhitespace(*pos_)) {
+ ++pos_;
+ }
+ return true;
+ }
+
+ Status MatchString(const char* keyword, bool skipws = true) {
+ const uint8_t* ppos = pos_;
+ while (*keyword) {
+ if (ppos >= end_) return JXL_FAILURE("PAM: unexpected end of input");
+ if (*keyword != *ppos) return false;
+ ppos++;
+ keyword++;
+ }
+ pos_ = ppos;
+ if (skipws) {
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ } else {
+ JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
+ }
+ return true;
+ }
+
+ Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) {
+ size_t depth = 3;
+ size_t max_val = 255;
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ while (!MatchString("ENDHDR", /*skipws=*/false)) {
+ if (MatchString("WIDTH")) {
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ } else if (MatchString("HEIGHT")) {
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ } else if (MatchString("DEPTH")) {
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&depth));
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ } else if (MatchString("MAXVAL")) {
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ } else if (MatchString("TUPLTYPE")) {
+ if (MatchString("RGB_ALPHA")) {
+ header->has_alpha = true;
+ } else if (MatchString("RGB")) {
+ } else if (MatchString("GRAYSCALE_ALPHA")) {
+ header->has_alpha = true;
+ header->is_gray = true;
+ } else if (MatchString("GRAYSCALE")) {
+ header->is_gray = true;
+ } else if (MatchString("BLACKANDWHITE_ALPHA")) {
+ header->has_alpha = true;
+ header->is_gray = true;
+ max_val = 1;
+ } else if (MatchString("BLACKANDWHITE")) {
+ header->is_gray = true;
+ max_val = 1;
+ } else if (MatchString("Alpha")) {
+ header->ec_types.push_back(JXL_CHANNEL_ALPHA);
+ } else if (MatchString("Depth")) {
+ header->ec_types.push_back(JXL_CHANNEL_DEPTH);
+ } else if (MatchString("SpotColor")) {
+ header->ec_types.push_back(JXL_CHANNEL_SPOT_COLOR);
+ } else if (MatchString("SelectionMask")) {
+ header->ec_types.push_back(JXL_CHANNEL_SELECTION_MASK);
+ } else if (MatchString("Black")) {
+ header->ec_types.push_back(JXL_CHANNEL_BLACK);
+ } else if (MatchString("CFA")) {
+ header->ec_types.push_back(JXL_CHANNEL_CFA);
+ } else if (MatchString("Thermal")) {
+ header->ec_types.push_back(JXL_CHANNEL_THERMAL);
+ } else {
+ return JXL_FAILURE("PAM: unknown TUPLTYPE");
+ }
+ } else {
+ constexpr size_t kMaxHeaderLength = 20;
+ char unknown_header[kMaxHeaderLength + 1];
+ size_t len = std::min<size_t>(kMaxHeaderLength, end_ - pos_);
+ strncpy(unknown_header, reinterpret_cast<const char*>(pos_), len);
+ unknown_header[len] = 0;
+ return JXL_FAILURE("PAM: unknown header keyword: %s", unknown_header);
+ }
+ }
+ size_t num_channels = header->is_gray ? 1 : 3;
+ if (header->has_alpha) num_channels++;
+ if (num_channels + header->ec_types.size() != depth) {
+ return JXL_FAILURE("PAM: bad DEPTH");
+ }
+ if (max_val == 0 || max_val >= 65536) {
+ return JXL_FAILURE("PAM: bad MAXVAL");
+ }
+ // e.g. When `max_val` is 1 , we want 1 bit:
+ header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
+ if ((1u << header->bits_per_sample) - 1 != max_val)
+ return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
+ // PAM does not pack bits as in PBM.
+
+ header->floating_point = false;
+ header->big_endian = true;
+ *pos = pos_;
+ return true;
+ }
+
+ Status ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) {
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
+
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
+
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
+ size_t max_val;
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
+ if (max_val == 0 || max_val >= 65536) {
+ return JXL_FAILURE("PNM: bad MaxVal");
+ }
+ header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
+ if ((1u << header->bits_per_sample) - 1 != max_val)
+ return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
+ header->floating_point = false;
+ header->big_endian = true;
+
+ JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
+
+ *pos = pos_;
+ return true;
+ }
+
+ Status ParseHeaderPFM(HeaderPNM* header, const uint8_t** pos) {
+ JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
+
+ JXL_RETURN_IF_ERROR(SkipBlank());
+ JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
+
+ 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));
+ 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());
+
+ *pos = pos_;
+ return true;
+ }
+
+ const uint8_t* pos_;
+ const uint8_t* const end_;
+};
+
+Span<const uint8_t> MakeSpan(const char* str) {
+ return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str),
+ strlen(str));
+}
+
+} // namespace
+
+Status DecodeImagePNM(const Span<const uint8_t> bytes,
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+ Parser parser(bytes);
+ HeaderPNM header = {};
+ const uint8_t* pos = nullptr;
+ if (!parser.ParseHeader(&header, &pos)) return false;
+ JXL_RETURN_IF_ERROR(
+ VerifyDimensions(constraints, header.xsize, header.ysize));
+
+ if (header.bits_per_sample == 0 || header.bits_per_sample > 32) {
+ return JXL_FAILURE("PNM: bits_per_sample invalid");
+ }
+
+ // PPM specify that in the raster, the sample values are "nonlinear" (BP.709,
+ // with gamma number of 2.2). Deviate from the specification and assume
+ // `sRGB` in our implementation.
+ JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
+ header.is_gray, ppf));
+
+ ppf->info.xsize = header.xsize;
+ ppf->info.ysize = header.ysize;
+ if (header.floating_point) {
+ ppf->info.bits_per_sample = 32;
+ ppf->info.exponent_bits_per_sample = 8;
+ } else {
+ ppf->info.bits_per_sample = header.bits_per_sample;
+ ppf->info.exponent_bits_per_sample = 0;
+ }
+
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+
+ // No alpha in PNM and PFM
+ ppf->info.alpha_bits = (header.has_alpha ? ppf->info.bits_per_sample : 0);
+ ppf->info.alpha_exponent_bits = 0;
+ ppf->info.num_color_channels = (header.is_gray ? 1 : 3);
+ uint32_t num_alpha_channels = (header.has_alpha ? 1 : 0);
+ uint32_t num_interleaved_channels =
+ ppf->info.num_color_channels + num_alpha_channels;
+ ppf->info.num_extra_channels = num_alpha_channels + header.ec_types.size();
+
+ for (auto type : header.ec_types) {
+ PackedExtraChannel pec;
+ pec.ec_info.bits_per_sample = ppf->info.bits_per_sample;
+ pec.ec_info.type = type;
+ ppf->extra_channels_info.emplace_back(std::move(pec));
+ }
+
+ JxlDataType data_type;
+ if (header.floating_point) {
+ // There's no float16 pnm version.
+ data_type = JXL_TYPE_FLOAT;
+ } else {
+ if (header.bits_per_sample > 8) {
+ data_type = JXL_TYPE_UINT16;
+ } else {
+ data_type = JXL_TYPE_UINT8;
+ }
+ }
+
+ const JxlPixelFormat format{
+ /*num_channels=*/num_interleaved_channels,
+ /*data_type=*/data_type,
+ /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
+ /*align=*/0,
+ };
+ const JxlPixelFormat ec_format{1, format.data_type, format.endianness, 0};
+ ppf->frames.clear();
+ ppf->frames.emplace_back(header.xsize, header.ysize, format);
+ auto* frame = &ppf->frames.back();
+ for (size_t i = 0; i < header.ec_types.size(); ++i) {
+ frame->extra_channels.emplace_back(header.xsize, header.ysize, ec_format);
+ }
+ 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");
+ }
+
+ uint8_t* out = reinterpret_cast<uint8_t*>(frame->color.pixels());
+ std::vector<uint8_t*> ec_out(header.ec_types.size());
+ for (size_t i = 0; i < ec_out.size(); ++i) {
+ ec_out[i] = reinterpret_cast<uint8_t*>(frame->extra_channels[i].pixels());
+ }
+ if (ec_out.empty()) {
+ const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped
+ for (size_t y = 0; y < header.ysize; ++y) {
+ size_t y_in = flipped_y ? header.ysize - 1 - y : y;
+ const uint8_t* row_in = &pos[y_in * frame->color.stride];
+ uint8_t* row_out = &out[y * frame->color.stride];
+ memcpy(row_out, row_in, frame->color.stride);
+ }
+ } else {
+ size_t pwidth = PackedImage::BitsPerChannel(data_type) / 8;
+ for (size_t y = 0; y < header.ysize; ++y) {
+ for (size_t x = 0; x < header.xsize; ++x) {
+ memcpy(out, pos, frame->color.pixel_stride());
+ out += frame->color.pixel_stride();
+ pos += frame->color.pixel_stride();
+ for (auto& p : ec_out) {
+ memcpy(p, pos, pwidth);
+ pos += pwidth;
+ p += pwidth;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+void TestCodecPNM() {
+ size_t u = 77777; // Initialized to wrong value.
+ double d = 77.77;
+// Failing to parse invalid strings results in a crash if `JXL_CRASH_ON_ERROR`
+// is defined and hence the tests fail. Therefore we only run these tests if
+// `JXL_CRASH_ON_ERROR` is not defined.
+#ifndef JXL_CRASH_ON_ERROR
+ JXL_CHECK(false == Parser(MakeSpan("")).ParseUnsigned(&u));
+ JXL_CHECK(false == Parser(MakeSpan("+")).ParseUnsigned(&u));
+ JXL_CHECK(false == Parser(MakeSpan("-")).ParseUnsigned(&u));
+ JXL_CHECK(false == Parser(MakeSpan("A")).ParseUnsigned(&u));
+
+ JXL_CHECK(false == Parser(MakeSpan("")).ParseSigned(&d));
+ JXL_CHECK(false == Parser(MakeSpan("+")).ParseSigned(&d));
+ JXL_CHECK(false == Parser(MakeSpan("-")).ParseSigned(&d));
+ JXL_CHECK(false == Parser(MakeSpan("A")).ParseSigned(&d));
+#endif
+ JXL_CHECK(true == Parser(MakeSpan("1")).ParseUnsigned(&u));
+ JXL_CHECK(u == 1);
+
+ JXL_CHECK(true == Parser(MakeSpan("32")).ParseUnsigned(&u));
+ JXL_CHECK(u == 32);
+
+ JXL_CHECK(true == Parser(MakeSpan("1")).ParseSigned(&d));
+ JXL_CHECK(d == 1.0);
+ JXL_CHECK(true == Parser(MakeSpan("+2")).ParseSigned(&d));
+ JXL_CHECK(d == 2.0);
+ JXL_CHECK(true == Parser(MakeSpan("-3")).ParseSigned(&d));
+ JXL_CHECK(std::abs(d - -3.0) < 1E-15);
+ JXL_CHECK(true == Parser(MakeSpan("3.141592")).ParseSigned(&d));
+ JXL_CHECK(std::abs(d - 3.141592) < 1E-15);
+ JXL_CHECK(true == Parser(MakeSpan("-3.141592")).ParseSigned(&d));
+ JXL_CHECK(std::abs(d - -3.141592) < 1E-15);
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/dec/pnm.h b/third_party/jpeg-xl/lib/extras/dec/pnm.h
new file mode 100644
index 0000000000..0745b2f20d
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pnm.h
@@ -0,0 +1,41 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_PNM_H_
+#define LIB_EXTRAS_DEC_PNM_H_
+
+// Decodes PBM/PGM/PPM/PFM pixels in memory.
+
+#include <stddef.h>
+#include <stdint.h>
+
+// TODO(janwas): workaround for incorrect Win64 codegen (cause unknown)
+#include <hwy/highway.h>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+// Decodes `bytes` into `ppf`. color_hints may specify "color_space", which
+// defaults to sRGB.
+Status DecodeImagePNM(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
+
+void TestCodecPNM();
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_PNM_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/apng.cc b/third_party/jpeg-xl/lib/extras/enc/apng.cc
new file mode 100644
index 0000000000..79d083349d
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/apng.cc
@@ -0,0 +1,371 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/apng.h"
+
+// Parts of this code are taken from apngdis, which has the following license:
+/* APNG Disassembler 2.8
+ *
+ * Deconstructs APNG files into individual frames.
+ *
+ * http://apngdis.sourceforge.net
+ *
+ * Copyright (c) 2010-2015 Max Stepin
+ * maxst at users.sourceforge.net
+ *
+ * zlib license
+ * ------------
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty. In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ * claim that you wrote the original software. If you use this software
+ * in a product, an acknowledgment in the product documentation would be
+ * appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ * misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ *
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/exif.h"
+#include "lib/jxl/base/byte_order.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "png.h" /* original (unpatched) libpng is ok */
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
+ 0x66, 0x00, 0x00};
+
+class APNGEncoder : public Encoder {
+ public:
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ std::vector<JxlPixelFormat> formats;
+ for (const uint32_t num_channels : {1, 2, 3, 4}) {
+ for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) {
+ for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
+ formats.push_back(
+ JxlPixelFormat{num_channels, data_type, endianness, /*align=*/0});
+ }
+ }
+ }
+ return formats;
+ }
+ Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
+ ThreadPool* pool) const override {
+ JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
+ encoded_image->icc.clear();
+ encoded_image->bitstreams.resize(1);
+ return EncodePackedPixelFileToAPNG(ppf, pool,
+ &encoded_image->bitstreams.front());
+ }
+
+ private:
+ Status EncodePackedPixelFileToAPNG(const PackedPixelFile& ppf,
+ ThreadPool* pool,
+ std::vector<uint8_t>* bytes) const;
+};
+
+static void PngWrite(png_structp png_ptr, png_bytep data, png_size_t length) {
+ std::vector<uint8_t>* bytes =
+ static_cast<std::vector<uint8_t>*>(png_get_io_ptr(png_ptr));
+ bytes->insert(bytes->end(), data, data + length);
+}
+
+// Stores XMP and EXIF/IPTC into key/value strings for PNG
+class BlobsWriterPNG {
+ public:
+ static Status Encode(const PackedMetadata& blobs,
+ std::vector<std::string>* strings) {
+ if (!blobs.exif.empty()) {
+ // PNG viewers typically ignore Exif orientation but not all of them do
+ // (and e.g. cjxl doesn't), so we overwrite the Exif orientation to the
+ // identity to avoid repeated orientation.
+ std::vector<uint8_t> exif = blobs.exif;
+ ResetExifOrientation(exif);
+ // By convention, the data is prefixed with "Exif\0\0" when stored in
+ // the legacy (and non-standard) "Raw profile type exif" text chunk
+ // currently used here.
+ // TODO: Store Exif data in an eXIf chunk instead, which always begins
+ // with the TIFF header.
+ if (exif.size() >= sizeof kExifSignature &&
+ memcmp(exif.data(), kExifSignature, sizeof kExifSignature) != 0) {
+ exif.insert(exif.begin(), kExifSignature,
+ kExifSignature + sizeof kExifSignature);
+ }
+ 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()) {
+ // TODO: Store XMP data in an "XML:com.adobe.xmp" text chunk instead.
+ 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 std::vector<uint8_t>& bytes,
+ std::vector<std::string>* 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;
+ }
+};
+
+void MaybeAddCICP(JxlColorEncoding c_enc, png_structp png_ptr,
+ png_infop info_ptr) {
+ png_byte cicp_data[4] = {};
+ png_unknown_chunk cicp_chunk;
+ if (c_enc.color_space != JXL_COLOR_SPACE_RGB) {
+ return;
+ }
+ if (c_enc.primaries == JXL_PRIMARIES_P3) {
+ if (c_enc.white_point == JXL_WHITE_POINT_D65) {
+ cicp_data[0] = 12;
+ } else if (c_enc.white_point == JXL_WHITE_POINT_DCI) {
+ cicp_data[0] = 11;
+ } else {
+ return;
+ }
+ } else if (c_enc.primaries != JXL_PRIMARIES_CUSTOM &&
+ c_enc.white_point == JXL_WHITE_POINT_D65) {
+ cicp_data[0] = static_cast<png_byte>(c_enc.primaries);
+ } else {
+ return;
+ }
+ if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_UNKNOWN ||
+ c_enc.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) {
+ return;
+ }
+ cicp_data[1] = static_cast<png_byte>(c_enc.transfer_function);
+ cicp_data[2] = 0;
+ cicp_data[3] = 1;
+ cicp_chunk.data = cicp_data;
+ cicp_chunk.size = sizeof(cicp_data);
+ cicp_chunk.location = PNG_HAVE_PLTE;
+ memcpy(cicp_chunk.name, "cICP", 5);
+ png_set_keep_unknown_chunks(png_ptr, 3,
+ reinterpret_cast<const png_byte*>("cICP"), 1);
+ png_set_unknown_chunks(png_ptr, info_ptr, &cicp_chunk, 1);
+}
+
+Status APNGEncoder::EncodePackedPixelFileToAPNG(
+ const PackedPixelFile& ppf, ThreadPool* pool,
+ std::vector<uint8_t>* bytes) const {
+ size_t xsize = ppf.info.xsize;
+ size_t ysize = ppf.info.ysize;
+ bool has_alpha = ppf.info.alpha_bits != 0;
+ bool is_gray = ppf.info.num_color_channels == 1;
+ size_t color_channels = ppf.info.num_color_channels;
+ size_t num_channels = color_channels + (has_alpha ? 1 : 0);
+ size_t num_samples = num_channels * xsize * ysize;
+
+ if (!ppf.info.have_animation && ppf.frames.size() != 1) {
+ return JXL_FAILURE("Invalid number of frames");
+ }
+
+ size_t count = 0;
+ size_t anim_chunks = 0;
+
+ for (const auto& frame : ppf.frames) {
+ JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info));
+
+ const PackedImage& color = frame.color;
+ const JxlPixelFormat format = color.format;
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels());
+ size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type);
+ size_t bytes_per_sample = data_bits_per_sample / 8;
+ size_t out_bytes_per_sample = bytes_per_sample > 1 ? 2 : 1;
+ size_t out_stride = xsize * num_channels * out_bytes_per_sample;
+ size_t out_size = ysize * out_stride;
+ std::vector<uint8_t> out(out_size);
+
+ if (format.data_type == JXL_TYPE_UINT8) {
+ if (ppf.info.bits_per_sample < 8) {
+ float mul = 255.0 / ((1u << ppf.info.bits_per_sample) - 1);
+ for (size_t i = 0; i < num_samples; ++i) {
+ out[i] = static_cast<uint8_t>(in[i] * mul + 0.5);
+ }
+ } else {
+ memcpy(&out[0], in, out_size);
+ }
+ } else if (format.data_type == JXL_TYPE_UINT16) {
+ if (ppf.info.bits_per_sample < 16 ||
+ format.endianness != JXL_BIG_ENDIAN) {
+ float mul = 65535.0 / ((1u << ppf.info.bits_per_sample) - 1);
+ const uint8_t* p_in = in;
+ uint8_t* p_out = out.data();
+ for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) {
+ uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE16(p_in)
+ : LoadLE16(p_in));
+ StoreBE16(static_cast<uint32_t>(val * mul + 0.5), p_out);
+ }
+ } else {
+ memcpy(&out[0], in, out_size);
+ }
+ }
+ png_structp png_ptr;
+ png_infop info_ptr;
+
+ png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+
+ if (!png_ptr) return JXL_FAILURE("Could not init png encoder");
+
+ info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr) return JXL_FAILURE("Could not init png info struct");
+
+ png_set_write_fn(png_ptr, bytes, PngWrite, NULL);
+ png_set_flush(png_ptr, 0);
+
+ int width = xsize;
+ int height = ysize;
+
+ png_byte color_type = (is_gray ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB);
+ if (has_alpha) color_type |= PNG_COLOR_MASK_ALPHA;
+ png_byte bit_depth = out_bytes_per_sample * 8;
+
+ png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type,
+ PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE,
+ PNG_FILTER_TYPE_BASE);
+ if (count == 0) {
+ MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr);
+ if (!ppf.icc.empty()) {
+ png_set_benign_errors(png_ptr, 1);
+ png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(), ppf.icc.size());
+ }
+ std::vector<std::string> textstrings;
+ JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings));
+ for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) {
+ png_text text;
+ text.key = const_cast<png_charp>(textstrings[kk].c_str());
+ text.text = const_cast<png_charp>(textstrings[kk + 1].c_str());
+ text.compression = PNG_TEXT_COMPRESSION_zTXt;
+ png_set_text(png_ptr, info_ptr, &text, 1);
+ }
+
+ png_write_info(png_ptr, info_ptr);
+ } else {
+ // fake writing a header, otherwise libpng gets confused
+ size_t pos = bytes->size();
+ png_write_info(png_ptr, info_ptr);
+ bytes->resize(pos);
+ }
+
+ if (ppf.info.have_animation) {
+ if (count == 0) {
+ png_byte adata[8];
+ png_save_uint_32(adata, ppf.frames.size());
+ png_save_uint_32(adata + 4, ppf.info.animation.num_loops);
+ png_byte actl[5] = "acTL";
+ png_write_chunk(png_ptr, actl, adata, 8);
+ }
+ png_byte fdata[26];
+ // TODO(jon): also make this work for the non-coalesced case
+ png_save_uint_32(fdata, anim_chunks++);
+ png_save_uint_32(fdata + 4, width);
+ png_save_uint_32(fdata + 8, height);
+ png_save_uint_32(fdata + 12, 0);
+ png_save_uint_32(fdata + 16, 0);
+ png_save_uint_16(fdata + 20, frame.frame_info.duration *
+ ppf.info.animation.tps_denominator);
+ png_save_uint_16(fdata + 22, ppf.info.animation.tps_numerator);
+ fdata[24] = 1;
+ fdata[25] = 0;
+ png_byte fctl[5] = "fcTL";
+ png_write_chunk(png_ptr, fctl, fdata, 26);
+ }
+
+ std::vector<uint8_t*> rows(height);
+ for (int y = 0; y < height; ++y) {
+ rows[y] = out.data() + y * out_stride;
+ }
+
+ png_write_flush(png_ptr);
+ const size_t pos = bytes->size();
+ png_write_image(png_ptr, &rows[0]);
+ png_write_flush(png_ptr);
+ if (count > 0) {
+ std::vector<uint8_t> fdata(4);
+ png_save_uint_32(fdata.data(), anim_chunks++);
+ size_t p = pos;
+ while (p + 8 < bytes->size()) {
+ size_t len = png_get_uint_32(bytes->data() + p);
+ JXL_ASSERT(bytes->operator[](p + 4) == 'I');
+ JXL_ASSERT(bytes->operator[](p + 5) == 'D');
+ JXL_ASSERT(bytes->operator[](p + 6) == 'A');
+ JXL_ASSERT(bytes->operator[](p + 7) == 'T');
+ fdata.insert(fdata.end(), bytes->data() + p + 8,
+ bytes->data() + p + 8 + len);
+ p += len + 12;
+ }
+ bytes->resize(pos);
+
+ png_byte fdat[5] = "fdAT";
+ png_write_chunk(png_ptr, fdat, fdata.data(), fdata.size());
+ }
+
+ count++;
+ if (count == ppf.frames.size() || !ppf.info.have_animation) {
+ png_write_end(png_ptr, NULL);
+ }
+
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ }
+
+ return true;
+}
+
+} // namespace
+
+std::unique_ptr<Encoder> GetAPNGEncoder() {
+ return jxl::make_unique<APNGEncoder>();
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/apng.h b/third_party/jpeg-xl/lib/extras/enc/apng.h
new file mode 100644
index 0000000000..2a2139c8fa
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/apng.h
@@ -0,0 +1,23 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_APNG_H_
+#define LIB_EXTRAS_ENC_APNG_H_
+
+// Encodes APNG images in memory.
+
+#include <memory>
+
+#include "lib/extras/enc/encode.h"
+
+namespace jxl {
+namespace extras {
+
+std::unique_ptr<Encoder> GetAPNGEncoder();
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_APNG_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/encode.cc b/third_party/jpeg-xl/lib/extras/enc/encode.cc
new file mode 100644
index 0000000000..9ffba9d0dd
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/encode.cc
@@ -0,0 +1,170 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/encode.h"
+
+#include <locale>
+
+#if JPEGXL_ENABLE_APNG
+#include "lib/extras/enc/apng.h"
+#endif
+#if JPEGXL_ENABLE_EXR
+#include "lib/extras/enc/exr.h"
+#endif
+#if JPEGXL_ENABLE_JPEG
+#include "lib/extras/enc/jpg.h"
+#endif
+#include "lib/extras/enc/npy.h"
+#include "lib/extras/enc/pgx.h"
+#include "lib/extras/enc/pnm.h"
+#include "lib/jxl/base/printf_macros.h"
+
+namespace jxl {
+namespace extras {
+
+Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) {
+ if (info.xsize == 0 || info.ysize == 0) {
+ return JXL_FAILURE("Empty image");
+ }
+ if (info.num_color_channels != 1 && info.num_color_channels != 3) {
+ return JXL_FAILURE("Invalid number of color channels");
+ }
+ if (info.alpha_bits > 0 && info.alpha_bits != info.bits_per_sample) {
+ return JXL_FAILURE("Alpha bit depth does not match image bit depth");
+ }
+ if (info.orientation != JXL_ORIENT_IDENTITY) {
+ return JXL_FAILURE("Orientation must be identity");
+ }
+ return true;
+}
+
+Status Encoder::VerifyFormat(const JxlPixelFormat& format) const {
+ for (auto f : AcceptedFormats()) {
+ if (f.num_channels != format.num_channels) continue;
+ if (f.data_type != format.data_type) continue;
+ if (f.data_type == JXL_TYPE_UINT8 || f.endianness == format.endianness) {
+ return true;
+ }
+ }
+ return JXL_FAILURE("Format is not in the list of accepted formats.");
+}
+
+Status Encoder::VerifyBitDepth(JxlDataType data_type, uint32_t bits_per_sample,
+ uint32_t exponent_bits) {
+ if ((data_type == JXL_TYPE_UINT8 &&
+ (bits_per_sample == 0 || bits_per_sample > 8 || exponent_bits != 0)) ||
+ (data_type == JXL_TYPE_UINT16 &&
+ (bits_per_sample <= 8 || bits_per_sample > 16 || exponent_bits != 0)) ||
+ (data_type == JXL_TYPE_FLOAT16 &&
+ (bits_per_sample != 16 || exponent_bits != 5)) ||
+ (data_type == JXL_TYPE_FLOAT &&
+ (bits_per_sample != 32 || exponent_bits != 8))) {
+ return JXL_FAILURE(
+ "Incompatible data_type %d and bit depth %u with exponent bits %u",
+ (int)data_type, bits_per_sample, exponent_bits);
+ }
+ return true;
+}
+
+Status Encoder::VerifyImageSize(const PackedImage& image,
+ const JxlBasicInfo& info) {
+ if (image.pixels() == nullptr) {
+ return JXL_FAILURE("Invalid image.");
+ }
+ if (image.stride != image.xsize * image.pixel_stride()) {
+ return JXL_FAILURE("Invalid image stride.");
+ }
+ if (image.pixels_size != image.ysize * image.stride) {
+ return JXL_FAILURE("Invalid image size.");
+ }
+ size_t info_num_channels =
+ (info.num_color_channels + (info.alpha_bits > 0 ? 1 : 0));
+ if (image.xsize != info.xsize || image.ysize != info.ysize ||
+ image.format.num_channels != info_num_channels) {
+ return JXL_FAILURE("Frame size does not match image size");
+ }
+ return true;
+}
+
+Status Encoder::VerifyPackedImage(const PackedImage& image,
+ const JxlBasicInfo& info) const {
+ JXL_RETURN_IF_ERROR(VerifyImageSize(image, info));
+ JXL_RETURN_IF_ERROR(VerifyFormat(image.format));
+ JXL_RETURN_IF_ERROR(VerifyBitDepth(image.format.data_type,
+ info.bits_per_sample,
+ info.exponent_bits_per_sample));
+ return true;
+}
+
+Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats,
+ const JxlBasicInfo& basic_info, JxlPixelFormat* format) {
+ const size_t original_bit_depth = basic_info.bits_per_sample;
+ size_t current_bit_depth = 0;
+ size_t num_alpha_channels = (basic_info.alpha_bits != 0 ? 1 : 0);
+ size_t num_channels = basic_info.num_color_channels + num_alpha_channels;
+ for (;;) {
+ for (const JxlPixelFormat& candidate : accepted_formats) {
+ if (candidate.num_channels != num_channels) continue;
+ const size_t candidate_bit_depth =
+ PackedImage::BitsPerChannel(candidate.data_type);
+ if (
+ // Candidate bit depth is less than what we have and still enough
+ (original_bit_depth <= candidate_bit_depth &&
+ candidate_bit_depth < current_bit_depth) ||
+ // Or larger than the too-small bit depth we currently have
+ (current_bit_depth < candidate_bit_depth &&
+ current_bit_depth < original_bit_depth)) {
+ *format = candidate;
+ current_bit_depth = candidate_bit_depth;
+ }
+ }
+ if (current_bit_depth == 0) {
+ if (num_channels > basic_info.num_color_channels) {
+ // Try dropping the alpha channel.
+ --num_channels;
+ continue;
+ }
+ return JXL_FAILURE("no appropriate format found");
+ }
+ break;
+ }
+ if (current_bit_depth < original_bit_depth) {
+ JXL_WARNING("encoding %" PRIuS "-bit original to %" PRIuS " bits",
+ original_bit_depth, current_bit_depth);
+ }
+ return true;
+}
+
+std::unique_ptr<Encoder> Encoder::FromExtension(std::string extension) {
+ std::transform(
+ extension.begin(), extension.end(), extension.begin(),
+ [](char c) { return std::tolower(c, std::locale::classic()); });
+#if JPEGXL_ENABLE_APNG
+ if (extension == ".png" || extension == ".apng") return GetAPNGEncoder();
+#endif
+
+#if JPEGXL_ENABLE_JPEG
+ if (extension == ".jpg") return GetJPEGEncoder();
+ if (extension == ".jpeg") return GetJPEGEncoder();
+#endif
+
+ if (extension == ".npy") return GetNumPyEncoder();
+
+ if (extension == ".pgx") return GetPGXEncoder();
+
+ if (extension == ".pam") return GetPAMEncoder();
+ if (extension == ".pgm") return GetPGMEncoder();
+ if (extension == ".ppm") return GetPPMEncoder();
+ if (extension == ".pfm") return GetPFMEncoder();
+
+#if JPEGXL_ENABLE_EXR
+ if (extension == ".exr") return GetEXREncoder();
+#endif
+
+ return nullptr;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/encode.h b/third_party/jpeg-xl/lib/extras/enc/encode.h
new file mode 100644
index 0000000000..63cabaf30f
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/encode.h
@@ -0,0 +1,83 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_ENCODE_H_
+#define LIB_EXTRAS_ENC_ENCODE_H_
+
+// Facade for image encoders.
+
+#include <string>
+#include <unordered_map>
+
+#include "lib/extras/dec/decode.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+struct EncodedImage {
+ // One (if the format supports animations or the image has only one frame) or
+ // more sequential bitstreams.
+ std::vector<std::vector<uint8_t>> bitstreams;
+
+ // For each extra channel one or more sequential bitstreams.
+ std::vector<std::vector<std::vector<uint8_t>>> extra_channel_bitstreams;
+
+ std::vector<uint8_t> preview_bitstream;
+
+ // If the format does not support embedding color profiles into the bitstreams
+ // above, it will be present here, to be written as a separate file. If it
+ // does support them, this field will be empty.
+ std::vector<uint8_t> icc;
+
+ // Additional output for conformance testing, only filled in by NumPyEncoder.
+ std::vector<uint8_t> metadata;
+};
+
+class Encoder {
+ public:
+ static std::unique_ptr<Encoder> FromExtension(std::string extension);
+
+ virtual ~Encoder() = default;
+
+ virtual std::vector<JxlPixelFormat> AcceptedFormats() const = 0;
+
+ // Any existing data in encoded_image is discarded.
+ virtual Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
+ ThreadPool* pool = nullptr) const = 0;
+
+ void SetOption(std::string name, std::string value) {
+ options_[std::move(name)] = std::move(value);
+ }
+
+ static Status VerifyBasicInfo(const JxlBasicInfo& info);
+ static Status VerifyImageSize(const PackedImage& image,
+ const JxlBasicInfo& info);
+ static Status VerifyBitDepth(JxlDataType data_type, uint32_t bits_per_sample,
+ uint32_t exponent_bits);
+
+ protected:
+ const std::unordered_map<std::string, std::string>& options() const {
+ return options_;
+ }
+
+ Status VerifyFormat(const JxlPixelFormat& format) const;
+
+ Status VerifyPackedImage(const PackedImage& image,
+ const JxlBasicInfo& info) const;
+
+ private:
+ std::unordered_map<std::string, std::string> options_;
+};
+
+// TODO(sboukortt): consider exposing this as part of the C API.
+Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats,
+ const JxlBasicInfo& basic_info, JxlPixelFormat* format);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_ENCODE_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/exr.cc b/third_party/jpeg-xl/lib/extras/enc/exr.cc
new file mode 100644
index 0000000000..1d70913936
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/exr.cc
@@ -0,0 +1,200 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/exr.h"
+
+#include <ImfChromaticitiesAttribute.h>
+#include <ImfIO.h>
+#include <ImfRgbaFile.h>
+#include <ImfStandardAttributes.h>
+#include <jxl/codestream_header.h>
+
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/byte_order.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+namespace OpenEXR = OPENEXR_IMF_NAMESPACE;
+namespace Imath = IMATH_NAMESPACE;
+
+// OpenEXR::Int64 is deprecated in favor of using uint64_t directly, but using
+// uint64_t as recommended causes build failures with previous OpenEXR versions
+// on macOS, where the definition for OpenEXR::Int64 was actually not equivalent
+// to uint64_t. This alternative should work in all cases.
+using ExrInt64 = decltype(std::declval<OpenEXR::IStream>().tellg());
+
+class InMemoryOStream : public OpenEXR::OStream {
+ public:
+ // `bytes` must outlive the InMemoryOStream.
+ explicit InMemoryOStream(std::vector<uint8_t>* const bytes)
+ : OStream(/*fileName=*/""), bytes_(*bytes) {}
+
+ void write(const char c[], const int n) override {
+ if (bytes_.size() < pos_ + n) {
+ bytes_.resize(pos_ + n);
+ }
+ std::copy_n(c, n, bytes_.begin() + pos_);
+ pos_ += n;
+ }
+
+ ExrInt64 tellp() override { return pos_; }
+ void seekp(const ExrInt64 pos) override {
+ if (bytes_.size() + 1 < pos) {
+ bytes_.resize(pos - 1);
+ }
+ pos_ = pos;
+ }
+
+ private:
+ std::vector<uint8_t>& bytes_;
+ size_t pos_ = 0;
+};
+
+// Loads a Big-Endian float
+float LoadBEFloat(const uint8_t* p) {
+ uint32_t u = LoadBE32(p);
+ float result;
+ memcpy(&result, &u, 4);
+ return result;
+}
+
+// Loads a Little-Endian float
+float LoadLEFloat(const uint8_t* p) {
+ uint32_t u = LoadLE32(p);
+ float result;
+ memcpy(&result, &u, 4);
+ return result;
+}
+
+Status EncodeImageEXR(const PackedImage& image, const JxlBasicInfo& info,
+ const JxlColorEncoding& c_enc, ThreadPool* pool,
+ std::vector<uint8_t>* bytes) {
+ OpenEXR::setGlobalThreadCount(0);
+
+ const size_t xsize = info.xsize;
+ const size_t ysize = info.ysize;
+ const bool has_alpha = info.alpha_bits > 0;
+ const bool alpha_is_premultiplied = info.alpha_premultiplied;
+
+ if (info.num_color_channels != 3 ||
+ c_enc.color_space != JXL_COLOR_SPACE_RGB ||
+ c_enc.transfer_function != JXL_TRANSFER_FUNCTION_LINEAR) {
+ return JXL_FAILURE("Unsupported color encoding for OpenEXR output.");
+ }
+
+ const size_t num_channels = 3 + (has_alpha ? 1 : 0);
+ const JxlPixelFormat format = image.format;
+
+ if (format.data_type != JXL_TYPE_FLOAT) {
+ return JXL_FAILURE("Unsupported pixel format for OpenEXR output");
+ }
+
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels());
+ size_t in_stride = num_channels * 4 * xsize;
+
+ OpenEXR::Header header(xsize, ysize);
+ OpenEXR::Chromaticities chromaticities;
+ chromaticities.red =
+ Imath::V2f(c_enc.primaries_red_xy[0], c_enc.primaries_red_xy[1]);
+ chromaticities.green =
+ Imath::V2f(c_enc.primaries_green_xy[0], c_enc.primaries_green_xy[1]);
+ chromaticities.blue =
+ Imath::V2f(c_enc.primaries_blue_xy[0], c_enc.primaries_blue_xy[1]);
+ chromaticities.white =
+ Imath::V2f(c_enc.white_point_xy[0], c_enc.white_point_xy[1]);
+ OpenEXR::addChromaticities(header, chromaticities);
+ OpenEXR::addWhiteLuminance(header, info.intensity_target);
+
+ auto loadFloat =
+ format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat : LoadLEFloat;
+ auto loadAlpha =
+ has_alpha ? loadFloat : [](const uint8_t* p) -> float { return 1.0f; };
+
+ // Ensure that the destructor of RgbaOutputFile has run before we look at the
+ // size of `bytes`.
+ {
+ InMemoryOStream os(bytes);
+ OpenEXR::RgbaOutputFile output(
+ os, header, has_alpha ? OpenEXR::WRITE_RGBA : OpenEXR::WRITE_RGB);
+ // How many rows to write at once. Again, the OpenEXR documentation
+ // recommends writing the whole image in one call.
+ const int y_chunk_size = ysize;
+ std::vector<OpenEXR::Rgba> output_rows(xsize * y_chunk_size);
+
+ for (size_t start_y = 0; start_y < ysize; start_y += y_chunk_size) {
+ // Inclusive.
+ const size_t end_y = std::min(start_y + y_chunk_size - 1, ysize - 1);
+ output.setFrameBuffer(output_rows.data() - start_y * xsize,
+ /*xStride=*/1, /*yStride=*/xsize);
+ for (size_t y = start_y; y <= end_y; ++y) {
+ const uint8_t* in_row = &in[(y - start_y) * in_stride];
+ OpenEXR::Rgba* const JXL_RESTRICT row_data =
+ &output_rows[(y - start_y) * xsize];
+ for (size_t x = 0; x < xsize; ++x) {
+ const uint8_t* in_pixel = &in_row[4 * num_channels * x];
+ float r = loadFloat(&in_pixel[0]);
+ float g = loadFloat(&in_pixel[4]);
+ float b = loadFloat(&in_pixel[8]);
+ const float alpha = loadAlpha(&in_pixel[12]);
+ if (!alpha_is_premultiplied) {
+ r *= alpha;
+ g *= alpha;
+ b *= alpha;
+ }
+ row_data[x] = OpenEXR::Rgba(r, g, b, alpha);
+ }
+ }
+ output.writePixels(/*numScanLines=*/end_y - start_y + 1);
+ }
+ }
+
+ return true;
+}
+
+class EXREncoder : public Encoder {
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ std::vector<JxlPixelFormat> formats;
+ for (const uint32_t num_channels : {1, 2, 3, 4}) {
+ for (const JxlDataType data_type : {JXL_TYPE_FLOAT, JXL_TYPE_FLOAT16}) {
+ for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
+ formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
+ /*data_type=*/data_type,
+ /*endianness=*/endianness,
+ /*align=*/0});
+ }
+ }
+ }
+ return formats;
+ }
+ Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
+ ThreadPool* pool = nullptr) const override {
+ JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
+ encoded_image->icc.clear();
+ encoded_image->bitstreams.clear();
+ encoded_image->bitstreams.reserve(ppf.frames.size());
+ for (const auto& frame : ppf.frames) {
+ JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info));
+ encoded_image->bitstreams.emplace_back();
+ JXL_RETURN_IF_ERROR(EncodeImageEXR(frame.color, ppf.info,
+ ppf.color_encoding, pool,
+ &encoded_image->bitstreams.back()));
+ }
+ return true;
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Encoder> GetEXREncoder() {
+ return jxl::make_unique<EXREncoder>();
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/exr.h b/third_party/jpeg-xl/lib/extras/enc/exr.h
new file mode 100644
index 0000000000..1baaa0272f
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/exr.h
@@ -0,0 +1,23 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_EXR_H_
+#define LIB_EXTRAS_ENC_EXR_H_
+
+// Encodes OpenEXR images in memory.
+
+#include <memory>
+
+#include "lib/extras/enc/encode.h"
+
+namespace jxl {
+namespace extras {
+
+std::unique_ptr<Encoder> GetEXREncoder();
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_EXR_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/jpegli.cc b/third_party/jpeg-xl/lib/extras/enc/jpegli.cc
new file mode 100644
index 0000000000..43cf32a19c
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/jpegli.cc
@@ -0,0 +1,503 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/jpegli.h"
+
+#include <jxl/codestream_header.h>
+#include <setjmp.h>
+#include <stdint.h>
+
+#include "lib/extras/enc/encode.h"
+#include "lib/jpegli/encode.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_xyb.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+void MyErrorExit(j_common_ptr cinfo) {
+ jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data);
+ (*cinfo->err->output_message)(cinfo);
+ jpegli_destroy_compress(reinterpret_cast<j_compress_ptr>(cinfo));
+ longjmp(*env, 1);
+}
+
+Status VerifyInput(const PackedPixelFile& ppf) {
+ const JxlBasicInfo& info = ppf.info;
+ JXL_RETURN_IF_ERROR(Encoder::VerifyBasicInfo(info));
+ if (info.alpha_bits > 0) {
+ return JXL_FAILURE("Alpha is not supported for JPEG output.");
+ }
+ if (ppf.frames.size() != 1) {
+ return JXL_FAILURE("JPEG input must have exactly one frame.");
+ }
+ const PackedImage& image = ppf.frames[0].color;
+ JXL_RETURN_IF_ERROR(Encoder::VerifyImageSize(image, info));
+ if (image.format.data_type == JXL_TYPE_FLOAT16) {
+ return JXL_FAILURE("FLOAT16 input is not supported.");
+ }
+ JXL_RETURN_IF_ERROR(Encoder::VerifyBitDepth(image.format.data_type,
+ info.bits_per_sample,
+ info.exponent_bits_per_sample));
+ if ((image.format.data_type == JXL_TYPE_UINT8 && info.bits_per_sample != 8) ||
+ (image.format.data_type == JXL_TYPE_UINT16 &&
+ info.bits_per_sample != 16)) {
+ return JXL_FAILURE("Only full bit depth unsigned types are supported.");
+ }
+ return true;
+}
+
+Status GetColorEncoding(const PackedPixelFile& ppf,
+ ColorEncoding* color_encoding) {
+ if (!ppf.icc.empty()) {
+ PaddedBytes icc;
+ icc.assign(ppf.icc.data(), ppf.icc.data() + ppf.icc.size());
+ JXL_RETURN_IF_ERROR(color_encoding->SetICC(std::move(icc)));
+ } else {
+ JXL_RETURN_IF_ERROR(ConvertExternalToInternalColorEncoding(
+ ppf.color_encoding, color_encoding));
+ }
+ if (color_encoding->ICC().empty()) {
+ return JXL_FAILURE("Invalid color encoding.");
+ }
+ return true;
+}
+
+bool HasICCProfile(const std::vector<uint8_t>& app_data) {
+ size_t pos = 0;
+ while (pos < app_data.size()) {
+ if (pos + 16 > app_data.size()) return false;
+ uint8_t marker = app_data[pos + 1];
+ size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2;
+ if (marker == 0xe2 && memcmp(&app_data[pos + 4], "ICC_PROFILE", 12) == 0) {
+ return true;
+ }
+ pos += marker_len;
+ }
+ return false;
+}
+
+Status WriteAppData(j_compress_ptr cinfo,
+ const std::vector<uint8_t>& app_data) {
+ size_t pos = 0;
+ while (pos < app_data.size()) {
+ if (pos + 4 > app_data.size()) {
+ return JXL_FAILURE("Incomplete APP header.");
+ }
+ uint8_t marker = app_data[pos + 1];
+ size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2;
+ if (app_data[pos] != 0xff || marker < 0xe0 || marker > 0xef) {
+ return JXL_FAILURE("Invalid APP marker %02x %02x", app_data[pos], marker);
+ }
+ if (marker_len <= 4) {
+ return JXL_FAILURE("Invalid APP marker length.");
+ }
+ if (pos + marker_len > app_data.size()) {
+ return JXL_FAILURE("Incomplete APP data");
+ }
+ jpegli_write_marker(cinfo, marker, &app_data[pos + 4], marker_len - 4);
+ pos += marker_len;
+ }
+ return true;
+}
+
+static constexpr int kICCMarker = 0xe2;
+constexpr unsigned char kICCSignature[12] = {
+ 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00};
+static constexpr uint8_t kUnknownTf = 2;
+static constexpr unsigned char kCICPTagSignature[4] = {0x63, 0x69, 0x63, 0x70};
+static constexpr size_t kCICPTagSize = 12;
+
+bool FindCICPTag(const uint8_t* icc_data, size_t len, bool is_first_chunk,
+ size_t* cicp_offset, size_t* cicp_length, uint8_t* cicp_tag,
+ size_t* cicp_pos) {
+ if (is_first_chunk) {
+ // Look up the offset of the CICP tag from the first chunk of ICC data.
+ if (len < 132) {
+ return false;
+ }
+ uint32_t tag_count = LoadBE32(&icc_data[128]);
+ if (len < 132 + 12 * tag_count) {
+ return false;
+ }
+ for (uint32_t i = 0; i < tag_count; ++i) {
+ if (memcmp(&icc_data[132 + 12 * i], kCICPTagSignature, 4) == 0) {
+ *cicp_offset = LoadBE32(&icc_data[136 + 12 * i]);
+ *cicp_length = LoadBE32(&icc_data[140 + 12 * i]);
+ }
+ }
+ if (*cicp_length < kCICPTagSize) {
+ return false;
+ }
+ }
+ if (*cicp_offset < len) {
+ size_t n_bytes = std::min(len - *cicp_offset, kCICPTagSize - *cicp_pos);
+ memcpy(&cicp_tag[*cicp_pos], &icc_data[*cicp_offset], n_bytes);
+ *cicp_pos += n_bytes;
+ *cicp_offset = 0;
+ } else {
+ *cicp_offset -= len;
+ }
+ return true;
+}
+
+uint8_t LookupCICPTransferFunctionFromAppData(const uint8_t* app_data,
+ size_t len) {
+ size_t last_index = 0;
+ size_t cicp_offset = 0;
+ size_t cicp_length = 0;
+ uint8_t cicp_tag[kCICPTagSize] = {};
+ size_t cicp_pos = 0;
+ size_t pos = 0;
+ while (pos < len) {
+ const uint8_t* marker = &app_data[pos];
+ if (pos + 4 > len) {
+ return kUnknownTf;
+ }
+ size_t marker_size = (marker[2] << 8) + marker[3] + 2;
+ if (pos + marker_size > len) {
+ return kUnknownTf;
+ }
+ if (marker_size < 18 || marker[0] != 0xff || marker[1] != kICCMarker ||
+ memcmp(&marker[4], kICCSignature, 12) != 0) {
+ pos += marker_size;
+ continue;
+ }
+ uint8_t index = marker[16];
+ uint8_t total = marker[17];
+ const uint8_t* payload = marker + 18;
+ const size_t payload_size = marker_size - 18;
+ if (index != last_index + 1 || index > total) {
+ return kUnknownTf;
+ }
+ if (!FindCICPTag(payload, payload_size, last_index == 0, &cicp_offset,
+ &cicp_length, &cicp_tag[0], &cicp_pos)) {
+ return kUnknownTf;
+ }
+ if (cicp_pos == kCICPTagSize) {
+ break;
+ }
+ ++last_index;
+ }
+ if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) {
+ return cicp_tag[9];
+ }
+ return kUnknownTf;
+}
+
+uint8_t LookupCICPTransferFunctionFromICCProfile(const uint8_t* icc_data,
+ size_t len) {
+ size_t cicp_offset = 0;
+ size_t cicp_length = 0;
+ uint8_t cicp_tag[kCICPTagSize] = {};
+ size_t cicp_pos = 0;
+ if (!FindCICPTag(icc_data, len, true, &cicp_offset, &cicp_length,
+ &cicp_tag[0], &cicp_pos)) {
+ return kUnknownTf;
+ }
+ if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) {
+ return cicp_tag[9];
+ }
+ return kUnknownTf;
+}
+
+JpegliDataType ConvertDataType(JxlDataType type) {
+ switch (type) {
+ case JXL_TYPE_UINT8:
+ return JPEGLI_TYPE_UINT8;
+ case JXL_TYPE_UINT16:
+ return JPEGLI_TYPE_UINT16;
+ case JXL_TYPE_FLOAT:
+ return JPEGLI_TYPE_FLOAT;
+ default:
+ return JPEGLI_TYPE_UINT8;
+ }
+}
+
+JpegliEndianness ConvertEndianness(JxlEndianness endianness) {
+ switch (endianness) {
+ case JXL_NATIVE_ENDIAN:
+ return JPEGLI_NATIVE_ENDIAN;
+ case JXL_LITTLE_ENDIAN:
+ return JPEGLI_LITTLE_ENDIAN;
+ case JXL_BIG_ENDIAN:
+ return JPEGLI_BIG_ENDIAN;
+ default:
+ return JPEGLI_NATIVE_ENDIAN;
+ }
+}
+
+void ToFloatRow(const uint8_t* row_in, JxlPixelFormat format, size_t len,
+ float* row_out) {
+ bool is_little_endian =
+ (format.endianness == JXL_LITTLE_ENDIAN ||
+ (format.endianness == JXL_NATIVE_ENDIAN && IsLittleEndian()));
+ static constexpr double kMul8 = 1.0 / 255.0;
+ static constexpr double kMul16 = 1.0 / 65535.0;
+ if (format.data_type == JXL_TYPE_UINT8) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = row_in[x] * kMul8;
+ }
+ } else if (format.data_type == JXL_TYPE_UINT16 && is_little_endian) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = LoadLE16(&row_in[2 * x]) * kMul16;
+ }
+ } else if (format.data_type == JXL_TYPE_UINT16 && !is_little_endian) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = LoadBE16(&row_in[2 * x]) * kMul16;
+ }
+ } else if (format.data_type == JXL_TYPE_FLOAT && is_little_endian) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = LoadLEFloat(&row_in[4 * x]);
+ }
+ } else if (format.data_type == JXL_TYPE_FLOAT && !is_little_endian) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = LoadBEFloat(&row_in[4 * x]);
+ }
+ }
+}
+
+Status EncodeJpegToTargetSize(const PackedPixelFile& ppf,
+ const JpegSettings& jpeg_settings,
+ size_t target_size, ThreadPool* pool,
+ std::vector<uint8_t>* output) {
+ output->clear();
+ size_t best_error = std::numeric_limits<size_t>::max();
+ float distance0 = -1.0f;
+ float distance1 = -1.0f;
+ float distance = 1.0f;
+ for (int step = 0; step < 15; ++step) {
+ JpegSettings settings = jpeg_settings;
+ settings.libjpeg_quality = 0;
+ settings.distance = distance;
+ settings.target_size = 0;
+ std::vector<uint8_t> compressed;
+ JXL_RETURN_IF_ERROR(EncodeJpeg(ppf, settings, pool, &compressed));
+ size_t size = compressed.size();
+ // prefer being under the target size to being over it
+ size_t error = size < target_size
+ ? target_size - size
+ : static_cast<size_t>(1.2f * (size - target_size));
+ if (error < best_error) {
+ best_error = error;
+ std::swap(*output, compressed);
+ }
+ float rel_error = size * 1.0f / target_size;
+ if (std::abs(rel_error - 1.0f) < 0.002f) {
+ break;
+ }
+ if (size < target_size) {
+ distance1 = distance;
+ } else {
+ distance0 = distance;
+ }
+ if (distance1 == -1) {
+ distance *= std::pow(rel_error, 1.5) * 1.05;
+ } else if (distance0 == -1) {
+ distance *= std::pow(rel_error, 1.5) * 0.95;
+ } else {
+ distance = 0.5 * (distance0 + distance1);
+ }
+ }
+ return true;
+}
+
+} // namespace
+
+Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings,
+ ThreadPool* pool, std::vector<uint8_t>* compressed) {
+ if (jpeg_settings.libjpeg_quality > 0) {
+ auto encoder = Encoder::FromExtension(".jpg");
+ encoder->SetOption("q", std::to_string(jpeg_settings.libjpeg_quality));
+ if (!jpeg_settings.libjpeg_chroma_subsampling.empty()) {
+ encoder->SetOption("chroma_subsampling",
+ jpeg_settings.libjpeg_chroma_subsampling);
+ }
+ EncodedImage encoded;
+ JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool));
+ size_t target_size = encoded.bitstreams[0].size();
+ return EncodeJpegToTargetSize(ppf, jpeg_settings, target_size, pool,
+ compressed);
+ }
+ if (jpeg_settings.target_size > 0) {
+ return EncodeJpegToTargetSize(ppf, jpeg_settings, jpeg_settings.target_size,
+ pool, compressed);
+ }
+ JXL_RETURN_IF_ERROR(VerifyInput(ppf));
+
+ ColorEncoding color_encoding;
+ JXL_RETURN_IF_ERROR(GetColorEncoding(ppf, &color_encoding));
+
+ ColorSpaceTransform c_transform(GetJxlCms());
+ ColorEncoding xyb_encoding;
+ if (jpeg_settings.xyb) {
+ if (ppf.info.num_color_channels != 3) {
+ return JXL_FAILURE("Only RGB input is supported in XYB mode.");
+ }
+ if (HasICCProfile(jpeg_settings.app_data)) {
+ return JXL_FAILURE("APP data ICC profile is not supported in XYB mode.");
+ }
+ const ColorEncoding& c_desired = ColorEncoding::LinearSRGB(false);
+ JXL_RETURN_IF_ERROR(
+ c_transform.Init(color_encoding, c_desired, 255.0f, ppf.info.xsize, 1));
+ xyb_encoding.SetColorSpace(jxl::ColorSpace::kXYB);
+ xyb_encoding.rendering_intent = jxl::RenderingIntent::kPerceptual;
+ JXL_RETURN_IF_ERROR(xyb_encoding.CreateICC());
+ }
+ const ColorEncoding& output_encoding =
+ jpeg_settings.xyb ? xyb_encoding : color_encoding;
+
+ // We need to declare all the non-trivial destructor local variables
+ // before the call to setjmp().
+ std::vector<uint8_t> pixels;
+ unsigned char* output_buffer = nullptr;
+ unsigned long output_size = 0;
+ std::vector<uint8_t> row_bytes;
+ size_t rowlen = RoundUpTo(ppf.info.xsize, VectorSize());
+ hwy::AlignedFreeUniquePtr<float[]> xyb_tmp =
+ hwy::AllocateAligned<float>(6 * rowlen);
+ hwy::AlignedFreeUniquePtr<float[]> premul_absorb =
+ hwy::AllocateAligned<float>(VectorSize() * 12);
+ ComputePremulAbsorb(255.0f, premul_absorb.get());
+
+ jpeg_compress_struct cinfo;
+ const auto try_catch_block = [&]() -> bool {
+ jpeg_error_mgr jerr;
+ jmp_buf env;
+ cinfo.err = jpegli_std_error(&jerr);
+ jerr.error_exit = &MyErrorExit;
+ if (setjmp(env)) {
+ return false;
+ }
+ cinfo.client_data = static_cast<void*>(&env);
+ jpegli_create_compress(&cinfo);
+ jpegli_mem_dest(&cinfo, &output_buffer, &output_size);
+ const JxlBasicInfo& info = ppf.info;
+ cinfo.image_width = info.xsize;
+ cinfo.image_height = info.ysize;
+ cinfo.input_components = info.num_color_channels;
+ cinfo.in_color_space =
+ cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB;
+ if (jpeg_settings.xyb) {
+ jpegli_set_xyb_mode(&cinfo);
+ } else if (jpeg_settings.use_std_quant_tables) {
+ jpegli_use_standard_quant_tables(&cinfo);
+ }
+ uint8_t cicp_tf = kUnknownTf;
+ if (!jpeg_settings.app_data.empty()) {
+ cicp_tf = LookupCICPTransferFunctionFromAppData(
+ jpeg_settings.app_data.data(), jpeg_settings.app_data.size());
+ } else if (!output_encoding.IsSRGB()) {
+ cicp_tf = LookupCICPTransferFunctionFromICCProfile(
+ output_encoding.ICC().data(), output_encoding.ICC().size());
+ }
+ jpegli_set_cicp_transfer_function(&cinfo, cicp_tf);
+ jpegli_set_defaults(&cinfo);
+ if (!jpeg_settings.chroma_subsampling.empty()) {
+ if (jpeg_settings.chroma_subsampling == "444") {
+ cinfo.comp_info[0].h_samp_factor = 1;
+ cinfo.comp_info[0].v_samp_factor = 1;
+ } else if (jpeg_settings.chroma_subsampling == "440") {
+ cinfo.comp_info[0].h_samp_factor = 1;
+ cinfo.comp_info[0].v_samp_factor = 2;
+ } else if (jpeg_settings.chroma_subsampling == "422") {
+ cinfo.comp_info[0].h_samp_factor = 2;
+ cinfo.comp_info[0].v_samp_factor = 1;
+ } else if (jpeg_settings.chroma_subsampling == "420") {
+ cinfo.comp_info[0].h_samp_factor = 2;
+ cinfo.comp_info[0].v_samp_factor = 2;
+ } else {
+ return false;
+ }
+ for (int i = 1; i < cinfo.num_components; ++i) {
+ cinfo.comp_info[i].h_samp_factor = 1;
+ cinfo.comp_info[i].v_samp_factor = 1;
+ }
+ }
+ jpegli_enable_adaptive_quantization(
+ &cinfo, jpeg_settings.use_adaptive_quantization);
+ jpegli_set_distance(&cinfo, jpeg_settings.distance, TRUE);
+ jpegli_set_progressive_level(&cinfo, jpeg_settings.progressive_level);
+ cinfo.optimize_coding = jpeg_settings.optimize_coding;
+ if (!jpeg_settings.app_data.empty()) {
+ // Make sure jpegli_start_compress() does not write any APP markers.
+ cinfo.write_JFIF_header = false;
+ cinfo.write_Adobe_marker = false;
+ }
+ const PackedImage& image = ppf.frames[0].color;
+ if (jpeg_settings.xyb) {
+ jpegli_set_input_format(&cinfo, JPEGLI_TYPE_FLOAT, JPEGLI_NATIVE_ENDIAN);
+ } else {
+ jpegli_set_input_format(&cinfo, ConvertDataType(image.format.data_type),
+ ConvertEndianness(image.format.endianness));
+ }
+ jpegli_start_compress(&cinfo, TRUE);
+ if (!jpeg_settings.app_data.empty()) {
+ JXL_RETURN_IF_ERROR(WriteAppData(&cinfo, jpeg_settings.app_data));
+ }
+ if ((jpeg_settings.app_data.empty() && !output_encoding.IsSRGB()) ||
+ jpeg_settings.xyb) {
+ jpegli_write_icc_profile(&cinfo, output_encoding.ICC().data(),
+ output_encoding.ICC().size());
+ }
+ const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels());
+ if (jpeg_settings.xyb) {
+ float* src_buf = c_transform.BufSrc(0);
+ float* dst_buf = c_transform.BufDst(0);
+ for (size_t y = 0; y < image.ysize; ++y) {
+ // convert to float
+ ToFloatRow(&pixels[y * image.stride], image.format, 3 * image.xsize,
+ src_buf);
+ // convert to linear srgb
+ if (!c_transform.Run(0, src_buf, dst_buf)) {
+ return false;
+ }
+ // deinterleave channels
+ float* row0 = &xyb_tmp[0];
+ float* row1 = &xyb_tmp[rowlen];
+ float* row2 = &xyb_tmp[2 * rowlen];
+ for (size_t x = 0; x < image.xsize; ++x) {
+ row0[x] = dst_buf[3 * x + 0];
+ row1[x] = dst_buf[3 * x + 1];
+ row2[x] = dst_buf[3 * x + 2];
+ }
+ // convert to xyb
+ LinearRGBRowToXYB(row0, row1, row2, premul_absorb.get(), image.xsize);
+ // scale xyb
+ ScaleXYBRow(row0, row1, row2, image.xsize);
+ // interleave channels
+ float* row_out = &xyb_tmp[3 * rowlen];
+ for (size_t x = 0; x < image.xsize; ++x) {
+ row_out[3 * x + 0] = row0[x];
+ row_out[3 * x + 1] = row1[x];
+ row_out[3 * x + 2] = row2[x];
+ }
+ // feed to jpegli as native endian floats
+ JSAMPROW row[] = {reinterpret_cast<uint8_t*>(row_out)};
+ jpegli_write_scanlines(&cinfo, row, 1);
+ }
+ } else {
+ row_bytes.resize(image.stride);
+ for (size_t y = 0; y < info.ysize; ++y) {
+ memcpy(&row_bytes[0], pixels + y * image.stride, image.stride);
+ JSAMPROW row[] = {row_bytes.data()};
+ jpegli_write_scanlines(&cinfo, row, 1);
+ }
+ }
+ jpegli_finish_compress(&cinfo);
+ compressed->resize(output_size);
+ std::copy_n(output_buffer, output_size, compressed->data());
+ return true;
+ };
+ bool success = try_catch_block();
+ jpegli_destroy_compress(&cinfo);
+ if (output_buffer) free(output_buffer);
+ return success;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/jpegli.h b/third_party/jpeg-xl/lib/extras/enc/jpegli.h
new file mode 100644
index 0000000000..194b9f7e48
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/jpegli.h
@@ -0,0 +1,47 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_JPEGLI_H_
+#define LIB_EXTRAS_ENC_JPEGLI_H_
+
+// Encodes JPG pixels and metadata in memory using the libjpegli library.
+
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+struct JpegSettings {
+ bool xyb = false;
+ size_t target_size = 0;
+ float distance = 1.f;
+ bool use_adaptive_quantization = true;
+ bool use_std_quant_tables = false;
+ int progressive_level = 2;
+ bool optimize_coding = true;
+ std::string chroma_subsampling;
+ int libjpeg_quality = 0;
+ std::string libjpeg_chroma_subsampling;
+ // If not empty, must contain concatenated APP marker segments. In this case,
+ // these and only these APP marker segments will be written to the JPEG
+ // output. In xyb mode app_data must not contain an ICC profile, in this
+ // case an additional APP2 ICC profile for the XYB colorspace will be emitted.
+ std::vector<uint8_t> app_data;
+};
+
+Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings,
+ ThreadPool* pool, std::vector<uint8_t>* compressed);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_JPEGLI_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/jpg.cc b/third_party/jpeg-xl/lib/extras/enc/jpg.cc
new file mode 100644
index 0000000000..179bcbe777
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/jpg.cc
@@ -0,0 +1,427 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/jpg.h"
+
+#include <jpeglib.h>
+#include <setjmp.h>
+#include <stdint.h>
+
+#include <algorithm>
+#include <array>
+#include <iterator>
+#include <numeric>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+#include "lib/extras/exif.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/sanitizers.h"
+#if JPEGXL_ENABLE_SJPEG
+#include "sjpeg.h"
+#endif
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+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;
+
+enum class JpegEncoder {
+ kLibJpeg,
+ kSJpeg,
+};
+
+#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0]))
+
+// Popular jpeg scan scripts
+// The fields of the individual scans are:
+// comps_in_scan, component_index[], Ss, Se, Ah, Al
+static constexpr jpeg_scan_info kScanScript1[] = {
+ {1, {0}, 0, 0, 0, 0}, //
+ {1, {1}, 0, 0, 0, 0}, //
+ {1, {2}, 0, 0, 0, 0}, //
+ {1, {0}, 1, 8, 0, 0}, //
+ {1, {0}, 9, 63, 0, 0}, //
+ {1, {1}, 1, 63, 0, 0}, //
+ {1, {2}, 1, 63, 0, 0}, //
+};
+static constexpr size_t kNumScans1 = ARRAY_SIZE(kScanScript1);
+
+static constexpr jpeg_scan_info kScanScript2[] = {
+ {1, {0}, 0, 0, 0, 0}, //
+ {1, {1}, 0, 0, 0, 0}, //
+ {1, {2}, 0, 0, 0, 0}, //
+ {1, {0}, 1, 2, 0, 1}, //
+ {1, {0}, 3, 63, 0, 1}, //
+ {1, {0}, 1, 63, 1, 0}, //
+ {1, {1}, 1, 63, 0, 0}, //
+ {1, {2}, 1, 63, 0, 0}, //
+};
+static constexpr size_t kNumScans2 = ARRAY_SIZE(kScanScript2);
+
+static constexpr jpeg_scan_info kScanScript3[] = {
+ {1, {0}, 0, 0, 0, 0}, //
+ {1, {1}, 0, 0, 0, 0}, //
+ {1, {2}, 0, 0, 0, 0}, //
+ {1, {0}, 1, 63, 0, 2}, //
+ {1, {0}, 1, 63, 2, 1}, //
+ {1, {0}, 1, 63, 1, 0}, //
+ {1, {1}, 1, 63, 0, 0}, //
+ {1, {2}, 1, 63, 0, 0}, //
+};
+static constexpr size_t kNumScans3 = ARRAY_SIZE(kScanScript3);
+
+static constexpr jpeg_scan_info kScanScript4[] = {
+ {3, {0, 1, 2}, 0, 0, 0, 1}, //
+ {1, {0}, 1, 5, 0, 2}, //
+ {1, {2}, 1, 63, 0, 1}, //
+ {1, {1}, 1, 63, 0, 1}, //
+ {1, {0}, 6, 63, 0, 2}, //
+ {1, {0}, 1, 63, 2, 1}, //
+ {3, {0, 1, 2}, 0, 0, 1, 0}, //
+ {1, {2}, 1, 63, 1, 0}, //
+ {1, {1}, 1, 63, 1, 0}, //
+ {1, {0}, 1, 63, 1, 0}, //
+};
+static constexpr size_t kNumScans4 = ARRAY_SIZE(kScanScript4);
+
+static constexpr jpeg_scan_info kScanScript5[] = {
+ {3, {0, 1, 2}, 0, 0, 0, 1}, //
+ {1, {0}, 1, 5, 0, 2}, //
+ {1, {1}, 1, 5, 0, 2}, //
+ {1, {2}, 1, 5, 0, 2}, //
+ {1, {1}, 6, 63, 0, 2}, //
+ {1, {2}, 6, 63, 0, 2}, //
+ {1, {0}, 6, 63, 0, 2}, //
+ {1, {0}, 1, 63, 2, 1}, //
+ {1, {1}, 1, 63, 2, 1}, //
+ {1, {2}, 1, 63, 2, 1}, //
+ {3, {0, 1, 2}, 0, 0, 1, 0}, //
+ {1, {0}, 1, 63, 1, 0}, //
+ {1, {1}, 1, 63, 1, 0}, //
+ {1, {2}, 1, 63, 1, 0}, //
+};
+static constexpr size_t kNumScans5 = ARRAY_SIZE(kScanScript5);
+
+// Adapt RGB scan info to grayscale jpegs.
+void FilterScanComponents(const jpeg_compress_struct* cinfo,
+ jpeg_scan_info* si) {
+ const int all_comps_in_scan = si->comps_in_scan;
+ si->comps_in_scan = 0;
+ for (int j = 0; j < all_comps_in_scan; ++j) {
+ const int component = si->component_index[j];
+ if (component < cinfo->input_components) {
+ si->component_index[si->comps_in_scan++] = component;
+ }
+ }
+}
+
+Status SetJpegProgression(int progressive_id,
+ std::vector<jpeg_scan_info>* scan_infos,
+ jpeg_compress_struct* cinfo) {
+ if (progressive_id < 0) {
+ return true;
+ }
+ if (progressive_id == 0) {
+ jpeg_simple_progression(cinfo);
+ return true;
+ }
+ constexpr const jpeg_scan_info* kScanScripts[] = {
+ kScanScript1, kScanScript2, kScanScript3, kScanScript4, kScanScript5,
+ };
+ constexpr size_t kNumScans[] = {kNumScans1, kNumScans2, kNumScans3,
+ kNumScans4, kNumScans5};
+ if (progressive_id > static_cast<int>(ARRAY_SIZE(kNumScans))) {
+ return JXL_FAILURE("Unknown jpeg scan script id %d", progressive_id);
+ }
+ const jpeg_scan_info* scan_script = kScanScripts[progressive_id - 1];
+ const size_t num_scans = kNumScans[progressive_id - 1];
+ // filter scan script for number of components
+ for (size_t i = 0; i < num_scans; ++i) {
+ jpeg_scan_info scan_info = scan_script[i];
+ FilterScanComponents(cinfo, &scan_info);
+ if (scan_info.comps_in_scan > 0) {
+ scan_infos->emplace_back(std::move(scan_info));
+ }
+ }
+ cinfo->scan_info = scan_infos->data();
+ cinfo->num_scans = scan_infos->size();
+ return true;
+}
+
+bool IsSRGBEncoding(const JxlColorEncoding& c) {
+ return ((c.color_space == JXL_COLOR_SPACE_RGB ||
+ c.color_space == JXL_COLOR_SPACE_GRAY) &&
+ c.primaries == JXL_PRIMARIES_SRGB &&
+ c.white_point == JXL_WHITE_POINT_D65 &&
+ c.transfer_function == JXL_TRANSFER_FUNCTION_SRGB);
+}
+
+void WriteICCProfile(jpeg_compress_struct* const cinfo,
+ const std::vector<uint8_t>& icc) {
+ constexpr size_t kMaxIccBytesInMarker =
+ kMaxBytesInMarker - sizeof kICCSignature - 2;
+ const int num_markers =
+ static_cast<int>(DivCeil(icc.size(), kMaxIccBytesInMarker));
+ size_t begin = 0;
+ for (int current_marker = 0; current_marker < num_markers; ++current_marker) {
+ const size_t length = std::min(kMaxIccBytesInMarker, icc.size() - begin);
+ jpeg_write_m_header(
+ cinfo, kICCMarker,
+ static_cast<unsigned int>(length + sizeof kICCSignature + 2));
+ for (const unsigned char c : kICCSignature) {
+ jpeg_write_m_byte(cinfo, c);
+ }
+ jpeg_write_m_byte(cinfo, current_marker + 1);
+ jpeg_write_m_byte(cinfo, num_markers);
+ for (size_t i = 0; i < length; ++i) {
+ jpeg_write_m_byte(cinfo, icc[begin]);
+ ++begin;
+ }
+ }
+}
+void WriteExif(jpeg_compress_struct* const cinfo,
+ const std::vector<uint8_t>& exif) {
+ jpeg_write_m_header(
+ cinfo, kExifMarker,
+ static_cast<unsigned int>(exif.size() + sizeof kExifSignature));
+ for (const unsigned char c : kExifSignature) {
+ jpeg_write_m_byte(cinfo, c);
+ }
+ for (size_t i = 0; i < exif.size(); ++i) {
+ jpeg_write_m_byte(cinfo, exif[i]);
+ }
+}
+
+Status SetChromaSubsampling(const std::string& subsampling,
+ jpeg_compress_struct* const cinfo) {
+ const std::pair<const char*,
+ std::pair<std::array<uint8_t, 3>, std::array<uint8_t, 3>>>
+ options[] = {{"444", {{{1, 1, 1}}, {{1, 1, 1}}}},
+ {"420", {{{2, 1, 1}}, {{2, 1, 1}}}},
+ {"422", {{{2, 1, 1}}, {{1, 1, 1}}}},
+ {"440", {{{1, 1, 1}}, {{2, 1, 1}}}}};
+ for (const auto& option : options) {
+ if (subsampling == option.first) {
+ for (size_t i = 0; i < 3; i++) {
+ cinfo->comp_info[i].h_samp_factor = option.second.first[i];
+ cinfo->comp_info[i].v_samp_factor = option.second.second[i];
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info,
+ const JxlColorEncoding& color_encoding,
+ const std::vector<uint8_t>& icc,
+ std::vector<uint8_t> exif, size_t quality,
+ const std::string& chroma_subsampling,
+ int progressive_id, bool optimize_coding,
+ std::vector<uint8_t>* bytes) {
+ if (BITS_IN_JSAMPLE != 8 || sizeof(JSAMPLE) != 1) {
+ return JXL_FAILURE("Only 8 bit JSAMPLE is supported.");
+ }
+ jpeg_compress_struct cinfo = {};
+ jpeg_error_mgr jerr;
+ cinfo.err = jpeg_std_error(&jerr);
+ jpeg_create_compress(&cinfo);
+ unsigned char* buffer = nullptr;
+ unsigned long size = 0;
+ jpeg_mem_dest(&cinfo, &buffer, &size);
+ cinfo.image_width = image.xsize;
+ cinfo.image_height = image.ysize;
+ cinfo.input_components = info.num_color_channels;
+ cinfo.in_color_space = info.num_color_channels == 1 ? JCS_GRAYSCALE : JCS_RGB;
+ jpeg_set_defaults(&cinfo);
+ cinfo.optimize_coding = optimize_coding;
+ if (cinfo.input_components == 3) {
+ JXL_RETURN_IF_ERROR(SetChromaSubsampling(chroma_subsampling, &cinfo));
+ }
+ if (color_encoding.color_space == JXL_COLOR_SPACE_XYB) {
+ // Tell libjpeg not to convert XYB data to YCbCr.
+ jpeg_set_colorspace(&cinfo, JCS_RGB);
+ }
+ jpeg_set_quality(&cinfo, quality, TRUE);
+ std::vector<jpeg_scan_info> scan_infos;
+ JXL_RETURN_IF_ERROR(SetJpegProgression(progressive_id, &scan_infos, &cinfo));
+ jpeg_start_compress(&cinfo, TRUE);
+ if (!icc.empty()) {
+ WriteICCProfile(&cinfo, icc);
+ }
+ if (!exif.empty()) {
+ ResetExifOrientation(exif);
+ WriteExif(&cinfo, exif);
+ }
+ if (cinfo.input_components > 3 || cinfo.input_components < 0)
+ return JXL_FAILURE("invalid numbers of components");
+
+ std::vector<uint8_t> raw_bytes(image.pixels_size);
+ memcpy(&raw_bytes[0], reinterpret_cast<const uint8_t*>(image.pixels()),
+ image.pixels_size);
+ for (size_t y = 0; y < info.ysize; ++y) {
+ JSAMPROW row[] = {raw_bytes.data() + y * image.stride};
+
+ jpeg_write_scanlines(&cinfo, row, 1);
+ }
+ jpeg_finish_compress(&cinfo);
+ jpeg_destroy_compress(&cinfo);
+ bytes->resize(size);
+ // Compressed image data is initialized by libjpeg, which we are not
+ // instrumenting with msan.
+ msan::UnpoisonMemory(buffer, size);
+ std::copy_n(buffer, size, bytes->data());
+ std::free(buffer);
+ return true;
+}
+
+Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info,
+ const std::vector<uint8_t>& icc,
+ std::vector<uint8_t> exif, size_t quality,
+ const std::string& chroma_subsampling,
+ std::vector<uint8_t>* bytes) {
+#if !JPEGXL_ENABLE_SJPEG
+ return JXL_FAILURE("JPEG XL was built without sjpeg support");
+#else
+ sjpeg::EncoderParam param(quality);
+ if (!icc.empty()) {
+ param.iccp.assign(icc.begin(), icc.end());
+ }
+ if (!exif.empty()) {
+ ResetExifOrientation(exif);
+ param.exif.assign(exif.begin(), exif.end());
+ }
+ if (chroma_subsampling == "444") {
+ param.yuv_mode = SJPEG_YUV_444;
+ } else if (chroma_subsampling == "420") {
+ param.yuv_mode = SJPEG_YUV_SHARP;
+ } else {
+ return JXL_FAILURE("sjpeg does not support this chroma subsampling mode");
+ }
+ size_t stride = info.xsize * 3;
+ const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels());
+ std::string output;
+ JXL_RETURN_IF_ERROR(
+ sjpeg::Encode(pixels, image.xsize, image.ysize, stride, param, &output));
+ bytes->assign(
+ reinterpret_cast<const uint8_t*>(output.data()),
+ reinterpret_cast<const uint8_t*>(output.data() + output.size()));
+ return true;
+#endif
+}
+
+Status EncodeImageJPG(const PackedImage& image, const JxlBasicInfo& info,
+ const JxlColorEncoding& color_encoding,
+ const std::vector<uint8_t>& icc,
+ std::vector<uint8_t> exif, JpegEncoder encoder,
+ size_t quality, const std::string& chroma_subsampling,
+ int progressive_id, bool optimize_coding,
+ ThreadPool* pool, std::vector<uint8_t>* bytes) {
+ if (image.format.data_type != JXL_TYPE_UINT8) {
+ return JXL_FAILURE("Unsupported pixel data type");
+ }
+ if (info.alpha_bits > 0) {
+ return JXL_FAILURE("alpha is not supported");
+ }
+ if (quality > 100) {
+ return JXL_FAILURE("please specify a 0-100 JPEG quality");
+ }
+
+ switch (encoder) {
+ case JpegEncoder::kLibJpeg:
+ JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(
+ image, info, color_encoding, icc, std::move(exif), quality,
+ chroma_subsampling, progressive_id, optimize_coding, bytes));
+ break;
+ case JpegEncoder::kSJpeg:
+ JXL_RETURN_IF_ERROR(EncodeWithSJpeg(image, info, icc, std::move(exif),
+ quality, chroma_subsampling, bytes));
+ break;
+ default:
+ return JXL_FAILURE("tried to use an unknown JPEG encoder");
+ }
+
+ return true;
+}
+
+class JPEGEncoder : public Encoder {
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ std::vector<JxlPixelFormat> formats;
+ for (const uint32_t num_channels : {1, 3}) {
+ for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
+ formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/endianness,
+ /*align=*/0});
+ }
+ }
+ return formats;
+ }
+ Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
+ ThreadPool* pool = nullptr) const override {
+ JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
+ int quality = 100;
+ std::string chroma_subsampling = "444";
+ JpegEncoder jpeg_encoder = JpegEncoder::kLibJpeg;
+ int progressive_id = -1;
+ bool optimize_coding = true;
+ for (const auto& it : options()) {
+ if (it.first == "q") {
+ std::istringstream is(it.second);
+ JXL_RETURN_IF_ERROR(static_cast<bool>(is >> quality));
+ } else if (it.first == "chroma_subsampling") {
+ chroma_subsampling = it.second;
+ } else if (it.first == "jpeg_encoder") {
+ if (it.second == "libjpeg") {
+ jpeg_encoder = JpegEncoder::kLibJpeg;
+ } else if (it.second == "sjpeg") {
+ jpeg_encoder = JpegEncoder::kSJpeg;
+ } else {
+ return JXL_FAILURE("unknown jpeg encoder \"%s\"", it.second.c_str());
+ }
+ } else if (it.first == "progressive") {
+ std::istringstream is(it.second);
+ JXL_RETURN_IF_ERROR(static_cast<bool>(is >> progressive_id));
+ } else if (it.first == "optimize" && it.second == "OFF") {
+ optimize_coding = false;
+ }
+ }
+ std::vector<uint8_t> icc;
+ if (!IsSRGBEncoding(ppf.color_encoding)) {
+ icc = ppf.icc;
+ }
+ encoded_image->bitstreams.clear();
+ encoded_image->bitstreams.reserve(ppf.frames.size());
+ for (const auto& frame : ppf.frames) {
+ JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info));
+ encoded_image->bitstreams.emplace_back();
+ JXL_RETURN_IF_ERROR(EncodeImageJPG(
+ frame.color, ppf.info, ppf.color_encoding, icc, ppf.metadata.exif,
+ jpeg_encoder, quality, chroma_subsampling, progressive_id,
+ optimize_coding, pool, &encoded_image->bitstreams.back()));
+ }
+ return true;
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Encoder> GetJPEGEncoder() {
+ return jxl::make_unique<JPEGEncoder>();
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/jpg.h b/third_party/jpeg-xl/lib/extras/enc/jpg.h
new file mode 100644
index 0000000000..20b37cd168
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/jpg.h
@@ -0,0 +1,23 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_JPG_H_
+#define LIB_EXTRAS_ENC_JPG_H_
+
+// Encodes JPG pixels and metadata in memory.
+
+#include <memory>
+
+#include "lib/extras/enc/encode.h"
+
+namespace jxl {
+namespace extras {
+
+std::unique_ptr<Encoder> GetJPEGEncoder();
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_JPG_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/jxl.cc b/third_party/jpeg-xl/lib/extras/enc/jxl.cc
new file mode 100644
index 0000000000..633c6f2ade
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/jxl.cc
@@ -0,0 +1,276 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/jxl.h"
+
+#include <jxl/encode_cxx.h>
+
+#include "lib/jxl/exif.h"
+
+namespace jxl {
+namespace extras {
+
+JxlEncoderStatus SetOption(const JXLOption& opt,
+ JxlEncoderFrameSettings* settings) {
+ return opt.is_float
+ ? JxlEncoderFrameSettingsSetFloatOption(settings, opt.id, opt.fval)
+ : JxlEncoderFrameSettingsSetOption(settings, opt.id, opt.ival);
+}
+
+bool SetFrameOptions(const std::vector<JXLOption>& options, size_t frame_index,
+ size_t* option_idx, JxlEncoderFrameSettings* settings) {
+ while (*option_idx < options.size()) {
+ const auto& opt = options[*option_idx];
+ if (opt.frame_index > frame_index) {
+ break;
+ }
+ if (JXL_ENC_SUCCESS != SetOption(opt, settings)) {
+ fprintf(stderr, "Setting option id %d failed.\n", opt.id);
+ return false;
+ }
+ (*option_idx)++;
+ }
+ return true;
+}
+
+bool EncodeImageJXL(const JXLCompressParams& params, const PackedPixelFile& ppf,
+ const std::vector<uint8_t>* jpeg_bytes,
+ std::vector<uint8_t>* compressed) {
+ auto encoder = JxlEncoderMake(/*memory_manager=*/nullptr);
+ JxlEncoder* enc = encoder.get();
+
+ if (params.allow_expert_options) {
+ JxlEncoderAllowExpertOptions(enc);
+ }
+
+ if (params.runner_opaque != nullptr &&
+ JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc, params.runner,
+ params.runner_opaque)) {
+ fprintf(stderr, "JxlEncoderSetParallelRunner failed\n");
+ return false;
+ }
+
+ auto settings = JxlEncoderFrameSettingsCreate(enc, nullptr);
+ size_t option_idx = 0;
+ if (!SetFrameOptions(params.options, 0, &option_idx, settings)) {
+ return false;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetFrameDistance(settings, params.distance)) {
+ fprintf(stderr, "Setting frame distance failed.\n");
+ return false;
+ }
+
+ bool use_boxes = !ppf.metadata.exif.empty() || !ppf.metadata.xmp.empty() ||
+ !ppf.metadata.jumbf.empty() || !ppf.metadata.iptc.empty();
+ bool use_container = params.use_container || use_boxes ||
+ (jpeg_bytes && params.jpeg_store_metadata);
+
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderUseContainer(enc, static_cast<int>(use_container))) {
+ fprintf(stderr, "JxlEncoderUseContainer failed.\n");
+ return false;
+ }
+
+ if (jpeg_bytes) {
+ if (params.jpeg_store_metadata &&
+ JXL_ENC_SUCCESS != JxlEncoderStoreJPEGMetadata(enc, JXL_TRUE)) {
+ fprintf(stderr, "Storing JPEG metadata failed.\n");
+ return false;
+ }
+ if (JXL_ENC_SUCCESS != JxlEncoderAddJPEGFrame(settings, jpeg_bytes->data(),
+ jpeg_bytes->size())) {
+ fprintf(stderr, "JxlEncoderAddJPEGFrame() failed.\n");
+ return false;
+ }
+ } else {
+ size_t num_alpha_channels = 0; // Adjusted below.
+ JxlBasicInfo basic_info = ppf.info;
+ if (basic_info.alpha_bits > 0) num_alpha_channels = 1;
+ if (params.intensity_target > 0) {
+ basic_info.intensity_target = params.intensity_target;
+ }
+ basic_info.num_extra_channels =
+ std::max<uint32_t>(num_alpha_channels, ppf.info.num_extra_channels);
+ basic_info.num_color_channels = ppf.info.num_color_channels;
+ const bool lossless = params.distance == 0;
+ basic_info.uses_original_profile = lossless;
+ if (params.override_bitdepth != 0) {
+ basic_info.bits_per_sample = params.override_bitdepth;
+ basic_info.exponent_bits_per_sample =
+ params.override_bitdepth == 32 ? 8 : 0;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetCodestreamLevel(enc, params.codestream_level)) {
+ fprintf(stderr, "Setting --codestream_level failed.\n");
+ return false;
+ }
+ if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc, &basic_info)) {
+ fprintf(stderr, "JxlEncoderSetBasicInfo() failed.\n");
+ return false;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetFrameBitDepth(settings, &params.input_bitdepth)) {
+ fprintf(stderr, "JxlEncoderSetFrameBitDepth() failed.\n");
+ return false;
+ }
+ if (num_alpha_channels != 0 &&
+ JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelDistance(
+ settings, 0, params.alpha_distance)) {
+ fprintf(stderr, "Setting alpha distance failed.\n");
+ return false;
+ }
+ if (lossless &&
+ JXL_ENC_SUCCESS != JxlEncoderSetFrameLossless(settings, JXL_TRUE)) {
+ fprintf(stderr, "JxlEncoderSetFrameLossless() failed.\n");
+ return false;
+ }
+ if (!ppf.icc.empty()) {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetICCProfile(enc, ppf.icc.data(), ppf.icc.size())) {
+ fprintf(stderr, "JxlEncoderSetICCProfile() failed.\n");
+ return false;
+ }
+ } else {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetColorEncoding(enc, &ppf.color_encoding)) {
+ fprintf(stderr, "JxlEncoderSetColorEncoding() failed.\n");
+ return false;
+ }
+ }
+
+ if (use_boxes) {
+ if (JXL_ENC_SUCCESS != JxlEncoderUseBoxes(enc)) {
+ fprintf(stderr, "JxlEncoderUseBoxes() failed.\n");
+ return false;
+ }
+ // Prepend 4 zero bytes to exif for tiff header offset
+ std::vector<uint8_t> exif_with_offset;
+ bool bigendian;
+ if (IsExif(ppf.metadata.exif, &bigendian)) {
+ exif_with_offset.resize(ppf.metadata.exif.size() + 4);
+ memcpy(exif_with_offset.data() + 4, ppf.metadata.exif.data(),
+ ppf.metadata.exif.size());
+ }
+ const struct BoxInfo {
+ const char* type;
+ const std::vector<uint8_t>& bytes;
+ } boxes[] = {
+ {"Exif", exif_with_offset},
+ {"xml ", ppf.metadata.xmp},
+ {"jumb", ppf.metadata.jumbf},
+ {"xml ", ppf.metadata.iptc},
+ };
+ for (size_t i = 0; i < sizeof boxes / sizeof *boxes; ++i) {
+ const BoxInfo& box = boxes[i];
+ if (!box.bytes.empty() &&
+ JXL_ENC_SUCCESS != JxlEncoderAddBox(enc, box.type, box.bytes.data(),
+ box.bytes.size(),
+ params.compress_boxes)) {
+ fprintf(stderr, "JxlEncoderAddBox() failed (%s).\n", box.type);
+ return false;
+ }
+ }
+ JxlEncoderCloseBoxes(enc);
+ }
+
+ for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) {
+ const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame];
+ const jxl::extras::PackedImage& pimage = pframe.color;
+ JxlPixelFormat ppixelformat = pimage.format;
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetFrameHeader(settings, &pframe.frame_info)) {
+ fprintf(stderr, "JxlEncoderSetFrameHeader() failed.\n");
+ return false;
+ }
+ if (!SetFrameOptions(params.options, num_frame, &option_idx, settings)) {
+ return false;
+ }
+ if (num_alpha_channels > 0) {
+ JxlExtraChannelInfo extra_channel_info;
+ JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &extra_channel_info);
+ extra_channel_info.bits_per_sample = ppf.info.alpha_bits;
+ extra_channel_info.exponent_bits_per_sample =
+ ppf.info.alpha_exponent_bits;
+ if (params.premultiply != -1) {
+ if (params.premultiply != 0 && params.premultiply != 1) {
+ fprintf(stderr, "premultiply must be one of: -1, 0, 1.\n");
+ return false;
+ }
+ extra_channel_info.alpha_premultiplied = params.premultiply;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetExtraChannelInfo(enc, 0, &extra_channel_info)) {
+ fprintf(stderr, "JxlEncoderSetExtraChannelInfo() failed.\n");
+ return false;
+ }
+ // We take the extra channel blend info frame_info, but don't do
+ // clamping.
+ JxlBlendInfo extra_channel_blend_info =
+ pframe.frame_info.layer_info.blend_info;
+ extra_channel_blend_info.clamp = JXL_FALSE;
+ JxlEncoderSetExtraChannelBlendInfo(settings, 0,
+ &extra_channel_blend_info);
+ }
+ size_t num_interleaved_alpha =
+ (ppixelformat.num_channels - ppf.info.num_color_channels);
+ // Add extra channel info for the rest of the extra channels.
+ for (size_t i = 0; i < ppf.info.num_extra_channels; ++i) {
+ if (i < ppf.extra_channels_info.size()) {
+ const auto& ec_info = ppf.extra_channels_info[i].ec_info;
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetExtraChannelInfo(enc, num_interleaved_alpha + i,
+ &ec_info)) {
+ fprintf(stderr, "JxlEncoderSetExtraChannelInfo() failed.\n");
+ return false;
+ }
+ }
+ }
+ if (JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(settings, &ppixelformat,
+ pimage.pixels(),
+ pimage.pixels_size)) {
+ fprintf(stderr, "JxlEncoderAddImageFrame() failed.\n");
+ return false;
+ }
+ // Only set extra channel buffer if it is provided non-interleaved.
+ for (size_t i = 0; i < pframe.extra_channels.size(); ++i) {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetExtraChannelBuffer(settings, &ppixelformat,
+ pframe.extra_channels[i].pixels(),
+ pframe.extra_channels[i].stride *
+ pframe.extra_channels[i].ysize,
+ num_interleaved_alpha + i)) {
+ fprintf(stderr, "JxlEncoderSetExtraChannelBuffer() failed.\n");
+ return false;
+ }
+ }
+ }
+ }
+ JxlEncoderCloseInput(enc);
+ // Reading compressed output
+ compressed->clear();
+ compressed->resize(4096);
+ uint8_t* next_out = compressed->data();
+ size_t avail_out = compressed->size() - (next_out - compressed->data());
+ JxlEncoderStatus result = JXL_ENC_NEED_MORE_OUTPUT;
+ while (result == JXL_ENC_NEED_MORE_OUTPUT) {
+ result = JxlEncoderProcessOutput(enc, &next_out, &avail_out);
+ if (result == JXL_ENC_NEED_MORE_OUTPUT) {
+ size_t offset = next_out - compressed->data();
+ compressed->resize(compressed->size() * 2);
+ next_out = compressed->data() + offset;
+ avail_out = compressed->size() - offset;
+ }
+ }
+ compressed->resize(next_out - compressed->data());
+ if (result != JXL_ENC_SUCCESS) {
+ fprintf(stderr, "JxlEncoderProcessOutput failed.\n");
+ return false;
+ }
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/jxl.h b/third_party/jpeg-xl/lib/extras/enc/jxl.h
new file mode 100644
index 0000000000..3e77fce3c1
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/jxl.h
@@ -0,0 +1,78 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_JXL_H_
+#define LIB_EXTRAS_ENC_JXL_H_
+
+#include <jxl/encode.h>
+#include <jxl/parallel_runner.h>
+#include <jxl/thread_parallel_runner.h>
+#include <jxl/types.h>
+#include <stdint.h>
+
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+
+namespace jxl {
+namespace extras {
+
+struct JXLOption {
+ JXLOption(JxlEncoderFrameSettingId id, int64_t val, size_t frame_index)
+ : id(id), is_float(false), ival(val), frame_index(frame_index) {}
+ JXLOption(JxlEncoderFrameSettingId id, float val, size_t frame_index)
+ : id(id), is_float(true), fval(val), frame_index(frame_index) {}
+
+ JxlEncoderFrameSettingId id;
+ bool is_float;
+ union {
+ int64_t ival;
+ float fval;
+ };
+ size_t frame_index;
+};
+
+struct JXLCompressParams {
+ std::vector<JXLOption> options;
+ // Target butteraugli distance, 0.0 means lossless.
+ float distance = 1.0f;
+ float alpha_distance = 1.0f;
+ // If set to true, forces container mode.
+ bool use_container = false;
+ // Whether to enable/disable byte-exact jpeg reconstruction for jpeg inputs.
+ bool jpeg_store_metadata = true;
+ // Whether to create brob boxes.
+ bool compress_boxes = true;
+ // Upper bound on the intensity level present in the image in nits (zero means
+ // that the library chooses a default).
+ float intensity_target = 0;
+ // Overrides for bitdepth, codestream level and alpha premultiply.
+ size_t override_bitdepth = 0;
+ int32_t codestream_level = -1;
+ int32_t premultiply = -1;
+ // Override input buffer interpretation.
+ JxlBitDepth input_bitdepth = {JXL_BIT_DEPTH_FROM_PIXEL_FORMAT, 0, 0};
+ // If runner_opaque is set, the decoder uses this parallel runner.
+ JxlParallelRunner runner = JxlThreadParallelRunner;
+ void* runner_opaque = nullptr;
+
+ bool allow_expert_options = false;
+
+ void AddOption(JxlEncoderFrameSettingId id, int64_t val) {
+ options.emplace_back(JXLOption(id, val, 0));
+ }
+ void AddFloatOption(JxlEncoderFrameSettingId id, float val) {
+ options.emplace_back(JXLOption(id, val, 0));
+ }
+};
+
+bool EncodeImageJXL(const JXLCompressParams& params, const PackedPixelFile& ppf,
+ const std::vector<uint8_t>* jpeg_bytes,
+ std::vector<uint8_t>* compressed);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_JXL_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/npy.cc b/third_party/jpeg-xl/lib/extras/enc/npy.cc
new file mode 100644
index 0000000000..e7a659184b
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/npy.cc
@@ -0,0 +1,322 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/npy.h"
+
+#include <jxl/types.h>
+#include <stdio.h>
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+// JSON value writing
+
+class JSONField {
+ public:
+ virtual ~JSONField() = default;
+ virtual void Write(std::ostream& o, uint32_t indent) const = 0;
+
+ protected:
+ JSONField() = default;
+};
+
+class JSONValue : public JSONField {
+ public:
+ template <typename T>
+ explicit JSONValue(const T& value) : value_(std::to_string(value)) {}
+
+ explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {}
+
+ explicit JSONValue(bool value) : value_(value ? "true" : "false") {}
+
+ void Write(std::ostream& o, uint32_t indent) const override { o << value_; }
+
+ private:
+ std::string value_;
+};
+
+class JSONDict : public JSONField {
+ public:
+ JSONDict() = default;
+
+ template <typename T>
+ T* AddEmpty(const std::string& key) {
+ static_assert(std::is_convertible<T*, JSONField*>::value,
+ "T must be a JSONField");
+ T* ret = new T();
+ values_.emplace_back(
+ key, std::unique_ptr<JSONField>(static_cast<JSONField*>(ret)));
+ return ret;
+ }
+
+ template <typename T>
+ void Add(const std::string& key, const T& value) {
+ values_.emplace_back(key, std::unique_ptr<JSONField>(new JSONValue(value)));
+ }
+
+ void Write(std::ostream& o, uint32_t indent) const override {
+ std::string indent_str(indent, ' ');
+ o << "{";
+ bool is_first = true;
+ for (const auto& key_value : values_) {
+ if (!is_first) {
+ o << ",";
+ }
+ is_first = false;
+ o << std::endl << indent_str << " \"" << key_value.first << "\": ";
+ key_value.second->Write(o, indent + 2);
+ }
+ if (!values_.empty()) {
+ o << std::endl << indent_str;
+ }
+ o << "}";
+ }
+
+ private:
+ // Dictionary with order.
+ std::vector<std::pair<std::string, std::unique_ptr<JSONField>>> values_;
+};
+
+class JSONArray : public JSONField {
+ public:
+ JSONArray() = default;
+
+ template <typename T>
+ T* AddEmpty() {
+ static_assert(std::is_convertible<T*, JSONField*>::value,
+ "T must be a JSONField");
+ T* ret = new T();
+ values_.emplace_back(ret);
+ return ret;
+ }
+
+ template <typename T>
+ void Add(const T& value) {
+ values_.emplace_back(new JSONValue(value));
+ }
+
+ void Write(std::ostream& o, uint32_t indent) const override {
+ std::string indent_str(indent, ' ');
+ o << "[";
+ bool is_first = true;
+ for (const auto& value : values_) {
+ if (!is_first) {
+ o << ",";
+ }
+ is_first = false;
+ o << std::endl << indent_str << " ";
+ value->Write(o, indent + 2);
+ }
+ if (!values_.empty()) {
+ o << std::endl << indent_str;
+ }
+ o << "]";
+ }
+
+ private:
+ std::vector<std::unique_ptr<JSONField>> values_;
+};
+
+void GenerateMetadata(const PackedPixelFile& ppf, std::vector<uint8_t>* out) {
+ JSONDict meta;
+ // Same order as in 18181-3 CD.
+
+ // Frames.
+ auto* meta_frames = meta.AddEmpty<JSONArray>("frames");
+ for (size_t i = 0; i < ppf.frames.size(); i++) {
+ auto* frame_i = meta_frames->AddEmpty<JSONDict>();
+ if (ppf.info.have_animation) {
+ frame_i->Add("duration",
+ JSONValue(ppf.frames[i].frame_info.duration * 1.0f *
+ ppf.info.animation.tps_denominator /
+ ppf.info.animation.tps_numerator));
+ }
+
+ frame_i->Add("name", JSONValue(ppf.frames[i].name));
+
+ if (ppf.info.animation.have_timecodes) {
+ frame_i->Add("timecode", JSONValue(ppf.frames[i].frame_info.timecode));
+ }
+ }
+
+#define METADATA(FIELD) meta.Add(#FIELD, ppf.info.FIELD)
+
+ METADATA(intensity_target);
+ METADATA(min_nits);
+ METADATA(relative_to_max_display);
+ METADATA(linear_below);
+
+ if (ppf.info.have_preview) {
+ meta.AddEmpty<JSONDict>("preview");
+ // TODO(veluca): can we have duration/name/timecode here?
+ }
+
+ {
+ auto ectype = meta.AddEmpty<JSONArray>("extra_channel_type");
+ auto bps = meta.AddEmpty<JSONArray>("bits_per_sample");
+ auto ebps = meta.AddEmpty<JSONArray>("exp_bits_per_sample");
+ bps->Add(ppf.info.bits_per_sample);
+ ebps->Add(ppf.info.exponent_bits_per_sample);
+ for (size_t i = 0; i < ppf.extra_channels_info.size(); i++) {
+ switch (ppf.extra_channels_info[i].ec_info.type) {
+ case JXL_CHANNEL_ALPHA: {
+ ectype->Add(std::string("Alpha"));
+ break;
+ }
+ case JXL_CHANNEL_DEPTH: {
+ ectype->Add(std::string("Depth"));
+ break;
+ }
+ case JXL_CHANNEL_SPOT_COLOR: {
+ ectype->Add(std::string("SpotColor"));
+ break;
+ }
+ case JXL_CHANNEL_SELECTION_MASK: {
+ ectype->Add(std::string("SelectionMask"));
+ break;
+ }
+ case JXL_CHANNEL_BLACK: {
+ ectype->Add(std::string("Black"));
+ break;
+ }
+ case JXL_CHANNEL_CFA: {
+ ectype->Add(std::string("CFA"));
+ break;
+ }
+ case JXL_CHANNEL_THERMAL: {
+ ectype->Add(std::string("Thermal"));
+ break;
+ }
+ default: {
+ ectype->Add(std::string("UNKNOWN"));
+ break;
+ }
+ }
+ bps->Add(ppf.extra_channels_info[i].ec_info.bits_per_sample);
+ ebps->Add(ppf.extra_channels_info[i].ec_info.exponent_bits_per_sample);
+ }
+ }
+
+ std::ostringstream os;
+ meta.Write(os, 0);
+ out->resize(os.str().size());
+ memcpy(out->data(), os.str().data(), os.str().size());
+}
+
+void Append(std::vector<uint8_t>* out, const void* data, size_t size) {
+ size_t pos = out->size();
+ out->resize(pos + size);
+ memcpy(out->data() + pos, data, size);
+}
+
+void WriteNPYHeader(size_t xsize, size_t ysize, uint32_t num_channels,
+ size_t num_frames, std::vector<uint8_t>* out) {
+ const uint8_t header[] = "\x93NUMPY\x01\x00";
+ Append(out, header, 8);
+ std::stringstream ss;
+ ss << "{'descr': '<f4', 'fortran_order': False, 'shape': (" << num_frames
+ << ", " << ysize << ", " << xsize << ", " << num_channels << "), }\n";
+ // 16-bit little endian header length.
+ uint8_t header_len[2] = {static_cast<uint8_t>(ss.str().size() % 256),
+ static_cast<uint8_t>(ss.str().size() / 256)};
+ Append(out, header_len, 2);
+ Append(out, ss.str().data(), ss.str().size());
+}
+
+bool WriteFrameToNPYArray(size_t xsize, size_t ysize, const PackedFrame& frame,
+ std::vector<uint8_t>* out) {
+ const auto& color = frame.color;
+ if (color.xsize != xsize || color.ysize != ysize) {
+ return false;
+ }
+ for (const auto& ec : frame.extra_channels) {
+ if (ec.xsize != xsize || ec.ysize != ysize) {
+ return false;
+ }
+ }
+ // interleave the samples from color and extra channels
+ for (size_t y = 0; y < ysize; ++y) {
+ for (size_t x = 0; x < xsize; ++x) {
+ {
+ size_t sample_size = color.pixel_stride();
+ size_t offset = y * color.stride + x * sample_size;
+ uint8_t* pixels = reinterpret_cast<uint8_t*>(color.pixels());
+ JXL_ASSERT(offset + sample_size <= color.pixels_size);
+ Append(out, pixels + offset, sample_size);
+ }
+ for (const auto& ec : frame.extra_channels) {
+ size_t sample_size = ec.pixel_stride();
+ size_t offset = y * ec.stride + x * sample_size;
+ uint8_t* pixels = reinterpret_cast<uint8_t*>(ec.pixels());
+ JXL_ASSERT(offset + sample_size <= ec.pixels_size);
+ Append(out, pixels + offset, sample_size);
+ }
+ }
+ }
+ return true;
+}
+
+// Writes a PackedPixelFile as a numpy 4D ndarray in binary format.
+bool WriteNPYArray(const PackedPixelFile& ppf, std::vector<uint8_t>* out) {
+ size_t xsize = ppf.info.xsize;
+ size_t ysize = ppf.info.ysize;
+ WriteNPYHeader(xsize, ysize,
+ ppf.info.num_color_channels + ppf.extra_channels_info.size(),
+ ppf.frames.size(), out);
+ for (const auto& frame : ppf.frames) {
+ if (!WriteFrameToNPYArray(xsize, ysize, frame, out)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+class NumPyEncoder : public Encoder {
+ public:
+ Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
+ ThreadPool* pool = nullptr) const override {
+ JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
+ GenerateMetadata(ppf, &encoded_image->metadata);
+ encoded_image->bitstreams.emplace_back();
+ if (!WriteNPYArray(ppf, &encoded_image->bitstreams.back())) {
+ return false;
+ }
+ if (ppf.preview_frame) {
+ size_t xsize = ppf.info.preview.xsize;
+ size_t ysize = ppf.info.preview.ysize;
+ WriteNPYHeader(xsize, ysize, ppf.info.num_color_channels, 1,
+ &encoded_image->preview_bitstream);
+ if (!WriteFrameToNPYArray(xsize, ysize, *ppf.preview_frame,
+ &encoded_image->preview_bitstream)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ std::vector<JxlPixelFormat> formats;
+ for (const uint32_t num_channels : {1, 3}) {
+ formats.push_back(JxlPixelFormat{num_channels, JXL_TYPE_FLOAT,
+ JXL_LITTLE_ENDIAN, /*align=*/0});
+ }
+ return formats;
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Encoder> GetNumPyEncoder() {
+ return jxl::make_unique<NumPyEncoder>();
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/npy.h b/third_party/jpeg-xl/lib/extras/enc/npy.h
new file mode 100644
index 0000000000..3ee6208ec2
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/npy.h
@@ -0,0 +1,23 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_NPY_H_
+#define LIB_EXTRAS_ENC_NPY_H_
+
+// Encodes pixels to numpy array, used for conformance testing.
+
+#include <memory>
+
+#include "lib/extras/enc/encode.h"
+
+namespace jxl {
+namespace extras {
+
+std::unique_ptr<Encoder> GetNumPyEncoder();
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_NPY_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/pgx.cc b/third_party/jpeg-xl/lib/extras/enc/pgx.cc
new file mode 100644
index 0000000000..201c8b4189
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/pgx.cc
@@ -0,0 +1,123 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/pgx.h"
+
+#include <jxl/codestream_header.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/byte_order.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+constexpr size_t kMaxHeaderSize = 200;
+
+Status EncodeHeader(const JxlBasicInfo& info, char* header,
+ int* chars_written) {
+ if (info.alpha_bits > 0) {
+ return JXL_FAILURE("PGX: can't store alpha");
+ }
+ if (info.num_color_channels != 1) {
+ return JXL_FAILURE("PGX: must be grayscale");
+ }
+ // TODO(lode): verify other bit depths: for other bit depths such as 1 or 4
+ // bits, have a test case to verify it works correctly. For bits > 16, we may
+ // need to change the way external_image works.
+ if (info.bits_per_sample != 8 && info.bits_per_sample != 16) {
+ return JXL_FAILURE("PGX: bits other than 8 or 16 not yet supported");
+ }
+
+ // Use ML (Big Endian), LM may not be well supported by all decoders.
+ *chars_written = snprintf(header, kMaxHeaderSize, "PG ML + %u %u %u\n",
+ info.bits_per_sample, info.xsize, info.ysize);
+ JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) <
+ kMaxHeaderSize);
+ return true;
+}
+
+Status EncodeImagePGX(const PackedFrame& frame, const JxlBasicInfo& info,
+ std::vector<uint8_t>* bytes) {
+ char header[kMaxHeaderSize];
+ int header_size = 0;
+ JXL_RETURN_IF_ERROR(EncodeHeader(info, header, &header_size));
+
+ const PackedImage& color = frame.color;
+ const JxlPixelFormat format = color.format;
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels());
+ size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type);
+ size_t bytes_per_sample = data_bits_per_sample / kBitsPerByte;
+ size_t num_samples = info.xsize * info.ysize;
+
+ if (info.bits_per_sample != data_bits_per_sample) {
+ return JXL_FAILURE("Bit depth does not match pixel data type");
+ }
+
+ std::vector<uint8_t> pixels(num_samples * bytes_per_sample);
+
+ if (format.data_type == JXL_TYPE_UINT8) {
+ memcpy(&pixels[0], in, num_samples * bytes_per_sample);
+ } else if (format.data_type == JXL_TYPE_UINT16) {
+ if (format.endianness != JXL_BIG_ENDIAN) {
+ const uint8_t* p_in = in;
+ uint8_t* p_out = pixels.data();
+ for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) {
+ StoreBE16(LoadLE16(p_in), p_out);
+ }
+ } else {
+ memcpy(&pixels[0], in, num_samples * bytes_per_sample);
+ }
+ } else {
+ return JXL_FAILURE("Unsupported pixel data type");
+ }
+
+ bytes->resize(static_cast<size_t>(header_size) + pixels.size());
+ memcpy(bytes->data(), header, static_cast<size_t>(header_size));
+ memcpy(bytes->data() + header_size, pixels.data(), pixels.size());
+
+ return true;
+}
+
+class PGXEncoder : public Encoder {
+ public:
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ std::vector<JxlPixelFormat> formats;
+ for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) {
+ for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
+ formats.push_back(JxlPixelFormat{/*num_channels=*/1,
+ /*data_type=*/data_type,
+ /*endianness=*/endianness,
+ /*align=*/0});
+ }
+ }
+ return formats;
+ }
+ Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
+ ThreadPool* pool) const override {
+ JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
+ encoded_image->icc.assign(ppf.icc.begin(), ppf.icc.end());
+ encoded_image->bitstreams.clear();
+ encoded_image->bitstreams.reserve(ppf.frames.size());
+ for (const auto& frame : ppf.frames) {
+ JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info));
+ encoded_image->bitstreams.emplace_back();
+ JXL_RETURN_IF_ERROR(
+ EncodeImagePGX(frame, ppf.info, &encoded_image->bitstreams.back()));
+ }
+ return true;
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Encoder> GetPGXEncoder() {
+ return jxl::make_unique<PGXEncoder>();
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/pgx.h b/third_party/jpeg-xl/lib/extras/enc/pgx.h
new file mode 100644
index 0000000000..f24e391b09
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/pgx.h
@@ -0,0 +1,24 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_PGX_H_
+#define LIB_EXTRAS_ENC_PGX_H_
+
+// Encodes PGX pixels in memory.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "lib/extras/enc/encode.h"
+
+namespace jxl {
+namespace extras {
+
+std::unique_ptr<Encoder> GetPGXEncoder();
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_PGX_H_
diff --git a/third_party/jpeg-xl/lib/extras/enc/pnm.cc b/third_party/jpeg-xl/lib/extras/enc/pnm.cc
new file mode 100644
index 0000000000..7cebcc2a1f
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/pnm.cc
@@ -0,0 +1,303 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/pnm.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/packed_image.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/base/status.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/dec_external_image.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_external_image.h"
+#include "lib/jxl/enc_image_bundle.h"
+#include "lib/jxl/fields.h" // AllDefault
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+constexpr size_t kMaxHeaderSize = 200;
+
+class PNMEncoder : public Encoder {
+ public:
+ Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
+ ThreadPool* pool = nullptr) const override {
+ JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
+ if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() ||
+ !ppf.metadata.jumbf.empty() || !ppf.metadata.xmp.empty()) {
+ JXL_WARNING("PNM encoder ignoring metadata - use a different codec");
+ }
+ encoded_image->icc = ppf.icc;
+ encoded_image->bitstreams.clear();
+ encoded_image->bitstreams.reserve(ppf.frames.size());
+ for (const auto& frame : ppf.frames) {
+ JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info));
+ encoded_image->bitstreams.emplace_back();
+ JXL_RETURN_IF_ERROR(
+ EncodeFrame(ppf, frame, &encoded_image->bitstreams.back()));
+ }
+ for (size_t i = 0; i < ppf.extra_channels_info.size(); ++i) {
+ const auto& ec_info = ppf.extra_channels_info[i].ec_info;
+ encoded_image->extra_channel_bitstreams.emplace_back();
+ auto& ec_bitstreams = encoded_image->extra_channel_bitstreams.back();
+ for (const auto& frame : ppf.frames) {
+ ec_bitstreams.emplace_back();
+ JXL_RETURN_IF_ERROR(EncodeExtraChannel(frame.extra_channels[i],
+ ec_info.bits_per_sample,
+ &ec_bitstreams.back()));
+ }
+ }
+ return true;
+ }
+
+ protected:
+ virtual Status EncodeFrame(const PackedPixelFile& ppf,
+ const PackedFrame& frame,
+ std::vector<uint8_t>* bytes) const = 0;
+ virtual Status EncodeExtraChannel(const PackedImage& image,
+ size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const = 0;
+};
+
+class PPMEncoder : public PNMEncoder {
+ public:
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ return {JxlPixelFormat{3, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0},
+ JxlPixelFormat{3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}};
+ }
+ Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame,
+ std::vector<uint8_t>* bytes) const override {
+ return EncodeImage(frame.color, ppf.info.bits_per_sample, bytes);
+ }
+ Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const override {
+ return EncodeImage(image, bits_per_sample, bytes);
+ }
+
+ private:
+ Status EncodeImage(const PackedImage& image, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const {
+ uint32_t maxval = (1u << bits_per_sample) - 1;
+ char type = image.format.num_channels == 1 ? '5' : '6';
+ char header[kMaxHeaderSize];
+ size_t header_size =
+ snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n",
+ type, image.xsize, image.ysize, maxval);
+ JXL_RETURN_IF_ERROR(header_size < kMaxHeaderSize);
+ bytes->resize(header_size + image.pixels_size);
+ memcpy(bytes->data(), header, header_size);
+ memcpy(bytes->data() + header_size,
+ reinterpret_cast<uint8_t*>(image.pixels()), image.pixels_size);
+ return true;
+ }
+};
+
+class PGMEncoder : public PPMEncoder {
+ public:
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ return {JxlPixelFormat{1, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0},
+ JxlPixelFormat{1, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}};
+ }
+};
+
+class PFMEncoder : public PNMEncoder {
+ public:
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ std::vector<JxlPixelFormat> formats;
+ for (const uint32_t num_channels : {1, 3}) {
+ for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
+ formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
+ /*data_type=*/JXL_TYPE_FLOAT,
+ /*endianness=*/endianness,
+ /*align=*/0});
+ }
+ }
+ return formats;
+ }
+ Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame,
+ std::vector<uint8_t>* bytes) const override {
+ return EncodeImage(frame.color, bytes);
+ }
+ Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const override {
+ return EncodeImage(image, bytes);
+ }
+
+ private:
+ Status EncodeImage(const PackedImage& image,
+ std::vector<uint8_t>* bytes) const {
+ char type = image.format.num_channels == 1 ? 'f' : 'F';
+ double scale = image.format.endianness == JXL_LITTLE_ENDIAN ? -1.0 : 1.0;
+ char header[kMaxHeaderSize];
+ size_t header_size =
+ snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n",
+ type, image.xsize, image.ysize, scale);
+ JXL_RETURN_IF_ERROR(header_size < kMaxHeaderSize);
+ bytes->resize(header_size + image.pixels_size);
+ memcpy(bytes->data(), header, header_size);
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels());
+ uint8_t* out = bytes->data() + header_size;
+ for (size_t y = 0; y < image.ysize; ++y) {
+ size_t y_out = image.ysize - 1 - y;
+ const uint8_t* row_in = &in[y * image.stride];
+ uint8_t* row_out = &out[y_out * image.stride];
+ memcpy(row_out, row_in, image.stride);
+ }
+ return true;
+ }
+};
+
+class PAMEncoder : public PNMEncoder {
+ public:
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ std::vector<JxlPixelFormat> formats;
+ for (const uint32_t num_channels : {1, 2, 3, 4}) {
+ for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) {
+ formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
+ /*data_type=*/data_type,
+ /*endianness=*/JXL_BIG_ENDIAN,
+ /*align=*/0});
+ }
+ }
+ return formats;
+ }
+ Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame,
+ std::vector<uint8_t>* bytes) const override {
+ const PackedImage& color = frame.color;
+ const auto& ec_info = ppf.extra_channels_info;
+ JXL_RETURN_IF_ERROR(frame.extra_channels.size() == ec_info.size());
+ for (const auto& ec : frame.extra_channels) {
+ if (ec.xsize != color.xsize || ec.ysize != color.ysize) {
+ return JXL_FAILURE("Extra channel and color size mismatch.");
+ }
+ if (ec.format.data_type != color.format.data_type ||
+ ec.format.endianness != color.format.endianness) {
+ return JXL_FAILURE("Extra channel and color format mismatch.");
+ }
+ }
+ if (ppf.info.bits_per_sample != ppf.info.alpha_bits) {
+ return JXL_FAILURE("Alpha bit depth does not match image bit depth");
+ }
+ for (const auto& it : ec_info) {
+ if (it.ec_info.bits_per_sample != ppf.info.bits_per_sample) {
+ return JXL_FAILURE(
+ "Extra channel bit depth does not match image bit depth");
+ }
+ }
+ const char* kColorTypes[4] = {"GRAYSCALE", "GRAYSCALE_ALPHA", "RGB",
+ "RGB_ALPHA"};
+ uint32_t maxval = (1u << ppf.info.bits_per_sample) - 1;
+ uint32_t depth = color.format.num_channels + ec_info.size();
+ char header[kMaxHeaderSize];
+ size_t pos = 0;
+ pos += snprintf(header + pos, kMaxHeaderSize - pos,
+ "P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS
+ "\nDEPTH %u\n"
+ "MAXVAL %u\nTUPLTYPE %s\n",
+ color.xsize, color.ysize, depth, maxval,
+ kColorTypes[color.format.num_channels - 1]);
+ JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize);
+ for (const auto& info : ec_info) {
+ pos += snprintf(header + pos, kMaxHeaderSize - pos, "TUPLTYPE %s\n",
+ ExtraChannelTypeName(info.ec_info.type).c_str());
+ JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize);
+ }
+ pos += snprintf(header + pos, kMaxHeaderSize - pos, "ENDHDR\n");
+ JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize);
+ size_t total_size = color.pixels_size;
+ for (const auto& ec : frame.extra_channels) {
+ total_size += ec.pixels_size;
+ }
+ bytes->resize(pos + total_size);
+ memcpy(bytes->data(), header, pos);
+ // If we have no extra channels, just copy color pixel data over.
+ if (frame.extra_channels.empty()) {
+ memcpy(bytes->data() + pos, reinterpret_cast<uint8_t*>(color.pixels()),
+ color.pixels_size);
+ return true;
+ }
+ // Interleave color and extra channels.
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels());
+ std::vector<const uint8_t*> ec_in(frame.extra_channels.size());
+ for (size_t i = 0; i < frame.extra_channels.size(); ++i) {
+ ec_in[i] =
+ reinterpret_cast<const uint8_t*>(frame.extra_channels[i].pixels());
+ }
+ uint8_t* out = bytes->data() + pos;
+ size_t pwidth = PackedImage::BitsPerChannel(color.format.data_type) / 8;
+ for (size_t y = 0; y < color.ysize; ++y) {
+ for (size_t x = 0; x < color.xsize; ++x) {
+ memcpy(out, in, color.pixel_stride());
+ out += color.pixel_stride();
+ in += color.pixel_stride();
+ for (auto& p : ec_in) {
+ memcpy(out, p, pwidth);
+ out += pwidth;
+ p += pwidth;
+ }
+ }
+ }
+ return true;
+ }
+ Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const override {
+ return true;
+ }
+
+ private:
+ static std::string ExtraChannelTypeName(JxlExtraChannelType type) {
+ switch (type) {
+ case JXL_CHANNEL_ALPHA:
+ return std::string("Alpha");
+ case JXL_CHANNEL_DEPTH:
+ return std::string("Depth");
+ case JXL_CHANNEL_SPOT_COLOR:
+ return std::string("SpotColor");
+ case JXL_CHANNEL_SELECTION_MASK:
+ return std::string("SelectionMask");
+ case JXL_CHANNEL_BLACK:
+ return std::string("Black");
+ case JXL_CHANNEL_CFA:
+ return std::string("CFA");
+ case JXL_CHANNEL_THERMAL:
+ return std::string("Thermal");
+ default:
+ return std::string("UNKNOWN");
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Encoder> GetPPMEncoder() {
+ return jxl::make_unique<PPMEncoder>();
+}
+
+std::unique_ptr<Encoder> GetPFMEncoder() {
+ return jxl::make_unique<PFMEncoder>();
+}
+
+std::unique_ptr<Encoder> GetPGMEncoder() {
+ return jxl::make_unique<PGMEncoder>();
+}
+
+std::unique_ptr<Encoder> GetPAMEncoder() {
+ return jxl::make_unique<PAMEncoder>();
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/enc/pnm.h b/third_party/jpeg-xl/lib/extras/enc/pnm.h
new file mode 100644
index 0000000000..403208cecd
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/enc/pnm.h
@@ -0,0 +1,28 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_PNM_H_
+#define LIB_EXTRAS_ENC_PNM_H_
+
+// Encodes/decodes PBM/PGM/PPM/PFM pixels in memory.
+
+// TODO(janwas): workaround for incorrect Win64 codegen (cause unknown)
+#include <hwy/highway.h>
+#include <memory>
+
+#include "lib/extras/enc/encode.h"
+
+namespace jxl {
+namespace extras {
+
+std::unique_ptr<Encoder> GetPAMEncoder();
+std::unique_ptr<Encoder> GetPGMEncoder();
+std::unique_ptr<Encoder> GetPPMEncoder();
+std::unique_ptr<Encoder> GetPFMEncoder();
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_PNM_H_
diff --git a/third_party/jpeg-xl/lib/extras/exif.cc b/third_party/jpeg-xl/lib/extras/exif.cc
new file mode 100644
index 0000000000..7d926558c3
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/exif.cc
@@ -0,0 +1,55 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/exif.h"
+
+#include "lib/jxl/base/byte_order.h"
+
+namespace jxl {
+
+constexpr uint16_t kExifOrientationTag = 274;
+
+void ResetExifOrientation(std::vector<uint8_t>& exif) {
+ if (exif.size() < 12) return; // not enough bytes for a valid exif blob
+ bool bigendian;
+ uint8_t* t = exif.data();
+ if (LoadLE32(t) == 0x2A004D4D) {
+ bigendian = true;
+ } else if (LoadLE32(t) == 0x002A4949) {
+ bigendian = false;
+ } else {
+ return; // not a valid tiff header
+ }
+ t += 4;
+ uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t));
+ if (exif.size() < 12 + offset + 2 || offset < 8) return;
+ t += offset - 4;
+ uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t));
+ t += 2;
+ while (nb_tags > 0) {
+ if (t + 12 >= exif.data() + exif.size()) return;
+ uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t));
+ t += 2;
+ if (tag == kExifOrientationTag) {
+ uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t));
+ t += 2;
+ uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t));
+ t += 4;
+ if (type == 3 && count == 1) {
+ if (bigendian) {
+ StoreBE16(1, t);
+ } else {
+ StoreLE16(1, t);
+ }
+ }
+ return;
+ } else {
+ t += 10;
+ nb_tags--;
+ }
+ }
+}
+
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/exif.h b/third_party/jpeg-xl/lib/extras/exif.h
new file mode 100644
index 0000000000..f22b2ccef5
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/exif.h
@@ -0,0 +1,20 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_EXIF_H_
+#define LIB_EXTRAS_EXIF_H_
+
+#include <stdint.h>
+
+#include <vector>
+
+namespace jxl {
+
+// Sets the Exif orientation to the identity, to avoid repeated orientation
+void ResetExifOrientation(std::vector<uint8_t>& exif);
+
+} // namespace jxl
+
+#endif // LIB_EXTRAS_EXIF_H_
diff --git a/third_party/jpeg-xl/lib/extras/hlg.cc b/third_party/jpeg-xl/lib/extras/hlg.cc
new file mode 100644
index 0000000000..e39a0807f5
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/hlg.cc
@@ -0,0 +1,56 @@
+// Copyright (c) the JPEG XL Project 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/hlg.h"
+
+#include <cmath>
+
+#include "lib/jxl/enc_color_management.h"
+
+namespace jxl {
+
+float GetHlgGamma(const float peak_luminance, const float surround_luminance) {
+ return 1.2f * std::pow(1.111f, std::log2(peak_luminance / 1000.f)) *
+ std::pow(0.98f, std::log2(surround_luminance / 5.f));
+}
+
+Status HlgOOTF(ImageBundle* ib, const float gamma, ThreadPool* pool) {
+ ColorEncoding linear_rec2020;
+ linear_rec2020.SetColorSpace(ColorSpace::kRGB);
+ linear_rec2020.primaries = Primaries::k2100;
+ linear_rec2020.white_point = WhitePoint::kD65;
+ linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
+ JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
+ JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool));
+
+ JXL_RETURN_IF_ERROR(RunOnPool(
+ pool, 0, ib->ysize(), ThreadPool::NoInit,
+ [&](const int y, const int thread) {
+ float* const JXL_RESTRICT rows[3] = {ib->color()->PlaneRow(0, y),
+ ib->color()->PlaneRow(1, y),
+ ib->color()->PlaneRow(2, y)};
+ for (size_t x = 0; x < ib->xsize(); ++x) {
+ float& red = rows[0][x];
+ float& green = rows[1][x];
+ float& blue = rows[2][x];
+ const float luminance =
+ 0.2627f * red + 0.6780f * green + 0.0593f * blue;
+ const float ratio = std::pow(luminance, gamma - 1);
+ if (std::isfinite(ratio)) {
+ red *= ratio;
+ green *= ratio;
+ blue *= ratio;
+ }
+ }
+ },
+ "HlgOOTF"));
+ return true;
+}
+
+Status HlgInverseOOTF(ImageBundle* ib, const float gamma, ThreadPool* pool) {
+ return HlgOOTF(ib, 1.f / gamma, pool);
+}
+
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/hlg.h b/third_party/jpeg-xl/lib/extras/hlg.h
new file mode 100644
index 0000000000..4cfec444f4
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/hlg.h
@@ -0,0 +1,21 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_HLG_H_
+#define LIB_EXTRAS_HLG_H_
+
+#include "lib/jxl/image_bundle.h"
+
+namespace jxl {
+
+float GetHlgGamma(float peak_luminance, float surround_luminance = 5.f);
+
+Status HlgOOTF(ImageBundle* ib, float gamma, ThreadPool* pool = nullptr);
+
+Status HlgInverseOOTF(ImageBundle* ib, float gamma, ThreadPool* pool = nullptr);
+
+} // namespace jxl
+
+#endif // LIB_EXTRAS_HLG_H_
diff --git a/third_party/jpeg-xl/lib/extras/jpegli_test.cc b/third_party/jpeg-xl/lib/extras/jpegli_test.cc
new file mode 100644
index 0000000000..6aa8afe4c0
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/jpegli_test.cc
@@ -0,0 +1,405 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#if JPEGXL_ENABLE_JPEG && JPEGXL_ENABLE_JPEGLI
+
+#include "lib/extras/dec/jpegli.h"
+
+#include <jxl/color_encoding.h>
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/dec/decode.h"
+#include "lib/extras/dec/jpg.h"
+#include "lib/extras/enc/encode.h"
+#include "lib/extras/enc/jpegli.h"
+#include "lib/extras/enc/jpg.h"
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/test_image.h"
+#include "lib/jxl/test_utils.h"
+#include "lib/jxl/testing.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+using test::Butteraugli3Norm;
+using test::ButteraugliDistance;
+using test::TestImage;
+
+Status ReadTestImage(const std::string& pathname, PackedPixelFile* ppf) {
+ const PaddedBytes encoded = jxl::test::ReadTestData(pathname);
+ ColorHints color_hints;
+ if (pathname.find(".ppm") != std::string::npos) {
+ color_hints.Add("color_space", "RGB_D65_SRG_Rel_SRG");
+ } else if (pathname.find(".pgm") != std::string::npos) {
+ color_hints.Add("color_space", "Gra_D65_Rel_SRG");
+ }
+ return DecodeBytes(Span<const uint8_t>(encoded), color_hints, ppf);
+}
+
+std::vector<uint8_t> GetAppData(const std::vector<uint8_t>& compressed) {
+ std::vector<uint8_t> result;
+ size_t pos = 2; // After SOI
+ while (pos + 4 < compressed.size()) {
+ if (compressed[pos] != 0xff || compressed[pos + 1] < 0xe0 ||
+ compressed[pos + 1] > 0xf0) {
+ break;
+ }
+ size_t len = (compressed[pos + 2] << 8) + compressed[pos + 3] + 2;
+ if (pos + len > compressed.size()) {
+ break;
+ }
+ result.insert(result.end(), &compressed[pos], &compressed[pos] + len);
+ pos += len;
+ }
+ return result;
+}
+
+Status DecodeWithLibjpeg(const std::vector<uint8_t>& compressed,
+ PackedPixelFile* ppf,
+ const JPGDecompressParams* dparams = nullptr) {
+ return DecodeImageJPG(Span<const uint8_t>(compressed), ColorHints(), ppf,
+ /*constraints=*/nullptr, dparams);
+}
+
+Status EncodeWithLibjpeg(const PackedPixelFile& ppf, int quality,
+ std::vector<uint8_t>* compressed) {
+ std::unique_ptr<Encoder> encoder = GetJPEGEncoder();
+ encoder->SetOption("q", std::to_string(quality));
+ EncodedImage encoded;
+ JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded));
+ JXL_RETURN_IF_ERROR(!encoded.bitstreams.empty());
+ *compressed = std::move(encoded.bitstreams[0]);
+ return true;
+}
+
+std::string Description(const JxlColorEncoding& color_encoding) {
+ ColorEncoding c_enc;
+ JXL_CHECK(ConvertExternalToInternalColorEncoding(color_encoding, &c_enc));
+ return Description(c_enc);
+}
+
+float BitsPerPixel(const PackedPixelFile& ppf,
+ const std::vector<uint8_t>& compressed) {
+ const size_t num_pixels = ppf.info.xsize * ppf.info.ysize;
+ return compressed.size() * 8.0 / num_pixels;
+}
+
+TEST(JpegliTest, JpegliSRGBDecodeTest) {
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf0;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf0));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding));
+ EXPECT_EQ(8, ppf0.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed));
+
+ PackedPixelFile ppf1;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1));
+ PackedPixelFile ppf2;
+ JpegDecompressParams dparams;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2));
+ EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1));
+}
+
+TEST(JpegliTest, JpegliGrayscaleDecodeTest) {
+ std::string testimage = "jxl/flower/flower_small.g.depth8.pgm";
+ PackedPixelFile ppf0;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf0));
+ EXPECT_EQ("Gra_D65_Rel_SRG", Description(ppf0.color_encoding));
+ EXPECT_EQ(8, ppf0.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed));
+
+ PackedPixelFile ppf1;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1));
+ PackedPixelFile ppf2;
+ JpegDecompressParams dparams;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2));
+ EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1));
+}
+
+TEST(JpegliTest, JpegliXYBEncodeTest) {
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.xyb = true;
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out));
+ EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.45f));
+ EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.32f));
+}
+
+TEST(JpegliTest, JpegliDecodeTestLargeSmoothArea) {
+ TestImage t;
+ const size_t xsize = 2070;
+ const size_t ysize = 1063;
+ t.SetDimensions(xsize, ysize).SetChannels(3);
+ t.SetAllBitDepths(8).SetEndianness(JXL_NATIVE_ENDIAN);
+ TestImage::Frame frame = t.AddFrame();
+ frame.RandomFill();
+ // Create a large smooth area in the top half of the image. This is to test
+ // that the bias statistics calculation can handle many blocks with all-zero
+ // AC coefficients.
+ for (size_t y = 0; y < ysize / 2; ++y) {
+ for (size_t x = 0; x < xsize; ++x) {
+ for (size_t c = 0; c < 3; ++c) {
+ frame.SetValue(y, x, c, 0.5f);
+ }
+ }
+ }
+ const PackedPixelFile& ppf0 = t.ppf();
+
+ std::vector<uint8_t> compressed;
+ ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed));
+
+ PackedPixelFile ppf1;
+ JpegDecompressParams dparams;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf1));
+ EXPECT_LT(ButteraugliDistance(ppf0, ppf1), 3.0f);
+}
+
+TEST(JpegliTest, JpegliYUVEncodeTest) {
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.xyb = false;
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out));
+ EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.7f));
+ EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.32f));
+}
+
+TEST(JpegliTest, JpegliYUVChromaSubsamplingEncodeTest) {
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ for (const char* sampling : {"440", "422", "420"}) {
+ settings.xyb = false;
+ settings.chroma_subsampling = std::string(sampling);
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out));
+ EXPECT_LE(BitsPerPixel(ppf_in, compressed), 1.55f);
+ EXPECT_LE(ButteraugliDistance(ppf_in, ppf_out), 1.82f);
+ }
+}
+
+TEST(JpegliTest, JpegliYUVEncodeTestNoAq) {
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.xyb = false;
+ settings.use_adaptive_quantization = false;
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out));
+ EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.85f));
+ EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.25f));
+}
+
+TEST(JpegliTest, JpegliHDRRoundtripTest) {
+ std::string testimage = "jxl/hdr_room.png";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_202_Rel_HLG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(16, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.xyb = false;
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ JpegDecompressParams dparams;
+ dparams.output_data_type = JXL_TYPE_UINT16;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf_out));
+ EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(2.95f));
+ EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.05f));
+}
+
+TEST(JpegliTest, JpegliSetAppData) {
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.app_data = {0xff, 0xe3, 0, 4, 0, 1};
+ EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+ EXPECT_EQ(settings.app_data, GetAppData(compressed));
+
+ settings.app_data = {0xff, 0xe3, 0, 6, 0, 1, 2, 3, 0xff, 0xef, 0, 4, 0, 1};
+ EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+ EXPECT_EQ(settings.app_data, GetAppData(compressed));
+
+ settings.xyb = true;
+ EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+ EXPECT_EQ(0, memcmp(settings.app_data.data(), GetAppData(compressed).data(),
+ settings.app_data.size()));
+
+ settings.xyb = false;
+ settings.app_data = {0};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xe0};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xe0, 0, 2};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xeb, 0, 4, 0};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xeb, 0, 4, 0, 1, 2, 3};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xab, 0, 4, 0, 1};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.xyb = false;
+ settings.app_data = {
+ 0xff, 0xeb, 0, 4, 0, 1, //
+ 0xff, 0xe2, 0, 20, 0x49, 0x43, 0x43, 0x5F, 0x50, //
+ 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00, 0, 1, //
+ 0, 0, 0, 0, //
+ };
+ EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+ EXPECT_EQ(settings.app_data, GetAppData(compressed));
+
+ settings.xyb = true;
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+}
+
+struct TestConfig {
+ int num_colors;
+ int passes;
+ int dither;
+};
+
+class JpegliColorQuantTestParam : public ::testing::TestWithParam<TestConfig> {
+};
+
+TEST_P(JpegliColorQuantTestParam, JpegliColorQuantizeTest) {
+ TestConfig config = GetParam();
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf0;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf0));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding));
+ EXPECT_EQ(8, ppf0.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed));
+
+ PackedPixelFile ppf1;
+ JPGDecompressParams dparams1;
+ dparams1.two_pass_quant = (config.passes == 2);
+ dparams1.num_colors = config.num_colors;
+ dparams1.dither_mode = config.dither;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1, &dparams1));
+
+ PackedPixelFile ppf2;
+ JpegDecompressParams dparams2;
+ dparams2.two_pass_quant = (config.passes == 2);
+ dparams2.num_colors = config.num_colors;
+ dparams2.dither_mode = config.dither;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams2, nullptr, &ppf2));
+
+ double dist1 = Butteraugli3Norm(ppf0, ppf1);
+ double dist2 = Butteraugli3Norm(ppf0, ppf2);
+ printf("distance: %f vs %f\n", dist2, dist1);
+ if (config.passes == 1) {
+ if (config.num_colors == 16 && config.dither == 2) {
+ // TODO(szabadka) Fix this case.
+ EXPECT_LT(dist2, dist1 * 1.5);
+ } else {
+ EXPECT_LT(dist2, dist1 * 1.05);
+ }
+ } else if (config.num_colors > 64) {
+ // TODO(szabadka) Fix 2pass quantization for <= 64 colors.
+ EXPECT_LT(dist2, dist1 * 1.1);
+ } else if (config.num_colors > 32) {
+ EXPECT_LT(dist2, dist1 * 1.2);
+ } else {
+ EXPECT_LT(dist2, dist1 * 1.7);
+ }
+}
+
+std::vector<TestConfig> GenerateTests() {
+ std::vector<TestConfig> all_tests;
+ for (int num_colors = 8; num_colors <= 256; num_colors *= 2) {
+ for (int passes = 1; passes <= 2; ++passes) {
+ for (int dither = 0; dither < 3; dither += passes) {
+ TestConfig config;
+ config.num_colors = num_colors;
+ config.passes = passes;
+ config.dither = dither;
+ all_tests.push_back(config);
+ }
+ }
+ }
+ return all_tests;
+}
+
+std::ostream& operator<<(std::ostream& os, const TestConfig& c) {
+ static constexpr const char* kDitherModeStr[] = {"No", "Ordered", "FS"};
+ os << c.passes << "pass";
+ os << c.num_colors << "colors";
+ os << kDitherModeStr[c.dither] << "dither";
+ return os;
+}
+
+std::string TestDescription(const testing::TestParamInfo<TestConfig>& info) {
+ std::stringstream name;
+ name << info.param;
+ return name.str();
+}
+
+JXL_GTEST_INSTANTIATE_TEST_SUITE_P(JpegliColorQuantTest,
+ JpegliColorQuantTestParam,
+ testing::ValuesIn(GenerateTests()),
+ TestDescription);
+
+} // namespace
+} // namespace extras
+} // namespace jxl
+#endif // JPEGXL_ENABLE_JPEG
diff --git a/third_party/jpeg-xl/lib/extras/packed_image.h b/third_party/jpeg-xl/lib/extras/packed_image.h
new file mode 100644
index 0000000000..3eaf5a0c6d
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/packed_image.h
@@ -0,0 +1,170 @@
+// Copyright (c) the JPEG XL Project 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_PACKED_IMAGE_H_
+#define LIB_EXTRAS_PACKED_IMAGE_H_
+
+// Helper class for storing external (int or float, interleaved) images. This is
+// the common format used by other libraries and in the libjxl API.
+
+#include <jxl/codestream_header.h>
+#include <jxl/encode.h>
+#include <jxl/types.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "lib/jxl/common.h"
+
+namespace jxl {
+namespace extras {
+
+// Class representing an interleaved image with a bunch of channels.
+class PackedImage {
+ public:
+ PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format)
+ : PackedImage(xsize, ysize, format, CalcStride(format, xsize)) {}
+
+ PackedImage Copy() const {
+ PackedImage copy(xsize, ysize, format);
+ memcpy(reinterpret_cast<uint8_t*>(copy.pixels()),
+ reinterpret_cast<const uint8_t*>(pixels()), pixels_size);
+ return copy;
+ }
+
+ // The interleaved pixels as defined in the storage format.
+ void* pixels() const { return pixels_.get(); }
+
+ // The image size in pixels.
+ size_t xsize;
+ size_t ysize;
+
+ // The number of bytes per row.
+ size_t stride;
+
+ // Pixel storage format and buffer size of the pixels_ pointer.
+ JxlPixelFormat format;
+ size_t pixels_size;
+
+ size_t pixel_stride() const {
+ return (BitsPerChannel(format.data_type) * format.num_channels /
+ jxl::kBitsPerByte);
+ }
+
+ static size_t BitsPerChannel(JxlDataType data_type) {
+ switch (data_type) {
+ case JXL_TYPE_UINT8:
+ return 8;
+ case JXL_TYPE_UINT16:
+ return 16;
+ case JXL_TYPE_FLOAT:
+ return 32;
+ case JXL_TYPE_FLOAT16:
+ return 16;
+ default:
+ JXL_ABORT("Unhandled JxlDataType");
+ }
+ }
+
+ private:
+ PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format,
+ size_t stride)
+ : xsize(xsize),
+ ysize(ysize),
+ stride(stride),
+ format(format),
+ pixels_size(ysize * stride),
+ pixels_(malloc(std::max<size_t>(1, pixels_size)), free) {}
+
+ static size_t CalcStride(const JxlPixelFormat& format, size_t xsize) {
+ size_t stride = xsize * (BitsPerChannel(format.data_type) *
+ format.num_channels / jxl::kBitsPerByte);
+ if (format.align > 1) {
+ stride = jxl::DivCeil(stride, format.align) * format.align;
+ }
+ return stride;
+ }
+
+ std::unique_ptr<void, decltype(free)*> pixels_;
+};
+
+// Helper class representing a frame, as seen from the API. Animations will have
+// multiple frames, but a single frame can have a color/grayscale channel and
+// multiple extra channels. The order of the extra channels should be the same
+// as all other frames in the same image.
+class PackedFrame {
+ public:
+ template <typename... Args>
+ explicit PackedFrame(Args&&... args) : color(std::forward<Args>(args)...) {}
+
+ PackedFrame Copy() const {
+ PackedFrame copy(color.xsize, color.ysize, color.format);
+ copy.frame_info = frame_info;
+ copy.name = name;
+ copy.color = color.Copy();
+ for (size_t i = 0; i < extra_channels.size(); ++i) {
+ PackedImage ec = extra_channels[i].Copy();
+ copy.extra_channels.emplace_back(std::move(ec));
+ }
+ return copy;
+ }
+
+ // The Frame metadata.
+ JxlFrameHeader frame_info = {};
+ std::string name;
+
+ // The pixel data for the color (or grayscale) channels.
+ PackedImage color;
+ // Extra channel image data.
+ std::vector<PackedImage> extra_channels;
+};
+
+// Optional metadata associated with a file
+class PackedMetadata {
+ public:
+ std::vector<uint8_t> exif;
+ std::vector<uint8_t> iptc;
+ std::vector<uint8_t> jumbf;
+ std::vector<uint8_t> xmp;
+};
+
+// The extra channel metadata information.
+struct PackedExtraChannel {
+ JxlExtraChannelInfo ec_info;
+ size_t index;
+ std::string name;
+};
+
+// Helper class representing a JXL image file as decoded to pixels from the API.
+class PackedPixelFile {
+ public:
+ JxlBasicInfo info = {};
+
+ std::vector<PackedExtraChannel> extra_channels_info;
+
+ // Color information of the decoded pixels.
+ // If the icc is empty, the JxlColorEncoding should be used instead.
+ std::vector<uint8_t> icc;
+ JxlColorEncoding color_encoding = {};
+ // The icc profile of the original image.
+ std::vector<uint8_t> orig_icc;
+
+ std::unique_ptr<PackedFrame> preview_frame;
+ std::vector<PackedFrame> frames;
+
+ PackedMetadata metadata;
+ PackedPixelFile() { JxlEncoderInitBasicInfo(&info); };
+};
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_PACKED_IMAGE_H_
diff --git a/third_party/jpeg-xl/lib/extras/packed_image_convert.cc b/third_party/jpeg-xl/lib/extras/packed_image_convert.cc
new file mode 100644
index 0000000000..1dd2b45a7f
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/packed_image_convert.cc
@@ -0,0 +1,300 @@
+// Copyright (c) the JPEG XL Project 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/packed_image_convert.h"
+
+#include <jxl/color_encoding.h>
+#include <jxl/types.h>
+
+#include <cstdint>
+
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/dec_external_image.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/enc_external_image.h"
+#include "lib/jxl/enc_image_bundle.h"
+#include "lib/jxl/luminance.h"
+
+namespace jxl {
+namespace extras {
+
+Status ConvertPackedFrameToImageBundle(const JxlBasicInfo& info,
+ const PackedFrame& frame,
+ const CodecInOut& io, ThreadPool* pool,
+ ImageBundle* bundle) {
+ JXL_ASSERT(frame.color.pixels() != nullptr);
+ const bool float_in = frame.color.format.data_type == JXL_TYPE_FLOAT16 ||
+ frame.color.format.data_type == JXL_TYPE_FLOAT;
+ size_t frame_bits_per_sample =
+ float_in ? PackedImage::BitsPerChannel(frame.color.format.data_type)
+ : info.bits_per_sample;
+ JXL_ASSERT(frame_bits_per_sample != 0);
+ // It is ok for the frame.color.format.num_channels to not match the
+ // number of channels on the image.
+ JXL_ASSERT(1 <= frame.color.format.num_channels &&
+ frame.color.format.num_channels <= 4);
+
+ const Span<const uint8_t> span(
+ static_cast<const uint8_t*>(frame.color.pixels()),
+ frame.color.pixels_size);
+ JXL_ASSERT(Rect(frame.frame_info.layer_info.crop_x0,
+ frame.frame_info.layer_info.crop_y0,
+ frame.frame_info.layer_info.xsize,
+ frame.frame_info.layer_info.ysize)
+ .IsInside(Rect(0, 0, info.xsize, info.ysize)));
+ if (info.have_animation) {
+ bundle->duration = frame.frame_info.duration;
+ bundle->blend = frame.frame_info.layer_info.blend_info.blendmode > 0;
+ bundle->use_for_next_frame =
+ frame.frame_info.layer_info.save_as_reference > 0;
+ bundle->origin.x0 = frame.frame_info.layer_info.crop_x0;
+ bundle->origin.y0 = frame.frame_info.layer_info.crop_y0;
+ }
+ bundle->name = frame.name; // frame.frame_info.name_length is ignored here.
+ JXL_ASSERT(io.metadata.m.color_encoding.IsGray() ==
+ (frame.color.format.num_channels <= 2));
+
+ JXL_RETURN_IF_ERROR(ConvertFromExternal(
+ span, frame.color.xsize, frame.color.ysize, io.metadata.m.color_encoding,
+ frame_bits_per_sample, frame.color.format, pool, bundle));
+
+ bundle->extra_channels().resize(io.metadata.m.extra_channel_info.size());
+ for (size_t i = 0; i < frame.extra_channels.size(); i++) {
+ const auto& ppf_ec = frame.extra_channels[i];
+ bundle->extra_channels()[i] = ImageF(ppf_ec.xsize, ppf_ec.ysize);
+ JXL_CHECK(BufferToImageF(ppf_ec.format, ppf_ec.xsize, ppf_ec.ysize,
+ ppf_ec.pixels(), ppf_ec.pixels_size, pool,
+ &bundle->extra_channels()[i]));
+ }
+ return true;
+}
+
+Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
+ ThreadPool* pool, CodecInOut* io) {
+ const bool has_alpha = ppf.info.alpha_bits != 0;
+ JXL_ASSERT(!ppf.frames.empty());
+ if (has_alpha) {
+ JXL_ASSERT(ppf.info.alpha_bits == ppf.info.bits_per_sample);
+ JXL_ASSERT(ppf.info.alpha_exponent_bits ==
+ ppf.info.exponent_bits_per_sample);
+ }
+
+ const bool is_gray = ppf.info.num_color_channels == 1;
+ JXL_ASSERT(ppf.info.num_color_channels == 1 ||
+ ppf.info.num_color_channels == 3);
+
+ // Convert the image metadata
+ io->SetSize(ppf.info.xsize, ppf.info.ysize);
+ io->metadata.m.bit_depth.bits_per_sample = ppf.info.bits_per_sample;
+ io->metadata.m.bit_depth.exponent_bits_per_sample =
+ ppf.info.exponent_bits_per_sample;
+ io->metadata.m.bit_depth.floating_point_sample =
+ ppf.info.exponent_bits_per_sample != 0;
+ io->metadata.m.modular_16_bit_buffer_sufficient =
+ ppf.info.exponent_bits_per_sample == 0 && ppf.info.bits_per_sample <= 12;
+
+ io->metadata.m.SetAlphaBits(ppf.info.alpha_bits,
+ ppf.info.alpha_premultiplied);
+
+ io->metadata.m.xyb_encoded = !ppf.info.uses_original_profile;
+ JXL_ASSERT(ppf.info.orientation > 0 && ppf.info.orientation <= 8);
+ io->metadata.m.orientation = ppf.info.orientation;
+
+ // Convert animation metadata
+ JXL_ASSERT(ppf.frames.size() == 1 || ppf.info.have_animation);
+ io->metadata.m.have_animation = ppf.info.have_animation;
+ io->metadata.m.animation.tps_numerator = ppf.info.animation.tps_numerator;
+ io->metadata.m.animation.tps_denominator = ppf.info.animation.tps_denominator;
+ io->metadata.m.animation.num_loops = ppf.info.animation.num_loops;
+
+ // Convert the color encoding.
+ if (!ppf.icc.empty()) {
+ PaddedBytes icc;
+ icc.append(ppf.icc);
+ if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) {
+ fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB\n");
+ io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
+ } else {
+ if (io->metadata.m.color_encoding.IsGray() != is_gray) {
+ // E.g. JPG image has 3 channels, but gray ICC.
+ return JXL_FAILURE("Embedded ICC does not match image color type");
+ }
+ }
+ } else {
+ JXL_RETURN_IF_ERROR(ConvertExternalToInternalColorEncoding(
+ ppf.color_encoding, &io->metadata.m.color_encoding));
+ if (io->metadata.m.color_encoding.ICC().empty()) {
+ return JXL_FAILURE("Failed to serialize ICC");
+ }
+ }
+
+ // Convert the extra blobs
+ io->blobs.exif = ppf.metadata.exif;
+ io->blobs.iptc = ppf.metadata.iptc;
+ io->blobs.jumbf = ppf.metadata.jumbf;
+ io->blobs.xmp = ppf.metadata.xmp;
+
+ // Append all other extra channels.
+ for (const auto& info : ppf.extra_channels_info) {
+ ExtraChannelInfo out;
+ out.type = static_cast<jxl::ExtraChannel>(info.ec_info.type);
+ out.bit_depth.bits_per_sample = info.ec_info.bits_per_sample;
+ out.bit_depth.exponent_bits_per_sample =
+ info.ec_info.exponent_bits_per_sample;
+ out.bit_depth.floating_point_sample =
+ info.ec_info.exponent_bits_per_sample != 0;
+ out.dim_shift = info.ec_info.dim_shift;
+ out.name = info.name;
+ out.alpha_associated = (info.ec_info.alpha_premultiplied != 0);
+ out.spot_color[0] = info.ec_info.spot_color[0];
+ out.spot_color[1] = info.ec_info.spot_color[1];
+ out.spot_color[2] = info.ec_info.spot_color[2];
+ out.spot_color[3] = info.ec_info.spot_color[3];
+ io->metadata.m.extra_channel_info.push_back(std::move(out));
+ }
+
+ // Convert the preview
+ if (ppf.preview_frame) {
+ size_t preview_xsize = ppf.preview_frame->color.xsize;
+ size_t preview_ysize = ppf.preview_frame->color.ysize;
+ io->metadata.m.have_preview = true;
+ JXL_RETURN_IF_ERROR(
+ io->metadata.m.preview_size.Set(preview_xsize, preview_ysize));
+ JXL_RETURN_IF_ERROR(ConvertPackedFrameToImageBundle(
+ ppf.info, *ppf.preview_frame, *io, pool, &io->preview_frame));
+ }
+
+ // Convert the pixels
+ io->frames.clear();
+ for (const auto& frame : ppf.frames) {
+ ImageBundle bundle(&io->metadata.m);
+ JXL_RETURN_IF_ERROR(
+ ConvertPackedFrameToImageBundle(ppf.info, frame, *io, pool, &bundle));
+ io->frames.push_back(std::move(bundle));
+ }
+
+ if (ppf.info.exponent_bits_per_sample == 0) {
+ // uint case.
+ io->metadata.m.bit_depth.bits_per_sample = io->Main().DetectRealBitdepth();
+ }
+ if (ppf.info.intensity_target != 0) {
+ io->metadata.m.SetIntensityTarget(ppf.info.intensity_target);
+ } else {
+ SetIntensityTarget(&io->metadata.m);
+ }
+ io->CheckMetadata();
+ return true;
+}
+
+// Allows converting from internal CodecInOut to external PackedPixelFile
+Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io,
+ const JxlPixelFormat& pixel_format,
+ const ColorEncoding& c_desired,
+ ThreadPool* pool,
+ PackedPixelFile* ppf) {
+ const bool has_alpha = io.metadata.m.HasAlpha();
+ bool alpha_premultiplied = false;
+ JXL_ASSERT(!io.frames.empty());
+
+ if (has_alpha) {
+ JXL_ASSERT(io.metadata.m.GetAlphaBits() ==
+ io.metadata.m.bit_depth.bits_per_sample);
+ const auto* alpha_channel = io.metadata.m.Find(ExtraChannel::kAlpha);
+ JXL_ASSERT(alpha_channel->bit_depth.exponent_bits_per_sample ==
+ io.metadata.m.bit_depth.exponent_bits_per_sample);
+ alpha_premultiplied = alpha_channel->alpha_associated;
+ }
+
+ // Convert the image metadata
+ ppf->info.xsize = io.metadata.size.xsize();
+ ppf->info.ysize = io.metadata.size.ysize();
+ ppf->info.num_color_channels = io.metadata.m.color_encoding.Channels();
+ ppf->info.bits_per_sample = io.metadata.m.bit_depth.bits_per_sample;
+ ppf->info.exponent_bits_per_sample =
+ io.metadata.m.bit_depth.exponent_bits_per_sample;
+
+ ppf->info.intensity_target = io.metadata.m.tone_mapping.intensity_target;
+ ppf->info.linear_below = io.metadata.m.tone_mapping.linear_below;
+ ppf->info.min_nits = io.metadata.m.tone_mapping.min_nits;
+ ppf->info.relative_to_max_display =
+ io.metadata.m.tone_mapping.relative_to_max_display;
+
+ ppf->info.alpha_bits = io.metadata.m.GetAlphaBits();
+ ppf->info.alpha_premultiplied = alpha_premultiplied;
+
+ ppf->info.uses_original_profile = !io.metadata.m.xyb_encoded;
+ JXL_ASSERT(0 < io.metadata.m.orientation && io.metadata.m.orientation <= 8);
+ ppf->info.orientation =
+ static_cast<JxlOrientation>(io.metadata.m.orientation);
+ ppf->info.num_color_channels = io.metadata.m.color_encoding.Channels();
+
+ // Convert animation metadata
+ JXL_ASSERT(io.frames.size() == 1 || io.metadata.m.have_animation);
+ ppf->info.have_animation = io.metadata.m.have_animation;
+ ppf->info.animation.tps_numerator = io.metadata.m.animation.tps_numerator;
+ ppf->info.animation.tps_denominator = io.metadata.m.animation.tps_denominator;
+ ppf->info.animation.num_loops = io.metadata.m.animation.num_loops;
+
+ // Convert the color encoding
+ ppf->icc.assign(c_desired.ICC().begin(), c_desired.ICC().end());
+ ConvertInternalToExternalColorEncoding(c_desired, &ppf->color_encoding);
+
+ // Convert the extra blobs
+ ppf->metadata.exif = io.blobs.exif;
+ ppf->metadata.iptc = io.blobs.iptc;
+ ppf->metadata.jumbf = io.blobs.jumbf;
+ ppf->metadata.xmp = io.blobs.xmp;
+ const bool float_out = pixel_format.data_type == JXL_TYPE_FLOAT ||
+ pixel_format.data_type == JXL_TYPE_FLOAT16;
+ // Convert the pixels
+ ppf->frames.clear();
+ for (const auto& frame : io.frames) {
+ JXL_ASSERT(frame.metadata()->bit_depth.bits_per_sample != 0);
+ // It is ok for the frame.color().kNumPlanes to not match the
+ // number of channels on the image.
+ const uint32_t num_channels =
+ frame.metadata()->color_encoding.Channels() + has_alpha;
+ JxlPixelFormat format{/*num_channels=*/num_channels,
+ /*data_type=*/pixel_format.data_type,
+ /*endianness=*/pixel_format.endianness,
+ /*align=*/pixel_format.align};
+
+ PackedFrame packed_frame(frame.oriented_xsize(), frame.oriented_ysize(),
+ format);
+ const size_t bits_per_sample =
+ float_out ? packed_frame.color.BitsPerChannel(pixel_format.data_type)
+ : ppf->info.bits_per_sample;
+ packed_frame.name = frame.name;
+ packed_frame.frame_info.name_length = frame.name.size();
+ // Color transform
+ ImageBundle ib = frame.Copy();
+ const ImageBundle* to_color_transform = &ib;
+ ImageMetadata metadata = io.metadata.m;
+ ImageBundle store(&metadata);
+ const ImageBundle* transformed;
+ // TODO(firsching): handle the transform here.
+ JXL_RETURN_IF_ERROR(TransformIfNeeded(*to_color_transform, c_desired,
+ GetJxlCms(), pool, &store,
+ &transformed));
+
+ JXL_RETURN_IF_ERROR(ConvertToExternal(
+ *transformed, bits_per_sample, float_out, format.num_channels,
+ format.endianness,
+ /* stride_out=*/packed_frame.color.stride, pool,
+ packed_frame.color.pixels(), packed_frame.color.pixels_size,
+ /*out_callback=*/{}, frame.metadata()->GetOrientation()));
+
+ // TODO(firsching): Convert the extra channels, beside one potential alpha
+ // channel. FIXME!
+ JXL_CHECK(frame.extra_channels().size() <= has_alpha);
+ ppf->frames.push_back(std::move(packed_frame));
+ }
+
+ return true;
+}
+} // namespace extras
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/packed_image_convert.h b/third_party/jpeg-xl/lib/extras/packed_image_convert.h
new file mode 100644
index 0000000000..100adccc09
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/packed_image_convert.h
@@ -0,0 +1,36 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_PACKED_IMAGE_CONVERT_H_
+#define LIB_EXTRAS_PACKED_IMAGE_CONVERT_H_
+
+// Helper functions to convert from the external image types to the internal
+// CodecInOut to help transitioning to the external types.
+
+#include <jxl/types.h>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/codec_in_out.h"
+
+namespace jxl {
+namespace extras {
+
+// Converts an external PackedPixelFile to the internal CodecInOut for use with
+// internal functions directly.
+Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
+ ThreadPool* pool, CodecInOut* io);
+
+// Converts an internal CodecInOut for use with internal function to an external
+// PackedPixelFile.
+Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io,
+ const JxlPixelFormat& pixel_format,
+ const ColorEncoding& c_desired,
+ ThreadPool* pool,
+ PackedPixelFile* ppf);
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_PACKED_IMAGE_CONVERT_H_
diff --git a/third_party/jpeg-xl/lib/extras/size_constraints.h b/third_party/jpeg-xl/lib/extras/size_constraints.h
new file mode 100644
index 0000000000..cf06f8cb22
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/size_constraints.h
@@ -0,0 +1,43 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_JXL_SIZE_CONSTRAINTS_H_
+#define LIB_JXL_SIZE_CONSTRAINTS_H_
+
+#include <cstdint>
+#include <type_traits>
+
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints {
+ // Upper limit on pixel dimensions/area, enforced by VerifyDimensions
+ // (called from decoders). Fuzzers set smaller values to limit memory use.
+ uint32_t dec_max_xsize = 0xFFFFFFFFu;
+ uint32_t dec_max_ysize = 0xFFFFFFFFu;
+ uint64_t dec_max_pixels = 0xFFFFFFFFu; // Might be up to ~0ull
+};
+
+template <typename T,
+ class = typename std::enable_if<std::is_unsigned<T>::value>::type>
+Status VerifyDimensions(const SizeConstraints* constraints, T xs, T ys) {
+ if (!constraints) return true;
+
+ if (xs == 0 || ys == 0) return JXL_FAILURE("Empty image.");
+ if (xs > constraints->dec_max_xsize) return JXL_FAILURE("Image too wide.");
+ if (ys > constraints->dec_max_ysize) return JXL_FAILURE("Image too tall.");
+
+ const uint64_t num_pixels = static_cast<uint64_t>(xs) * ys;
+ if (num_pixels > constraints->dec_max_pixels) {
+ return JXL_FAILURE("Image too big.");
+ }
+
+ return true;
+}
+
+} // namespace jxl
+
+#endif // LIB_JXL_SIZE_CONSTRAINTS_H_
diff --git a/third_party/jpeg-xl/lib/extras/time.cc b/third_party/jpeg-xl/lib/extras/time.cc
new file mode 100644
index 0000000000..73d1b8f260
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/time.cc
@@ -0,0 +1,60 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/time.h"
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <ctime>
+
+#include "lib/jxl/base/os_macros.h" // for JXL_OS_*
+
+#if JXL_OS_WIN
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif // NOMINMAX
+#include <windows.h>
+#endif // JXL_OS_WIN
+
+#if JXL_OS_MAC
+#include <mach/mach.h>
+#include <mach/mach_time.h>
+#endif // JXL_OS_MAC
+
+#if JXL_OS_HAIKU
+#include <OS.h>
+#endif // JXL_OS_HAIKU
+
+namespace jxl {
+
+double Now() {
+#if JXL_OS_WIN
+ LARGE_INTEGER counter;
+ (void)QueryPerformanceCounter(&counter);
+ LARGE_INTEGER freq;
+ (void)QueryPerformanceFrequency(&freq);
+ return double(counter.QuadPart) / freq.QuadPart;
+#elif JXL_OS_MAC
+ const auto t = mach_absolute_time();
+ // On OSX/iOS platform the elapsed time is cpu time unit
+ // We have to query the time base information to convert it back
+ // See https://developer.apple.com/library/mac/qa/qa1398/_index.html
+ static mach_timebase_info_data_t timebase;
+ if (timebase.denom == 0) {
+ (void)mach_timebase_info(&timebase);
+ }
+ return double(t) * timebase.numer / timebase.denom * 1E-9;
+#elif JXL_OS_HAIKU
+ return double(system_time_nsecs()) * 1E-9;
+#else
+ timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return t.tv_sec + t.tv_nsec * 1E-9;
+#endif
+}
+
+} // namespace jxl
diff --git a/third_party/jpeg-xl/lib/extras/time.h b/third_party/jpeg-xl/lib/extras/time.h
new file mode 100644
index 0000000000..c71414b877
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/time.h
@@ -0,0 +1,19 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_TIME_H_
+#define LIB_EXTRAS_TIME_H_
+
+// OS-specific function for timing.
+
+namespace jxl {
+
+// Returns current time [seconds] from a monotonic clock with unspecified
+// starting point - only suitable for computing elapsed time.
+double Now();
+
+} // namespace jxl
+
+#endif // LIB_EXTRAS_TIME_H_
diff --git a/third_party/jpeg-xl/lib/extras/tone_mapping.cc b/third_party/jpeg-xl/lib/extras/tone_mapping.cc
new file mode 100644
index 0000000000..1cdd6ed826
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/tone_mapping.cc
@@ -0,0 +1,132 @@
+// Copyright (c) the JPEG XL Project 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/tone_mapping.h"
+
+#undef HWY_TARGET_INCLUDE
+#define HWY_TARGET_INCLUDE "lib/extras/tone_mapping.cc"
+#include <hwy/foreach_target.h>
+#include <hwy/highway.h>
+
+#include "lib/jxl/dec_tone_mapping-inl.h"
+#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/image_bundle.h"
+
+HWY_BEFORE_NAMESPACE();
+namespace jxl {
+namespace HWY_NAMESPACE {
+
+static constexpr float rec2020_luminances[3] = {0.2627f, 0.6780f, 0.0593f};
+
+Status ToneMapFrame(const std::pair<float, float> display_nits,
+ ImageBundle* const ib, ThreadPool* const pool) {
+ // Perform tone mapping as described in Report ITU-R BT.2390-8, section 5.4
+ // (pp. 23-25).
+ // https://www.itu.int/pub/R-REP-BT.2390-8-2020
+
+ HWY_FULL(float) df;
+ using V = decltype(Zero(df));
+
+ ColorEncoding linear_rec2020;
+ linear_rec2020.SetColorSpace(ColorSpace::kRGB);
+ linear_rec2020.primaries = Primaries::k2100;
+ linear_rec2020.white_point = WhitePoint::kD65;
+ linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
+ JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
+ JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool));
+
+ Rec2408ToneMapper<decltype(df)> tone_mapper(
+ {ib->metadata()->tone_mapping.min_nits,
+ ib->metadata()->IntensityTarget()},
+ display_nits, rec2020_luminances);
+
+ return RunOnPool(
+ pool, 0, ib->ysize(), ThreadPool::NoInit,
+ [&](const uint32_t y, size_t /* thread */) {
+ float* const JXL_RESTRICT row_r = ib->color()->PlaneRow(0, y);
+ float* const JXL_RESTRICT row_g = ib->color()->PlaneRow(1, y);
+ float* const JXL_RESTRICT row_b = ib->color()->PlaneRow(2, y);
+ for (size_t x = 0; x < ib->xsize(); x += Lanes(df)) {
+ V red = Load(df, row_r + x);
+ V green = Load(df, row_g + x);
+ V blue = Load(df, row_b + x);
+ tone_mapper.ToneMap(&red, &green, &blue);
+ Store(red, df, row_r + x);
+ Store(green, df, row_g + x);
+ Store(blue, df, row_b + x);
+ }
+ },
+ "ToneMap");
+}
+
+Status GamutMapFrame(ImageBundle* const ib, float preserve_saturation,
+ ThreadPool* const pool) {
+ HWY_FULL(float) df;
+ using V = decltype(Zero(df));
+
+ ColorEncoding linear_rec2020;
+ linear_rec2020.SetColorSpace(ColorSpace::kRGB);
+ linear_rec2020.primaries = Primaries::k2100;
+ linear_rec2020.white_point = WhitePoint::kD65;
+ linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
+ JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
+ JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool));
+
+ JXL_RETURN_IF_ERROR(RunOnPool(
+ pool, 0, ib->ysize(), ThreadPool::NoInit,
+ [&](const uint32_t y, size_t /* thread*/) {
+ float* const JXL_RESTRICT row_r = ib->color()->PlaneRow(0, y);
+ float* const JXL_RESTRICT row_g = ib->color()->PlaneRow(1, y);
+ float* const JXL_RESTRICT row_b = ib->color()->PlaneRow(2, y);
+ for (size_t x = 0; x < ib->xsize(); x += Lanes(df)) {
+ V red = Load(df, row_r + x);
+ V green = Load(df, row_g + x);
+ V blue = Load(df, row_b + x);
+ GamutMap(&red, &green, &blue, rec2020_luminances,
+ preserve_saturation);
+ Store(red, df, row_r + x);
+ Store(green, df, row_g + x);
+ Store(blue, df, row_b + x);
+ }
+ },
+ "GamutMap"));
+
+ return true;
+}
+
+// NOLINTNEXTLINE(google-readability-namespace-comments)
+} // namespace HWY_NAMESPACE
+} // namespace jxl
+HWY_AFTER_NAMESPACE();
+
+#if HWY_ONCE
+namespace jxl {
+
+namespace {
+HWY_EXPORT(ToneMapFrame);
+HWY_EXPORT(GamutMapFrame);
+} // namespace
+
+Status ToneMapTo(const std::pair<float, float> display_nits,
+ CodecInOut* const io, ThreadPool* const pool) {
+ const auto tone_map_frame = HWY_DYNAMIC_DISPATCH(ToneMapFrame);
+ for (ImageBundle& ib : io->frames) {
+ JXL_RETURN_IF_ERROR(tone_map_frame(display_nits, &ib, pool));
+ }
+ io->metadata.m.SetIntensityTarget(display_nits.second);
+ return true;
+}
+
+Status GamutMap(CodecInOut* const io, float preserve_saturation,
+ ThreadPool* const pool) {
+ const auto gamut_map_frame = HWY_DYNAMIC_DISPATCH(GamutMapFrame);
+ for (ImageBundle& ib : io->frames) {
+ JXL_RETURN_IF_ERROR(gamut_map_frame(&ib, preserve_saturation, pool));
+ }
+ return true;
+}
+
+} // namespace jxl
+#endif
diff --git a/third_party/jpeg-xl/lib/extras/tone_mapping.h b/third_party/jpeg-xl/lib/extras/tone_mapping.h
new file mode 100644
index 0000000000..1f474101eb
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/tone_mapping.h
@@ -0,0 +1,30 @@
+// Copyright (c) the JPEG XL Project 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_TONE_MAPPING_H_
+#define LIB_EXTRAS_TONE_MAPPING_H_
+
+#include "lib/jxl/codec_in_out.h"
+
+namespace jxl {
+
+// Important: after calling this, the result will contain many out-of-gamut
+// colors. It is very strongly recommended to call GamutMap afterwards to
+// rectify this.
+Status ToneMapTo(std::pair<float, float> display_nits, CodecInOut* io,
+ ThreadPool* pool = nullptr);
+
+// `preserve_saturation` indicates to what extent to favor saturation over
+// luminance when mapping out-of-gamut colors to Rec. 2020. 0 preserves
+// luminance at the complete expense of saturation, while 1 gives the most
+// saturated color with the same hue that Rec. 2020 can represent even if it
+// means lowering the luminance. Values in between correspond to linear mixtures
+// of those two extremes.
+Status GamutMap(CodecInOut* io, float preserve_saturation,
+ ThreadPool* pool = nullptr);
+
+} // namespace jxl
+
+#endif // LIB_EXTRAS_TONE_MAPPING_H_
diff --git a/third_party/jpeg-xl/lib/extras/tone_mapping_gbench.cc b/third_party/jpeg-xl/lib/extras/tone_mapping_gbench.cc
new file mode 100644
index 0000000000..f1d5357345
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/tone_mapping_gbench.cc
@@ -0,0 +1,40 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "benchmark/benchmark.h"
+#include "lib/extras/codec.h"
+#include "lib/extras/tone_mapping.h"
+#include "lib/jxl/enc_color_management.h"
+
+namespace jxl {
+
+static void BM_ToneMapping(benchmark::State& state) {
+ Image3F color(2268, 1512);
+ FillImage(0.5f, &color);
+
+ // Use linear Rec. 2020 so that `ToneMapTo` doesn't have to convert to it and
+ // we mainly measure the tone mapping itself.
+ ColorEncoding linear_rec2020;
+ linear_rec2020.SetColorSpace(ColorSpace::kRGB);
+ linear_rec2020.primaries = Primaries::k2100;
+ linear_rec2020.white_point = WhitePoint::kD65;
+ linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
+ JXL_CHECK(linear_rec2020.CreateICC());
+
+ for (auto _ : state) {
+ state.PauseTiming();
+ CodecInOut tone_mapping_input;
+ tone_mapping_input.SetFromImage(CopyImage(color), linear_rec2020);
+ tone_mapping_input.metadata.m.SetIntensityTarget(255);
+ state.ResumeTiming();
+
+ JXL_CHECK(ToneMapTo({0.1, 100}, &tone_mapping_input));
+ }
+
+ state.SetItemsProcessed(state.iterations() * color.xsize() * color.ysize());
+}
+BENCHMARK(BM_ToneMapping);
+
+} // namespace jxl