diff options
author | Jiyong <jiyong.min@samsung.com> | 2023-12-26 17:33:03 +0900 |
---|---|---|
committer | Jiyong <jiyong.min@samsung.com> | 2023-12-27 08:25:11 +0900 |
commit | a6d06c38e46e552195648836052eb909925fe5ff (patch) | |
tree | 5b34f3947c8331dc618a5166974e4d9757f8e782 /lib/extras | |
parent | f3e519be675ef7922a6c1c3a682232302b55496d (diff) | |
parent | 3b773d382e34fcfc7c8995d8bd681a6ef0529b02 (diff) | |
download | libjxl-accepted/tizen_unified_riscv.tar.gz libjxl-accepted/tizen_unified_riscv.tar.bz2 libjxl-accepted/tizen_unified_riscv.zip |
Merge branch 'upstream' into tizenaccepted/tizen/unified/riscv/20240103.054630accepted/tizen/unified/20231228.165749accepted/tizen_unified_riscv
Change-Id: I13b4d2c94ada4853484630800e2a8a5ae90d34c1
Diffstat (limited to 'lib/extras')
58 files changed, 4326 insertions, 1026 deletions
diff --git a/lib/extras/alpha_blend.cc b/lib/extras/alpha_blend.cc new file mode 100644 index 0000000..50c141c --- /dev/null +++ b/lib/extras/alpha_blend.cc @@ -0,0 +1,63 @@ +// 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/alpha_blend.h" + +#include "lib/extras/packed_image.h" + +namespace jxl { +namespace extras { + +namespace { + +void AlphaBlend(PackedFrame* frame, float background[3]) { + if (!frame) return; + const PackedImage& im = frame->color; + JxlPixelFormat format = im.format; + if (format.num_channels != 2 && format.num_channels != 4) { + return; + } + --format.num_channels; + PackedImage blended(im.xsize, im.ysize, format); + // TODO(szabadka) SIMDify this and make it work for float16. + for (size_t y = 0; y < im.ysize; ++y) { + for (size_t x = 0; x < im.xsize; ++x) { + if (format.num_channels == 2) { + float g = im.GetPixelValue(y, x, 0); + float a = im.GetPixelValue(y, x, 1); + float out = g * a + background[0] * (1 - a); + blended.SetPixelValue(y, x, 0, out); + } else { + float r = im.GetPixelValue(y, x, 0); + float g = im.GetPixelValue(y, x, 1); + float b = im.GetPixelValue(y, x, 2); + float a = im.GetPixelValue(y, x, 3); + float out_r = r * a + background[0] * (1 - a); + float out_g = g * a + background[1] * (1 - a); + float out_b = b * a + background[2] * (1 - a); + blended.SetPixelValue(y, x, 0, out_r); + blended.SetPixelValue(y, x, 1, out_g); + blended.SetPixelValue(y, x, 2, out_b); + } + } + } + frame->color = blended.Copy(); +} + +} // namespace + +void AlphaBlend(PackedPixelFile* ppf, float background[3]) { + if (!ppf || ppf->info.alpha_bits == 0) { + return; + } + ppf->info.alpha_bits = 0; + AlphaBlend(ppf->preview_frame.get(), background); + for (auto& frame : ppf->frames) { + AlphaBlend(&frame, background); + } +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/alpha_blend.h b/lib/extras/alpha_blend.h new file mode 100644 index 0000000..4d78e86 --- /dev/null +++ b/lib/extras/alpha_blend.h @@ -0,0 +1,19 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_ALPHA_BLEND_H_ +#define LIB_EXTRAS_ALPHA_BLEND_H_ + +#include "lib/extras/packed_image.h" + +namespace jxl { +namespace extras { + +void AlphaBlend(PackedPixelFile* ppf, float background[3]); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ALPHA_BLEND_H_ diff --git a/lib/extras/codec.cc b/lib/extras/codec.cc index 774b4cc..3ba31f2 100644 --- a/lib/extras/codec.cc +++ b/lib/extras/codec.cc @@ -5,27 +5,18 @@ #include "lib/extras/codec.h" -#include "jxl/decode.h" -#include "jxl/types.h" -#include "lib/extras/packed_image.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/status.h" +#include <jxl/decode.h> +#include <jxl/types.h> -#if JPEGXL_ENABLE_APNG +#include "lib/extras/dec/decode.h" #include "lib/extras/enc/apng.h" -#endif -#if JPEGXL_ENABLE_JPEG -#include "lib/extras/enc/jpg.h" -#endif -#if JPEGXL_ENABLE_EXR #include "lib/extras/enc/exr.h" -#endif - -#include "lib/extras/dec/decode.h" +#include "lib/extras/enc/jpg.h" #include "lib/extras/enc/pgx.h" #include "lib/extras/enc/pnm.h" +#include "lib/extras/packed_image.h" #include "lib/extras/packed_image_convert.h" -#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/status.h" #include "lib/jxl/image_bundle.h" namespace jxl { @@ -38,30 +29,21 @@ constexpr size_t kMinBytes = 9; Status SetFromBytes(const Span<const uint8_t> bytes, const extras::ColorHints& color_hints, CodecInOut* io, - ThreadPool* pool, extras::Codec* orig_codec) { + ThreadPool* pool, const SizeConstraints* constraints, + extras::Codec* orig_codec) { if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes"); extras::PackedPixelFile ppf; - if (extras::DecodeBytes(bytes, color_hints, io->constraints, &ppf, - orig_codec)) { + if (extras::DecodeBytes(bytes, color_hints, &ppf, constraints, orig_codec)) { return ConvertPackedPixelFileToCodecInOut(ppf, pool, io); } return JXL_FAILURE("Codecs failed to decode"); } -Status SetFromFile(const std::string& pathname, - const extras::ColorHints& color_hints, CodecInOut* io, - ThreadPool* pool, extras::Codec* orig_codec) { - std::vector<uint8_t> encoded; - JXL_RETURN_IF_ERROR(ReadFile(pathname, &encoded)); - JXL_RETURN_IF_ERROR(SetFromBytes(Span<const uint8_t>(encoded), color_hints, - io, pool, orig_codec)); - return true; -} - Status Encode(const CodecInOut& io, const extras::Codec codec, const ColorEncoding& c_desired, size_t bits_per_sample, std::vector<uint8_t>* bytes, ThreadPool* pool) { + bytes->clear(); JXL_CHECK(!io.Main().c_current().ICC().empty()); JXL_CHECK(!c_desired.ICC().empty()); io.CheckMetadata(); @@ -77,22 +59,22 @@ Status Encode(const CodecInOut& io, const extras::Codec codec, std::ostringstream os; switch (codec) { case extras::Codec::kPNG: -#if JPEGXL_ENABLE_APNG encoder = extras::GetAPNGEncoder(); - break; -#else - return JXL_FAILURE("JPEG XL was built without (A)PNG support"); -#endif + if (encoder) { + break; + } else { + return JXL_FAILURE("JPEG XL was built without (A)PNG support"); + } case extras::Codec::kJPG: -#if JPEGXL_ENABLE_JPEG format.data_type = JXL_TYPE_UINT8; encoder = extras::GetJPEGEncoder(); - os << io.jpeg_quality; - encoder->SetOption("q", os.str()); - break; -#else - return JXL_FAILURE("JPEG XL was built without JPEG support"); -#endif + if (encoder) { + os << io.jpeg_quality; + encoder->SetOption("q", os.str()); + break; + } else { + return JXL_FAILURE("JPEG XL was built without JPEG support"); + } case extras::Codec::kPNM: if (io.Main().HasAlpha()) { encoder = extras::GetPAMEncoder(); @@ -102,14 +84,9 @@ Status Encode(const CodecInOut& io, const extras::Codec codec, encoder = extras::GetPPMEncoder(); } else { format.data_type = JXL_TYPE_FLOAT; - format.endianness = JXL_NATIVE_ENDIAN; + format.endianness = JXL_LITTLE_ENDIAN; encoder = extras::GetPFMEncoder(); } - if (!c_desired.IsSRGB()) { - JXL_WARNING( - "PNM encoder cannot store custom ICC profile; decoder " - "will need hint key=color_space to get the same values"); - } break; case extras::Codec::kPGX: encoder = extras::GetPGXEncoder(); @@ -117,13 +94,17 @@ Status Encode(const CodecInOut& io, const extras::Codec codec, case extras::Codec::kGIF: return JXL_FAILURE("Encoding to GIF is not implemented"); case extras::Codec::kEXR: -#if JPEGXL_ENABLE_EXR format.data_type = JXL_TYPE_FLOAT; encoder = extras::GetEXREncoder(); - break; -#else - return JXL_FAILURE("JPEG XL was built without OpenEXR support"); -#endif + if (encoder) { + break; + } else { + return JXL_FAILURE("JPEG XL was built without OpenEXR support"); + } + case extras::Codec::kJXL: + // TODO(user): implement + return JXL_FAILURE("Codec::kJXL is not supported yet"); + case extras::Codec::kUnknown: return JXL_FAILURE("Cannot encode using Codec::kUnknown"); } @@ -135,6 +116,11 @@ Status Encode(const CodecInOut& io, const extras::Codec codec, extras::PackedPixelFile ppf; JXL_RETURN_IF_ERROR( ConvertCodecInOutToPackedPixelFile(io, format, c_desired, pool, &ppf)); + ppf.info.bits_per_sample = bits_per_sample; + if (format.data_type == JXL_TYPE_FLOAT) { + ppf.info.bits_per_sample = 32; + ppf.info.exponent_bits_per_sample = 8; + } extras::EncodedImage encoded_image; JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded_image, pool)); JXL_ASSERT(encoded_image.bitstreams.size() == 1); @@ -143,15 +129,15 @@ Status Encode(const CodecInOut& io, const extras::Codec codec, return true; } -Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, - size_t bits_per_sample, const std::string& pathname, - ThreadPool* pool) { - const std::string extension = Extension(pathname); - const extras::Codec codec = - extras::CodecFromExtension(extension, &bits_per_sample); +Status Encode(const CodecInOut& io, const ColorEncoding& c_desired, + size_t bits_per_sample, const std::string& pathname, + std::vector<uint8_t>* bytes, ThreadPool* pool) { + std::string extension; + const extras::Codec codec = extras::CodecFromPath( + pathname, &bits_per_sample, /* filename */ nullptr, &extension); // Warn about incorrect usage of PGM/PGX/PPM - only the latter supports - // color, but CodecFromExtension lumps them all together. + // color, but CodecFromPath lumps them all together. if (codec == extras::Codec::kPNM && extension != ".pfm") { if (io.Main().HasAlpha() && extension != ".pam") { JXL_WARNING( @@ -174,16 +160,14 @@ Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, bits_per_sample = 16; } - std::vector<uint8_t> encoded; - return Encode(io, codec, c_desired, bits_per_sample, &encoded, pool) && - WriteFile(encoded, pathname); + return Encode(io, codec, c_desired, bits_per_sample, bytes, pool); } -Status EncodeToFile(const CodecInOut& io, const std::string& pathname, - ThreadPool* pool) { +Status Encode(const CodecInOut& io, const std::string& pathname, + std::vector<uint8_t>* bytes, ThreadPool* pool) { // TODO(lode): need to take the floating_point_sample field into account - return EncodeToFile(io, io.metadata.m.color_encoding, - io.metadata.m.bit_depth.bits_per_sample, pathname, pool); + return Encode(io, io.metadata.m.color_encoding, + io.metadata.m.bit_depth.bits_per_sample, pathname, bytes, pool); } } // namespace jxl diff --git a/lib/extras/codec.h b/lib/extras/codec.h index 73fdc80..6b39ffd 100644 --- a/lib/extras/codec.h +++ b/lib/extras/codec.h @@ -17,7 +17,6 @@ #include "lib/extras/dec/decode.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/base/span.h" #include "lib/jxl/base/status.h" #include "lib/jxl/codec_in_out.h" @@ -26,25 +25,24 @@ namespace jxl { +struct SizeConstraints; + // Decodes "bytes" and sets io->metadata.m. // color_space_hint may specify the color space, otherwise, defaults to sRGB. Status SetFromBytes(Span<const uint8_t> bytes, const extras::ColorHints& color_hints, CodecInOut* io, ThreadPool* pool = nullptr, + const SizeConstraints* constraints = nullptr, extras::Codec* orig_codec = nullptr); // Helper function to use no color_space_hint. JXL_INLINE Status SetFromBytes(const Span<const uint8_t> bytes, CodecInOut* io, ThreadPool* pool = nullptr, + const SizeConstraints* constraints = nullptr, extras::Codec* orig_codec = nullptr) { - return SetFromBytes(bytes, extras::ColorHints(), io, pool, orig_codec); + return SetFromBytes(bytes, extras::ColorHints(), io, pool, constraints, + orig_codec); } -// Reads from file and calls SetFromBytes. -Status SetFromFile(const std::string& pathname, - const extras::ColorHints& color_hints, CodecInOut* io, - ThreadPool* pool = nullptr, - extras::Codec* orig_codec = nullptr); - // Replaces "bytes" with an encoding of pixels transformed from c_current // color space to c_desired. Status Encode(const CodecInOut& io, extras::Codec codec, @@ -52,12 +50,12 @@ Status Encode(const CodecInOut& io, extras::Codec codec, std::vector<uint8_t>* bytes, ThreadPool* pool = nullptr); // Deduces codec, calls Encode and writes to file. -Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, - size_t bits_per_sample, const std::string& pathname, - ThreadPool* pool = nullptr); +Status Encode(const CodecInOut& io, const ColorEncoding& c_desired, + size_t bits_per_sample, const std::string& pathname, + std::vector<uint8_t>* bytes, ThreadPool* pool = nullptr); // Same, but defaults to metadata.original color_encoding and bits_per_sample. -Status EncodeToFile(const CodecInOut& io, const std::string& pathname, - ThreadPool* pool = nullptr); +Status Encode(const CodecInOut& io, const std::string& pathname, + std::vector<uint8_t>* bytes, ThreadPool* pool = nullptr); } // namespace jxl diff --git a/lib/extras/codec_test.cc b/lib/extras/codec_test.cc index 19cac39..6e86ba9 100644 --- a/lib/extras/codec_test.cc +++ b/lib/extras/codec_test.cc @@ -6,30 +6,28 @@ #include "lib/extras/codec.h" #include <stddef.h> -#include <stdio.h> #include <algorithm> +#include <cstdint> #include <sstream> #include <string> #include <utility> #include <vector> -#include "lib/extras/dec/pgx.h" +#include "lib/extras/common.h" +#include "lib/extras/dec/decode.h" #include "lib/extras/dec/pnm.h" #include "lib/extras/enc/encode.h" -#include "lib/extras/packed_image_convert.h" -#include "lib/jxl/base/printf_macros.h" #include "lib/jxl/base/random.h" -#include "lib/jxl/base/thread_pool_internal.h" -#include "lib/jxl/color_management.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" -#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" #include "lib/jxl/test_utils.h" -#include "lib/jxl/testdata.h" +#include "lib/jxl/testing.h" namespace jxl { + +using test::ThreadPoolForTests; + namespace extras { namespace { @@ -37,7 +35,6 @@ using ::testing::AllOf; using ::testing::Contains; using ::testing::Field; using ::testing::IsEmpty; -using ::testing::NotNull; using ::testing::SizeIs; std::string ExtensionFromCodec(Codec codec, const bool is_gray, @@ -51,18 +48,14 @@ std::string ExtensionFromCodec(Codec codec, const bool is_gray, case Codec::kPNG: return ".png"; case Codec::kPNM: + if (bits_per_sample == 32) return ".pfm"; if (has_alpha) return ".pam"; - if (is_gray) return ".pgm"; - return (bits_per_sample == 32) ? ".pfm" : ".ppm"; - case Codec::kGIF: - return ".gif"; + return is_gray ? ".pgm" : ".ppm"; case Codec::kEXR: return ".exr"; - case Codec::kUnknown: + default: return std::string(); } - JXL_UNREACHABLE; - return std::string(); } void VerifySameImage(const PackedImage& im0, size_t bits_per_sample0, @@ -110,17 +103,16 @@ JxlColorEncoding CreateTestColorEncoding(bool is_gray) { // Roundtrip through internal color encoding to fill in primaries and white // point CIE xy coordinates. ColorEncoding c_internal; - JXL_CHECK(ConvertExternalToInternalColorEncoding(c, &c_internal)); - ConvertInternalToExternalColorEncoding(c_internal, &c); + JXL_CHECK(c_internal.FromExternal(c)); + c = c_internal.ToExternal(); return c; } std::vector<uint8_t> GenerateICC(JxlColorEncoding color_encoding) { ColorEncoding c; - JXL_CHECK(ConvertExternalToInternalColorEncoding(color_encoding, &c)); - JXL_CHECK(c.CreateICC()); - PaddedBytes icc = c.ICC(); - return std::vector<uint8_t>(icc.begin(), icc.end()); + JXL_CHECK(c.FromExternal(color_encoding)); + JXL_CHECK(!c.ICC().empty()); + return c.ICC(); } void StoreRandomValue(uint8_t* out, Rng* rng, JxlPixelFormat format, @@ -173,10 +165,11 @@ struct TestImageParams { bool is_gray; bool add_alpha; bool big_endian; + bool add_extra_channels; bool ShouldTestRoundtrip() const { if (codec == Codec::kPNG) { - return true; + return bits_per_sample <= 16; } else if (codec == Codec::kPNM) { // TODO(szabadka) Make PNM encoder endianness-aware. return ((bits_per_sample <= 16 && big_endian) || @@ -213,7 +206,7 @@ struct TestImageParams { std::string DebugString() const { std::ostringstream os; os << "bps:" << bits_per_sample << " gr:" << is_gray << " al:" << add_alpha - << " be: " << big_endian; + << " be: " << big_endian << " ec: " << add_extra_channels; return os.str(); } }; @@ -233,6 +226,19 @@ void CreateTestImage(const TestImageParams& params, PackedPixelFile* ppf) { PackedFrame frame(params.xsize, params.ysize, params.PixelFormat()); FillPackedImage(params.bits_per_sample, &frame.color); + if (params.add_extra_channels) { + for (size_t i = 0; i < 7; ++i) { + JxlPixelFormat ec_format = params.PixelFormat(); + ec_format.num_channels = 1; + PackedImage ec(params.xsize, params.ysize, ec_format); + FillPackedImage(params.bits_per_sample, &ec); + frame.extra_channels.emplace_back(std::move(ec)); + PackedExtraChannel pec; + pec.ec_info.bits_per_sample = params.bits_per_sample; + pec.ec_info.type = static_cast<JxlExtraChannelType>(i); + ppf->extra_channels_info.emplace_back(std::move(pec)); + } + } ppf->frames.emplace_back(std::move(frame)); } @@ -249,33 +255,67 @@ void TestRoundTrip(const TestImageParams& params, ThreadPool* pool) { EncodedImage encoded; auto encoder = Encoder::FromExtension(extension); - ASSERT_TRUE(encoder.get()); + if (!encoder) { + fprintf(stderr, "Skipping test because of missing codec support.\n"); + return; + } ASSERT_TRUE(encoder->Encode(ppf_in, &encoded, pool)); ASSERT_EQ(encoded.bitstreams.size(), 1); PackedPixelFile ppf_out; - ASSERT_TRUE(DecodeBytes(Span<const uint8_t>(encoded.bitstreams[0]), - ColorHints(), SizeConstraints(), &ppf_out)); - - if (params.codec != Codec::kPNM && params.codec != Codec::kPGX && - params.codec != Codec::kEXR) { + ColorHints color_hints; + if (params.codec == Codec::kPNM || params.codec == Codec::kPGX) { + color_hints.Add("color_space", + params.is_gray ? "Gra_D65_Rel_SRG" : "RGB_D65_SRG_Rel_SRG"); + } + ASSERT_TRUE(DecodeBytes(Bytes(encoded.bitstreams[0]), color_hints, &ppf_out)); + if (params.codec == Codec::kPNG && ppf_out.icc.empty()) { + // Decoding a PNG may drop the ICC profile if there's a valid cICP chunk. + // Rendering intent is not preserved in this case. + EXPECT_EQ(ppf_in.color_encoding.color_space, + ppf_out.color_encoding.color_space); + EXPECT_EQ(ppf_in.color_encoding.white_point, + ppf_out.color_encoding.white_point); + if (ppf_in.color_encoding.color_space != JXL_COLOR_SPACE_GRAY) { + EXPECT_EQ(ppf_in.color_encoding.primaries, + ppf_out.color_encoding.primaries); + } + EXPECT_EQ(ppf_in.color_encoding.transfer_function, + ppf_out.color_encoding.transfer_function); + EXPECT_EQ(ppf_out.color_encoding.rendering_intent, + JXL_RENDERING_INTENT_RELATIVE); + } else if (params.codec != Codec::kPNM && params.codec != Codec::kPGX && + params.codec != Codec::kEXR) { EXPECT_EQ(ppf_in.icc, ppf_out.icc); } ASSERT_EQ(ppf_out.frames.size(), 1); - VerifySameImage(ppf_in.frames[0].color, ppf_in.info.bits_per_sample, - ppf_out.frames[0].color, ppf_out.info.bits_per_sample, + const auto& frame_in = ppf_in.frames[0]; + const auto& frame_out = ppf_out.frames[0]; + VerifySameImage(frame_in.color, ppf_in.info.bits_per_sample, frame_out.color, + ppf_out.info.bits_per_sample, /*lossless=*/params.codec != Codec::kJPG); + ASSERT_EQ(frame_in.extra_channels.size(), frame_out.extra_channels.size()); + ASSERT_EQ(ppf_out.extra_channels_info.size(), + frame_out.extra_channels.size()); + for (size_t i = 0; i < frame_in.extra_channels.size(); ++i) { + VerifySameImage(frame_in.extra_channels[i], ppf_in.info.bits_per_sample, + frame_out.extra_channels[i], ppf_out.info.bits_per_sample, + /*lossless=*/true); + EXPECT_EQ(ppf_out.extra_channels_info[i].ec_info.type, + ppf_in.extra_channels_info[i].ec_info.type); + } } TEST(CodecTest, TestRoundTrip) { - ThreadPoolInternal pool(12); + ThreadPoolForTests pool(12); TestImageParams params; params.xsize = 7; params.ysize = 4; - for (Codec codec : AvailableCodecs()) { + for (Codec codec : + {Codec::kPNG, Codec::kPNM, Codec::kPGX, Codec::kEXR, Codec::kJPG}) { for (int bits_per_sample : {4, 8, 10, 12, 16, 32}) { for (bool is_gray : {false, true}) { for (bool add_alpha : {false, true}) { @@ -285,7 +325,12 @@ TEST(CodecTest, TestRoundTrip) { params.is_gray = is_gray; params.add_alpha = add_alpha; params.big_endian = big_endian; + params.add_extra_channels = false; TestRoundTrip(params, &pool); + if (codec == Codec::kPNM && add_alpha) { + params.add_extra_channels = true; + TestRoundTrip(params, &pool); + } } } } @@ -293,192 +338,39 @@ TEST(CodecTest, TestRoundTrip) { } } -CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool, - const ColorHints& color_hints = ColorHints()) { - CodecInOut io; - const PaddedBytes orig = ReadTestData(pathname); - JXL_CHECK( - SetFromBytes(Span<const uint8_t>(orig), color_hints, &io, pool, nullptr)); - const ImageBundle& ib1 = io.Main(); - - // Encode/Decode again to make sure Encode carries through all metadata. - std::vector<uint8_t> encoded; - JXL_CHECK(Encode(io, Codec::kPNG, io.metadata.m.color_encoding, - io.metadata.m.bit_depth.bits_per_sample, &encoded, pool)); - - CodecInOut io2; - JXL_CHECK(SetFromBytes(Span<const uint8_t>(encoded), color_hints, &io2, pool, - nullptr)); - const ImageBundle& ib2 = io2.Main(); - EXPECT_EQ(Description(ib1.metadata()->color_encoding), - Description(ib2.metadata()->color_encoding)); - EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current())); - - size_t bits_per_sample = io2.metadata.m.bit_depth.bits_per_sample; - - // "Same" pixels? - double max_l1 = bits_per_sample <= 12 ? 1.3 : 2E-3; - double max_rel = bits_per_sample <= 12 ? 6E-3 : 1E-4; - if (ib1.metadata()->color_encoding.IsGray()) { - max_rel *= 2.0; - } else if (ib1.metadata()->color_encoding.primaries != Primaries::kSRGB) { - // Need more tolerance for large gamuts (anything but sRGB) - max_l1 *= 1.5; - max_rel *= 3.0; - } - VerifyRelativeError(ib1.color(), ib2.color(), max_l1, max_rel); - - // Simulate the encoder removing profile and decoder restoring it. - if (!ib2.metadata()->color_encoding.WantICC()) { - io2.metadata.m.color_encoding.InternalRemoveICC(); - EXPECT_TRUE(io2.metadata.m.color_encoding.CreateICC()); - } - - return io2; -} - -#if 0 -TEST(CodecTest, TestMetadataSRGB) { - ThreadPoolInternal pool(12); - - const char* paths[] = {"external/raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png", - "external/raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png", - "external/raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png", - "external/raw.pixls/Nikon-D300-12bit_srgb8_dt.png", - "external/raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"}; - for (const char* relative_pathname : paths) { - const CodecInOut io = - DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool); - EXPECT_EQ(8, io.metadata.m.bit_depth.bits_per_sample); - EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); - EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample); - - EXPECT_EQ(64, io.xsize()); - EXPECT_EQ(64, io.ysize()); - EXPECT_FALSE(io.metadata.m.HasAlpha()); - - const ColorEncoding& c_original = io.metadata.m.color_encoding; - EXPECT_FALSE(c_original.ICC().empty()); - EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace()); - EXPECT_EQ(WhitePoint::kD65, c_original.white_point); - EXPECT_EQ(Primaries::kSRGB, c_original.primaries); - EXPECT_TRUE(c_original.tf.IsSRGB()); - } -} - -TEST(CodecTest, TestMetadataLinear) { - ThreadPoolInternal pool(12); - - const char* paths[3] = { - "external/raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png", - "external/raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png", - "external/raw.pixls/Nikon-D300-12bit_2020_g1_dt.png", - }; - const WhitePoint white_points[3] = {WhitePoint::kCustom, WhitePoint::kD65, - WhitePoint::kD65}; - const Primaries primaries[3] = {Primaries::kCustom, Primaries::kSRGB, - Primaries::k2100}; - - for (size_t i = 0; i < 3; ++i) { - const CodecInOut io = DecodeRoundtrip(paths[i], Codec::kPNG, &pool); - EXPECT_EQ(16, io.metadata.m.bit_depth.bits_per_sample); - EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); - EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample); - - EXPECT_EQ(64, io.xsize()); - EXPECT_EQ(64, io.ysize()); - EXPECT_FALSE(io.metadata.m.HasAlpha()); - - const ColorEncoding& c_original = io.metadata.m.color_encoding; - EXPECT_FALSE(c_original.ICC().empty()); - EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace()); - EXPECT_EQ(white_points[i], c_original.white_point); - EXPECT_EQ(primaries[i], c_original.primaries); - EXPECT_TRUE(c_original.tf.IsLinear()); - } -} - -TEST(CodecTest, TestMetadataICC) { - ThreadPoolInternal pool(12); - - const char* paths[] = { - "external/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png", - "external/raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png", - }; - for (const char* relative_pathname : paths) { - const CodecInOut io = - DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool); - EXPECT_GE(16, io.metadata.m.bit_depth.bits_per_sample); - EXPECT_LE(14, io.metadata.m.bit_depth.bits_per_sample); - - EXPECT_EQ(64, io.xsize()); - EXPECT_EQ(64, io.ysize()); - EXPECT_FALSE(io.metadata.m.HasAlpha()); - - const ColorEncoding& c_original = io.metadata.m.color_encoding; - EXPECT_FALSE(c_original.ICC().empty()); - EXPECT_EQ(RenderingIntent::kPerceptual, c_original.rendering_intent); - EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace()); - EXPECT_EQ(WhitePoint::kD65, c_original.white_point); - EXPECT_EQ(Primaries::kSRGB, c_original.primaries); - EXPECT_EQ(TransferFunction::k709, c_original.tf.GetTransferFunction()); +TEST(CodecTest, LosslessPNMRoundtrip) { + ThreadPoolForTests pool(12); + + static const char* kChannels[] = {"", "g", "ga", "rgb", "rgba"}; + static const char* kExtension[] = {"", ".pgm", ".pam", ".ppm", ".pam"}; + for (size_t bit_depth = 1; bit_depth <= 16; ++bit_depth) { + for (size_t channels = 1; channels <= 4; ++channels) { + if (bit_depth == 1 && (channels == 2 || channels == 4)) continue; + std::string extension(kExtension[channels]); + std::string filename = "jxl/flower/flower_small." + + std::string(kChannels[channels]) + ".depth" + + std::to_string(bit_depth) + extension; + const std::vector<uint8_t> orig = jxl::test::ReadTestData(filename); + + PackedPixelFile ppf; + ColorHints color_hints; + color_hints.Add("color_space", + channels < 3 ? "Gra_D65_Rel_SRG" : "RGB_D65_SRG_Rel_SRG"); + ASSERT_TRUE( + DecodeBytes(Bytes(orig.data(), orig.size()), color_hints, &ppf)); + + EncodedImage encoded; + auto encoder = Encoder::FromExtension(extension); + ASSERT_TRUE(encoder.get()); + ASSERT_TRUE(encoder->Encode(ppf, &encoded, &pool)); + ASSERT_EQ(encoded.bitstreams.size(), 1); + ASSERT_EQ(orig.size(), encoded.bitstreams[0].size()); + EXPECT_EQ(0, + memcmp(orig.data(), encoded.bitstreams[0].data(), orig.size())); + } } } -TEST(CodecTest, Testexternal/pngsuite) { - ThreadPoolInternal pool(12); - - // Ensure we can load PNG with text, japanese UTF-8, compressed text. - (void)DecodeRoundtrip("external/pngsuite/ct1n0g04.png", Codec::kPNG, &pool); - (void)DecodeRoundtrip("external/pngsuite/ctjn0g04.png", Codec::kPNG, &pool); - (void)DecodeRoundtrip("external/pngsuite/ctzn0g04.png", Codec::kPNG, &pool); - - // Extract gAMA - const CodecInOut b1 = - DecodeRoundtrip("external/pngsuite/g10n3p04.png", Codec::kPNG, &pool); - EXPECT_TRUE(b1.metadata.color_encoding.tf.IsLinear()); - - // Extract cHRM - const CodecInOut b_p = - DecodeRoundtrip("external/pngsuite/ccwn2c08.png", Codec::kPNG, &pool); - EXPECT_EQ(Primaries::kSRGB, b_p.metadata.color_encoding.primaries); - EXPECT_EQ(WhitePoint::kD65, b_p.metadata.color_encoding.white_point); - - // Extract EXIF from (new-style) dedicated chunk - const CodecInOut b_exif = - DecodeRoundtrip("external/pngsuite/exif2c08.png", Codec::kPNG, &pool); - EXPECT_EQ(978, b_exif.blobs.exif.size()); -} -#endif - -void VerifyWideGamutMetadata(const std::string& relative_pathname, - const Primaries primaries, ThreadPool* pool) { - const CodecInOut io = DecodeRoundtrip(relative_pathname, pool); - - EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); - EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); - EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); - - const ColorEncoding& c_original = io.metadata.m.color_encoding; - EXPECT_FALSE(c_original.ICC().empty()); - EXPECT_EQ(RenderingIntent::kAbsolute, c_original.rendering_intent); - EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace()); - EXPECT_EQ(WhitePoint::kD65, c_original.white_point); - EXPECT_EQ(primaries, c_original.primaries); -} - -TEST(CodecTest, TestWideGamut) { - ThreadPoolInternal pool(12); - // VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-bars.png", - // Primaries::kP3, &pool); - VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-ring.png", - Primaries::kP3, &pool); - // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-bars.png", - // Primaries::k2100, &pool); - // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-ring.png", - // Primaries::k2100, &pool); -} - TEST(CodecTest, TestPNM) { TestCodecPNM(); } TEST(CodecTest, FormatNegotiation) { @@ -520,13 +412,15 @@ TEST(CodecTest, EncodeToPNG) { ThreadPool* const pool = nullptr; std::unique_ptr<Encoder> png_encoder = Encoder::FromExtension(".png"); - ASSERT_THAT(png_encoder, NotNull()); + if (!png_encoder) { + fprintf(stderr, "Skipping test because of missing codec support.\n"); + return; + } - const PaddedBytes original_png = - ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + const std::vector<uint8_t> original_png = jxl::test::ReadTestData( + "external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); PackedPixelFile ppf; - ASSERT_TRUE(extras::DecodeBytes(Span<const uint8_t>(original_png), - ColorHints(), SizeConstraints(), &ppf)); + ASSERT_TRUE(extras::DecodeBytes(Bytes(original_png), ColorHints(), &ppf)); const JxlPixelFormat& format = ppf.frames.front().color.format; ASSERT_THAT( @@ -540,9 +434,8 @@ TEST(CodecTest, EncodeToPNG) { ASSERT_THAT(encoded_png.bitstreams, SizeIs(1)); PackedPixelFile decoded_ppf; - ASSERT_TRUE( - extras::DecodeBytes(Span<const uint8_t>(encoded_png.bitstreams.front()), - ColorHints(), SizeConstraints(), &decoded_ppf)); + ASSERT_TRUE(extras::DecodeBytes(Bytes(encoded_png.bitstreams.front()), + ColorHints(), &decoded_ppf)); ASSERT_EQ(decoded_ppf.info.bits_per_sample, ppf.info.bits_per_sample); ASSERT_EQ(decoded_ppf.frames.size(), 1); diff --git a/lib/extras/common.cc b/lib/extras/common.cc new file mode 100644 index 0000000..e85b43a --- /dev/null +++ b/lib/extras/common.cc @@ -0,0 +1,61 @@ +// 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/common.h" + +#include <jxl/codestream_header.h> +#include <jxl/types.h> + +#include <cstddef> +#include <vector> + +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace extras { + +Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats, + const JxlBasicInfo& basic_info, JxlPixelFormat* format) { + const size_t original_bit_depth = basic_info.bits_per_sample; + size_t current_bit_depth = 0; + size_t num_alpha_channels = (basic_info.alpha_bits != 0 ? 1 : 0); + size_t num_channels = basic_info.num_color_channels + num_alpha_channels; + for (;;) { + for (const JxlPixelFormat& candidate : accepted_formats) { + if (candidate.num_channels != num_channels) continue; + const size_t candidate_bit_depth = + PackedImage::BitsPerChannel(candidate.data_type); + if ( + // Candidate bit depth is less than what we have and still enough + (original_bit_depth <= candidate_bit_depth && + candidate_bit_depth < current_bit_depth) || + // Or larger than the too-small bit depth we currently have + (current_bit_depth < candidate_bit_depth && + current_bit_depth < original_bit_depth)) { + *format = candidate; + current_bit_depth = candidate_bit_depth; + } + } + if (current_bit_depth == 0) { + if (num_channels > basic_info.num_color_channels) { + // Try dropping the alpha channel. + --num_channels; + continue; + } + return JXL_FAILURE("no appropriate format found"); + } + break; + } + if (current_bit_depth < original_bit_depth) { + JXL_WARNING("encoding %" PRIuS "-bit original to %" PRIuS " bits", + original_bit_depth, current_bit_depth); + } + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/common.h b/lib/extras/common.h new file mode 100644 index 0000000..88ed581 --- /dev/null +++ b/lib/extras/common.h @@ -0,0 +1,26 @@ +// 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_COMMON_H_ +#define LIB_EXTRAS_COMMON_H_ + +#include <jxl/codestream_header.h> +#include <jxl/types.h> + +#include <vector> + +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace extras { + +// TODO(sboukortt): consider exposing this as part of the C API. +Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats, + const JxlBasicInfo& basic_info, JxlPixelFormat* format); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_COMMON_H_ 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 diff --git a/lib/extras/enc/apng.cc b/lib/extras/enc/apng.cc index db6cf9e..f2f8754 100644 --- a/lib/extras/enc/apng.cc +++ b/lib/extras/enc/apng.cc @@ -36,7 +36,6 @@ * */ -#include <stdio.h> #include <string.h> #include <string> @@ -45,21 +44,29 @@ #include "lib/extras/exif.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/printf_macros.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}; + class APNGEncoder : public Encoder { public: std::vector<JxlPixelFormat> AcceptedFormats() const override { std::vector<JxlPixelFormat> formats; for (const uint32_t num_channels : {1, 2, 3, 4}) { for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { - formats.push_back(JxlPixelFormat{num_channels, data_type, - JXL_BIG_ENDIAN, /*align=*/0}); + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back( + JxlPixelFormat{num_channels, data_type, endianness, /*align=*/0}); + } } } return formats; @@ -96,12 +103,24 @@ class BlobsWriterPNG { // identity to avoid repeated orientation. std::vector<uint8_t> exif = blobs.exif; ResetExifOrientation(exif); + // By convention, the data is prefixed with "Exif\0\0" when stored in + // the legacy (and non-standard) "Raw profile type exif" text chunk + // currently used here. + // TODO(user): Store Exif data in an eXIf chunk instead, which always + // begins with the TIFF header. + if (exif.size() >= sizeof kExifSignature && + memcmp(exif.data(), kExifSignature, sizeof kExifSignature) != 0) { + exif.insert(exif.begin(), kExifSignature, + kExifSignature + sizeof kExifSignature); + } JXL_RETURN_IF_ERROR(EncodeBase16("exif", exif, strings)); } if (!blobs.iptc.empty()) { JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings)); } if (!blobs.xmp.empty()) { + // TODO(user): Store XMP data in an "XML:com.adobe.xmp" text chunk + // instead. JXL_RETURN_IF_ERROR(EncodeBase16("xmp", blobs.xmp, strings)); } return true; @@ -142,7 +161,7 @@ class BlobsWriterPNG { } }; -void MaybeAddCICP(JxlColorEncoding c_enc, png_structp png_ptr, +void MaybeAddCICP(const JxlColorEncoding& c_enc, png_structp png_ptr, png_infop info_ptr) { png_byte cicp_data[4] = {}; png_unknown_chunk cicp_chunk; @@ -172,13 +191,80 @@ void MaybeAddCICP(JxlColorEncoding c_enc, png_structp png_ptr, cicp_data[3] = 1; cicp_chunk.data = cicp_data; cicp_chunk.size = sizeof(cicp_data); - cicp_chunk.location = PNG_HAVE_PLTE; + cicp_chunk.location = PNG_HAVE_IHDR; memcpy(cicp_chunk.name, "cICP", 5); - png_set_keep_unknown_chunks(png_ptr, 3, + png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, reinterpret_cast<const png_byte*>("cICP"), 1); png_set_unknown_chunks(png_ptr, info_ptr, &cicp_chunk, 1); } +bool MaybeAddSRGB(const JxlColorEncoding& c_enc, png_structp png_ptr, + png_infop info_ptr) { + if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_SRGB && + (c_enc.color_space == JXL_COLOR_SPACE_GRAY || + (c_enc.color_space == JXL_COLOR_SPACE_RGB && + c_enc.primaries == JXL_PRIMARIES_SRGB && + c_enc.white_point == JXL_WHITE_POINT_D65))) { + png_set_sRGB(png_ptr, info_ptr, c_enc.rendering_intent); + png_set_cHRM_fixed(png_ptr, info_ptr, 31270, 32900, 64000, 33000, 30000, + 60000, 15000, 6000); + png_set_gAMA_fixed(png_ptr, info_ptr, 45455); + return true; + } + return false; +} + +void MaybeAddCHRM(const JxlColorEncoding& c_enc, png_structp png_ptr, + png_infop info_ptr) { + if (c_enc.color_space != JXL_COLOR_SPACE_RGB) return; + if (c_enc.primaries == 0) return; + png_set_cHRM(png_ptr, info_ptr, c_enc.white_point_xy[0], + c_enc.white_point_xy[1], c_enc.primaries_red_xy[0], + c_enc.primaries_red_xy[1], c_enc.primaries_green_xy[0], + c_enc.primaries_green_xy[1], c_enc.primaries_blue_xy[0], + c_enc.primaries_blue_xy[1]); +} + +void MaybeAddGAMA(const JxlColorEncoding& c_enc, png_structp png_ptr, + png_infop info_ptr) { + switch (c_enc.transfer_function) { + case JXL_TRANSFER_FUNCTION_LINEAR: + png_set_gAMA_fixed(png_ptr, info_ptr, PNG_FP_1); + break; + case JXL_TRANSFER_FUNCTION_SRGB: + png_set_gAMA_fixed(png_ptr, info_ptr, 45455); + break; + case JXL_TRANSFER_FUNCTION_GAMMA: + png_set_gAMA(png_ptr, info_ptr, c_enc.gamma); + break; + + default:; + // No gAMA chunk. + } +} + +void MaybeAddCLLi(const JxlColorEncoding& c_enc, const float intensity_target, + png_structp png_ptr, png_infop info_ptr) { + if (c_enc.transfer_function != JXL_TRANSFER_FUNCTION_PQ) return; + + const uint32_t max_cll = + static_cast<uint32_t>(10000.f * Clamp1(intensity_target, 0.f, 10000.f)); + png_byte chunk_data[8] = {}; + chunk_data[0] = (max_cll >> 24) & 0xFF; + chunk_data[1] = (max_cll >> 16) & 0xFF; + chunk_data[2] = (max_cll >> 8) & 0xFF; + chunk_data[3] = max_cll & 0xFF; + // Leave MaxFALL set to 0. + png_unknown_chunk chunk; + memcpy(chunk.name, "cLLi", 5); + chunk.data = chunk_data; + chunk.size = sizeof chunk_data; + chunk.location = PNG_HAVE_IHDR; + png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, + reinterpret_cast<const png_byte*>("cLLi"), 1); + png_set_unknown_chunks(png_ptr, info_ptr, &chunk, 1); +} + Status APNGEncoder::EncodePackedPixelFileToAPNG( const PackedPixelFile& ppf, ThreadPool* pool, std::vector<uint8_t>* bytes) const { @@ -233,21 +319,7 @@ Status APNGEncoder::EncodePackedPixelFileToAPNG( } else { memcpy(&out[0], in, out_size); } - } else if (format.data_type == JXL_TYPE_FLOAT) { - float mul = 65535.0; - const uint8_t* p_in = in; - uint8_t* p_out = out.data(); - for (size_t i = 0; i < num_samples; ++i, p_in += 4, p_out += 2) { - uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE32(p_in) - : LoadLE32(p_in)); - float fval; - memcpy(&fval, &val, 4); - StoreBE16(static_cast<uint32_t>(fval * mul + 0.5), p_out); - } - } else { - return JXL_FAILURE("Unsupported pixel data type"); } - png_structp png_ptr; png_infop info_ptr; @@ -272,11 +344,19 @@ Status APNGEncoder::EncodePackedPixelFileToAPNG( PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); if (count == 0) { - MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr); - if (!ppf.icc.empty()) { - png_set_benign_errors(png_ptr, 1); - png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(), ppf.icc.size()); + if (!MaybeAddSRGB(ppf.color_encoding, png_ptr, info_ptr)) { + MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr); + if (!ppf.icc.empty()) { + png_set_benign_errors(png_ptr, 1); + png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(), + ppf.icc.size()); + } + MaybeAddCHRM(ppf.color_encoding, png_ptr, info_ptr); + MaybeAddGAMA(ppf.color_encoding, png_ptr, info_ptr); } + MaybeAddCLLi(ppf.color_encoding, ppf.info.intensity_target, png_ptr, + info_ptr); + std::vector<std::string> textstrings; JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings)); for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) { @@ -360,9 +440,14 @@ Status APNGEncoder::EncodePackedPixelFileToAPNG( } } // namespace +#endif std::unique_ptr<Encoder> GetAPNGEncoder() { +#if JPEGXL_ENABLE_APNG return jxl::make_unique<APNGEncoder>(); +#else + return nullptr; +#endif } } // namespace extras diff --git a/lib/extras/enc/encode.cc b/lib/extras/enc/encode.cc index dc593d2..8c9a148 100644 --- a/lib/extras/enc/encode.cc +++ b/lib/extras/enc/encode.cc @@ -7,24 +7,17 @@ #include <locale> -#if JPEGXL_ENABLE_APNG #include "lib/extras/enc/apng.h" -#endif -#if JPEGXL_ENABLE_EXR #include "lib/extras/enc/exr.h" -#endif -#if JPEGXL_ENABLE_JPEG #include "lib/extras/enc/jpg.h" -#endif #include "lib/extras/enc/npy.h" #include "lib/extras/enc/pgx.h" #include "lib/extras/enc/pnm.h" -#include "lib/jxl/base/printf_macros.h" namespace jxl { namespace extras { -Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) const { +Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) { if (info.xsize == 0 || info.ysize == 0) { return JXL_FAILURE("Empty image"); } @@ -40,8 +33,34 @@ Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) const { return true; } -Status Encoder::VerifyPackedImage(const PackedImage& image, - const JxlBasicInfo& info) const { +Status Encoder::VerifyFormat(const JxlPixelFormat& format) const { + for (auto f : AcceptedFormats()) { + if (f.num_channels != format.num_channels) continue; + if (f.data_type != format.data_type) continue; + if (f.data_type == JXL_TYPE_UINT8 || f.endianness == format.endianness) { + return true; + } + } + return JXL_FAILURE("Format is not in the list of accepted formats."); +} + +Status Encoder::VerifyBitDepth(JxlDataType data_type, uint32_t bits_per_sample, + uint32_t exponent_bits) { + if ((data_type == JXL_TYPE_UINT8 && + (bits_per_sample == 0 || bits_per_sample > 8 || exponent_bits != 0)) || + (data_type == JXL_TYPE_UINT16 && + (bits_per_sample <= 8 || bits_per_sample > 16 || exponent_bits != 0)) || + (data_type == JXL_TYPE_FLOAT16 && + (bits_per_sample > 16 || exponent_bits > 5))) { + return JXL_FAILURE( + "Incompatible data_type %d and bit depth %u with exponent bits %u", + (int)data_type, bits_per_sample, exponent_bits); + } + return true; +} + +Status Encoder::VerifyImageSize(const PackedImage& image, + const JxlBasicInfo& info) { if (image.pixels() == nullptr) { return JXL_FAILURE("Invalid image."); } @@ -57,77 +76,60 @@ Status Encoder::VerifyPackedImage(const PackedImage& image, image.format.num_channels != info_num_channels) { return JXL_FAILURE("Frame size does not match image size"); } - if (info.bits_per_sample > - PackedImage::BitsPerChannel(image.format.data_type)) { - return JXL_FAILURE("Bit depth does not fit pixel data type"); - } return true; } -Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats, - const JxlBasicInfo& basic_info, JxlPixelFormat* format) { - const size_t original_bit_depth = basic_info.bits_per_sample; - size_t current_bit_depth = 0; - size_t num_alpha_channels = (basic_info.alpha_bits != 0 ? 1 : 0); - size_t num_channels = basic_info.num_color_channels + num_alpha_channels; - for (;;) { - for (const JxlPixelFormat& candidate : accepted_formats) { - if (candidate.num_channels != num_channels) continue; - const size_t candidate_bit_depth = - PackedImage::BitsPerChannel(candidate.data_type); - if ( - // Candidate bit depth is less than what we have and still enough - (original_bit_depth <= candidate_bit_depth && - candidate_bit_depth < current_bit_depth) || - // Or larger than the too-small bit depth we currently have - (current_bit_depth < candidate_bit_depth && - current_bit_depth < original_bit_depth)) { - *format = candidate; - current_bit_depth = candidate_bit_depth; - } - } - if (current_bit_depth == 0) { - if (num_channels > basic_info.num_color_channels) { - // Try dropping the alpha channel. - --num_channels; - continue; - } - return JXL_FAILURE("no appropriate format found"); - } - break; - } - if (current_bit_depth < original_bit_depth) { - JXL_WARNING("encoding %" PRIuS "-bit original to %" PRIuS " bits", - original_bit_depth, current_bit_depth); - } +Status Encoder::VerifyPackedImage(const PackedImage& image, + const JxlBasicInfo& info) const { + JXL_RETURN_IF_ERROR(VerifyImageSize(image, info)); + JXL_RETURN_IF_ERROR(VerifyFormat(image.format)); + JXL_RETURN_IF_ERROR(VerifyBitDepth(image.format.data_type, + info.bits_per_sample, + info.exponent_bits_per_sample)); return true; } +template <int metadata> +class MetadataEncoder : public Encoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + // empty, i.e. no need for actual pixel data + return formats; + } + + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded, + ThreadPool* pool) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + encoded->icc.clear(); + encoded->bitstreams.resize(1); + if (metadata == 0) encoded->bitstreams.front() = ppf.metadata.exif; + if (metadata == 1) encoded->bitstreams.front() = ppf.metadata.xmp; + if (metadata == 2) encoded->bitstreams.front() = ppf.metadata.jumbf; + return true; + } +}; + std::unique_ptr<Encoder> Encoder::FromExtension(std::string extension) { std::transform( extension.begin(), extension.end(), extension.begin(), [](char c) { return std::tolower(c, std::locale::classic()); }); -#if JPEGXL_ENABLE_APNG if (extension == ".png" || extension == ".apng") return GetAPNGEncoder(); -#endif - -#if JPEGXL_ENABLE_JPEG if (extension == ".jpg") return GetJPEGEncoder(); if (extension == ".jpeg") return GetJPEGEncoder(); -#endif - if (extension == ".npy") return GetNumPyEncoder(); - if (extension == ".pgx") return GetPGXEncoder(); - if (extension == ".pam") return GetPAMEncoder(); if (extension == ".pgm") return GetPGMEncoder(); if (extension == ".ppm") return GetPPMEncoder(); + if (extension == ".pnm") return GetPNMEncoder(); if (extension == ".pfm") return GetPFMEncoder(); - -#if JPEGXL_ENABLE_EXR if (extension == ".exr") return GetEXREncoder(); -#endif + if (extension == ".exif") return jxl::make_unique<MetadataEncoder<0>>(); + if (extension == ".xmp") return jxl::make_unique<MetadataEncoder<1>>(); + if (extension == ".xml") return jxl::make_unique<MetadataEncoder<1>>(); + if (extension == ".jumbf") return jxl::make_unique<MetadataEncoder<2>>(); + if (extension == ".jumb") return jxl::make_unique<MetadataEncoder<2>>(); return nullptr; } diff --git a/lib/extras/enc/encode.h b/lib/extras/enc/encode.h index 92eec50..da5f509 100644 --- a/lib/extras/enc/encode.h +++ b/lib/extras/enc/encode.h @@ -8,10 +8,17 @@ // Facade for image encoders. +#include <jxl/codestream_header.h> +#include <jxl/types.h> + +#include <cstdint> +#include <memory> #include <string> #include <unordered_map> +#include <utility> +#include <vector> -#include "lib/extras/dec/decode.h" +#include "lib/extras/packed_image.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/status.h" @@ -20,7 +27,7 @@ namespace extras { struct EncodedImage { // One (if the format supports animations or the image has only one frame) or - // more sequential bitstreams. + // more 1quential bitstreams. std::vector<std::vector<uint8_t>> bitstreams; // For each extra channel one or more sequential bitstreams. @@ -43,6 +50,8 @@ class Encoder { virtual ~Encoder() = default; + // Set of pixel formats that this encoder takes as input. + // If empty, the 'encoder' does not need any pixels (it's metadata-only). virtual std::vector<JxlPixelFormat> AcceptedFormats() const = 0; // Any existing data in encoded_image is discarded. @@ -53,12 +62,18 @@ class Encoder { options_[std::move(name)] = std::move(value); } + static Status VerifyBasicInfo(const JxlBasicInfo& info); + static Status VerifyImageSize(const PackedImage& image, + const JxlBasicInfo& info); + static Status VerifyBitDepth(JxlDataType data_type, uint32_t bits_per_sample, + uint32_t exponent_bits); + protected: const std::unordered_map<std::string, std::string>& options() const { return options_; } - Status VerifyBasicInfo(const JxlBasicInfo& info) const; + Status VerifyFormat(const JxlPixelFormat& format) const; Status VerifyPackedImage(const PackedImage& image, const JxlBasicInfo& info) const; @@ -67,10 +82,6 @@ class Encoder { std::unordered_map<std::string, std::string> options_; }; -// TODO(sboukortt): consider exposing this as part of the C API. -Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats, - const JxlBasicInfo& basic_info, JxlPixelFormat* format); - } // namespace extras } // namespace jxl diff --git a/lib/extras/enc/exr.cc b/lib/extras/enc/exr.cc index 05e05f9..d4005c3 100644 --- a/lib/extras/enc/exr.cc +++ b/lib/extras/enc/exr.cc @@ -5,20 +5,23 @@ #include "lib/extras/enc/exr.h" +#if JPEGXL_ENABLE_EXR #include <ImfChromaticitiesAttribute.h> #include <ImfIO.h> #include <ImfRgbaFile.h> #include <ImfStandardAttributes.h> +#endif +#include <jxl/codestream_header.h> #include <vector> -#include "jxl/codestream_header.h" #include "lib/extras/packed_image.h" #include "lib/jxl/base/byte_order.h" namespace jxl { namespace extras { +#if JPEGXL_ENABLE_EXR namespace { namespace OpenEXR = OPENEXR_IMF_NAMESPACE; @@ -110,7 +113,7 @@ Status EncodeImageEXR(const PackedImage& image, const JxlBasicInfo& info, chromaticities.white = Imath::V2f(c_enc.white_point_xy[0], c_enc.white_point_xy[1]); OpenEXR::addChromaticities(header, chromaticities); - OpenEXR::addWhiteLuminance(header, 255.0f); + OpenEXR::addWhiteLuminance(header, info.intensity_target); auto loadFloat = format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat : LoadLEFloat; @@ -162,7 +165,7 @@ class EXREncoder : public Encoder { std::vector<JxlPixelFormat> AcceptedFormats() const override { std::vector<JxlPixelFormat> formats; for (const uint32_t num_channels : {1, 2, 3, 4}) { - for (const JxlDataType data_type : {JXL_TYPE_FLOAT, JXL_TYPE_FLOAT16}) { + for (const JxlDataType data_type : {JXL_TYPE_FLOAT}) { for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, /*data_type=*/data_type, @@ -191,9 +194,14 @@ class EXREncoder : public Encoder { }; } // namespace +#endif std::unique_ptr<Encoder> GetEXREncoder() { +#if JPEGXL_ENABLE_EXR return jxl::make_unique<EXREncoder>(); +#else + return nullptr; +#endif } } // namespace extras diff --git a/lib/extras/enc/jpegli.cc b/lib/extras/enc/jpegli.cc new file mode 100644 index 0000000..3b78764 --- /dev/null +++ b/lib/extras/enc/jpegli.cc @@ -0,0 +1,523 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/enc/jpegli.h" + +#include <jxl/cms.h> +#include <jxl/codestream_header.h> +#include <setjmp.h> +#include <stdint.h> + +#include "lib/extras/enc/encode.h" +#include "lib/jpegli/encode.h" +#include "lib/jxl/enc_xyb.h" + +namespace jxl { +namespace extras { + +namespace { + +void MyErrorExit(j_common_ptr cinfo) { + jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data); + (*cinfo->err->output_message)(cinfo); + jpegli_destroy_compress(reinterpret_cast<j_compress_ptr>(cinfo)); + longjmp(*env, 1); +} + +Status VerifyInput(const PackedPixelFile& ppf) { + const JxlBasicInfo& info = ppf.info; + JXL_RETURN_IF_ERROR(Encoder::VerifyBasicInfo(info)); + if (ppf.frames.size() != 1) { + return JXL_FAILURE("JPEG input must have exactly one frame."); + } + const PackedImage& image = ppf.frames[0].color; + JXL_RETURN_IF_ERROR(Encoder::VerifyImageSize(image, info)); + if (image.format.data_type == JXL_TYPE_FLOAT16) { + return JXL_FAILURE("FLOAT16 input is not supported."); + } + JXL_RETURN_IF_ERROR(Encoder::VerifyBitDepth(image.format.data_type, + info.bits_per_sample, + info.exponent_bits_per_sample)); + if ((image.format.data_type == JXL_TYPE_UINT8 && info.bits_per_sample != 8) || + (image.format.data_type == JXL_TYPE_UINT16 && + info.bits_per_sample != 16)) { + return JXL_FAILURE("Only full bit depth unsigned types are supported."); + } + return true; +} + +Status GetColorEncoding(const PackedPixelFile& ppf, + ColorEncoding* color_encoding) { + if (!ppf.icc.empty()) { + IccBytes icc = ppf.icc; + JXL_RETURN_IF_ERROR( + color_encoding->SetICC(std::move(icc), JxlGetDefaultCms())); + } else { + JXL_RETURN_IF_ERROR(color_encoding->FromExternal(ppf.color_encoding)); + } + if (color_encoding->ICC().empty()) { + return JXL_FAILURE("Invalid color encoding."); + } + return true; +} + +bool HasICCProfile(const std::vector<uint8_t>& app_data) { + size_t pos = 0; + while (pos < app_data.size()) { + if (pos + 16 > app_data.size()) return false; + uint8_t marker = app_data[pos + 1]; + size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2; + if (marker == 0xe2 && memcmp(&app_data[pos + 4], "ICC_PROFILE", 12) == 0) { + return true; + } + pos += marker_len; + } + return false; +} + +Status WriteAppData(j_compress_ptr cinfo, + const std::vector<uint8_t>& app_data) { + size_t pos = 0; + while (pos < app_data.size()) { + if (pos + 4 > app_data.size()) { + return JXL_FAILURE("Incomplete APP header."); + } + uint8_t marker = app_data[pos + 1]; + size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2; + if (app_data[pos] != 0xff || marker < 0xe0 || marker > 0xef) { + return JXL_FAILURE("Invalid APP marker %02x %02x", app_data[pos], marker); + } + if (marker_len <= 4) { + return JXL_FAILURE("Invalid APP marker length."); + } + if (pos + marker_len > app_data.size()) { + return JXL_FAILURE("Incomplete APP data"); + } + jpegli_write_marker(cinfo, marker, &app_data[pos + 4], marker_len - 4); + pos += marker_len; + } + return true; +} + +static constexpr int kICCMarker = 0xe2; +constexpr unsigned char kICCSignature[12] = { + 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00}; +static constexpr uint8_t kUnknownTf = 2; +static constexpr unsigned char kCICPTagSignature[4] = {0x63, 0x69, 0x63, 0x70}; +static constexpr size_t kCICPTagSize = 12; + +bool FindCICPTag(const uint8_t* icc_data, size_t len, bool is_first_chunk, + size_t* cicp_offset, size_t* cicp_length, uint8_t* cicp_tag, + size_t* cicp_pos) { + if (is_first_chunk) { + // Look up the offset of the CICP tag from the first chunk of ICC data. + if (len < 132) { + return false; + } + uint32_t tag_count = LoadBE32(&icc_data[128]); + if (len < 132 + 12 * tag_count) { + return false; + } + for (uint32_t i = 0; i < tag_count; ++i) { + if (memcmp(&icc_data[132 + 12 * i], kCICPTagSignature, 4) == 0) { + *cicp_offset = LoadBE32(&icc_data[136 + 12 * i]); + *cicp_length = LoadBE32(&icc_data[140 + 12 * i]); + } + } + if (*cicp_length < kCICPTagSize) { + return false; + } + } + if (*cicp_offset < len) { + size_t n_bytes = std::min(len - *cicp_offset, kCICPTagSize - *cicp_pos); + memcpy(&cicp_tag[*cicp_pos], &icc_data[*cicp_offset], n_bytes); + *cicp_pos += n_bytes; + *cicp_offset = 0; + } else { + *cicp_offset -= len; + } + return true; +} + +uint8_t LookupCICPTransferFunctionFromAppData(const uint8_t* app_data, + size_t len) { + size_t last_index = 0; + size_t cicp_offset = 0; + size_t cicp_length = 0; + uint8_t cicp_tag[kCICPTagSize] = {}; + size_t cicp_pos = 0; + size_t pos = 0; + while (pos < len) { + const uint8_t* marker = &app_data[pos]; + if (pos + 4 > len) { + return kUnknownTf; + } + size_t marker_size = (marker[2] << 8) + marker[3] + 2; + if (pos + marker_size > len) { + return kUnknownTf; + } + if (marker_size < 18 || marker[0] != 0xff || marker[1] != kICCMarker || + memcmp(&marker[4], kICCSignature, 12) != 0) { + pos += marker_size; + continue; + } + uint8_t index = marker[16]; + uint8_t total = marker[17]; + const uint8_t* payload = marker + 18; + const size_t payload_size = marker_size - 18; + if (index != last_index + 1 || index > total) { + return kUnknownTf; + } + if (!FindCICPTag(payload, payload_size, last_index == 0, &cicp_offset, + &cicp_length, &cicp_tag[0], &cicp_pos)) { + return kUnknownTf; + } + if (cicp_pos == kCICPTagSize) { + break; + } + ++last_index; + } + if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) { + return cicp_tag[9]; + } + return kUnknownTf; +} + +uint8_t LookupCICPTransferFunctionFromICCProfile(const uint8_t* icc_data, + size_t len) { + size_t cicp_offset = 0; + size_t cicp_length = 0; + uint8_t cicp_tag[kCICPTagSize] = {}; + size_t cicp_pos = 0; + if (!FindCICPTag(icc_data, len, true, &cicp_offset, &cicp_length, + &cicp_tag[0], &cicp_pos)) { + return kUnknownTf; + } + if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) { + return cicp_tag[9]; + } + return kUnknownTf; +} + +JpegliDataType ConvertDataType(JxlDataType type) { + switch (type) { + case JXL_TYPE_UINT8: + return JPEGLI_TYPE_UINT8; + case JXL_TYPE_UINT16: + return JPEGLI_TYPE_UINT16; + case JXL_TYPE_FLOAT: + return JPEGLI_TYPE_FLOAT; + default: + return JPEGLI_TYPE_UINT8; + } +} + +JpegliEndianness ConvertEndianness(JxlEndianness endianness) { + switch (endianness) { + case JXL_NATIVE_ENDIAN: + return JPEGLI_NATIVE_ENDIAN; + case JXL_LITTLE_ENDIAN: + return JPEGLI_LITTLE_ENDIAN; + case JXL_BIG_ENDIAN: + return JPEGLI_BIG_ENDIAN; + default: + return JPEGLI_NATIVE_ENDIAN; + } +} + +void ToFloatRow(const uint8_t* row_in, JxlPixelFormat format, size_t len, + float* row_out) { + bool is_little_endian = + (format.endianness == JXL_LITTLE_ENDIAN || + (format.endianness == JXL_NATIVE_ENDIAN && IsLittleEndian())); + static constexpr double kMul8 = 1.0 / 255.0; + static constexpr double kMul16 = 1.0 / 65535.0; + if (format.data_type == JXL_TYPE_UINT8) { + for (size_t x = 0; x < len; ++x) { + row_out[x] = row_in[x] * kMul8; + } + } else if (format.data_type == JXL_TYPE_UINT16 && is_little_endian) { + for (size_t x = 0; x < len; ++x) { + row_out[x] = LoadLE16(&row_in[2 * x]) * kMul16; + } + } else if (format.data_type == JXL_TYPE_UINT16 && !is_little_endian) { + for (size_t x = 0; x < len; ++x) { + row_out[x] = LoadBE16(&row_in[2 * x]) * kMul16; + } + } else if (format.data_type == JXL_TYPE_FLOAT && is_little_endian) { + for (size_t x = 0; x < len; ++x) { + row_out[x] = LoadLEFloat(&row_in[4 * x]); + } + } else if (format.data_type == JXL_TYPE_FLOAT && !is_little_endian) { + for (size_t x = 0; x < len; ++x) { + row_out[x] = LoadBEFloat(&row_in[4 * x]); + } + } +} + +Status EncodeJpegToTargetSize(const PackedPixelFile& ppf, + const JpegSettings& jpeg_settings, + size_t target_size, ThreadPool* pool, + std::vector<uint8_t>* output) { + output->clear(); + size_t best_error = std::numeric_limits<size_t>::max(); + float distance0 = -1.0f; + float distance1 = -1.0f; + float distance = 1.0f; + for (int step = 0; step < 15; ++step) { + JpegSettings settings = jpeg_settings; + settings.libjpeg_quality = 0; + settings.distance = distance; + settings.target_size = 0; + std::vector<uint8_t> compressed; + JXL_RETURN_IF_ERROR(EncodeJpeg(ppf, settings, pool, &compressed)); + size_t size = compressed.size(); + // prefer being under the target size to being over it + size_t error = size < target_size + ? target_size - size + : static_cast<size_t>(1.2f * (size - target_size)); + if (error < best_error) { + best_error = error; + std::swap(*output, compressed); + } + float rel_error = size * 1.0f / target_size; + if (std::abs(rel_error - 1.0f) < 0.002f) { + break; + } + if (size < target_size) { + distance1 = distance; + } else { + distance0 = distance; + } + if (distance1 == -1) { + distance *= std::pow(rel_error, 1.5) * 1.05; + } else if (distance0 == -1) { + distance *= std::pow(rel_error, 1.5) * 0.95; + } else { + distance = 0.5 * (distance0 + distance1); + } + } + return true; +} + +} // namespace + +Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings, + ThreadPool* pool, std::vector<uint8_t>* compressed) { + if (jpeg_settings.libjpeg_quality > 0) { + auto encoder = Encoder::FromExtension(".jpg"); + encoder->SetOption("q", std::to_string(jpeg_settings.libjpeg_quality)); + if (!jpeg_settings.libjpeg_chroma_subsampling.empty()) { + encoder->SetOption("chroma_subsampling", + jpeg_settings.libjpeg_chroma_subsampling); + } + EncodedImage encoded; + JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool)); + size_t target_size = encoded.bitstreams[0].size(); + return EncodeJpegToTargetSize(ppf, jpeg_settings, target_size, pool, + compressed); + } + if (jpeg_settings.target_size > 0) { + return EncodeJpegToTargetSize(ppf, jpeg_settings, jpeg_settings.target_size, + pool, compressed); + } + JXL_RETURN_IF_ERROR(VerifyInput(ppf)); + + ColorEncoding color_encoding; + JXL_RETURN_IF_ERROR(GetColorEncoding(ppf, &color_encoding)); + + ColorSpaceTransform c_transform(*JxlGetDefaultCms()); + ColorEncoding xyb_encoding; + if (jpeg_settings.xyb) { + if (ppf.info.num_color_channels != 3) { + return JXL_FAILURE("Only RGB input is supported in XYB mode."); + } + if (HasICCProfile(jpeg_settings.app_data)) { + return JXL_FAILURE("APP data ICC profile is not supported in XYB mode."); + } + const ColorEncoding& c_desired = ColorEncoding::LinearSRGB(false); + JXL_RETURN_IF_ERROR( + c_transform.Init(color_encoding, c_desired, 255.0f, ppf.info.xsize, 1)); + xyb_encoding.SetColorSpace(jxl::ColorSpace::kXYB); + xyb_encoding.SetRenderingIntent(jxl::RenderingIntent::kPerceptual); + JXL_RETURN_IF_ERROR(xyb_encoding.CreateICC()); + } + const ColorEncoding& output_encoding = + jpeg_settings.xyb ? xyb_encoding : color_encoding; + + // We need to declare all the non-trivial destructor local variables + // before the call to setjmp(). + std::vector<uint8_t> pixels; + unsigned char* output_buffer = nullptr; + unsigned long output_size = 0; + std::vector<uint8_t> row_bytes; + size_t rowlen = RoundUpTo(ppf.info.xsize, VectorSize()); + hwy::AlignedFreeUniquePtr<float[]> xyb_tmp = + hwy::AllocateAligned<float>(6 * rowlen); + hwy::AlignedFreeUniquePtr<float[]> premul_absorb = + hwy::AllocateAligned<float>(VectorSize() * 12); + ComputePremulAbsorb(255.0f, premul_absorb.get()); + + jpeg_compress_struct cinfo; + const auto try_catch_block = [&]() -> bool { + jpeg_error_mgr jerr; + jmp_buf env; + cinfo.err = jpegli_std_error(&jerr); + jerr.error_exit = &MyErrorExit; + if (setjmp(env)) { + return false; + } + cinfo.client_data = static_cast<void*>(&env); + jpegli_create_compress(&cinfo); + jpegli_mem_dest(&cinfo, &output_buffer, &output_size); + const JxlBasicInfo& info = ppf.info; + cinfo.image_width = info.xsize; + cinfo.image_height = info.ysize; + cinfo.input_components = info.num_color_channels; + cinfo.in_color_space = + cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB; + if (jpeg_settings.xyb) { + jpegli_set_xyb_mode(&cinfo); + } else if (jpeg_settings.use_std_quant_tables) { + jpegli_use_standard_quant_tables(&cinfo); + } + uint8_t cicp_tf = kUnknownTf; + if (!jpeg_settings.app_data.empty()) { + cicp_tf = LookupCICPTransferFunctionFromAppData( + jpeg_settings.app_data.data(), jpeg_settings.app_data.size()); + } else if (!output_encoding.IsSRGB()) { + cicp_tf = LookupCICPTransferFunctionFromICCProfile( + output_encoding.ICC().data(), output_encoding.ICC().size()); + } + jpegli_set_cicp_transfer_function(&cinfo, cicp_tf); + jpegli_set_defaults(&cinfo); + if (!jpeg_settings.chroma_subsampling.empty()) { + if (jpeg_settings.chroma_subsampling == "444") { + cinfo.comp_info[0].h_samp_factor = 1; + cinfo.comp_info[0].v_samp_factor = 1; + } else if (jpeg_settings.chroma_subsampling == "440") { + cinfo.comp_info[0].h_samp_factor = 1; + cinfo.comp_info[0].v_samp_factor = 2; + } else if (jpeg_settings.chroma_subsampling == "422") { + cinfo.comp_info[0].h_samp_factor = 2; + cinfo.comp_info[0].v_samp_factor = 1; + } else if (jpeg_settings.chroma_subsampling == "420") { + cinfo.comp_info[0].h_samp_factor = 2; + cinfo.comp_info[0].v_samp_factor = 2; + } else { + return false; + } + for (int i = 1; i < cinfo.num_components; ++i) { + cinfo.comp_info[i].h_samp_factor = 1; + cinfo.comp_info[i].v_samp_factor = 1; + } + } + jpegli_enable_adaptive_quantization( + &cinfo, jpeg_settings.use_adaptive_quantization); + if (jpeg_settings.psnr_target > 0.0) { + jpegli_set_psnr(&cinfo, jpeg_settings.psnr_target, + jpeg_settings.search_tolerance, + jpeg_settings.min_distance, jpeg_settings.max_distance); + } else if (jpeg_settings.quality > 0.0) { + float distance = jpegli_quality_to_distance(jpeg_settings.quality); + jpegli_set_distance(&cinfo, distance, TRUE); + } else { + jpegli_set_distance(&cinfo, jpeg_settings.distance, TRUE); + } + jpegli_set_progressive_level(&cinfo, jpeg_settings.progressive_level); + cinfo.optimize_coding = jpeg_settings.optimize_coding; + if (!jpeg_settings.app_data.empty()) { + // Make sure jpegli_start_compress() does not write any APP markers. + cinfo.write_JFIF_header = false; + cinfo.write_Adobe_marker = false; + } + const PackedImage& image = ppf.frames[0].color; + if (jpeg_settings.xyb) { + jpegli_set_input_format(&cinfo, JPEGLI_TYPE_FLOAT, JPEGLI_NATIVE_ENDIAN); + } else { + jpegli_set_input_format(&cinfo, ConvertDataType(image.format.data_type), + ConvertEndianness(image.format.endianness)); + } + jpegli_start_compress(&cinfo, TRUE); + if (!jpeg_settings.app_data.empty()) { + JXL_RETURN_IF_ERROR(WriteAppData(&cinfo, jpeg_settings.app_data)); + } + if ((jpeg_settings.app_data.empty() && !output_encoding.IsSRGB()) || + jpeg_settings.xyb) { + jpegli_write_icc_profile(&cinfo, output_encoding.ICC().data(), + output_encoding.ICC().size()); + } + const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels()); + if (jpeg_settings.xyb) { + float* src_buf = c_transform.BufSrc(0); + float* dst_buf = c_transform.BufDst(0); + for (size_t y = 0; y < image.ysize; ++y) { + // convert to float + ToFloatRow(&pixels[y * image.stride], image.format, 3 * image.xsize, + src_buf); + // convert to linear srgb + if (!c_transform.Run(0, src_buf, dst_buf)) { + return false; + } + // deinterleave channels + float* row0 = &xyb_tmp[0]; + float* row1 = &xyb_tmp[rowlen]; + float* row2 = &xyb_tmp[2 * rowlen]; + for (size_t x = 0; x < image.xsize; ++x) { + row0[x] = dst_buf[3 * x + 0]; + row1[x] = dst_buf[3 * x + 1]; + row2[x] = dst_buf[3 * x + 2]; + } + // convert to xyb + LinearRGBRowToXYB(row0, row1, row2, premul_absorb.get(), image.xsize); + // scale xyb + ScaleXYBRow(row0, row1, row2, image.xsize); + // interleave channels + float* row_out = &xyb_tmp[3 * rowlen]; + for (size_t x = 0; x < image.xsize; ++x) { + row_out[3 * x + 0] = row0[x]; + row_out[3 * x + 1] = row1[x]; + row_out[3 * x + 2] = row2[x]; + } + // feed to jpegli as native endian floats + JSAMPROW row[] = {reinterpret_cast<uint8_t*>(row_out)}; + jpegli_write_scanlines(&cinfo, row, 1); + } + } else { + row_bytes.resize(image.stride); + if (cinfo.num_components == (int)image.format.num_channels) { + for (size_t y = 0; y < info.ysize; ++y) { + memcpy(&row_bytes[0], pixels + y * image.stride, image.stride); + JSAMPROW row[] = {row_bytes.data()}; + jpegli_write_scanlines(&cinfo, row, 1); + } + } else { + for (size_t y = 0; y < info.ysize; ++y) { + int bytes_per_channel = + PackedImage::BitsPerChannel(image.format.data_type) / 8; + int bytes_per_pixel = cinfo.num_components * bytes_per_channel; + for (size_t x = 0; x < info.xsize; ++x) { + memcpy(&row_bytes[x * bytes_per_pixel], + &pixels[y * image.stride + x * image.pixel_stride()], + bytes_per_pixel); + } + JSAMPROW row[] = {row_bytes.data()}; + jpegli_write_scanlines(&cinfo, row, 1); + } + } + } + jpegli_finish_compress(&cinfo); + compressed->resize(output_size); + std::copy_n(output_buffer, output_size, compressed->data()); + return true; + }; + bool success = try_catch_block(); + jpegli_destroy_compress(&cinfo); + if (output_buffer) free(output_buffer); + return success; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/enc/jpegli.h b/lib/extras/enc/jpegli.h new file mode 100644 index 0000000..9538b2e --- /dev/null +++ b/lib/extras/enc/jpegli.h @@ -0,0 +1,53 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_ENC_JPEGLI_H_ +#define LIB_EXTRAS_ENC_JPEGLI_H_ + +// Encodes JPG pixels and metadata in memory using the libjpegli library. + +#include <stdint.h> + +#include <string> +#include <vector> + +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace extras { + +struct JpegSettings { + bool xyb = false; + size_t target_size = 0; + float quality = 0.0f; + float distance = 1.f; + bool use_adaptive_quantization = true; + bool use_std_quant_tables = false; + int progressive_level = 2; + bool optimize_coding = true; + std::string chroma_subsampling; + int libjpeg_quality = 0; + std::string libjpeg_chroma_subsampling; + // Parameters for selecting distance based on PSNR target. + float psnr_target = 0.0f; + float search_tolerance = 0.01; + float min_distance = 0.1f; + float max_distance = 25.0f; + // If not empty, must contain concatenated APP marker segments. In this case, + // these and only these APP marker segments will be written to the JPEG + // output. In xyb mode app_data must not contain an ICC profile, in this + // case an additional APP2 ICC profile for the XYB colorspace will be emitted. + std::vector<uint8_t> app_data; +}; + +Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings, + ThreadPool* pool, std::vector<uint8_t>* compressed); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ENC_JPEGLI_H_ diff --git a/lib/extras/enc/jpg.cc b/lib/extras/enc/jpg.cc index 93a39dd..f1355bb 100644 --- a/lib/extras/enc/jpg.cc +++ b/lib/extras/enc/jpg.cc @@ -5,12 +5,18 @@ #include "lib/extras/enc/jpg.h" +#if JPEGXL_ENABLE_JPEG #include <jpeglib.h> #include <setjmp.h> +#endif #include <stdint.h> #include <algorithm> +#include <array> +#include <cmath> +#include <fstream> #include <iterator> +#include <memory> #include <numeric> #include <sstream> #include <utility> @@ -21,11 +27,13 @@ #include "lib/jxl/sanitizers.h" #if JPEGXL_ENABLE_SJPEG #include "sjpeg.h" +#include "sjpegi.h" #endif namespace jxl { namespace extras { +#if JPEGXL_ENABLE_JPEG namespace { constexpr unsigned char kICCSignature[12] = { @@ -42,6 +50,142 @@ enum class JpegEncoder { kSJpeg, }; +#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0])) + +// Popular jpeg scan scripts +// The fields of the individual scans are: +// comps_in_scan, component_index[], Ss, Se, Ah, Al +static constexpr jpeg_scan_info kScanScript1[] = { + {1, {0}, 0, 0, 0, 0}, // + {1, {1}, 0, 0, 0, 0}, // + {1, {2}, 0, 0, 0, 0}, // + {1, {0}, 1, 8, 0, 0}, // + {1, {0}, 9, 63, 0, 0}, // + {1, {1}, 1, 63, 0, 0}, // + {1, {2}, 1, 63, 0, 0}, // +}; +static constexpr size_t kNumScans1 = ARRAY_SIZE(kScanScript1); + +static constexpr jpeg_scan_info kScanScript2[] = { + {1, {0}, 0, 0, 0, 0}, // + {1, {1}, 0, 0, 0, 0}, // + {1, {2}, 0, 0, 0, 0}, // + {1, {0}, 1, 2, 0, 1}, // + {1, {0}, 3, 63, 0, 1}, // + {1, {0}, 1, 63, 1, 0}, // + {1, {1}, 1, 63, 0, 0}, // + {1, {2}, 1, 63, 0, 0}, // +}; +static constexpr size_t kNumScans2 = ARRAY_SIZE(kScanScript2); + +static constexpr jpeg_scan_info kScanScript3[] = { + {1, {0}, 0, 0, 0, 0}, // + {1, {1}, 0, 0, 0, 0}, // + {1, {2}, 0, 0, 0, 0}, // + {1, {0}, 1, 63, 0, 2}, // + {1, {0}, 1, 63, 2, 1}, // + {1, {0}, 1, 63, 1, 0}, // + {1, {1}, 1, 63, 0, 0}, // + {1, {2}, 1, 63, 0, 0}, // +}; +static constexpr size_t kNumScans3 = ARRAY_SIZE(kScanScript3); + +static constexpr jpeg_scan_info kScanScript4[] = { + {3, {0, 1, 2}, 0, 0, 0, 1}, // + {1, {0}, 1, 5, 0, 2}, // + {1, {2}, 1, 63, 0, 1}, // + {1, {1}, 1, 63, 0, 1}, // + {1, {0}, 6, 63, 0, 2}, // + {1, {0}, 1, 63, 2, 1}, // + {3, {0, 1, 2}, 0, 0, 1, 0}, // + {1, {2}, 1, 63, 1, 0}, // + {1, {1}, 1, 63, 1, 0}, // + {1, {0}, 1, 63, 1, 0}, // +}; +static constexpr size_t kNumScans4 = ARRAY_SIZE(kScanScript4); + +static constexpr jpeg_scan_info kScanScript5[] = { + {3, {0, 1, 2}, 0, 0, 0, 1}, // + {1, {0}, 1, 5, 0, 2}, // + {1, {1}, 1, 5, 0, 2}, // + {1, {2}, 1, 5, 0, 2}, // + {1, {1}, 6, 63, 0, 2}, // + {1, {2}, 6, 63, 0, 2}, // + {1, {0}, 6, 63, 0, 2}, // + {1, {0}, 1, 63, 2, 1}, // + {1, {1}, 1, 63, 2, 1}, // + {1, {2}, 1, 63, 2, 1}, // + {3, {0, 1, 2}, 0, 0, 1, 0}, // + {1, {0}, 1, 63, 1, 0}, // + {1, {1}, 1, 63, 1, 0}, // + {1, {2}, 1, 63, 1, 0}, // +}; +static constexpr size_t kNumScans5 = ARRAY_SIZE(kScanScript5); + +// default progressive mode of jpegli +static constexpr jpeg_scan_info kScanScript6[] = { + {3, {0, 1, 2}, 0, 0, 0, 0}, // + {1, {0}, 1, 2, 0, 0}, // + {1, {1}, 1, 2, 0, 0}, // + {1, {2}, 1, 2, 0, 0}, // + {1, {0}, 3, 63, 0, 2}, // + {1, {1}, 3, 63, 0, 2}, // + {1, {2}, 3, 63, 0, 2}, // + {1, {0}, 3, 63, 2, 1}, // + {1, {1}, 3, 63, 2, 1}, // + {1, {2}, 3, 63, 2, 1}, // + {1, {0}, 3, 63, 1, 0}, // + {1, {1}, 3, 63, 1, 0}, // + {1, {2}, 3, 63, 1, 0}, // +}; +static constexpr size_t kNumScans6 = ARRAY_SIZE(kScanScript6); + +// Adapt RGB scan info to grayscale jpegs. +void FilterScanComponents(const jpeg_compress_struct* cinfo, + jpeg_scan_info* si) { + const int all_comps_in_scan = si->comps_in_scan; + si->comps_in_scan = 0; + for (int j = 0; j < all_comps_in_scan; ++j) { + const int component = si->component_index[j]; + if (component < cinfo->input_components) { + si->component_index[si->comps_in_scan++] = component; + } + } +} + +Status SetJpegProgression(int progressive_id, + std::vector<jpeg_scan_info>* scan_infos, + jpeg_compress_struct* cinfo) { + if (progressive_id < 0) { + return true; + } + if (progressive_id == 0) { + jpeg_simple_progression(cinfo); + return true; + } + constexpr const jpeg_scan_info* kScanScripts[] = {kScanScript1, kScanScript2, + kScanScript3, kScanScript4, + kScanScript5, kScanScript6}; + constexpr size_t kNumScans[] = {kNumScans1, kNumScans2, kNumScans3, + kNumScans4, kNumScans5, kNumScans6}; + if (progressive_id > static_cast<int>(ARRAY_SIZE(kNumScans))) { + return JXL_FAILURE("Unknown jpeg scan script id %d", progressive_id); + } + const jpeg_scan_info* scan_script = kScanScripts[progressive_id - 1]; + const size_t num_scans = kNumScans[progressive_id - 1]; + // filter scan script for number of components + for (size_t i = 0; i < num_scans; ++i) { + jpeg_scan_info scan_info = scan_script[i]; + FilterScanComponents(cinfo, &scan_info); + if (scan_info.comps_in_scan > 0) { + scan_infos->emplace_back(std::move(scan_info)); + } + } + cinfo->scan_info = scan_infos->data(); + cinfo->num_scans = scan_infos->size(); + return true; +} + bool IsSRGBEncoding(const JxlColorEncoding& c) { return ((c.color_space == JXL_COLOR_SPACE_RGB || c.color_space == JXL_COLOR_SPACE_GRAY) && @@ -106,18 +250,37 @@ Status SetChromaSubsampling(const std::string& subsampling, return false; } +struct JpegParams { + // Common between sjpeg and libjpeg + int quality = 100; + std::string chroma_subsampling = "444"; + // Libjpeg parameters + int progressive_id = -1; + bool optimize_coding = true; + bool is_xyb = false; + // Sjpeg parameters + int libjpeg_quality = 0; + std::string libjpeg_chroma_subsampling = "444"; + float psnr_target = 0; + std::string custom_base_quant_fn; + float search_q_start = 65.0f; + float search_q_min = 1.0f; + float search_q_max = 100.0f; + int search_max_iters = 20; + float search_tolerance = 0.1f; + float search_q_precision = 0.01f; + float search_first_iter_slope = 3.0f; + bool enable_adaptive_quant = true; +}; + Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info, const std::vector<uint8_t>& icc, - std::vector<uint8_t> exif, size_t quality, - const std::string& chroma_subsampling, + std::vector<uint8_t> exif, const JpegParams& params, std::vector<uint8_t>* bytes) { if (BITS_IN_JSAMPLE != 8 || sizeof(JSAMPLE) != 1) { return JXL_FAILURE("Only 8 bit JSAMPLE is supported."); } - jpeg_compress_struct cinfo; - // cinfo is initialized by libjpeg, which we are not instrumenting with - // msan. - msan::UnpoisonMemory(&cinfo, sizeof(cinfo)); + jpeg_compress_struct cinfo = {}; jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); @@ -129,11 +292,19 @@ Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info, cinfo.input_components = info.num_color_channels; cinfo.in_color_space = info.num_color_channels == 1 ? JCS_GRAYSCALE : JCS_RGB; jpeg_set_defaults(&cinfo); - cinfo.optimize_coding = TRUE; + cinfo.optimize_coding = params.optimize_coding; if (cinfo.input_components == 3) { - JXL_RETURN_IF_ERROR(SetChromaSubsampling(chroma_subsampling, &cinfo)); + JXL_RETURN_IF_ERROR( + SetChromaSubsampling(params.chroma_subsampling, &cinfo)); } - jpeg_set_quality(&cinfo, quality, TRUE); + if (params.is_xyb) { + // Tell libjpeg not to convert XYB data to YCbCr. + jpeg_set_colorspace(&cinfo, JCS_RGB); + } + jpeg_set_quality(&cinfo, params.quality, TRUE); + std::vector<jpeg_scan_info> scan_infos; + JXL_RETURN_IF_ERROR( + SetJpegProgression(params.progressive_id, &scan_infos, &cinfo)); jpeg_start_compress(&cinfo, TRUE); if (!icc.empty()) { WriteICCProfile(&cinfo, icc); @@ -145,13 +316,39 @@ Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info, if (cinfo.input_components > 3 || cinfo.input_components < 0) return JXL_FAILURE("invalid numbers of components"); - std::vector<uint8_t> raw_bytes(image.pixels_size); - memcpy(&raw_bytes[0], reinterpret_cast<const uint8_t*>(image.pixels()), - image.pixels_size); - for (size_t y = 0; y < info.ysize; ++y) { - JSAMPROW row[] = {raw_bytes.data() + y * image.stride}; - - jpeg_write_scanlines(&cinfo, row, 1); + std::vector<uint8_t> row_bytes(image.stride); + const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels()); + if (cinfo.num_components == (int)image.format.num_channels && + image.format.data_type == JXL_TYPE_UINT8) { + for (size_t y = 0; y < info.ysize; ++y) { + memcpy(&row_bytes[0], pixels + y * image.stride, image.stride); + JSAMPROW row[] = {row_bytes.data()}; + jpeg_write_scanlines(&cinfo, row, 1); + } + } else if (image.format.data_type == JXL_TYPE_UINT8) { + for (size_t y = 0; y < info.ysize; ++y) { + const uint8_t* image_row = pixels + y * image.stride; + for (size_t x = 0; x < info.xsize; ++x) { + const uint8_t* image_pixel = image_row + x * image.pixel_stride(); + memcpy(&row_bytes[x * cinfo.num_components], image_pixel, + cinfo.num_components); + } + JSAMPROW row[] = {row_bytes.data()}; + jpeg_write_scanlines(&cinfo, row, 1); + } + } else { + for (size_t y = 0; y < info.ysize; ++y) { + const uint8_t* image_row = pixels + y * image.stride; + for (size_t x = 0; x < info.xsize; ++x) { + const uint8_t* image_pixel = image_row + x * image.pixel_stride(); + for (int c = 0; c < cinfo.num_components; ++c) { + uint32_t val16 = (image_pixel[2 * c] << 8) + image_pixel[2 * c + 1]; + row_bytes[x * cinfo.num_components + c] = (val16 + 128) / 257; + } + } + JSAMPROW row[] = {row_bytes.data()}; + jpeg_write_scanlines(&cinfo, row, 1); + } } jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); @@ -164,15 +361,93 @@ Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info, return true; } +#if JPEGXL_ENABLE_SJPEG +struct MySearchHook : public sjpeg::SearchHook { + uint8_t base_tables[2][64]; + float q_start; + float q_precision; + float first_iter_slope; + void ReadBaseTables(const std::string& fn) { + const uint8_t kJPEGAnnexKMatrices[2][64] = { + {16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99}, + {17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99}}; + memcpy(base_tables[0], kJPEGAnnexKMatrices[0], sizeof(base_tables[0])); + memcpy(base_tables[1], kJPEGAnnexKMatrices[1], sizeof(base_tables[1])); + if (!fn.empty()) { + std::ifstream f(fn); + std::string line; + int idx = 0; + while (idx < 128 && std::getline(f, line)) { + if (line.empty() || line[0] == '#') continue; + std::istringstream line_stream(line); + std::string token; + while (idx < 128 && std::getline(line_stream, token, ',')) { + uint8_t val = std::stoi(token); + base_tables[idx / 64][idx % 64] = val; + idx++; + } + } + } + } + bool Setup(const sjpeg::EncoderParam& param) override { + sjpeg::SearchHook::Setup(param); + q = q_start; + return true; + } + void NextMatrix(int idx, uint8_t dst[64]) override { + float factor = (q <= 0) ? 5000.0f + : (q < 50.0f) ? 5000.0f / q + : (q < 100.0f) ? 2 * (100.0f - q) + : 0.0f; + sjpeg::SetQuantMatrix(base_tables[idx], factor, dst); + } + bool Update(float result) override { + value = result; + if (fabs(value - target) < tolerance * target) { + return true; + } + if (value > target) { + qmax = q; + } else { + qmin = q; + } + if (qmin == qmax) { + return true; + } + const float last_q = q; + if (pass == 0) { + q += first_iter_slope * + (for_size ? 0.1 * std::log(target / value) : (target - value)); + q = std::max(qmin, std::min(qmax, q)); + } else { + q = (qmin + qmax) / 2.; + } + return (pass > 0 && fabs(q - last_q) < q_precision); + } + ~MySearchHook() override {} +}; +#endif + Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info, const std::vector<uint8_t>& icc, - std::vector<uint8_t> exif, size_t quality, - const std::string& chroma_subsampling, + std::vector<uint8_t> exif, const JpegParams& params, std::vector<uint8_t>* bytes) { #if !JPEGXL_ENABLE_SJPEG return JXL_FAILURE("JPEG XL was built without sjpeg support"); #else - sjpeg::EncoderParam param(quality); + if (image.format.data_type != JXL_TYPE_UINT8) { + return JXL_FAILURE("Unsupported pixel data type"); + } + if (info.alpha_bits > 0) { + return JXL_FAILURE("alpha is not supported"); + } + sjpeg::EncoderParam param(params.quality); if (!icc.empty()) { param.iccp.assign(icc.begin(), icc.end()); } @@ -180,13 +455,43 @@ Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info, ResetExifOrientation(exif); param.exif.assign(exif.begin(), exif.end()); } - if (chroma_subsampling == "444") { + if (params.chroma_subsampling == "444") { param.yuv_mode = SJPEG_YUV_444; - } else if (chroma_subsampling == "420") { + } else if (params.chroma_subsampling == "420") { + param.yuv_mode = SJPEG_YUV_420; + } else if (params.chroma_subsampling == "420sharp") { param.yuv_mode = SJPEG_YUV_SHARP; } else { return JXL_FAILURE("sjpeg does not support this chroma subsampling mode"); } + param.adaptive_quantization = params.enable_adaptive_quant; + std::unique_ptr<MySearchHook> hook; + if (params.libjpeg_quality > 0) { + JpegParams libjpeg_params; + libjpeg_params.quality = params.libjpeg_quality; + libjpeg_params.chroma_subsampling = params.libjpeg_chroma_subsampling; + std::vector<uint8_t> libjpeg_bytes; + JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(image, info, icc, exif, + libjpeg_params, &libjpeg_bytes)); + param.target_mode = sjpeg::EncoderParam::TARGET_SIZE; + param.target_value = libjpeg_bytes.size(); + } + if (params.psnr_target > 0) { + param.target_mode = sjpeg::EncoderParam::TARGET_PSNR; + param.target_value = params.psnr_target; + } + if (param.target_mode != sjpeg::EncoderParam::TARGET_NONE) { + param.passes = params.search_max_iters; + param.tolerance = params.search_tolerance; + param.qmin = params.search_q_min; + param.qmax = params.search_q_max; + hook.reset(new MySearchHook()); + hook->ReadBaseTables(params.custom_base_quant_fn); + hook->q_start = params.search_q_start; + hook->q_precision = params.search_q_precision; + hook->first_iter_slope = params.search_first_iter_slope; + param.search_hook = hook.get(); + } size_t stride = info.xsize * 3; const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels()); std::string output; @@ -202,27 +507,20 @@ Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info, Status EncodeImageJPG(const PackedImage& image, const JxlBasicInfo& info, const std::vector<uint8_t>& icc, std::vector<uint8_t> exif, JpegEncoder encoder, - size_t quality, const std::string& chroma_subsampling, - ThreadPool* pool, std::vector<uint8_t>* bytes) { - if (image.format.data_type != JXL_TYPE_UINT8) { - return JXL_FAILURE("Unsupported pixel data type"); - } - if (info.alpha_bits > 0) { - return JXL_FAILURE("alpha is not supported"); - } - if (quality > 100) { + const JpegParams& params, ThreadPool* pool, + std::vector<uint8_t>* bytes) { + if (params.quality > 100) { return JXL_FAILURE("please specify a 0-100 JPEG quality"); } switch (encoder) { case JpegEncoder::kLibJpeg: - JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(image, info, icc, std::move(exif), - quality, chroma_subsampling, - bytes)); + JXL_RETURN_IF_ERROR( + EncodeWithLibJpeg(image, info, icc, std::move(exif), params, bytes)); break; case JpegEncoder::kSJpeg: - JXL_RETURN_IF_ERROR(EncodeWithSJpeg(image, info, icc, std::move(exif), - quality, chroma_subsampling, bytes)); + JXL_RETURN_IF_ERROR( + EncodeWithSJpeg(image, info, icc, std::move(exif), params, bytes)); break; default: return JXL_FAILURE("tried to use an unknown JPEG encoder"); @@ -234,43 +532,72 @@ Status EncodeImageJPG(const PackedImage& image, const JxlBasicInfo& info, class JPEGEncoder : public Encoder { std::vector<JxlPixelFormat> AcceptedFormats() const override { std::vector<JxlPixelFormat> formats; - for (const uint32_t num_channels : {1, 3}) { + for (const uint32_t num_channels : {1, 2, 3, 4}) { for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, /*data_type=*/JXL_TYPE_UINT8, /*endianness=*/endianness, /*align=*/0}); } + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/JXL_TYPE_UINT16, + /*endianness=*/JXL_BIG_ENDIAN, + /*align=*/0}); } return formats; } Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, ThreadPool* pool = nullptr) const override { JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); - const auto& options = this->options(); - int quality = 100; - auto it_quality = options.find("q"); - if (it_quality != options.end()) { - std::istringstream is(it_quality->second); - JXL_RETURN_IF_ERROR(static_cast<bool>(is >> quality)); - } - std::string chroma_subsampling = "444"; - auto it_chroma_subsampling = options.find("chroma_subsampling"); - if (it_chroma_subsampling != options.end()) { - chroma_subsampling = it_chroma_subsampling->second; - } JpegEncoder jpeg_encoder = JpegEncoder::kLibJpeg; - auto it_encoder = options.find("jpeg_encoder"); - if (it_encoder != options.end()) { - if (it_encoder->second == "libjpeg") { - jpeg_encoder = JpegEncoder::kLibJpeg; - } else if (it_encoder->second == "sjpeg") { - jpeg_encoder = JpegEncoder::kSJpeg; - } else { - return JXL_FAILURE("unknown jpeg encoder \"%s\"", - it_encoder->second.c_str()); + JpegParams params; + for (const auto& it : options()) { + if (it.first == "q") { + std::istringstream is(it.second); + JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.quality)); + } else if (it.first == "libjpeg_quality") { + std::istringstream is(it.second); + JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.libjpeg_quality)); + } else if (it.first == "chroma_subsampling") { + params.chroma_subsampling = it.second; + } else if (it.first == "libjpeg_chroma_subsampling") { + params.libjpeg_chroma_subsampling = it.second; + } else if (it.first == "jpeg_encoder") { + if (it.second == "libjpeg") { + jpeg_encoder = JpegEncoder::kLibJpeg; + } else if (it.second == "sjpeg") { + jpeg_encoder = JpegEncoder::kSJpeg; + } else { + return JXL_FAILURE("unknown jpeg encoder \"%s\"", it.second.c_str()); + } + } else if (it.first == "progressive") { + std::istringstream is(it.second); + JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.progressive_id)); + } else if (it.first == "optimize" && it.second == "OFF") { + params.optimize_coding = false; + } else if (it.first == "adaptive_q" && it.second == "OFF") { + params.enable_adaptive_quant = false; + } else if (it.first == "psnr") { + params.psnr_target = std::stof(it.second); + } else if (it.first == "base_quant_fn") { + params.custom_base_quant_fn = it.second; + } else if (it.first == "search_q_start") { + params.search_q_start = std::stof(it.second); + } else if (it.first == "search_q_min") { + params.search_q_min = std::stof(it.second); + } else if (it.first == "search_q_max") { + params.search_q_max = std::stof(it.second); + } else if (it.first == "search_max_iters") { + params.search_max_iters = std::stoi(it.second); + } else if (it.first == "search_tolerance") { + params.search_tolerance = std::stof(it.second); + } else if (it.first == "search_q_precision") { + params.search_q_precision = std::stof(it.second); + } else if (it.first == "search_first_iter_slope") { + params.search_first_iter_slope = std::stof(it.second); } } + params.is_xyb = (ppf.color_encoding.color_space == JXL_COLOR_SPACE_XYB); std::vector<uint8_t> icc; if (!IsSRGBEncoding(ppf.color_encoding)) { icc = ppf.icc; @@ -281,17 +608,22 @@ class JPEGEncoder : public Encoder { JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); encoded_image->bitstreams.emplace_back(); JXL_RETURN_IF_ERROR(EncodeImageJPG( - frame.color, ppf.info, icc, ppf.metadata.exif, jpeg_encoder, quality, - chroma_subsampling, pool, &encoded_image->bitstreams.back())); + frame.color, ppf.info, icc, ppf.metadata.exif, jpeg_encoder, params, + pool, &encoded_image->bitstreams.back())); } return true; } }; } // namespace +#endif std::unique_ptr<Encoder> GetJPEGEncoder() { +#if JPEGXL_ENABLE_JPEG return jxl::make_unique<JPEGEncoder>(); +#else + return nullptr; +#endif } } // namespace extras diff --git a/lib/extras/enc/jxl.cc b/lib/extras/enc/jxl.cc new file mode 100644 index 0000000..054d15e --- /dev/null +++ b/lib/extras/enc/jxl.cc @@ -0,0 +1,359 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/enc/jxl.h" + +#include <jxl/encode.h> +#include <jxl/encode_cxx.h> + +#include "lib/jxl/exif.h" + +namespace jxl { +namespace extras { + +JxlEncoderStatus SetOption(const JXLOption& opt, + JxlEncoderFrameSettings* settings) { + return opt.is_float + ? JxlEncoderFrameSettingsSetFloatOption(settings, opt.id, opt.fval) + : JxlEncoderFrameSettingsSetOption(settings, opt.id, opt.ival); +} + +bool SetFrameOptions(const std::vector<JXLOption>& options, size_t frame_index, + size_t* option_idx, JxlEncoderFrameSettings* settings) { + while (*option_idx < options.size()) { + const auto& opt = options[*option_idx]; + if (opt.frame_index > frame_index) { + break; + } + if (JXL_ENC_SUCCESS != SetOption(opt, settings)) { + fprintf(stderr, "Setting option id %d failed.\n", opt.id); + return false; + } + (*option_idx)++; + } + return true; +} + +bool SetupFrame(JxlEncoder* enc, JxlEncoderFrameSettings* settings, + const JxlFrameHeader& frame_header, + const JXLCompressParams& params, const PackedPixelFile& ppf, + size_t frame_index, size_t num_alpha_channels, + size_t num_interleaved_alpha, size_t& option_idx) { + if (JXL_ENC_SUCCESS != JxlEncoderSetFrameHeader(settings, &frame_header)) { + fprintf(stderr, "JxlEncoderSetFrameHeader() failed.\n"); + return false; + } + if (!SetFrameOptions(params.options, frame_index, &option_idx, settings)) { + return false; + } + if (num_alpha_channels > 0) { + JxlExtraChannelInfo extra_channel_info; + JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &extra_channel_info); + extra_channel_info.bits_per_sample = ppf.info.alpha_bits; + extra_channel_info.exponent_bits_per_sample = ppf.info.alpha_exponent_bits; + if (params.premultiply != -1) { + if (params.premultiply != 0 && params.premultiply != 1) { + fprintf(stderr, "premultiply must be one of: -1, 0, 1.\n"); + return false; + } + extra_channel_info.alpha_premultiplied = params.premultiply; + } + if (JXL_ENC_SUCCESS != + JxlEncoderSetExtraChannelInfo(enc, 0, &extra_channel_info)) { + fprintf(stderr, "JxlEncoderSetExtraChannelInfo() failed.\n"); + return false; + } + // We take the extra channel blend info frame_info, but don't do + // clamping. + JxlBlendInfo extra_channel_blend_info = frame_header.layer_info.blend_info; + extra_channel_blend_info.clamp = JXL_FALSE; + JxlEncoderSetExtraChannelBlendInfo(settings, 0, &extra_channel_blend_info); + } + // Add extra channel info for the rest of the extra channels. + for (size_t i = 0; i < ppf.info.num_extra_channels; ++i) { + if (i < ppf.extra_channels_info.size()) { + const auto& ec_info = ppf.extra_channels_info[i].ec_info; + if (JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelInfo( + enc, num_interleaved_alpha + i, &ec_info)) { + fprintf(stderr, "JxlEncoderSetExtraChannelInfo() failed.\n"); + return false; + } + } + } + return true; +} + +bool ReadCompressedOutput(JxlEncoder* enc, std::vector<uint8_t>* compressed) { + compressed->clear(); + compressed->resize(4096); + uint8_t* next_out = compressed->data(); + size_t avail_out = compressed->size() - (next_out - compressed->data()); + JxlEncoderStatus result = JXL_ENC_NEED_MORE_OUTPUT; + while (result == JXL_ENC_NEED_MORE_OUTPUT) { + result = JxlEncoderProcessOutput(enc, &next_out, &avail_out); + if (result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed->data(); + compressed->resize(compressed->size() * 2); + next_out = compressed->data() + offset; + avail_out = compressed->size() - offset; + } + } + compressed->resize(next_out - compressed->data()); + if (result != JXL_ENC_SUCCESS) { + fprintf(stderr, "JxlEncoderProcessOutput failed.\n"); + return false; + } + return true; +} + +bool EncodeImageJXL(const JXLCompressParams& params, const PackedPixelFile& ppf, + const std::vector<uint8_t>* jpeg_bytes, + std::vector<uint8_t>* compressed) { + auto encoder = JxlEncoderMake(/*memory_manager=*/nullptr); + JxlEncoder* enc = encoder.get(); + + if (params.allow_expert_options) { + JxlEncoderAllowExpertOptions(enc); + } + + if (params.runner_opaque != nullptr && + JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc, params.runner, + params.runner_opaque)) { + fprintf(stderr, "JxlEncoderSetParallelRunner failed\n"); + return false; + } + + if (params.HasOutputProcessor() && + JXL_ENC_SUCCESS != + JxlEncoderSetOutputProcessor(enc, params.output_processor)) { + fprintf(stderr, "JxlEncoderSetOutputProcessorfailed\n"); + return false; + } + + auto settings = JxlEncoderFrameSettingsCreate(enc, nullptr); + size_t option_idx = 0; + if (!SetFrameOptions(params.options, 0, &option_idx, settings)) { + return false; + } + if (JXL_ENC_SUCCESS != + JxlEncoderSetFrameDistance(settings, params.distance)) { + fprintf(stderr, "Setting frame distance failed.\n"); + return false; + } + if (params.debug_image) { + JxlEncoderSetDebugImageCallback(settings, params.debug_image, + params.debug_image_opaque); + } + if (params.stats) { + JxlEncoderCollectStats(settings, params.stats); + } + + bool use_boxes = !ppf.metadata.exif.empty() || !ppf.metadata.xmp.empty() || + !ppf.metadata.jumbf.empty() || !ppf.metadata.iptc.empty(); + bool use_container = params.use_container || use_boxes || + (jpeg_bytes && params.jpeg_store_metadata); + + if (JXL_ENC_SUCCESS != + JxlEncoderUseContainer(enc, static_cast<int>(use_container))) { + fprintf(stderr, "JxlEncoderUseContainer failed.\n"); + return false; + } + + if (jpeg_bytes) { + if (params.jpeg_store_metadata && + JXL_ENC_SUCCESS != JxlEncoderStoreJPEGMetadata(enc, JXL_TRUE)) { + fprintf(stderr, "Storing JPEG metadata failed.\n"); + return false; + } + if (!params.jpeg_store_metadata && params.jpeg_strip_exif) { + JxlEncoderFrameSettingsSetOption(settings, + JXL_ENC_FRAME_SETTING_JPEG_KEEP_EXIF, 0); + } + if (!params.jpeg_store_metadata && params.jpeg_strip_xmp) { + JxlEncoderFrameSettingsSetOption(settings, + JXL_ENC_FRAME_SETTING_JPEG_KEEP_XMP, 0); + } + if (params.jpeg_strip_jumbf) { + JxlEncoderFrameSettingsSetOption( + settings, JXL_ENC_FRAME_SETTING_JPEG_KEEP_JUMBF, 0); + } + if (JXL_ENC_SUCCESS != JxlEncoderAddJPEGFrame(settings, jpeg_bytes->data(), + jpeg_bytes->size())) { + JxlEncoderError error = JxlEncoderGetError(enc); + if (error == JXL_ENC_ERR_BAD_INPUT) { + fprintf(stderr, + "Error while decoding the JPEG image. It may be corrupt (e.g. " + "truncated) or of an unsupported type (e.g. CMYK).\n"); + } else if (error == JXL_ENC_ERR_JBRD) { + fprintf(stderr, + "JPEG bitstream reconstruction data could not be created. " + "Possibly there is too much tail data.\n" + "Try using --jpeg_store_metadata 0, to losslessly " + "recompress the JPEG image data without bitstream " + "reconstruction data.\n"); + } else { + fprintf(stderr, "JxlEncoderAddJPEGFrame() failed.\n"); + } + return false; + } + } else { + size_t num_alpha_channels = 0; // Adjusted below. + JxlBasicInfo basic_info = ppf.info; + basic_info.xsize *= params.already_downsampled; + basic_info.ysize *= params.already_downsampled; + if (basic_info.alpha_bits > 0) num_alpha_channels = 1; + if (params.intensity_target > 0) { + basic_info.intensity_target = params.intensity_target; + } + basic_info.num_extra_channels = + std::max<uint32_t>(num_alpha_channels, ppf.info.num_extra_channels); + basic_info.num_color_channels = ppf.info.num_color_channels; + const bool lossless = params.distance == 0; + basic_info.uses_original_profile = lossless; + if (params.override_bitdepth != 0) { + basic_info.bits_per_sample = params.override_bitdepth; + basic_info.exponent_bits_per_sample = + params.override_bitdepth == 32 ? 8 : 0; + } + if (JXL_ENC_SUCCESS != + JxlEncoderSetCodestreamLevel(enc, params.codestream_level)) { + fprintf(stderr, "Setting --codestream_level failed.\n"); + return false; + } + if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc, &basic_info)) { + fprintf(stderr, "JxlEncoderSetBasicInfo() failed.\n"); + return false; + } + if (JXL_ENC_SUCCESS != + JxlEncoderSetUpsamplingMode(enc, params.already_downsampled, + params.upsampling_mode)) { + fprintf(stderr, "JxlEncoderSetUpsamplingMode() failed.\n"); + return false; + } + if (JXL_ENC_SUCCESS != + JxlEncoderSetFrameBitDepth(settings, ¶ms.input_bitdepth)) { + fprintf(stderr, "JxlEncoderSetFrameBitDepth() failed.\n"); + return false; + } + if (num_alpha_channels != 0 && + JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelDistance( + settings, 0, params.alpha_distance)) { + fprintf(stderr, "Setting alpha distance failed.\n"); + return false; + } + if (lossless && + JXL_ENC_SUCCESS != JxlEncoderSetFrameLossless(settings, JXL_TRUE)) { + fprintf(stderr, "JxlEncoderSetFrameLossless() failed.\n"); + return false; + } + if (!ppf.icc.empty()) { + if (JXL_ENC_SUCCESS != + JxlEncoderSetICCProfile(enc, ppf.icc.data(), ppf.icc.size())) { + fprintf(stderr, "JxlEncoderSetICCProfile() failed.\n"); + return false; + } + } else { + if (JXL_ENC_SUCCESS != + JxlEncoderSetColorEncoding(enc, &ppf.color_encoding)) { + fprintf(stderr, "JxlEncoderSetColorEncoding() failed.\n"); + return false; + } + } + + if (use_boxes) { + if (JXL_ENC_SUCCESS != JxlEncoderUseBoxes(enc)) { + fprintf(stderr, "JxlEncoderUseBoxes() failed.\n"); + return false; + } + // Prepend 4 zero bytes to exif for tiff header offset + std::vector<uint8_t> exif_with_offset; + bool bigendian; + if (IsExif(ppf.metadata.exif, &bigendian)) { + exif_with_offset.resize(ppf.metadata.exif.size() + 4); + memcpy(exif_with_offset.data() + 4, ppf.metadata.exif.data(), + ppf.metadata.exif.size()); + } + const struct BoxInfo { + const char* type; + const std::vector<uint8_t>& bytes; + } boxes[] = { + {"Exif", exif_with_offset}, + {"xml ", ppf.metadata.xmp}, + {"jumb", ppf.metadata.jumbf}, + {"xml ", ppf.metadata.iptc}, + }; + for (size_t i = 0; i < sizeof boxes / sizeof *boxes; ++i) { + const BoxInfo& box = boxes[i]; + if (!box.bytes.empty() && + JXL_ENC_SUCCESS != JxlEncoderAddBox(enc, box.type, box.bytes.data(), + box.bytes.size(), + params.compress_boxes)) { + fprintf(stderr, "JxlEncoderAddBox() failed (%s).\n", box.type); + return false; + } + } + JxlEncoderCloseBoxes(enc); + } + + for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) { + const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame]; + const jxl::extras::PackedImage& pimage = pframe.color; + JxlPixelFormat ppixelformat = pimage.format; + size_t num_interleaved_alpha = + (ppixelformat.num_channels - ppf.info.num_color_channels); + if (!SetupFrame(enc, settings, pframe.frame_info, params, ppf, num_frame, + num_alpha_channels, num_interleaved_alpha, option_idx)) { + return false; + } + if (JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(settings, &ppixelformat, + pimage.pixels(), + pimage.pixels_size)) { + fprintf(stderr, "JxlEncoderAddImageFrame() failed.\n"); + return false; + } + // Only set extra channel buffer if it is provided non-interleaved. + for (size_t i = 0; i < pframe.extra_channels.size(); ++i) { + if (JXL_ENC_SUCCESS != + JxlEncoderSetExtraChannelBuffer(settings, &ppixelformat, + pframe.extra_channels[i].pixels(), + pframe.extra_channels[i].stride * + pframe.extra_channels[i].ysize, + num_interleaved_alpha + i)) { + fprintf(stderr, "JxlEncoderSetExtraChannelBuffer() failed.\n"); + return false; + } + } + } + for (size_t fi = 0; fi < ppf.chunked_frames.size(); ++fi) { + ChunkedPackedFrame& chunked_frame = ppf.chunked_frames[fi]; + size_t num_interleaved_alpha = + (chunked_frame.format.num_channels - ppf.info.num_color_channels); + if (!SetupFrame(enc, settings, chunked_frame.frame_info, params, ppf, fi, + num_alpha_channels, num_interleaved_alpha, option_idx)) { + return false; + } + const bool last_frame = fi + 1 == ppf.chunked_frames.size(); + if (JXL_ENC_SUCCESS != + JxlEncoderAddChunkedFrame(settings, last_frame, + chunked_frame.GetInputSource())) { + fprintf(stderr, "JxlEncoderAddChunkedFrame() failed.\n"); + return false; + } + } + } + JxlEncoderCloseInput(enc); + if (params.HasOutputProcessor()) { + if (JXL_ENC_SUCCESS != JxlEncoderFlushInput(enc)) { + fprintf(stderr, "JxlEncoderAddChunkedFrame() failed.\n"); + return false; + } + } else if (!ReadCompressedOutput(enc, compressed)) { + return false; + } + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/enc/jxl.h b/lib/extras/enc/jxl.h new file mode 100644 index 0000000..b8ca5bd --- /dev/null +++ b/lib/extras/enc/jxl.h @@ -0,0 +1,91 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_ENC_JXL_H_ +#define LIB_EXTRAS_ENC_JXL_H_ + +#include <jxl/encode.h> +#include <jxl/parallel_runner.h> +#include <jxl/thread_parallel_runner.h> +#include <jxl/types.h> +#include <stdint.h> + +#include <vector> + +#include "lib/extras/packed_image.h" + +namespace jxl { +namespace extras { + +struct JXLOption { + JXLOption(JxlEncoderFrameSettingId id, int64_t val, size_t frame_index) + : id(id), is_float(false), ival(val), frame_index(frame_index) {} + JXLOption(JxlEncoderFrameSettingId id, float val, size_t frame_index) + : id(id), is_float(true), fval(val), frame_index(frame_index) {} + + JxlEncoderFrameSettingId id; + bool is_float; + union { + int64_t ival; + float fval; + }; + size_t frame_index; +}; + +struct JXLCompressParams { + std::vector<JXLOption> options; + // Target butteraugli distance, 0.0 means lossless. + float distance = 1.0f; + float alpha_distance = 1.0f; + // If set to true, forces container mode. + bool use_container = false; + // Whether to enable/disable byte-exact jpeg reconstruction for jpeg inputs. + bool jpeg_store_metadata = true; + bool jpeg_strip_exif = false; + bool jpeg_strip_xmp = false; + bool jpeg_strip_jumbf = false; + // Whether to create brob boxes. + bool compress_boxes = true; + // Upper bound on the intensity level present in the image in nits (zero means + // that the library chooses a default). + float intensity_target = 0; + int already_downsampled = 1; + int upsampling_mode = -1; + // Overrides for bitdepth, codestream level and alpha premultiply. + size_t override_bitdepth = 0; + int32_t codestream_level = -1; + int32_t premultiply = -1; + // Override input buffer interpretation. + JxlBitDepth input_bitdepth = {JXL_BIT_DEPTH_FROM_PIXEL_FORMAT, 0, 0}; + // If runner_opaque is set, the decoder uses this parallel runner. + JxlParallelRunner runner = JxlThreadParallelRunner; + void* runner_opaque = nullptr; + JxlEncoderOutputProcessor output_processor = {}; + JxlDebugImageCallback debug_image = nullptr; + void* debug_image_opaque = nullptr; + JxlEncoderStats* stats = nullptr; + bool allow_expert_options = false; + + void AddOption(JxlEncoderFrameSettingId id, int64_t val) { + options.emplace_back(JXLOption(id, val, 0)); + } + void AddFloatOption(JxlEncoderFrameSettingId id, float val) { + options.emplace_back(JXLOption(id, val, 0)); + } + bool HasOutputProcessor() const { + return (output_processor.get_buffer != nullptr && + output_processor.release_buffer != nullptr && + output_processor.set_finalized_position != nullptr); + } +}; + +bool EncodeImageJXL(const JXLCompressParams& params, const PackedPixelFile& ppf, + const std::vector<uint8_t>* jpeg_bytes, + std::vector<uint8_t>* compressed); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ENC_JXL_H_ diff --git a/lib/extras/enc/npy.cc b/lib/extras/enc/npy.cc index 1428e64..ae8cf13 100644 --- a/lib/extras/enc/npy.cc +++ b/lib/extras/enc/npy.cc @@ -5,13 +5,12 @@ #include "lib/extras/enc/npy.h" -#include <stdio.h> +#include <jxl/types.h> #include <sstream> #include <string> #include <vector> -#include "jxl/types.h" #include "lib/extras/packed_image.h" namespace jxl { diff --git a/lib/extras/enc/pgx.cc b/lib/extras/enc/pgx.cc index ef204ad..d4809e3 100644 --- a/lib/extras/enc/pgx.cc +++ b/lib/extras/enc/pgx.cc @@ -5,13 +5,11 @@ #include "lib/extras/enc/pgx.h" -#include <stdio.h> +#include <jxl/codestream_header.h> #include <string.h> -#include "jxl/codestream_header.h" #include "lib/extras/packed_image.h" #include "lib/jxl/base/byte_order.h" -#include "lib/jxl/base/printf_macros.h" namespace jxl { namespace extras { diff --git a/lib/extras/enc/pnm.cc b/lib/extras/enc/pnm.cc index 9b5f6cb..4183900 100644 --- a/lib/extras/enc/pnm.cc +++ b/lib/extras/enc/pnm.cc @@ -5,7 +5,6 @@ #include "lib/extras/enc/pnm.h" -#include <stdio.h> #include <string.h> #include <string> @@ -14,12 +13,9 @@ #include "lib/extras/packed_image.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/compiler_specific.h" -#include "lib/jxl/base/file_io.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/base/status.h" -#include "lib/jxl/color_management.h" #include "lib/jxl/dec_external_image.h" -#include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_external_image.h" #include "lib/jxl/enc_image_bundle.h" #include "lib/jxl/fields.h" // AllDefault @@ -32,66 +28,7 @@ namespace { constexpr size_t kMaxHeaderSize = 200; -Status EncodeHeader(const PackedImage& image, size_t bits_per_sample, - bool little_endian, char* header, int* chars_written) { - size_t num_channels = image.format.num_channels; - bool is_gray = num_channels <= 2; - bool has_alpha = num_channels == 2 || num_channels == 4; - if (has_alpha) { // PAM - if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits"); - const uint32_t max_val = (1U << bits_per_sample) - 1; - *chars_written = - snprintf(header, kMaxHeaderSize, - "P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS - "\nDEPTH %u\nMAXVAL %u\nTUPLTYPE %s\nENDHDR\n", - image.xsize, image.ysize, is_gray ? 2 : 4, max_val, - is_gray ? "GRAYSCALE_ALPHA" : "RGB_ALPHA"); - JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < - kMaxHeaderSize); - } else if (bits_per_sample == 32) { // PFM - const char type = is_gray ? 'f' : 'F'; - const double scale = little_endian ? -1.0 : 1.0; - *chars_written = - snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n", - type, image.xsize, image.ysize, scale); - JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < - kMaxHeaderSize); - } else { // PGM/PPM - if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits"); - const uint32_t max_val = (1U << bits_per_sample) - 1; - const char type = is_gray ? '5' : '6'; - *chars_written = - snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n", - type, image.xsize, image.ysize, max_val); - JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < - kMaxHeaderSize); - } - return true; -} - -Status EncodeImagePNM(const PackedImage& image, size_t bits_per_sample, - std::vector<uint8_t>* bytes) { - // Choose native for PFM; PGM/PPM require big-endian - bool is_little_endian = bits_per_sample > 16 && IsLittleEndian(); - char header[kMaxHeaderSize]; - int header_size = 0; - JXL_RETURN_IF_ERROR(EncodeHeader(image, bits_per_sample, is_little_endian, - header, &header_size)); - bytes->resize(static_cast<size_t>(header_size) + image.pixels_size); - memcpy(bytes->data(), header, static_cast<size_t>(header_size)); - const bool flipped_y = bits_per_sample == 32; // PFMs are flipped - const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels()); - uint8_t* out = bytes->data() + header_size; - for (size_t y = 0; y < image.ysize; ++y) { - size_t y_out = flipped_y ? image.ysize - 1 - y : y; - const uint8_t* row_in = &in[y * image.stride]; - uint8_t* row_out = &out[y_out * image.stride]; - memcpy(row_out, row_in, image.stride); - } - return true; -} - -class PNMEncoder : public Encoder { +class BasePNMEncoder : public Encoder { public: Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, ThreadPool* pool = nullptr) const override { @@ -106,8 +43,8 @@ class PNMEncoder : public Encoder { for (const auto& frame : ppf.frames) { JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); encoded_image->bitstreams.emplace_back(); - JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.color, ppf.info.bits_per_sample, - &encoded_image->bitstreams.back())); + JXL_RETURN_IF_ERROR( + EncodeFrame(ppf, frame, &encoded_image->bitstreams.back())); } for (size_t i = 0; i < ppf.extra_channels_info.size(); ++i) { const auto& ec_info = ppf.extra_channels_info[i].ec_info; @@ -115,85 +52,258 @@ class PNMEncoder : public Encoder { auto& ec_bitstreams = encoded_image->extra_channel_bitstreams.back(); for (const auto& frame : ppf.frames) { ec_bitstreams.emplace_back(); - JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.extra_channels[i], - ec_info.bits_per_sample, - &ec_bitstreams.back())); + JXL_RETURN_IF_ERROR(EncodeExtraChannel(frame.extra_channels[i], + ec_info.bits_per_sample, + &ec_bitstreams.back())); } } return true; } + + protected: + virtual Status EncodeFrame(const PackedPixelFile& ppf, + const PackedFrame& frame, + std::vector<uint8_t>* bytes) const = 0; + virtual Status EncodeExtraChannel(const PackedImage& image, + size_t bits_per_sample, + std::vector<uint8_t>* bytes) const = 0; }; +class PNMEncoder : public BasePNMEncoder { + public: + static const std::vector<JxlPixelFormat> kAcceptedFormats; + + std::vector<JxlPixelFormat> AcceptedFormats() const override { + return kAcceptedFormats; + } + + Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame, + std::vector<uint8_t>* bytes) const override { + return EncodeImage(frame.color, ppf.info.bits_per_sample, bytes); + } + Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) const override { + return EncodeImage(image, bits_per_sample, bytes); + } + + private: + Status EncodeImage(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) const { + uint32_t maxval = (1u << bits_per_sample) - 1; + char type = image.format.num_channels == 1 ? '5' : '6'; + char header[kMaxHeaderSize]; + size_t header_size = + snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n", + type, image.xsize, image.ysize, maxval); + JXL_RETURN_IF_ERROR(header_size < kMaxHeaderSize); + bytes->resize(header_size + image.pixels_size); + memcpy(bytes->data(), header, header_size); + memcpy(bytes->data() + header_size, + reinterpret_cast<uint8_t*>(image.pixels()), image.pixels_size); + return true; + } +}; + +class PGMEncoder : public PNMEncoder { + public: + static const std::vector<JxlPixelFormat> kAcceptedFormats; + + std::vector<JxlPixelFormat> AcceptedFormats() const override { + return kAcceptedFormats; + } +}; + +const std::vector<JxlPixelFormat> PGMEncoder::kAcceptedFormats = { + JxlPixelFormat{1, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0}, + JxlPixelFormat{1, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}}; + class PPMEncoder : public PNMEncoder { public: + static const std::vector<JxlPixelFormat> kAcceptedFormats; + std::vector<JxlPixelFormat> AcceptedFormats() const override { - std::vector<JxlPixelFormat> formats; - for (const uint32_t num_channels : {1, 2, 3, 4}) { - for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { - for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { - formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, - /*data_type=*/data_type, - /*endianness=*/endianness, - /*align=*/0}); - } - } - } - return formats; + return kAcceptedFormats; } }; -class PFMEncoder : public PNMEncoder { +const std::vector<JxlPixelFormat> PPMEncoder::kAcceptedFormats = { + JxlPixelFormat{3, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0}, + JxlPixelFormat{3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}}; + +const std::vector<JxlPixelFormat> PNMEncoder::kAcceptedFormats = [] { + std::vector<JxlPixelFormat> combined = PPMEncoder::kAcceptedFormats; + combined.insert(combined.end(), PGMEncoder::kAcceptedFormats.begin(), + PGMEncoder::kAcceptedFormats.end()); + return combined; +}(); + +class PFMEncoder : public BasePNMEncoder { public: std::vector<JxlPixelFormat> AcceptedFormats() const override { std::vector<JxlPixelFormat> formats; for (const uint32_t num_channels : {1, 3}) { - for (const JxlDataType data_type : {JXL_TYPE_FLOAT16, JXL_TYPE_FLOAT}) { - for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { - formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, - /*data_type=*/data_type, - /*endianness=*/endianness, - /*align=*/0}); - } + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/JXL_TYPE_FLOAT, + /*endianness=*/endianness, + /*align=*/0}); } } return formats; } -}; + Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame, + std::vector<uint8_t>* bytes) const override { + return EncodeImage(frame.color, bytes); + } + Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) const override { + return EncodeImage(image, bytes); + } -class PGMEncoder : public PPMEncoder { - public: - std::vector<JxlPixelFormat> AcceptedFormats() const override { - std::vector<JxlPixelFormat> formats = PPMEncoder::AcceptedFormats(); - for (auto it = formats.begin(); it != formats.end();) { - if (it->num_channels > 2) { - it = formats.erase(it); - } else { - ++it; - } + private: + Status EncodeImage(const PackedImage& image, + std::vector<uint8_t>* bytes) const { + char type = image.format.num_channels == 1 ? 'f' : 'F'; + double scale = image.format.endianness == JXL_LITTLE_ENDIAN ? -1.0 : 1.0; + char header[kMaxHeaderSize]; + size_t header_size = + snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n", + type, image.xsize, image.ysize, scale); + JXL_RETURN_IF_ERROR(header_size < kMaxHeaderSize); + bytes->resize(header_size + image.pixels_size); + memcpy(bytes->data(), header, header_size); + const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels()); + uint8_t* out = bytes->data() + header_size; + for (size_t y = 0; y < image.ysize; ++y) { + size_t y_out = image.ysize - 1 - y; + const uint8_t* row_in = &in[y * image.stride]; + uint8_t* row_out = &out[y_out * image.stride]; + memcpy(row_out, row_in, image.stride); } - return formats; + return true; } }; -class PAMEncoder : public PPMEncoder { +class PAMEncoder : public BasePNMEncoder { public: std::vector<JxlPixelFormat> AcceptedFormats() const override { - std::vector<JxlPixelFormat> formats = PPMEncoder::AcceptedFormats(); - for (auto it = formats.begin(); it != formats.end();) { - if (it->num_channels != 2 && it->num_channels != 4) { - it = formats.erase(it); - } else { - ++it; + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 2, 3, 4}) { + for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/data_type, + /*endianness=*/JXL_BIG_ENDIAN, + /*align=*/0}); } } return formats; } -}; + Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame, + std::vector<uint8_t>* bytes) const override { + const PackedImage& color = frame.color; + const auto& ec_info = ppf.extra_channels_info; + JXL_RETURN_IF_ERROR(frame.extra_channels.size() == ec_info.size()); + for (const auto& ec : frame.extra_channels) { + if (ec.xsize != color.xsize || ec.ysize != color.ysize) { + return JXL_FAILURE("Extra channel and color size mismatch."); + } + if (ec.format.data_type != color.format.data_type || + ec.format.endianness != color.format.endianness) { + return JXL_FAILURE("Extra channel and color format mismatch."); + } + } + if (ppf.info.alpha_bits && + (ppf.info.bits_per_sample != ppf.info.alpha_bits)) { + return JXL_FAILURE("Alpha bit depth does not match image bit depth"); + } + for (const auto& it : ec_info) { + if (it.ec_info.bits_per_sample != ppf.info.bits_per_sample) { + return JXL_FAILURE( + "Extra channel bit depth does not match image bit depth"); + } + } + const char* kColorTypes[4] = {"GRAYSCALE", "GRAYSCALE_ALPHA", "RGB", + "RGB_ALPHA"}; + uint32_t maxval = (1u << ppf.info.bits_per_sample) - 1; + uint32_t depth = color.format.num_channels + ec_info.size(); + char header[kMaxHeaderSize]; + size_t pos = 0; + pos += snprintf(header + pos, kMaxHeaderSize - pos, + "P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS + "\nDEPTH %u\n" + "MAXVAL %u\nTUPLTYPE %s\n", + color.xsize, color.ysize, depth, maxval, + kColorTypes[color.format.num_channels - 1]); + JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize); + for (const auto& info : ec_info) { + pos += snprintf(header + pos, kMaxHeaderSize - pos, "TUPLTYPE %s\n", + ExtraChannelTypeName(info.ec_info.type).c_str()); + JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize); + } + pos += snprintf(header + pos, kMaxHeaderSize - pos, "ENDHDR\n"); + JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize); + size_t total_size = color.pixels_size; + for (const auto& ec : frame.extra_channels) { + total_size += ec.pixels_size; + } + bytes->resize(pos + total_size); + memcpy(bytes->data(), header, pos); + // If we have no extra channels, just copy color pixel data over. + if (frame.extra_channels.empty()) { + memcpy(bytes->data() + pos, reinterpret_cast<uint8_t*>(color.pixels()), + color.pixels_size); + return true; + } + // Interleave color and extra channels. + const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels()); + std::vector<const uint8_t*> ec_in(frame.extra_channels.size()); + for (size_t i = 0; i < frame.extra_channels.size(); ++i) { + ec_in[i] = + reinterpret_cast<const uint8_t*>(frame.extra_channels[i].pixels()); + } + uint8_t* out = bytes->data() + pos; + size_t pwidth = PackedImage::BitsPerChannel(color.format.data_type) / 8; + for (size_t y = 0; y < color.ysize; ++y) { + for (size_t x = 0; x < color.xsize; ++x) { + memcpy(out, in, color.pixel_stride()); + out += color.pixel_stride(); + in += color.pixel_stride(); + for (auto& p : ec_in) { + memcpy(out, p, pwidth); + out += pwidth; + p += pwidth; + } + } + } + return true; + } + Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) const override { + return true; + } -Span<const uint8_t> MakeSpan(const char* str) { - return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), - strlen(str)); -} + private: + static std::string ExtraChannelTypeName(JxlExtraChannelType type) { + switch (type) { + case JXL_CHANNEL_ALPHA: + return std::string("Alpha"); + case JXL_CHANNEL_DEPTH: + return std::string("Depth"); + case JXL_CHANNEL_SPOT_COLOR: + return std::string("SpotColor"); + case JXL_CHANNEL_SELECTION_MASK: + return std::string("SelectionMask"); + case JXL_CHANNEL_BLACK: + return std::string("Black"); + case JXL_CHANNEL_CFA: + return std::string("CFA"); + case JXL_CHANNEL_THERMAL: + return std::string("Thermal"); + default: + return std::string("UNKNOWN"); + } + } +}; } // namespace @@ -201,6 +311,10 @@ std::unique_ptr<Encoder> GetPPMEncoder() { return jxl::make_unique<PPMEncoder>(); } +std::unique_ptr<Encoder> GetPNMEncoder() { + return jxl::make_unique<PNMEncoder>(); +} + std::unique_ptr<Encoder> GetPFMEncoder() { return jxl::make_unique<PFMEncoder>(); } diff --git a/lib/extras/enc/pnm.h b/lib/extras/enc/pnm.h index 403208c..1e0020c 100644 --- a/lib/extras/enc/pnm.h +++ b/lib/extras/enc/pnm.h @@ -19,6 +19,7 @@ namespace extras { std::unique_ptr<Encoder> GetPAMEncoder(); std::unique_ptr<Encoder> GetPGMEncoder(); +std::unique_ptr<Encoder> GetPNMEncoder(); std::unique_ptr<Encoder> GetPPMEncoder(); std::unique_ptr<Encoder> GetPFMEncoder(); diff --git a/lib/extras/exif.cc b/lib/extras/exif.cc index 7d92655..aea6327 100644 --- a/lib/extras/exif.cc +++ b/lib/extras/exif.cc @@ -23,7 +23,7 @@ void ResetExifOrientation(std::vector<uint8_t>& exif) { return; // not a valid tiff header } t += 4; - uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t)); + uint64_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t)); if (exif.size() < 12 + offset + 2 || offset < 8) return; t += offset - 4; uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t)); diff --git a/lib/extras/hlg.cc b/lib/extras/hlg.cc index e39a080..1250055 100644 --- a/lib/extras/hlg.cc +++ b/lib/extras/hlg.cc @@ -5,9 +5,9 @@ #include "lib/extras/hlg.h" -#include <cmath> +#include <jxl/cms.h> -#include "lib/jxl/enc_color_management.h" +#include <cmath> namespace jxl { @@ -19,11 +19,12 @@ float GetHlgGamma(const float peak_luminance, const float surround_luminance) { Status HlgOOTF(ImageBundle* ib, const float gamma, ThreadPool* pool) { ColorEncoding linear_rec2020; linear_rec2020.SetColorSpace(ColorSpace::kRGB); - linear_rec2020.primaries = Primaries::k2100; - linear_rec2020.white_point = WhitePoint::kD65; - linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear); + JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100)); + JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65)); + linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear); JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC()); - JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool)); + JXL_RETURN_IF_ERROR( + ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool)); JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, ib->ysize(), ThreadPool::NoInit, diff --git a/lib/extras/jpegli_test.cc b/lib/extras/jpegli_test.cc new file mode 100644 index 0000000..0ebf0c1 --- /dev/null +++ b/lib/extras/jpegli_test.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. + +#if JPEGXL_ENABLE_JPEGLI + +#include "lib/extras/dec/jpegli.h" + +#include <jxl/color_encoding.h> +#include <stdint.h> + +#include <cstdint> +#include <memory> +#include <string> +#include <vector> + +#include "lib/extras/dec/color_hints.h" +#include "lib/extras/dec/decode.h" +#include "lib/extras/dec/jpg.h" +#include "lib/extras/enc/encode.h" +#include "lib/extras/enc/jpegli.h" +#include "lib/extras/enc/jpg.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/test_image.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testing.h" + +namespace jxl { +namespace extras { +namespace { + +using test::Butteraugli3Norm; +using test::ButteraugliDistance; +using test::TestImage; + +Status ReadTestImage(const std::string& pathname, PackedPixelFile* ppf) { + const std::vector<uint8_t> encoded = jxl::test::ReadTestData(pathname); + ColorHints color_hints; + if (pathname.find(".ppm") != std::string::npos) { + color_hints.Add("color_space", "RGB_D65_SRG_Rel_SRG"); + } else if (pathname.find(".pgm") != std::string::npos) { + color_hints.Add("color_space", "Gra_D65_Rel_SRG"); + } + return DecodeBytes(Bytes(encoded), color_hints, ppf); +} + +std::vector<uint8_t> GetAppData(const std::vector<uint8_t>& compressed) { + std::vector<uint8_t> result; + size_t pos = 2; // After SOI + while (pos + 4 < compressed.size()) { + if (compressed[pos] != 0xff || compressed[pos + 1] < 0xe0 || + compressed[pos + 1] > 0xf0) { + break; + } + size_t len = (compressed[pos + 2] << 8) + compressed[pos + 3] + 2; + if (pos + len > compressed.size()) { + break; + } + result.insert(result.end(), &compressed[pos], &compressed[pos] + len); + pos += len; + } + return result; +} + +Status DecodeWithLibjpeg(const std::vector<uint8_t>& compressed, + PackedPixelFile* ppf, + const JPGDecompressParams* dparams = nullptr) { + return DecodeImageJPG(Bytes(compressed), ColorHints(), ppf, + /*constraints=*/nullptr, dparams); +} + +Status EncodeWithLibjpeg(const PackedPixelFile& ppf, int quality, + std::vector<uint8_t>* compressed) { + std::unique_ptr<Encoder> encoder = GetJPEGEncoder(); + encoder->SetOption("q", std::to_string(quality)); + EncodedImage encoded; + JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded)); + JXL_RETURN_IF_ERROR(!encoded.bitstreams.empty()); + *compressed = std::move(encoded.bitstreams[0]); + return true; +} + +std::string Description(const JxlColorEncoding& color_encoding) { + ColorEncoding c_enc; + JXL_CHECK(c_enc.FromExternal(color_encoding)); + return Description(c_enc); +} + +float BitsPerPixel(const PackedPixelFile& ppf, + const std::vector<uint8_t>& compressed) { + const size_t num_pixels = ppf.info.xsize * ppf.info.ysize; + return compressed.size() * 8.0 / num_pixels; +} + +TEST(JpegliTest, JpegliSRGBDecodeTest) { + TEST_LIBJPEG_SUPPORT(); + std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; + PackedPixelFile ppf0; + ASSERT_TRUE(ReadTestImage(testimage, &ppf0)); + EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding)); + EXPECT_EQ(8, ppf0.info.bits_per_sample); + + std::vector<uint8_t> compressed; + ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); + + PackedPixelFile ppf1; + ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1)); + PackedPixelFile ppf2; + JpegDecompressParams dparams; + ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2)); + EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1)); +} + +TEST(JpegliTest, JpegliGrayscaleDecodeTest) { + TEST_LIBJPEG_SUPPORT(); + std::string testimage = "jxl/flower/flower_small.g.depth8.pgm"; + PackedPixelFile ppf0; + ASSERT_TRUE(ReadTestImage(testimage, &ppf0)); + EXPECT_EQ("Gra_D65_Rel_SRG", Description(ppf0.color_encoding)); + EXPECT_EQ(8, ppf0.info.bits_per_sample); + + std::vector<uint8_t> compressed; + ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); + + PackedPixelFile ppf1; + ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1)); + PackedPixelFile ppf2; + JpegDecompressParams dparams; + ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2)); + EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1)); +} + +TEST(JpegliTest, JpegliXYBEncodeTest) { + TEST_LIBJPEG_SUPPORT(); + std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; + PackedPixelFile ppf_in; + ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); + EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); + EXPECT_EQ(8, ppf_in.info.bits_per_sample); + + std::vector<uint8_t> compressed; + JpegSettings settings; + settings.xyb = true; + ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + PackedPixelFile ppf_out; + ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); + EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.45f)); + EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.32f)); +} + +TEST(JpegliTest, JpegliDecodeTestLargeSmoothArea) { + TEST_LIBJPEG_SUPPORT(); + TestImage t; + const size_t xsize = 2070; + const size_t ysize = 1063; + t.SetDimensions(xsize, ysize).SetChannels(3); + t.SetAllBitDepths(8).SetEndianness(JXL_NATIVE_ENDIAN); + TestImage::Frame frame = t.AddFrame(); + frame.RandomFill(); + // Create a large smooth area in the top half of the image. This is to test + // that the bias statistics calculation can handle many blocks with all-zero + // AC coefficients. + for (size_t y = 0; y < ysize / 2; ++y) { + for (size_t x = 0; x < xsize; ++x) { + for (size_t c = 0; c < 3; ++c) { + frame.SetValue(y, x, c, 0.5f); + } + } + } + const PackedPixelFile& ppf0 = t.ppf(); + + std::vector<uint8_t> compressed; + ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); + + PackedPixelFile ppf1; + JpegDecompressParams dparams; + ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf1)); + EXPECT_LT(ButteraugliDistance(ppf0, ppf1), 3.0f); +} + +TEST(JpegliTest, JpegliYUVEncodeTest) { + TEST_LIBJPEG_SUPPORT(); + std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; + PackedPixelFile ppf_in; + ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); + EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); + EXPECT_EQ(8, ppf_in.info.bits_per_sample); + + std::vector<uint8_t> compressed; + JpegSettings settings; + settings.xyb = false; + ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + PackedPixelFile ppf_out; + ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); + EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.7f)); + EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.32f)); +} + +TEST(JpegliTest, JpegliYUVChromaSubsamplingEncodeTest) { + TEST_LIBJPEG_SUPPORT(); + std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; + PackedPixelFile ppf_in; + ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); + EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); + EXPECT_EQ(8, ppf_in.info.bits_per_sample); + + std::vector<uint8_t> compressed; + JpegSettings settings; + for (const char* sampling : {"440", "422", "420"}) { + settings.xyb = false; + settings.chroma_subsampling = std::string(sampling); + ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + PackedPixelFile ppf_out; + ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); + EXPECT_LE(BitsPerPixel(ppf_in, compressed), 1.55f); + EXPECT_LE(ButteraugliDistance(ppf_in, ppf_out), 1.82f); + } +} + +TEST(JpegliTest, JpegliYUVEncodeTestNoAq) { + TEST_LIBJPEG_SUPPORT(); + std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; + PackedPixelFile ppf_in; + ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); + EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); + EXPECT_EQ(8, ppf_in.info.bits_per_sample); + + std::vector<uint8_t> compressed; + JpegSettings settings; + settings.xyb = false; + settings.use_adaptive_quantization = false; + ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + PackedPixelFile ppf_out; + ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); + EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.85f)); + EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.25f)); +} + +TEST(JpegliTest, JpegliHDRRoundtripTest) { + std::string testimage = "jxl/hdr_room.png"; + PackedPixelFile ppf_in; + ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); + EXPECT_EQ("RGB_D65_202_Rel_HLG", Description(ppf_in.color_encoding)); + EXPECT_EQ(16, ppf_in.info.bits_per_sample); + + std::vector<uint8_t> compressed; + JpegSettings settings; + settings.xyb = false; + ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + PackedPixelFile ppf_out; + JpegDecompressParams dparams; + dparams.output_data_type = JXL_TYPE_UINT16; + ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf_out)); + EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(2.95f)); + EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.05f)); +} + +TEST(JpegliTest, JpegliSetAppData) { + std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; + PackedPixelFile ppf_in; + ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); + EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); + EXPECT_EQ(8, ppf_in.info.bits_per_sample); + + std::vector<uint8_t> compressed; + JpegSettings settings; + settings.app_data = {0xff, 0xe3, 0, 4, 0, 1}; + EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + EXPECT_EQ(settings.app_data, GetAppData(compressed)); + + settings.app_data = {0xff, 0xe3, 0, 6, 0, 1, 2, 3, 0xff, 0xef, 0, 4, 0, 1}; + EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + EXPECT_EQ(settings.app_data, GetAppData(compressed)); + + settings.xyb = true; + EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + EXPECT_EQ(0, memcmp(settings.app_data.data(), GetAppData(compressed).data(), + settings.app_data.size())); + + settings.xyb = false; + settings.app_data = {0}; + EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + settings.app_data = {0xff, 0xe0}; + EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + settings.app_data = {0xff, 0xe0, 0, 2}; + EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + settings.app_data = {0xff, 0xeb, 0, 4, 0}; + EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + settings.app_data = {0xff, 0xeb, 0, 4, 0, 1, 2, 3}; + EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + settings.app_data = {0xff, 0xab, 0, 4, 0, 1}; + EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + + settings.xyb = false; + settings.app_data = { + 0xff, 0xeb, 0, 4, 0, 1, // + 0xff, 0xe2, 0, 20, 0x49, 0x43, 0x43, 0x5F, 0x50, // + 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00, 0, 1, // + 0, 0, 0, 0, // + }; + EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); + EXPECT_EQ(settings.app_data, GetAppData(compressed)); + + settings.xyb = true; + EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); +} + +struct TestConfig { + int num_colors; + int passes; + int dither; +}; + +class JpegliColorQuantTestParam : public ::testing::TestWithParam<TestConfig> { +}; + +TEST_P(JpegliColorQuantTestParam, JpegliColorQuantizeTest) { + TEST_LIBJPEG_SUPPORT(); + TestConfig config = GetParam(); + std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; + PackedPixelFile ppf0; + ASSERT_TRUE(ReadTestImage(testimage, &ppf0)); + EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding)); + EXPECT_EQ(8, ppf0.info.bits_per_sample); + + std::vector<uint8_t> compressed; + ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); + + PackedPixelFile ppf1; + JPGDecompressParams dparams1; + dparams1.two_pass_quant = (config.passes == 2); + dparams1.num_colors = config.num_colors; + dparams1.dither_mode = config.dither; + ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1, &dparams1)); + + PackedPixelFile ppf2; + JpegDecompressParams dparams2; + dparams2.two_pass_quant = (config.passes == 2); + dparams2.num_colors = config.num_colors; + dparams2.dither_mode = config.dither; + ASSERT_TRUE(DecodeJpeg(compressed, dparams2, nullptr, &ppf2)); + + double dist1 = Butteraugli3Norm(ppf0, ppf1); + double dist2 = Butteraugli3Norm(ppf0, ppf2); + printf("distance: %f vs %f\n", dist2, dist1); + if (config.passes == 1) { + if (config.num_colors == 16 && config.dither == 2) { + // TODO(szabadka) Fix this case. + EXPECT_LT(dist2, dist1 * 1.5); + } else { + EXPECT_LT(dist2, dist1 * 1.05); + } + } else if (config.num_colors > 64) { + // TODO(szabadka) Fix 2pass quantization for <= 64 colors. + EXPECT_LT(dist2, dist1 * 1.1); + } else if (config.num_colors > 32) { + EXPECT_LT(dist2, dist1 * 1.2); + } else { + EXPECT_LT(dist2, dist1 * 1.7); + } +} + +std::vector<TestConfig> GenerateTests() { + std::vector<TestConfig> all_tests; + for (int num_colors = 8; num_colors <= 256; num_colors *= 2) { + for (int passes = 1; passes <= 2; ++passes) { + for (int dither = 0; dither < 3; dither += passes) { + TestConfig config; + config.num_colors = num_colors; + config.passes = passes; + config.dither = dither; + all_tests.push_back(config); + } + } + } + return all_tests; +} + +std::ostream& operator<<(std::ostream& os, const TestConfig& c) { + static constexpr const char* kDitherModeStr[] = {"No", "Ordered", "FS"}; + os << c.passes << "pass"; + os << c.num_colors << "colors"; + os << kDitherModeStr[c.dither] << "dither"; + return os; +} + +std::string TestDescription(const testing::TestParamInfo<TestConfig>& info) { + std::stringstream name; + name << info.param; + return name.str(); +} + +JXL_GTEST_INSTANTIATE_TEST_SUITE_P(JpegliColorQuantTest, + JpegliColorQuantTestParam, + testing::ValuesIn(GenerateTests()), + TestDescription); + +} // namespace +} // namespace extras +} // namespace jxl +#endif // JPEGXL_ENABLE_JPEGLI diff --git a/lib/extras/metrics.cc b/lib/extras/metrics.cc new file mode 100644 index 0000000..4259d3c --- /dev/null +++ b/lib/extras/metrics.cc @@ -0,0 +1,224 @@ +// 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/metrics.h" + +#include <math.h> +#include <stdlib.h> + +#include <atomic> + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/extras/metrics.cc" +#include <hwy/foreach_target.h> +#include <hwy/highway.h> + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Add; +using hwy::HWY_NAMESPACE::GetLane; +using hwy::HWY_NAMESPACE::Mul; +using hwy::HWY_NAMESPACE::Rebind; + +double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params, + double p) { + const double onePerPixels = 1.0 / (distmap.ysize() * distmap.xsize()); + if (std::abs(p - 3.0) < 1E-6) { + double sum1[3] = {0.0}; + +// Prefer double if possible, but otherwise use float rather than scalar. +#if HWY_CAP_FLOAT64 + using T = double; + const Rebind<float, HWY_FULL(double)> df; +#else + using T = float; +#endif + const HWY_FULL(T) d; + constexpr size_t N = MaxLanes(d); + // Manually aligned storage to avoid asan crash on clang-7 due to + // unaligned spill. + HWY_ALIGN T sum_totals0[N] = {0}; + HWY_ALIGN T sum_totals1[N] = {0}; + HWY_ALIGN T sum_totals2[N] = {0}; + + for (size_t y = 0; y < distmap.ysize(); ++y) { + const float* JXL_RESTRICT row = distmap.ConstRow(y); + + auto sums0 = Zero(d); + auto sums1 = Zero(d); + auto sums2 = Zero(d); + + size_t x = 0; + for (; x + Lanes(d) <= distmap.xsize(); x += Lanes(d)) { +#if HWY_CAP_FLOAT64 + const auto d1 = PromoteTo(d, Load(df, row + x)); +#else + const auto d1 = Load(d, row + x); +#endif + const auto d2 = Mul(d1, Mul(d1, d1)); + sums0 = Add(sums0, d2); + const auto d3 = Mul(d2, d2); + sums1 = Add(sums1, d3); + const auto d4 = Mul(d3, d3); + sums2 = Add(sums2, d4); + } + + Store(Add(sums0, Load(d, sum_totals0)), d, sum_totals0); + Store(Add(sums1, Load(d, sum_totals1)), d, sum_totals1); + Store(Add(sums2, Load(d, sum_totals2)), d, sum_totals2); + + for (; x < distmap.xsize(); ++x) { + const double d1 = row[x]; + double d2 = d1 * d1 * d1; + sum1[0] += d2; + d2 *= d2; + sum1[1] += d2; + d2 *= d2; + sum1[2] += d2; + } + } + double v = 0; + v += pow( + onePerPixels * (sum1[0] + GetLane(SumOfLanes(d, Load(d, sum_totals0)))), + 1.0 / (p * 1.0)); + v += pow( + onePerPixels * (sum1[1] + GetLane(SumOfLanes(d, Load(d, sum_totals1)))), + 1.0 / (p * 2.0)); + v += pow( + onePerPixels * (sum1[2] + GetLane(SumOfLanes(d, Load(d, sum_totals2)))), + 1.0 / (p * 4.0)); + v /= 3.0; + return v; + } else { + static std::atomic<int> once{0}; + if (once.fetch_add(1, std::memory_order_relaxed) == 0) { + JXL_WARNING("WARNING: using slow ComputeDistanceP"); + } + double sum1[3] = {0.0}; + for (size_t y = 0; y < distmap.ysize(); ++y) { + const float* JXL_RESTRICT row = distmap.ConstRow(y); + for (size_t x = 0; x < distmap.xsize(); ++x) { + double d2 = std::pow(row[x], p); + sum1[0] += d2; + d2 *= d2; + sum1[1] += d2; + d2 *= d2; + sum1[2] += d2; + } + } + double v = 0; + for (int i = 0; i < 3; ++i) { + v += pow(onePerPixels * (sum1[i]), 1.0 / (p * (1 << i))); + } + v /= 3.0; + return v; + } +} + +void ComputeSumOfSquares(const ImageBundle& ib1, const ImageBundle& ib2, + const JxlCmsInterface& cms, double sum_of_squares[3]) { + // Convert to sRGB - closer to perception than linear. + const Image3F* srgb1 = &ib1.color(); + Image3F copy1; + if (!ib1.IsSRGB()) { + JXL_CHECK( + ib1.CopyTo(Rect(ib1), ColorEncoding::SRGB(ib1.IsGray()), cms, ©1)); + srgb1 = ©1; + } + const Image3F* srgb2 = &ib2.color(); + Image3F copy2; + if (!ib2.IsSRGB()) { + JXL_CHECK( + ib2.CopyTo(Rect(ib2), ColorEncoding::SRGB(ib2.IsGray()), cms, ©2)); + srgb2 = ©2; + } + + JXL_CHECK(SameSize(*srgb1, *srgb2)); + + // TODO(veluca): SIMD. + float yuvmatrix[3][3] = {{0.299, 0.587, 0.114}, + {-0.14713, -0.28886, 0.436}, + {0.615, -0.51499, -0.10001}}; + for (size_t y = 0; y < srgb1->ysize(); ++y) { + const float* JXL_RESTRICT row1[3]; + const float* JXL_RESTRICT row2[3]; + for (size_t j = 0; j < 3; j++) { + row1[j] = srgb1->ConstPlaneRow(j, y); + row2[j] = srgb2->ConstPlaneRow(j, y); + } + for (size_t x = 0; x < srgb1->xsize(); ++x) { + float cdiff[3] = {}; + // YUV conversion is linear, so we can run it on the difference. + for (size_t j = 0; j < 3; j++) { + cdiff[j] = row1[j][x] - row2[j][x]; + } + float yuvdiff[3] = {}; + for (size_t j = 0; j < 3; j++) { + for (size_t k = 0; k < 3; k++) { + yuvdiff[j] += yuvmatrix[j][k] * cdiff[k]; + } + } + for (size_t j = 0; j < 3; j++) { + sum_of_squares[j] += yuvdiff[j] * yuvdiff[j]; + } + } + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(ComputeDistanceP); +double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params, + double p) { + return HWY_DYNAMIC_DISPATCH(ComputeDistanceP)(distmap, params, p); +} + +HWY_EXPORT(ComputeSumOfSquares); + +double ComputeDistance2(const ImageBundle& ib1, const ImageBundle& ib2, + const JxlCmsInterface& cms) { + double sum_of_squares[3] = {}; + HWY_DYNAMIC_DISPATCH(ComputeSumOfSquares)(ib1, ib2, cms, sum_of_squares); + // Weighted PSNR as in JPEG-XL: chroma counts 1/8. + const float weights[3] = {6.0f / 8, 1.0f / 8, 1.0f / 8}; + // Avoid squaring the weight - 1/64 is too extreme. + double norm = 0; + for (size_t i = 0; i < 3; i++) { + norm += std::sqrt(sum_of_squares[i]) * weights[i]; + } + // This function returns distance *squared*. + return norm * norm; +} + +double ComputePSNR(const ImageBundle& ib1, const ImageBundle& ib2, + const JxlCmsInterface& cms) { + if (!SameSize(ib1, ib2)) return 0.0; + double sum_of_squares[3] = {}; + HWY_DYNAMIC_DISPATCH(ComputeSumOfSquares)(ib1, ib2, cms, sum_of_squares); + constexpr double kChannelWeights[3] = {6.0 / 8, 1.0 / 8, 1.0 / 8}; + double avg_psnr = 0; + const size_t input_pixels = ib1.xsize() * ib1.ysize(); + for (int i = 0; i < 3; ++i) { + const double rmse = std::sqrt(sum_of_squares[i] / input_pixels); + const double psnr = + sum_of_squares[i] == 0 ? 99.99 : (20 * std::log10(1 / rmse)); + avg_psnr += kChannelWeights[i] * psnr; + } + return avg_psnr; +} + +} // namespace jxl +#endif diff --git a/lib/extras/metrics.h b/lib/extras/metrics.h new file mode 100644 index 0000000..87a69a9 --- /dev/null +++ b/lib/extras/metrics.h @@ -0,0 +1,28 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_METRICS_H_ +#define LIB_EXTRAS_METRICS_H_ + +#include <stdint.h> + +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +// Computes p-norm given the butteraugli distmap. +double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params, + double p); + +double ComputeDistance2(const ImageBundle& ib1, const ImageBundle& ib2, + const JxlCmsInterface& cms); + +double ComputePSNR(const ImageBundle& ib1, const ImageBundle& ib2, + const JxlCmsInterface& cms); + +} // namespace jxl + +#endif // LIB_EXTRAS_METRICS_H_ diff --git a/lib/extras/packed_image.h b/lib/extras/packed_image.h index 1296472..d3ba9ce 100644 --- a/lib/extras/packed_image.h +++ b/lib/extras/packed_image.h @@ -9,20 +9,24 @@ // Helper class for storing external (int or float, interleaved) images. This is // the common format used by other libraries and in the libjxl API. +#include <jxl/codestream_header.h> +#include <jxl/encode.h> +#include <jxl/types.h> #include <stddef.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <algorithm> +#include <cmath> #include <memory> +#include <set> #include <string> #include <vector> -#include "jxl/codestream_header.h" -#include "jxl/encode.h" -#include "jxl/types.h" -#include "lib/jxl/common.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/common.h" +#include "lib/jxl/base/status.h" namespace jxl { namespace extras { @@ -33,9 +37,26 @@ class PackedImage { PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format) : PackedImage(xsize, ysize, format, CalcStride(format, xsize)) {} + PackedImage Copy() const { + PackedImage copy(xsize, ysize, format); + memcpy(reinterpret_cast<uint8_t*>(copy.pixels()), + reinterpret_cast<const uint8_t*>(pixels()), pixels_size); + return copy; + } + // The interleaved pixels as defined in the storage format. void* pixels() const { return pixels_.get(); } + uint8_t* pixels(size_t y, size_t x, size_t c) const { + return (reinterpret_cast<uint8_t*>(pixels_.get()) + y * stride + + x * pixel_stride_ + c * bytes_per_channel_); + } + + const uint8_t* const_pixels(size_t y, size_t x, size_t c) const { + return (reinterpret_cast<const uint8_t*>(pixels_.get()) + y * stride + + x * pixel_stride_ + c * bytes_per_channel_); + } + // The image size in pixels. size_t xsize; size_t ysize; @@ -47,10 +68,7 @@ class PackedImage { JxlPixelFormat format; size_t pixels_size; - size_t pixel_stride() const { - return (BitsPerChannel(format.data_type) * format.num_channels / - jxl::kBitsPerByte); - } + size_t pixel_stride() const { return pixel_stride_; } static size_t BitsPerChannel(JxlDataType data_type) { switch (data_type) { @@ -67,6 +85,52 @@ class PackedImage { } } + float GetPixelValue(size_t y, size_t x, size_t c) const { + const uint8_t* data = const_pixels(y, x, c); + switch (format.data_type) { + case JXL_TYPE_UINT8: + return data[0] * (1.0f / 255); + case JXL_TYPE_UINT16: { + uint16_t val; + memcpy(&val, data, 2); + return (swap_endianness_ ? JXL_BSWAP16(val) : val) * (1.0f / 65535); + } + case JXL_TYPE_FLOAT: { + float val; + memcpy(&val, data, 4); + return swap_endianness_ ? BSwapFloat(val) : val; + } + default: + JXL_ABORT("Unhandled JxlDataType"); + } + } + + void SetPixelValue(size_t y, size_t x, size_t c, float val) { + uint8_t* data = pixels(y, x, c); + switch (format.data_type) { + case JXL_TYPE_UINT8: + data[0] = Clamp1(std::round(val * 255), 0.0f, 255.0f); + break; + case JXL_TYPE_UINT16: { + uint16_t val16 = Clamp1(std::round(val * 65535), 0.0f, 65535.0f); + if (swap_endianness_) { + val16 = JXL_BSWAP16(val16); + } + memcpy(data, &val16, 2); + break; + } + case JXL_TYPE_FLOAT: { + if (swap_endianness_) { + val = BSwapFloat(val); + } + memcpy(data, &val, 4); + break; + } + default: + JXL_ABORT("Unhandled JxlDataType"); + } + } + private: PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format, size_t stride) @@ -75,7 +139,11 @@ class PackedImage { stride(stride), format(format), pixels_size(ysize * stride), - pixels_(malloc(std::max<size_t>(1, pixels_size)), free) {} + pixels_(malloc(std::max<size_t>(1, pixels_size)), free) { + bytes_per_channel_ = BitsPerChannel(format.data_type) / jxl::kBitsPerByte; + pixel_stride_ = format.num_channels * bytes_per_channel_; + swap_endianness_ = SwapEndianness(format.endianness); + } static size_t CalcStride(const JxlPixelFormat& format, size_t xsize) { size_t stride = xsize * (BitsPerChannel(format.data_type) * @@ -86,6 +154,9 @@ class PackedImage { return stride; } + size_t bytes_per_channel_; + size_t pixel_stride_; + bool swap_endianness_; std::unique_ptr<void, decltype(free)*> pixels_; }; @@ -98,6 +169,18 @@ class PackedFrame { template <typename... Args> explicit PackedFrame(Args&&... args) : color(std::forward<Args>(args)...) {} + PackedFrame Copy() const { + PackedFrame copy(color.xsize, color.ysize, color.format); + copy.frame_info = frame_info; + copy.name = name; + copy.color = color.Copy(); + for (size_t i = 0; i < extra_channels.size(); ++i) { + PackedImage ec = extra_channels[i].Copy(); + copy.extra_channels.emplace_back(std::move(ec)); + } + return copy; + } + // The Frame metadata. JxlFrameHeader frame_info = {}; std::string name; @@ -108,6 +191,85 @@ class PackedFrame { std::vector<PackedImage> extra_channels; }; +class ChunkedPackedFrame { + public: + typedef void (*ReadLine)(void* opaque, size_t xpos, size_t ypos, size_t xsize, + uint8_t* buffer, size_t len); + ChunkedPackedFrame(size_t xsize, size_t ysize, const JxlPixelFormat& format, + void* opaque, ReadLine read_line) + : xsize(xsize), + ysize(ysize), + format(format), + opaque_(opaque), + read_line_(read_line) {} + + JxlChunkedFrameInputSource GetInputSource() { + return JxlChunkedFrameInputSource{this, + GetColorChannelsPixelFormat, + GetColorChannelDataAt, + GetExtraChannelPixelFormat, + GetExtraChannelDataAt, + ReleaseCurrentData}; + } + + // The Frame metadata. + JxlFrameHeader frame_info = {}; + std::string name; + + size_t xsize; + size_t ysize; + JxlPixelFormat format; + + private: + static void GetColorChannelsPixelFormat(void* opaque, + JxlPixelFormat* pixel_format) { + ChunkedPackedFrame* self = reinterpret_cast<ChunkedPackedFrame*>(opaque); + *pixel_format = self->format; + } + + static const void* GetColorChannelDataAt(void* opaque, size_t xpos, + size_t ypos, size_t xsize, + size_t ysize, size_t* row_offset) { + ChunkedPackedFrame* self = reinterpret_cast<ChunkedPackedFrame*>(opaque); + size_t bytes_per_channel = + PackedImage::BitsPerChannel(self->format.data_type) / jxl::kBitsPerByte; + size_t bytes_per_pixel = bytes_per_channel * self->format.num_channels; + *row_offset = xsize * bytes_per_pixel; + uint8_t* buffer = reinterpret_cast<uint8_t*>(malloc(ysize * (*row_offset))); + for (size_t y = 0; y < ysize; ++y) { + self->read_line_(self->opaque_, xpos, ypos + y, xsize, + &buffer[y * (*row_offset)], *row_offset); + } + self->buffers_.insert(buffer); + return buffer; + } + + static void GetExtraChannelPixelFormat(void* opaque, size_t ec_index, + JxlPixelFormat* pixel_format) { + JXL_ABORT("Not implemented"); + } + + static const void* GetExtraChannelDataAt(void* opaque, size_t ec_index, + size_t xpos, size_t ypos, + size_t xsize, size_t ysize, + size_t* row_offset) { + JXL_ABORT("Not implemented"); + } + + static void ReleaseCurrentData(void* opaque, const void* buffer) { + ChunkedPackedFrame* self = reinterpret_cast<ChunkedPackedFrame*>(opaque); + auto iter = self->buffers_.find(const_cast<void*>(buffer)); + if (iter != self->buffers_.end()) { + free(*iter); + self->buffers_.erase(iter); + } + } + + void* opaque_; + ReadLine read_line_; + std::set<void*> buffers_; +}; + // Optional metadata associated with a file class PackedMetadata { public: @@ -117,17 +279,18 @@ class PackedMetadata { std::vector<uint8_t> xmp; }; +// The extra channel metadata information. +struct PackedExtraChannel { + JxlExtraChannelInfo ec_info; + size_t index; + std::string name; +}; + // Helper class representing a JXL image file as decoded to pixels from the API. class PackedPixelFile { public: JxlBasicInfo info = {}; - // The extra channel metadata information. - struct PackedExtraChannel { - JxlExtraChannelInfo ec_info; - size_t index; - std::string name; - }; std::vector<PackedExtraChannel> extra_channels_info; // Color information of the decoded pixels. @@ -139,9 +302,14 @@ class PackedPixelFile { std::unique_ptr<PackedFrame> preview_frame; std::vector<PackedFrame> frames; + mutable std::vector<ChunkedPackedFrame> chunked_frames; PackedMetadata metadata; PackedPixelFile() { JxlEncoderInitBasicInfo(&info); }; + + size_t num_frames() const { + return chunked_frames.empty() ? frames.size() : chunked_frames.size(); + } }; } // namespace extras diff --git a/lib/extras/packed_image_convert.cc b/lib/extras/packed_image_convert.cc index dcdd12a..1bc8f20 100644 --- a/lib/extras/packed_image_convert.cc +++ b/lib/extras/packed_image_convert.cc @@ -5,17 +5,18 @@ #include "lib/extras/packed_image_convert.h" +#include <jxl/cms.h> +#include <jxl/color_encoding.h> +#include <jxl/types.h> + #include <cstdint> -#include "jxl/color_encoding.h" -#include "jxl/types.h" #include "lib/jxl/base/status.h" #include "lib/jxl/color_encoding_internal.h" -#include "lib/jxl/color_management.h" #include "lib/jxl/dec_external_image.h" -#include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_external_image.h" #include "lib/jxl/enc_image_bundle.h" +#include "lib/jxl/luminance.h" namespace jxl { namespace extras { @@ -58,10 +59,7 @@ Status ConvertPackedFrameToImageBundle(const JxlBasicInfo& info, JXL_RETURN_IF_ERROR(ConvertFromExternal( span, frame.color.xsize, frame.color.ysize, io.metadata.m.color_encoding, - frame.color.format.num_channels, - /*alpha_is_premultiplied=*/info.alpha_premultiplied, - frame_bits_per_sample, frame.color.format.endianness, pool, bundle, - /*float_in=*/float_in, /*align=*/0)); + frame_bits_per_sample, frame.color.format, pool, bundle)); bundle->extra_channels().resize(io.metadata.m.extra_channel_info.size()); for (size_t i = 0; i < frame.extra_channels.size(); i++) { @@ -84,7 +82,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, ppf.info.exponent_bits_per_sample); } - const bool is_gray = ppf.info.num_color_channels == 1; + const bool is_gray = (ppf.info.num_color_channels == 1); JXL_ASSERT(ppf.info.num_color_channels == 1 || ppf.info.num_color_channels == 3); @@ -114,20 +112,24 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, // Convert the color encoding. if (!ppf.icc.empty()) { - PaddedBytes icc; - icc.append(ppf.icc); - if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) { + IccBytes icc = ppf.icc; + if (!io->metadata.m.color_encoding.SetICC(std::move(icc), + JxlGetDefaultCms())) { fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB\n"); io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray); } else { + if (io->metadata.m.color_encoding.IsCMYK()) { + // We expect gray or tri-color. + return JXL_FAILURE("Embedded ICC is CMYK"); + } if (io->metadata.m.color_encoding.IsGray() != is_gray) { // E.g. JPG image has 3 channels, but gray ICC. return JXL_FAILURE("Embedded ICC does not match image color type"); } } } else { - JXL_RETURN_IF_ERROR(ConvertExternalToInternalColorEncoding( - ppf.color_encoding, &io->metadata.m.color_encoding)); + JXL_RETURN_IF_ERROR( + io->metadata.m.color_encoding.FromExternal(ppf.color_encoding)); if (io->metadata.m.color_encoding.ICC().empty()) { return JXL_FAILURE("Failed to serialize ICC"); } @@ -140,8 +142,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, io->blobs.xmp = ppf.metadata.xmp; // Append all other extra channels. - for (const PackedPixelFile::PackedExtraChannel& info : - ppf.extra_channels_info) { + for (const auto& info : ppf.extra_channels_info) { ExtraChannelInfo out; out.type = static_cast<jxl::ExtraChannel>(info.ec_info.type); out.bit_depth.bits_per_sample = info.ec_info.bits_per_sample; @@ -171,14 +172,12 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, } // Convert the pixels - io->dec_pixels = 0; io->frames.clear(); for (const auto& frame : ppf.frames) { ImageBundle bundle(&io->metadata.m); JXL_RETURN_IF_ERROR( ConvertPackedFrameToImageBundle(ppf.info, frame, *io, pool, &bundle)); io->frames.push_back(std::move(bundle)); - io->dec_pixels += frame.color.xsize * frame.color.ysize; } if (ppf.info.exponent_bits_per_sample == 0) { @@ -188,7 +187,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, if (ppf.info.intensity_target != 0) { io->metadata.m.SetIntensityTarget(ppf.info.intensity_target); } else { - SetIntensityTarget(io); + SetIntensityTarget(&io->metadata.m); } io->CheckMetadata(); return true; @@ -221,6 +220,12 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, ppf->info.exponent_bits_per_sample = io.metadata.m.bit_depth.exponent_bits_per_sample; + ppf->info.intensity_target = io.metadata.m.tone_mapping.intensity_target; + ppf->info.linear_below = io.metadata.m.tone_mapping.linear_below; + ppf->info.min_nits = io.metadata.m.tone_mapping.min_nits; + ppf->info.relative_to_max_display = + io.metadata.m.tone_mapping.relative_to_max_display; + ppf->info.alpha_bits = io.metadata.m.GetAlphaBits(); ppf->info.alpha_premultiplied = alpha_premultiplied; @@ -239,7 +244,7 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, // Convert the color encoding ppf->icc.assign(c_desired.ICC().begin(), c_desired.ICC().end()); - ConvertInternalToExternalColorEncoding(c_desired, &ppf->color_encoding); + ppf->color_encoding = c_desired.ToExternal(); // Convert the extra blobs ppf->metadata.exif = io.blobs.exif; @@ -276,12 +281,8 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, const ImageBundle* transformed; // TODO(firsching): handle the transform here. JXL_RETURN_IF_ERROR(TransformIfNeeded(*to_color_transform, c_desired, - GetJxlCms(), pool, &store, + *JxlGetDefaultCms(), pool, &store, &transformed)); - size_t stride = ib.oriented_xsize() * - (c_desired.Channels() * ppf->info.bits_per_sample) / - kBitsPerByte; - PaddedBytes pixels(stride * ib.oriented_ysize()); JXL_RETURN_IF_ERROR(ConvertToExternal( *transformed, bits_per_sample, float_out, format.num_channels, diff --git a/lib/extras/packed_image_convert.h b/lib/extras/packed_image_convert.h index cada660..100adcc 100644 --- a/lib/extras/packed_image_convert.h +++ b/lib/extras/packed_image_convert.h @@ -9,7 +9,8 @@ // Helper functions to convert from the external image types to the internal // CodecInOut to help transitioning to the external types. -#include "jxl/types.h" +#include <jxl/types.h> + #include "lib/extras/packed_image.h" #include "lib/jxl/base/status.h" #include "lib/jxl/codec_in_out.h" diff --git a/lib/extras/render_hdr.cc b/lib/extras/render_hdr.cc deleted file mode 100644 index b247699..0000000 --- a/lib/extras/render_hdr.cc +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) the JPEG XL Project Authors. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#include "lib/extras/render_hdr.h" - -#include "lib/extras/hlg.h" -#include "lib/extras/tone_mapping.h" -#include "lib/jxl/enc_color_management.h" - -namespace jxl { - -Status RenderHDR(CodecInOut* io, float display_nits, ThreadPool* pool) { - const ColorEncoding& original_color_encoding = io->metadata.m.color_encoding; - if (!(original_color_encoding.tf.IsPQ() || - original_color_encoding.tf.IsHLG())) { - // Nothing to do. - return true; - } - - if (original_color_encoding.tf.IsPQ()) { - JXL_RETURN_IF_ERROR(ToneMapTo({0, display_nits}, io, pool)); - JXL_RETURN_IF_ERROR(GamutMap(io, /*preserve_saturation=*/0.1, pool)); - } else { - const float intensity_target = io->metadata.m.IntensityTarget(); - const float gamma_hlg_to_display = GetHlgGamma(display_nits); - // If the image is already in display space, we need to account for the - // already-applied OOTF. - const float gamma_display_to_display = - gamma_hlg_to_display / GetHlgGamma(intensity_target); - // Ensures that conversions to linear in HlgOOTF below will not themselves - // include the OOTF. - io->metadata.m.SetIntensityTarget(300); - - bool need_gamut_mapping = false; - for (ImageBundle& ib : io->frames) { - const float gamma = ib.c_current().tf.IsHLG() ? gamma_hlg_to_display - : gamma_display_to_display; - if (gamma < 1) need_gamut_mapping = true; - JXL_RETURN_IF_ERROR(HlgOOTF(&ib, gamma, pool)); - } - io->metadata.m.SetIntensityTarget(display_nits); - - if (need_gamut_mapping) { - JXL_RETURN_IF_ERROR(GamutMap(io, /*preserve_saturation=*/0.1, pool)); - } - } - - ColorEncoding rec2020_pq; - rec2020_pq.SetColorSpace(ColorSpace::kRGB); - rec2020_pq.white_point = WhitePoint::kD65; - rec2020_pq.primaries = Primaries::k2100; - rec2020_pq.tf.SetTransferFunction(TransferFunction::kPQ); - JXL_RETURN_IF_ERROR(rec2020_pq.CreateICC()); - io->metadata.m.color_encoding = rec2020_pq; - return io->TransformTo(rec2020_pq, GetJxlCms(), pool); -} - -} // namespace jxl diff --git a/lib/extras/render_hdr.h b/lib/extras/render_hdr.h deleted file mode 100644 index 95127e0..0000000 --- a/lib/extras/render_hdr.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) the JPEG XL Project Authors. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#ifndef LIB_EXTRAS_RENDER_HDR_H_ -#define LIB_EXTRAS_RENDER_HDR_H_ - -#include "lib/jxl/codec_in_out.h" - -namespace jxl { - -// If `io` has an original color space using PQ or HLG, this renders it -// appropriately for a display with a peak luminance of `display_nits` and -// converts the result to a Rec. 2020 / PQ image. Otherwise, leaves the image as -// is. -// PQ images are tone-mapped using the method described in Rep. ITU-R BT.2408-5 -// annex 5, while HLG images are rendered using the HLG OOTF with a gamma -// appropriate for the given target luminance. -// With a sufficiently bright SDR display, converting the output of this -// function to an SDR colorspace may look decent. -Status RenderHDR(CodecInOut* io, float display_nits, - ThreadPool* pool = nullptr); - -} // namespace jxl - -#endif // LIB_EXTRAS_RENDER_HDR_H_ diff --git a/lib/extras/size_constraints.h b/lib/extras/size_constraints.h new file mode 100644 index 0000000..cf06f8c --- /dev/null +++ b/lib/extras/size_constraints.h @@ -0,0 +1,43 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_JXL_SIZE_CONSTRAINTS_H_ +#define LIB_JXL_SIZE_CONSTRAINTS_H_ + +#include <cstdint> +#include <type_traits> + +#include "lib/jxl/base/status.h" + +namespace jxl { + +struct SizeConstraints { + // Upper limit on pixel dimensions/area, enforced by VerifyDimensions + // (called from decoders). Fuzzers set smaller values to limit memory use. + uint32_t dec_max_xsize = 0xFFFFFFFFu; + uint32_t dec_max_ysize = 0xFFFFFFFFu; + uint64_t dec_max_pixels = 0xFFFFFFFFu; // Might be up to ~0ull +}; + +template <typename T, + class = typename std::enable_if<std::is_unsigned<T>::value>::type> +Status VerifyDimensions(const SizeConstraints* constraints, T xs, T ys) { + if (!constraints) return true; + + if (xs == 0 || ys == 0) return JXL_FAILURE("Empty image."); + if (xs > constraints->dec_max_xsize) return JXL_FAILURE("Image too wide."); + if (ys > constraints->dec_max_ysize) return JXL_FAILURE("Image too tall."); + + const uint64_t num_pixels = static_cast<uint64_t>(xs) * ys; + if (num_pixels > constraints->dec_max_pixels) { + return JXL_FAILURE("Image too big."); + } + + return true; +} + +} // namespace jxl + +#endif // LIB_JXL_SIZE_CONSTRAINTS_H_ diff --git a/lib/extras/time.cc b/lib/extras/time.cc index 73d1b8f..d4f4175 100644 --- a/lib/extras/time.cc +++ b/lib/extras/time.cc @@ -6,7 +6,6 @@ #include "lib/extras/time.h" #include <stdint.h> -#include <stdio.h> #include <stdlib.h> #include <ctime> diff --git a/lib/extras/tone_mapping.cc b/lib/extras/tone_mapping.cc index 1ed1b29..3d02695 100644 --- a/lib/extras/tone_mapping.cc +++ b/lib/extras/tone_mapping.cc @@ -7,11 +7,13 @@ #undef HWY_TARGET_INCLUDE #define HWY_TARGET_INCLUDE "lib/extras/tone_mapping.cc" +#include <jxl/cms.h> + #include <hwy/foreach_target.h> #include <hwy/highway.h> -#include "lib/jxl/dec_tone_mapping-inl.h" -#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/cms/tone_mapping-inl.h" +#include "lib/jxl/image_bundle.h" HWY_BEFORE_NAMESPACE(); namespace jxl { @@ -30,11 +32,12 @@ Status ToneMapFrame(const std::pair<float, float> display_nits, ColorEncoding linear_rec2020; linear_rec2020.SetColorSpace(ColorSpace::kRGB); - linear_rec2020.primaries = Primaries::k2100; - linear_rec2020.white_point = WhitePoint::kD65; - linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear); + JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100)); + JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65)); + linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear); JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC()); - JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool)); + JXL_RETURN_IF_ERROR( + ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool)); Rec2408ToneMapper<decltype(df)> tone_mapper( {ib->metadata()->tone_mapping.min_nits, @@ -67,11 +70,12 @@ Status GamutMapFrame(ImageBundle* const ib, float preserve_saturation, ColorEncoding linear_rec2020; linear_rec2020.SetColorSpace(ColorSpace::kRGB); - linear_rec2020.primaries = Primaries::k2100; - linear_rec2020.white_point = WhitePoint::kD65; - linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear); + JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100)); + JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65)); + linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear); JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC()); - JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool)); + JXL_RETURN_IF_ERROR( + ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool)); JXL_RETURN_IF_ERROR(RunOnPool( pool, 0, ib->ysize(), ThreadPool::NoInit, diff --git a/lib/extras/tone_mapping_gbench.cc b/lib/extras/tone_mapping_gbench.cc index 2f97b88..34cbdde 100644 --- a/lib/extras/tone_mapping_gbench.cc +++ b/lib/extras/tone_mapping_gbench.cc @@ -6,39 +6,35 @@ #include "benchmark/benchmark.h" #include "lib/extras/codec.h" #include "lib/extras/tone_mapping.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/testdata.h" namespace jxl { static void BM_ToneMapping(benchmark::State& state) { - CodecInOut image; - const PaddedBytes image_bytes = ReadTestData("jxl/flower/flower.png"); - JXL_CHECK(SetFromBytes(Span<const uint8_t>(image_bytes), &image)); + Image3F color(2268, 1512); + FillImage(0.5f, &color); - // Convert to linear Rec. 2020 so that `ToneMapTo` doesn't have to and we - // mainly measure the tone mapping itself. + // Use linear Rec. 2020 so that `ToneMapTo` doesn't have to convert to it and + // we mainly measure the tone mapping itself. ColorEncoding linear_rec2020; linear_rec2020.SetColorSpace(ColorSpace::kRGB); - linear_rec2020.primaries = Primaries::k2100; - linear_rec2020.white_point = WhitePoint::kD65; - linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear); + JXL_CHECK(linear_rec2020.SetPrimariesType(Primaries::k2100)); + JXL_CHECK(linear_rec2020.SetWhitePointType(WhitePoint::kD65)); + linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear); JXL_CHECK(linear_rec2020.CreateICC()); - JXL_CHECK(image.TransformTo(linear_rec2020, GetJxlCms())); for (auto _ : state) { state.PauseTiming(); CodecInOut tone_mapping_input; - tone_mapping_input.SetFromImage(CopyImage(*image.Main().color()), - image.Main().c_current()); - tone_mapping_input.metadata.m.SetIntensityTarget( - image.metadata.m.IntensityTarget()); + Image3F color2(color.xsize(), color.ysize()); + CopyImageTo(color, &color2); + tone_mapping_input.SetFromImage(std::move(color2), linear_rec2020); + tone_mapping_input.metadata.m.SetIntensityTarget(255); state.ResumeTiming(); JXL_CHECK(ToneMapTo({0.1, 100}, &tone_mapping_input)); } - state.SetItemsProcessed(state.iterations() * image.xsize() * image.ysize()); + state.SetItemsProcessed(state.iterations() * color.xsize() * color.ysize()); } BENCHMARK(BM_ToneMapping); |