summaryrefslogtreecommitdiffstats
path: root/third_party/jpeg-xl/lib/extras/dec
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/jpeg-xl/lib/extras/dec
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/jpeg-xl/lib/extras/dec')
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/apng.cc996
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/apng.h35
-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.cc37
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/color_hints.cc78
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/color_hints.h74
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/decode.cc148
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/decode.h57
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/exr.cc201
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/exr.h33
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/gif.cc415
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/gif.h35
-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.cc338
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jpg.h45
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jxl.cc572
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/jxl.h73
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pgx.cc202
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pgx.h34
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pgx_test.cc80
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pnm.cc575
-rw-r--r--third_party/jpeg-xl/lib/extras/dec/pnm.h68
24 files changed, 4649 insertions, 0 deletions
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..f77dab77d1
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/apng.cc
@@ -0,0 +1,996 @@
+// 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 <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/common.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/sanitizers.h"
+#if JPEGXL_ENABLE_APNG
+#include "png.h" /* original (unpatched) libpng is ok */
+#endif
+
+namespace jxl {
+namespace extras {
+
+#if JPEGXL_ENABLE_APNG
+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
+#endif
+
+bool CanDecodeAPNG() {
+#if JPEGXL_ENABLE_APNG
+ return true;
+#else
+ return false;
+#endif
+}
+
+Status DecodeImageAPNG(const Span<const uint8_t> bytes,
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+#if JPEGXL_ENABLE_APNG
+ 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;
+ bool seenFctl = 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;
+ seenFctl |= (id == kId_fcTL);
+
+ 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
+ if (seenFctl) {
+ // `fcTL` chunk must appear after all `IDAT` chunks
+ return JXL_FAILURE("IDAT chunk after fcTL chunk");
+ }
+ 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 if (sigbits) {
+ int maxbps = std::max(sigbits->red,
+ std::max(sigbits->green, sigbits->blue));
+ JXL_WARNING(
+ "sBIT chunk: bit depths for R, G, and B are not the same (%i "
+ "%i %i), while in JPEG XL they have to be the same. Setting "
+ "RGB bit depth to %i.",
+ sigbits->red, sigbits->green, sigbits->blue, maxbps);
+ ppf->info.bits_per_sample = maxbps;
+ }
+ } 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 && sigbits->alpha != ppf->info.bits_per_sample) {
+ JXL_WARNING(
+ "sBIT chunk: bit depths for RGBA are inconsistent "
+ "(%i %i %i %i). Setting A bitdepth to %i.",
+ sigbits->red, sigbits->green, sigbits->blue, sigbits->alpha,
+ ppf->info.bits_per_sample);
+ }
+ } 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) {
+ if (!hasInfo) {
+ return JXL_FAILURE("fDAT chunk before iDAT");
+ }
+ 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 placeholder 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;
+#else
+ return false;
+#endif
+}
+
+} // 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..d91364b1e6
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/apng.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_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/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+bool CanDecodeAPNG();
+
+// 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..e6e34f0edf
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/color_description_test.cc
@@ -0,0 +1,37 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#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(c_internal.FromExternal(c_external));
+ 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..5c6d7b84a0
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/color_hints.cc
@@ -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.
+
+#include "lib/extras/dec/color_hints.h"
+
+#include <jxl/encode.h>
+
+#include <vector>
+
+#include "lib/extras/dec/color_description.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+Status ApplyColorHints(const ColorHints& color_hints,
+ const bool color_already_set, const bool is_gray,
+ PackedPixelFile* ppf) {
+ bool got_color_space = color_already_set;
+
+ JXL_RETURN_IF_ERROR(color_hints.Foreach(
+ [color_already_set, is_gray, ppf, &got_color_space](
+ const std::string& key, const std::string& value) -> Status {
+ if (color_already_set && (key == "color_space" || key == "icc")) {
+ JXL_WARNING("Decoder ignoring %s hint", key.c_str());
+ return true;
+ }
+ 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") {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data());
+ std::vector<uint8_t> icc(data, data + value.size());
+ ppf->icc.swap(icc);
+ got_color_space = true;
+ } else if (key == "exif") {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data());
+ std::vector<uint8_t> blob(data, data + value.size());
+ ppf->metadata.exif.swap(blob);
+ } else if (key == "xmp") {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data());
+ std::vector<uint8_t> blob(data, data + value.size());
+ ppf->metadata.xmp.swap(blob);
+ } else if (key == "jumbf") {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data());
+ std::vector<uint8_t> blob(data, data + value.size());
+ ppf->metadata.jumbf.swap(blob);
+ } 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..036f203e26
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/color_hints.h
@@ -0,0 +1,74 @@
+// 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.
+// Besides color space information, 'ColorHints' may also include other
+// additional information such as Exif, XMP and JUMBF metadata.
+
+#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..b3ca711bb2
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/decode.cc
@@ -0,0 +1,148 @@
+// 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>
+
+#include "lib/extras/dec/apng.h"
+#include "lib/extras/dec/exr.h"
+#include "lib/extras/dec/gif.h"
+#include "lib/extras/dec/jpg.h"
+#include "lib/extras/dec/jxl.h"
+#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;
+
+std::string GetExtension(const std::string& path) {
+ // Pattern: "name.png"
+ size_t pos = path.find_last_of('.');
+ if (pos != std::string::npos) {
+ return path.substr(pos);
+ }
+
+ // Extension not found
+ return "";
+}
+
+} // namespace
+
+Codec CodecFromPath(std::string path, size_t* JXL_RESTRICT bits_per_sample,
+ std::string* extension) {
+ std::string ext = GetExtension(path);
+ if (extension) {
+ if (extension->empty()) {
+ *extension = ext;
+ } else {
+ ext = *extension;
+ }
+ }
+ std::transform(ext.begin(), ext.end(), ext.begin(), [](char c) {
+ return std::tolower(c, std::locale::classic());
+ });
+ if (ext == ".png") return Codec::kPNG;
+
+ if (ext == ".jpg") return Codec::kJPG;
+ if (ext == ".jpeg") return Codec::kJPG;
+
+ if (ext == ".pgx") return Codec::kPGX;
+
+ if (ext == ".pam") return Codec::kPNM;
+ if (ext == ".pnm") return Codec::kPNM;
+ if (ext == ".pgm") return Codec::kPNM;
+ if (ext == ".ppm") return Codec::kPNM;
+ if (ext == ".pfm") {
+ if (bits_per_sample != nullptr) *bits_per_sample = 32;
+ return Codec::kPNM;
+ }
+
+ if (ext == ".gif") return Codec::kGIF;
+
+ if (ext == ".exr") return Codec::kEXR;
+
+ return Codec::kUnknown;
+}
+
+bool CanDecode(Codec codec) {
+ switch (codec) {
+ case Codec::kEXR:
+ return CanDecodeEXR();
+ case Codec::kGIF:
+ return CanDecodeGIF();
+ case Codec::kJPG:
+ return CanDecodeJPG();
+ case Codec::kPNG:
+ return CanDecodeAPNG();
+ case Codec::kPNM:
+ case Codec::kPGX:
+ case Codec::kJXL:
+ return true;
+ default:
+ return false;
+ }
+}
+
+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 (DecodeImageAPNG(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPNG;
+ }
+ if (DecodeImagePGX(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPGX;
+ }
+ if (DecodeImagePNM(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPNM;
+ }
+ JXLDecompressParams dparams = {};
+ for (const uint32_t num_channels : {1, 2, 3, 4}) {
+ dparams.accepted_formats.push_back(
+ {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0});
+ }
+ size_t decoded_bytes;
+ if (DecodeImageJXL(bytes.data(), bytes.size(), dparams, &decoded_bytes,
+ ppf) &&
+ ApplyColorHints(color_hints, true, ppf->info.num_color_channels == 1,
+ ppf)) {
+ return Codec::kJXL;
+ }
+ if (DecodeImageGIF(bytes, color_hints, ppf, constraints)) {
+ return Codec::kGIF;
+ }
+ if (DecodeImageJPG(bytes, color_hints, ppf, constraints)) {
+ return Codec::kJPG;
+ }
+ if (DecodeImageEXR(bytes, color_hints, ppf, constraints)) {
+ return Codec::kEXR;
+ }
+ 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..0d7dfcbef2
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/decode.h
@@ -0,0 +1,57 @@
+// 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 DecodeBytes.
+enum class Codec : uint32_t {
+ kUnknown, // for CodecFromPath
+ kPNG,
+ kPNM,
+ kPGX,
+ kJPG,
+ kGIF,
+ kEXR,
+ kJXL
+};
+
+bool CanDecode(Codec codec);
+
+// 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 CodecFromPath(std::string path,
+ size_t* JXL_RESTRICT bits_per_sample = nullptr,
+ std::string* extension = 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..821e0f4b21
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/exr.cc
@@ -0,0 +1,201 @@
+// 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"
+
+#if JPEGXL_ENABLE_EXR
+#include <ImfChromaticitiesAttribute.h>
+#include <ImfIO.h>
+#include <ImfRgbaFile.h>
+#include <ImfStandardAttributes.h>
+#endif
+
+#include <vector>
+
+namespace jxl {
+namespace extras {
+
+#if JPEGXL_ENABLE_EXR
+namespace {
+
+namespace OpenEXR = OPENEXR_IMF_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
+#endif
+
+bool CanDecodeEXR() {
+#if JPEGXL_ENABLE_EXR
+ return true;
+#else
+ return false;
+#endif
+}
+
+Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+#if JPEGXL_ENABLE_EXR
+ InMemoryIStream is(bytes);
+
+#ifdef __EXCEPTIONS
+ std::unique_ptr<OpenEXR::RgbaInputFile> input_ptr;
+ try {
+ input_ptr.reset(new OpenEXR::RgbaInputFile(is));
+ } catch (...) {
+ // silently return false if it is not an EXR file
+ return false;
+ }
+ 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;
+ // TODO(eustas): UB: OpenEXR::Rgba is not TriviallyCopyable
+ 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;
+#else
+ return false;
+#endif
+}
+
+} // 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..0605cbba06
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/exr.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_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/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+bool CanDecodeEXR();
+
+// 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..3d963941c0
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/gif.cc
@@ -0,0 +1,415 @@
+// 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"
+
+#if JPEGXL_ENABLE_GIF
+#include <gif_lib.h>
+#endif
+#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 {
+
+#if JPEGXL_ENABLE_GIF
+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
+#endif
+
+bool CanDecodeGIF() {
+#if JPEGXL_ENABLE_GIF
+ return true;
+#else
+ return false;
+#endif
+}
+
+Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+#if JPEGXL_ENABLE_GIF
+ 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;
+#else
+ return false;
+#endif
+}
+
+} // 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..4d5be8664e
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/gif.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_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 {
+
+bool CanDecodeGIF();
+
+// 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..3c8a4bccfe
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jpg.cc
@@ -0,0 +1,338 @@
+// 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"
+
+#if JPEGXL_ENABLE_JPEG
+#include <jpeglib.h>
+#include <setjmp.h>
+#endif
+#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 {
+
+#if JPEGXL_ENABLE_JPEG
+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
+#endif
+
+bool CanDecodeJPG() {
+#if JPEGXL_ENABLE_JPEG
+ return true;
+#else
+ return false;
+#endif
+}
+
+Status DecodeImageJPG(const Span<const uint8_t> bytes,
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints,
+ const JPGDecompressParams* dparams) {
+#if JPEGXL_ENABLE_JPEG
+ // 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();
+#else
+ return false;
+#endif
+}
+
+} // 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..6e7b2f786b
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jpg.h
@@ -0,0 +1,45 @@
+// 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/span.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints;
+
+namespace extras {
+
+bool CanDecodeJPG();
+
+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..f3e62c970a
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jxl.cc
@@ -0,0 +1,572 @@
+// 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/cms.h>
+#include <jxl/decode.h>
+#include <jxl/decode_cxx.h>
+#include <jxl/types.h>
+
+#include <cinttypes>
+
+#include "lib/extras/common.h"
+#include "lib/extras/dec/color_description.h"
+#include "lib/jxl/base/exif.h"
+#include "lib/jxl/base/printf_macros.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+struct BoxProcessor {
+ BoxProcessor(JxlDecoder* dec) : dec_(dec) { Reset(); }
+
+ void InitializeOutput(std::vector<uint8_t>* out) {
+ box_data_ = out;
+ AddMoreOutput();
+ }
+
+ bool AddMoreOutput() {
+ Flush();
+ static const size_t kBoxOutputChunkSize = 1 << 16;
+ box_data_->resize(box_data_->size() + kBoxOutputChunkSize);
+ next_out_ = box_data_->data() + total_size_;
+ avail_out_ = box_data_->size() - total_size_;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetBoxBuffer(dec_, next_out_, avail_out_)) {
+ fprintf(stderr, "JxlDecoderSetBoxBuffer failed\n");
+ return false;
+ }
+ return true;
+ }
+
+ void FinalizeOutput() {
+ if (box_data_ == nullptr) return;
+ Flush();
+ box_data_->resize(total_size_);
+ Reset();
+ }
+
+ private:
+ JxlDecoder* dec_;
+ std::vector<uint8_t>* box_data_;
+ uint8_t* next_out_;
+ size_t avail_out_;
+ size_t total_size_;
+
+ void Reset() {
+ box_data_ = nullptr;
+ next_out_ = nullptr;
+ avail_out_ = 0;
+ total_size_ = 0;
+ }
+ void Flush() {
+ if (box_data_ == nullptr) return;
+ size_t remaining = JxlDecoderReleaseBoxBuffer(dec_);
+ size_t bytes_written = avail_out_ - remaining;
+ next_out_ += bytes_written;
+ avail_out_ -= bytes_written;
+ total_size_ += bytes_written;
+ }
+};
+
+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) {
+ JxlSignature sig = JxlSignatureCheck(bytes, bytes_size);
+ // silently return false if this is not a JXL file
+ if (sig == JXL_SIG_INVALID) return false;
+
+ 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;
+
+ 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) {
+ // This bound is very likely to be enough to hold the entire
+ // reconstructed JPEG, to avoid having to do expensive retries.
+ jpeg_data_chunk.resize(bytes_size * 3 / 2 + 1024);
+ 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 (accepted_formats.empty()) {
+ // decoding just the metadata, not the pixel data
+ events ^= (JXL_DEC_FULL_IMAGE | JXL_DEC_PREVIEW_IMAGE);
+ }
+ }
+ 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 = accepted_formats.empty();
+ 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 (accepted_formats.empty()) continue;
+ 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 {
+ JxlDecoderSetCms(dec, *JxlGetDefaultCms());
+ 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;
+ ppf->color_encoding.color_space = JXL_COLOR_SPACE_UNKNOWN;
+ if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsEncodedProfile(
+ dec, target, &ppf->color_encoding) ||
+ dparams.need_icc) {
+ // only get ICC if it is not an Enum color encoding
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetICCProfileSize(dec, target, &icc_size)) {
+ fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
+ }
+ if (icc_size != 0) {
+ ppf->icc.resize(icc_size);
+ if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile(
+ dec, target, ppf->icc.data(), icc_size)) {
+ fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
+ return false;
+ }
+ }
+ }
+ icc_size = 0;
+ target = JXL_COLOR_PROFILE_TARGET_ORIGINAL;
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetICCProfileSize(dec, target, &icc_size)) {
+ fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
+ }
+ if (icc_size != 0) {
+ ppf->orig_icc.resize(icc_size);
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetColorAsICCProfile(dec, 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..cbada1f6dd
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/jxl.h
@@ -0,0 +1,73 @@
+// 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;
+
+ // Set to true if an ICC profile has to be synthesized for Enum color
+ // encodings
+ bool need_icc = 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..ce852e6965
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pgx.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_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/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..5dbc3149a2
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pgx_test.cc
@@ -0,0 +1,80 @@
+// 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 <cstring>
+
+#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 Bytes(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..4c4618d41d
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pnm.cc
@@ -0,0 +1,575 @@
+// 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 <mutex>
+
+#include "jxl/encode.h"
+#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 {
+
+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 Bytes(reinterpret_cast<const uint8_t*>(str), strlen(str));
+}
+
+} // namespace
+
+struct PNMChunkedInputFrame {
+ JxlChunkedFrameInputSource operator()() {
+ return JxlChunkedFrameInputSource{
+ this,
+ METHOD_TO_C_CALLBACK(
+ &PNMChunkedInputFrame::GetColorChannelsPixelFormat),
+ METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetColorChannelDataAt),
+ METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetExtraChannelPixelFormat),
+ METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::GetExtraChannelDataAt),
+ METHOD_TO_C_CALLBACK(&PNMChunkedInputFrame::ReleaseCurrentData)};
+ }
+
+ void GetColorChannelsPixelFormat(JxlPixelFormat* pixel_format) {
+ *pixel_format = format;
+ }
+
+ const void* GetColorChannelDataAt(size_t xpos, size_t ypos, size_t xsize,
+ size_t ysize, size_t* row_offset) {
+ const size_t bytes_per_channel =
+ DivCeil(dec->header_.bits_per_sample, jxl::kBitsPerByte);
+ const size_t num_channels = dec->header_.is_gray ? 1 : 3;
+ const size_t bytes_per_pixel = num_channels * bytes_per_channel;
+ *row_offset = dec->header_.xsize * bytes_per_pixel;
+ const size_t offset = ypos * *row_offset + xpos * bytes_per_pixel;
+ return dec->pnm_.data() + offset + dec->data_start_;
+ }
+
+ void GetExtraChannelPixelFormat(size_t ec_index,
+ JxlPixelFormat* pixel_format) {
+ JXL_ABORT("Not implemented");
+ }
+
+ const void* GetExtraChannelDataAt(size_t ec_index, size_t xpos, size_t ypos,
+ size_t xsize, size_t ysize,
+ size_t* row_offset) {
+ JXL_ABORT("Not implemented");
+ }
+
+ void ReleaseCurrentData(const void* buffer) {}
+
+ JxlPixelFormat format;
+ const ChunkedPNMDecoder* dec;
+};
+
+StatusOr<ChunkedPNMDecoder> ChunkedPNMDecoder::Init(const char* path) {
+ ChunkedPNMDecoder dec;
+ JXL_ASSIGN_OR_RETURN(dec.pnm_, MemoryMappedFile::Init(path));
+ size_t size = dec.pnm_.size();
+ if (size < 2) return JXL_FAILURE("Invalid ppm");
+ size_t hdr_buf = std::min<size_t>(size, 10 * 1024);
+ Span<const uint8_t> span(dec.pnm_.data(), hdr_buf);
+ Parser parser(span);
+ HeaderPNM& header = dec.header_;
+ const uint8_t* pos = nullptr;
+ if (!parser.ParseHeader(&header, &pos)) {
+ return StatusCode::kGenericError;
+ }
+ dec.data_start_ = pos - span.data();
+
+ if (header.bits_per_sample == 0 || header.bits_per_sample > 16) {
+ return JXL_FAILURE("Invalid bits_per_sample");
+ }
+ if (header.has_alpha || !header.ec_types.empty() || header.floating_point) {
+ return JXL_FAILURE("Only PGM and PPM inputs are supported");
+ }
+
+ const size_t bytes_per_channel =
+ DivCeil(dec.header_.bits_per_sample, jxl::kBitsPerByte);
+ const size_t num_channels = dec.header_.is_gray ? 1 : 3;
+ const size_t bytes_per_pixel = num_channels * bytes_per_channel;
+ size_t row_size = dec.header_.xsize * bytes_per_pixel;
+ if (header.ysize * row_size + dec.data_start_ < size) {
+ return JXL_FAILURE("Invalid ppm");
+ }
+ return dec;
+}
+
+jxl::Status ChunkedPNMDecoder::InitializePPF(const ColorHints& color_hints,
+ PackedPixelFile* ppf) {
+ // PPM specifies 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;
+ ppf->info.bits_per_sample = header_.bits_per_sample;
+ ppf->info.exponent_bits_per_sample = 0;
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+ ppf->info.alpha_bits = 0;
+ ppf->info.alpha_exponent_bits = 0;
+ ppf->info.num_color_channels = (header_.is_gray ? 1 : 3);
+ ppf->info.num_extra_channels = 0;
+
+ const JxlDataType data_type =
+ header_.bits_per_sample > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8;
+ const JxlPixelFormat format{
+ /*num_channels=*/ppf->info.num_color_channels,
+ /*data_type=*/data_type,
+ /*endianness=*/header_.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
+ /*align=*/0,
+ };
+
+ PNMChunkedInputFrame frame;
+ frame.format = format;
+ frame.dec = this;
+ ppf->chunked_frames.emplace_back(header_.xsize, header_.ysize, frame);
+ return true;
+}
+
+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 specifies 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..b740a79af5
--- /dev/null
+++ b/third_party/jpeg-xl/lib/extras/dec/pnm.h
@@ -0,0 +1,68 @@
+// 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 <mutex>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/mmap.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 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();
+
+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 ChunkedPNMDecoder {
+ public:
+ static StatusOr<ChunkedPNMDecoder> Init(const char* file_path);
+ // Initializes `ppf` with a pointer to this `ChunkedPNMDecoder`.
+ jxl::Status InitializePPF(const ColorHints& color_hints,
+ PackedPixelFile* ppf);
+
+ private:
+ HeaderPNM header_ = {};
+ size_t data_start_ = 0;
+ MemoryMappedFile pnm_;
+
+ friend struct PNMChunkedInputFrame;
+};
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_PNM_H_