diff options
Diffstat (limited to 'lib/extras/dec')
-rw-r--r-- | lib/extras/dec/apng.cc | 289 | ||||
-rw-r--r-- | lib/extras/dec/apng.h | 11 | ||||
-rw-r--r-- | lib/extras/dec/color_description.cc | 4 | ||||
-rw-r--r-- | lib/extras/dec/color_description.h | 3 | ||||
-rw-r--r-- | lib/extras/dec/color_description_test.cc | 5 | ||||
-rw-r--r-- | lib/extras/dec/color_hints.cc | 44 | ||||
-rw-r--r-- | lib/extras/dec/color_hints.h | 2 | ||||
-rw-r--r-- | lib/extras/dec/decode.cc | 180 | ||||
-rw-r--r-- | lib/extras/dec/decode.h | 24 | ||||
-rw-r--r-- | lib/extras/dec/exr.cc | 27 | ||||
-rw-r--r-- | lib/extras/dec/exr.h | 10 | ||||
-rw-r--r-- | lib/extras/dec/gif.cc | 45 | ||||
-rw-r--r-- | lib/extras/dec/gif.h | 9 | ||||
-rw-r--r-- | lib/extras/dec/jpegli.cc | 271 | ||||
-rw-r--r-- | lib/extras/dec/jpegli.h | 41 | ||||
-rw-r--r-- | lib/extras/dec/jpg.cc | 73 | ||||
-rw-r--r-- | lib/extras/dec/jpg.h | 18 | ||||
-rw-r--r-- | lib/extras/dec/jxl.cc | 158 | ||||
-rw-r--r-- | lib/extras/dec/jxl.h | 11 | ||||
-rw-r--r-- | lib/extras/dec/pgx.cc | 8 | ||||
-rw-r--r-- | lib/extras/dec/pgx.h | 8 | ||||
-rw-r--r-- | lib/extras/dec/pgx_test.cc | 14 | ||||
-rw-r--r-- | lib/extras/dec/pnm.cc | 176 | ||||
-rw-r--r-- | lib/extras/dec/pnm.h | 28 |
24 files changed, 1173 insertions, 286 deletions
diff --git a/lib/extras/dec/apng.cc b/lib/extras/dec/apng.cc index 5667466..f77dab7 100644 --- a/lib/extras/dec/apng.cc +++ b/lib/extras/dec/apng.cc @@ -36,27 +36,34 @@ * */ -#include <stdio.h> +#include <jxl/codestream_header.h> +#include <jxl/encode.h> #include <string.h> #include <string> #include <utility> #include <vector> -#include "jxl/codestream_header.h" -#include "jxl/encode.h" +#include "lib/extras/size_constraints.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/common.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/base/scope_guard.h" -#include "lib/jxl/common.h" #include "lib/jxl/sanitizers.h" +#if JPEGXL_ENABLE_APNG #include "png.h" /* original (unpatched) libpng is ok */ +#endif namespace jxl { namespace extras { +#if JPEGXL_ENABLE_APNG namespace { +constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, + 0x66, 0x00, 0x00}; + /* hIST chunk tail is not proccesed properly; skip this chunk completely; see https://github.com/glennrp/libpng/pull/413 */ const png_byte kIgnoredPngChunks[] = { @@ -73,11 +80,145 @@ Status DecodeSRGB(const unsigned char* payload, const size_t payload_size, if (payload_size != 1) return JXL_FAILURE("Wrong sRGB size"); // (PNG uses the same values as ICC.) if (payload[0] >= 4) return JXL_FAILURE("Invalid Rendering Intent"); + color_encoding->white_point = JXL_WHITE_POINT_D65; + color_encoding->primaries = JXL_PRIMARIES_SRGB; + color_encoding->transfer_function = JXL_TRANSFER_FUNCTION_SRGB; color_encoding->rendering_intent = static_cast<JxlRenderingIntent>(payload[0]); return true; } +// If the cICP profile is not fully supported, return false and leave +// color_encoding unmodified. +Status DecodeCICP(const unsigned char* payload, const size_t payload_size, + JxlColorEncoding* color_encoding) { + if (payload_size != 4) return JXL_FAILURE("Wrong cICP size"); + JxlColorEncoding color_enc = *color_encoding; + + // From https://www.itu.int/rec/T-REC-H.273-202107-I/en + if (payload[0] == 1) { + // IEC 61966-2-1 sRGB + color_enc.primaries = JXL_PRIMARIES_SRGB; + color_enc.white_point = JXL_WHITE_POINT_D65; + } else if (payload[0] == 4) { + // Rec. ITU-R BT.470-6 System M + color_enc.primaries = JXL_PRIMARIES_CUSTOM; + color_enc.primaries_red_xy[0] = 0.67; + color_enc.primaries_red_xy[1] = 0.33; + color_enc.primaries_green_xy[0] = 0.21; + color_enc.primaries_green_xy[1] = 0.71; + color_enc.primaries_blue_xy[0] = 0.14; + color_enc.primaries_blue_xy[1] = 0.08; + color_enc.white_point = JXL_WHITE_POINT_CUSTOM; + color_enc.white_point_xy[0] = 0.310; + color_enc.white_point_xy[1] = 0.316; + } else if (payload[0] == 5) { + // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + color_enc.primaries = JXL_PRIMARIES_CUSTOM; + color_enc.primaries_red_xy[0] = 0.64; + color_enc.primaries_red_xy[1] = 0.33; + color_enc.primaries_green_xy[0] = 0.29; + color_enc.primaries_green_xy[1] = 0.60; + color_enc.primaries_blue_xy[0] = 0.15; + color_enc.primaries_blue_xy[1] = 0.06; + color_enc.white_point = JXL_WHITE_POINT_D65; + } else if (payload[0] == 6 || payload[0] == 7) { + // SMPTE ST 170 (2004) / SMPTE ST 240 (1999) + color_enc.primaries = JXL_PRIMARIES_CUSTOM; + color_enc.primaries_red_xy[0] = 0.630; + color_enc.primaries_red_xy[1] = 0.340; + color_enc.primaries_green_xy[0] = 0.310; + color_enc.primaries_green_xy[1] = 0.595; + color_enc.primaries_blue_xy[0] = 0.155; + color_enc.primaries_blue_xy[1] = 0.070; + color_enc.white_point = JXL_WHITE_POINT_D65; + } else if (payload[0] == 8) { + // Generic film (colour filters using Illuminant C) + color_enc.primaries = JXL_PRIMARIES_CUSTOM; + color_enc.primaries_red_xy[0] = 0.681; + color_enc.primaries_red_xy[1] = 0.319; + color_enc.primaries_green_xy[0] = 0.243; + color_enc.primaries_green_xy[1] = 0.692; + color_enc.primaries_blue_xy[0] = 0.145; + color_enc.primaries_blue_xy[1] = 0.049; + color_enc.white_point = JXL_WHITE_POINT_CUSTOM; + color_enc.white_point_xy[0] = 0.310; + color_enc.white_point_xy[1] = 0.316; + } else if (payload[0] == 9) { + // Rec. ITU-R BT.2100-2 + color_enc.primaries = JXL_PRIMARIES_2100; + color_enc.white_point = JXL_WHITE_POINT_D65; + } else if (payload[0] == 10) { + // CIE 1931 XYZ + color_enc.primaries = JXL_PRIMARIES_CUSTOM; + color_enc.primaries_red_xy[0] = 1; + color_enc.primaries_red_xy[1] = 0; + color_enc.primaries_green_xy[0] = 0; + color_enc.primaries_green_xy[1] = 1; + color_enc.primaries_blue_xy[0] = 0; + color_enc.primaries_blue_xy[1] = 0; + color_enc.white_point = JXL_WHITE_POINT_E; + } else if (payload[0] == 11) { + // SMPTE RP 431-2 (2011) + color_enc.primaries = JXL_PRIMARIES_P3; + color_enc.white_point = JXL_WHITE_POINT_DCI; + } else if (payload[0] == 12) { + // SMPTE EG 432-1 (2010) + color_enc.primaries = JXL_PRIMARIES_P3; + color_enc.white_point = JXL_WHITE_POINT_D65; + } else if (payload[0] == 22) { + color_enc.primaries = JXL_PRIMARIES_CUSTOM; + color_enc.primaries_red_xy[0] = 0.630; + color_enc.primaries_red_xy[1] = 0.340; + color_enc.primaries_green_xy[0] = 0.295; + color_enc.primaries_green_xy[1] = 0.605; + color_enc.primaries_blue_xy[0] = 0.155; + color_enc.primaries_blue_xy[1] = 0.077; + color_enc.white_point = JXL_WHITE_POINT_D65; + } else { + JXL_WARNING("Unsupported primaries specified in cICP chunk: %d", + static_cast<int>(payload[0])); + return false; + } + + if (payload[1] == 1 || payload[1] == 6 || payload[1] == 14 || + payload[1] == 15) { + // Rec. ITU-R BT.709-6 + color_enc.transfer_function = JXL_TRANSFER_FUNCTION_709; + } else if (payload[1] == 4) { + // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; + color_enc.gamma = 1 / 2.2; + } else if (payload[1] == 5) { + // Rec. ITU-R BT.470-6 System B, G + color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; + color_enc.gamma = 1 / 2.8; + } else if (payload[1] == 8 || payload[1] == 13 || payload[1] == 16 || + payload[1] == 17 || payload[1] == 18) { + // These codes all match the corresponding JXL enum values + color_enc.transfer_function = static_cast<JxlTransferFunction>(payload[1]); + } else { + JXL_WARNING("Unsupported transfer function specified in cICP chunk: %d", + static_cast<int>(payload[1])); + return false; + } + + if (payload[2] != 0) { + JXL_WARNING("Unsupported color space specified in cICP chunk: %d", + static_cast<int>(payload[2])); + return false; + } + if (payload[3] != 1) { + JXL_WARNING("Unsupported full-range flag specified in cICP chunk: %d", + static_cast<int>(payload[3])); + return false; + } + // cICP has no rendering intent, so use the default + color_enc.rendering_intent = JXL_RENDERING_INTENT_RELATIVE; + *color_encoding = color_enc; + return true; +} + Status DecodeGAMA(const unsigned char* payload, const size_t payload_size, JxlColorEncoding* color_encoding) { if (payload_size != 4) return JXL_FAILURE("Wrong gAMA size"); @@ -129,6 +270,11 @@ class BlobsReaderPNG { return false; } if (type == "exif") { + // Remove "Exif\0\0" prefix if present + if (bytes.size() >= sizeof kExifSignature && + memcmp(bytes.data(), kExifSignature, sizeof kExifSignature) == 0) { + bytes.erase(bytes.begin(), bytes.begin() + sizeof kExifSignature); + } if (!metadata->exif.empty()) { JXL_WARNING("overwriting EXIF (%" PRIuS " bytes) with base16 (%" PRIuS " bytes)", @@ -136,9 +282,9 @@ class BlobsReaderPNG { } metadata->exif = std::move(bytes); } else if (type == "iptc") { - // TODO (jon): Deal with IPTC in some way + // TODO(jon): Deal with IPTC in some way } else if (type == "8bim") { - // TODO (jon): Deal with 8bim in some way + // 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 @@ -228,6 +374,10 @@ class BlobsReaderPNG { // We parsed so far a \n, some number of non \n characters and are now // pointing at a \n. if (*(pos++) != '\n') return false; + // Skip leading spaces + while (pos < encoded_end && *pos == ' ') { + pos++; + } uint32_t bytes_to_decode = 0; JXL_RETURN_IF_ERROR(DecodeDecimal(&pos, encoded_end, &bytes_to_decode)); @@ -274,6 +424,7 @@ constexpr uint32_t kId_fcTL = 0x4C546366; constexpr uint32_t kId_IDAT = 0x54414449; constexpr uint32_t kId_fdAT = 0x54416466; constexpr uint32_t kId_IEND = 0x444E4549; +constexpr uint32_t kId_cICP = 0x50434963; constexpr uint32_t kId_iCCP = 0x50434369; constexpr uint32_t kId_sRGB = 0x42475273; constexpr uint32_t kId_gAMA = 0x414D4167; @@ -342,6 +493,12 @@ int processing_start(png_structp& png_ptr, png_infop& info_ptr, void* frame_ptr, std::vector<std::vector<uint8_t>>& chunksInfo) { unsigned char header[8] = {137, 80, 78, 71, 13, 10, 26, 10}; + // Cleanup prior decoder, if any. + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + // Just in case. Not all versions on libpng wipe-out the pointers. + png_ptr = nullptr; + info_ptr = nullptr; + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); info_ptr = png_create_info_struct(png_ptr); if (!png_ptr || !info_ptr) return 1; @@ -403,11 +560,20 @@ int processing_finish(png_structp png_ptr, png_infop info_ptr, } } // namespace +#endif + +bool CanDecodeAPNG() { +#if JPEGXL_ENABLE_APNG + return true; +#else + return false; +#endif +} Status DecodeImageAPNG(const Span<const uint8_t> bytes, - const ColorHints& color_hints, - const SizeConstraints& constraints, - PackedPixelFile* ppf) { + const ColorHints& color_hints, PackedPixelFile* ppf, + const SizeConstraints* constraints) { +#if JPEGXL_ENABLE_APNG Reader r; unsigned int id, j, w, h, w0, h0, x0, y0; unsigned int delay_num, delay_den, dop, bop, rowbytes, imagesize; @@ -419,6 +585,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, std::vector<std::vector<uint8_t>> chunksInfo; bool isAnimated = false; bool hasInfo = false; + bool seenFctl = false; APNGFrame frameRaw = {}; uint32_t num_channels; JxlPixelFormat format; @@ -457,7 +624,8 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, ppf->frames.clear(); - bool have_color = false, have_srgb = false; + bool have_color = false; + bool have_cicp = false, have_iccp = false, have_srgb = false; bool errorstate = true; if (id == kId_IHDR && chunkIHDR.size() == 25) { x0 = 0; @@ -478,12 +646,14 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, ppf->color_encoding.white_point = JXL_WHITE_POINT_D65; ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB; ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB; + ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_RELATIVE; if (!processing_start(png_ptr, info_ptr, (void*)&frameRaw, hasInfo, chunkIHDR, chunksInfo)) { while (!r.Eof()) { id = read_chunk(&r, &chunk); if (!id) break; + seenFctl |= (id == kId_fcTL); if (id == kId_acTL && !hasInfo && !isAnimated) { isAnimated = true; @@ -544,11 +714,16 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, } } else if (id == kId_IDAT) { // First IDAT chunk means we now have all header info + if (seenFctl) { + // `fcTL` chunk must appear after all `IDAT` chunks + return JXL_FAILURE("IDAT chunk after fcTL chunk"); + } hasInfo = true; JXL_CHECK(w == png_get_image_width(png_ptr, info_ptr)); JXL_CHECK(h == png_get_image_height(png_ptr, info_ptr)); int colortype = png_get_color_type(png_ptr, info_ptr); - ppf->info.bits_per_sample = png_get_bit_depth(png_ptr, info_ptr); + int png_bit_depth = png_get_bit_depth(png_ptr, info_ptr); + ppf->info.bits_per_sample = png_bit_depth; png_color_8p sigbits = NULL; png_get_sBIT(png_ptr, info_ptr, &sigbits); if (colortype & 1) { @@ -559,8 +734,18 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, 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) + sigbits->green == sigbits->blue) { ppf->info.bits_per_sample = sigbits->red; + } else if (sigbits) { + int maxbps = std::max(sigbits->red, + std::max(sigbits->green, sigbits->blue)); + JXL_WARNING( + "sBIT chunk: bit depths for R, G, and B are not the same (%i " + "%i %i), while in JPEG XL they have to be the same. Setting " + "RGB bit depth to %i.", + sigbits->red, sigbits->green, sigbits->blue, maxbps); + ppf->info.bits_per_sample = maxbps; + } } else { ppf->info.num_color_channels = 1; ppf->color_encoding.color_space = JXL_COLOR_SPACE_GRAY; @@ -569,12 +754,12 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, 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; + if (sigbits && sigbits->alpha != ppf->info.bits_per_sample) { + JXL_WARNING( + "sBIT chunk: bit depths for RGBA are inconsistent " + "(%i %i %i %i). Setting A bitdepth to %i.", + sigbits->red, sigbits->green, sigbits->blue, sigbits->alpha, + ppf->info.bits_per_sample); } } else { ppf->info.alpha_bits = 0; @@ -584,7 +769,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, : JXL_COLOR_SPACE_RGB); ppf->info.xsize = w; ppf->info.ysize = h; - JXL_RETURN_IF_ERROR(VerifyDimensions(&constraints, w, h)); + JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, w, h)); num_channels = ppf->info.num_color_channels + (ppf->info.alpha_bits ? 1 : 0); format = { @@ -594,6 +779,9 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, /*endianness=*/JXL_BIG_ENDIAN, /*align=*/0, }; + if (png_bit_depth > 8 && format.data_type == JXL_TYPE_UINT8) { + png_set_strip_16(png_ptr); + } bytes_per_pixel = num_channels * (format.data_type == JXL_TYPE_UINT16 ? 2 : 1); rowbytes = w * bytes_per_pixel; @@ -607,13 +795,26 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, break; } } else if (id == kId_fdAT && isAnimated) { + if (!hasInfo) { + return JXL_FAILURE("fDAT chunk before iDAT"); + } png_save_uint_32(chunk.data() + 4, chunk.size() - 16); memcpy(chunk.data() + 8, "IDAT", 4); if (processing_data(png_ptr, info_ptr, chunk.data() + 4, chunk.size() - 4)) { break; } - } else if (id == kId_iCCP) { + } else if (id == kId_cICP) { + // Color profile chunks: cICP has the highest priority, followed by + // iCCP and sRGB (which shouldn't co-exist, but if they do, we use + // iCCP), followed finally by gAMA and cHRM. + if (DecodeCICP(chunk.data() + 8, chunk.size() - 12, + &ppf->color_encoding)) { + have_cicp = true; + have_color = true; + ppf->icc.clear(); + } + } else if (!have_cicp && id == kId_iCCP) { if (processing_data(png_ptr, info_ptr, chunk.data(), chunk.size())) { JXL_WARNING("Corrupt iCCP chunk"); break; @@ -630,19 +831,20 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, if (ok && proflen) { ppf->icc.assign(profile, profile + proflen); have_color = true; + have_iccp = true; } else { // TODO(eustas): JXL_WARNING? } - } else if (id == kId_sRGB) { + } else if (!have_cicp && !have_iccp && id == kId_sRGB) { JXL_RETURN_IF_ERROR(DecodeSRGB(chunk.data() + 8, chunk.size() - 12, &ppf->color_encoding)); have_srgb = true; have_color = true; - } else if (id == kId_gAMA) { + } else if (!have_cicp && !have_srgb && !have_iccp && id == kId_gAMA) { JXL_RETURN_IF_ERROR(DecodeGAMA(chunk.data() + 8, chunk.size() - 12, &ppf->color_encoding)); have_color = true; - } else if (id == kId_cHRM) { + } else if (!have_cicp && !have_srgb && !have_iccp && id == kId_cHRM) { JXL_RETURN_IF_ERROR(DecodeCHRM(chunk.data() + 8, chunk.size() - 12, &ppf->color_encoding)); have_color = true; @@ -665,12 +867,6 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, } } - 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)); } @@ -706,31 +902,29 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, 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 && + if (px0 >= x0 && py0 >= y0 && px0 + pxs <= x0 + xsize && + py0 + pys <= y0 + ysize && frame.blend_op == BLEND_OP_SOURCE && use_for_next_frame) { // If the previous frame is entirely contained in the current frame and // we are using BLEND_OP_SOURCE, nothing special needs to be done. ppf->frames.emplace_back(std::move(frame.data)); - } else if (px0 == x0 && py0 == y0 && px0 + pxs == x0 + xs && - py0 + pys == y0 + ys && use_for_next_frame) { + } else if (px0 == x0 && py0 == y0 && px0 + pxs == x0 + xsize && + py0 + pys == y0 + ysize && use_for_next_frame) { // If the new frame has the same size as the old one, but we are // blending, we can instead just not blend. should_blend = false; ppf->frames.emplace_back(std::move(frame.data)); - } else if (px0 <= x0 && py0 <= y0 && px0 + pxs >= x0 + xs && - py0 + pys >= y0 + ys && use_for_next_frame) { + } else if (px0 <= x0 && py0 <= y0 && px0 + pxs >= x0 + xsize && + py0 + pys >= y0 + ysize && use_for_next_frame) { // If the new frame is contained within the old frame, we can pad the // new frame with zeros and not blend. PackedImage new_data(pxs, pys, frame.data.format); memset(new_data.pixels(), 0, new_data.pixels_size); - for (size_t y = 0; y < ys; y++) { + for (size_t y = 0; y < ysize; y++) { size_t bytes_per_pixel = PackedImage::BitsPerChannel(new_data.format.data_type) * new_data.format.num_channels / 8; @@ -739,7 +933,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, bytes_per_pixel * (x0 - px0), static_cast<const uint8_t*>(frame.data.pixels()) + frame.data.stride * y, - xs * bytes_per_pixel); + xsize * bytes_per_pixel); } x0 = px0; @@ -749,19 +943,21 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, should_blend = false; ppf->frames.emplace_back(std::move(new_data)); } else { - // If all else fails, insert a dummy blank frame with kReplace. + // If all else fails, insert a placeholder blank frame with kReplace. PackedImage blank(pxs, pys, frame.data.format); memset(blank.pixels(), 0, blank.pixels_size); ppf->frames.emplace_back(std::move(blank)); auto& pframe = ppf->frames.back(); pframe.frame_info.layer_info.crop_x0 = px0; pframe.frame_info.layer_info.crop_y0 = py0; - pframe.frame_info.layer_info.xsize = frame.xsize; - pframe.frame_info.layer_info.ysize = frame.ysize; + pframe.frame_info.layer_info.xsize = pxs; + pframe.frame_info.layer_info.ysize = pys; pframe.frame_info.duration = 0; - pframe.frame_info.layer_info.have_crop = 0; + bool is_full_size = px0 == 0 && py0 == 0 && pxs == ppf->info.xsize && + pys == ppf->info.ysize; + pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1; pframe.frame_info.layer_info.blend_info.blendmode = JXL_BLEND_REPLACE; - pframe.frame_info.layer_info.blend_info.source = 0; + pframe.frame_info.layer_info.blend_info.source = 1; pframe.frame_info.layer_info.save_as_reference = 1; ppf->frames.emplace_back(std::move(frame.data)); } @@ -780,7 +976,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, 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.source = 1; pframe.frame_info.layer_info.blend_info.alpha = 0; pframe.frame_info.layer_info.save_as_reference = use_for_next_frame ? 1 : 0; @@ -791,6 +987,9 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes, ppf->frames.back().frame_info.is_last = true; return true; +#else + return false; +#endif } } // namespace extras diff --git a/lib/extras/dec/apng.h b/lib/extras/dec/apng.h index a68f6f8..d91364b 100644 --- a/lib/extras/dec/apng.h +++ b/lib/extras/dec/apng.h @@ -13,18 +13,21 @@ #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 { + +struct SizeConstraints; + namespace extras { +bool CanDecodeAPNG(); + // Decodes `bytes` into `ppf`. Status DecodeImageAPNG(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, - PackedPixelFile* ppf); + PackedPixelFile* ppf, + const SizeConstraints* constraints = nullptr); } // namespace extras } // namespace jxl diff --git a/lib/extras/dec/color_description.cc b/lib/extras/dec/color_description.cc index 2325b50..54f6aa4 100644 --- a/lib/extras/dec/color_description.cc +++ b/lib/extras/dec/color_description.cc @@ -69,9 +69,9 @@ Status ParseEnum(const std::string& token, const EnumName<T>* enum_values, } return false; } -#define ARRAYSIZE(X) (sizeof(X) / sizeof((X)[0])) +#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0])) #define PARSE_ENUM(type, token, value) \ - ParseEnum<type>(token, k##type##Names, ARRAYSIZE(k##type##Names), value) + ParseEnum<type>(token, k##type##Names, ARRAY_SIZE(k##type##Names), value) class Tokenizer { public: diff --git a/lib/extras/dec/color_description.h b/lib/extras/dec/color_description.h index 989d591..23680ff 100644 --- a/lib/extras/dec/color_description.h +++ b/lib/extras/dec/color_description.h @@ -6,9 +6,10 @@ #ifndef LIB_EXTRAS_COLOR_DESCRIPTION_H_ #define LIB_EXTRAS_COLOR_DESCRIPTION_H_ +#include <jxl/color_encoding.h> + #include <string> -#include "jxl/color_encoding.h" #include "lib/jxl/base/status.h" namespace jxl { diff --git a/lib/extras/dec/color_description_test.cc b/lib/extras/dec/color_description_test.cc index 8ae9e5d..e6e34f0 100644 --- a/lib/extras/dec/color_description_test.cc +++ b/lib/extras/dec/color_description_test.cc @@ -5,9 +5,9 @@ #include "lib/extras/dec/color_description.h" -#include "gtest/gtest.h" #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/test_utils.h" +#include "lib/jxl/testing.h" namespace jxl { @@ -21,8 +21,7 @@ TEST(ColorDescriptionTest, RoundTripAll) { JxlColorEncoding c_external = {}; EXPECT_TRUE(ParseDescription(description, &c_external)); ColorEncoding c_internal; - EXPECT_TRUE( - ConvertExternalToInternalColorEncoding(c_external, &c_internal)); + EXPECT_TRUE(c_internal.FromExternal(c_external)); EXPECT_TRUE(c_original.SameColorEncoding(c_internal)) << "Where c_original=" << c_original << " and c_internal=" << c_internal; diff --git a/lib/extras/dec/color_hints.cc b/lib/extras/dec/color_hints.cc index cf7d3e3..5c6d7b8 100644 --- a/lib/extras/dec/color_hints.cc +++ b/lib/extras/dec/color_hints.cc @@ -5,9 +5,12 @@ #include "lib/extras/dec/color_hints.h" -#include "jxl/encode.h" +#include <jxl/encode.h> + +#include <vector> + #include "lib/extras/dec/color_description.h" -#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/status.h" namespace jxl { namespace extras { @@ -15,19 +18,15 @@ 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; + bool got_color_space = color_already_set; JXL_RETURN_IF_ERROR(color_hints.Foreach( - [is_gray, ppf, &got_color_space](const std::string& key, - const std::string& value) -> Status { + [color_already_set, is_gray, ppf, &got_color_space]( + const std::string& key, const std::string& value) -> Status { + if (color_already_set && (key == "color_space" || key == "icc")) { + JXL_WARNING("Decoder ignoring %s hint", key.c_str()); + return true; + } if (key == "color_space") { JxlColorEncoding c_original_external; if (!ParseDescription(value, &c_original_external)) { @@ -41,9 +40,23 @@ Status ApplyColorHints(const ColorHints& color_hints, } got_color_space = true; - } else if (key == "icc_pathname") { - JXL_RETURN_IF_ERROR(ReadFile(value, &ppf->icc)); + } else if (key == "icc") { + const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data()); + std::vector<uint8_t> icc(data, data + value.size()); + ppf->icc.swap(icc); got_color_space = true; + } else if (key == "exif") { + const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data()); + std::vector<uint8_t> blob(data, data + value.size()); + ppf->metadata.exif.swap(blob); + } else if (key == "xmp") { + const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data()); + std::vector<uint8_t> blob(data, data + value.size()); + ppf->metadata.xmp.swap(blob); + } else if (key == "jumbf") { + const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data()); + std::vector<uint8_t> blob(data, data + value.size()); + ppf->metadata.jumbf.swap(blob); } else { JXL_WARNING("Ignoring %s hint", key.c_str()); } @@ -51,7 +64,6 @@ Status ApplyColorHints(const ColorHints& color_hints, })); 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; diff --git a/lib/extras/dec/color_hints.h b/lib/extras/dec/color_hints.h index 9c7de88..036f203 100644 --- a/lib/extras/dec/color_hints.h +++ b/lib/extras/dec/color_hints.h @@ -10,6 +10,8 @@ // information into the file, and those that support it may not have it. // To allow attaching color information to those file formats the caller can // define these color hints. +// Besides color space information, 'ColorHints' may also include other +// additional information such as Exif, XMP and JUMBF metadata. #include <stddef.h> #include <stdint.h> diff --git a/lib/extras/dec/decode.cc b/lib/extras/dec/decode.cc index 8712e03..9149208 100644 --- a/lib/extras/dec/decode.cc +++ b/lib/extras/dec/decode.cc @@ -7,18 +7,11 @@ #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/jxl.h" #include "lib/extras/dec/pgx.h" #include "lib/extras/dec/pnm.h" @@ -29,59 +22,89 @@ namespace { // Any valid encoding is larger (ensures codecs can read the first few bytes) constexpr size_t kMinBytes = 9; -} // namespace - -std::vector<Codec> AvailableCodecs() { - std::vector<Codec> out; -#if JPEGXL_ENABLE_APNG - out.push_back(Codec::kPNG); -#endif -#if JPEGXL_ENABLE_EXR - out.push_back(Codec::kEXR); -#endif -#if JPEGXL_ENABLE_GIF - out.push_back(Codec::kGIF); -#endif -#if JPEGXL_ENABLE_JPEG - out.push_back(Codec::kJPG); -#endif - out.push_back(Codec::kPGX); - out.push_back(Codec::kPNM); - return out; -} +void BasenameAndExtension(const std::string& path, std::string* filename, + std::string* extension) { + // Pattern: "png:name" or "png:-" + size_t pos = path.find_first_of(':'); + if (pos != std::string::npos) { + *extension = "." + path.substr(0, pos); + *filename = path.substr(pos + 1); + //+ ((path.length() == pos + 2 && path.substr(pos + 1, 1) == "-") ? "" : + //*extension); + return; + } -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; + // Pattern: "name.png" + pos = path.find_last_of('.'); + if (pos != std::string::npos) { + *extension = path.substr(pos); + *filename = path; + return; + } - if (extension == ".jpg") return Codec::kJPG; - if (extension == ".jpeg") return Codec::kJPG; + // Extension not found + *filename = path; + *extension = ""; +} - if (extension == ".pgx") return Codec::kPGX; +} // namespace - 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") { +Codec CodecFromPath(std::string path, size_t* JXL_RESTRICT bits_per_sample, + std::string* filename, std::string* extension) { + std::string base; + std::string ext; + BasenameAndExtension(path, &base, &ext); + if (filename) *filename = base; + if (extension) *extension = ext; + + std::transform(ext.begin(), ext.end(), ext.begin(), [](char c) { + return std::tolower(c, std::locale::classic()); + }); + if (ext == ".png") return Codec::kPNG; + + if (ext == ".jpg") return Codec::kJPG; + if (ext == ".jpeg") return Codec::kJPG; + + if (ext == ".pgx") return Codec::kPGX; + + if (ext == ".pam") return Codec::kPNM; + if (ext == ".pnm") return Codec::kPNM; + if (ext == ".pgm") return Codec::kPNM; + if (ext == ".ppm") return Codec::kPNM; + if (ext == ".pfm") { if (bits_per_sample != nullptr) *bits_per_sample = 32; return Codec::kPNM; } - if (extension == ".gif") return Codec::kGIF; + if (ext == ".gif") return Codec::kGIF; - if (extension == ".exr") return Codec::kEXR; + if (ext == ".exr") return Codec::kEXR; return Codec::kUnknown; } +bool CanDecode(Codec codec) { + switch (codec) { + case Codec::kEXR: + return CanDecodeEXR(); + case Codec::kGIF: + return CanDecodeGIF(); + case Codec::kJPG: + return CanDecodeJPG(); + case Codec::kPNG: + return CanDecodeAPNG(); + case Codec::kPNM: + case Codec::kPGX: + case Codec::kJXL: + return true; + default: + return false; + } +} + Status DecodeBytes(const Span<const uint8_t> bytes, - const ColorHints& color_hints, - const SizeConstraints& constraints, - extras::PackedPixelFile* ppf, Codec* orig_codec) { + const ColorHints& color_hints, extras::PackedPixelFile* ppf, + const SizeConstraints* constraints, Codec* orig_codec) { if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes"); *ppf = extras::PackedPixelFile(); @@ -90,33 +113,42 @@ Status DecodeBytes(const Span<const uint8_t> bytes, 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 { + const auto choose_codec = [&]() -> Codec { + if (DecodeImageAPNG(bytes, color_hints, ppf, constraints)) { + return Codec::kPNG; + } + if (DecodeImagePGX(bytes, color_hints, ppf, constraints)) { + return Codec::kPGX; + } + if (DecodeImagePNM(bytes, color_hints, ppf, constraints)) { + return Codec::kPNM; + } + JXLDecompressParams dparams = {}; + for (const uint32_t num_channels : {1, 2, 3, 4}) { + dparams.accepted_formats.push_back( + {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0}); + } + size_t decoded_bytes; + if (DecodeImageJXL(bytes.data(), bytes.size(), dparams, &decoded_bytes, + ppf) && + ApplyColorHints(color_hints, true, ppf->info.num_color_channels == 1, + ppf)) { + return Codec::kJXL; + } + if (DecodeImageGIF(bytes, color_hints, ppf, constraints)) { + return Codec::kGIF; + } + if (DecodeImageJPG(bytes, color_hints, ppf, constraints)) { + return Codec::kJPG; + } + if (DecodeImageEXR(bytes, color_hints, ppf, constraints)) { + return Codec::kEXR; + } + return Codec::kUnknown; + }; + + Codec codec = choose_codec(); + if (codec == Codec::kUnknown) { return JXL_FAILURE("Codecs failed to decode"); } if (orig_codec) *orig_codec = codec; diff --git a/lib/extras/dec/decode.h b/lib/extras/dec/decode.h index 7f0ff70..0f864dd 100644 --- a/lib/extras/dec/decode.h +++ b/lib/extras/dec/decode.h @@ -17,34 +17,40 @@ #include "lib/extras/dec/color_hints.h" #include "lib/jxl/base/span.h" #include "lib/jxl/base/status.h" -#include "lib/jxl/codec_in_out.h" namespace jxl { + +struct SizeConstraints; + namespace extras { -// Codecs supported by CodecInOut::Encode. +// Codecs supported by DecodeBytes. enum class Codec : uint32_t { - kUnknown, // for CodecFromExtension + kUnknown, // for CodecFromPath kPNG, kPNM, kPGX, kJPG, kGIF, - kEXR + kEXR, + kJXL }; -std::vector<Codec> AvailableCodecs(); +bool CanDecode(Codec codec); // If and only if extension is ".pfm", *bits_per_sample is updated to 32 so // that Encode() would encode to PFM instead of PPM. -Codec CodecFromExtension(std::string extension, - size_t* JXL_RESTRICT bits_per_sample = nullptr); +Codec CodecFromPath(std::string path, + size_t* JXL_RESTRICT bits_per_sample = nullptr, + std::string* filename = nullptr, + std::string* extension = nullptr); // Decodes "bytes" info *ppf. // color_space_hint may specify the color space, otherwise, defaults to sRGB. Status DecodeBytes(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, - extras::PackedPixelFile* ppf, Codec* orig_codec = nullptr); + extras::PackedPixelFile* ppf, + const SizeConstraints* constraints = nullptr, + Codec* orig_codec = nullptr); } // namespace extras } // namespace jxl diff --git a/lib/extras/dec/exr.cc b/lib/extras/dec/exr.cc index ddb6d53..821e0f4 100644 --- a/lib/extras/dec/exr.cc +++ b/lib/extras/dec/exr.cc @@ -5,20 +5,22 @@ #include "lib/extras/dec/exr.h" +#if JPEGXL_ENABLE_EXR #include <ImfChromaticitiesAttribute.h> #include <ImfIO.h> #include <ImfRgbaFile.h> #include <ImfStandardAttributes.h> +#endif #include <vector> namespace jxl { namespace extras { +#if JPEGXL_ENABLE_EXR namespace { namespace OpenEXR = OPENEXR_IMF_NAMESPACE; -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 @@ -60,10 +62,20 @@ class InMemoryIStream : public OpenEXR::IStream { }; } // namespace +#endif + +bool CanDecodeEXR() { +#if JPEGXL_ENABLE_EXR + return true; +#else + return false; +#endif +} Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, - PackedPixelFile* ppf) { + PackedPixelFile* ppf, + const SizeConstraints* constraints) { +#if JPEGXL_ENABLE_EXR InMemoryIStream is(bytes); #ifdef __EXCEPTIONS @@ -71,7 +83,8 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints, try { input_ptr.reset(new OpenEXR::RgbaInputFile(is)); } catch (...) { - return JXL_FAILURE("OpenEXR failed to parse input"); + // silently return false if it is not an EXR file + return false; } OpenEXR::RgbaInputFile& input = *input_ptr; #else @@ -87,7 +100,7 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints, const float intensity_target = OpenEXR::hasWhiteLuminance(input.header()) ? OpenEXR::whiteLuminance(input.header()) - : kDefaultIntensityTarget; + : 0; auto image_size = input.displayWindow().size(); // Size is computed as max - min, but both bounds are inclusive. @@ -144,6 +157,7 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints, std::min(input.dataWindow().max.x, input.displayWindow().max.x); ++exr_x) { const int image_x = exr_x - input.displayWindow().min.x; + // TODO(eustas): UB: OpenEXR::Rgba is not TriviallyCopyable memcpy(row + image_x * pixel_size, input_row + (exr_x - input.dataWindow().min.x), pixel_size); } @@ -178,6 +192,9 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints, } ppf->info.intensity_target = intensity_target; return true; +#else + return false; +#endif } } // namespace extras diff --git a/lib/extras/dec/exr.h b/lib/extras/dec/exr.h index 6af4e6b..0605cbb 100644 --- a/lib/extras/dec/exr.h +++ b/lib/extras/dec/exr.h @@ -11,17 +11,21 @@ #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 { + +struct SizeConstraints; + namespace extras { +bool CanDecodeEXR(); + // Decodes `bytes` into `ppf`. color_hints are ignored. Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, PackedPixelFile* ppf); + PackedPixelFile* ppf, + const SizeConstraints* constraints = nullptr); } // namespace extras } // namespace jxl diff --git a/lib/extras/dec/gif.cc b/lib/extras/dec/gif.cc index 5167bf5..3d96394 100644 --- a/lib/extras/dec/gif.cc +++ b/lib/extras/dec/gif.cc @@ -5,20 +5,24 @@ #include "lib/extras/dec/gif.h" +#if JPEGXL_ENABLE_GIF #include <gif_lib.h> +#endif +#include <jxl/codestream_header.h> #include <string.h> #include <memory> #include <utility> #include <vector> -#include "jxl/codestream_header.h" +#include "lib/extras/size_constraints.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/sanitizers.h" namespace jxl { namespace extras { +#if JPEGXL_ENABLE_GIF namespace { struct ReadState { @@ -38,21 +42,6 @@ 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{ @@ -67,12 +56,21 @@ void ensure_have_alpha(PackedFrame* frame) { std::fill_n(static_cast<uint8_t*>(frame->extra_channels[0].pixels()), frame->color.xsize * frame->color.ysize, 255u); } - } // namespace +#endif + +bool CanDecodeGIF() { +#if JPEGXL_ENABLE_GIF + return true; +#else + return false; +#endif +} Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, - PackedPixelFile* ppf) { + PackedPixelFile* ppf, + const SizeConstraints* constraints) { +#if JPEGXL_ENABLE_GIF int error = GIF_OK; ReadState state = {bytes}; const auto ReadFromSpan = [](GifFileType* const gif, GifByteType* const bytes, @@ -111,20 +109,20 @@ Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints, sizeof(*gif->SavedImages) * gif->ImageCount); JXL_RETURN_IF_ERROR( - VerifyDimensions<uint32_t>(&constraints, gif->SWidth, gif->SHeight)); + 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)); + 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) { + if (constraints && (total_pixel_count > constraints->dec_max_pixels)) { return JXL_FAILURE("Image too big"); } } @@ -408,6 +406,9 @@ Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints, } } return true; +#else + return false; +#endif } } // namespace extras diff --git a/lib/extras/dec/gif.h b/lib/extras/dec/gif.h index b359517..4d5be86 100644 --- a/lib/extras/dec/gif.h +++ b/lib/extras/dec/gif.h @@ -15,14 +15,19 @@ #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 { + +struct SizeConstraints; + namespace extras { +bool CanDecodeGIF(); + // Decodes `bytes` into `ppf`. color_hints are ignored. Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, PackedPixelFile* ppf); + PackedPixelFile* ppf, + const SizeConstraints* constraints = nullptr); } // namespace extras } // namespace jxl diff --git a/lib/extras/dec/jpegli.cc b/lib/extras/dec/jpegli.cc new file mode 100644 index 0000000..ffa1b79 --- /dev/null +++ b/lib/extras/dec/jpegli.cc @@ -0,0 +1,271 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/jpegli.h" + +#include <setjmp.h> +#include <stdint.h> + +#include <algorithm> +#include <numeric> +#include <utility> +#include <vector> + +#include "lib/jpegli/decode.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/sanitizers.h" + +namespace jxl { +namespace extras { + +namespace { + +constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, + 0x66, 0x00, 0x00}; +constexpr int kExifMarker = JPEG_APP0 + 1; +constexpr int kICCMarker = JPEG_APP0 + 2; + +static inline bool IsJPG(const std::vector<uint8_t>& bytes) { + if (bytes.size() < 2) return false; + if (bytes[0] != 0xFF || bytes[1] != 0xD8) return false; + return true; +} + +bool MarkerIsExif(const jpeg_saved_marker_ptr marker) { + return marker->marker == kExifMarker && + marker->data_length >= sizeof kExifSignature + 2 && + std::equal(std::begin(kExifSignature), std::end(kExifSignature), + marker->data); +} + +Status ReadICCProfile(jpeg_decompress_struct* const cinfo, + std::vector<uint8_t>* const icc) { + uint8_t* icc_data_ptr; + unsigned int icc_data_len; + if (jpegli_read_icc_profile(cinfo, &icc_data_ptr, &icc_data_len)) { + icc->assign(icc_data_ptr, icc_data_ptr + icc_data_len); + free(icc_data_ptr); + return true; + } + return false; +} + +void ReadExif(jpeg_decompress_struct* const cinfo, + std::vector<uint8_t>* const exif) { + constexpr size_t kExifSignatureSize = sizeof kExifSignature; + for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; + marker = marker->next) { + // marker is initialized by libjpeg, which we are not instrumenting with + // msan. + msan::UnpoisonMemory(marker, sizeof(*marker)); + msan::UnpoisonMemory(marker->data, marker->data_length); + if (!MarkerIsExif(marker)) continue; + size_t marker_length = marker->data_length - kExifSignatureSize; + exif->resize(marker_length); + std::copy_n(marker->data + kExifSignatureSize, marker_length, exif->data()); + return; + } +} + +JpegliDataType ConvertDataType(JxlDataType type) { + switch (type) { + case JXL_TYPE_UINT8: + return JPEGLI_TYPE_UINT8; + case JXL_TYPE_UINT16: + return JPEGLI_TYPE_UINT16; + case JXL_TYPE_FLOAT: + return JPEGLI_TYPE_FLOAT; + default: + return JPEGLI_TYPE_UINT8; + } +} + +JpegliEndianness ConvertEndianness(JxlEndianness type) { + switch (type) { + case JXL_NATIVE_ENDIAN: + return JPEGLI_NATIVE_ENDIAN; + case JXL_BIG_ENDIAN: + return JPEGLI_BIG_ENDIAN; + case JXL_LITTLE_ENDIAN: + return JPEGLI_LITTLE_ENDIAN; + default: + return JPEGLI_NATIVE_ENDIAN; + } +} + +JxlColorSpace ConvertColorSpace(J_COLOR_SPACE colorspace) { + switch (colorspace) { + case JCS_GRAYSCALE: + return JXL_COLOR_SPACE_GRAY; + case JCS_RGB: + return JXL_COLOR_SPACE_RGB; + default: + return JXL_COLOR_SPACE_UNKNOWN; + } +} + +void MyErrorExit(j_common_ptr cinfo) { + jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data); + (*cinfo->err->output_message)(cinfo); + jpegli_destroy_decompress(reinterpret_cast<j_decompress_ptr>(cinfo)); + longjmp(*env, 1); +} + +void MyOutputMessage(j_common_ptr cinfo) { +#if JXL_DEBUG_WARNING == 1 + char buf[JMSG_LENGTH_MAX + 1]; + (*cinfo->err->format_message)(cinfo, buf); + buf[JMSG_LENGTH_MAX] = 0; + JXL_WARNING("%s", buf); +#endif +} + +void UnmapColors(uint8_t* row, size_t xsize, int components, + JSAMPARRAY colormap, size_t num_colors) { + JXL_CHECK(colormap != nullptr); + std::vector<uint8_t> tmp(xsize * components); + for (size_t x = 0; x < xsize; ++x) { + JXL_CHECK(row[x] < num_colors); + for (int c = 0; c < components; ++c) { + tmp[x * components + c] = colormap[c][row[x]]; + } + } + memcpy(row, tmp.data(), tmp.size()); +} + +} // namespace + +Status DecodeJpeg(const std::vector<uint8_t>& compressed, + const JpegDecompressParams& dparams, ThreadPool* pool, + PackedPixelFile* ppf) { + // Don't do anything for non-JPEG files (no need to report an error) + if (!IsJPG(compressed)) return false; + + // TODO(veluca): use JPEGData also for pixels? + + // We need to declare all the non-trivial destructor local variables before + // the call to setjmp(). + std::unique_ptr<JSAMPLE[]> row; + + jpeg_decompress_struct cinfo; + const auto try_catch_block = [&]() -> bool { + // Setup error handling in jpeg library so we can deal with broken jpegs in + // the fuzzer. + jpeg_error_mgr jerr; + jmp_buf env; + cinfo.err = jpegli_std_error(&jerr); + jerr.error_exit = &MyErrorExit; + jerr.output_message = &MyOutputMessage; + if (setjmp(env)) { + return false; + } + cinfo.client_data = static_cast<void*>(&env); + + jpegli_create_decompress(&cinfo); + jpegli_mem_src(&cinfo, + reinterpret_cast<const unsigned char*>(compressed.data()), + compressed.size()); + jpegli_save_markers(&cinfo, kICCMarker, 0xFFFF); + jpegli_save_markers(&cinfo, kExifMarker, 0xFFFF); + const auto failure = [&cinfo](const char* str) -> Status { + jpegli_abort_decompress(&cinfo); + jpegli_destroy_decompress(&cinfo); + return JXL_FAILURE("%s", str); + }; + jpegli_read_header(&cinfo, TRUE); + // Might cause CPU-zip bomb. + if (cinfo.arith_code) { + return failure("arithmetic code JPEGs are not supported"); + } + int nbcomp = cinfo.num_components; + if (nbcomp != 1 && nbcomp != 3) { + return failure("unsupported number of components in JPEG"); + } + if (dparams.force_rgb) { + cinfo.out_color_space = JCS_RGB; + } else if (dparams.force_grayscale) { + cinfo.out_color_space = JCS_GRAYSCALE; + } + if (!ReadICCProfile(&cinfo, &ppf->icc)) { + ppf->icc.clear(); + // Default to SRGB + ppf->color_encoding.color_space = + ConvertColorSpace(cinfo.out_color_space); + ppf->color_encoding.white_point = JXL_WHITE_POINT_D65; + ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB; + ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB; + ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL; + } + ReadExif(&cinfo, &ppf->metadata.exif); + + ppf->info.xsize = cinfo.image_width; + ppf->info.ysize = cinfo.image_height; + if (dparams.output_data_type == JXL_TYPE_UINT8) { + ppf->info.bits_per_sample = 8; + ppf->info.exponent_bits_per_sample = 0; + } else if (dparams.output_data_type == JXL_TYPE_UINT16) { + ppf->info.bits_per_sample = 16; + ppf->info.exponent_bits_per_sample = 0; + } else if (dparams.output_data_type == JXL_TYPE_FLOAT) { + ppf->info.bits_per_sample = 32; + ppf->info.exponent_bits_per_sample = 8; + } else { + return failure("unsupported data type"); + } + ppf->info.uses_original_profile = true; + + // No alpha in JPG + ppf->info.alpha_bits = 0; + ppf->info.alpha_exponent_bits = 0; + ppf->info.orientation = JXL_ORIENT_IDENTITY; + + jpegli_set_output_format(&cinfo, ConvertDataType(dparams.output_data_type), + ConvertEndianness(dparams.output_endianness)); + + if (dparams.num_colors > 0) { + cinfo.quantize_colors = TRUE; + cinfo.desired_number_of_colors = dparams.num_colors; + cinfo.two_pass_quantize = dparams.two_pass_quant; + cinfo.dither_mode = (J_DITHER_MODE)dparams.dither_mode; + } + + jpegli_start_decompress(&cinfo); + + ppf->info.num_color_channels = cinfo.out_color_components; + const JxlPixelFormat format{ + /*num_channels=*/static_cast<uint32_t>(cinfo.out_color_components), + dparams.output_data_type, + dparams.output_endianness, + /*align=*/0, + }; + ppf->frames.clear(); + // Allocates the frame buffer. + ppf->frames.emplace_back(cinfo.image_width, cinfo.image_height, format); + const auto& frame = ppf->frames.back(); + JXL_ASSERT(sizeof(JSAMPLE) * cinfo.out_color_components * + cinfo.image_width <= + frame.color.stride); + + for (size_t y = 0; y < cinfo.image_height; ++y) { + JSAMPROW rows[] = {reinterpret_cast<JSAMPLE*>( + static_cast<uint8_t*>(frame.color.pixels()) + + frame.color.stride * y)}; + jpegli_read_scanlines(&cinfo, rows, 1); + if (dparams.num_colors > 0) { + UnmapColors(rows[0], cinfo.output_width, cinfo.out_color_components, + cinfo.colormap, cinfo.actual_number_of_colors); + } + } + + jpegli_finish_decompress(&cinfo); + return true; + }; + bool success = try_catch_block(); + jpegli_destroy_decompress(&cinfo); + return success; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/dec/jpegli.h b/lib/extras/dec/jpegli.h new file mode 100644 index 0000000..574df54 --- /dev/null +++ b/lib/extras/dec/jpegli.h @@ -0,0 +1,41 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_DEC_JPEGLI_H_ +#define LIB_EXTRAS_DEC_JPEGLI_H_ + +// Decodes JPG pixels and metadata in memory using the libjpegli library. + +#include <jxl/types.h> +#include <stdint.h> + +#include <vector> + +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace extras { + +struct JpegDecompressParams { + JxlDataType output_data_type = JXL_TYPE_UINT8; + JxlEndianness output_endianness = JXL_NATIVE_ENDIAN; + bool force_rgb = false; + bool force_grayscale = false; + int num_colors = 0; + bool two_pass_quant = true; + // 0 = none, 1 = ordered, 2 = Floyd-Steinberg + int dither_mode = 2; +}; + +Status DecodeJpeg(const std::vector<uint8_t>& compressed, + const JpegDecompressParams& dparams, ThreadPool* pool, + PackedPixelFile* ppf); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_JPEGLI_H_ diff --git a/lib/extras/dec/jpg.cc b/lib/extras/dec/jpg.cc index 6b92f4a..3c8a4bc 100644 --- a/lib/extras/dec/jpg.cc +++ b/lib/extras/dec/jpg.cc @@ -5,8 +5,10 @@ #include "lib/extras/dec/jpg.h" +#if JPEGXL_ENABLE_JPEG #include <jpeglib.h> #include <setjmp.h> +#endif #include <stdint.h> #include <algorithm> @@ -14,12 +16,14 @@ #include <utility> #include <vector> +#include "lib/extras/size_constraints.h" #include "lib/jxl/base/status.h" #include "lib/jxl/sanitizers.h" namespace jxl { namespace extras { +#if JPEGXL_ENABLE_JPEG namespace { constexpr unsigned char kICCSignature[12] = { @@ -160,12 +164,35 @@ void MyOutputMessage(j_common_ptr cinfo) { #endif } +void UnmapColors(uint8_t* row, size_t xsize, int components, + JSAMPARRAY colormap, size_t num_colors) { + JXL_CHECK(colormap != nullptr); + std::vector<uint8_t> tmp(xsize * components); + for (size_t x = 0; x < xsize; ++x) { + JXL_CHECK(row[x] < num_colors); + for (int c = 0; c < components; ++c) { + tmp[x * components + c] = colormap[c][row[x]]; + } + } + memcpy(row, tmp.data(), tmp.size()); +} + } // namespace +#endif + +bool CanDecodeJPG() { +#if JPEGXL_ENABLE_JPEG + return true; +#else + return false; +#endif +} Status DecodeImageJPG(const Span<const uint8_t> bytes, - const ColorHints& color_hints, - const SizeConstraints& constraints, - PackedPixelFile* ppf) { + const ColorHints& color_hints, PackedPixelFile* ppf, + const SizeConstraints* constraints, + const JPGDecompressParams* dparams) { +#if JPEGXL_ENABLE_JPEG // Don't do anything for non-JPEG files (no need to report an error) if (!IsJPG(bytes)) return false; @@ -176,10 +203,7 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes, 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)); + jpeg_decompress_struct cinfo = {}; // Setup error handling in jpeg library so we can deal with broken jpegs in // the fuzzer. jpeg_error_mgr jerr; @@ -207,8 +231,7 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes, if (read_header_result == JPEG_SUSPENDED) { return failure("truncated JPEG input"); } - if (!VerifyDimensions(&constraints, cinfo.image_width, - cinfo.image_height)) { + if (!VerifyDimensions(constraints, cinfo.image_width, cinfo.image_height)) { return failure("image too big"); } // Might cause CPU-zip bomb. @@ -252,12 +275,21 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes, ppf->info.num_color_channels = nbcomp; ppf->info.orientation = JXL_ORIENT_IDENTITY; + if (dparams && dparams->num_colors > 0) { + cinfo.quantize_colors = TRUE; + cinfo.desired_number_of_colors = dparams->num_colors; + cinfo.two_pass_quantize = dparams->two_pass_quant; + cinfo.dither_mode = (J_DITHER_MODE)dparams->dither_mode; + } + jpeg_start_decompress(&cinfo); - JXL_ASSERT(cinfo.output_components == nbcomp); + JXL_ASSERT(cinfo.out_color_components == nbcomp); + JxlDataType data_type = + ppf->info.bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16; const JxlPixelFormat format{ /*num_channels=*/static_cast<uint32_t>(nbcomp), - /*data_type=*/BITS_IN_JSAMPLE == 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, + data_type, /*endianness=*/JXL_NATIVE_ENDIAN, /*align=*/0, }; @@ -265,9 +297,19 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes, // 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 <= + JXL_ASSERT(sizeof(JSAMPLE) * cinfo.out_color_components * + cinfo.image_width <= frame.color.stride); + if (cinfo.quantize_colors) { + jxl::msan::UnpoisonMemory(cinfo.colormap, cinfo.out_color_components * + sizeof(cinfo.colormap[0])); + for (int c = 0; c < cinfo.out_color_components; ++c) { + jxl::msan::UnpoisonMemory( + cinfo.colormap[c], + cinfo.actual_number_of_colors * sizeof(cinfo.colormap[c][0])); + } + } for (size_t y = 0; y < cinfo.image_height; ++y) { JSAMPROW rows[] = {reinterpret_cast<JSAMPLE*>( static_cast<uint8_t*>(frame.color.pixels()) + @@ -275,6 +317,10 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes, jpeg_read_scanlines(&cinfo, rows, 1); msan::UnpoisonMemory(rows[0], sizeof(JSAMPLE) * cinfo.output_components * cinfo.image_width); + if (dparams && dparams->num_colors > 0) { + UnmapColors(rows[0], cinfo.output_width, cinfo.out_color_components, + cinfo.colormap, cinfo.actual_number_of_colors); + } } jpeg_finish_decompress(&cinfo); @@ -283,6 +329,9 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes, }; return try_catch_block(); +#else + return false; +#endif } } // namespace extras diff --git a/lib/extras/dec/jpg.h b/lib/extras/dec/jpg.h index 66b3452..6e7b2f7 100644 --- a/lib/extras/dec/jpg.h +++ b/lib/extras/dec/jpg.h @@ -13,19 +13,31 @@ #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 { + +struct SizeConstraints; + namespace extras { +bool CanDecodeJPG(); + +struct JPGDecompressParams { + int num_colors = 0; + bool two_pass_quant = false; + // 0 = none, 1 = ordered, 2 = Floyd-Steinberg + int dither_mode = 0; +}; + // Decodes `bytes` into `ppf`. color_hints are ignored. // `elapsed_deinterleave`, if non-null, will be set to the time (in seconds) // that it took to deinterleave the raw JSAMPLEs to planar floats. Status DecodeImageJPG(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, PackedPixelFile* ppf); + PackedPixelFile* ppf, + const SizeConstraints* constraints = nullptr, + const JPGDecompressParams* dparams = nullptr); } // namespace extras } // namespace jxl diff --git a/lib/extras/dec/jxl.cc b/lib/extras/dec/jxl.cc index 0e10356..1f3a3ff 100644 --- a/lib/extras/dec/jxl.cc +++ b/lib/extras/dec/jxl.cc @@ -5,12 +5,15 @@ #include "lib/extras/dec/jxl.h" -#include "jxl/decode.h" -#include "jxl/decode_cxx.h" -#include "jxl/types.h" +#include <jxl/cms.h> +#include <jxl/decode.h> +#include <jxl/decode_cxx.h> +#include <jxl/types.h> + +#include "lib/extras/common.h" #include "lib/extras/dec/color_description.h" -#include "lib/extras/enc/encode.h" #include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/exif.h" namespace jxl { namespace extras { @@ -68,11 +71,48 @@ struct BoxProcessor { } }; +void SetBitDepthFromDataType(JxlDataType data_type, uint32_t* bits_per_sample, + uint32_t* exponent_bits_per_sample) { + switch (data_type) { + case JXL_TYPE_UINT8: + *bits_per_sample = 8; + *exponent_bits_per_sample = 0; + break; + case JXL_TYPE_UINT16: + *bits_per_sample = 16; + *exponent_bits_per_sample = 0; + break; + case JXL_TYPE_FLOAT16: + *bits_per_sample = 16; + *exponent_bits_per_sample = 5; + break; + case JXL_TYPE_FLOAT: + *bits_per_sample = 32; + *exponent_bits_per_sample = 8; + break; + } +} + +template <typename T> +void UpdateBitDepth(JxlBitDepth bit_depth, JxlDataType data_type, T* info) { + if (bit_depth.type == JXL_BIT_DEPTH_FROM_PIXEL_FORMAT) { + SetBitDepthFromDataType(data_type, &info->bits_per_sample, + &info->exponent_bits_per_sample); + } else if (bit_depth.type == JXL_BIT_DEPTH_CUSTOM) { + info->bits_per_sample = bit_depth.bits_per_sample; + info->exponent_bits_per_sample = bit_depth.exponent_bits_per_sample; + } +} + } // namespace bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, const JXLDecompressParams& dparams, size_t* decoded_bytes, PackedPixelFile* ppf, std::vector<uint8_t>* jpeg_bytes) { + JxlSignature sig = JxlSignatureCheck(bytes, bytes_size); + // silently return false if this is not a JXL file + if (sig == JXL_SIG_INVALID) return false; + auto decoder = JxlDecoderMake(/*memory_manager=*/nullptr); JxlDecoder* dec = decoder.get(); ppf->frames.clear(); @@ -86,12 +126,7 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, JxlPixelFormat format; std::vector<JxlPixelFormat> accepted_formats = dparams.accepted_formats; - if (accepted_formats.empty()) { - for (const uint32_t num_channels : {1, 2, 3, 4}) { - accepted_formats.push_back( - {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0}); - } - } + JxlColorEncoding color_encoding; size_t num_color_channels = 0; if (!dparams.color_space.empty()) { @@ -107,7 +142,9 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, bool can_reconstruct_jpeg = false; std::vector<uint8_t> jpeg_data_chunk; if (jpeg_bytes != nullptr) { - jpeg_data_chunk.resize(16384); + // This bound is very likely to be enough to hold the entire + // reconstructed JPEG, to avoid having to do expensive retries. + jpeg_data_chunk.resize(bytes_size * 3 / 2 + 1024); jpeg_bytes->resize(0); } @@ -128,6 +165,10 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, } else { events |= (JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_PREVIEW_IMAGE | JXL_DEC_BOX); + if (accepted_formats.empty()) { + // decoding just the metadata, not the pixel data + events ^= (JXL_DEC_FULL_IMAGE | JXL_DEC_PREVIEW_IMAGE); + } } if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec, events)) { fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); @@ -165,7 +206,7 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, return false; } uint32_t progression_index = 0; - bool codestream_done = false; + bool codestream_done = accepted_formats.empty(); BoxProcessor boxes(dec); for (;;) { JxlDecoderStatus status = JxlDecoderProcessInput(dec); @@ -185,8 +226,12 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, } break; } + size_t released_size = JxlDecoderReleaseInput(dec); fprintf(stderr, - "Input file is truncated and allow_partial_input was disabled."); + "Input file is truncated (total bytes: %" PRIuS + ", processed bytes: %" PRIuS + ") and --allow_partial_files is not present.\n", + bytes_size, bytes_size - released_size); return false; } else if (status == JXL_DEC_BOX) { boxes.FinalizeOutput(); @@ -240,11 +285,16 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, fprintf(stderr, "JxlDecoderGetBasicInfo failed\n"); return false; } + if (accepted_formats.empty()) continue; if (num_color_channels != 0) { // Mark the change in number of color channels due to the requested // color space. ppf->info.num_color_channels = num_color_channels; } + if (dparams.output_bitdepth.type == JXL_BIT_DEPTH_CUSTOM) { + // Select format based on custom bits per sample. + ppf->info.bits_per_sample = dparams.output_bitdepth.bits_per_sample; + } // Select format according to accepted formats. if (!jxl::extras::SelectFormat(accepted_formats, ppf->info, &format)) { fprintf(stderr, "SelectFormat failed\n"); @@ -254,9 +304,11 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, if (!have_alpha) { // Mark in the basic info that alpha channel was dropped. ppf->info.alpha_bits = 0; - } else if (dparams.unpremultiply_alpha) { - // Mark in the basic info that alpha was unpremultiplied. - ppf->info.alpha_premultiplied = false; + } else { + if (dparams.unpremultiply_alpha) { + // Mark in the basic info that alpha was unpremultiplied. + ppf->info.alpha_premultiplied = false; + } } bool alpha_found = false; for (uint32_t i = 0; i < ppf->info.num_extra_channels; ++i) { @@ -287,6 +339,7 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, "Warning: --color_space ignored because the image is " "not XYB encoded.\n"); } else { + JxlDecoderSetCms(dec, *JxlGetDefaultCms()); if (JXL_DEC_SUCCESS != JxlDecoderSetPreferredColorProfile(dec, &color_encoding)) { fprintf(stderr, "Failed to set color space.\n"); @@ -296,34 +349,35 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, } size_t icc_size = 0; JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_DATA; - if (JXL_DEC_SUCCESS != - JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) { - fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); - } - if (icc_size != 0) { - ppf->icc.resize(icc_size); + ppf->color_encoding.color_space = JXL_COLOR_SPACE_UNKNOWN; + if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsEncodedProfile( + dec, target, &ppf->color_encoding) || + dparams.need_icc) { + // only get ICC if it is not an Enum color encoding if (JXL_DEC_SUCCESS != - JxlDecoderGetColorAsICCProfile(dec, nullptr, target, - ppf->icc.data(), icc_size)) { - fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); - return false; + JxlDecoderGetICCProfileSize(dec, target, &icc_size)) { + fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); + } + if (icc_size != 0) { + ppf->icc.resize(icc_size); + if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile( + dec, target, ppf->icc.data(), icc_size)) { + fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); + return false; + } } - } - if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsEncodedProfile( - dec, nullptr, target, &ppf->color_encoding)) { - ppf->color_encoding.color_space = JXL_COLOR_SPACE_UNKNOWN; } icc_size = 0; target = JXL_COLOR_PROFILE_TARGET_ORIGINAL; if (JXL_DEC_SUCCESS != - JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) { + JxlDecoderGetICCProfileSize(dec, target, &icc_size)) { fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); } if (icc_size != 0) { ppf->orig_icc.resize(icc_size); if (JXL_DEC_SUCCESS != - JxlDecoderGetColorAsICCProfile(dec, nullptr, target, - ppf->orig_icc.data(), icc_size)) { + JxlDecoderGetColorAsICCProfile(dec, target, ppf->orig_icc.data(), + icc_size)) { fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); return false; } @@ -421,9 +475,21 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, return false; } } + if (JXL_DEC_SUCCESS != + JxlDecoderSetImageOutBitDepth(dec, &dparams.output_bitdepth)) { + fprintf(stderr, "JxlDecoderSetImageOutBitDepth failed\n"); + return false; + } + UpdateBitDepth(dparams.output_bitdepth, format.data_type, &ppf->info); + bool have_alpha = (format.num_channels == 2 || format.num_channels == 4); + if (have_alpha) { + // Interleaved alpha channels has the same bit depth as color channels. + ppf->info.alpha_bits = ppf->info.bits_per_sample; + ppf->info.alpha_exponent_bits = ppf->info.exponent_bits_per_sample; + } JxlPixelFormat ec_format = format; ec_format.num_channels = 1; - for (const auto& eci : ppf->extra_channels_info) { + for (auto& eci : ppf->extra_channels_info) { frame.extra_channels.emplace_back(jxl::extras::PackedImage( ppf->info.xsize, ppf->info.ysize, ec_format)); auto& ec = frame.extra_channels.back(); @@ -446,6 +512,8 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, fprintf(stderr, "JxlDecoderSetExtraChannelBuffer failed\n"); return false; } + UpdateBitDepth(dparams.output_bitdepth, ec_format.data_type, + &eci.ec_info); } } else if (status == JXL_DEC_SUCCESS) { // Decoding finished successfully. @@ -463,6 +531,28 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, } } boxes.FinalizeOutput(); + if (!ppf->metadata.exif.empty()) { + // Verify that Exif box has a valid TIFF header at the specified offset. + // Discard bytes preceding the header. + if (ppf->metadata.exif.size() >= 4) { + uint32_t offset = LoadBE32(ppf->metadata.exif.data()); + if (offset <= ppf->metadata.exif.size() - 8) { + std::vector<uint8_t> exif(ppf->metadata.exif.begin() + 4 + offset, + ppf->metadata.exif.end()); + bool bigendian; + if (IsExif(exif, &bigendian)) { + ppf->metadata.exif = std::move(exif); + } else { + fprintf(stderr, "Warning: invalid TIFF header in Exif\n"); + } + } else { + fprintf(stderr, "Warning: invalid Exif offset: %" PRIu32 "\n", offset); + } + } else { + fprintf(stderr, "Warning: invalid Exif length: %" PRIuS "\n", + ppf->metadata.exif.size()); + } + } if (jpeg_bytes != nullptr) { if (!can_reconstruct_jpeg) return false; size_t used_jpeg_output = diff --git a/lib/extras/dec/jxl.h b/lib/extras/dec/jxl.h index c462fa4..cbada1f 100644 --- a/lib/extras/dec/jxl.h +++ b/lib/extras/dec/jxl.h @@ -8,14 +8,14 @@ // Decodes JPEG XL images in memory. +#include <jxl/parallel_runner.h> +#include <jxl/types.h> #include <stdint.h> #include <limits> #include <string> #include <vector> -#include "jxl/parallel_runner.h" -#include "jxl/types.h" #include "lib/extras/packed_image.h" namespace jxl { @@ -41,6 +41,10 @@ struct JXLDecompressParams { // Whether truncated input should be treated as an error. bool allow_partial_input = false; + // Set to true if an ICC profile has to be synthesized for Enum color + // encodings + bool need_icc = false; + // How many passes to decode at most. By default, decode everything. uint32_t max_passes = std::numeric_limits<uint32_t>::max(); @@ -53,6 +57,9 @@ struct JXLDecompressParams { bool use_image_callback = true; // Whether to unpremultiply colors for associated alpha channels. bool unpremultiply_alpha = false; + + // Controls the effective bit depth of the output pixels. + JxlBitDepth output_bitdepth = {JXL_BIT_DEPTH_FROM_PIXEL_FORMAT, 0, 0}; }; bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, diff --git a/lib/extras/dec/pgx.cc b/lib/extras/dec/pgx.cc index 1417348..a99eb0f 100644 --- a/lib/extras/dec/pgx.cc +++ b/lib/extras/dec/pgx.cc @@ -7,6 +7,7 @@ #include <string.h> +#include "lib/extras/size_constraints.h" #include "lib/jxl/base/bits.h" #include "lib/jxl/base/compiler_specific.h" @@ -145,15 +146,14 @@ class Parser { } // namespace Status DecodeImagePGX(const Span<const uint8_t> bytes, - const ColorHints& color_hints, - const SizeConstraints& constraints, - PackedPixelFile* ppf) { + const ColorHints& color_hints, PackedPixelFile* ppf, + const SizeConstraints* constraints) { Parser parser(bytes); HeaderPGX header = {}; const uint8_t* pos; if (!parser.ParseHeader(&header, &pos)) return false; JXL_RETURN_IF_ERROR( - VerifyDimensions(&constraints, header.xsize, header.ysize)); + 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"); } diff --git a/lib/extras/dec/pgx.h b/lib/extras/dec/pgx.h index 38aedf5..ce852e6 100644 --- a/lib/extras/dec/pgx.h +++ b/lib/extras/dec/pgx.h @@ -14,17 +14,19 @@ #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 { + +struct SizeConstraints; + namespace extras { // Decodes `bytes` into `ppf`. Status DecodeImagePGX(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, PackedPixelFile* ppf); + PackedPixelFile* ppf, + const SizeConstraints* constraints = nullptr); } // namespace extras } // namespace jxl diff --git a/lib/extras/dec/pgx_test.cc b/lib/extras/dec/pgx_test.cc index 41e6bf8..5dbc314 100644 --- a/lib/extras/dec/pgx_test.cc +++ b/lib/extras/dec/pgx_test.cc @@ -5,16 +5,18 @@ #include "lib/extras/dec/pgx.h" -#include "gtest/gtest.h" +#include <cstring> + #include "lib/extras/packed_image_convert.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/testing.h" namespace jxl { namespace extras { namespace { Span<const uint8_t> MakeSpan(const char* str) { - return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), - strlen(str)); + return Bytes(reinterpret_cast<const uint8_t*>(str), strlen(str)); } TEST(CodecPGXTest, Test8bits) { @@ -23,8 +25,7 @@ TEST(CodecPGXTest, Test8bits) { PackedPixelFile ppf; ThreadPool* pool = nullptr; - EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), - SizeConstraints(), &ppf)); + EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), &ppf)); CodecInOut io; EXPECT_TRUE(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io)); @@ -51,8 +52,7 @@ TEST(CodecPGXTest, Test16bits) { PackedPixelFile ppf; ThreadPool* pool = nullptr; - EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), - SizeConstraints(), &ppf)); + EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), &ppf)); CodecInOut io; EXPECT_TRUE(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io)); diff --git a/lib/extras/dec/pnm.cc b/lib/extras/dec/pnm.cc index 03aecef..c576385 100644 --- a/lib/extras/dec/pnm.cc +++ b/lib/extras/dec/pnm.cc @@ -8,6 +8,9 @@ #include <stdlib.h> #include <string.h> +#include <cmath> + +#include "lib/extras/size_constraints.h" #include "lib/jxl/base/bits.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/status.h" @@ -16,16 +19,6 @@ 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) @@ -183,16 +176,20 @@ class Parser { Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) { size_t depth = 3; size_t max_val = 255; + JXL_RETURN_IF_ERROR(SkipWhitespace()); while (!MatchString("ENDHDR", /*skipws=*/false)) { - JXL_RETURN_IF_ERROR(SkipWhitespace()); if (MatchString("WIDTH")) { JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); + JXL_RETURN_IF_ERROR(SkipWhitespace()); } else if (MatchString("HEIGHT")) { JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); + JXL_RETURN_IF_ERROR(SkipWhitespace()); } else if (MatchString("DEPTH")) { JXL_RETURN_IF_ERROR(ParseUnsigned(&depth)); + JXL_RETURN_IF_ERROR(SkipWhitespace()); } else if (MatchString("MAXVAL")) { JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val)); + JXL_RETURN_IF_ERROR(SkipWhitespace()); } else if (MatchString("TUPLTYPE")) { if (MatchString("RGB_ALPHA")) { header->has_alpha = true; @@ -209,6 +206,20 @@ class Parser { } else if (MatchString("BLACKANDWHITE")) { header->is_gray = true; max_val = 1; + } else if (MatchString("Alpha")) { + header->ec_types.push_back(JXL_CHANNEL_ALPHA); + } else if (MatchString("Depth")) { + header->ec_types.push_back(JXL_CHANNEL_DEPTH); + } else if (MatchString("SpotColor")) { + header->ec_types.push_back(JXL_CHANNEL_SPOT_COLOR); + } else if (MatchString("SelectionMask")) { + header->ec_types.push_back(JXL_CHANNEL_SELECTION_MASK); + } else if (MatchString("Black")) { + header->ec_types.push_back(JXL_CHANNEL_BLACK); + } else if (MatchString("CFA")) { + header->ec_types.push_back(JXL_CHANNEL_CFA); + } else if (MatchString("Thermal")) { + header->ec_types.push_back(JXL_CHANNEL_THERMAL); } else { return JXL_FAILURE("PAM: unknown TUPLTYPE"); } @@ -223,13 +234,13 @@ class Parser { } size_t num_channels = header->is_gray ? 1 : 3; if (header->has_alpha) num_channels++; - if (num_channels != depth) { + if (num_channels + header->ec_types.size() != depth) { return JXL_FAILURE("PAM: bad DEPTH"); } if (max_val == 0 || max_val >= 65536) { return JXL_FAILURE("PAM: bad MAXVAL"); } - // e.g When `max_val` is 1 , we want 1 bit: + // 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)"); @@ -298,30 +309,98 @@ class Parser { }; Span<const uint8_t> MakeSpan(const char* str) { - return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), - strlen(str)); + return Bytes(reinterpret_cast<const uint8_t*>(str), strlen(str)); +} + +void ReadLinePNM(void* opaque, size_t xpos, size_t ypos, size_t xsize, + uint8_t* buffer, size_t len) { + ChunkedPNMDecoder* dec = reinterpret_cast<ChunkedPNMDecoder*>(opaque); + const size_t bytes_per_channel = + DivCeil(dec->header.bits_per_sample, jxl::kBitsPerByte); + const size_t pixel_offset = ypos * dec->header.xsize + xpos; + const size_t num_channels = dec->header.is_gray ? 1 : 3; + const size_t offset = pixel_offset * num_channels * bytes_per_channel; + const size_t num_bytes = xsize * num_channels * bytes_per_channel; + if (fseek(dec->f, dec->data_start + offset, SEEK_SET) != 0) { + return; + } + JXL_ASSERT(num_bytes == len); + if (num_bytes != fread(buffer, 1, num_bytes, dec->f)) { + JXL_WARNING("Failed to read from PNM file\n"); + } } } // namespace -Status DecodeImagePNM(const Span<const uint8_t> bytes, - const ColorHints& color_hints, - const SizeConstraints& constraints, +Status DecodeImagePNM(ChunkedPNMDecoder* dec, const ColorHints& color_hints, PackedPixelFile* ppf) { + std::vector<uint8_t> buffer(10 * 1024); + const size_t bytes_read = fread(buffer.data(), 1, buffer.size(), dec->f); + if (ferror(dec->f) || bytes_read > buffer.size()) { + return false; + } + Span<const uint8_t> span(buffer); + Parser parser(span); + HeaderPNM& header = dec->header; + const uint8_t* pos = nullptr; + if (!parser.ParseHeader(&header, &pos)) { + return false; + } + dec->data_start = pos - &buffer[0]; + + if (header.bits_per_sample == 0 || header.bits_per_sample > 16) { + return JXL_FAILURE("Invalid bits_per_sample"); + } + if (header.has_alpha || !header.ec_types.empty() || header.floating_point) { + return JXL_FAILURE("Only PGM and PPM inputs are supported"); + } + + // PPM specifies that in the raster, the sample values are "nonlinear" + // (BP.709, with gamma number of 2.2). Deviate from the specification and + // assume `sRGB` in our implementation. + JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, + header.is_gray, ppf)); + + ppf->info.xsize = header.xsize; + ppf->info.ysize = header.ysize; + ppf->info.bits_per_sample = header.bits_per_sample; + ppf->info.exponent_bits_per_sample = 0; + ppf->info.orientation = JXL_ORIENT_IDENTITY; + ppf->info.alpha_bits = 0; + ppf->info.alpha_exponent_bits = 0; + ppf->info.num_color_channels = (header.is_gray ? 1 : 3); + ppf->info.num_extra_channels = 0; + + const JxlDataType data_type = + header.bits_per_sample > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8; + const JxlPixelFormat format{ + /*num_channels=*/ppf->info.num_color_channels, + /*data_type=*/data_type, + /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, + /*align=*/0, + }; + ppf->chunked_frames.emplace_back(header.xsize, header.ysize, format, dec, + ReadLinePNM); + return true; +} + +Status DecodeImagePNM(const Span<const uint8_t> bytes, + const ColorHints& color_hints, PackedPixelFile* ppf, + const SizeConstraints* constraints) { Parser parser(bytes); HeaderPNM header = {}; const uint8_t* pos = nullptr; if (!parser.ParseHeader(&header, &pos)) return false; JXL_RETURN_IF_ERROR( - VerifyDimensions(&constraints, header.xsize, header.ysize)); + 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. + // PPM specifies that in the raster, the sample values are "nonlinear" + // (BP.709, with gamma number of 2.2). Deviate from the specification and + // assume `sRGB` in our implementation. JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, header.is_gray, ppf)); @@ -341,7 +420,17 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes, 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); + uint32_t num_alpha_channels = (header.has_alpha ? 1 : 0); + uint32_t num_interleaved_channels = + ppf->info.num_color_channels + num_alpha_channels; + ppf->info.num_extra_channels = num_alpha_channels + header.ec_types.size(); + + for (auto type : header.ec_types) { + PackedExtraChannel pec; + pec.ec_info.bits_per_sample = ppf->info.bits_per_sample; + pec.ec_info.type = type; + ppf->extra_channels_info.emplace_back(std::move(pec)); + } JxlDataType data_type; if (header.floating_point) { @@ -356,27 +445,50 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes, } const JxlPixelFormat format{ - /*num_channels=*/ppf->info.num_color_channels + - ppf->info.num_extra_channels, + /*num_channels=*/num_interleaved_channels, /*data_type=*/data_type, /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, /*align=*/0, }; + const JxlPixelFormat ec_format{1, format.data_type, format.endianness, 0}; ppf->frames.clear(); ppf->frames.emplace_back(header.xsize, header.ysize, format); auto* frame = &ppf->frames.back(); - + for (size_t i = 0; i < header.ec_types.size(); ++i) { + frame->extra_channels.emplace_back(header.xsize, header.ysize, ec_format); + } size_t pnm_remaining_size = bytes.data() + bytes.size() - pos; if (pnm_remaining_size < frame->color.pixels_size) { return JXL_FAILURE("PNM file too small"); } - const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped + uint8_t* out = reinterpret_cast<uint8_t*>(frame->color.pixels()); - for (size_t y = 0; y < header.ysize; ++y) { - size_t y_in = flipped_y ? header.ysize - 1 - y : y; - const uint8_t* row_in = &pos[y_in * frame->color.stride]; - uint8_t* row_out = &out[y * frame->color.stride]; - memcpy(row_out, row_in, frame->color.stride); + std::vector<uint8_t*> ec_out(header.ec_types.size()); + for (size_t i = 0; i < ec_out.size(); ++i) { + ec_out[i] = reinterpret_cast<uint8_t*>(frame->extra_channels[i].pixels()); + } + if (ec_out.empty()) { + const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped + for (size_t y = 0; y < header.ysize; ++y) { + size_t y_in = flipped_y ? header.ysize - 1 - y : y; + const uint8_t* row_in = &pos[y_in * frame->color.stride]; + uint8_t* row_out = &out[y * frame->color.stride]; + memcpy(row_out, row_in, frame->color.stride); + } + } else { + size_t pwidth = PackedImage::BitsPerChannel(data_type) / 8; + for (size_t y = 0; y < header.ysize; ++y) { + for (size_t x = 0; x < header.xsize; ++x) { + memcpy(out, pos, frame->color.pixel_stride()); + out += frame->color.pixel_stride(); + pos += frame->color.pixel_stride(); + for (auto& p : ec_out) { + memcpy(p, pos, pwidth); + pos += pwidth; + p += pwidth; + } + } + } } return true; } diff --git a/lib/extras/dec/pnm.h b/lib/extras/dec/pnm.h index f637483..9b68e56 100644 --- a/lib/extras/dec/pnm.h +++ b/lib/extras/dec/pnm.h @@ -17,21 +17,43 @@ #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 { + +struct SizeConstraints; + namespace extras { // Decodes `bytes` into `ppf`. color_hints may specify "color_space", which // defaults to sRGB. Status DecodeImagePNM(Span<const uint8_t> bytes, const ColorHints& color_hints, - const SizeConstraints& constraints, PackedPixelFile* ppf); + PackedPixelFile* ppf, + const SizeConstraints* constraints = nullptr); void TestCodecPNM(); +struct HeaderPNM { + size_t xsize; + size_t ysize; + bool is_gray; // PGM + bool has_alpha; // PAM + size_t bits_per_sample; + bool floating_point; + bool big_endian; + std::vector<JxlExtraChannelType> ec_types; // PAM +}; + +struct ChunkedPNMDecoder { + FILE* f; + HeaderPNM header = {}; + size_t data_start; +}; + +Status DecodeImagePNM(ChunkedPNMDecoder* dec, const ColorHints& color_hints, + PackedPixelFile* ppf); + } // namespace extras } // namespace jxl |