diff options
Diffstat (limited to 'media/libjxl/src/lib/extras')
30 files changed, 2410 insertions, 811 deletions
diff --git a/media/libjxl/src/lib/extras/codec.cc b/media/libjxl/src/lib/extras/codec.cc index 80ed7c867c..774b4ccb6e 100644 --- a/media/libjxl/src/lib/extras/codec.cc +++ b/media/libjxl/src/lib/extras/codec.cc @@ -52,7 +52,7 @@ Status SetFromBytes(const Span<const uint8_t> bytes, Status SetFromFile(const std::string& pathname, const extras::ColorHints& color_hints, CodecInOut* io, ThreadPool* pool, extras::Codec* orig_codec) { - PaddedBytes encoded; + 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)); @@ -61,67 +61,66 @@ Status SetFromFile(const std::string& pathname, Status Encode(const CodecInOut& io, const extras::Codec codec, const ColorEncoding& c_desired, size_t bits_per_sample, - PaddedBytes* bytes, ThreadPool* pool) { + std::vector<uint8_t>* bytes, ThreadPool* pool) { JXL_CHECK(!io.Main().c_current().ICC().empty()); JXL_CHECK(!c_desired.ICC().empty()); io.CheckMetadata(); if (io.Main().IsJPEG()) { JXL_WARNING("Writing JPEG data as pixels"); } - - extras::PackedPixelFile ppf; - size_t num_channels = io.metadata.m.color_encoding.Channels(); JxlPixelFormat format = { - static_cast<uint32_t>(num_channels), - bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, - JXL_NATIVE_ENDIAN, 0}; - std::vector<uint8_t> bytes_vector; + 0, // num_channels is ignored by the converter + bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16, JXL_BIG_ENDIAN, + 0}; const bool floating_point = bits_per_sample > 16; + std::unique_ptr<extras::Encoder> encoder; + std::ostringstream os; switch (codec) { case extras::Codec::kPNG: #if JPEGXL_ENABLE_APNG - return extras::EncodeImageAPNG(&io, c_desired, bits_per_sample, pool, - bytes); + encoder = extras::GetAPNGEncoder(); + break; #else return JXL_FAILURE("JPEG XL was built without (A)PNG support"); #endif case extras::Codec::kJPG: #if JPEGXL_ENABLE_JPEG - return EncodeImageJPG(&io, - io.use_sjpeg ? extras::JpegEncoder::kSJpeg - : extras::JpegEncoder::kLibJpeg, - io.jpeg_quality, YCbCrChromaSubsampling(), pool, - bytes); + format.data_type = JXL_TYPE_UINT8; + encoder = extras::GetJPEGEncoder(); + os << io.jpeg_quality; + encoder->SetOption("q", os.str()); + break; #else return JXL_FAILURE("JPEG XL was built without JPEG support"); #endif case extras::Codec::kPNM: - - // Choose native for PFM; PGM/PPM require big-endian (N/A for PBM) - format.endianness = floating_point ? JXL_NATIVE_ENDIAN : JXL_BIG_ENDIAN; - if (floating_point) { + if (io.Main().HasAlpha()) { + encoder = extras::GetPAMEncoder(); + } else if (io.Main().IsGray()) { + encoder = extras::GetPGMEncoder(); + } else if (!floating_point) { + encoder = extras::GetPPMEncoder(); + } else { format.data_type = JXL_TYPE_FLOAT; + format.endianness = JXL_NATIVE_ENDIAN; + encoder = extras::GetPFMEncoder(); } if (!c_desired.IsSRGB()) { JXL_WARNING( - "PNM encoder cannot store custom ICC profile; decoder\n" + "PNM encoder cannot store custom ICC profile; decoder " "will need hint key=color_space to get the same values"); } - JXL_RETURN_IF_ERROR(extras::ConvertCodecInOutToPackedPixelFile( - io, format, c_desired, pool, &ppf)); - JXL_RETURN_IF_ERROR(extras::EncodeImagePNM( - ppf, bits_per_sample, pool, /*frame_index=*/0, &bytes_vector)); - bytes->assign(bytes_vector.data(), - bytes_vector.data() + bytes_vector.size()); - return true; + break; case extras::Codec::kPGX: - return extras::EncodeImagePGX(&io, c_desired, bits_per_sample, pool, - bytes); + encoder = extras::GetPGXEncoder(); + break; case extras::Codec::kGIF: return JXL_FAILURE("Encoding to GIF is not implemented"); case extras::Codec::kEXR: #if JPEGXL_ENABLE_EXR - return extras::EncodeImageEXR(&io, c_desired, pool, bytes); + format.data_type = JXL_TYPE_FLOAT; + encoder = extras::GetEXREncoder(); + break; #else return JXL_FAILURE("JPEG XL was built without OpenEXR support"); #endif @@ -129,7 +128,19 @@ Status Encode(const CodecInOut& io, const extras::Codec codec, return JXL_FAILURE("Cannot encode using Codec::kUnknown"); } - return JXL_FAILURE("Invalid codec"); + if (!encoder) { + return JXL_FAILURE("Invalid codec."); + } + + extras::PackedPixelFile ppf; + JXL_RETURN_IF_ERROR( + ConvertCodecInOutToPackedPixelFile(io, format, c_desired, pool, &ppf)); + extras::EncodedImage encoded_image; + JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded_image, pool)); + JXL_ASSERT(encoded_image.bitstreams.size() == 1); + *bytes = encoded_image.bitstreams[0]; + + return true; } Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, @@ -163,7 +174,7 @@ Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, bits_per_sample = 16; } - PaddedBytes encoded; + std::vector<uint8_t> encoded; return Encode(io, codec, c_desired, bits_per_sample, &encoded, pool) && WriteFile(encoded, pathname); } diff --git a/media/libjxl/src/lib/extras/codec.h b/media/libjxl/src/lib/extras/codec.h index 88e96d308e..73fdc80be9 100644 --- a/media/libjxl/src/lib/extras/codec.h +++ b/media/libjxl/src/lib/extras/codec.h @@ -49,7 +49,7 @@ Status SetFromFile(const std::string& pathname, // color space to c_desired. Status Encode(const CodecInOut& io, extras::Codec codec, const ColorEncoding& c_desired, size_t bits_per_sample, - PaddedBytes* bytes, ThreadPool* pool = nullptr); + 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, diff --git a/media/libjxl/src/lib/extras/codec_test.cc b/media/libjxl/src/lib/extras/codec_test.cc index d7d9b86954..19cac39979 100644 --- a/media/libjxl/src/lib/extras/codec_test.cc +++ b/media/libjxl/src/lib/extras/codec_test.cc @@ -9,12 +9,15 @@ #include <stdio.h> #include <algorithm> +#include <sstream> +#include <string> #include <utility> #include <vector> -#include "gtest/gtest.h" #include "lib/extras/dec/pgx.h" #include "lib/extras/dec/pnm.h" +#include "lib/extras/enc/encode.h" +#include "lib/extras/packed_image_convert.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/base/random.h" #include "lib/jxl/base/thread_pool_internal.h" @@ -23,12 +26,20 @@ #include "lib/jxl/image.h" #include "lib/jxl/image_bundle.h" #include "lib/jxl/image_test_utils.h" +#include "lib/jxl/test_utils.h" #include "lib/jxl/testdata.h" namespace jxl { namespace extras { namespace { +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::NotNull; +using ::testing::SizeIs; + std::string ExtensionFromCodec(Codec codec, const bool is_gray, const bool has_alpha, const size_t bits_per_sample) { @@ -54,159 +65,233 @@ std::string ExtensionFromCodec(Codec codec, const bool is_gray, return std::string(); } -CodecInOut CreateTestImage(const size_t xsize, const size_t ysize, - const bool is_gray, const bool add_alpha, - const size_t bits_per_sample, - const ColorEncoding& c_native) { - Image3F image(xsize, ysize); - Rng rng(129); - if (is_gray) { - for (size_t y = 0; y < ysize; ++y) { - float* JXL_RESTRICT row0 = image.PlaneRow(0, y); - float* JXL_RESTRICT row1 = image.PlaneRow(1, y); - float* JXL_RESTRICT row2 = image.PlaneRow(2, y); - for (size_t x = 0; x < xsize; ++x) { - row0[x] = row1[x] = row2[x] = rng.UniformF(0.0f, 1.0f); +void VerifySameImage(const PackedImage& im0, size_t bits_per_sample0, + const PackedImage& im1, size_t bits_per_sample1, + bool lossless = true) { + ASSERT_EQ(im0.xsize, im1.xsize); + ASSERT_EQ(im0.ysize, im1.ysize); + ASSERT_EQ(im0.format.num_channels, im1.format.num_channels); + auto get_factor = [](JxlPixelFormat f, size_t bits) -> double { + return 1.0 / ((1u << std::min(test::GetPrecision(f.data_type), bits)) - 1); + }; + double factor0 = get_factor(im0.format, bits_per_sample0); + double factor1 = get_factor(im1.format, bits_per_sample1); + auto pixels0 = static_cast<const uint8_t*>(im0.pixels()); + auto pixels1 = static_cast<const uint8_t*>(im1.pixels()); + auto rgba0 = + test::ConvertToRGBA32(pixels0, im0.xsize, im0.ysize, im0.format, factor0); + auto rgba1 = + test::ConvertToRGBA32(pixels1, im1.xsize, im1.ysize, im1.format, factor1); + double tolerance = + lossless ? 0.5 * std::min(factor0, factor1) : 3.0f / 255.0f; + if (bits_per_sample0 == 32 || bits_per_sample1 == 32) { + tolerance = 0.5 * std::max(factor0, factor1); + } + for (size_t y = 0; y < im0.ysize; ++y) { + for (size_t x = 0; x < im0.xsize; ++x) { + for (size_t c = 0; c < im0.format.num_channels; ++c) { + size_t ix = (y * im0.xsize + x) * 4 + c; + double val0 = rgba0[ix]; + double val1 = rgba1[ix]; + ASSERT_NEAR(val1, val0, tolerance) + << "y = " << y << " x = " << x << " c = " << c; } } - } else { - RandomFillImage(&image, 0.0f, 1.0f); } - CodecInOut io; +} + +JxlColorEncoding CreateTestColorEncoding(bool is_gray) { + JxlColorEncoding c; + c.color_space = is_gray ? JXL_COLOR_SPACE_GRAY : JXL_COLOR_SPACE_RGB; + c.white_point = JXL_WHITE_POINT_D65; + c.primaries = JXL_PRIMARIES_P3; + c.rendering_intent = JXL_RENDERING_INTENT_RELATIVE; + c.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR; + // Roundtrip through internal color encoding to fill in primaries and white + // point CIE xy coordinates. + ColorEncoding c_internal; + JXL_CHECK(ConvertExternalToInternalColorEncoding(c, &c_internal)); + ConvertInternalToExternalColorEncoding(c_internal, &c); + return c; +} - if (bits_per_sample == 32) { - io.metadata.m.SetFloat32Samples(); +std::vector<uint8_t> GenerateICC(JxlColorEncoding color_encoding) { + ColorEncoding c; + JXL_CHECK(ConvertExternalToInternalColorEncoding(color_encoding, &c)); + JXL_CHECK(c.CreateICC()); + PaddedBytes icc = c.ICC(); + return std::vector<uint8_t>(icc.begin(), icc.end()); +} + +void StoreRandomValue(uint8_t* out, Rng* rng, JxlPixelFormat format, + size_t bits_per_sample) { + uint64_t max_val = (1ull << bits_per_sample) - 1; + if (format.data_type == JXL_TYPE_UINT8) { + *out = rng->UniformU(0, max_val); + } else if (format.data_type == JXL_TYPE_UINT16) { + uint32_t val = rng->UniformU(0, max_val); + if (format.endianness == JXL_BIG_ENDIAN) { + StoreBE16(val, out); + } else { + StoreLE16(val, out); + } } else { - io.metadata.m.SetUintSamples(bits_per_sample); - } - io.metadata.m.color_encoding = c_native; - io.SetFromImage(std::move(image), c_native); - if (add_alpha) { - ImageF alpha(xsize, ysize); - RandomFillImage(&alpha, 0.0f, 1.f); - io.metadata.m.SetAlphaBits(bits_per_sample <= 8 ? 8 : 16); - io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + ASSERT_EQ(format.data_type, JXL_TYPE_FLOAT); + float val = rng->UniformF(0.0, 1.0); + uint32_t uval; + memcpy(&uval, &val, 4); + if (format.endianness == JXL_BIG_ENDIAN) { + StoreBE32(uval, out); + } else { + StoreLE32(uval, out); + } } - return io; } -// Ensures reading a newly written file leads to the same image pixels. -void TestRoundTrip(Codec codec, const size_t xsize, const size_t ysize, - const bool is_gray, const bool add_alpha, - const size_t bits_per_sample, ThreadPool* pool) { - // JPEG encoding is not lossless. - if (codec == Codec::kJPG) return; - if (codec == Codec::kPNM && add_alpha) return; - // Our EXR codec always uses 16-bit premultiplied alpha, does not support - // grayscale, and somehow does not have sufficient precision for this test. - if (codec == Codec::kEXR) return; - printf("Codec %s bps:%" PRIuS " gr:%d al:%d\n", - ExtensionFromCodec(codec, is_gray, add_alpha, bits_per_sample).c_str(), - bits_per_sample, is_gray, add_alpha); - - ColorEncoding c_native; - c_native.SetColorSpace(is_gray ? ColorSpace::kGray : ColorSpace::kRGB); - // Note: this must not be wider than c_external, otherwise gamut clipping - // will cause large round-trip errors. - c_native.primaries = Primaries::kP3; - c_native.tf.SetTransferFunction(TransferFunction::kLinear); - JXL_CHECK(c_native.CreateICC()); - - // Generally store same color space to reduce round trip errors.. - ColorEncoding c_external = c_native; - // .. unless we have enough precision for some transforms. - if (bits_per_sample >= 16) { - c_external.white_point = WhitePoint::kE; - c_external.primaries = Primaries::k2100; - c_external.tf.SetTransferFunction(TransferFunction::kSRGB); +void FillPackedImage(size_t bits_per_sample, PackedImage* image) { + JxlPixelFormat format = image->format; + size_t bytes_per_channel = PackedImage::BitsPerChannel(format.data_type) / 8; + uint8_t* out = static_cast<uint8_t*>(image->pixels()); + size_t stride = image->xsize * format.num_channels * bytes_per_channel; + ASSERT_EQ(image->pixels_size, image->ysize * stride); + Rng rng(129); + for (size_t y = 0; y < image->ysize; ++y) { + for (size_t x = 0; x < image->xsize; ++x) { + for (size_t c = 0; c < format.num_channels; ++c) { + StoreRandomValue(out, &rng, format, bits_per_sample); + out += bytes_per_channel; + } + } } - JXL_CHECK(c_external.CreateICC()); +} - const CodecInOut io = CreateTestImage(xsize, ysize, is_gray, add_alpha, - bits_per_sample, c_native); - const ImageBundle& ib1 = io.Main(); +struct TestImageParams { + Codec codec; + size_t xsize; + size_t ysize; + size_t bits_per_sample; + bool is_gray; + bool add_alpha; + bool big_endian; + + bool ShouldTestRoundtrip() const { + if (codec == Codec::kPNG) { + return true; + } else if (codec == Codec::kPNM) { + // TODO(szabadka) Make PNM encoder endianness-aware. + return ((bits_per_sample <= 16 && big_endian) || + (bits_per_sample == 32 && !add_alpha && !big_endian)); + } else if (codec == Codec::kPGX) { + return ((bits_per_sample == 8 || bits_per_sample == 16) && is_gray && + !add_alpha); + } else if (codec == Codec::kEXR) { +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) + // OpenEXR 2.3 has a memory leak in IlmThread_2_3::ThreadPool + return false; +#else + return bits_per_sample == 32 && !is_gray; +#endif + } else if (codec == Codec::kJPG) { + return bits_per_sample == 8 && !add_alpha; + } else { + return false; + } + } - PaddedBytes encoded; - JXL_CHECK(Encode(io, codec, c_external, bits_per_sample, &encoded, pool)); + JxlPixelFormat PixelFormat() const { + JxlPixelFormat format; + format.num_channels = (is_gray ? 1 : 3) + (add_alpha ? 1 : 0); + format.data_type = (bits_per_sample == 32 ? JXL_TYPE_FLOAT + : bits_per_sample > 8 ? JXL_TYPE_UINT16 + : JXL_TYPE_UINT8); + format.endianness = big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN; + format.align = 0; + return format; + } - CodecInOut io2; - ColorHints color_hints; - // Only for PNM because PNG will warn about ignoring them. - if (codec == Codec::kPNM) { - color_hints.Add("color_space", Description(c_external)); + std::string DebugString() const { + std::ostringstream os; + os << "bps:" << bits_per_sample << " gr:" << is_gray << " al:" << add_alpha + << " be: " << big_endian; + return os.str(); } - JXL_CHECK(SetFromBytes(Span<const uint8_t>(encoded), color_hints, &io2, pool, - nullptr)); - ImageBundle& ib2 = io2.Main(); +}; + +void CreateTestImage(const TestImageParams& params, PackedPixelFile* ppf) { + ppf->info.xsize = params.xsize; + ppf->info.ysize = params.ysize; + ppf->info.bits_per_sample = params.bits_per_sample; + ppf->info.exponent_bits_per_sample = params.bits_per_sample == 32 ? 8 : 0; + ppf->info.num_color_channels = params.is_gray ? 1 : 3; + ppf->info.alpha_bits = params.add_alpha ? params.bits_per_sample : 0; + ppf->info.alpha_premultiplied = (params.codec == Codec::kEXR); + + JxlColorEncoding color_encoding = CreateTestColorEncoding(params.is_gray); + ppf->icc = GenerateICC(color_encoding); + ppf->color_encoding = color_encoding; + + PackedFrame frame(params.xsize, params.ysize, params.PixelFormat()); + FillPackedImage(params.bits_per_sample, &frame.color); + ppf->frames.emplace_back(std::move(frame)); +} - EXPECT_EQ(Description(c_external), - Description(io2.metadata.m.color_encoding)); +// Ensures reading a newly written file leads to the same image pixels. +void TestRoundTrip(const TestImageParams& params, ThreadPool* pool) { + if (!params.ShouldTestRoundtrip()) return; - // See c_external above - for low bits_per_sample the encoded space is - // already the same. - if (bits_per_sample < 16) { - EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current())); - } + std::string extension = ExtensionFromCodec( + params.codec, params.is_gray, params.add_alpha, params.bits_per_sample); + printf("Codec %s %s\n", extension.c_str(), params.DebugString().c_str()); - if (add_alpha) { - EXPECT_TRUE(SamePixels(ib1.alpha(), *ib2.alpha())); - } + PackedPixelFile ppf_in; + CreateTestImage(params, &ppf_in); - JXL_CHECK(ib2.TransformTo(ib1.c_current(), GetJxlCms(), pool)); + EncodedImage encoded; + auto encoder = Encoder::FromExtension(extension); + ASSERT_TRUE(encoder.get()); + ASSERT_TRUE(encoder->Encode(ppf_in, &encoded, pool)); + ASSERT_EQ(encoded.bitstreams.size(), 1); - double max_l1, max_rel; - // Round-trip tolerances must be higher than in external_image_test because - // codecs do not support unbounded ranges. -#if JPEGXL_ENABLE_SKCMS - if (bits_per_sample <= 12) { - max_l1 = 0.5; - max_rel = 6E-3; - } else { - max_l1 = 1E-3; - max_rel = 5E-4; - } -#else // JPEGXL_ENABLE_SKCMS - if (bits_per_sample <= 12) { - max_l1 = 0.5; - max_rel = 6E-3; - } else if (bits_per_sample == 16) { - max_l1 = 3E-3; - max_rel = 1E-4; - } else { -#ifdef __ARM_ARCH - // pow() implementation in arm is a bit less precise than in x86 and - // therefore we need a bigger error margin in this case. - max_l1 = 1E-7; - max_rel = 1E-4; -#else - max_l1 = 1E-7; - max_rel = 1E-5; -#endif + 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) { + EXPECT_EQ(ppf_in.icc, ppf_out.icc); } -#endif // JPEGXL_ENABLE_SKCMS - VerifyRelativeError(ib1.color(), *ib2.color(), max_l1, max_rel); + 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, + /*lossless=*/params.codec != Codec::kJPG); } -#if 0 TEST(CodecTest, TestRoundTrip) { ThreadPoolInternal pool(12); - const size_t xsize = 7; - const size_t ysize = 4; + TestImageParams params; + params.xsize = 7; + params.ysize = 4; - for (Codec codec : Values<Codec>()) { - for (int bits_per_sample : {8, 10, 12, 16, 32}) { + for (Codec codec : AvailableCodecs()) { + for (int bits_per_sample : {4, 8, 10, 12, 16, 32}) { for (bool is_gray : {false, true}) { for (bool add_alpha : {false, true}) { - TestRoundTrip(codec, xsize, ysize, is_gray, add_alpha, - static_cast<size_t>(bits_per_sample), &pool); + for (bool big_endian : {false, true}) { + params.codec = codec; + params.bits_per_sample = static_cast<size_t>(bits_per_sample); + params.is_gray = is_gray; + params.add_alpha = add_alpha; + params.big_endian = big_endian; + TestRoundTrip(params, &pool); + } } } } } } -#endif CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool, const ColorHints& color_hints = ColorHints()) { @@ -217,7 +302,7 @@ CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool, const ImageBundle& ib1 = io.Main(); // Encode/Decode again to make sure Encode carries through all metadata. - PaddedBytes encoded; + 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)); @@ -256,11 +341,11 @@ CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool, TEST(CodecTest, TestMetadataSRGB) { ThreadPoolInternal pool(12); - const char* paths[] = {"third_party/raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png", - "third_party/raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png", - "third_party/raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png", - "third_party/raw.pixls/Nikon-D300-12bit_srgb8_dt.png", - "third_party/raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"}; + 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); @@ -285,9 +370,9 @@ TEST(CodecTest, TestMetadataLinear) { ThreadPoolInternal pool(12); const char* paths[3] = { - "third_party/raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png", - "third_party/raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png", - "third_party/raw.pixls/Nikon-D300-12bit_2020_g1_dt.png", + "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}; @@ -317,8 +402,8 @@ TEST(CodecTest, TestMetadataICC) { ThreadPoolInternal pool(12); const char* paths[] = { - "third_party/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png", - "third_party/raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png", + "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 = @@ -340,28 +425,28 @@ TEST(CodecTest, TestMetadataICC) { } } -TEST(CodecTest, Testthird_party/pngsuite) { +TEST(CodecTest, Testexternal/pngsuite) { ThreadPoolInternal pool(12); // Ensure we can load PNG with text, japanese UTF-8, compressed text. - (void)DecodeRoundtrip("third_party/pngsuite/ct1n0g04.png", Codec::kPNG, &pool); - (void)DecodeRoundtrip("third_party/pngsuite/ctjn0g04.png", Codec::kPNG, &pool); - (void)DecodeRoundtrip("third_party/pngsuite/ctzn0g04.png", Codec::kPNG, &pool); + (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("third_party/pngsuite/g10n3p04.png", Codec::kPNG, &pool); + DecodeRoundtrip("external/pngsuite/g10n3p04.png", Codec::kPNG, &pool); EXPECT_TRUE(b1.metadata.color_encoding.tf.IsLinear()); // Extract cHRM const CodecInOut b_p = - DecodeRoundtrip("third_party/pngsuite/ccwn2c08.png", Codec::kPNG, &pool); + 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("third_party/pngsuite/exif2c08.png", Codec::kPNG, &pool); + DecodeRoundtrip("external/pngsuite/exif2c08.png", Codec::kPNG, &pool); EXPECT_EQ(978, b_exif.blobs.exif.size()); } #endif @@ -384,18 +469,88 @@ void VerifyWideGamutMetadata(const std::string& relative_pathname, TEST(CodecTest, TestWideGamut) { ThreadPoolInternal pool(12); - // VerifyWideGamutMetadata("third_party/wide-gamut-tests/P3-sRGB-color-bars.png", + // VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-bars.png", // Primaries::kP3, &pool); - VerifyWideGamutMetadata("third_party/wide-gamut-tests/P3-sRGB-color-ring.png", + VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-ring.png", Primaries::kP3, &pool); - // VerifyWideGamutMetadata("third_party/wide-gamut-tests/R2020-sRGB-color-bars.png", + // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-bars.png", // Primaries::k2100, &pool); - // VerifyWideGamutMetadata("third_party/wide-gamut-tests/R2020-sRGB-color-ring.png", + // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-ring.png", // Primaries::k2100, &pool); } TEST(CodecTest, TestPNM) { TestCodecPNM(); } +TEST(CodecTest, FormatNegotiation) { + const std::vector<JxlPixelFormat> accepted_formats = { + {/*num_channels=*/4, + /*data_type=*/JXL_TYPE_UINT16, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}, + {/*num_channels=*/3, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}, + {/*num_channels=*/3, + /*data_type=*/JXL_TYPE_UINT16, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}, + {/*num_channels=*/1, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/JXL_NATIVE_ENDIAN, + /*align=*/0}, + }; + + JxlBasicInfo info; + JxlEncoderInitBasicInfo(&info); + info.bits_per_sample = 12; + info.num_color_channels = 2; + + JxlPixelFormat format; + EXPECT_FALSE(SelectFormat(accepted_formats, info, &format)); + + info.num_color_channels = 3; + ASSERT_TRUE(SelectFormat(accepted_formats, info, &format)); + EXPECT_EQ(format.num_channels, info.num_color_channels); + // 16 is the smallest accepted format that can accommodate the 12-bit data. + EXPECT_EQ(format.data_type, JXL_TYPE_UINT16); +} + +TEST(CodecTest, EncodeToPNG) { + ThreadPool* const pool = nullptr; + + std::unique_ptr<Encoder> png_encoder = Encoder::FromExtension(".png"); + ASSERT_THAT(png_encoder, NotNull()); + + const PaddedBytes original_png = + ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + PackedPixelFile ppf; + ASSERT_TRUE(extras::DecodeBytes(Span<const uint8_t>(original_png), + ColorHints(), SizeConstraints(), &ppf)); + + const JxlPixelFormat& format = ppf.frames.front().color.format; + ASSERT_THAT( + png_encoder->AcceptedFormats(), + Contains(AllOf(Field(&JxlPixelFormat::num_channels, format.num_channels), + Field(&JxlPixelFormat::data_type, format.data_type), + Field(&JxlPixelFormat::endianness, format.endianness)))); + EncodedImage encoded_png; + ASSERT_TRUE(png_encoder->Encode(ppf, &encoded_png, pool)); + EXPECT_THAT(encoded_png.icc, IsEmpty()); + ASSERT_THAT(encoded_png.bitstreams, SizeIs(1)); + + PackedPixelFile decoded_ppf; + ASSERT_TRUE( + extras::DecodeBytes(Span<const uint8_t>(encoded_png.bitstreams.front()), + ColorHints(), SizeConstraints(), &decoded_ppf)); + + ASSERT_EQ(decoded_ppf.info.bits_per_sample, ppf.info.bits_per_sample); + ASSERT_EQ(decoded_ppf.frames.size(), 1); + VerifySameImage(ppf.frames[0].color, ppf.info.bits_per_sample, + decoded_ppf.frames[0].color, + decoded_ppf.info.bits_per_sample); +} + } // namespace } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/decode.cc b/media/libjxl/src/lib/extras/dec/decode.cc index 55ce0ba3f2..8712e03aac 100644 --- a/media/libjxl/src/lib/extras/dec/decode.cc +++ b/media/libjxl/src/lib/extras/dec/decode.cc @@ -31,6 +31,25 @@ constexpr size_t kMinBytes = 9; } // namespace +std::vector<Codec> AvailableCodecs() { + std::vector<Codec> out; +#if JPEGXL_ENABLE_APNG + out.push_back(Codec::kPNG); +#endif +#if JPEGXL_ENABLE_EXR + out.push_back(Codec::kEXR); +#endif +#if JPEGXL_ENABLE_GIF + out.push_back(Codec::kGIF); +#endif +#if JPEGXL_ENABLE_JPEG + out.push_back(Codec::kJPG); +#endif + out.push_back(Codec::kPGX); + out.push_back(Codec::kPNM); + return out; +} + Codec CodecFromExtension(std::string extension, size_t* JXL_RESTRICT bits_per_sample) { std::transform( diff --git a/media/libjxl/src/lib/extras/dec/decode.h b/media/libjxl/src/lib/extras/dec/decode.h index 32ec4c6368..7f0ff70aa8 100644 --- a/media/libjxl/src/lib/extras/dec/decode.h +++ b/media/libjxl/src/lib/extras/dec/decode.h @@ -12,15 +12,12 @@ #include <stdint.h> #include <string> +#include <vector> #include "lib/extras/dec/color_hints.h" -#include "lib/jxl/base/compiler_specific.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/base/span.h" #include "lib/jxl/base/status.h" #include "lib/jxl/codec_in_out.h" -#include "lib/jxl/field_encodings.h" // MakeBit namespace jxl { namespace extras { @@ -36,27 +33,14 @@ enum class Codec : uint32_t { kEXR }; -static inline constexpr uint64_t EnumBits(Codec /*unused*/) { - // Return only fully-supported codecs (kGIF is decode-only). - return MakeBit(Codec::kPNM) -#if JPEGXL_ENABLE_APNG - | MakeBit(Codec::kPNG) -#endif -#if JPEGXL_ENABLE_JPEG - | MakeBit(Codec::kJPG) -#endif -#if JPEGXL_ENABLE_EXR - | MakeBit(Codec::kEXR) -#endif - ; -} +std::vector<Codec> AvailableCodecs(); // If and only if extension is ".pfm", *bits_per_sample is updated to 32 so // that Encode() would encode to PFM instead of PPM. Codec CodecFromExtension(std::string extension, size_t* JXL_RESTRICT bits_per_sample = nullptr); -// Decodes "bytes" and sets io->metadata.m. +// 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, diff --git a/media/libjxl/src/lib/extras/dec/jxl.cc b/media/libjxl/src/lib/extras/dec/jxl.cc new file mode 100644 index 0000000000..0e1035646d --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/jxl.cc @@ -0,0 +1,480 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/dec/jxl.h" + +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/types.h" +#include "lib/extras/dec/color_description.h" +#include "lib/extras/enc/encode.h" +#include "lib/jxl/base/printf_macros.h" + +namespace jxl { +namespace extras { +namespace { + +struct BoxProcessor { + BoxProcessor(JxlDecoder* dec) : dec_(dec) { Reset(); } + + void InitializeOutput(std::vector<uint8_t>* out) { + box_data_ = out; + AddMoreOutput(); + } + + bool AddMoreOutput() { + Flush(); + static const size_t kBoxOutputChunkSize = 1 << 16; + box_data_->resize(box_data_->size() + kBoxOutputChunkSize); + next_out_ = box_data_->data() + total_size_; + avail_out_ = box_data_->size() - total_size_; + if (JXL_DEC_SUCCESS != + JxlDecoderSetBoxBuffer(dec_, next_out_, avail_out_)) { + fprintf(stderr, "JxlDecoderSetBoxBuffer failed\n"); + return false; + } + return true; + } + + void FinalizeOutput() { + if (box_data_ == nullptr) return; + Flush(); + box_data_->resize(total_size_); + Reset(); + } + + private: + JxlDecoder* dec_; + std::vector<uint8_t>* box_data_; + uint8_t* next_out_; + size_t avail_out_; + size_t total_size_; + + void Reset() { + box_data_ = nullptr; + next_out_ = nullptr; + avail_out_ = 0; + total_size_ = 0; + } + void Flush() { + if (box_data_ == nullptr) return; + size_t remaining = JxlDecoderReleaseBoxBuffer(dec_); + size_t bytes_written = avail_out_ - remaining; + next_out_ += bytes_written; + avail_out_ -= bytes_written; + total_size_ += bytes_written; + } +}; + +} // namespace + +bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, + const JXLDecompressParams& dparams, size_t* decoded_bytes, + PackedPixelFile* ppf, std::vector<uint8_t>* jpeg_bytes) { + auto decoder = JxlDecoderMake(/*memory_manager=*/nullptr); + JxlDecoder* dec = decoder.get(); + ppf->frames.clear(); + + if (dparams.runner_opaque != nullptr && + JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec, dparams.runner, + dparams.runner_opaque)) { + fprintf(stderr, "JxlEncoderSetParallelRunner failed\n"); + return false; + } + + JxlPixelFormat format; + std::vector<JxlPixelFormat> accepted_formats = dparams.accepted_formats; + if (accepted_formats.empty()) { + for (const uint32_t num_channels : {1, 2, 3, 4}) { + accepted_formats.push_back( + {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0}); + } + } + JxlColorEncoding color_encoding; + size_t num_color_channels = 0; + if (!dparams.color_space.empty()) { + if (!jxl::ParseDescription(dparams.color_space, &color_encoding)) { + fprintf(stderr, "Failed to parse color space %s.\n", + dparams.color_space.c_str()); + return false; + } + num_color_channels = + color_encoding.color_space == JXL_COLOR_SPACE_GRAY ? 1 : 3; + } + + bool can_reconstruct_jpeg = false; + std::vector<uint8_t> jpeg_data_chunk; + if (jpeg_bytes != nullptr) { + jpeg_data_chunk.resize(16384); + jpeg_bytes->resize(0); + } + + int events = (JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE); + + bool max_passes_defined = + (dparams.max_passes < std::numeric_limits<uint32_t>::max()); + if (max_passes_defined || dparams.max_downsampling > 1) { + events |= JXL_DEC_FRAME_PROGRESSION; + if (max_passes_defined) { + JxlDecoderSetProgressiveDetail(dec, JxlProgressiveDetail::kPasses); + } else { + JxlDecoderSetProgressiveDetail(dec, JxlProgressiveDetail::kLastPasses); + } + } + if (jpeg_bytes != nullptr) { + events |= JXL_DEC_JPEG_RECONSTRUCTION; + } else { + events |= (JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_PREVIEW_IMAGE | + JXL_DEC_BOX); + } + if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec, events)) { + fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); + return false; + } + if (jpeg_bytes == nullptr) { + if (JXL_DEC_SUCCESS != + JxlDecoderSetRenderSpotcolors(dec, dparams.render_spotcolors)) { + fprintf(stderr, "JxlDecoderSetRenderSpotColors failed\n"); + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetKeepOrientation(dec, dparams.keep_orientation)) { + fprintf(stderr, "JxlDecoderSetKeepOrientation failed\n"); + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetUnpremultiplyAlpha(dec, dparams.unpremultiply_alpha)) { + fprintf(stderr, "JxlDecoderSetUnpremultiplyAlpha failed\n"); + return false; + } + if (dparams.display_nits > 0 && + JXL_DEC_SUCCESS != + JxlDecoderSetDesiredIntensityTarget(dec, dparams.display_nits)) { + fprintf(stderr, "Decoder failed to set desired intensity target\n"); + return false; + } + if (JXL_DEC_SUCCESS != JxlDecoderSetDecompressBoxes(dec, JXL_TRUE)) { + fprintf(stderr, "JxlDecoderSetDecompressBoxes failed\n"); + return false; + } + } + if (JXL_DEC_SUCCESS != JxlDecoderSetInput(dec, bytes, bytes_size)) { + fprintf(stderr, "Decoder failed to set input\n"); + return false; + } + uint32_t progression_index = 0; + bool codestream_done = false; + BoxProcessor boxes(dec); + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + if (status == JXL_DEC_ERROR) { + fprintf(stderr, "Failed to decode image\n"); + return false; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + if (codestream_done) { + break; + } + if (dparams.allow_partial_input) { + if (JXL_DEC_SUCCESS != JxlDecoderFlushImage(dec)) { + fprintf(stderr, + "Input file is truncated and there is no preview " + "available yet.\n"); + return false; + } + break; + } + fprintf(stderr, + "Input file is truncated and allow_partial_input was disabled."); + return false; + } else if (status == JXL_DEC_BOX) { + boxes.FinalizeOutput(); + JxlBoxType box_type; + if (JXL_DEC_SUCCESS != JxlDecoderGetBoxType(dec, box_type, JXL_TRUE)) { + fprintf(stderr, "JxlDecoderGetBoxType failed\n"); + return false; + } + std::vector<uint8_t>* box_data = nullptr; + if (memcmp(box_type, "Exif", 4) == 0) { + box_data = &ppf->metadata.exif; + } else if (memcmp(box_type, "iptc", 4) == 0) { + box_data = &ppf->metadata.iptc; + } else if (memcmp(box_type, "jumb", 4) == 0) { + box_data = &ppf->metadata.jumbf; + } else if (memcmp(box_type, "xml ", 4) == 0) { + box_data = &ppf->metadata.xmp; + } + if (box_data) { + boxes.InitializeOutput(box_data); + } + } else if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) { + boxes.AddMoreOutput(); + } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { + can_reconstruct_jpeg = true; + // Decoding to JPEG. + if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec, + jpeg_data_chunk.data(), + jpeg_data_chunk.size())) { + fprintf(stderr, "Decoder failed to set JPEG Buffer\n"); + return false; + } + } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + // Decoded a chunk to JPEG. + size_t used_jpeg_output = + jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec); + jpeg_bytes->insert(jpeg_bytes->end(), jpeg_data_chunk.data(), + jpeg_data_chunk.data() + used_jpeg_output); + if (used_jpeg_output == 0) { + // Chunk is too small. + jpeg_data_chunk.resize(jpeg_data_chunk.size() * 2); + } + if (JXL_DEC_SUCCESS != JxlDecoderSetJPEGBuffer(dec, + jpeg_data_chunk.data(), + jpeg_data_chunk.size())) { + fprintf(stderr, "Decoder failed to set JPEG Buffer\n"); + return false; + } + } else if (status == JXL_DEC_BASIC_INFO) { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec, &ppf->info)) { + fprintf(stderr, "JxlDecoderGetBasicInfo failed\n"); + return false; + } + if (num_color_channels != 0) { + // Mark the change in number of color channels due to the requested + // color space. + ppf->info.num_color_channels = num_color_channels; + } + // Select format according to accepted formats. + if (!jxl::extras::SelectFormat(accepted_formats, ppf->info, &format)) { + fprintf(stderr, "SelectFormat failed\n"); + return false; + } + bool have_alpha = (format.num_channels == 2 || format.num_channels == 4); + if (!have_alpha) { + // Mark in the basic info that alpha channel was dropped. + ppf->info.alpha_bits = 0; + } else if (dparams.unpremultiply_alpha) { + // Mark in the basic info that alpha was unpremultiplied. + ppf->info.alpha_premultiplied = false; + } + bool alpha_found = false; + for (uint32_t i = 0; i < ppf->info.num_extra_channels; ++i) { + JxlExtraChannelInfo eci; + if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &eci)) { + fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n"); + return false; + } + if (eci.type == JXL_CHANNEL_ALPHA && have_alpha && !alpha_found) { + // Skip the first alpha channels because it is already present in the + // interleaved image. + alpha_found = true; + continue; + } + std::string name(eci.name_length + 1, 0); + if (JXL_DEC_SUCCESS != + JxlDecoderGetExtraChannelName(dec, i, &name[0], name.size())) { + fprintf(stderr, "JxlDecoderGetExtraChannelName failed\n"); + return false; + } + name.resize(eci.name_length); + ppf->extra_channels_info.push_back({eci, i, name}); + } + } else if (status == JXL_DEC_COLOR_ENCODING) { + if (!dparams.color_space.empty()) { + if (ppf->info.uses_original_profile) { + fprintf(stderr, + "Warning: --color_space ignored because the image is " + "not XYB encoded.\n"); + } else { + if (JXL_DEC_SUCCESS != + JxlDecoderSetPreferredColorProfile(dec, &color_encoding)) { + fprintf(stderr, "Failed to set color space.\n"); + return false; + } + } + } + size_t icc_size = 0; + JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_DATA; + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) { + fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); + } + if (icc_size != 0) { + ppf->icc.resize(icc_size); + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsICCProfile(dec, nullptr, target, + ppf->icc.data(), icc_size)) { + fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); + return false; + } + } + if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsEncodedProfile( + dec, nullptr, target, &ppf->color_encoding)) { + ppf->color_encoding.color_space = JXL_COLOR_SPACE_UNKNOWN; + } + icc_size = 0; + target = JXL_COLOR_PROFILE_TARGET_ORIGINAL; + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) { + fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); + } + if (icc_size != 0) { + ppf->orig_icc.resize(icc_size); + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsICCProfile(dec, nullptr, target, + ppf->orig_icc.data(), icc_size)) { + fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); + return false; + } + } + } else if (status == JXL_DEC_FRAME) { + jxl::extras::PackedFrame frame(ppf->info.xsize, ppf->info.ysize, format); + if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &frame.frame_info)) { + fprintf(stderr, "JxlDecoderGetFrameHeader failed\n"); + return false; + } + frame.name.resize(frame.frame_info.name_length + 1, 0); + if (JXL_DEC_SUCCESS != + JxlDecoderGetFrameName(dec, &frame.name[0], frame.name.size())) { + fprintf(stderr, "JxlDecoderGetFrameName failed\n"); + return false; + } + frame.name.resize(frame.frame_info.name_length); + ppf->frames.emplace_back(std::move(frame)); + progression_index = 0; + } else if (status == JXL_DEC_FRAME_PROGRESSION) { + size_t downsampling = JxlDecoderGetIntendedDownsamplingRatio(dec); + if ((max_passes_defined && progression_index >= dparams.max_passes) || + (!max_passes_defined && downsampling <= dparams.max_downsampling)) { + if (JXL_DEC_SUCCESS != JxlDecoderFlushImage(dec)) { + fprintf(stderr, "JxlDecoderFlushImage failed\n"); + return false; + } + if (ppf->frames.back().frame_info.is_last) { + break; + } + if (JXL_DEC_SUCCESS != JxlDecoderSkipCurrentFrame(dec)) { + fprintf(stderr, "JxlDecoderSkipCurrentFrame failed\n"); + return false; + } + } + ++progression_index; + } else if (status == JXL_DEC_NEED_PREVIEW_OUT_BUFFER) { + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderPreviewOutBufferSize(dec, &format, &buffer_size)) { + fprintf(stderr, "JxlDecoderPreviewOutBufferSize failed\n"); + return false; + } + ppf->preview_frame = std::unique_ptr<jxl::extras::PackedFrame>( + new jxl::extras::PackedFrame(ppf->info.preview.xsize, + ppf->info.preview.ysize, format)); + if (buffer_size != ppf->preview_frame->color.pixels_size) { + fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n", + buffer_size, ppf->preview_frame->color.pixels_size); + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetPreviewOutBuffer( + dec, &format, ppf->preview_frame->color.pixels(), buffer_size)) { + fprintf(stderr, "JxlDecoderSetPreviewOutBuffer failed\n"); + return false; + } + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + if (jpeg_bytes != nullptr) { + break; + } + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)) { + fprintf(stderr, "JxlDecoderImageOutBufferSize failed\n"); + return false; + } + jxl::extras::PackedFrame& frame = ppf->frames.back(); + if (buffer_size != frame.color.pixels_size) { + fprintf(stderr, "Invalid out buffer size %" PRIuS " %" PRIuS "\n", + buffer_size, frame.color.pixels_size); + return false; + } + + if (dparams.use_image_callback) { + auto callback = [](void* opaque, size_t x, size_t y, size_t num_pixels, + const void* pixels) { + auto* ppf = reinterpret_cast<jxl::extras::PackedPixelFile*>(opaque); + jxl::extras::PackedImage& color = ppf->frames.back().color; + uint8_t* pixels_buffer = reinterpret_cast<uint8_t*>(color.pixels()); + size_t sample_size = color.pixel_stride(); + memcpy(pixels_buffer + (color.stride * y + sample_size * x), pixels, + num_pixels * sample_size); + }; + if (JXL_DEC_SUCCESS != + JxlDecoderSetImageOutCallback(dec, &format, callback, ppf)) { + fprintf(stderr, "JxlDecoderSetImageOutCallback failed\n"); + return false; + } + } else { + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec, &format, + frame.color.pixels(), + buffer_size)) { + fprintf(stderr, "JxlDecoderSetImageOutBuffer failed\n"); + return false; + } + } + JxlPixelFormat ec_format = format; + ec_format.num_channels = 1; + for (const auto& eci : ppf->extra_channels_info) { + frame.extra_channels.emplace_back(jxl::extras::PackedImage( + ppf->info.xsize, ppf->info.ysize, ec_format)); + auto& ec = frame.extra_channels.back(); + size_t buffer_size; + if (JXL_DEC_SUCCESS != JxlDecoderExtraChannelBufferSize( + dec, &ec_format, &buffer_size, eci.index)) { + fprintf(stderr, "JxlDecoderExtraChannelBufferSize failed\n"); + return false; + } + if (buffer_size != ec.pixels_size) { + fprintf(stderr, + "Invalid extra channel buffer size" + " %" PRIuS " %" PRIuS "\n", + buffer_size, ec.pixels_size); + return false; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetExtraChannelBuffer(dec, &ec_format, ec.pixels(), + buffer_size, eci.index)) { + fprintf(stderr, "JxlDecoderSetExtraChannelBuffer failed\n"); + return false; + } + } + } else if (status == JXL_DEC_SUCCESS) { + // Decoding finished successfully. + break; + } else if (status == JXL_DEC_PREVIEW_IMAGE) { + // Nothing to do. + } else if (status == JXL_DEC_FULL_IMAGE) { + if (jpeg_bytes != nullptr || ppf->frames.back().frame_info.is_last) { + codestream_done = true; + } + } else { + fprintf(stderr, "Error: unexpected status: %d\n", + static_cast<int>(status)); + return false; + } + } + boxes.FinalizeOutput(); + if (jpeg_bytes != nullptr) { + if (!can_reconstruct_jpeg) return false; + size_t used_jpeg_output = + jpeg_data_chunk.size() - JxlDecoderReleaseJPEGBuffer(dec); + jpeg_bytes->insert(jpeg_bytes->end(), jpeg_data_chunk.data(), + jpeg_data_chunk.data() + used_jpeg_output); + } + if (decoded_bytes) { + *decoded_bytes = bytes_size - JxlDecoderReleaseInput(dec); + } + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/dec/jxl.h b/media/libjxl/src/lib/extras/dec/jxl.h new file mode 100644 index 0000000000..c462fa4b74 --- /dev/null +++ b/media/libjxl/src/lib/extras/dec/jxl.h @@ -0,0 +1,66 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_DEC_JXL_H_ +#define LIB_EXTRAS_DEC_JXL_H_ + +// Decodes JPEG XL images in memory. + +#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 { +namespace extras { + +struct JXLDecompressParams { + // If empty, little endian float formats will be accepted. + std::vector<JxlPixelFormat> accepted_formats; + + // Requested output color space description. + std::string color_space; + // If set, performs tone mapping to this intensity target luminance. + float display_nits = 0.0; + // Whether spot colors are rendered on the image. + bool render_spotcolors = true; + // Whether to keep or undo the orientation given in the header. + bool keep_orientation = false; + + // If runner_opaque is set, the decoder uses this parallel runner. + JxlParallelRunner runner; + void* runner_opaque = nullptr; + + // Whether truncated input should be treated as an error. + bool allow_partial_input = false; + + // How many passes to decode at most. By default, decode everything. + uint32_t max_passes = std::numeric_limits<uint32_t>::max(); + + // Alternatively, one can specify the maximum tolerable downscaling factor + // with respect to the full size of the image. By default, nothing less than + // the full size is requested. + size_t max_downsampling = 1; + + // Whether to use the image callback or the image buffer to get the output. + bool use_image_callback = true; + // Whether to unpremultiply colors for associated alpha channels. + bool unpremultiply_alpha = false; +}; + +bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size, + const JXLDecompressParams& dparams, size_t* decoded_bytes, + PackedPixelFile* ppf, + std::vector<uint8_t>* jpeg_bytes = nullptr); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_DEC_JXL_H_ diff --git a/media/libjxl/src/lib/extras/dec/pnm.cc b/media/libjxl/src/lib/extras/dec/pnm.cc index 93a42be321..03aecef29c 100644 --- a/media/libjxl/src/lib/extras/dec/pnm.cc +++ b/media/libjxl/src/lib/extras/dec/pnm.cc @@ -366,13 +366,18 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes, ppf->frames.emplace_back(header.xsize, header.ysize, format); auto* frame = &ppf->frames.back(); - frame->color.bitdepth_from_format = false; - frame->color.flipped_y = header.bits_per_sample == 32; // PFMs are flipped size_t pnm_remaining_size = bytes.data() + bytes.size() - pos; if (pnm_remaining_size < frame->color.pixels_size) { return JXL_FAILURE("PNM file too small"); } - memcpy(frame->color.pixels(), pos, frame->color.pixels_size); + 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); + } return true; } diff --git a/media/libjxl/src/lib/extras/enc/apng.cc b/media/libjxl/src/lib/extras/enc/apng.cc index 372b3b32ea..db6cf9ef4a 100644 --- a/media/libjxl/src/lib/extras/enc/apng.cc +++ b/media/libjxl/src/lib/extras/enc/apng.cc @@ -42,18 +42,9 @@ #include <string> #include <vector> -#include "jxl/encode.h" -#include "lib/jxl/base/compiler_specific.h" +#include "lib/extras/exif.h" +#include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/printf_macros.h" -#include "lib/jxl/color_encoding_internal.h" -#include "lib/jxl/dec_external_image.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_image_bundle.h" -#include "lib/jxl/exif.h" -#include "lib/jxl/frame_header.h" -#include "lib/jxl/headers.h" -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" #include "png.h" /* original (unpatched) libpng is ok */ namespace jxl { @@ -61,15 +52,44 @@ namespace extras { namespace { +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}); + } + } + return formats; + } + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + encoded_image->icc.clear(); + encoded_image->bitstreams.resize(1); + return EncodePackedPixelFileToAPNG(ppf, pool, + &encoded_image->bitstreams.front()); + } + + private: + Status EncodePackedPixelFileToAPNG(const PackedPixelFile& ppf, + ThreadPool* pool, + std::vector<uint8_t>* bytes) const; +}; + static void PngWrite(png_structp png_ptr, png_bytep data, png_size_t length) { - PaddedBytes* bytes = static_cast<PaddedBytes*>(png_get_io_ptr(png_ptr)); - bytes->append(data, data + length); + std::vector<uint8_t>* bytes = + static_cast<std::vector<uint8_t>*>(png_get_io_ptr(png_ptr)); + bytes->insert(bytes->end(), data, data + length); } // Stores XMP and EXIF/IPTC into key/value strings for PNG class BlobsWriterPNG { public: - static Status Encode(const Blobs& blobs, std::vector<std::string>* strings) { + static Status Encode(const PackedMetadata& blobs, + std::vector<std::string>* strings) { if (!blobs.exif.empty()) { // PNG viewers typically ignore Exif orientation but not all of them do // (and e.g. cjxl doesn't), so we overwrite the Exif orientation to the @@ -122,26 +142,112 @@ class BlobsWriterPNG { } }; -} // namespace +void MaybeAddCICP(JxlColorEncoding c_enc, png_structp png_ptr, + png_infop info_ptr) { + png_byte cicp_data[4] = {}; + png_unknown_chunk cicp_chunk; + if (c_enc.color_space != JXL_COLOR_SPACE_RGB) { + return; + } + if (c_enc.primaries == JXL_PRIMARIES_P3) { + if (c_enc.white_point == JXL_WHITE_POINT_D65) { + cicp_data[0] = 12; + } else if (c_enc.white_point == JXL_WHITE_POINT_DCI) { + cicp_data[0] = 11; + } else { + return; + } + } else if (c_enc.primaries != JXL_PRIMARIES_CUSTOM && + c_enc.white_point == JXL_WHITE_POINT_D65) { + cicp_data[0] = static_cast<png_byte>(c_enc.primaries); + } else { + return; + } + if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_UNKNOWN || + c_enc.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) { + return; + } + cicp_data[1] = static_cast<png_byte>(c_enc.transfer_function); + cicp_data[2] = 0; + cicp_data[3] = 1; + cicp_chunk.data = cicp_data; + cicp_chunk.size = sizeof(cicp_data); + cicp_chunk.location = PNG_HAVE_PLTE; + memcpy(cicp_chunk.name, "cICP", 5); + png_set_keep_unknown_chunks(png_ptr, 3, + reinterpret_cast<const png_byte*>("cICP"), 1); + png_set_unknown_chunks(png_ptr, info_ptr, &cicp_chunk, 1); +} -Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes) { - if (bits_per_sample > 8) { - bits_per_sample = 16; - } else if (bits_per_sample < 8) { - // PNG can also do 4, 2, and 1 bits per sample, but it isn't implemented - bits_per_sample = 8; +Status APNGEncoder::EncodePackedPixelFileToAPNG( + const PackedPixelFile& ppf, ThreadPool* pool, + std::vector<uint8_t>* bytes) const { + size_t xsize = ppf.info.xsize; + size_t ysize = ppf.info.ysize; + bool has_alpha = ppf.info.alpha_bits != 0; + bool is_gray = ppf.info.num_color_channels == 1; + size_t color_channels = ppf.info.num_color_channels; + size_t num_channels = color_channels + (has_alpha ? 1 : 0); + size_t num_samples = num_channels * xsize * ysize; + + if (!ppf.info.have_animation && ppf.frames.size() != 1) { + return JXL_FAILURE("Invalid number of frames"); } size_t count = 0; - bool have_anim = io->metadata.m.have_animation; size_t anim_chunks = 0; - int W = 0, H = 0; - for (size_t i = 0; i < io->frames.size(); i++) { - auto& frame = io->frames[i]; - if (!have_anim && i + 1 < io->frames.size()) continue; + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + + const PackedImage& color = frame.color; + const JxlPixelFormat format = color.format; + const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels()); + size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type); + size_t bytes_per_sample = data_bits_per_sample / 8; + size_t out_bytes_per_sample = bytes_per_sample > 1 ? 2 : 1; + size_t out_stride = xsize * num_channels * out_bytes_per_sample; + size_t out_size = ysize * out_stride; + std::vector<uint8_t> out(out_size); + + if (format.data_type == JXL_TYPE_UINT8) { + if (ppf.info.bits_per_sample < 8) { + float mul = 255.0 / ((1u << ppf.info.bits_per_sample) - 1); + for (size_t i = 0; i < num_samples; ++i) { + out[i] = static_cast<uint8_t>(in[i] * mul + 0.5); + } + } else { + memcpy(&out[0], in, out_size); + } + } else if (format.data_type == JXL_TYPE_UINT16) { + if (ppf.info.bits_per_sample < 16 || + format.endianness != JXL_BIG_ENDIAN) { + float mul = 65535.0 / ((1u << ppf.info.bits_per_sample) - 1); + const uint8_t* p_in = in; + uint8_t* p_out = out.data(); + for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) { + uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE16(p_in) + : LoadLE16(p_in)); + StoreBE16(static_cast<uint32_t>(val * mul + 0.5), p_out); + } + } else { + memcpy(&out[0], in, out_size); + } + } 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; @@ -155,46 +261,24 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, png_set_write_fn(png_ptr, bytes, PngWrite, NULL); png_set_flush(png_ptr, 0); - ImageBundle ib = frame.Copy(); - const size_t alpha_bits = ib.HasAlpha() ? bits_per_sample : 0; - ImageMetadata metadata = io->metadata.m; - ImageBundle store(&metadata); - const ImageBundle* transformed; - JXL_RETURN_IF_ERROR(TransformIfNeeded(ib, c_desired, GetJxlCms(), pool, - &store, &transformed)); - size_t stride = ib.oriented_xsize() * - DivCeil(c_desired.Channels() * bits_per_sample + alpha_bits, - kBitsPerByte); - std::vector<uint8_t> raw_bytes(stride * ib.oriented_ysize()); - JXL_RETURN_IF_ERROR(ConvertToExternal( - *transformed, bits_per_sample, /*float_out=*/false, - c_desired.Channels() + (ib.HasAlpha() ? 1 : 0), JXL_BIG_ENDIAN, stride, - pool, raw_bytes.data(), raw_bytes.size(), - /*out_callback=*/{}, metadata.GetOrientation())); - - int width = ib.oriented_xsize(); - int height = ib.oriented_ysize(); - - png_byte color_type = - (c_desired.Channels() == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_GRAY); - if (ib.HasAlpha()) color_type |= PNG_COLOR_MASK_ALPHA; - png_byte bit_depth = bits_per_sample; + int width = xsize; + int height = ysize; + + png_byte color_type = (is_gray ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB); + if (has_alpha) color_type |= PNG_COLOR_MASK_ALPHA; + png_byte bit_depth = out_bytes_per_sample * 8; png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); if (count == 0) { - W = width; - H = height; - - // TODO(jon): instead of always setting an iCCP, could try to avoid that - // have to avoid warnings on the ICC profile becoming fatal - png_set_benign_errors(png_ptr, 1); - png_set_iCCP(png_ptr, info_ptr, "1", 0, c_desired.ICC().data(), - c_desired.ICC().size()); - + MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr); + if (!ppf.icc.empty()) { + png_set_benign_errors(png_ptr, 1); + png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(), ppf.icc.size()); + } std::vector<std::string> textstrings; - JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(io->blobs, &textstrings)); + JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings)); for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) { png_text text; text.key = const_cast<png_charp>(textstrings[kk].c_str()); @@ -211,27 +295,24 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, bytes->resize(pos); } - if (have_anim) { + if (ppf.info.have_animation) { if (count == 0) { png_byte adata[8]; - png_save_uint_32(adata, io->frames.size()); - png_save_uint_32(adata + 4, io->metadata.m.animation.num_loops); + png_save_uint_32(adata, ppf.frames.size()); + png_save_uint_32(adata + 4, ppf.info.animation.num_loops); png_byte actl[5] = "acTL"; png_write_chunk(png_ptr, actl, adata, 8); } png_byte fdata[26]; - JXL_ASSERT(W == width); - JXL_ASSERT(H == height); // TODO(jon): also make this work for the non-coalesced case png_save_uint_32(fdata, anim_chunks++); png_save_uint_32(fdata + 4, width); png_save_uint_32(fdata + 8, height); png_save_uint_32(fdata + 12, 0); png_save_uint_32(fdata + 16, 0); - png_save_uint_16( - fdata + 20, - frame.duration * io->metadata.m.animation.tps_denominator); - png_save_uint_16(fdata + 22, io->metadata.m.animation.tps_numerator); + png_save_uint_16(fdata + 20, frame.frame_info.duration * + ppf.info.animation.tps_denominator); + png_save_uint_16(fdata + 22, ppf.info.animation.tps_numerator); fdata[24] = 1; fdata[25] = 0; png_byte fctl[5] = "fcTL"; @@ -240,7 +321,7 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, std::vector<uint8_t*> rows(height); for (int y = 0; y < height; ++y) { - rows[y] = raw_bytes.data() + y * stride; + rows[y] = out.data() + y * out_stride; } png_write_flush(png_ptr); @@ -268,7 +349,9 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, } count++; - if (count == io->frames.size() || !have_anim) png_write_end(png_ptr, NULL); + if (count == ppf.frames.size() || !ppf.info.have_animation) { + png_write_end(png_ptr, NULL); + } png_destroy_write_struct(&png_ptr, &info_ptr); } @@ -276,5 +359,11 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, return true; } +} // namespace + +std::unique_ptr<Encoder> GetAPNGEncoder() { + return jxl::make_unique<APNGEncoder>(); +} + } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/apng.h b/media/libjxl/src/lib/extras/enc/apng.h index 7f0fb94d9b..2a2139c8fa 100644 --- a/media/libjxl/src/lib/extras/enc/apng.h +++ b/media/libjxl/src/lib/extras/enc/apng.h @@ -8,19 +8,14 @@ // Encodes APNG images in memory. -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/span.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/codec_in_out.h" +#include <memory> + +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -// Encodes `io` into `bytes`. -Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes); +std::unique_ptr<Encoder> GetAPNGEncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/encode.cc b/media/libjxl/src/lib/extras/enc/encode.cc new file mode 100644 index 0000000000..dc593d2900 --- /dev/null +++ b/media/libjxl/src/lib/extras/enc/encode.cc @@ -0,0 +1,136 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/enc/encode.h" + +#include <locale> + +#if JPEGXL_ENABLE_APNG +#include "lib/extras/enc/apng.h" +#endif +#if JPEGXL_ENABLE_EXR +#include "lib/extras/enc/exr.h" +#endif +#if JPEGXL_ENABLE_JPEG +#include "lib/extras/enc/jpg.h" +#endif +#include "lib/extras/enc/npy.h" +#include "lib/extras/enc/pgx.h" +#include "lib/extras/enc/pnm.h" +#include "lib/jxl/base/printf_macros.h" + +namespace jxl { +namespace extras { + +Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) const { + if (info.xsize == 0 || info.ysize == 0) { + return JXL_FAILURE("Empty image"); + } + if (info.num_color_channels != 1 && info.num_color_channels != 3) { + return JXL_FAILURE("Invalid number of color channels"); + } + if (info.alpha_bits > 0 && info.alpha_bits != info.bits_per_sample) { + return JXL_FAILURE("Alpha bit depth does not match image bit depth"); + } + if (info.orientation != JXL_ORIENT_IDENTITY) { + return JXL_FAILURE("Orientation must be identity"); + } + return true; +} + +Status Encoder::VerifyPackedImage(const PackedImage& image, + const JxlBasicInfo& info) const { + if (image.pixels() == nullptr) { + return JXL_FAILURE("Invalid image."); + } + if (image.stride != image.xsize * image.pixel_stride()) { + return JXL_FAILURE("Invalid image stride."); + } + if (image.pixels_size != image.ysize * image.stride) { + return JXL_FAILURE("Invalid image size."); + } + size_t info_num_channels = + (info.num_color_channels + (info.alpha_bits > 0 ? 1 : 0)); + if (image.xsize != info.xsize || image.ysize != info.ysize || + image.format.num_channels != info_num_channels) { + return JXL_FAILURE("Frame size does not match image size"); + } + 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); + } + return true; +} + +std::unique_ptr<Encoder> Encoder::FromExtension(std::string extension) { + std::transform( + extension.begin(), extension.end(), extension.begin(), + [](char c) { return std::tolower(c, std::locale::classic()); }); +#if JPEGXL_ENABLE_APNG + if (extension == ".png" || extension == ".apng") return GetAPNGEncoder(); +#endif + +#if JPEGXL_ENABLE_JPEG + if (extension == ".jpg") return GetJPEGEncoder(); + if (extension == ".jpeg") return GetJPEGEncoder(); +#endif + + if (extension == ".npy") return GetNumPyEncoder(); + + if (extension == ".pgx") return GetPGXEncoder(); + + if (extension == ".pam") return GetPAMEncoder(); + if (extension == ".pgm") return GetPGMEncoder(); + if (extension == ".ppm") return GetPPMEncoder(); + if (extension == ".pfm") return GetPFMEncoder(); + +#if JPEGXL_ENABLE_EXR + if (extension == ".exr") return GetEXREncoder(); +#endif + + return nullptr; +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/encode.h b/media/libjxl/src/lib/extras/enc/encode.h new file mode 100644 index 0000000000..92eec50b6f --- /dev/null +++ b/media/libjxl/src/lib/extras/enc/encode.h @@ -0,0 +1,77 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_ENC_ENCODE_H_ +#define LIB_EXTRAS_ENC_ENCODE_H_ + +// Facade for image encoders. + +#include <string> +#include <unordered_map> + +#include "lib/extras/dec/decode.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace extras { + +struct EncodedImage { + // One (if the format supports animations or the image has only one frame) or + // more sequential bitstreams. + std::vector<std::vector<uint8_t>> bitstreams; + + // For each extra channel one or more sequential bitstreams. + std::vector<std::vector<std::vector<uint8_t>>> extra_channel_bitstreams; + + std::vector<uint8_t> preview_bitstream; + + // If the format does not support embedding color profiles into the bitstreams + // above, it will be present here, to be written as a separate file. If it + // does support them, this field will be empty. + std::vector<uint8_t> icc; + + // Additional output for conformance testing, only filled in by NumPyEncoder. + std::vector<uint8_t> metadata; +}; + +class Encoder { + public: + static std::unique_ptr<Encoder> FromExtension(std::string extension); + + virtual ~Encoder() = default; + + virtual std::vector<JxlPixelFormat> AcceptedFormats() const = 0; + + // Any existing data in encoded_image is discarded. + virtual Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const = 0; + + void SetOption(std::string name, std::string value) { + options_[std::move(name)] = std::move(value); + } + + protected: + const std::unordered_map<std::string, std::string>& options() const { + return options_; + } + + Status VerifyBasicInfo(const JxlBasicInfo& info) const; + + Status VerifyPackedImage(const PackedImage& image, + const JxlBasicInfo& info) const; + + private: + std::unordered_map<std::string, std::string> options_; +}; + +// TODO(sboukortt): consider exposing this as part of the C API. +Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats, + const JxlBasicInfo& basic_info, JxlPixelFormat* format); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ENC_ENCODE_H_ diff --git a/media/libjxl/src/lib/extras/enc/exr.cc b/media/libjxl/src/lib/extras/enc/exr.cc index 3d88404888..05e05f96ce 100644 --- a/media/libjxl/src/lib/extras/enc/exr.cc +++ b/media/libjxl/src/lib/extras/enc/exr.cc @@ -12,11 +12,9 @@ #include <vector> -#include "lib/jxl/alpha.h" -#include "lib/jxl/color_encoding_internal.h" -#include "lib/jxl/color_management.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_image_bundle.h" +#include "jxl/codestream_header.h" +#include "lib/extras/packed_image.h" +#include "lib/jxl/base/byte_order.h" namespace jxl { namespace extras { @@ -32,22 +30,10 @@ namespace Imath = IMATH_NAMESPACE; // to uint64_t. This alternative should work in all cases. using ExrInt64 = decltype(std::declval<OpenEXR::IStream>().tellg()); -size_t GetNumThreads(ThreadPool* pool) { - size_t exr_num_threads = 1; - JXL_CHECK(RunOnPool( - pool, 0, 1, - [&](size_t num_threads) { - exr_num_threads = num_threads; - return true; - }, - [&](uint32_t /* task */, size_t /*thread*/) {}, "DecodeImageEXRThreads")); - return exr_num_threads; -} - class InMemoryOStream : public OpenEXR::OStream { public: // `bytes` must outlive the InMemoryOStream. - explicit InMemoryOStream(PaddedBytes* const bytes) + explicit InMemoryOStream(std::vector<uint8_t>* const bytes) : OStream(/*fileName=*/""), bytes_(*bytes) {} void write(const char c[], const int n) override { @@ -67,42 +53,69 @@ class InMemoryOStream : public OpenEXR::OStream { } private: - PaddedBytes& bytes_; + std::vector<uint8_t>& bytes_; size_t pos_ = 0; }; -} // namespace +// Loads a Big-Endian float +float LoadBEFloat(const uint8_t* p) { + uint32_t u = LoadBE32(p); + float result; + memcpy(&result, &u, 4); + return result; +} + +// Loads a Little-Endian float +float LoadLEFloat(const uint8_t* p) { + uint32_t u = LoadLE32(p); + float result; + memcpy(&result, &u, 4); + return result; +} + +Status EncodeImageEXR(const PackedImage& image, const JxlBasicInfo& info, + const JxlColorEncoding& c_enc, ThreadPool* pool, + std::vector<uint8_t>* bytes) { + OpenEXR::setGlobalThreadCount(0); + + const size_t xsize = info.xsize; + const size_t ysize = info.ysize; + const bool has_alpha = info.alpha_bits > 0; + const bool alpha_is_premultiplied = info.alpha_premultiplied; + + if (info.num_color_channels != 3 || + c_enc.color_space != JXL_COLOR_SPACE_RGB || + c_enc.transfer_function != JXL_TRANSFER_FUNCTION_LINEAR) { + return JXL_FAILURE("Unsupported color encoding for OpenEXR output."); + } + + const size_t num_channels = 3 + (has_alpha ? 1 : 0); + const JxlPixelFormat format = image.format; + + if (format.data_type != JXL_TYPE_FLOAT) { + return JXL_FAILURE("Unsupported pixel format for OpenEXR output"); + } -Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, - ThreadPool* pool, PaddedBytes* bytes) { - // As in `DecodeImageEXR`, `pool` is only used for pixel conversion, not for - // actual OpenEXR I/O. - OpenEXR::setGlobalThreadCount(GetNumThreads(pool)); - - ColorEncoding c_linear = c_desired; - c_linear.tf.SetTransferFunction(TransferFunction::kLinear); - JXL_RETURN_IF_ERROR(c_linear.CreateICC()); - ImageMetadata metadata = io->metadata.m; - ImageBundle store(&metadata); - const ImageBundle* linear; - JXL_RETURN_IF_ERROR(TransformIfNeeded(io->Main(), c_linear, GetJxlCms(), pool, - &store, &linear)); - - const bool has_alpha = io->Main().HasAlpha(); - const bool alpha_is_premultiplied = io->Main().AlphaIsPremultiplied(); - - OpenEXR::Header header(io->xsize(), io->ysize()); - const PrimariesCIExy& primaries = c_linear.HasPrimaries() - ? c_linear.GetPrimaries() - : ColorEncoding::SRGB().GetPrimaries(); + const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels()); + size_t in_stride = num_channels * 4 * xsize; + + OpenEXR::Header header(xsize, ysize); OpenEXR::Chromaticities chromaticities; - chromaticities.red = Imath::V2f(primaries.r.x, primaries.r.y); - chromaticities.green = Imath::V2f(primaries.g.x, primaries.g.y); - chromaticities.blue = Imath::V2f(primaries.b.x, primaries.b.y); + chromaticities.red = + Imath::V2f(c_enc.primaries_red_xy[0], c_enc.primaries_red_xy[1]); + chromaticities.green = + Imath::V2f(c_enc.primaries_green_xy[0], c_enc.primaries_green_xy[1]); + chromaticities.blue = + Imath::V2f(c_enc.primaries_blue_xy[0], c_enc.primaries_blue_xy[1]); chromaticities.white = - Imath::V2f(c_linear.GetWhitePoint().x, c_linear.GetWhitePoint().y); + Imath::V2f(c_enc.white_point_xy[0], c_enc.white_point_xy[1]); OpenEXR::addChromaticities(header, chromaticities); - OpenEXR::addWhiteLuminance(header, io->metadata.m.IntensityTarget()); + OpenEXR::addWhiteLuminance(header, 255.0f); + + auto loadFloat = + format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat : LoadLEFloat; + auto loadAlpha = + has_alpha ? loadFloat : [](const uint8_t* p) -> float { return 1.0f; }; // Ensure that the destructor of RgbaOutputFile has run before we look at the // size of `bytes`. @@ -112,50 +125,32 @@ Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, os, header, has_alpha ? OpenEXR::WRITE_RGBA : OpenEXR::WRITE_RGB); // How many rows to write at once. Again, the OpenEXR documentation // recommends writing the whole image in one call. - const int y_chunk_size = io->ysize(); - std::vector<OpenEXR::Rgba> output_rows(io->xsize() * y_chunk_size); + const int y_chunk_size = ysize; + std::vector<OpenEXR::Rgba> output_rows(xsize * y_chunk_size); - for (size_t start_y = 0; start_y < io->ysize(); start_y += y_chunk_size) { + for (size_t start_y = 0; start_y < ysize; start_y += y_chunk_size) { // Inclusive. - const size_t end_y = - std::min(start_y + y_chunk_size - 1, io->ysize() - 1); - output.setFrameBuffer(output_rows.data() - start_y * io->xsize(), - /*xStride=*/1, /*yStride=*/io->xsize()); - JXL_RETURN_IF_ERROR(RunOnPool( - pool, start_y, end_y + 1, ThreadPool::NoInit, - [&](const uint32_t y, size_t /* thread */) { - const float* const JXL_RESTRICT input_rows[] = { - linear->color().ConstPlaneRow(0, y), - linear->color().ConstPlaneRow(1, y), - linear->color().ConstPlaneRow(2, y), - }; - OpenEXR::Rgba* const JXL_RESTRICT row_data = - &output_rows[(y - start_y) * io->xsize()]; - if (has_alpha) { - const float* const JXL_RESTRICT alpha_row = - io->Main().alpha().ConstRow(y); - if (alpha_is_premultiplied) { - for (size_t x = 0; x < io->xsize(); ++x) { - row_data[x] = - OpenEXR::Rgba(input_rows[0][x], input_rows[1][x], - input_rows[2][x], alpha_row[x]); - } - } else { - for (size_t x = 0; x < io->xsize(); ++x) { - row_data[x] = OpenEXR::Rgba(alpha_row[x] * input_rows[0][x], - alpha_row[x] * input_rows[1][x], - alpha_row[x] * input_rows[2][x], - alpha_row[x]); - } - } - } else { - for (size_t x = 0; x < io->xsize(); ++x) { - row_data[x] = OpenEXR::Rgba(input_rows[0][x], input_rows[1][x], - input_rows[2][x], 1.f); - } - } - }, - "EncodeImageEXR")); + const size_t end_y = std::min(start_y + y_chunk_size - 1, ysize - 1); + output.setFrameBuffer(output_rows.data() - start_y * xsize, + /*xStride=*/1, /*yStride=*/xsize); + for (size_t y = start_y; y <= end_y; ++y) { + const uint8_t* in_row = &in[(y - start_y) * in_stride]; + OpenEXR::Rgba* const JXL_RESTRICT row_data = + &output_rows[(y - start_y) * xsize]; + for (size_t x = 0; x < xsize; ++x) { + const uint8_t* in_pixel = &in_row[4 * num_channels * x]; + float r = loadFloat(&in_pixel[0]); + float g = loadFloat(&in_pixel[4]); + float b = loadFloat(&in_pixel[8]); + const float alpha = loadAlpha(&in_pixel[12]); + if (!alpha_is_premultiplied) { + r *= alpha; + g *= alpha; + b *= alpha; + } + row_data[x] = OpenEXR::Rgba(r, g, b, alpha); + } + } output.writePixels(/*numScanLines=*/end_y - start_y + 1); } } @@ -163,5 +158,43 @@ Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, return true; } +class EXREncoder : public Encoder { + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 2, 3, 4}) { + for (const JxlDataType data_type : {JXL_TYPE_FLOAT, JXL_TYPE_FLOAT16}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/data_type, + /*endianness=*/endianness, + /*align=*/0}); + } + } + } + return formats; + } + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + encoded_image->icc.clear(); + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImageEXR(frame.color, ppf.info, + ppf.color_encoding, pool, + &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr<Encoder> GetEXREncoder() { + return jxl::make_unique<EXREncoder>(); +} + } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/exr.h b/media/libjxl/src/lib/extras/enc/exr.h index 0423bcbadb..1baaa0272f 100644 --- a/media/libjxl/src/lib/extras/enc/exr.h +++ b/media/libjxl/src/lib/extras/enc/exr.h @@ -8,21 +8,14 @@ // Encodes OpenEXR images in memory. -#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" -#include "lib/jxl/color_encoding_internal.h" +#include <memory> + +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -// Transforms from io->c_current to `c_desired` (with the transfer function set -// to linear as that is the OpenEXR convention) and encodes into `bytes`. -Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, - ThreadPool* pool, PaddedBytes* bytes); +std::unique_ptr<Encoder> GetEXREncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/jpg.cc b/media/libjxl/src/lib/extras/enc/jpg.cc index 83b2895756..93a39dd2e4 100644 --- a/media/libjxl/src/lib/extras/enc/jpg.cc +++ b/media/libjxl/src/lib/extras/enc/jpg.cc @@ -12,19 +12,12 @@ #include <algorithm> #include <iterator> #include <numeric> +#include <sstream> #include <utility> #include <vector> -#include "lib/jxl/base/compiler_specific.h" +#include "lib/extras/exif.h" #include "lib/jxl/base/status.h" -#include "lib/jxl/color_encoding_internal.h" -#include "lib/jxl/common.h" -#include "lib/jxl/dec_external_image.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_image_bundle.h" -#include "lib/jxl/exif.h" -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" #include "lib/jxl/sanitizers.h" #if JPEGXL_ENABLE_SJPEG #include "sjpeg.h" @@ -44,8 +37,21 @@ constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, 0x66, 0x00, 0x00}; constexpr int kExifMarker = JPEG_APP0 + 1; +enum class JpegEncoder { + kLibJpeg, + kSJpeg, +}; + +bool IsSRGBEncoding(const JxlColorEncoding& c) { + return ((c.color_space == JXL_COLOR_SPACE_RGB || + c.color_space == JXL_COLOR_SPACE_GRAY) && + c.primaries == JXL_PRIMARIES_SRGB && + c.white_point == JXL_WHITE_POINT_D65 && + c.transfer_function == JXL_TRANSFER_FUNCTION_SRGB); +} + void WriteICCProfile(jpeg_compress_struct* const cinfo, - const PaddedBytes& icc) { + const std::vector<uint8_t>& icc) { constexpr size_t kMaxIccBytesInMarker = kMaxBytesInMarker - sizeof kICCSignature - 2; const int num_markers = @@ -80,25 +86,34 @@ void WriteExif(jpeg_compress_struct* const cinfo, } } -Status SetChromaSubsampling(const YCbCrChromaSubsampling& chroma_subsampling, +Status SetChromaSubsampling(const std::string& subsampling, jpeg_compress_struct* const cinfo) { - for (size_t i = 0; i < 3; i++) { - cinfo->comp_info[i].h_samp_factor = - 1 << (chroma_subsampling.MaxHShift() - - chroma_subsampling.HShift(i < 2 ? i ^ 1 : i)); - cinfo->comp_info[i].v_samp_factor = - 1 << (chroma_subsampling.MaxVShift() - - chroma_subsampling.VShift(i < 2 ? i ^ 1 : i)); + const std::pair<const char*, + std::pair<std::array<uint8_t, 3>, std::array<uint8_t, 3>>> + options[] = {{"444", {{{1, 1, 1}}, {{1, 1, 1}}}}, + {"420", {{{2, 1, 1}}, {{2, 1, 1}}}}, + {"422", {{{2, 1, 1}}, {{1, 1, 1}}}}, + {"440", {{{1, 1, 1}}, {{2, 1, 1}}}}}; + for (const auto& option : options) { + if (subsampling == option.first) { + for (size_t i = 0; i < 3; i++) { + cinfo->comp_info[i].h_samp_factor = option.second.first[i]; + cinfo->comp_info[i].v_samp_factor = option.second.second[i]; + } + return true; + } } - return true; + return false; } -} // namespace - -Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, - size_t quality, - const YCbCrChromaSubsampling& chroma_subsampling, - PaddedBytes* bytes) { +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>* 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. @@ -109,15 +124,10 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, unsigned char* buffer = nullptr; unsigned long size = 0; jpeg_mem_dest(&cinfo, &buffer, &size); - cinfo.image_width = ib->oriented_xsize(); - cinfo.image_height = ib->oriented_ysize(); - if (ib->IsGray()) { - cinfo.input_components = 1; - cinfo.in_color_space = JCS_GRAYSCALE; - } else { - cinfo.input_components = 3; - cinfo.in_color_space = JCS_RGB; - } + cinfo.image_width = image.xsize; + cinfo.image_height = image.ysize; + cinfo.input_components = info.num_color_channels; + cinfo.in_color_space = info.num_color_channels == 1 ? JCS_GRAYSCALE : JCS_RGB; jpeg_set_defaults(&cinfo); cinfo.optimize_coding = TRUE; if (cinfo.input_components == 3) { @@ -125,27 +135,21 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, } jpeg_set_quality(&cinfo, quality, TRUE); jpeg_start_compress(&cinfo, TRUE); - if (!ib->IsSRGB()) { - WriteICCProfile(&cinfo, ib->c_current().ICC()); + if (!icc.empty()) { + WriteICCProfile(&cinfo, icc); } - if (!io->blobs.exif.empty()) { - std::vector<uint8_t> exif = io->blobs.exif; + if (!exif.empty()) { ResetExifOrientation(exif); WriteExif(&cinfo, exif); } if (cinfo.input_components > 3 || cinfo.input_components < 0) return JXL_FAILURE("invalid numbers of components"); - size_t stride = - ib->oriented_xsize() * cinfo.input_components * sizeof(JSAMPLE); - PaddedBytes raw_bytes(stride * ib->oriented_ysize()); - JXL_RETURN_IF_ERROR(ConvertToExternal( - *ib, BITS_IN_JSAMPLE, /*float_out=*/false, cinfo.input_components, - JXL_BIG_ENDIAN, stride, nullptr, raw_bytes.data(), raw_bytes.size(), - /*out_callback=*/{}, ib->metadata()->GetOrientation())); - - for (size_t y = 0; y < ib->oriented_ysize(); ++y) { - JSAMPROW row[] = {raw_bytes.data() + y * stride}; + 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); } @@ -160,41 +164,34 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, return true; } -Status EncodeWithSJpeg(const ImageBundle* ib, const CodecInOut* io, - size_t quality, - const YCbCrChromaSubsampling& chroma_subsampling, - PaddedBytes* bytes) { +Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info, + const std::vector<uint8_t>& icc, + std::vector<uint8_t> exif, size_t quality, + const std::string& chroma_subsampling, + std::vector<uint8_t>* bytes) { #if !JPEGXL_ENABLE_SJPEG return JXL_FAILURE("JPEG XL was built without sjpeg support"); #else sjpeg::EncoderParam param(quality); - if (!ib->IsSRGB()) { - param.iccp.assign(ib->metadata()->color_encoding.ICC().begin(), - ib->metadata()->color_encoding.ICC().end()); + if (!icc.empty()) { + param.iccp.assign(icc.begin(), icc.end()); } - std::vector<uint8_t> exif = io->blobs.exif; if (!exif.empty()) { ResetExifOrientation(exif); param.exif.assign(exif.begin(), exif.end()); } - if (chroma_subsampling.Is444()) { + if (chroma_subsampling == "444") { param.yuv_mode = SJPEG_YUV_444; - } else if (chroma_subsampling.Is420()) { + } else if (chroma_subsampling == "420") { param.yuv_mode = SJPEG_YUV_SHARP; } else { return JXL_FAILURE("sjpeg does not support this chroma subsampling mode"); } - size_t stride = ib->oriented_xsize() * 3; - PaddedBytes rgb(ib->xsize() * ib->ysize() * 3); - JXL_RETURN_IF_ERROR( - ConvertToExternal(*ib, 8, /*float_out=*/false, 3, JXL_BIG_ENDIAN, stride, - nullptr, rgb.data(), rgb.size(), - /*out_callback=*/{}, ib->metadata()->GetOrientation())); - + size_t stride = info.xsize * 3; + const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels()); std::string output; - JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib->oriented_xsize(), - ib->oriented_ysize(), stride, param, - &output)); + JXL_RETURN_IF_ERROR( + sjpeg::Encode(pixels, image.xsize, image.ysize, stride, param, &output)); bytes->assign( reinterpret_cast<const uint8_t*>(output.data()), reinterpret_cast<const uint8_t*>(output.data() + output.size())); @@ -202,31 +199,30 @@ Status EncodeWithSJpeg(const ImageBundle* ib, const CodecInOut* io, #endif } -Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, - YCbCrChromaSubsampling chroma_subsampling, - ThreadPool* pool, PaddedBytes* bytes) { - if (io->Main().HasAlpha()) { +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) { return JXL_FAILURE("please specify a 0-100 JPEG quality"); } - const ImageBundle* ib; - ImageMetadata metadata = io->metadata.m; - ImageBundle ib_store(&metadata); - JXL_RETURN_IF_ERROR(TransformIfNeeded(io->Main(), - io->metadata.m.color_encoding, - GetJxlCms(), pool, &ib_store, &ib)); - switch (encoder) { case JpegEncoder::kLibJpeg: - JXL_RETURN_IF_ERROR( - EncodeWithLibJpeg(ib, io, quality, chroma_subsampling, bytes)); + JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(image, info, icc, std::move(exif), + quality, chroma_subsampling, + bytes)); break; case JpegEncoder::kSJpeg: - JXL_RETURN_IF_ERROR( - EncodeWithSJpeg(ib, io, quality, chroma_subsampling, bytes)); + JXL_RETURN_IF_ERROR(EncodeWithSJpeg(image, info, icc, std::move(exif), + quality, chroma_subsampling, bytes)); break; default: return JXL_FAILURE("tried to use an unknown JPEG encoder"); @@ -235,5 +231,68 @@ Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, return true; } +class JPEGEncoder : public Encoder { + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 3}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/JXL_TYPE_UINT8, + /*endianness=*/endianness, + /*align=*/0}); + } + } + return formats; + } + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + 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()); + } + } + std::vector<uint8_t> icc; + if (!IsSRGBEncoding(ppf.color_encoding)) { + icc = ppf.icc; + } + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImageJPG( + frame.color, ppf.info, icc, ppf.metadata.exif, jpeg_encoder, quality, + chroma_subsampling, pool, &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr<Encoder> GetJPEGEncoder() { + return jxl::make_unique<JPEGEncoder>(); +} + } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/jpg.h b/media/libjxl/src/lib/extras/enc/jpg.h index ccea1415a8..20b37cd168 100644 --- a/media/libjxl/src/lib/extras/enc/jpg.h +++ b/media/libjxl/src/lib/extras/enc/jpg.h @@ -8,27 +8,14 @@ // Encodes JPG pixels and metadata in memory. -#include <stdint.h> +#include <memory> -#include "lib/extras/codec.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/span.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/codec_in_out.h" +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -enum class JpegEncoder { - kLibJpeg, - kSJpeg, -}; - -// Encodes into `bytes`. -Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, - YCbCrChromaSubsampling chroma_subsampling, - ThreadPool* pool, PaddedBytes* bytes); +std::unique_ptr<Encoder> GetJPEGEncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/npy.cc b/media/libjxl/src/lib/extras/enc/npy.cc new file mode 100644 index 0000000000..1428e6427d --- /dev/null +++ b/media/libjxl/src/lib/extras/enc/npy.cc @@ -0,0 +1,322 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/enc/npy.h" + +#include <stdio.h> + +#include <sstream> +#include <string> +#include <vector> + +#include "jxl/types.h" +#include "lib/extras/packed_image.h" + +namespace jxl { +namespace extras { +namespace { + +// JSON value writing + +class JSONField { + public: + virtual ~JSONField() = default; + virtual void Write(std::ostream& o, uint32_t indent) const = 0; + + protected: + JSONField() = default; +}; + +class JSONValue : public JSONField { + public: + template <typename T> + explicit JSONValue(const T& value) : value_(std::to_string(value)) {} + + explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {} + + explicit JSONValue(bool value) : value_(value ? "true" : "false") {} + + void Write(std::ostream& o, uint32_t indent) const override { o << value_; } + + private: + std::string value_; +}; + +class JSONDict : public JSONField { + public: + JSONDict() = default; + + template <typename T> + T* AddEmpty(const std::string& key) { + static_assert(std::is_convertible<T*, JSONField*>::value, + "T must be a JSONField"); + T* ret = new T(); + values_.emplace_back( + key, std::unique_ptr<JSONField>(static_cast<JSONField*>(ret))); + return ret; + } + + template <typename T> + void Add(const std::string& key, const T& value) { + values_.emplace_back(key, std::unique_ptr<JSONField>(new JSONValue(value))); + } + + void Write(std::ostream& o, uint32_t indent) const override { + std::string indent_str(indent, ' '); + o << "{"; + bool is_first = true; + for (const auto& key_value : values_) { + if (!is_first) { + o << ","; + } + is_first = false; + o << std::endl << indent_str << " \"" << key_value.first << "\": "; + key_value.second->Write(o, indent + 2); + } + if (!values_.empty()) { + o << std::endl << indent_str; + } + o << "}"; + } + + private: + // Dictionary with order. + std::vector<std::pair<std::string, std::unique_ptr<JSONField>>> values_; +}; + +class JSONArray : public JSONField { + public: + JSONArray() = default; + + template <typename T> + T* AddEmpty() { + static_assert(std::is_convertible<T*, JSONField*>::value, + "T must be a JSONField"); + T* ret = new T(); + values_.emplace_back(ret); + return ret; + } + + template <typename T> + void Add(const T& value) { + values_.emplace_back(new JSONValue(value)); + } + + void Write(std::ostream& o, uint32_t indent) const override { + std::string indent_str(indent, ' '); + o << "["; + bool is_first = true; + for (const auto& value : values_) { + if (!is_first) { + o << ","; + } + is_first = false; + o << std::endl << indent_str << " "; + value->Write(o, indent + 2); + } + if (!values_.empty()) { + o << std::endl << indent_str; + } + o << "]"; + } + + private: + std::vector<std::unique_ptr<JSONField>> values_; +}; + +void GenerateMetadata(const PackedPixelFile& ppf, std::vector<uint8_t>* out) { + JSONDict meta; + // Same order as in 18181-3 CD. + + // Frames. + auto* meta_frames = meta.AddEmpty<JSONArray>("frames"); + for (size_t i = 0; i < ppf.frames.size(); i++) { + auto* frame_i = meta_frames->AddEmpty<JSONDict>(); + if (ppf.info.have_animation) { + frame_i->Add("duration", + JSONValue(ppf.frames[i].frame_info.duration * 1.0f * + ppf.info.animation.tps_denominator / + ppf.info.animation.tps_numerator)); + } + + frame_i->Add("name", JSONValue(ppf.frames[i].name)); + + if (ppf.info.animation.have_timecodes) { + frame_i->Add("timecode", JSONValue(ppf.frames[i].frame_info.timecode)); + } + } + +#define METADATA(FIELD) meta.Add(#FIELD, ppf.info.FIELD) + + METADATA(intensity_target); + METADATA(min_nits); + METADATA(relative_to_max_display); + METADATA(linear_below); + + if (ppf.info.have_preview) { + meta.AddEmpty<JSONDict>("preview"); + // TODO(veluca): can we have duration/name/timecode here? + } + + { + auto ectype = meta.AddEmpty<JSONArray>("extra_channel_type"); + auto bps = meta.AddEmpty<JSONArray>("bits_per_sample"); + auto ebps = meta.AddEmpty<JSONArray>("exp_bits_per_sample"); + bps->Add(ppf.info.bits_per_sample); + ebps->Add(ppf.info.exponent_bits_per_sample); + for (size_t i = 0; i < ppf.extra_channels_info.size(); i++) { + switch (ppf.extra_channels_info[i].ec_info.type) { + case JXL_CHANNEL_ALPHA: { + ectype->Add(std::string("Alpha")); + break; + } + case JXL_CHANNEL_DEPTH: { + ectype->Add(std::string("Depth")); + break; + } + case JXL_CHANNEL_SPOT_COLOR: { + ectype->Add(std::string("SpotColor")); + break; + } + case JXL_CHANNEL_SELECTION_MASK: { + ectype->Add(std::string("SelectionMask")); + break; + } + case JXL_CHANNEL_BLACK: { + ectype->Add(std::string("Black")); + break; + } + case JXL_CHANNEL_CFA: { + ectype->Add(std::string("CFA")); + break; + } + case JXL_CHANNEL_THERMAL: { + ectype->Add(std::string("Thermal")); + break; + } + default: { + ectype->Add(std::string("UNKNOWN")); + break; + } + } + bps->Add(ppf.extra_channels_info[i].ec_info.bits_per_sample); + ebps->Add(ppf.extra_channels_info[i].ec_info.exponent_bits_per_sample); + } + } + + std::ostringstream os; + meta.Write(os, 0); + out->resize(os.str().size()); + memcpy(out->data(), os.str().data(), os.str().size()); +} + +void Append(std::vector<uint8_t>* out, const void* data, size_t size) { + size_t pos = out->size(); + out->resize(pos + size); + memcpy(out->data() + pos, data, size); +} + +void WriteNPYHeader(size_t xsize, size_t ysize, uint32_t num_channels, + size_t num_frames, std::vector<uint8_t>* out) { + const uint8_t header[] = "\x93NUMPY\x01\x00"; + Append(out, header, 8); + std::stringstream ss; + ss << "{'descr': '<f4', 'fortran_order': False, 'shape': (" << num_frames + << ", " << ysize << ", " << xsize << ", " << num_channels << "), }\n"; + // 16-bit little endian header length. + uint8_t header_len[2] = {static_cast<uint8_t>(ss.str().size() % 256), + static_cast<uint8_t>(ss.str().size() / 256)}; + Append(out, header_len, 2); + Append(out, ss.str().data(), ss.str().size()); +} + +bool WriteFrameToNPYArray(size_t xsize, size_t ysize, const PackedFrame& frame, + std::vector<uint8_t>* out) { + const auto& color = frame.color; + if (color.xsize != xsize || color.ysize != ysize) { + return false; + } + for (const auto& ec : frame.extra_channels) { + if (ec.xsize != xsize || ec.ysize != ysize) { + return false; + } + } + // interleave the samples from color and extra channels + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + { + size_t sample_size = color.pixel_stride(); + size_t offset = y * color.stride + x * sample_size; + uint8_t* pixels = reinterpret_cast<uint8_t*>(color.pixels()); + JXL_ASSERT(offset + sample_size <= color.pixels_size); + Append(out, pixels + offset, sample_size); + } + for (const auto& ec : frame.extra_channels) { + size_t sample_size = ec.pixel_stride(); + size_t offset = y * ec.stride + x * sample_size; + uint8_t* pixels = reinterpret_cast<uint8_t*>(ec.pixels()); + JXL_ASSERT(offset + sample_size <= ec.pixels_size); + Append(out, pixels + offset, sample_size); + } + } + } + return true; +} + +// Writes a PackedPixelFile as a numpy 4D ndarray in binary format. +bool WriteNPYArray(const PackedPixelFile& ppf, std::vector<uint8_t>* out) { + size_t xsize = ppf.info.xsize; + size_t ysize = ppf.info.ysize; + WriteNPYHeader(xsize, ysize, + ppf.info.num_color_channels + ppf.extra_channels_info.size(), + ppf.frames.size(), out); + for (const auto& frame : ppf.frames) { + if (!WriteFrameToNPYArray(xsize, ysize, frame, out)) { + return false; + } + } + return true; +} + +class NumPyEncoder : public Encoder { + public: + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + GenerateMetadata(ppf, &encoded_image->metadata); + encoded_image->bitstreams.emplace_back(); + if (!WriteNPYArray(ppf, &encoded_image->bitstreams.back())) { + return false; + } + if (ppf.preview_frame) { + size_t xsize = ppf.info.preview.xsize; + size_t ysize = ppf.info.preview.ysize; + WriteNPYHeader(xsize, ysize, ppf.info.num_color_channels, 1, + &encoded_image->preview_bitstream); + if (!WriteFrameToNPYArray(xsize, ysize, *ppf.preview_frame, + &encoded_image->preview_bitstream)) { + return false; + } + } + return true; + } + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 3}) { + formats.push_back(JxlPixelFormat{num_channels, JXL_TYPE_FLOAT, + JXL_LITTLE_ENDIAN, /*align=*/0}); + } + return formats; + } +}; + +} // namespace + +std::unique_ptr<Encoder> GetNumPyEncoder() { + return jxl::make_unique<NumPyEncoder>(); +} + +} // namespace extras +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/npy.h b/media/libjxl/src/lib/extras/enc/npy.h new file mode 100644 index 0000000000..3ee6208ec2 --- /dev/null +++ b/media/libjxl/src/lib/extras/enc/npy.h @@ -0,0 +1,23 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_ENC_NPY_H_ +#define LIB_EXTRAS_ENC_NPY_H_ + +// Encodes pixels to numpy array, used for conformance testing. + +#include <memory> + +#include "lib/extras/enc/encode.h" + +namespace jxl { +namespace extras { + +std::unique_ptr<Encoder> GetNumPyEncoder(); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ENC_NPY_H_ diff --git a/media/libjxl/src/lib/extras/enc/pgx.cc b/media/libjxl/src/lib/extras/enc/pgx.cc index cc333a62ac..ef204ad1c6 100644 --- a/media/libjxl/src/lib/extras/enc/pgx.cc +++ b/media/libjxl/src/lib/extras/enc/pgx.cc @@ -8,18 +8,10 @@ #include <stdio.h> #include <string.h> -#include "lib/jxl/base/bits.h" -#include "lib/jxl/base/compiler_specific.h" -#include "lib/jxl/base/file_io.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" -#include "lib/jxl/color_management.h" -#include "lib/jxl/dec_external_image.h" -#include "lib/jxl/enc_color_management.h" -#include "lib/jxl/enc_external_image.h" -#include "lib/jxl/enc_image_bundle.h" -#include "lib/jxl/fields.h" // AllDefault -#include "lib/jxl/image.h" -#include "lib/jxl/image_bundle.h" namespace jxl { namespace extras { @@ -27,60 +19,63 @@ namespace { constexpr size_t kMaxHeaderSize = 200; -Status EncodeHeader(const ImageBundle& ib, const size_t bits_per_sample, - char* header, int* JXL_RESTRICT chars_written) { - if (ib.HasAlpha()) return JXL_FAILURE("PGX: can't store alpha"); - if (!ib.IsGray()) return JXL_FAILURE("PGX: must be grayscale"); +Status EncodeHeader(const JxlBasicInfo& info, char* header, + int* chars_written) { + if (info.alpha_bits > 0) { + return JXL_FAILURE("PGX: can't store alpha"); + } + if (info.num_color_channels != 1) { + return JXL_FAILURE("PGX: must be grayscale"); + } // TODO(lode): verify other bit depths: for other bit depths such as 1 or 4 // bits, have a test case to verify it works correctly. For bits > 16, we may // need to change the way external_image works. - if (bits_per_sample != 8 && bits_per_sample != 16) { + if (info.bits_per_sample != 8 && info.bits_per_sample != 16) { return JXL_FAILURE("PGX: bits other than 8 or 16 not yet supported"); } // Use ML (Big Endian), LM may not be well supported by all decoders. - *chars_written = snprintf(header, kMaxHeaderSize, - "PG ML + %" PRIuS " %" PRIuS " %" PRIuS "\n", - bits_per_sample, ib.xsize(), ib.ysize()); + *chars_written = snprintf(header, kMaxHeaderSize, "PG ML + %u %u %u\n", + info.bits_per_sample, info.xsize, info.ysize); JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < kMaxHeaderSize); return true; } -} // namespace +Status EncodeImagePGX(const PackedFrame& frame, const JxlBasicInfo& info, + std::vector<uint8_t>* bytes) { + char header[kMaxHeaderSize]; + int header_size = 0; + JXL_RETURN_IF_ERROR(EncodeHeader(info, header, &header_size)); -Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes) { - if (!Bundle::AllDefault(io->metadata.m)) { - JXL_WARNING("PGX encoder ignoring metadata - use a different codec"); - } - if (!c_desired.IsSRGB()) { - JXL_WARNING( - "PGX encoder cannot store custom ICC profile; decoder\n" - "will need hint key=color_space to get the same values"); + const PackedImage& color = frame.color; + const JxlPixelFormat format = color.format; + const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels()); + size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type); + size_t bytes_per_sample = data_bits_per_sample / kBitsPerByte; + size_t num_samples = info.xsize * info.ysize; + + if (info.bits_per_sample != data_bits_per_sample) { + return JXL_FAILURE("Bit depth does not match pixel data type"); } - ImageBundle ib = io->Main().Copy(); - - ImageMetadata metadata = io->metadata.m; - ImageBundle store(&metadata); - const ImageBundle* transformed; - JXL_RETURN_IF_ERROR(TransformIfNeeded(ib, c_desired, GetJxlCms(), pool, - &store, &transformed)); - PaddedBytes pixels(ib.xsize() * ib.ysize() * - (bits_per_sample / kBitsPerByte)); - size_t stride = ib.xsize() * (bits_per_sample / kBitsPerByte); - JXL_RETURN_IF_ERROR( - ConvertToExternal(*transformed, bits_per_sample, - /*float_out=*/false, - /*num_channels=*/1, JXL_BIG_ENDIAN, stride, pool, - pixels.data(), pixels.size(), - /*out_callback=*/{}, metadata.GetOrientation())); + std::vector<uint8_t> pixels(num_samples * bytes_per_sample); - char header[kMaxHeaderSize]; - int header_size = 0; - JXL_RETURN_IF_ERROR(EncodeHeader(ib, bits_per_sample, header, &header_size)); + if (format.data_type == JXL_TYPE_UINT8) { + memcpy(&pixels[0], in, num_samples * bytes_per_sample); + } else if (format.data_type == JXL_TYPE_UINT16) { + if (format.endianness != JXL_BIG_ENDIAN) { + const uint8_t* p_in = in; + uint8_t* p_out = pixels.data(); + for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) { + StoreBE16(LoadLE16(p_in), p_out); + } + } else { + memcpy(&pixels[0], in, num_samples * bytes_per_sample); + } + } else { + return JXL_FAILURE("Unsupported pixel data type"); + } bytes->resize(static_cast<size_t>(header_size) + pixels.size()); memcpy(bytes->data(), header, static_cast<size_t>(header_size)); @@ -89,5 +84,41 @@ Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, return true; } +class PGXEncoder : public Encoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/1, + /*data_type=*/data_type, + /*endianness=*/endianness, + /*align=*/0}); + } + } + return formats; + } + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + encoded_image->icc.assign(ppf.icc.begin(), ppf.icc.end()); + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR( + EncodeImagePGX(frame, ppf.info, &encoded_image->bitstreams.back())); + } + return true; + } +}; + +} // namespace + +std::unique_ptr<Encoder> GetPGXEncoder() { + return jxl::make_unique<PGXEncoder>(); +} + } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/pgx.h b/media/libjxl/src/lib/extras/enc/pgx.h index 6f14e20668..f24e391b09 100644 --- a/media/libjxl/src/lib/extras/enc/pgx.h +++ b/media/libjxl/src/lib/extras/enc/pgx.h @@ -11,21 +11,12 @@ #include <stddef.h> #include <stdint.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" -#include "lib/jxl/color_encoding_internal.h" +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -// Transforms from io->c_current to `c_desired` and encodes into `bytes`. -Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, - size_t bits_per_sample, ThreadPool* pool, - PaddedBytes* bytes); +std::unique_ptr<Encoder> GetPGXEncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/enc/pnm.cc b/media/libjxl/src/lib/extras/enc/pnm.cc index 9cedda09d5..9b5f6cbc95 100644 --- a/media/libjxl/src/lib/extras/enc/pnm.cc +++ b/media/libjxl/src/lib/extras/enc/pnm.cc @@ -32,22 +32,19 @@ namespace { constexpr size_t kMaxHeaderSize = 200; -Status EncodeHeader(const PackedPixelFile& ppf, const size_t bits_per_sample, - const bool little_endian, char* header, - int* JXL_RESTRICT chars_written) { - bool is_gray = ppf.info.num_color_channels <= 2; - size_t oriented_xsize = - ppf.info.orientation <= 4 ? ppf.info.xsize : ppf.info.ysize; - size_t oriented_ysize = - ppf.info.orientation <= 4 ? ppf.info.ysize : ppf.info.xsize; - if (ppf.info.alpha_bits > 0) { // PAM +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", - oriented_xsize, oriented_ysize, is_gray ? 2 : 4, max_val, + 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); @@ -56,77 +53,164 @@ Status EncodeHeader(const PackedPixelFile& ppf, const size_t bits_per_sample, const double scale = little_endian ? -1.0 : 1.0; *chars_written = snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n", - type, oriented_xsize, oriented_ysize, scale); + 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; - if (max_val >= 65536) return JXL_FAILURE("PNM cannot have > 16 bits"); const char type = is_gray ? '5' : '6'; *chars_written = snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n", - type, oriented_xsize, oriented_ysize, max_val); + type, image.xsize, image.ysize, max_val); JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < kMaxHeaderSize); } return true; } -Span<const uint8_t> MakeSpan(const char* str) { - return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), - strlen(str)); +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; } -// Flip the image vertically for loading/saving PFM files which have the -// scanlines inverted. -void VerticallyFlipImage(float* const float_image, const size_t xsize, - const size_t ysize, const size_t num_channels) { - for (size_t y = 0; y < ysize / 2; y++) { - float* first_row = &float_image[y * num_channels * xsize]; - float* other_row = &float_image[(ysize - y - 1) * num_channels * xsize]; - for (size_t c = 0; c < num_channels; c++) { - for (size_t x = 0; x < xsize; ++x) { - float tmp = first_row[x * num_channels + c]; - first_row[x * num_channels + c] = other_row[x * num_channels + c]; - other_row[x * num_channels + c] = tmp; +class PNMEncoder : public Encoder { + public: + Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, + ThreadPool* pool = nullptr) const override { + JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); + if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() || + !ppf.metadata.jumbf.empty() || !ppf.metadata.xmp.empty()) { + JXL_WARNING("PNM encoder ignoring metadata - use a different codec"); + } + encoded_image->icc = ppf.icc; + encoded_image->bitstreams.clear(); + encoded_image->bitstreams.reserve(ppf.frames.size()); + for (const auto& frame : ppf.frames) { + JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); + encoded_image->bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.color, ppf.info.bits_per_sample, + &encoded_image->bitstreams.back())); + } + for (size_t i = 0; i < ppf.extra_channels_info.size(); ++i) { + const auto& ec_info = ppf.extra_channels_info[i].ec_info; + encoded_image->extra_channel_bitstreams.emplace_back(); + auto& ec_bitstreams = encoded_image->extra_channel_bitstreams.back(); + for (const auto& frame : ppf.frames) { + ec_bitstreams.emplace_back(); + JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.extra_channels[i], + ec_info.bits_per_sample, + &ec_bitstreams.back())); } } + return true; } -} +}; -} // namespace +class PPMEncoder : public PNMEncoder { + public: + std::vector<JxlPixelFormat> AcceptedFormats() const override { + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 2, 3, 4}) { + for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/data_type, + /*endianness=*/endianness, + /*align=*/0}); + } + } + } + return formats; + } +}; -Status EncodeImagePNM(const PackedPixelFile& ppf, size_t bits_per_sample, - ThreadPool* pool, size_t frame_index, - std::vector<uint8_t>* bytes) { - const bool floating_point = bits_per_sample > 16; - // Choose native for PFM; PGM/PPM require big-endian - const JxlEndianness endianness = - floating_point ? JXL_NATIVE_ENDIAN : JXL_BIG_ENDIAN; - if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() || - !ppf.metadata.jumbf.empty() || !ppf.metadata.xmp.empty()) { - JXL_WARNING("PNM encoder ignoring metadata - use a different codec"); +class PFMEncoder : public PNMEncoder { + 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}); + } + } + } + return formats; } +}; - char header[kMaxHeaderSize]; - int header_size = 0; - bool is_little_endian = endianness == JXL_LITTLE_ENDIAN || - (endianness == JXL_NATIVE_ENDIAN && IsLittleEndian()); - JXL_RETURN_IF_ERROR(EncodeHeader(ppf, bits_per_sample, is_little_endian, - header, &header_size)); - bytes->resize(static_cast<size_t>(header_size) + - ppf.frames[frame_index].color.pixels_size); - memcpy(bytes->data(), header, static_cast<size_t>(header_size)); - memcpy(bytes->data() + header_size, ppf.frames[frame_index].color.pixels(), - ppf.frames[frame_index].color.pixels_size); - if (floating_point) { - VerticallyFlipImage(reinterpret_cast<float*>(bytes->data() + header_size), - ppf.frames[frame_index].color.xsize, - ppf.frames[frame_index].color.ysize, - ppf.info.num_color_channels); +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; + } + } + return formats; } +}; - return true; +class PAMEncoder : 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->num_channels != 4) { + it = formats.erase(it); + } else { + ++it; + } + } + return formats; + } +}; + +Span<const uint8_t> MakeSpan(const char* str) { + return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), + strlen(str)); +} + +} // namespace + +std::unique_ptr<Encoder> GetPPMEncoder() { + return jxl::make_unique<PPMEncoder>(); +} + +std::unique_ptr<Encoder> GetPFMEncoder() { + return jxl::make_unique<PFMEncoder>(); +} + +std::unique_ptr<Encoder> GetPGMEncoder() { + return jxl::make_unique<PGMEncoder>(); +} + +std::unique_ptr<Encoder> GetPAMEncoder() { + return jxl::make_unique<PAMEncoder>(); } } // namespace extras diff --git a/media/libjxl/src/lib/extras/enc/pnm.h b/media/libjxl/src/lib/extras/enc/pnm.h index ecf0526a1a..403208cecd 100644 --- a/media/libjxl/src/lib/extras/enc/pnm.h +++ b/media/libjxl/src/lib/extras/enc/pnm.h @@ -10,21 +10,17 @@ // TODO(janwas): workaround for incorrect Win64 codegen (cause unknown) #include <hwy/highway.h> +#include <memory> -#include "lib/extras/packed_image.h" -#include "lib/jxl/base/data_parallel.h" -#include "lib/jxl/base/padded_bytes.h" -#include "lib/jxl/base/status.h" -#include "lib/jxl/codec_in_out.h" -#include "lib/jxl/color_encoding_internal.h" +#include "lib/extras/enc/encode.h" namespace jxl { namespace extras { -// Transforms from io->c_current to `c_desired` and encodes into `bytes`. -Status EncodeImagePNM(const PackedPixelFile& ppf, size_t bits_per_sample, - ThreadPool* pool, size_t frame_index, - std::vector<uint8_t>* bytes); +std::unique_ptr<Encoder> GetPAMEncoder(); +std::unique_ptr<Encoder> GetPGMEncoder(); +std::unique_ptr<Encoder> GetPPMEncoder(); +std::unique_ptr<Encoder> GetPFMEncoder(); } // namespace extras } // namespace jxl diff --git a/media/libjxl/src/lib/extras/exif.cc b/media/libjxl/src/lib/extras/exif.cc new file mode 100644 index 0000000000..7d926558c3 --- /dev/null +++ b/media/libjxl/src/lib/extras/exif.cc @@ -0,0 +1,55 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/exif.h" + +#include "lib/jxl/base/byte_order.h" + +namespace jxl { + +constexpr uint16_t kExifOrientationTag = 274; + +void ResetExifOrientation(std::vector<uint8_t>& exif) { + if (exif.size() < 12) return; // not enough bytes for a valid exif blob + bool bigendian; + uint8_t* t = exif.data(); + if (LoadLE32(t) == 0x2A004D4D) { + bigendian = true; + } else if (LoadLE32(t) == 0x002A4949) { + bigendian = false; + } else { + return; // not a valid tiff header + } + t += 4; + uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t)); + if (exif.size() < 12 + offset + 2 || offset < 8) return; + t += offset - 4; + uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + while (nb_tags > 0) { + if (t + 12 >= exif.data() + exif.size()) return; + uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + if (tag == kExifOrientationTag) { + uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t)); + t += 2; + uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t)); + t += 4; + if (type == 3 && count == 1) { + if (bigendian) { + StoreBE16(1, t); + } else { + StoreLE16(1, t); + } + } + return; + } else { + t += 10; + nb_tags--; + } + } +} + +} // namespace jxl diff --git a/media/libjxl/src/lib/extras/exif.h b/media/libjxl/src/lib/extras/exif.h new file mode 100644 index 0000000000..f22b2ccef5 --- /dev/null +++ b/media/libjxl/src/lib/extras/exif.h @@ -0,0 +1,20 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_EXIF_H_ +#define LIB_EXTRAS_EXIF_H_ + +#include <stdint.h> + +#include <vector> + +namespace jxl { + +// Sets the Exif orientation to the identity, to avoid repeated orientation +void ResetExifOrientation(std::vector<uint8_t>& exif); + +} // namespace jxl + +#endif // LIB_EXTRAS_EXIF_H_ diff --git a/media/libjxl/src/lib/extras/packed_image.h b/media/libjxl/src/lib/extras/packed_image.h index 59dbfe57c4..1296472101 100644 --- a/media/libjxl/src/lib/extras/packed_image.h +++ b/media/libjxl/src/lib/extras/packed_image.h @@ -22,7 +22,6 @@ #include "jxl/codestream_header.h" #include "jxl/encode.h" #include "jxl/types.h" -#include "lib/jxl/base/status.h" #include "lib/jxl/common.h" namespace jxl { @@ -33,26 +32,6 @@ class PackedImage { public: PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format) : PackedImage(xsize, ysize, format, CalcStride(format, xsize)) {} - PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format, - size_t stride) - : xsize(xsize), - ysize(ysize), - stride(stride), - format(format), - pixels_size(ysize * stride), - pixels_(malloc(std::max<size_t>(1, pixels_size)), free) {} - // Construct the image using the passed pixel buffer. The buffer is owned by - // this object and released with free(). - PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format, - void* pixels, size_t pixels_size) - : xsize(xsize), - ysize(ysize), - stride(CalcStride(format, xsize)), - format(format), - pixels_size(pixels_size), - pixels_(pixels, free) { - JXL_ASSERT(pixels_size >= stride * ysize); - } // The interleaved pixels as defined in the storage format. void* pixels() const { return pixels_.get(); } @@ -61,15 +40,6 @@ class PackedImage { size_t xsize; size_t ysize; - // Whether the y coordinate is flipped (y=0 is the last row). - bool flipped_y = false; - - // Whether the range is determined by format or by JxlBasicInfo - // e.g. if format is UINT16 and JxlBasicInfo bits_per_sample is 10, - // then if bitdepth_from_format == true, the range is 0..65535 - // while if bitdepth_from_format == false, the range is 0..1023. - bool bitdepth_from_format = true; - // The number of bytes per row. size_t stride; @@ -77,6 +47,11 @@ class PackedImage { JxlPixelFormat format; size_t pixels_size; + size_t pixel_stride() const { + return (BitsPerChannel(format.data_type) * format.num_channels / + jxl::kBitsPerByte); + } + static size_t BitsPerChannel(JxlDataType data_type) { switch (data_type) { case JXL_TYPE_UINT8: @@ -93,6 +68,15 @@ class PackedImage { } private: + PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format, + size_t stride) + : xsize(xsize), + ysize(ysize), + stride(stride), + format(format), + pixels_size(ysize * stride), + pixels_(malloc(std::max<size_t>(1, pixels_size)), free) {} + static size_t CalcStride(const JxlPixelFormat& format, size_t xsize) { size_t stride = xsize * (BitsPerChannel(format.data_type) * format.num_channels / jxl::kBitsPerByte); @@ -140,20 +124,20 @@ class PackedPixelFile { // The extra channel metadata information. struct PackedExtraChannel { - PackedExtraChannel(const JxlExtraChannelInfo& ec_info, - const std::string& name) - : ec_info(ec_info), name(name) {} - JxlExtraChannelInfo ec_info; + size_t index; std::string name; }; std::vector<PackedExtraChannel> extra_channels_info; - // Color information. If the icc is empty, the JxlColorEncoding should be used - // instead. + // Color information of the decoded pixels. + // If the icc is empty, the JxlColorEncoding should be used instead. std::vector<uint8_t> icc; JxlColorEncoding color_encoding = {}; + // The icc profile of the original image. + std::vector<uint8_t> orig_icc; + std::unique_ptr<PackedFrame> preview_frame; std::vector<PackedFrame> frames; PackedMetadata metadata; diff --git a/media/libjxl/src/lib/extras/packed_image_convert.cc b/media/libjxl/src/lib/extras/packed_image_convert.cc index 65336a70a1..dcdd12a673 100644 --- a/media/libjxl/src/lib/extras/packed_image_convert.cc +++ b/media/libjxl/src/lib/extras/packed_image_convert.cc @@ -20,6 +20,60 @@ namespace jxl { namespace extras { +Status ConvertPackedFrameToImageBundle(const JxlBasicInfo& info, + const PackedFrame& frame, + const CodecInOut& io, ThreadPool* pool, + ImageBundle* bundle) { + JXL_ASSERT(frame.color.pixels() != nullptr); + const bool float_in = frame.color.format.data_type == JXL_TYPE_FLOAT16 || + frame.color.format.data_type == JXL_TYPE_FLOAT; + size_t frame_bits_per_sample = + float_in ? PackedImage::BitsPerChannel(frame.color.format.data_type) + : info.bits_per_sample; + JXL_ASSERT(frame_bits_per_sample != 0); + // It is ok for the frame.color.format.num_channels to not match the + // number of channels on the image. + JXL_ASSERT(1 <= frame.color.format.num_channels && + frame.color.format.num_channels <= 4); + + const Span<const uint8_t> span( + static_cast<const uint8_t*>(frame.color.pixels()), + frame.color.pixels_size); + JXL_ASSERT(Rect(frame.frame_info.layer_info.crop_x0, + frame.frame_info.layer_info.crop_y0, + frame.frame_info.layer_info.xsize, + frame.frame_info.layer_info.ysize) + .IsInside(Rect(0, 0, info.xsize, info.ysize))); + if (info.have_animation) { + bundle->duration = frame.frame_info.duration; + bundle->blend = frame.frame_info.layer_info.blend_info.blendmode > 0; + bundle->use_for_next_frame = + frame.frame_info.layer_info.save_as_reference > 0; + bundle->origin.x0 = frame.frame_info.layer_info.crop_x0; + bundle->origin.y0 = frame.frame_info.layer_info.crop_y0; + } + bundle->name = frame.name; // frame.frame_info.name_length is ignored here. + JXL_ASSERT(io.metadata.m.color_encoding.IsGray() == + (frame.color.format.num_channels <= 2)); + + JXL_RETURN_IF_ERROR(ConvertFromExternal( + span, frame.color.xsize, frame.color.ysize, io.metadata.m.color_encoding, + frame.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)); + + bundle->extra_channels().resize(io.metadata.m.extra_channel_info.size()); + for (size_t i = 0; i < frame.extra_channels.size(); i++) { + const auto& ppf_ec = frame.extra_channels[i]; + bundle->extra_channels()[i] = ImageF(ppf_ec.xsize, ppf_ec.ysize); + JXL_CHECK(BufferToImageF(ppf_ec.format, ppf_ec.xsize, ppf_ec.ysize, + ppf_ec.pixels(), ppf_ec.pixels_size, pool, + &bundle->extra_channels()[i])); + } + return true; +} + Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, ThreadPool* pool, CodecInOut* io) { const bool has_alpha = ppf.info.alpha_bits != 0; @@ -63,7 +117,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, PaddedBytes icc; icc.append(ppf.icc); if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) { - fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB"); + fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB\n"); io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray); } else { if (io->metadata.m.color_encoding.IsGray() != is_gray) { @@ -105,61 +159,24 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf, io->metadata.m.extra_channel_info.push_back(std::move(out)); } + // Convert the preview + if (ppf.preview_frame) { + size_t preview_xsize = ppf.preview_frame->color.xsize; + size_t preview_ysize = ppf.preview_frame->color.ysize; + io->metadata.m.have_preview = true; + JXL_RETURN_IF_ERROR( + io->metadata.m.preview_size.Set(preview_xsize, preview_ysize)); + JXL_RETURN_IF_ERROR(ConvertPackedFrameToImageBundle( + ppf.info, *ppf.preview_frame, *io, pool, &io->preview_frame)); + } + // Convert the pixels io->dec_pixels = 0; io->frames.clear(); for (const auto& frame : ppf.frames) { - JXL_ASSERT(frame.color.pixels() != nullptr); - size_t frame_bits_per_sample = - (frame.color.bitdepth_from_format - ? frame.color.BitsPerChannel(frame.color.format.data_type) - : ppf.info.bits_per_sample); - JXL_ASSERT(frame_bits_per_sample != 0); - // It is ok for the frame.color.format.num_channels to not match the - // number of channels on the image. - JXL_ASSERT(1 <= frame.color.format.num_channels && - frame.color.format.num_channels <= 4); - - const Span<const uint8_t> span( - static_cast<const uint8_t*>(frame.color.pixels()), - frame.color.pixels_size); - Rect frame_rect = Rect(frame.frame_info.layer_info.crop_x0, - frame.frame_info.layer_info.crop_y0, - frame.frame_info.layer_info.xsize, - frame.frame_info.layer_info.ysize); - JXL_ASSERT(frame_rect.IsInside(Rect(0, 0, ppf.info.xsize, ppf.info.ysize))); ImageBundle bundle(&io->metadata.m); - if (ppf.info.have_animation) { - bundle.duration = frame.frame_info.duration; - bundle.blend = frame.frame_info.layer_info.blend_info.blendmode > 0; - bundle.use_for_next_frame = - frame.frame_info.layer_info.save_as_reference > 0; - bundle.origin.x0 = frame.frame_info.layer_info.crop_x0; - bundle.origin.y0 = frame.frame_info.layer_info.crop_y0; - } - bundle.name = frame.name; // frame.frame_info.name_length is ignored here. - JXL_ASSERT(io->metadata.m.color_encoding.IsGray() == - (frame.color.format.num_channels <= 2)); - - const bool float_in = frame.color.format.data_type == JXL_TYPE_FLOAT16 || - frame.color.format.data_type == JXL_TYPE_FLOAT; - 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=*/ppf.info.alpha_premultiplied, - frame_bits_per_sample, frame.color.format.endianness, - /*flipped_y=*/frame.color.flipped_y, pool, &bundle, - /*float_in=*/float_in, /*align=*/0)); - - bundle.extra_channels().resize(io->metadata.m.extra_channel_info.size()); - for (size_t i = 0; i < frame.extra_channels.size(); i++) { - const auto& ppf_ec = frame.extra_channels[i]; - bundle.extra_channels()[i] = ImageF(ppf_ec.xsize, ppf_ec.ysize); - JXL_CHECK(BufferToImageF(ppf_ec.format, ppf_ec.xsize, ppf_ec.ysize, - ppf_ec.pixels(), ppf_ec.pixels_size, pool, - &bundle.extra_channels()[i])); - } - + 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; } @@ -222,9 +239,7 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, // Convert the color encoding ppf->icc.assign(c_desired.ICC().begin(), c_desired.ICC().end()); - if (ppf->icc.empty()) { - ConvertInternalToExternalColorEncoding(c_desired, &ppf->color_encoding); - } + ConvertInternalToExternalColorEncoding(c_desired, &ppf->color_encoding); // Convert the extra blobs ppf->metadata.exif = io.blobs.exif; @@ -236,8 +251,7 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, // Convert the pixels ppf->frames.clear(); for (const auto& frame : io.frames) { - size_t frame_bits_per_sample = frame.metadata()->bit_depth.bits_per_sample; - JXL_ASSERT(frame_bits_per_sample != 0); + JXL_ASSERT(frame.metadata()->bit_depth.bits_per_sample != 0); // It is ok for the frame.color().kNumPlanes to not match the // number of channels on the image. const uint32_t num_channels = @@ -249,11 +263,9 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io, PackedFrame packed_frame(frame.oriented_xsize(), frame.oriented_ysize(), format); - packed_frame.color.bitdepth_from_format = float_out; const size_t bits_per_sample = - packed_frame.color.bitdepth_from_format - ? packed_frame.color.BitsPerChannel(pixel_format.data_type) - : ppf->info.bits_per_sample; + float_out ? packed_frame.color.BitsPerChannel(pixel_format.data_type) + : ppf->info.bits_per_sample; packed_frame.name = frame.name; packed_frame.frame_info.name_length = frame.name.size(); // Color transform diff --git a/media/libjxl/src/lib/extras/render_hdr.cc b/media/libjxl/src/lib/extras/render_hdr.cc new file mode 100644 index 0000000000..b247699cd3 --- /dev/null +++ b/media/libjxl/src/lib/extras/render_hdr.cc @@ -0,0 +1,60 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "lib/extras/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/media/libjxl/src/lib/extras/render_hdr.h b/media/libjxl/src/lib/extras/render_hdr.h new file mode 100644 index 0000000000..95127e074b --- /dev/null +++ b/media/libjxl/src/lib/extras/render_hdr.h @@ -0,0 +1,27 @@ +// 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/media/libjxl/src/lib/extras/tone_mapping.cc b/media/libjxl/src/lib/extras/tone_mapping.cc index ac4306f8ed..1ed1b29119 100644 --- a/media/libjxl/src/lib/extras/tone_mapping.cc +++ b/media/libjxl/src/lib/extras/tone_mapping.cc @@ -10,13 +10,15 @@ #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/transfer_functions-inl.h" HWY_BEFORE_NAMESPACE(); namespace jxl { namespace HWY_NAMESPACE { +static constexpr float rec2020_luminances[3] = {0.2627f, 0.6780f, 0.0593f}; + Status ToneMapFrame(const std::pair<float, float> display_nits, ImageBundle* const ib, ThreadPool* const pool) { // Perform tone mapping as described in Report ITU-R BT.2390-8, section 5.4 @@ -34,42 +36,12 @@ Status ToneMapFrame(const std::pair<float, float> display_nits, JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC()); JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool)); - const auto eotf_inv = [&df](const V luminance) -> V { - return TF_PQ().EncodedFromDisplay(df, luminance * Set(df, 1. / 10000)); - }; - - const V pq_mastering_min = - eotf_inv(Set(df, ib->metadata()->tone_mapping.min_nits)); - const V pq_mastering_max = - eotf_inv(Set(df, ib->metadata()->tone_mapping.intensity_target)); - const V pq_mastering_range = pq_mastering_max - pq_mastering_min; - const V inv_pq_mastering_range = - Set(df, 1) / (pq_mastering_max - pq_mastering_min); - const V min_lum = (eotf_inv(Set(df, display_nits.first)) - pq_mastering_min) * - inv_pq_mastering_range; - const V max_lum = - (eotf_inv(Set(df, display_nits.second)) - pq_mastering_min) * - inv_pq_mastering_range; - const V ks = MulAdd(Set(df, 1.5f), max_lum, Set(df, -0.5f)); - const V b = min_lum; - - const V inv_one_minus_ks = Set(df, 1) / Max(Set(df, 1e-6f), Set(df, 1) - ks); - const auto T = [ks, inv_one_minus_ks](const V a) { - return (a - ks) * inv_one_minus_ks; - }; - const auto P = [&T, &df, ks, max_lum](const V b) { - const V t_b = T(b); - const V t_b_2 = t_b * t_b; - const V t_b_3 = t_b_2 * t_b; - return MulAdd( - MulAdd(Set(df, 2), t_b_3, MulAdd(Set(df, -3), t_b_2, Set(df, 1))), ks, - MulAdd(t_b_3 + MulAdd(Set(df, -2), t_b_2, t_b), Set(df, 1) - ks, - MulAdd(Set(df, -2), t_b_3, Set(df, 3) * t_b_2) * max_lum)); - }; - - const V inv_max_display_nits = Set(df, 1 / display_nits.second); + Rec2408ToneMapper<decltype(df)> tone_mapper( + {ib->metadata()->tone_mapping.min_nits, + ib->metadata()->IntensityTarget()}, + display_nits, rec2020_luminances); - JXL_RETURN_IF_ERROR(RunOnPool( + return RunOnPool( pool, 0, ib->ysize(), ThreadPool::NoInit, [&](const uint32_t y, size_t /* thread */) { float* const JXL_RESTRICT row_r = ib->color()->PlaneRow(0, y); @@ -79,43 +51,13 @@ Status ToneMapFrame(const std::pair<float, float> display_nits, V red = Load(df, row_r + x); V green = Load(df, row_g + x); V blue = Load(df, row_b + x); - const V luminance = Set(df, ib->metadata()->IntensityTarget()) * - (MulAdd(Set(df, 0.2627f), red, - MulAdd(Set(df, 0.6780f), green, - Set(df, 0.0593f) * blue))); - const V normalized_pq = - Min(Set(df, 1.f), (eotf_inv(luminance) - pq_mastering_min) * - inv_pq_mastering_range); - const V e2 = - IfThenElse(normalized_pq < ks, normalized_pq, P(normalized_pq)); - const V one_minus_e2 = Set(df, 1) - e2; - const V one_minus_e2_2 = one_minus_e2 * one_minus_e2; - const V one_minus_e2_4 = one_minus_e2_2 * one_minus_e2_2; - const V e3 = MulAdd(b, one_minus_e2_4, e2); - const V e4 = MulAdd(e3, pq_mastering_range, pq_mastering_min); - const V new_luminance = - Min(Set(df, display_nits.second), - ZeroIfNegative(Set(df, 10000) * - TF_PQ().DisplayFromEncoded(df, e4))); - - const V ratio = new_luminance / luminance; - const V normalizer = - Set(df, ib->metadata()->IntensityTarget()) * inv_max_display_nits; - - for (V* const val : {&red, &green, &blue}) { - *val = IfThenElse(luminance <= Set(df, 1e-6f), new_luminance, - *val * ratio) * - normalizer; - } - + tone_mapper.ToneMap(&red, &green, &blue); Store(red, df, row_r + x); Store(green, df, row_g + x); Store(blue, df, row_b + x); } }, - "ToneMap")); - - return true; + "ToneMap"); } Status GamutMapFrame(ImageBundle* const ib, float preserve_saturation, @@ -141,44 +83,8 @@ Status GamutMapFrame(ImageBundle* const ib, float preserve_saturation, V red = Load(df, row_r + x); V green = Load(df, row_g + x); V blue = Load(df, row_b + x); - const V luminance = - MulAdd(Set(df, 0.2627f), red, - MulAdd(Set(df, 0.6780f), green, Set(df, 0.0593f) * blue)); - - // Desaturate out-of-gamut pixels. This is done by mixing each pixel - // with just enough gray of the target luminance to make all - // components non-negative. - // - For saturation preservation, if a component is still larger than - // 1 then the pixel is normalized to have a maximum component of 1. - // That will reduce its luminance. - // - For luminance preservation, getting all components below 1 is - // done by mixing in yet more gray. That will desaturate it further. - V gray_mix_saturation = Zero(df); - V gray_mix_luminance = Zero(df); - for (const V val : {red, green, blue}) { - const V inv_val_minus_gray = Set(df, 1) / (val - luminance); - gray_mix_saturation = - IfThenElse(val >= luminance, gray_mix_saturation, - Max(gray_mix_saturation, val * inv_val_minus_gray)); - gray_mix_luminance = - Max(gray_mix_luminance, - IfThenElse(val <= luminance, gray_mix_saturation, - (val - Set(df, 1)) * inv_val_minus_gray)); - } - const V gray_mix = - Clamp(Set(df, preserve_saturation) * - (gray_mix_saturation - gray_mix_luminance) + - gray_mix_luminance, - Zero(df), Set(df, 1)); - for (V* const val : {&red, &green, &blue}) { - *val = MulAdd(gray_mix, luminance - *val, *val); - } - const V normalizer = - Set(df, 1) / Max(Set(df, 1), Max(red, Max(green, blue))); - for (V* const val : {&red, &green, &blue}) { - *val = *val * normalizer; - } - + GamutMap(&red, &green, &blue, rec2020_luminances, + preserve_saturation); Store(red, df, row_r + x); Store(green, df, row_g + x); Store(blue, df, row_b + x); diff --git a/media/libjxl/src/lib/extras/tone_mapping_gbench.cc b/media/libjxl/src/lib/extras/tone_mapping_gbench.cc index b156992813..2f97b88667 100644 --- a/media/libjxl/src/lib/extras/tone_mapping_gbench.cc +++ b/media/libjxl/src/lib/extras/tone_mapping_gbench.cc @@ -13,8 +13,7 @@ namespace jxl { static void BM_ToneMapping(benchmark::State& state) { CodecInOut image; - const PaddedBytes image_bytes = - ReadTestData("third_party/imagecompression.info/flower_foveon.png"); + const PaddedBytes image_bytes = ReadTestData("jxl/flower/flower.png"); JXL_CHECK(SetFromBytes(Span<const uint8_t>(image_bytes), &image)); // Convert to linear Rec. 2020 so that `ToneMapTo` doesn't have to and we |