diff options
Diffstat (limited to 'media/libjxl/src/lib/extras/dec')
20 files changed, 3168 insertions, 0 deletions
diff --git a/media/libjxl/src/lib/extras/dec/apng.cc b/media/libjxl/src/lib/extras/dec/apng.cc new file mode 100644 index 0000000000..566746665d --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/apng.cc @@ -0,0 +1,797 @@ +// 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 <stdio.h> +#include <string.h> + +#include <string> +#include <utility> +#include <vector> + +#include "jxl/codestream_header.h" +#include "jxl/encode.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/scope_guard.h" +#include "lib/jxl/common.h" +#include "lib/jxl/sanitizers.h" +#include "png.h" /* original (unpatched) libpng is ok */ + +namespace jxl { +namespace extras { + +namespace { + +/* 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->rendering_intent = + static_cast<JxlRenderingIntent>(payload[0]); + 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") { + 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; + 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_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}; + + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + info_ptr = png_create_info_struct(png_ptr); + if (!png_ptr || !info_ptr) return 1; + + if (setjmp(png_jmpbuf(png_ptr))) { + return 1; + } + + png_set_keep_unknown_chunks(png_ptr, 1, kIgnoredPngChunks, + (int)sizeof(kIgnoredPngChunks) / 5); + + png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); + png_set_progressive_read_fn(png_ptr, frame_ptr, info_fn, row_fn, NULL); + + png_process_data(png_ptr, info_ptr, header, 8); + png_process_data(png_ptr, info_ptr, chunkIHDR.data(), chunkIHDR.size()); + + if (hasInfo) { + for (unsigned int i = 0; i < chunksInfo.size(); i++) { + png_process_data(png_ptr, info_ptr, chunksInfo[i].data(), + chunksInfo[i].size()); + } + } + return 0; +} + +int processing_data(png_structp png_ptr, png_infop info_ptr, unsigned char* p, + unsigned int size) { + if (!png_ptr || !info_ptr) return 1; + + if (setjmp(png_jmpbuf(png_ptr))) { + return 1; + } + + png_process_data(png_ptr, info_ptr, p, size); + return 0; +} + +int processing_finish(png_structp png_ptr, png_infop info_ptr, + PackedMetadata* metadata) { + unsigned char footer[12] = {0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130}; + + if (!png_ptr || !info_ptr) return 1; + + if (setjmp(png_jmpbuf(png_ptr))) { + return 1; + } + + png_process_data(png_ptr, info_ptr, footer, 12); + // before destroying: check if we encountered any metadata chunks + png_textp text_ptr; + int num_text; + png_get_text(png_ptr, info_ptr, &text_ptr, &num_text); + for (int i = 0; i < num_text; i++) { + (void)BlobsReaderPNG::Decode(text_ptr[i], metadata); + } + + return 0; +} + +} // namespace + +Status DecodeImageAPNG(const Span<const uint8_t> bytes, + const ColorHints& color_hints, + const SizeConstraints& constraints, + PackedPixelFile* ppf) { + Reader r; + unsigned int id, j, w, h, w0, h0, x0, y0; + unsigned int delay_num, delay_den, dop, bop, rowbytes, imagesize; + unsigned char sig[8]; + png_structp png_ptr = nullptr; + png_infop info_ptr = nullptr; + std::vector<uint8_t> chunk; + std::vector<uint8_t> chunkIHDR; + std::vector<std::vector<uint8_t>> chunksInfo; + bool isAnimated = false; + bool hasInfo = false; + APNGFrame frameRaw = {}; + uint32_t num_channels; + JxlPixelFormat format; + unsigned int bytes_per_pixel = 0; + + struct FrameInfo { + PackedImage data; + uint32_t duration; + size_t x0, xsize; + size_t y0, ysize; + uint32_t dispose_op; + uint32_t blend_op; + }; + + std::vector<FrameInfo> frames; + + // Make sure png memory is released in any case. + auto scope_guard = MakeScopeGuard([&]() { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + // Just in case. Not all versions on libpng wipe-out the pointers. + png_ptr = nullptr; + info_ptr = nullptr; + }); + + r = {bytes.data(), bytes.data() + bytes.size()}; + // Not a PNG => not an error + unsigned char png_signature[8] = {137, 80, 78, 71, 13, 10, 26, 10}; + if (!r.Read(sig, 8) || memcmp(sig, png_signature, 8) != 0) { + return false; + } + id = read_chunk(&r, &chunkIHDR); + + ppf->info.exponent_bits_per_sample = 0; + ppf->info.alpha_exponent_bits = 0; + ppf->info.orientation = JXL_ORIENT_IDENTITY; + + ppf->frames.clear(); + + bool have_color = false, 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; + + if (!processing_start(png_ptr, info_ptr, (void*)&frameRaw, hasInfo, + chunkIHDR, chunksInfo)) { + while (!r.Eof()) { + id = read_chunk(&r, &chunk); + if (!id) break; + + if (id == kId_acTL && !hasInfo && !isAnimated) { + isAnimated = true; + ppf->info.have_animation = true; + ppf->info.animation.tps_numerator = 1000; + ppf->info.animation.tps_denominator = 1; + } else if (id == kId_IEND || + (id == kId_fcTL && (!hasInfo || isAnimated))) { + if (hasInfo) { + if (!processing_finish(png_ptr, info_ptr, &ppf->metadata)) { + // Allocates the frame buffer. + uint32_t duration = delay_num * 1000 / delay_den; + frames.push_back(FrameInfo{PackedImage(w0, h0, format), duration, + x0, w0, y0, h0, dop, bop}); + auto& frame = frames.back().data; + for (size_t y = 0; y < h0; ++y) { + memcpy(static_cast<uint8_t*>(frame.pixels()) + frame.stride * y, + frameRaw.rows[y], bytes_per_pixel * w0); + } + } else { + break; + } + } + + if (id == kId_IEND) { + errorstate = false; + break; + } + if (chunk.size() < 34) { + return JXL_FAILURE("Received a chunk that is too small (%" PRIuS + "B)", + chunk.size()); + } + // At this point the old frame is done. Let's start a new one. + w0 = png_get_uint_32(chunk.data() + 12); + h0 = png_get_uint_32(chunk.data() + 16); + x0 = png_get_uint_32(chunk.data() + 20); + y0 = png_get_uint_32(chunk.data() + 24); + delay_num = png_get_uint_16(chunk.data() + 28); + delay_den = png_get_uint_16(chunk.data() + 30); + dop = chunk[32]; + bop = chunk[33]; + + if (!delay_den) delay_den = 100; + + if (w0 > cMaxPNGSize || h0 > cMaxPNGSize || x0 > cMaxPNGSize || + y0 > cMaxPNGSize || x0 + w0 > w || y0 + h0 > h || dop > 2 || + bop > 1) { + break; + } + + if (hasInfo) { + memcpy(chunkIHDR.data() + 8, chunk.data() + 12, 8); + if (processing_start(png_ptr, info_ptr, (void*)&frameRaw, hasInfo, + chunkIHDR, chunksInfo)) { + break; + } + } + } else if (id == kId_IDAT) { + // First IDAT chunk means we now have all header info + hasInfo = true; + JXL_CHECK(w == png_get_image_width(png_ptr, info_ptr)); + JXL_CHECK(h == png_get_image_height(png_ptr, info_ptr)); + int colortype = png_get_color_type(png_ptr, info_ptr); + ppf->info.bits_per_sample = png_get_bit_depth(png_ptr, info_ptr); + png_color_8p sigbits = NULL; + png_get_sBIT(png_ptr, info_ptr, &sigbits); + if (colortype & 1) { + // palette will actually be 8-bit regardless of the index bitdepth + ppf->info.bits_per_sample = 8; + } + if (colortype & 2) { + ppf->info.num_color_channels = 3; + ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB; + if (sigbits && sigbits->red == sigbits->green && + sigbits->green == sigbits->blue) + ppf->info.bits_per_sample = sigbits->red; + } else { + ppf->info.num_color_channels = 1; + ppf->color_encoding.color_space = JXL_COLOR_SPACE_GRAY; + if (sigbits) ppf->info.bits_per_sample = sigbits->gray; + } + if (colortype & 4 || + png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + ppf->info.alpha_bits = ppf->info.bits_per_sample; + if (sigbits) { + if (sigbits->alpha && + sigbits->alpha != ppf->info.bits_per_sample) { + return JXL_FAILURE("Unsupported alpha bit-depth"); + } + ppf->info.alpha_bits = sigbits->alpha; + } + } else { + ppf->info.alpha_bits = 0; + } + ppf->color_encoding.color_space = + (ppf->info.num_color_channels == 1 ? JXL_COLOR_SPACE_GRAY + : JXL_COLOR_SPACE_RGB); + ppf->info.xsize = w; + ppf->info.ysize = h; + JXL_RETURN_IF_ERROR(VerifyDimensions(&constraints, w, h)); + num_channels = + ppf->info.num_color_channels + (ppf->info.alpha_bits ? 1 : 0); + format = { + /*num_channels=*/num_channels, + /*data_type=*/ppf->info.bits_per_sample > 8 ? JXL_TYPE_UINT16 + : JXL_TYPE_UINT8, + /*endianness=*/JXL_BIG_ENDIAN, + /*align=*/0, + }; + bytes_per_pixel = + num_channels * (format.data_type == JXL_TYPE_UINT16 ? 2 : 1); + rowbytes = w * bytes_per_pixel; + imagesize = h * rowbytes; + frameRaw.pixels.resize(imagesize); + frameRaw.rows.resize(h); + for (j = 0; j < h; j++) + frameRaw.rows[j] = frameRaw.pixels.data() + j * rowbytes; + + if (processing_data(png_ptr, info_ptr, chunk.data(), chunk.size())) { + break; + } + } else if (id == kId_fdAT && isAnimated) { + png_save_uint_32(chunk.data() + 4, chunk.size() - 16); + memcpy(chunk.data() + 8, "IDAT", 4); + if (processing_data(png_ptr, info_ptr, chunk.data() + 4, + chunk.size() - 4)) { + break; + } + } else if (id == kId_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; + } else { + // TODO(eustas): JXL_WARNING? + } + } else if (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 (id == kId_gAMA) { + JXL_RETURN_IF_ERROR(DecodeGAMA(chunk.data() + 8, chunk.size() - 12, + &ppf->color_encoding)); + have_color = true; + } else if (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; + } + } + } + } + + if (have_srgb) { + 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; + } + 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 xs = frame.data.xsize; + size_t ys = frame.data.ysize; + 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 + xs && + py0 + pys <= y0 + ys && 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 + xs && + py0 + pys == y0 + ys && 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 + xs && + py0 + pys >= y0 + ys && 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 < ys; 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, + xs * bytes_per_pixel); + } + + x0 = px0; + y0 = py0; + xsize = pxs; + ysize = pys; + should_blend = false; + ppf->frames.emplace_back(std::move(new_data)); + } else { + // If all else fails, insert a dummy blank frame with kReplace. + PackedImage blank(pxs, pys, frame.data.format); + memset(blank.pixels(), 0, blank.pixels_size); + ppf->frames.emplace_back(std::move(blank)); + auto& pframe = ppf->frames.back(); + pframe.frame_info.layer_info.crop_x0 = px0; + pframe.frame_info.layer_info.crop_y0 = py0; + pframe.frame_info.layer_info.xsize = frame.xsize; + pframe.frame_info.layer_info.ysize = frame.ysize; + pframe.frame_info.duration = 0; + pframe.frame_info.layer_info.have_crop = 0; + pframe.frame_info.layer_info.blend_info.blendmode = JXL_BLEND_REPLACE; + pframe.frame_info.layer_info.blend_info.source = 0; + pframe.frame_info.layer_info.save_as_reference = 1; + ppf->frames.emplace_back(std::move(frame.data)); + } + } else { + ppf->frames.emplace_back(std::move(frame.data)); + } + + auto& pframe = ppf->frames.back(); + pframe.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 = should_blend ? 1 : 0; + pframe.frame_info.layer_info.blend_info.alpha = 0; + pframe.frame_info.layer_info.save_as_reference = use_for_next_frame ? 1 : 0; + + previous_frame_should_be_cleared = + has_nontrivial_background && frame.dispose_op == DISPOSE_OP_BACKGROUND; + } + if (ppf->frames.empty()) return JXL_FAILURE("No frames decoded"); + ppf->frames.back().frame_info.is_last = true; + + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/apng.h b/media/libjxl/src/lib/extras/dec/apng.h new file mode 100644 index 0000000000..a68f6f8ec7 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/apng.h @@ -0,0 +1,32 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_DEC_APNG_H_ +#define LIB_EXTRAS_DEC_APNG_H_ + +// Decodes APNG images in memory. + +#include <stdint.h> + +#include "lib/extras/dec/color_hints.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { +namespace extras { + +// Decodes `bytes` into `ppf`. +Status DecodeImageAPNG(Span<const uint8_t> bytes, const ColorHints& color_hints, + const SizeConstraints& constraints, + PackedPixelFile* ppf); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_APNG_H_ diff --git a/media/libjxl/src/lib/extras/dec/color_description.cc b/media/libjxl/src/lib/extras/dec/color_description.cc new file mode 100644 index 0000000000..2325b50f3b --- /dev/null +++ b/media/libjxl/src/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 ARRAYSIZE(X) (sizeof(X) / sizeof((X)[0])) +#define PARSE_ENUM(type, token, value) \ + ParseEnum<type>(token, k##type##Names, ARRAYSIZE(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/media/libjxl/src/lib/extras/dec/color_description.h b/media/libjxl/src/lib/extras/dec/color_description.h new file mode 100644 index 0000000000..989d5910c3 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/color_description.h @@ -0,0 +1,22 @@ +// 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 <string> + +#include "jxl/color_encoding.h" +#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/media/libjxl/src/lib/extras/dec/color_description_test.cc b/media/libjxl/src/lib/extras/dec/color_description_test.cc new file mode 100644 index 0000000000..8ae9e5dce3 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/color_description_test.cc @@ -0,0 +1,38 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/color_description.h" + +#include "gtest/gtest.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/test_utils.h" + +namespace jxl { + +// Verify ParseDescription(Description) yields the same ColorEncoding +TEST(ColorDescriptionTest, RoundTripAll) { + for (const auto& cdesc : test::AllEncodings()) { + const ColorEncoding c_original = test::ColorEncodingFromDescriptor(cdesc); + const std::string description = Description(c_original); + printf("%s\n", description.c_str()); + + JxlColorEncoding c_external = {}; + EXPECT_TRUE(ParseDescription(description, &c_external)); + ColorEncoding c_internal; + EXPECT_TRUE( + ConvertExternalToInternalColorEncoding(c_external, &c_internal)); + EXPECT_TRUE(c_original.SameColorEncoding(c_internal)) + << "Where c_original=" << c_original + << " and c_internal=" << c_internal; + } +} + +TEST(ColorDescriptionTest, NanGamma) { + const std::string description = "Gra_2_Per_gnan"; + JxlColorEncoding c; + EXPECT_FALSE(ParseDescription(description, &c)); +} + +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/color_hints.cc b/media/libjxl/src/lib/extras/dec/color_hints.cc new file mode 100644 index 0000000000..cf7d3e31f0 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/color_hints.cc @@ -0,0 +1,66 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/color_hints.h" + +#include "jxl/encode.h" +#include "lib/extras/dec/color_description.h" +#include "lib/jxl/base/file_io.h" + +namespace jxl { +namespace extras { + +Status ApplyColorHints(const ColorHints& color_hints, + const bool color_already_set, const bool is_gray, + PackedPixelFile* ppf) { + if (color_already_set) { + return color_hints.Foreach( + [](const std::string& key, const std::string& /*value*/) { + JXL_WARNING("Decoder ignoring %s hint", key.c_str()); + return true; + }); + } + + bool got_color_space = false; + + JXL_RETURN_IF_ERROR(color_hints.Foreach( + [is_gray, ppf, &got_color_space](const std::string& key, + const std::string& value) -> Status { + if (key == "color_space") { + JxlColorEncoding c_original_external; + if (!ParseDescription(value, &c_original_external)) { + return JXL_FAILURE("Failed to apply color_space"); + } + ppf->color_encoding = c_original_external; + + if (is_gray != + (ppf->color_encoding.color_space == JXL_COLOR_SPACE_GRAY)) { + return JXL_FAILURE("mismatch between file and color_space hint"); + } + + got_color_space = true; + } else if (key == "icc_pathname") { + JXL_RETURN_IF_ERROR(ReadFile(value, &ppf->icc)); + got_color_space = true; + } else { + JXL_WARNING("Ignoring %s hint", key.c_str()); + } + return true; + })); + + if (!got_color_space) { + JXL_WARNING("No color_space/icc_pathname given, assuming sRGB"); + 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/media/libjxl/src/lib/extras/dec/color_hints.h b/media/libjxl/src/lib/extras/dec/color_hints.h new file mode 100644 index 0000000000..9c7de884f9 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/color_hints.h @@ -0,0 +1,72 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_COLOR_HINTS_H_ +#define LIB_EXTRAS_COLOR_HINTS_H_ + +// Not all the formats implemented in the extras lib support bundling color +// information into the file, and those that support it may not have it. +// To allow attaching color information to those file formats the caller can +// define these color hints. + +#include <stddef.h> +#include <stdint.h> + +#include <string> +#include <vector> + +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace extras { + +class ColorHints { + public: + // key=color_space, value=Description(c/pp): specify the ColorEncoding of + // the pixels for decoding. Otherwise, if the codec did not obtain an ICC + // profile from the image, assume sRGB. + // + // Strings are taken from the command line, so avoid spaces for convenience. + void Add(const std::string& key, const std::string& value) { + kv_.emplace_back(key, value); + } + + // Calls `func(key, value)` for each key/value in the order they were added, + // returning false immediately if `func` returns false. + template <class Func> + Status Foreach(const Func& func) const { + for (const KeyValue& kv : kv_) { + Status ok = func(kv.key, kv.value); + if (!ok) { + return JXL_FAILURE("ColorHints::Foreach returned false"); + } + } + return true; + } + + private: + // Splitting into key/value avoids parsing in each codec. + struct KeyValue { + KeyValue(std::string key, std::string value) + : key(std::move(key)), value(std::move(value)) {} + + std::string key; + std::string value; + }; + + std::vector<KeyValue> kv_; +}; + +// Apply the color hints to the decoded image in PackedPixelFile if any. +// color_already_set tells whether the color encoding was already set, in which +// case the hints are ignored if any hint is passed. +Status ApplyColorHints(const ColorHints& color_hints, bool color_already_set, + bool is_gray, PackedPixelFile* ppf); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_COLOR_HINTS_H_ diff --git a/media/libjxl/src/lib/extras/dec/decode.cc b/media/libjxl/src/lib/extras/dec/decode.cc new file mode 100644 index 0000000000..55ce0ba3f2 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/decode.cc @@ -0,0 +1,109 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/decode.h" + +#include <locale> + +#if JPEGXL_ENABLE_APNG +#include "lib/extras/dec/apng.h" +#endif +#if JPEGXL_ENABLE_EXR +#include "lib/extras/dec/exr.h" +#endif +#if JPEGXL_ENABLE_GIF +#include "lib/extras/dec/gif.h" +#endif +#if JPEGXL_ENABLE_JPEG +#include "lib/extras/dec/jpg.h" +#endif +#include "lib/extras/dec/pgx.h" +#include "lib/extras/dec/pnm.h" + +namespace jxl { +namespace extras { +namespace { + +// Any valid encoding is larger (ensures codecs can read the first few bytes) +constexpr size_t kMinBytes = 9; + +} // namespace + +Codec CodecFromExtension(std::string extension, + size_t* JXL_RESTRICT bits_per_sample) { + std::transform( + extension.begin(), extension.end(), extension.begin(), + [](char c) { return std::tolower(c, std::locale::classic()); }); + if (extension == ".png") return Codec::kPNG; + + if (extension == ".jpg") return Codec::kJPG; + if (extension == ".jpeg") return Codec::kJPG; + + if (extension == ".pgx") return Codec::kPGX; + + if (extension == ".pam") return Codec::kPNM; + if (extension == ".pnm") return Codec::kPNM; + if (extension == ".pgm") return Codec::kPNM; + if (extension == ".ppm") return Codec::kPNM; + if (extension == ".pfm") { + if (bits_per_sample != nullptr) *bits_per_sample = 32; + return Codec::kPNM; + } + + if (extension == ".gif") return Codec::kGIF; + + if (extension == ".exr") return Codec::kEXR; + + return Codec::kUnknown; +} + +Status DecodeBytes(const Span<const uint8_t> bytes, + const ColorHints& color_hints, + const SizeConstraints& constraints, + extras::PackedPixelFile* ppf, 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; + + Codec codec; +#if JPEGXL_ENABLE_APNG + if (DecodeImageAPNG(bytes, color_hints, constraints, ppf)) { + codec = Codec::kPNG; + } else +#endif + if (DecodeImagePGX(bytes, color_hints, constraints, ppf)) { + codec = Codec::kPGX; + } else if (DecodeImagePNM(bytes, color_hints, constraints, ppf)) { + codec = Codec::kPNM; + } +#if JPEGXL_ENABLE_GIF + else if (DecodeImageGIF(bytes, color_hints, constraints, ppf)) { + codec = Codec::kGIF; + } +#endif +#if JPEGXL_ENABLE_JPEG + else if (DecodeImageJPG(bytes, color_hints, constraints, ppf)) { + codec = Codec::kJPG; + } +#endif +#if JPEGXL_ENABLE_EXR + else if (DecodeImageEXR(bytes, color_hints, constraints, ppf)) { + codec = Codec::kEXR; + } +#endif + else { + return JXL_FAILURE("Codecs failed to decode"); + } + if (orig_codec) *orig_codec = codec; + + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/decode.h b/media/libjxl/src/lib/extras/dec/decode.h new file mode 100644 index 0000000000..32ec4c6368 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/decode.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_DECODE_H_ +#define LIB_EXTRAS_DEC_DECODE_H_ + +// Facade for image decoders (PNG, PNM, ...). + +#include <stddef.h> +#include <stdint.h> + +#include <string> + +#include "lib/extras/dec/color_hints.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/field_encodings.h" // MakeBit + +namespace jxl { +namespace extras { + +// Codecs supported by CodecInOut::Encode. +enum class Codec : uint32_t { + kUnknown, // for CodecFromExtension + kPNG, + kPNM, + kPGX, + kJPG, + kGIF, + kEXR +}; + +static inline constexpr uint64_t EnumBits(Codec /*unused*/) { + // Return only fully-supported codecs (kGIF is decode-only). + return MakeBit(Codec::kPNM) +#if JPEGXL_ENABLE_APNG + | MakeBit(Codec::kPNG) +#endif +#if JPEGXL_ENABLE_JPEG + | MakeBit(Codec::kJPG) +#endif +#if JPEGXL_ENABLE_EXR + | MakeBit(Codec::kEXR) +#endif + ; +} + +// If and only if extension is ".pfm", *bits_per_sample is updated to 32 so +// that Encode() would encode to PFM instead of PPM. +Codec CodecFromExtension(std::string extension, + size_t* JXL_RESTRICT bits_per_sample = nullptr); + +// Decodes "bytes" and sets io->metadata.m. +// color_space_hint may specify the color space, otherwise, defaults to sRGB. +Status DecodeBytes(Span<const uint8_t> bytes, const ColorHints& color_hints, + const SizeConstraints& constraints, + extras::PackedPixelFile* ppf, Codec* orig_codec = nullptr); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_DECODE_H_ diff --git a/media/libjxl/src/lib/extras/dec/exr.cc b/media/libjxl/src/lib/extras/dec/exr.cc new file mode 100644 index 0000000000..ddb6d534e5 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/exr.cc @@ -0,0 +1,184 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/exr.h" + +#include <ImfChromaticitiesAttribute.h> +#include <ImfIO.h> +#include <ImfRgbaFile.h> +#include <ImfStandardAttributes.h> + +#include <vector> + +namespace jxl { +namespace extras { + +namespace { + +namespace OpenEXR = OPENEXR_IMF_NAMESPACE; +namespace Imath = IMATH_NAMESPACE; + +// OpenEXR::Int64 is deprecated in favor of using uint64_t directly, but using +// uint64_t as recommended causes build failures with previous OpenEXR versions +// on macOS, where the definition for OpenEXR::Int64 was actually not equivalent +// to uint64_t. This alternative should work in all cases. +using ExrInt64 = decltype(std::declval<OpenEXR::IStream>().tellg()); + +constexpr int kExrBitsPerSample = 16; +constexpr int kExrAlphaBits = 16; + +class InMemoryIStream : public OpenEXR::IStream { + public: + // The data pointed to by `bytes` must outlive the InMemoryIStream. + explicit InMemoryIStream(const Span<const uint8_t> bytes) + : IStream(/*fileName=*/""), bytes_(bytes) {} + + bool isMemoryMapped() const override { return true; } + char* readMemoryMapped(const int n) override { + JXL_ASSERT(pos_ + n <= bytes_.size()); + char* const result = + const_cast<char*>(reinterpret_cast<const char*>(bytes_.data() + pos_)); + pos_ += n; + return result; + } + bool read(char c[], const int n) override { + std::copy_n(readMemoryMapped(n), n, c); + return pos_ < bytes_.size(); + } + + ExrInt64 tellg() override { return pos_; } + void seekg(const ExrInt64 pos) override { + JXL_ASSERT(pos + 1 <= bytes_.size()); + pos_ = pos; + } + + private: + const Span<const uint8_t> bytes_; + size_t pos_ = 0; +}; + +} // namespace + +Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints, + const SizeConstraints& constraints, + PackedPixelFile* ppf) { + InMemoryIStream is(bytes); + +#ifdef __EXCEPTIONS + std::unique_ptr<OpenEXR::RgbaInputFile> input_ptr; + try { + input_ptr.reset(new OpenEXR::RgbaInputFile(is)); + } catch (...) { + return JXL_FAILURE("OpenEXR failed to parse input"); + } + OpenEXR::RgbaInputFile& input = *input_ptr; +#else + OpenEXR::RgbaInputFile input(is); +#endif + + if ((input.channels() & OpenEXR::RgbaChannels::WRITE_RGB) != + OpenEXR::RgbaChannels::WRITE_RGB) { + return JXL_FAILURE("only RGB OpenEXR files are supported"); + } + const bool has_alpha = (input.channels() & OpenEXR::RgbaChannels::WRITE_A) == + OpenEXR::RgbaChannels::WRITE_A; + + const float intensity_target = OpenEXR::hasWhiteLuminance(input.header()) + ? OpenEXR::whiteLuminance(input.header()) + : kDefaultIntensityTarget; + + auto image_size = input.displayWindow().size(); + // Size is computed as max - min, but both bounds are inclusive. + ++image_size.x; + ++image_size.y; + + ppf->info.xsize = image_size.x; + ppf->info.ysize = image_size.y; + ppf->info.num_color_channels = 3; + + const JxlDataType data_type = + kExrBitsPerSample == 16 ? JXL_TYPE_FLOAT16 : JXL_TYPE_FLOAT; + const JxlPixelFormat format{ + /*num_channels=*/3u + (has_alpha ? 1u : 0u), + /*data_type=*/data_type, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0, + }; + ppf->frames.clear(); + // Allocates the frame buffer. + ppf->frames.emplace_back(image_size.x, image_size.y, format); + const auto& frame = ppf->frames.back(); + + const int row_size = input.dataWindow().size().x + 1; + // Number of rows to read at a time. + // https://www.openexr.com/documentation/ReadingAndWritingImageFiles.pdf + // recommends reading the whole file at once. + const int y_chunk_size = input.displayWindow().size().y + 1; + std::vector<OpenEXR::Rgba> input_rows(row_size * y_chunk_size); + for (int start_y = + std::max(input.dataWindow().min.y, input.displayWindow().min.y); + start_y <= + std::min(input.dataWindow().max.y, input.displayWindow().max.y); + start_y += y_chunk_size) { + // Inclusive. + const int end_y = std::min( + start_y + y_chunk_size - 1, + std::min(input.dataWindow().max.y, input.displayWindow().max.y)); + input.setFrameBuffer( + input_rows.data() - input.dataWindow().min.x - start_y * row_size, + /*xStride=*/1, /*yStride=*/row_size); + input.readPixels(start_y, end_y); + for (int exr_y = start_y; exr_y <= end_y; ++exr_y) { + const int image_y = exr_y - input.displayWindow().min.y; + const OpenEXR::Rgba* const JXL_RESTRICT input_row = + &input_rows[(exr_y - start_y) * row_size]; + uint8_t* row = static_cast<uint8_t*>(frame.color.pixels()) + + frame.color.stride * image_y; + const uint32_t pixel_size = + (3 + (has_alpha ? 1 : 0)) * kExrBitsPerSample / 8; + for (int exr_x = + std::max(input.dataWindow().min.x, input.displayWindow().min.x); + exr_x <= + std::min(input.dataWindow().max.x, input.displayWindow().max.x); + ++exr_x) { + const int image_x = exr_x - input.displayWindow().min.x; + memcpy(row + image_x * pixel_size, + input_row + (exr_x - input.dataWindow().min.x), pixel_size); + } + } + } + + ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR; + ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB; + ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB; + ppf->color_encoding.white_point = JXL_WHITE_POINT_D65; + if (OpenEXR::hasChromaticities(input.header())) { + ppf->color_encoding.primaries = JXL_PRIMARIES_CUSTOM; + ppf->color_encoding.white_point = JXL_WHITE_POINT_CUSTOM; + const auto& chromaticities = OpenEXR::chromaticities(input.header()); + ppf->color_encoding.primaries_red_xy[0] = chromaticities.red.x; + ppf->color_encoding.primaries_red_xy[1] = chromaticities.red.y; + ppf->color_encoding.primaries_green_xy[0] = chromaticities.green.x; + ppf->color_encoding.primaries_green_xy[1] = chromaticities.green.y; + ppf->color_encoding.primaries_blue_xy[0] = chromaticities.blue.x; + ppf->color_encoding.primaries_blue_xy[1] = chromaticities.blue.y; + ppf->color_encoding.white_point_xy[0] = chromaticities.white.x; + ppf->color_encoding.white_point_xy[1] = chromaticities.white.y; + } + + // EXR uses binary16 or binary32 floating point format. + ppf->info.bits_per_sample = kExrBitsPerSample; + ppf->info.exponent_bits_per_sample = kExrBitsPerSample == 16 ? 5 : 8; + if (has_alpha) { + ppf->info.alpha_bits = kExrAlphaBits; + ppf->info.alpha_exponent_bits = ppf->info.exponent_bits_per_sample; + ppf->info.alpha_premultiplied = true; + } + ppf->info.intensity_target = intensity_target; + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/exr.h b/media/libjxl/src/lib/extras/dec/exr.h new file mode 100644 index 0000000000..6af4e6becf --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/exr.h @@ -0,0 +1,29 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_DEC_EXR_H_ +#define LIB_EXTRAS_DEC_EXR_H_ + +// Decodes OpenEXR images in memory. + +#include "lib/extras/dec/color_hints.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { +namespace extras { + +// Decodes `bytes` into `ppf`. color_hints are ignored. +Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints, + const SizeConstraints& constraints, PackedPixelFile* ppf); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_EXR_H_ diff --git a/media/libjxl/src/lib/extras/dec/gif.cc b/media/libjxl/src/lib/extras/dec/gif.cc new file mode 100644 index 0000000000..5167bf5fa0 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/gif.cc @@ -0,0 +1,414 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/gif.h" + +#include <gif_lib.h> +#include <string.h> + +#include <memory> +#include <utility> +#include <vector> + +#include "jxl/codestream_header.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/sanitizers.h" + +namespace jxl { +namespace extras { + +namespace { + +struct ReadState { + Span<const uint8_t> bytes; +}; + +struct DGifCloser { + void operator()(GifFileType* const ptr) const { DGifCloseFile(ptr, nullptr); } +}; +using GifUniquePtr = std::unique_ptr<GifFileType, DGifCloser>; + +struct PackedRgba { + uint8_t r, g, b, a; +}; + +struct PackedRgb { + uint8_t r, g, b; +}; + +// Gif does not support partial transparency, so this considers any nonzero +// alpha channel value as opaque. +bool AllOpaque(const PackedImage& color) { + for (size_t y = 0; y < color.ysize; ++y) { + const PackedRgba* const JXL_RESTRICT row = + static_cast<const PackedRgba*>(color.pixels()) + y * color.xsize; + for (size_t x = 0; x < color.xsize; ++x) { + if (row[x].a == 0) { + return false; + } + } + } + return true; +} + +void ensure_have_alpha(PackedFrame* frame) { + if (!frame->extra_channels.empty()) return; + const JxlPixelFormat alpha_format{ + /*num_channels=*/1u, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0, + }; + frame->extra_channels.emplace_back(frame->color.xsize, frame->color.ysize, + alpha_format); + // We need to set opaque-by-default. + std::fill_n(static_cast<uint8_t*>(frame->extra_channels[0].pixels()), + frame->color.xsize * frame->color.ysize, 255u); +} + +} // namespace + +Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints, + const SizeConstraints& constraints, + PackedPixelFile* ppf) { + 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 (total_pixel_count > constraints.dec_max_pixels) { + return JXL_FAILURE("Image too big"); + } + } + + if (!gif->SColorMap) { + for (int i = 0; i < gif->ImageCount; ++i) { + if (!gif->SavedImages[i].ImageDesc.ColorMap) { + return JXL_FAILURE("Missing GIF color map"); + } + } + } + + if (gif->ImageCount > 1) { + ppf->info.have_animation = true; + // Delays in GIF are specified in 100ths of a second. + ppf->info.animation.tps_numerator = 100; + ppf->info.animation.tps_denominator = 1; + } + + ppf->frames.clear(); + ppf->frames.reserve(gif->ImageCount); + + ppf->info.xsize = gif->SWidth; + ppf->info.ysize = gif->SHeight; + ppf->info.bits_per_sample = 8; + ppf->info.exponent_bits_per_sample = 0; + // alpha_bits is later set to 8 if we find a frame with transparent pixels. + ppf->info.alpha_bits = 0; + ppf->info.alpha_exponent_bits = 0; + JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, + /*is_gray=*/false, ppf)); + + ppf->info.num_color_channels = 3; + + // Pixel format for the 'canvas' onto which we paint + // the (potentially individually cropped) GIF frames + // of an animation. + const JxlPixelFormat canvas_format{ + /*num_channels=*/4u, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0, + }; + + // Pixel format for the JXL PackedFrame that goes into the + // PackedPixelFile. Here, we use 3 color channels, and provide + // the alpha channel as an extra_channel wherever it is used. + const JxlPixelFormat packed_frame_format{ + /*num_channels=*/3u, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0, + }; + + GifColorType background_color; + if (gif->SColorMap == nullptr || + gif->SBackGroundColor >= gif->SColorMap->ColorCount) { + background_color = {0, 0, 0}; + } else { + background_color = gif->SColorMap->Colors[gif->SBackGroundColor]; + } + const PackedRgba background_rgba{background_color.Red, background_color.Green, + background_color.Blue, 0}; + PackedFrame canvas(gif->SWidth, gif->SHeight, canvas_format); + std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()), + canvas.color.xsize * canvas.color.ysize, background_rgba); + Rect canvas_rect{0, 0, canvas.color.xsize, canvas.color.ysize}; + + Rect previous_rect_if_restore_to_background; + + bool replace = true; + bool last_base_was_none = true; + for (int i = 0; i < gif->ImageCount; ++i) { + const SavedImage& image = gif->SavedImages[i]; + msan::UnpoisonMemory(image.RasterBits, sizeof(*image.RasterBits) * + image.ImageDesc.Width * + image.ImageDesc.Height); + const Rect image_rect(image.ImageDesc.Left, image.ImageDesc.Top, + image.ImageDesc.Width, image.ImageDesc.Height); + + Rect total_rect; + if (previous_rect_if_restore_to_background.xsize() != 0 || + previous_rect_if_restore_to_background.ysize() != 0) { + const size_t xbegin = std::min( + image_rect.x0(), previous_rect_if_restore_to_background.x0()); + const size_t ybegin = std::min( + image_rect.y0(), previous_rect_if_restore_to_background.y0()); + const size_t xend = + std::max(image_rect.x0() + image_rect.xsize(), + previous_rect_if_restore_to_background.x0() + + previous_rect_if_restore_to_background.xsize()); + const size_t yend = + std::max(image_rect.y0() + image_rect.ysize(), + previous_rect_if_restore_to_background.y0() + + previous_rect_if_restore_to_background.ysize()); + total_rect = Rect(xbegin, ybegin, xend - xbegin, yend - ybegin); + previous_rect_if_restore_to_background = Rect(); + replace = true; + } else { + total_rect = image_rect; + replace = false; + } + if (!image_rect.IsInside(canvas_rect)) { + return JXL_FAILURE("GIF frame extends outside of the canvas"); + } + + // Allocates the frame buffer. + ppf->frames.emplace_back(total_rect.xsize(), total_rect.ysize(), + packed_frame_format); + PackedFrame* frame = &ppf->frames.back(); + + // We cannot tell right from the start whether there will be a + // need for an alpha channel. This is discovered only as soon as + // we see a transparent pixel. We hence initialize alpha lazily. + auto set_pixel_alpha = [&frame](size_t x, size_t y, uint8_t a) { + // If we do not have an alpha-channel and a==255 (fully opaque), + // we can skip setting this pixel-value and rely on + // "no alpha channel = no transparency". + if (a == 255 && !frame->extra_channels.empty()) return; + ensure_have_alpha(frame); + static_cast<uint8_t*>( + frame->extra_channels[0].pixels())[y * frame->color.xsize + x] = a; + }; + + const ColorMapObject* const color_map = + image.ImageDesc.ColorMap ? image.ImageDesc.ColorMap : gif->SColorMap; + JXL_CHECK(color_map); + msan::UnpoisonMemory(color_map, sizeof(*color_map)); + msan::UnpoisonMemory(color_map->Colors, + sizeof(*color_map->Colors) * color_map->ColorCount); + GraphicsControlBlock gcb; + DGifSavedExtensionToGCB(gif.get(), i, &gcb); + msan::UnpoisonMemory(&gcb, sizeof(gcb)); + bool is_full_size = total_rect.x0() == 0 && total_rect.y0() == 0 && + total_rect.xsize() == canvas.color.xsize && + total_rect.ysize() == canvas.color.ysize; + if (ppf->info.have_animation) { + frame->frame_info.duration = gcb.DelayTime; + frame->frame_info.layer_info.have_crop = static_cast<int>(!is_full_size); + frame->frame_info.layer_info.crop_x0 = total_rect.x0(); + frame->frame_info.layer_info.crop_y0 = total_rect.y0(); + frame->frame_info.layer_info.xsize = frame->color.xsize; + frame->frame_info.layer_info.ysize = frame->color.ysize; + if (last_base_was_none) { + replace = true; + } + frame->frame_info.layer_info.blend_info.blendmode = + replace ? JXL_BLEND_REPLACE : JXL_BLEND_BLEND; + // We always only reference at most the last frame + frame->frame_info.layer_info.blend_info.source = + last_base_was_none ? 0u : 1u; + frame->frame_info.layer_info.blend_info.clamp = 1; + frame->frame_info.layer_info.blend_info.alpha = 0; + // TODO(veluca): this could in principle be implemented. + if (last_base_was_none && + (total_rect.x0() != 0 || total_rect.y0() != 0 || + total_rect.xsize() != canvas.color.xsize || + total_rect.ysize() != canvas.color.ysize || !replace)) { + return JXL_FAILURE( + "GIF with dispose-to-0 is not supported for non-full or " + "blended frames"); + } + switch (gcb.DisposalMode) { + case DISPOSE_DO_NOT: + case DISPOSE_BACKGROUND: + frame->frame_info.layer_info.save_as_reference = 1u; + last_base_was_none = false; + break; + case DISPOSE_PREVIOUS: + frame->frame_info.layer_info.save_as_reference = 0u; + break; + default: + frame->frame_info.layer_info.save_as_reference = 0u; + last_base_was_none = true; + } + } + + // Update the canvas by creating a copy first. + PackedImage new_canvas_image(canvas.color.xsize, canvas.color.ysize, + canvas.color.format); + memcpy(new_canvas_image.pixels(), canvas.color.pixels(), + new_canvas_image.pixels_size); + for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) { + // Assumes format.align == 0. row points to the beginning of the y row in + // the image_rect. + PackedRgba* row = static_cast<PackedRgba*>(new_canvas_image.pixels()) + + (y + image_rect.y0()) * new_canvas_image.xsize + + image_rect.x0(); + for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) { + const GifByteType byte = image.RasterBits[byte_index]; + if (byte >= color_map->ColorCount) { + return JXL_FAILURE("GIF color is out of bounds"); + } + + if (byte == gcb.TransparentColor) continue; + GifColorType color = color_map->Colors[byte]; + row[x].r = color.Red; + row[x].g = color.Green; + row[x].b = color.Blue; + row[x].a = 255; + } + } + const PackedImage& sub_frame_image = frame->color; + if (replace) { + // Copy from the new canvas image to the subframe + for (size_t y = 0; y < total_rect.ysize(); ++y) { + const PackedRgba* row_in = + static_cast<const PackedRgba*>(new_canvas_image.pixels()) + + (y + total_rect.y0()) * new_canvas_image.xsize + total_rect.x0(); + PackedRgb* row_out = static_cast<PackedRgb*>(sub_frame_image.pixels()) + + y * sub_frame_image.xsize; + for (size_t x = 0; x < sub_frame_image.xsize; ++x) { + row_out[x].r = row_in[x].r; + row_out[x].g = row_in[x].g; + row_out[x].b = row_in[x].b; + set_pixel_alpha(x, y, row_in[x].a); + } + } + } else { + for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) { + // Assumes format.align == 0 + PackedRgb* row = static_cast<PackedRgb*>(sub_frame_image.pixels()) + + y * sub_frame_image.xsize; + for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) { + const GifByteType byte = image.RasterBits[byte_index]; + if (byte > color_map->ColorCount) { + return JXL_FAILURE("GIF color is out of bounds"); + } + if (byte == gcb.TransparentColor) { + row[x].r = 0; + row[x].g = 0; + row[x].b = 0; + set_pixel_alpha(x, y, 0); + continue; + } + GifColorType color = color_map->Colors[byte]; + row[x].r = color.Red; + row[x].g = color.Green; + row[x].b = color.Blue; + set_pixel_alpha(x, y, 255); + } + } + } + + if (!frame->extra_channels.empty()) { + ppf->info.alpha_bits = 8; + } + + switch (gcb.DisposalMode) { + case DISPOSE_DO_NOT: + canvas.color = std::move(new_canvas_image); + break; + + case DISPOSE_BACKGROUND: + std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()), + canvas.color.xsize * canvas.color.ysize, background_rgba); + previous_rect_if_restore_to_background = image_rect; + break; + + case DISPOSE_PREVIOUS: + break; + + case DISPOSAL_UNSPECIFIED: + default: + std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()), + canvas.color.xsize * canvas.color.ysize, background_rgba); + } + } + // Finally, if any frame has an alpha-channel, every frame will need + // to have an alpha-channel. + bool seen_alpha = false; + for (const PackedFrame& frame : ppf->frames) { + if (!frame.extra_channels.empty()) { + seen_alpha = true; + break; + } + } + if (seen_alpha) { + for (PackedFrame& frame : ppf->frames) { + ensure_have_alpha(&frame); + } + } + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/gif.h b/media/libjxl/src/lib/extras/dec/gif.h new file mode 100644 index 0000000000..b359517288 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/gif.h @@ -0,0 +1,30 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_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" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { +namespace extras { + +// Decodes `bytes` into `ppf`. color_hints are ignored. +Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints, + const SizeConstraints& constraints, PackedPixelFile* ppf); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_GIF_H_ diff --git a/media/libjxl/src/lib/extras/dec/jpg.cc b/media/libjxl/src/lib/extras/dec/jpg.cc new file mode 100644 index 0000000000..6b92f4a8a6 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/jpg.cc @@ -0,0 +1,289 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/jpg.h" + +#include <jpeglib.h> +#include <setjmp.h> +#include <stdint.h> + +#include <algorithm> +#include <numeric> +#include <utility> +#include <vector> + +#include "lib/jxl/base/status.h" +#include "lib/jxl/sanitizers.h" + +namespace jxl { +namespace extras { + +namespace { + +constexpr unsigned char kICCSignature[12] = { + 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00}; +constexpr int kICCMarker = JPEG_APP0 + 2; + +constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, + 0x66, 0x00, 0x00}; +constexpr int kExifMarker = JPEG_APP0 + 1; + +static inline bool IsJPG(const Span<const uint8_t> bytes) { + if (bytes.size() < 2) return false; + if (bytes[0] != 0xFF || bytes[1] != 0xD8) return false; + return true; +} + +bool MarkerIsICC(const jpeg_saved_marker_ptr marker) { + return marker->marker == kICCMarker && + marker->data_length >= sizeof kICCSignature + 2 && + std::equal(std::begin(kICCSignature), std::end(kICCSignature), + marker->data); +} +bool MarkerIsExif(const jpeg_saved_marker_ptr marker) { + return marker->marker == kExifMarker && + marker->data_length >= sizeof kExifSignature + 2 && + std::equal(std::begin(kExifSignature), std::end(kExifSignature), + marker->data); +} + +Status ReadICCProfile(jpeg_decompress_struct* const cinfo, + std::vector<uint8_t>* const icc) { + constexpr size_t kICCSignatureSize = sizeof kICCSignature; + // ICC signature + uint8_t index + uint8_t max_index. + constexpr size_t kICCHeadSize = kICCSignatureSize + 2; + // Markers are 1-indexed, and we keep them that way in this vector to get a + // convenient 0 at the front for when we compute the offsets later. + std::vector<size_t> marker_lengths; + int num_markers = 0; + int seen_markers_count = 0; + bool has_num_markers = false; + for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; + marker = marker->next) { + // marker is initialized by libjpeg, which we are not instrumenting with + // msan. + msan::UnpoisonMemory(marker, sizeof(*marker)); + msan::UnpoisonMemory(marker->data, marker->data_length); + if (!MarkerIsICC(marker)) continue; + + const int current_marker = marker->data[kICCSignatureSize]; + if (current_marker == 0) { + return JXL_FAILURE("inconsistent JPEG ICC marker numbering"); + } + const int current_num_markers = marker->data[kICCSignatureSize + 1]; + if (current_marker > current_num_markers) { + return JXL_FAILURE("inconsistent JPEG ICC marker numbering"); + } + if (has_num_markers) { + if (current_num_markers != num_markers) { + return JXL_FAILURE("inconsistent numbers of JPEG ICC markers"); + } + } else { + num_markers = current_num_markers; + has_num_markers = true; + marker_lengths.resize(num_markers + 1); + } + + size_t marker_length = marker->data_length - kICCHeadSize; + + if (marker_length == 0) { + // NB: if we allow empty chunks, then the next check is incorrect. + return JXL_FAILURE("Empty ICC chunk"); + } + + if (marker_lengths[current_marker] != 0) { + return JXL_FAILURE("duplicate JPEG ICC marker number"); + } + marker_lengths[current_marker] = marker_length; + seen_markers_count++; + } + + if (marker_lengths.empty()) { + // Not an error. + return false; + } + + if (seen_markers_count != num_markers) { + JXL_DASSERT(has_num_markers); + return JXL_FAILURE("Incomplete set of ICC chunks"); + } + + std::vector<size_t> offsets = std::move(marker_lengths); + std::partial_sum(offsets.begin(), offsets.end(), offsets.begin()); + icc->resize(offsets.back()); + + for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; + marker = marker->next) { + if (!MarkerIsICC(marker)) continue; + const uint8_t* first = marker->data + kICCHeadSize; + uint8_t current_marker = marker->data[kICCSignatureSize]; + size_t offset = offsets[current_marker - 1]; + size_t marker_length = offsets[current_marker] - offset; + std::copy_n(first, marker_length, icc->data() + offset); + } + + return true; +} + +void ReadExif(jpeg_decompress_struct* const cinfo, + std::vector<uint8_t>* const exif) { + constexpr size_t kExifSignatureSize = sizeof kExifSignature; + for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; + marker = marker->next) { + // marker is initialized by libjpeg, which we are not instrumenting with + // msan. + msan::UnpoisonMemory(marker, sizeof(*marker)); + msan::UnpoisonMemory(marker->data, marker->data_length); + if (!MarkerIsExif(marker)) continue; + size_t marker_length = marker->data_length - kExifSignatureSize; + exif->resize(marker_length); + std::copy_n(marker->data + kExifSignatureSize, marker_length, exif->data()); + return; + } +} + +void MyErrorExit(j_common_ptr cinfo) { + jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data); + (*cinfo->err->output_message)(cinfo); + jpeg_destroy_decompress(reinterpret_cast<j_decompress_ptr>(cinfo)); + longjmp(*env, 1); +} + +void MyOutputMessage(j_common_ptr cinfo) { +#if JXL_DEBUG_WARNING == 1 + char buf[JMSG_LENGTH_MAX + 1]; + (*cinfo->err->format_message)(cinfo, buf); + buf[JMSG_LENGTH_MAX] = 0; + JXL_WARNING("%s", buf); +#endif +} + +} // namespace + +Status DecodeImageJPG(const Span<const uint8_t> bytes, + const ColorHints& color_hints, + const SizeConstraints& constraints, + PackedPixelFile* ppf) { + // 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; + // cinfo is initialized by libjpeg, which we are not instrumenting with + // msan, therefore we need to initialize cinfo here. + msan::UnpoisonMemory(&cinfo, sizeof(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; + + jpeg_start_decompress(&cinfo); + JXL_ASSERT(cinfo.output_components == nbcomp); + + const JxlPixelFormat format{ + /*num_channels=*/static_cast<uint32_t>(nbcomp), + /*data_type=*/BITS_IN_JSAMPLE == 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, + /*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.output_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)}; + jpeg_read_scanlines(&cinfo, rows, 1); + msan::UnpoisonMemory(rows[0], sizeof(JSAMPLE) * cinfo.output_components * + cinfo.image_width); + } + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + return true; + }; + + return try_catch_block(); +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/jpg.h b/media/libjxl/src/lib/extras/dec/jpg.h new file mode 100644 index 0000000000..66b3452888 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/jpg.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_JPG_H_ +#define LIB_EXTRAS_DEC_JPG_H_ + +// Decodes JPG pixels and metadata in memory. + +#include <stdint.h> + +#include "lib/extras/codec.h" +#include "lib/extras/dec/color_hints.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { +namespace extras { + +// 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, + const SizeConstraints& constraints, PackedPixelFile* ppf); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_JPG_H_ diff --git a/media/libjxl/src/lib/extras/dec/pgx.cc b/media/libjxl/src/lib/extras/dec/pgx.cc new file mode 100644 index 0000000000..1417348c61 --- /dev/null +++ b/media/libjxl/src/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/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, + const SizeConstraints& constraints, + PackedPixelFile* ppf) { + 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/media/libjxl/src/lib/extras/dec/pgx.h b/media/libjxl/src/lib/extras/dec/pgx.h new file mode 100644 index 0000000000..38aedf51a4 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/pgx.h @@ -0,0 +1,32 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_DEC_PGX_H_ +#define LIB_EXTRAS_DEC_PGX_H_ + +// Decodes PGX pixels in memory. + +#include <stddef.h> +#include <stdint.h> + +#include "lib/extras/dec/color_hints.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { +namespace extras { + +// Decodes `bytes` into `ppf`. +Status DecodeImagePGX(Span<const uint8_t> bytes, const ColorHints& color_hints, + const SizeConstraints& constraints, PackedPixelFile* ppf); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_PGX_H_ diff --git a/media/libjxl/src/lib/extras/dec/pgx_test.cc b/media/libjxl/src/lib/extras/dec/pgx_test.cc new file mode 100644 index 0000000000..41e6bf8106 --- /dev/null +++ b/media/libjxl/src/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 "gtest/gtest.h" +#include "lib/extras/packed_image_convert.h" + +namespace jxl { +namespace extras { +namespace { + +Span<const uint8_t> MakeSpan(const char* str) { + return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), + strlen(str)); +} + +TEST(CodecPGXTest, Test8bits) { + std::string pgx = "PG ML + 8 2 3\npixels"; + + PackedPixelFile ppf; + ThreadPool* pool = nullptr; + + EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), + SizeConstraints(), &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(), + SizeConstraints(), &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/media/libjxl/src/lib/extras/dec/pnm.cc b/media/libjxl/src/lib/extras/dec/pnm.cc new file mode 100644 index 0000000000..93a42be321 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/pnm.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/pnm.h" + +#include <stdlib.h> +#include <string.h> + +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace extras { +namespace { + +struct HeaderPNM { + size_t xsize; + size_t ysize; + bool is_gray; // PGM + bool has_alpha; // PAM + size_t bits_per_sample; + bool floating_point; + bool big_endian; +}; + +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; + while (!MatchString("ENDHDR", /*skipws=*/false)) { + JXL_RETURN_IF_ERROR(SkipWhitespace()); + if (MatchString("WIDTH")) { + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); + } else if (MatchString("HEIGHT")) { + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); + } else if (MatchString("DEPTH")) { + JXL_RETURN_IF_ERROR(ParseUnsigned(&depth)); + } else if (MatchString("MAXVAL")) { + JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); + } 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 { + 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 != depth) { + return JXL_FAILURE("PAM: bad DEPTH"); + } + if (max_val == 0 || max_val >= 65536) { + return JXL_FAILURE("PAM: bad MAXVAL"); + } + // e.g When `max_val` is 1 , we want 1 bit: + header->bits_per_sample = FloorLog2Nonzero(max_val) + 1; + if ((1u << header->bits_per_sample) - 1 != max_val) + return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)"); + // PAM does not pack bits as in PBM. + + header->floating_point = false; + header->big_endian = true; + *pos = pos_; + return true; + } + + Status ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) { + JXL_RETURN_IF_ERROR(SkipWhitespace()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); + + JXL_RETURN_IF_ERROR(SkipWhitespace()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); + + JXL_RETURN_IF_ERROR(SkipWhitespace()); + size_t max_val; + JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); + if (max_val == 0 || max_val >= 65536) { + return JXL_FAILURE("PNM: bad MaxVal"); + } + header->bits_per_sample = FloorLog2Nonzero(max_val) + 1; + if ((1u << header->bits_per_sample) - 1 != max_val) + return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)"); + header->floating_point = false; + header->big_endian = true; + + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + + *pos = pos_; + return true; + } + + Status ParseHeaderPFM(HeaderPNM* header, const uint8_t** pos) { + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); + + JXL_RETURN_IF_ERROR(SkipBlank()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); + + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + // The scale has no meaning as multiplier, only its sign is used to + // indicate endianness. All software expects nominal range 0..1. + double scale; + JXL_RETURN_IF_ERROR(ParseSigned(&scale)); + if (scale == 0.0) { + return JXL_FAILURE("PFM: bad scale factor value."); + } else if (std::abs(scale) != 1.0) { + JXL_WARNING("PFM: Discarding non-unit scale factor"); + } + header->big_endian = scale > 0.0; + header->bits_per_sample = 32; + header->floating_point = true; + + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + + *pos = pos_; + return true; + } + + const uint8_t* pos_; + const uint8_t* const end_; +}; + +Span<const uint8_t> MakeSpan(const char* str) { + return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), + strlen(str)); +} + +} // namespace + +Status DecodeImagePNM(const Span<const uint8_t> bytes, + const ColorHints& color_hints, + const SizeConstraints& constraints, + PackedPixelFile* ppf) { + Parser parser(bytes); + HeaderPNM header = {}; + const uint8_t* pos = nullptr; + if (!parser.ParseHeader(&header, &pos)) return false; + JXL_RETURN_IF_ERROR( + VerifyDimensions(&constraints, header.xsize, header.ysize)); + + if (header.bits_per_sample == 0 || header.bits_per_sample > 32) { + return JXL_FAILURE("PNM: bits_per_sample invalid"); + } + + // PPM specify that in the raster, the sample values are "nonlinear" (BP.709, + // with gamma number of 2.2). Deviate from the specification and assume + // `sRGB` in our implementation. + JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, + header.is_gray, ppf)); + + ppf->info.xsize = header.xsize; + ppf->info.ysize = header.ysize; + if (header.floating_point) { + ppf->info.bits_per_sample = 32; + ppf->info.exponent_bits_per_sample = 8; + } else { + ppf->info.bits_per_sample = header.bits_per_sample; + ppf->info.exponent_bits_per_sample = 0; + } + + ppf->info.orientation = JXL_ORIENT_IDENTITY; + + // No alpha in PNM and PFM + ppf->info.alpha_bits = (header.has_alpha ? ppf->info.bits_per_sample : 0); + ppf->info.alpha_exponent_bits = 0; + ppf->info.num_color_channels = (header.is_gray ? 1 : 3); + ppf->info.num_extra_channels = (header.has_alpha ? 1 : 0); + + 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=*/ppf->info.num_color_channels + + ppf->info.num_extra_channels, + /*data_type=*/data_type, + /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, + /*align=*/0, + }; + ppf->frames.clear(); + ppf->frames.emplace_back(header.xsize, header.ysize, format); + auto* frame = &ppf->frames.back(); + + frame->color.bitdepth_from_format = false; + frame->color.flipped_y = header.bits_per_sample == 32; // PFMs are flipped + size_t pnm_remaining_size = bytes.data() + bytes.size() - pos; + if (pnm_remaining_size < frame->color.pixels_size) { + return JXL_FAILURE("PNM file too small"); + } + memcpy(frame->color.pixels(), pos, frame->color.pixels_size); + 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/media/libjxl/src/lib/extras/dec/pnm.h b/media/libjxl/src/lib/extras/dec/pnm.h new file mode 100644 index 0000000000..f6374830ce --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/pnm.h @@ -0,0 +1,38 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_DEC_PNM_H_ +#define LIB_EXTRAS_DEC_PNM_H_ + +// Decodes PBM/PGM/PPM/PFM pixels in memory. + +#include <stddef.h> +#include <stdint.h> + +// TODO(janwas): workaround for incorrect Win64 codegen (cause unknown) +#include <hwy/highway.h> + +#include "lib/extras/dec/color_hints.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { +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, + const SizeConstraints& constraints, PackedPixelFile* ppf); + +void TestCodecPNM(); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_PNM_H_ |