summaryrefslogtreecommitdiff
path: root/lib/extras
diff options
context:
space:
mode:
Diffstat (limited to 'lib/extras')
-rw-r--r--lib/extras/alpha_blend.cc63
-rw-r--r--lib/extras/alpha_blend.h19
-rw-r--r--lib/extras/codec.cc114
-rw-r--r--lib/extras/codec.h24
-rw-r--r--lib/extras/codec_test.cc349
-rw-r--r--lib/extras/common.cc61
-rw-r--r--lib/extras/common.h26
-rw-r--r--lib/extras/dec/apng.cc289
-rw-r--r--lib/extras/dec/apng.h11
-rw-r--r--lib/extras/dec/color_description.cc4
-rw-r--r--lib/extras/dec/color_description.h3
-rw-r--r--lib/extras/dec/color_description_test.cc5
-rw-r--r--lib/extras/dec/color_hints.cc44
-rw-r--r--lib/extras/dec/color_hints.h2
-rw-r--r--lib/extras/dec/decode.cc180
-rw-r--r--lib/extras/dec/decode.h24
-rw-r--r--lib/extras/dec/exr.cc27
-rw-r--r--lib/extras/dec/exr.h10
-rw-r--r--lib/extras/dec/gif.cc45
-rw-r--r--lib/extras/dec/gif.h9
-rw-r--r--lib/extras/dec/jpegli.cc271
-rw-r--r--lib/extras/dec/jpegli.h41
-rw-r--r--lib/extras/dec/jpg.cc73
-rw-r--r--lib/extras/dec/jpg.h18
-rw-r--r--lib/extras/dec/jxl.cc158
-rw-r--r--lib/extras/dec/jxl.h11
-rw-r--r--lib/extras/dec/pgx.cc8
-rw-r--r--lib/extras/dec/pgx.h8
-rw-r--r--lib/extras/dec/pgx_test.cc14
-rw-r--r--lib/extras/dec/pnm.cc176
-rw-r--r--lib/extras/dec/pnm.h28
-rw-r--r--lib/extras/enc/apng.cc133
-rw-r--r--lib/extras/enc/encode.cc124
-rw-r--r--lib/extras/enc/encode.h25
-rw-r--r--lib/extras/enc/exr.cc14
-rw-r--r--lib/extras/enc/jpegli.cc523
-rw-r--r--lib/extras/enc/jpegli.h53
-rw-r--r--lib/extras/enc/jpg.cc450
-rw-r--r--lib/extras/enc/jxl.cc359
-rw-r--r--lib/extras/enc/jxl.h91
-rw-r--r--lib/extras/enc/npy.cc3
-rw-r--r--lib/extras/enc/pgx.cc4
-rw-r--r--lib/extras/enc/pnm.cc340
-rw-r--r--lib/extras/enc/pnm.h1
-rw-r--r--lib/extras/exif.cc2
-rw-r--r--lib/extras/hlg.cc13
-rw-r--r--lib/extras/jpegli_test.cc415
-rw-r--r--lib/extras/metrics.cc224
-rw-r--r--lib/extras/metrics.h28
-rw-r--r--lib/extras/packed_image.h198
-rw-r--r--lib/extras/packed_image_convert.cc51
-rw-r--r--lib/extras/packed_image_convert.h3
-rw-r--r--lib/extras/render_hdr.cc60
-rw-r--r--lib/extras/render_hdr.h27
-rw-r--r--lib/extras/size_constraints.h43
-rw-r--r--lib/extras/time.cc1
-rw-r--r--lib/extras/tone_mapping.cc24
-rw-r--r--lib/extras/tone_mapping_gbench.cc28
58 files changed, 4326 insertions, 1026 deletions
diff --git a/lib/extras/alpha_blend.cc b/lib/extras/alpha_blend.cc
new file mode 100644
index 0000000..50c141c
--- /dev/null
+++ b/lib/extras/alpha_blend.cc
@@ -0,0 +1,63 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/alpha_blend.h"
+
+#include "lib/extras/packed_image.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+void AlphaBlend(PackedFrame* frame, float background[3]) {
+ if (!frame) return;
+ const PackedImage& im = frame->color;
+ JxlPixelFormat format = im.format;
+ if (format.num_channels != 2 && format.num_channels != 4) {
+ return;
+ }
+ --format.num_channels;
+ PackedImage blended(im.xsize, im.ysize, format);
+ // TODO(szabadka) SIMDify this and make it work for float16.
+ for (size_t y = 0; y < im.ysize; ++y) {
+ for (size_t x = 0; x < im.xsize; ++x) {
+ if (format.num_channels == 2) {
+ float g = im.GetPixelValue(y, x, 0);
+ float a = im.GetPixelValue(y, x, 1);
+ float out = g * a + background[0] * (1 - a);
+ blended.SetPixelValue(y, x, 0, out);
+ } else {
+ float r = im.GetPixelValue(y, x, 0);
+ float g = im.GetPixelValue(y, x, 1);
+ float b = im.GetPixelValue(y, x, 2);
+ float a = im.GetPixelValue(y, x, 3);
+ float out_r = r * a + background[0] * (1 - a);
+ float out_g = g * a + background[1] * (1 - a);
+ float out_b = b * a + background[2] * (1 - a);
+ blended.SetPixelValue(y, x, 0, out_r);
+ blended.SetPixelValue(y, x, 1, out_g);
+ blended.SetPixelValue(y, x, 2, out_b);
+ }
+ }
+ }
+ frame->color = blended.Copy();
+}
+
+} // namespace
+
+void AlphaBlend(PackedPixelFile* ppf, float background[3]) {
+ if (!ppf || ppf->info.alpha_bits == 0) {
+ return;
+ }
+ ppf->info.alpha_bits = 0;
+ AlphaBlend(ppf->preview_frame.get(), background);
+ for (auto& frame : ppf->frames) {
+ AlphaBlend(&frame, background);
+ }
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/lib/extras/alpha_blend.h b/lib/extras/alpha_blend.h
new file mode 100644
index 0000000..4d78e86
--- /dev/null
+++ b/lib/extras/alpha_blend.h
@@ -0,0 +1,19 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ALPHA_BLEND_H_
+#define LIB_EXTRAS_ALPHA_BLEND_H_
+
+#include "lib/extras/packed_image.h"
+
+namespace jxl {
+namespace extras {
+
+void AlphaBlend(PackedPixelFile* ppf, float background[3]);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ALPHA_BLEND_H_
diff --git a/lib/extras/codec.cc b/lib/extras/codec.cc
index 774b4cc..3ba31f2 100644
--- a/lib/extras/codec.cc
+++ b/lib/extras/codec.cc
@@ -5,27 +5,18 @@
#include "lib/extras/codec.h"
-#include "jxl/decode.h"
-#include "jxl/types.h"
-#include "lib/extras/packed_image.h"
-#include "lib/jxl/base/padded_bytes.h"
-#include "lib/jxl/base/status.h"
+#include <jxl/decode.h>
+#include <jxl/types.h>
-#if JPEGXL_ENABLE_APNG
+#include "lib/extras/dec/decode.h"
#include "lib/extras/enc/apng.h"
-#endif
-#if JPEGXL_ENABLE_JPEG
-#include "lib/extras/enc/jpg.h"
-#endif
-#if JPEGXL_ENABLE_EXR
#include "lib/extras/enc/exr.h"
-#endif
-
-#include "lib/extras/dec/decode.h"
+#include "lib/extras/enc/jpg.h"
#include "lib/extras/enc/pgx.h"
#include "lib/extras/enc/pnm.h"
+#include "lib/extras/packed_image.h"
#include "lib/extras/packed_image_convert.h"
-#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/status.h"
#include "lib/jxl/image_bundle.h"
namespace jxl {
@@ -38,30 +29,21 @@ constexpr size_t kMinBytes = 9;
Status SetFromBytes(const Span<const uint8_t> bytes,
const extras::ColorHints& color_hints, CodecInOut* io,
- ThreadPool* pool, extras::Codec* orig_codec) {
+ ThreadPool* pool, const SizeConstraints* constraints,
+ extras::Codec* orig_codec) {
if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes");
extras::PackedPixelFile ppf;
- if (extras::DecodeBytes(bytes, color_hints, io->constraints, &ppf,
- orig_codec)) {
+ if (extras::DecodeBytes(bytes, color_hints, &ppf, constraints, orig_codec)) {
return ConvertPackedPixelFileToCodecInOut(ppf, pool, io);
}
return JXL_FAILURE("Codecs failed to decode");
}
-Status SetFromFile(const std::string& pathname,
- const extras::ColorHints& color_hints, CodecInOut* io,
- ThreadPool* pool, extras::Codec* orig_codec) {
- std::vector<uint8_t> encoded;
- JXL_RETURN_IF_ERROR(ReadFile(pathname, &encoded));
- JXL_RETURN_IF_ERROR(SetFromBytes(Span<const uint8_t>(encoded), color_hints,
- io, pool, orig_codec));
- return true;
-}
-
Status Encode(const CodecInOut& io, const extras::Codec codec,
const ColorEncoding& c_desired, size_t bits_per_sample,
std::vector<uint8_t>* bytes, ThreadPool* pool) {
+ bytes->clear();
JXL_CHECK(!io.Main().c_current().ICC().empty());
JXL_CHECK(!c_desired.ICC().empty());
io.CheckMetadata();
@@ -77,22 +59,22 @@ Status Encode(const CodecInOut& io, const extras::Codec codec,
std::ostringstream os;
switch (codec) {
case extras::Codec::kPNG:
-#if JPEGXL_ENABLE_APNG
encoder = extras::GetAPNGEncoder();
- break;
-#else
- return JXL_FAILURE("JPEG XL was built without (A)PNG support");
-#endif
+ if (encoder) {
+ break;
+ } else {
+ return JXL_FAILURE("JPEG XL was built without (A)PNG support");
+ }
case extras::Codec::kJPG:
-#if JPEGXL_ENABLE_JPEG
format.data_type = JXL_TYPE_UINT8;
encoder = extras::GetJPEGEncoder();
- os << io.jpeg_quality;
- encoder->SetOption("q", os.str());
- break;
-#else
- return JXL_FAILURE("JPEG XL was built without JPEG support");
-#endif
+ if (encoder) {
+ os << io.jpeg_quality;
+ encoder->SetOption("q", os.str());
+ break;
+ } else {
+ return JXL_FAILURE("JPEG XL was built without JPEG support");
+ }
case extras::Codec::kPNM:
if (io.Main().HasAlpha()) {
encoder = extras::GetPAMEncoder();
@@ -102,14 +84,9 @@ Status Encode(const CodecInOut& io, const extras::Codec codec,
encoder = extras::GetPPMEncoder();
} else {
format.data_type = JXL_TYPE_FLOAT;
- format.endianness = JXL_NATIVE_ENDIAN;
+ format.endianness = JXL_LITTLE_ENDIAN;
encoder = extras::GetPFMEncoder();
}
- if (!c_desired.IsSRGB()) {
- JXL_WARNING(
- "PNM encoder cannot store custom ICC profile; decoder "
- "will need hint key=color_space to get the same values");
- }
break;
case extras::Codec::kPGX:
encoder = extras::GetPGXEncoder();
@@ -117,13 +94,17 @@ Status Encode(const CodecInOut& io, const extras::Codec codec,
case extras::Codec::kGIF:
return JXL_FAILURE("Encoding to GIF is not implemented");
case extras::Codec::kEXR:
-#if JPEGXL_ENABLE_EXR
format.data_type = JXL_TYPE_FLOAT;
encoder = extras::GetEXREncoder();
- break;
-#else
- return JXL_FAILURE("JPEG XL was built without OpenEXR support");
-#endif
+ if (encoder) {
+ break;
+ } else {
+ return JXL_FAILURE("JPEG XL was built without OpenEXR support");
+ }
+ case extras::Codec::kJXL:
+ // TODO(user): implement
+ return JXL_FAILURE("Codec::kJXL is not supported yet");
+
case extras::Codec::kUnknown:
return JXL_FAILURE("Cannot encode using Codec::kUnknown");
}
@@ -135,6 +116,11 @@ Status Encode(const CodecInOut& io, const extras::Codec codec,
extras::PackedPixelFile ppf;
JXL_RETURN_IF_ERROR(
ConvertCodecInOutToPackedPixelFile(io, format, c_desired, pool, &ppf));
+ ppf.info.bits_per_sample = bits_per_sample;
+ if (format.data_type == JXL_TYPE_FLOAT) {
+ ppf.info.bits_per_sample = 32;
+ ppf.info.exponent_bits_per_sample = 8;
+ }
extras::EncodedImage encoded_image;
JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded_image, pool));
JXL_ASSERT(encoded_image.bitstreams.size() == 1);
@@ -143,15 +129,15 @@ Status Encode(const CodecInOut& io, const extras::Codec codec,
return true;
}
-Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired,
- size_t bits_per_sample, const std::string& pathname,
- ThreadPool* pool) {
- const std::string extension = Extension(pathname);
- const extras::Codec codec =
- extras::CodecFromExtension(extension, &bits_per_sample);
+Status Encode(const CodecInOut& io, const ColorEncoding& c_desired,
+ size_t bits_per_sample, const std::string& pathname,
+ std::vector<uint8_t>* bytes, ThreadPool* pool) {
+ std::string extension;
+ const extras::Codec codec = extras::CodecFromPath(
+ pathname, &bits_per_sample, /* filename */ nullptr, &extension);
// Warn about incorrect usage of PGM/PGX/PPM - only the latter supports
- // color, but CodecFromExtension lumps them all together.
+ // color, but CodecFromPath lumps them all together.
if (codec == extras::Codec::kPNM && extension != ".pfm") {
if (io.Main().HasAlpha() && extension != ".pam") {
JXL_WARNING(
@@ -174,16 +160,14 @@ Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired,
bits_per_sample = 16;
}
- std::vector<uint8_t> encoded;
- return Encode(io, codec, c_desired, bits_per_sample, &encoded, pool) &&
- WriteFile(encoded, pathname);
+ return Encode(io, codec, c_desired, bits_per_sample, bytes, pool);
}
-Status EncodeToFile(const CodecInOut& io, const std::string& pathname,
- ThreadPool* pool) {
+Status Encode(const CodecInOut& io, const std::string& pathname,
+ std::vector<uint8_t>* bytes, ThreadPool* pool) {
// TODO(lode): need to take the floating_point_sample field into account
- return EncodeToFile(io, io.metadata.m.color_encoding,
- io.metadata.m.bit_depth.bits_per_sample, pathname, pool);
+ return Encode(io, io.metadata.m.color_encoding,
+ io.metadata.m.bit_depth.bits_per_sample, pathname, bytes, pool);
}
} // namespace jxl
diff --git a/lib/extras/codec.h b/lib/extras/codec.h
index 73fdc80..6b39ffd 100644
--- a/lib/extras/codec.h
+++ b/lib/extras/codec.h
@@ -17,7 +17,6 @@
#include "lib/extras/dec/decode.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/codec_in_out.h"
@@ -26,25 +25,24 @@
namespace jxl {
+struct SizeConstraints;
+
// Decodes "bytes" and sets io->metadata.m.
// color_space_hint may specify the color space, otherwise, defaults to sRGB.
Status SetFromBytes(Span<const uint8_t> bytes,
const extras::ColorHints& color_hints, CodecInOut* io,
ThreadPool* pool = nullptr,
+ const SizeConstraints* constraints = nullptr,
extras::Codec* orig_codec = nullptr);
// Helper function to use no color_space_hint.
JXL_INLINE Status SetFromBytes(const Span<const uint8_t> bytes, CodecInOut* io,
ThreadPool* pool = nullptr,
+ const SizeConstraints* constraints = nullptr,
extras::Codec* orig_codec = nullptr) {
- return SetFromBytes(bytes, extras::ColorHints(), io, pool, orig_codec);
+ return SetFromBytes(bytes, extras::ColorHints(), io, pool, constraints,
+ orig_codec);
}
-// Reads from file and calls SetFromBytes.
-Status SetFromFile(const std::string& pathname,
- const extras::ColorHints& color_hints, CodecInOut* io,
- ThreadPool* pool = nullptr,
- extras::Codec* orig_codec = nullptr);
-
// Replaces "bytes" with an encoding of pixels transformed from c_current
// color space to c_desired.
Status Encode(const CodecInOut& io, extras::Codec codec,
@@ -52,12 +50,12 @@ Status Encode(const CodecInOut& io, extras::Codec codec,
std::vector<uint8_t>* bytes, ThreadPool* pool = nullptr);
// Deduces codec, calls Encode and writes to file.
-Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired,
- size_t bits_per_sample, const std::string& pathname,
- ThreadPool* pool = nullptr);
+Status Encode(const CodecInOut& io, const ColorEncoding& c_desired,
+ size_t bits_per_sample, const std::string& pathname,
+ std::vector<uint8_t>* bytes, ThreadPool* pool = nullptr);
// Same, but defaults to metadata.original color_encoding and bits_per_sample.
-Status EncodeToFile(const CodecInOut& io, const std::string& pathname,
- ThreadPool* pool = nullptr);
+Status Encode(const CodecInOut& io, const std::string& pathname,
+ std::vector<uint8_t>* bytes, ThreadPool* pool = nullptr);
} // namespace jxl
diff --git a/lib/extras/codec_test.cc b/lib/extras/codec_test.cc
index 19cac39..6e86ba9 100644
--- a/lib/extras/codec_test.cc
+++ b/lib/extras/codec_test.cc
@@ -6,30 +6,28 @@
#include "lib/extras/codec.h"
#include <stddef.h>
-#include <stdio.h>
#include <algorithm>
+#include <cstdint>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
-#include "lib/extras/dec/pgx.h"
+#include "lib/extras/common.h"
+#include "lib/extras/dec/decode.h"
#include "lib/extras/dec/pnm.h"
#include "lib/extras/enc/encode.h"
-#include "lib/extras/packed_image_convert.h"
-#include "lib/jxl/base/printf_macros.h"
#include "lib/jxl/base/random.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/color_management.h"
-#include "lib/jxl/enc_color_management.h"
-#include "lib/jxl/image.h"
-#include "lib/jxl/image_bundle.h"
-#include "lib/jxl/image_test_utils.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
#include "lib/jxl/test_utils.h"
-#include "lib/jxl/testdata.h"
+#include "lib/jxl/testing.h"
namespace jxl {
+
+using test::ThreadPoolForTests;
+
namespace extras {
namespace {
@@ -37,7 +35,6 @@ using ::testing::AllOf;
using ::testing::Contains;
using ::testing::Field;
using ::testing::IsEmpty;
-using ::testing::NotNull;
using ::testing::SizeIs;
std::string ExtensionFromCodec(Codec codec, const bool is_gray,
@@ -51,18 +48,14 @@ std::string ExtensionFromCodec(Codec codec, const bool is_gray,
case Codec::kPNG:
return ".png";
case Codec::kPNM:
+ if (bits_per_sample == 32) return ".pfm";
if (has_alpha) return ".pam";
- if (is_gray) return ".pgm";
- return (bits_per_sample == 32) ? ".pfm" : ".ppm";
- case Codec::kGIF:
- return ".gif";
+ return is_gray ? ".pgm" : ".ppm";
case Codec::kEXR:
return ".exr";
- case Codec::kUnknown:
+ default:
return std::string();
}
- JXL_UNREACHABLE;
- return std::string();
}
void VerifySameImage(const PackedImage& im0, size_t bits_per_sample0,
@@ -110,17 +103,16 @@ JxlColorEncoding CreateTestColorEncoding(bool is_gray) {
// Roundtrip through internal color encoding to fill in primaries and white
// point CIE xy coordinates.
ColorEncoding c_internal;
- JXL_CHECK(ConvertExternalToInternalColorEncoding(c, &c_internal));
- ConvertInternalToExternalColorEncoding(c_internal, &c);
+ JXL_CHECK(c_internal.FromExternal(c));
+ c = c_internal.ToExternal();
return c;
}
std::vector<uint8_t> GenerateICC(JxlColorEncoding color_encoding) {
ColorEncoding c;
- JXL_CHECK(ConvertExternalToInternalColorEncoding(color_encoding, &c));
- JXL_CHECK(c.CreateICC());
- PaddedBytes icc = c.ICC();
- return std::vector<uint8_t>(icc.begin(), icc.end());
+ JXL_CHECK(c.FromExternal(color_encoding));
+ JXL_CHECK(!c.ICC().empty());
+ return c.ICC();
}
void StoreRandomValue(uint8_t* out, Rng* rng, JxlPixelFormat format,
@@ -173,10 +165,11 @@ struct TestImageParams {
bool is_gray;
bool add_alpha;
bool big_endian;
+ bool add_extra_channels;
bool ShouldTestRoundtrip() const {
if (codec == Codec::kPNG) {
- return true;
+ return bits_per_sample <= 16;
} else if (codec == Codec::kPNM) {
// TODO(szabadka) Make PNM encoder endianness-aware.
return ((bits_per_sample <= 16 && big_endian) ||
@@ -213,7 +206,7 @@ struct TestImageParams {
std::string DebugString() const {
std::ostringstream os;
os << "bps:" << bits_per_sample << " gr:" << is_gray << " al:" << add_alpha
- << " be: " << big_endian;
+ << " be: " << big_endian << " ec: " << add_extra_channels;
return os.str();
}
};
@@ -233,6 +226,19 @@ void CreateTestImage(const TestImageParams& params, PackedPixelFile* ppf) {
PackedFrame frame(params.xsize, params.ysize, params.PixelFormat());
FillPackedImage(params.bits_per_sample, &frame.color);
+ if (params.add_extra_channels) {
+ for (size_t i = 0; i < 7; ++i) {
+ JxlPixelFormat ec_format = params.PixelFormat();
+ ec_format.num_channels = 1;
+ PackedImage ec(params.xsize, params.ysize, ec_format);
+ FillPackedImage(params.bits_per_sample, &ec);
+ frame.extra_channels.emplace_back(std::move(ec));
+ PackedExtraChannel pec;
+ pec.ec_info.bits_per_sample = params.bits_per_sample;
+ pec.ec_info.type = static_cast<JxlExtraChannelType>(i);
+ ppf->extra_channels_info.emplace_back(std::move(pec));
+ }
+ }
ppf->frames.emplace_back(std::move(frame));
}
@@ -249,33 +255,67 @@ void TestRoundTrip(const TestImageParams& params, ThreadPool* pool) {
EncodedImage encoded;
auto encoder = Encoder::FromExtension(extension);
- ASSERT_TRUE(encoder.get());
+ if (!encoder) {
+ fprintf(stderr, "Skipping test because of missing codec support.\n");
+ return;
+ }
ASSERT_TRUE(encoder->Encode(ppf_in, &encoded, pool));
ASSERT_EQ(encoded.bitstreams.size(), 1);
PackedPixelFile ppf_out;
- ASSERT_TRUE(DecodeBytes(Span<const uint8_t>(encoded.bitstreams[0]),
- ColorHints(), SizeConstraints(), &ppf_out));
-
- if (params.codec != Codec::kPNM && params.codec != Codec::kPGX &&
- params.codec != Codec::kEXR) {
+ ColorHints color_hints;
+ if (params.codec == Codec::kPNM || params.codec == Codec::kPGX) {
+ color_hints.Add("color_space",
+ params.is_gray ? "Gra_D65_Rel_SRG" : "RGB_D65_SRG_Rel_SRG");
+ }
+ ASSERT_TRUE(DecodeBytes(Bytes(encoded.bitstreams[0]), color_hints, &ppf_out));
+ if (params.codec == Codec::kPNG && ppf_out.icc.empty()) {
+ // Decoding a PNG may drop the ICC profile if there's a valid cICP chunk.
+ // Rendering intent is not preserved in this case.
+ EXPECT_EQ(ppf_in.color_encoding.color_space,
+ ppf_out.color_encoding.color_space);
+ EXPECT_EQ(ppf_in.color_encoding.white_point,
+ ppf_out.color_encoding.white_point);
+ if (ppf_in.color_encoding.color_space != JXL_COLOR_SPACE_GRAY) {
+ EXPECT_EQ(ppf_in.color_encoding.primaries,
+ ppf_out.color_encoding.primaries);
+ }
+ EXPECT_EQ(ppf_in.color_encoding.transfer_function,
+ ppf_out.color_encoding.transfer_function);
+ EXPECT_EQ(ppf_out.color_encoding.rendering_intent,
+ JXL_RENDERING_INTENT_RELATIVE);
+ } else if (params.codec != Codec::kPNM && params.codec != Codec::kPGX &&
+ params.codec != Codec::kEXR) {
EXPECT_EQ(ppf_in.icc, ppf_out.icc);
}
ASSERT_EQ(ppf_out.frames.size(), 1);
- VerifySameImage(ppf_in.frames[0].color, ppf_in.info.bits_per_sample,
- ppf_out.frames[0].color, ppf_out.info.bits_per_sample,
+ const auto& frame_in = ppf_in.frames[0];
+ const auto& frame_out = ppf_out.frames[0];
+ VerifySameImage(frame_in.color, ppf_in.info.bits_per_sample, frame_out.color,
+ ppf_out.info.bits_per_sample,
/*lossless=*/params.codec != Codec::kJPG);
+ ASSERT_EQ(frame_in.extra_channels.size(), frame_out.extra_channels.size());
+ ASSERT_EQ(ppf_out.extra_channels_info.size(),
+ frame_out.extra_channels.size());
+ for (size_t i = 0; i < frame_in.extra_channels.size(); ++i) {
+ VerifySameImage(frame_in.extra_channels[i], ppf_in.info.bits_per_sample,
+ frame_out.extra_channels[i], ppf_out.info.bits_per_sample,
+ /*lossless=*/true);
+ EXPECT_EQ(ppf_out.extra_channels_info[i].ec_info.type,
+ ppf_in.extra_channels_info[i].ec_info.type);
+ }
}
TEST(CodecTest, TestRoundTrip) {
- ThreadPoolInternal pool(12);
+ ThreadPoolForTests pool(12);
TestImageParams params;
params.xsize = 7;
params.ysize = 4;
- for (Codec codec : AvailableCodecs()) {
+ for (Codec codec :
+ {Codec::kPNG, Codec::kPNM, Codec::kPGX, Codec::kEXR, Codec::kJPG}) {
for (int bits_per_sample : {4, 8, 10, 12, 16, 32}) {
for (bool is_gray : {false, true}) {
for (bool add_alpha : {false, true}) {
@@ -285,7 +325,12 @@ TEST(CodecTest, TestRoundTrip) {
params.is_gray = is_gray;
params.add_alpha = add_alpha;
params.big_endian = big_endian;
+ params.add_extra_channels = false;
TestRoundTrip(params, &pool);
+ if (codec == Codec::kPNM && add_alpha) {
+ params.add_extra_channels = true;
+ TestRoundTrip(params, &pool);
+ }
}
}
}
@@ -293,192 +338,39 @@ TEST(CodecTest, TestRoundTrip) {
}
}
-CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool,
- const ColorHints& color_hints = ColorHints()) {
- CodecInOut io;
- const PaddedBytes orig = ReadTestData(pathname);
- JXL_CHECK(
- SetFromBytes(Span<const uint8_t>(orig), color_hints, &io, pool, nullptr));
- const ImageBundle& ib1 = io.Main();
-
- // Encode/Decode again to make sure Encode carries through all metadata.
- std::vector<uint8_t> encoded;
- JXL_CHECK(Encode(io, Codec::kPNG, io.metadata.m.color_encoding,
- io.metadata.m.bit_depth.bits_per_sample, &encoded, pool));
-
- CodecInOut io2;
- JXL_CHECK(SetFromBytes(Span<const uint8_t>(encoded), color_hints, &io2, pool,
- nullptr));
- const ImageBundle& ib2 = io2.Main();
- EXPECT_EQ(Description(ib1.metadata()->color_encoding),
- Description(ib2.metadata()->color_encoding));
- EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current()));
-
- size_t bits_per_sample = io2.metadata.m.bit_depth.bits_per_sample;
-
- // "Same" pixels?
- double max_l1 = bits_per_sample <= 12 ? 1.3 : 2E-3;
- double max_rel = bits_per_sample <= 12 ? 6E-3 : 1E-4;
- if (ib1.metadata()->color_encoding.IsGray()) {
- max_rel *= 2.0;
- } else if (ib1.metadata()->color_encoding.primaries != Primaries::kSRGB) {
- // Need more tolerance for large gamuts (anything but sRGB)
- max_l1 *= 1.5;
- max_rel *= 3.0;
- }
- VerifyRelativeError(ib1.color(), ib2.color(), max_l1, max_rel);
-
- // Simulate the encoder removing profile and decoder restoring it.
- if (!ib2.metadata()->color_encoding.WantICC()) {
- io2.metadata.m.color_encoding.InternalRemoveICC();
- EXPECT_TRUE(io2.metadata.m.color_encoding.CreateICC());
- }
-
- return io2;
-}
-
-#if 0
-TEST(CodecTest, TestMetadataSRGB) {
- ThreadPoolInternal pool(12);
-
- const char* paths[] = {"external/raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png",
- "external/raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png",
- "external/raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png",
- "external/raw.pixls/Nikon-D300-12bit_srgb8_dt.png",
- "external/raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"};
- for (const char* relative_pathname : paths) {
- const CodecInOut io =
- DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool);
- EXPECT_EQ(8, io.metadata.m.bit_depth.bits_per_sample);
- EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
- EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample);
-
- EXPECT_EQ(64, io.xsize());
- EXPECT_EQ(64, io.ysize());
- EXPECT_FALSE(io.metadata.m.HasAlpha());
-
- const ColorEncoding& c_original = io.metadata.m.color_encoding;
- EXPECT_FALSE(c_original.ICC().empty());
- EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
- EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
- EXPECT_EQ(Primaries::kSRGB, c_original.primaries);
- EXPECT_TRUE(c_original.tf.IsSRGB());
- }
-}
-
-TEST(CodecTest, TestMetadataLinear) {
- ThreadPoolInternal pool(12);
-
- const char* paths[3] = {
- "external/raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png",
- "external/raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png",
- "external/raw.pixls/Nikon-D300-12bit_2020_g1_dt.png",
- };
- const WhitePoint white_points[3] = {WhitePoint::kCustom, WhitePoint::kD65,
- WhitePoint::kD65};
- const Primaries primaries[3] = {Primaries::kCustom, Primaries::kSRGB,
- Primaries::k2100};
-
- for (size_t i = 0; i < 3; ++i) {
- const CodecInOut io = DecodeRoundtrip(paths[i], Codec::kPNG, &pool);
- EXPECT_EQ(16, io.metadata.m.bit_depth.bits_per_sample);
- EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
- EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample);
-
- EXPECT_EQ(64, io.xsize());
- EXPECT_EQ(64, io.ysize());
- EXPECT_FALSE(io.metadata.m.HasAlpha());
-
- const ColorEncoding& c_original = io.metadata.m.color_encoding;
- EXPECT_FALSE(c_original.ICC().empty());
- EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
- EXPECT_EQ(white_points[i], c_original.white_point);
- EXPECT_EQ(primaries[i], c_original.primaries);
- EXPECT_TRUE(c_original.tf.IsLinear());
- }
-}
-
-TEST(CodecTest, TestMetadataICC) {
- ThreadPoolInternal pool(12);
-
- const char* paths[] = {
- "external/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png",
- "external/raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png",
- };
- for (const char* relative_pathname : paths) {
- const CodecInOut io =
- DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool);
- EXPECT_GE(16, io.metadata.m.bit_depth.bits_per_sample);
- EXPECT_LE(14, io.metadata.m.bit_depth.bits_per_sample);
-
- EXPECT_EQ(64, io.xsize());
- EXPECT_EQ(64, io.ysize());
- EXPECT_FALSE(io.metadata.m.HasAlpha());
-
- const ColorEncoding& c_original = io.metadata.m.color_encoding;
- EXPECT_FALSE(c_original.ICC().empty());
- EXPECT_EQ(RenderingIntent::kPerceptual, c_original.rendering_intent);
- EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
- EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
- EXPECT_EQ(Primaries::kSRGB, c_original.primaries);
- EXPECT_EQ(TransferFunction::k709, c_original.tf.GetTransferFunction());
+TEST(CodecTest, LosslessPNMRoundtrip) {
+ ThreadPoolForTests pool(12);
+
+ static const char* kChannels[] = {"", "g", "ga", "rgb", "rgba"};
+ static const char* kExtension[] = {"", ".pgm", ".pam", ".ppm", ".pam"};
+ for (size_t bit_depth = 1; bit_depth <= 16; ++bit_depth) {
+ for (size_t channels = 1; channels <= 4; ++channels) {
+ if (bit_depth == 1 && (channels == 2 || channels == 4)) continue;
+ std::string extension(kExtension[channels]);
+ std::string filename = "jxl/flower/flower_small." +
+ std::string(kChannels[channels]) + ".depth" +
+ std::to_string(bit_depth) + extension;
+ const std::vector<uint8_t> orig = jxl::test::ReadTestData(filename);
+
+ PackedPixelFile ppf;
+ ColorHints color_hints;
+ color_hints.Add("color_space",
+ channels < 3 ? "Gra_D65_Rel_SRG" : "RGB_D65_SRG_Rel_SRG");
+ ASSERT_TRUE(
+ DecodeBytes(Bytes(orig.data(), orig.size()), color_hints, &ppf));
+
+ EncodedImage encoded;
+ auto encoder = Encoder::FromExtension(extension);
+ ASSERT_TRUE(encoder.get());
+ ASSERT_TRUE(encoder->Encode(ppf, &encoded, &pool));
+ ASSERT_EQ(encoded.bitstreams.size(), 1);
+ ASSERT_EQ(orig.size(), encoded.bitstreams[0].size());
+ EXPECT_EQ(0,
+ memcmp(orig.data(), encoded.bitstreams[0].data(), orig.size()));
+ }
}
}
-TEST(CodecTest, Testexternal/pngsuite) {
- ThreadPoolInternal pool(12);
-
- // Ensure we can load PNG with text, japanese UTF-8, compressed text.
- (void)DecodeRoundtrip("external/pngsuite/ct1n0g04.png", Codec::kPNG, &pool);
- (void)DecodeRoundtrip("external/pngsuite/ctjn0g04.png", Codec::kPNG, &pool);
- (void)DecodeRoundtrip("external/pngsuite/ctzn0g04.png", Codec::kPNG, &pool);
-
- // Extract gAMA
- const CodecInOut b1 =
- DecodeRoundtrip("external/pngsuite/g10n3p04.png", Codec::kPNG, &pool);
- EXPECT_TRUE(b1.metadata.color_encoding.tf.IsLinear());
-
- // Extract cHRM
- const CodecInOut b_p =
- DecodeRoundtrip("external/pngsuite/ccwn2c08.png", Codec::kPNG, &pool);
- EXPECT_EQ(Primaries::kSRGB, b_p.metadata.color_encoding.primaries);
- EXPECT_EQ(WhitePoint::kD65, b_p.metadata.color_encoding.white_point);
-
- // Extract EXIF from (new-style) dedicated chunk
- const CodecInOut b_exif =
- DecodeRoundtrip("external/pngsuite/exif2c08.png", Codec::kPNG, &pool);
- EXPECT_EQ(978, b_exif.blobs.exif.size());
-}
-#endif
-
-void VerifyWideGamutMetadata(const std::string& relative_pathname,
- const Primaries primaries, ThreadPool* pool) {
- const CodecInOut io = DecodeRoundtrip(relative_pathname, pool);
-
- EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
- EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
- EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
-
- const ColorEncoding& c_original = io.metadata.m.color_encoding;
- EXPECT_FALSE(c_original.ICC().empty());
- EXPECT_EQ(RenderingIntent::kAbsolute, c_original.rendering_intent);
- EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
- EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
- EXPECT_EQ(primaries, c_original.primaries);
-}
-
-TEST(CodecTest, TestWideGamut) {
- ThreadPoolInternal pool(12);
- // VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-bars.png",
- // Primaries::kP3, &pool);
- VerifyWideGamutMetadata("external/wide-gamut-tests/P3-sRGB-color-ring.png",
- Primaries::kP3, &pool);
- // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-bars.png",
- // Primaries::k2100, &pool);
- // VerifyWideGamutMetadata("external/wide-gamut-tests/R2020-sRGB-color-ring.png",
- // Primaries::k2100, &pool);
-}
-
TEST(CodecTest, TestPNM) { TestCodecPNM(); }
TEST(CodecTest, FormatNegotiation) {
@@ -520,13 +412,15 @@ TEST(CodecTest, EncodeToPNG) {
ThreadPool* const pool = nullptr;
std::unique_ptr<Encoder> png_encoder = Encoder::FromExtension(".png");
- ASSERT_THAT(png_encoder, NotNull());
+ if (!png_encoder) {
+ fprintf(stderr, "Skipping test because of missing codec support.\n");
+ return;
+ }
- const PaddedBytes original_png =
- ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
+ const std::vector<uint8_t> original_png = jxl::test::ReadTestData(
+ "external/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
PackedPixelFile ppf;
- ASSERT_TRUE(extras::DecodeBytes(Span<const uint8_t>(original_png),
- ColorHints(), SizeConstraints(), &ppf));
+ ASSERT_TRUE(extras::DecodeBytes(Bytes(original_png), ColorHints(), &ppf));
const JxlPixelFormat& format = ppf.frames.front().color.format;
ASSERT_THAT(
@@ -540,9 +434,8 @@ TEST(CodecTest, EncodeToPNG) {
ASSERT_THAT(encoded_png.bitstreams, SizeIs(1));
PackedPixelFile decoded_ppf;
- ASSERT_TRUE(
- extras::DecodeBytes(Span<const uint8_t>(encoded_png.bitstreams.front()),
- ColorHints(), SizeConstraints(), &decoded_ppf));
+ ASSERT_TRUE(extras::DecodeBytes(Bytes(encoded_png.bitstreams.front()),
+ ColorHints(), &decoded_ppf));
ASSERT_EQ(decoded_ppf.info.bits_per_sample, ppf.info.bits_per_sample);
ASSERT_EQ(decoded_ppf.frames.size(), 1);
diff --git a/lib/extras/common.cc b/lib/extras/common.cc
new file mode 100644
index 0000000..e85b43a
--- /dev/null
+++ b/lib/extras/common.cc
@@ -0,0 +1,61 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/common.h"
+
+#include <jxl/codestream_header.h>
+#include <jxl/types.h>
+
+#include <cstddef>
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats,
+ const JxlBasicInfo& basic_info, JxlPixelFormat* format) {
+ const size_t original_bit_depth = basic_info.bits_per_sample;
+ size_t current_bit_depth = 0;
+ size_t num_alpha_channels = (basic_info.alpha_bits != 0 ? 1 : 0);
+ size_t num_channels = basic_info.num_color_channels + num_alpha_channels;
+ for (;;) {
+ for (const JxlPixelFormat& candidate : accepted_formats) {
+ if (candidate.num_channels != num_channels) continue;
+ const size_t candidate_bit_depth =
+ PackedImage::BitsPerChannel(candidate.data_type);
+ if (
+ // Candidate bit depth is less than what we have and still enough
+ (original_bit_depth <= candidate_bit_depth &&
+ candidate_bit_depth < current_bit_depth) ||
+ // Or larger than the too-small bit depth we currently have
+ (current_bit_depth < candidate_bit_depth &&
+ current_bit_depth < original_bit_depth)) {
+ *format = candidate;
+ current_bit_depth = candidate_bit_depth;
+ }
+ }
+ if (current_bit_depth == 0) {
+ if (num_channels > basic_info.num_color_channels) {
+ // Try dropping the alpha channel.
+ --num_channels;
+ continue;
+ }
+ return JXL_FAILURE("no appropriate format found");
+ }
+ break;
+ }
+ if (current_bit_depth < original_bit_depth) {
+ JXL_WARNING("encoding %" PRIuS "-bit original to %" PRIuS " bits",
+ original_bit_depth, current_bit_depth);
+ }
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/lib/extras/common.h b/lib/extras/common.h
new file mode 100644
index 0000000..88ed581
--- /dev/null
+++ b/lib/extras/common.h
@@ -0,0 +1,26 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_COMMON_H_
+#define LIB_EXTRAS_COMMON_H_
+
+#include <jxl/codestream_header.h>
+#include <jxl/types.h>
+
+#include <vector>
+
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+// TODO(sboukortt): consider exposing this as part of the C API.
+Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats,
+ const JxlBasicInfo& basic_info, JxlPixelFormat* format);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_COMMON_H_
diff --git a/lib/extras/dec/apng.cc b/lib/extras/dec/apng.cc
index 5667466..f77dab7 100644
--- a/lib/extras/dec/apng.cc
+++ b/lib/extras/dec/apng.cc
@@ -36,27 +36,34 @@
*
*/
-#include <stdio.h>
+#include <jxl/codestream_header.h>
+#include <jxl/encode.h>
#include <string.h>
#include <string>
#include <utility>
#include <vector>
-#include "jxl/codestream_header.h"
-#include "jxl/encode.h"
+#include "lib/extras/size_constraints.h"
+#include "lib/jxl/base/byte_order.h"
+#include "lib/jxl/base/common.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/printf_macros.h"
#include "lib/jxl/base/scope_guard.h"
-#include "lib/jxl/common.h"
#include "lib/jxl/sanitizers.h"
+#if JPEGXL_ENABLE_APNG
#include "png.h" /* original (unpatched) libpng is ok */
+#endif
namespace jxl {
namespace extras {
+#if JPEGXL_ENABLE_APNG
namespace {
+constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
+ 0x66, 0x00, 0x00};
+
/* hIST chunk tail is not proccesed properly; skip this chunk completely;
see https://github.com/glennrp/libpng/pull/413 */
const png_byte kIgnoredPngChunks[] = {
@@ -73,11 +80,145 @@ Status DecodeSRGB(const unsigned char* payload, const size_t payload_size,
if (payload_size != 1) return JXL_FAILURE("Wrong sRGB size");
// (PNG uses the same values as ICC.)
if (payload[0] >= 4) return JXL_FAILURE("Invalid Rendering Intent");
+ color_encoding->white_point = JXL_WHITE_POINT_D65;
+ color_encoding->primaries = JXL_PRIMARIES_SRGB;
+ color_encoding->transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
color_encoding->rendering_intent =
static_cast<JxlRenderingIntent>(payload[0]);
return true;
}
+// If the cICP profile is not fully supported, return false and leave
+// color_encoding unmodified.
+Status DecodeCICP(const unsigned char* payload, const size_t payload_size,
+ JxlColorEncoding* color_encoding) {
+ if (payload_size != 4) return JXL_FAILURE("Wrong cICP size");
+ JxlColorEncoding color_enc = *color_encoding;
+
+ // From https://www.itu.int/rec/T-REC-H.273-202107-I/en
+ if (payload[0] == 1) {
+ // IEC 61966-2-1 sRGB
+ color_enc.primaries = JXL_PRIMARIES_SRGB;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 4) {
+ // Rec. ITU-R BT.470-6 System M
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.67;
+ color_enc.primaries_red_xy[1] = 0.33;
+ color_enc.primaries_green_xy[0] = 0.21;
+ color_enc.primaries_green_xy[1] = 0.71;
+ color_enc.primaries_blue_xy[0] = 0.14;
+ color_enc.primaries_blue_xy[1] = 0.08;
+ color_enc.white_point = JXL_WHITE_POINT_CUSTOM;
+ color_enc.white_point_xy[0] = 0.310;
+ color_enc.white_point_xy[1] = 0.316;
+ } else if (payload[0] == 5) {
+ // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.64;
+ color_enc.primaries_red_xy[1] = 0.33;
+ color_enc.primaries_green_xy[0] = 0.29;
+ color_enc.primaries_green_xy[1] = 0.60;
+ color_enc.primaries_blue_xy[0] = 0.15;
+ color_enc.primaries_blue_xy[1] = 0.06;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 6 || payload[0] == 7) {
+ // SMPTE ST 170 (2004) / SMPTE ST 240 (1999)
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.630;
+ color_enc.primaries_red_xy[1] = 0.340;
+ color_enc.primaries_green_xy[0] = 0.310;
+ color_enc.primaries_green_xy[1] = 0.595;
+ color_enc.primaries_blue_xy[0] = 0.155;
+ color_enc.primaries_blue_xy[1] = 0.070;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 8) {
+ // Generic film (colour filters using Illuminant C)
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.681;
+ color_enc.primaries_red_xy[1] = 0.319;
+ color_enc.primaries_green_xy[0] = 0.243;
+ color_enc.primaries_green_xy[1] = 0.692;
+ color_enc.primaries_blue_xy[0] = 0.145;
+ color_enc.primaries_blue_xy[1] = 0.049;
+ color_enc.white_point = JXL_WHITE_POINT_CUSTOM;
+ color_enc.white_point_xy[0] = 0.310;
+ color_enc.white_point_xy[1] = 0.316;
+ } else if (payload[0] == 9) {
+ // Rec. ITU-R BT.2100-2
+ color_enc.primaries = JXL_PRIMARIES_2100;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 10) {
+ // CIE 1931 XYZ
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 1;
+ color_enc.primaries_red_xy[1] = 0;
+ color_enc.primaries_green_xy[0] = 0;
+ color_enc.primaries_green_xy[1] = 1;
+ color_enc.primaries_blue_xy[0] = 0;
+ color_enc.primaries_blue_xy[1] = 0;
+ color_enc.white_point = JXL_WHITE_POINT_E;
+ } else if (payload[0] == 11) {
+ // SMPTE RP 431-2 (2011)
+ color_enc.primaries = JXL_PRIMARIES_P3;
+ color_enc.white_point = JXL_WHITE_POINT_DCI;
+ } else if (payload[0] == 12) {
+ // SMPTE EG 432-1 (2010)
+ color_enc.primaries = JXL_PRIMARIES_P3;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else if (payload[0] == 22) {
+ color_enc.primaries = JXL_PRIMARIES_CUSTOM;
+ color_enc.primaries_red_xy[0] = 0.630;
+ color_enc.primaries_red_xy[1] = 0.340;
+ color_enc.primaries_green_xy[0] = 0.295;
+ color_enc.primaries_green_xy[1] = 0.605;
+ color_enc.primaries_blue_xy[0] = 0.155;
+ color_enc.primaries_blue_xy[1] = 0.077;
+ color_enc.white_point = JXL_WHITE_POINT_D65;
+ } else {
+ JXL_WARNING("Unsupported primaries specified in cICP chunk: %d",
+ static_cast<int>(payload[0]));
+ return false;
+ }
+
+ if (payload[1] == 1 || payload[1] == 6 || payload[1] == 14 ||
+ payload[1] == 15) {
+ // Rec. ITU-R BT.709-6
+ color_enc.transfer_function = JXL_TRANSFER_FUNCTION_709;
+ } else if (payload[1] == 4) {
+ // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
+ color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
+ color_enc.gamma = 1 / 2.2;
+ } else if (payload[1] == 5) {
+ // Rec. ITU-R BT.470-6 System B, G
+ color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
+ color_enc.gamma = 1 / 2.8;
+ } else if (payload[1] == 8 || payload[1] == 13 || payload[1] == 16 ||
+ payload[1] == 17 || payload[1] == 18) {
+ // These codes all match the corresponding JXL enum values
+ color_enc.transfer_function = static_cast<JxlTransferFunction>(payload[1]);
+ } else {
+ JXL_WARNING("Unsupported transfer function specified in cICP chunk: %d",
+ static_cast<int>(payload[1]));
+ return false;
+ }
+
+ if (payload[2] != 0) {
+ JXL_WARNING("Unsupported color space specified in cICP chunk: %d",
+ static_cast<int>(payload[2]));
+ return false;
+ }
+ if (payload[3] != 1) {
+ JXL_WARNING("Unsupported full-range flag specified in cICP chunk: %d",
+ static_cast<int>(payload[3]));
+ return false;
+ }
+ // cICP has no rendering intent, so use the default
+ color_enc.rendering_intent = JXL_RENDERING_INTENT_RELATIVE;
+ *color_encoding = color_enc;
+ return true;
+}
+
Status DecodeGAMA(const unsigned char* payload, const size_t payload_size,
JxlColorEncoding* color_encoding) {
if (payload_size != 4) return JXL_FAILURE("Wrong gAMA size");
@@ -129,6 +270,11 @@ class BlobsReaderPNG {
return false;
}
if (type == "exif") {
+ // Remove "Exif\0\0" prefix if present
+ if (bytes.size() >= sizeof kExifSignature &&
+ memcmp(bytes.data(), kExifSignature, sizeof kExifSignature) == 0) {
+ bytes.erase(bytes.begin(), bytes.begin() + sizeof kExifSignature);
+ }
if (!metadata->exif.empty()) {
JXL_WARNING("overwriting EXIF (%" PRIuS " bytes) with base16 (%" PRIuS
" bytes)",
@@ -136,9 +282,9 @@ class BlobsReaderPNG {
}
metadata->exif = std::move(bytes);
} else if (type == "iptc") {
- // TODO (jon): Deal with IPTC in some way
+ // TODO(jon): Deal with IPTC in some way
} else if (type == "8bim") {
- // TODO (jon): Deal with 8bim in some way
+ // TODO(jon): Deal with 8bim in some way
} else if (type == "xmp") {
if (!metadata->xmp.empty()) {
JXL_WARNING("overwriting XMP (%" PRIuS " bytes) with base16 (%" PRIuS
@@ -228,6 +374,10 @@ class BlobsReaderPNG {
// We parsed so far a \n, some number of non \n characters and are now
// pointing at a \n.
if (*(pos++) != '\n') return false;
+ // Skip leading spaces
+ while (pos < encoded_end && *pos == ' ') {
+ pos++;
+ }
uint32_t bytes_to_decode = 0;
JXL_RETURN_IF_ERROR(DecodeDecimal(&pos, encoded_end, &bytes_to_decode));
@@ -274,6 +424,7 @@ constexpr uint32_t kId_fcTL = 0x4C546366;
constexpr uint32_t kId_IDAT = 0x54414449;
constexpr uint32_t kId_fdAT = 0x54416466;
constexpr uint32_t kId_IEND = 0x444E4549;
+constexpr uint32_t kId_cICP = 0x50434963;
constexpr uint32_t kId_iCCP = 0x50434369;
constexpr uint32_t kId_sRGB = 0x42475273;
constexpr uint32_t kId_gAMA = 0x414D4167;
@@ -342,6 +493,12 @@ int processing_start(png_structp& png_ptr, png_infop& info_ptr, void* frame_ptr,
std::vector<std::vector<uint8_t>>& chunksInfo) {
unsigned char header[8] = {137, 80, 78, 71, 13, 10, 26, 10};
+ // Cleanup prior decoder, if any.
+ png_destroy_read_struct(&png_ptr, &info_ptr, 0);
+ // Just in case. Not all versions on libpng wipe-out the pointers.
+ png_ptr = nullptr;
+ info_ptr = nullptr;
+
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
info_ptr = png_create_info_struct(png_ptr);
if (!png_ptr || !info_ptr) return 1;
@@ -403,11 +560,20 @@ int processing_finish(png_structp png_ptr, png_infop info_ptr,
}
} // namespace
+#endif
+
+bool CanDecodeAPNG() {
+#if JPEGXL_ENABLE_APNG
+ return true;
+#else
+ return false;
+#endif
+}
Status DecodeImageAPNG(const Span<const uint8_t> bytes,
- const ColorHints& color_hints,
- const SizeConstraints& constraints,
- PackedPixelFile* ppf) {
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+#if JPEGXL_ENABLE_APNG
Reader r;
unsigned int id, j, w, h, w0, h0, x0, y0;
unsigned int delay_num, delay_den, dop, bop, rowbytes, imagesize;
@@ -419,6 +585,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
std::vector<std::vector<uint8_t>> chunksInfo;
bool isAnimated = false;
bool hasInfo = false;
+ bool seenFctl = false;
APNGFrame frameRaw = {};
uint32_t num_channels;
JxlPixelFormat format;
@@ -457,7 +624,8 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
ppf->frames.clear();
- bool have_color = false, have_srgb = false;
+ bool have_color = false;
+ bool have_cicp = false, have_iccp = false, have_srgb = false;
bool errorstate = true;
if (id == kId_IHDR && chunkIHDR.size() == 25) {
x0 = 0;
@@ -478,12 +646,14 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
+ ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_RELATIVE;
if (!processing_start(png_ptr, info_ptr, (void*)&frameRaw, hasInfo,
chunkIHDR, chunksInfo)) {
while (!r.Eof()) {
id = read_chunk(&r, &chunk);
if (!id) break;
+ seenFctl |= (id == kId_fcTL);
if (id == kId_acTL && !hasInfo && !isAnimated) {
isAnimated = true;
@@ -544,11 +714,16 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
}
} else if (id == kId_IDAT) {
// First IDAT chunk means we now have all header info
+ if (seenFctl) {
+ // `fcTL` chunk must appear after all `IDAT` chunks
+ return JXL_FAILURE("IDAT chunk after fcTL chunk");
+ }
hasInfo = true;
JXL_CHECK(w == png_get_image_width(png_ptr, info_ptr));
JXL_CHECK(h == png_get_image_height(png_ptr, info_ptr));
int colortype = png_get_color_type(png_ptr, info_ptr);
- ppf->info.bits_per_sample = png_get_bit_depth(png_ptr, info_ptr);
+ int png_bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+ ppf->info.bits_per_sample = png_bit_depth;
png_color_8p sigbits = NULL;
png_get_sBIT(png_ptr, info_ptr, &sigbits);
if (colortype & 1) {
@@ -559,8 +734,18 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
ppf->info.num_color_channels = 3;
ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB;
if (sigbits && sigbits->red == sigbits->green &&
- sigbits->green == sigbits->blue)
+ sigbits->green == sigbits->blue) {
ppf->info.bits_per_sample = sigbits->red;
+ } else if (sigbits) {
+ int maxbps = std::max(sigbits->red,
+ std::max(sigbits->green, sigbits->blue));
+ JXL_WARNING(
+ "sBIT chunk: bit depths for R, G, and B are not the same (%i "
+ "%i %i), while in JPEG XL they have to be the same. Setting "
+ "RGB bit depth to %i.",
+ sigbits->red, sigbits->green, sigbits->blue, maxbps);
+ ppf->info.bits_per_sample = maxbps;
+ }
} else {
ppf->info.num_color_channels = 1;
ppf->color_encoding.color_space = JXL_COLOR_SPACE_GRAY;
@@ -569,12 +754,12 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
if (colortype & 4 ||
png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
ppf->info.alpha_bits = ppf->info.bits_per_sample;
- if (sigbits) {
- if (sigbits->alpha &&
- sigbits->alpha != ppf->info.bits_per_sample) {
- return JXL_FAILURE("Unsupported alpha bit-depth");
- }
- ppf->info.alpha_bits = sigbits->alpha;
+ if (sigbits && sigbits->alpha != ppf->info.bits_per_sample) {
+ JXL_WARNING(
+ "sBIT chunk: bit depths for RGBA are inconsistent "
+ "(%i %i %i %i). Setting A bitdepth to %i.",
+ sigbits->red, sigbits->green, sigbits->blue, sigbits->alpha,
+ ppf->info.bits_per_sample);
}
} else {
ppf->info.alpha_bits = 0;
@@ -584,7 +769,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
: JXL_COLOR_SPACE_RGB);
ppf->info.xsize = w;
ppf->info.ysize = h;
- JXL_RETURN_IF_ERROR(VerifyDimensions(&constraints, w, h));
+ JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, w, h));
num_channels =
ppf->info.num_color_channels + (ppf->info.alpha_bits ? 1 : 0);
format = {
@@ -594,6 +779,9 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
/*endianness=*/JXL_BIG_ENDIAN,
/*align=*/0,
};
+ if (png_bit_depth > 8 && format.data_type == JXL_TYPE_UINT8) {
+ png_set_strip_16(png_ptr);
+ }
bytes_per_pixel =
num_channels * (format.data_type == JXL_TYPE_UINT16 ? 2 : 1);
rowbytes = w * bytes_per_pixel;
@@ -607,13 +795,26 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
break;
}
} else if (id == kId_fdAT && isAnimated) {
+ if (!hasInfo) {
+ return JXL_FAILURE("fDAT chunk before iDAT");
+ }
png_save_uint_32(chunk.data() + 4, chunk.size() - 16);
memcpy(chunk.data() + 8, "IDAT", 4);
if (processing_data(png_ptr, info_ptr, chunk.data() + 4,
chunk.size() - 4)) {
break;
}
- } else if (id == kId_iCCP) {
+ } else if (id == kId_cICP) {
+ // Color profile chunks: cICP has the highest priority, followed by
+ // iCCP and sRGB (which shouldn't co-exist, but if they do, we use
+ // iCCP), followed finally by gAMA and cHRM.
+ if (DecodeCICP(chunk.data() + 8, chunk.size() - 12,
+ &ppf->color_encoding)) {
+ have_cicp = true;
+ have_color = true;
+ ppf->icc.clear();
+ }
+ } else if (!have_cicp && id == kId_iCCP) {
if (processing_data(png_ptr, info_ptr, chunk.data(), chunk.size())) {
JXL_WARNING("Corrupt iCCP chunk");
break;
@@ -630,19 +831,20 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
if (ok && proflen) {
ppf->icc.assign(profile, profile + proflen);
have_color = true;
+ have_iccp = true;
} else {
// TODO(eustas): JXL_WARNING?
}
- } else if (id == kId_sRGB) {
+ } else if (!have_cicp && !have_iccp && id == kId_sRGB) {
JXL_RETURN_IF_ERROR(DecodeSRGB(chunk.data() + 8, chunk.size() - 12,
&ppf->color_encoding));
have_srgb = true;
have_color = true;
- } else if (id == kId_gAMA) {
+ } else if (!have_cicp && !have_srgb && !have_iccp && id == kId_gAMA) {
JXL_RETURN_IF_ERROR(DecodeGAMA(chunk.data() + 8, chunk.size() - 12,
&ppf->color_encoding));
have_color = true;
- } else if (id == kId_cHRM) {
+ } else if (!have_cicp && !have_srgb && !have_iccp && id == kId_cHRM) {
JXL_RETURN_IF_ERROR(DecodeCHRM(chunk.data() + 8, chunk.size() - 12,
&ppf->color_encoding));
have_color = true;
@@ -665,12 +867,6 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
}
}
- if (have_srgb) {
- ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
- ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
- ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
- ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL;
- }
JXL_RETURN_IF_ERROR(ApplyColorHints(
color_hints, have_color, ppf->info.num_color_channels == 1, ppf));
}
@@ -706,31 +902,29 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
size_t xsize = frame.data.xsize;
size_t ysize = frame.data.ysize;
if (previous_frame_should_be_cleared) {
- size_t xs = frame.data.xsize;
- size_t ys = frame.data.ysize;
size_t px0 = frames[i - 1].x0;
size_t py0 = frames[i - 1].y0;
size_t pxs = frames[i - 1].xsize;
size_t pys = frames[i - 1].ysize;
- if (px0 >= x0 && py0 >= y0 && px0 + pxs <= x0 + xs &&
- py0 + pys <= y0 + ys && frame.blend_op == BLEND_OP_SOURCE &&
+ if (px0 >= x0 && py0 >= y0 && px0 + pxs <= x0 + xsize &&
+ py0 + pys <= y0 + ysize && frame.blend_op == BLEND_OP_SOURCE &&
use_for_next_frame) {
// If the previous frame is entirely contained in the current frame and
// we are using BLEND_OP_SOURCE, nothing special needs to be done.
ppf->frames.emplace_back(std::move(frame.data));
- } else if (px0 == x0 && py0 == y0 && px0 + pxs == x0 + xs &&
- py0 + pys == y0 + ys && use_for_next_frame) {
+ } else if (px0 == x0 && py0 == y0 && px0 + pxs == x0 + xsize &&
+ py0 + pys == y0 + ysize && use_for_next_frame) {
// If the new frame has the same size as the old one, but we are
// blending, we can instead just not blend.
should_blend = false;
ppf->frames.emplace_back(std::move(frame.data));
- } else if (px0 <= x0 && py0 <= y0 && px0 + pxs >= x0 + xs &&
- py0 + pys >= y0 + ys && use_for_next_frame) {
+ } else if (px0 <= x0 && py0 <= y0 && px0 + pxs >= x0 + xsize &&
+ py0 + pys >= y0 + ysize && use_for_next_frame) {
// If the new frame is contained within the old frame, we can pad the
// new frame with zeros and not blend.
PackedImage new_data(pxs, pys, frame.data.format);
memset(new_data.pixels(), 0, new_data.pixels_size);
- for (size_t y = 0; y < ys; y++) {
+ for (size_t y = 0; y < ysize; y++) {
size_t bytes_per_pixel =
PackedImage::BitsPerChannel(new_data.format.data_type) *
new_data.format.num_channels / 8;
@@ -739,7 +933,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
bytes_per_pixel * (x0 - px0),
static_cast<const uint8_t*>(frame.data.pixels()) +
frame.data.stride * y,
- xs * bytes_per_pixel);
+ xsize * bytes_per_pixel);
}
x0 = px0;
@@ -749,19 +943,21 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
should_blend = false;
ppf->frames.emplace_back(std::move(new_data));
} else {
- // If all else fails, insert a dummy blank frame with kReplace.
+ // If all else fails, insert a placeholder blank frame with kReplace.
PackedImage blank(pxs, pys, frame.data.format);
memset(blank.pixels(), 0, blank.pixels_size);
ppf->frames.emplace_back(std::move(blank));
auto& pframe = ppf->frames.back();
pframe.frame_info.layer_info.crop_x0 = px0;
pframe.frame_info.layer_info.crop_y0 = py0;
- pframe.frame_info.layer_info.xsize = frame.xsize;
- pframe.frame_info.layer_info.ysize = frame.ysize;
+ pframe.frame_info.layer_info.xsize = pxs;
+ pframe.frame_info.layer_info.ysize = pys;
pframe.frame_info.duration = 0;
- pframe.frame_info.layer_info.have_crop = 0;
+ bool is_full_size = px0 == 0 && py0 == 0 && pxs == ppf->info.xsize &&
+ pys == ppf->info.ysize;
+ pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1;
pframe.frame_info.layer_info.blend_info.blendmode = JXL_BLEND_REPLACE;
- pframe.frame_info.layer_info.blend_info.source = 0;
+ pframe.frame_info.layer_info.blend_info.source = 1;
pframe.frame_info.layer_info.save_as_reference = 1;
ppf->frames.emplace_back(std::move(frame.data));
}
@@ -780,7 +976,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
bool is_full_size = x0 == 0 && y0 == 0 && xsize == ppf->info.xsize &&
ysize == ppf->info.ysize;
pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1;
- pframe.frame_info.layer_info.blend_info.source = should_blend ? 1 : 0;
+ pframe.frame_info.layer_info.blend_info.source = 1;
pframe.frame_info.layer_info.blend_info.alpha = 0;
pframe.frame_info.layer_info.save_as_reference = use_for_next_frame ? 1 : 0;
@@ -791,6 +987,9 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
ppf->frames.back().frame_info.is_last = true;
return true;
+#else
+ return false;
+#endif
}
} // namespace extras
diff --git a/lib/extras/dec/apng.h b/lib/extras/dec/apng.h
index a68f6f8..d91364b 100644
--- a/lib/extras/dec/apng.h
+++ b/lib/extras/dec/apng.h
@@ -13,18 +13,21 @@
#include "lib/extras/dec/color_hints.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h"
namespace jxl {
+
+struct SizeConstraints;
+
namespace extras {
+bool CanDecodeAPNG();
+
// Decodes `bytes` into `ppf`.
Status DecodeImageAPNG(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints,
- PackedPixelFile* ppf);
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
} // namespace extras
} // namespace jxl
diff --git a/lib/extras/dec/color_description.cc b/lib/extras/dec/color_description.cc
index 2325b50..54f6aa4 100644
--- a/lib/extras/dec/color_description.cc
+++ b/lib/extras/dec/color_description.cc
@@ -69,9 +69,9 @@ Status ParseEnum(const std::string& token, const EnumName<T>* enum_values,
}
return false;
}
-#define ARRAYSIZE(X) (sizeof(X) / sizeof((X)[0]))
+#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0]))
#define PARSE_ENUM(type, token, value) \
- ParseEnum<type>(token, k##type##Names, ARRAYSIZE(k##type##Names), value)
+ ParseEnum<type>(token, k##type##Names, ARRAY_SIZE(k##type##Names), value)
class Tokenizer {
public:
diff --git a/lib/extras/dec/color_description.h b/lib/extras/dec/color_description.h
index 989d591..23680ff 100644
--- a/lib/extras/dec/color_description.h
+++ b/lib/extras/dec/color_description.h
@@ -6,9 +6,10 @@
#ifndef LIB_EXTRAS_COLOR_DESCRIPTION_H_
#define LIB_EXTRAS_COLOR_DESCRIPTION_H_
+#include <jxl/color_encoding.h>
+
#include <string>
-#include "jxl/color_encoding.h"
#include "lib/jxl/base/status.h"
namespace jxl {
diff --git a/lib/extras/dec/color_description_test.cc b/lib/extras/dec/color_description_test.cc
index 8ae9e5d..e6e34f0 100644
--- a/lib/extras/dec/color_description_test.cc
+++ b/lib/extras/dec/color_description_test.cc
@@ -5,9 +5,9 @@
#include "lib/extras/dec/color_description.h"
-#include "gtest/gtest.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/test_utils.h"
+#include "lib/jxl/testing.h"
namespace jxl {
@@ -21,8 +21,7 @@ TEST(ColorDescriptionTest, RoundTripAll) {
JxlColorEncoding c_external = {};
EXPECT_TRUE(ParseDescription(description, &c_external));
ColorEncoding c_internal;
- EXPECT_TRUE(
- ConvertExternalToInternalColorEncoding(c_external, &c_internal));
+ EXPECT_TRUE(c_internal.FromExternal(c_external));
EXPECT_TRUE(c_original.SameColorEncoding(c_internal))
<< "Where c_original=" << c_original
<< " and c_internal=" << c_internal;
diff --git a/lib/extras/dec/color_hints.cc b/lib/extras/dec/color_hints.cc
index cf7d3e3..5c6d7b8 100644
--- a/lib/extras/dec/color_hints.cc
+++ b/lib/extras/dec/color_hints.cc
@@ -5,9 +5,12 @@
#include "lib/extras/dec/color_hints.h"
-#include "jxl/encode.h"
+#include <jxl/encode.h>
+
+#include <vector>
+
#include "lib/extras/dec/color_description.h"
-#include "lib/jxl/base/file_io.h"
+#include "lib/jxl/base/status.h"
namespace jxl {
namespace extras {
@@ -15,19 +18,15 @@ namespace extras {
Status ApplyColorHints(const ColorHints& color_hints,
const bool color_already_set, const bool is_gray,
PackedPixelFile* ppf) {
- if (color_already_set) {
- return color_hints.Foreach(
- [](const std::string& key, const std::string& /*value*/) {
- JXL_WARNING("Decoder ignoring %s hint", key.c_str());
- return true;
- });
- }
-
- bool got_color_space = false;
+ bool got_color_space = color_already_set;
JXL_RETURN_IF_ERROR(color_hints.Foreach(
- [is_gray, ppf, &got_color_space](const std::string& key,
- const std::string& value) -> Status {
+ [color_already_set, is_gray, ppf, &got_color_space](
+ const std::string& key, const std::string& value) -> Status {
+ if (color_already_set && (key == "color_space" || key == "icc")) {
+ JXL_WARNING("Decoder ignoring %s hint", key.c_str());
+ return true;
+ }
if (key == "color_space") {
JxlColorEncoding c_original_external;
if (!ParseDescription(value, &c_original_external)) {
@@ -41,9 +40,23 @@ Status ApplyColorHints(const ColorHints& color_hints,
}
got_color_space = true;
- } else if (key == "icc_pathname") {
- JXL_RETURN_IF_ERROR(ReadFile(value, &ppf->icc));
+ } else if (key == "icc") {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data());
+ std::vector<uint8_t> icc(data, data + value.size());
+ ppf->icc.swap(icc);
got_color_space = true;
+ } else if (key == "exif") {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data());
+ std::vector<uint8_t> blob(data, data + value.size());
+ ppf->metadata.exif.swap(blob);
+ } else if (key == "xmp") {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data());
+ std::vector<uint8_t> blob(data, data + value.size());
+ ppf->metadata.xmp.swap(blob);
+ } else if (key == "jumbf") {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(value.data());
+ std::vector<uint8_t> blob(data, data + value.size());
+ ppf->metadata.jumbf.swap(blob);
} else {
JXL_WARNING("Ignoring %s hint", key.c_str());
}
@@ -51,7 +64,6 @@ Status ApplyColorHints(const ColorHints& color_hints,
}));
if (!got_color_space) {
- JXL_WARNING("No color_space/icc_pathname given, assuming sRGB");
ppf->color_encoding.color_space =
is_gray ? JXL_COLOR_SPACE_GRAY : JXL_COLOR_SPACE_RGB;
ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
diff --git a/lib/extras/dec/color_hints.h b/lib/extras/dec/color_hints.h
index 9c7de88..036f203 100644
--- a/lib/extras/dec/color_hints.h
+++ b/lib/extras/dec/color_hints.h
@@ -10,6 +10,8 @@
// information into the file, and those that support it may not have it.
// To allow attaching color information to those file formats the caller can
// define these color hints.
+// Besides color space information, 'ColorHints' may also include other
+// additional information such as Exif, XMP and JUMBF metadata.
#include <stddef.h>
#include <stdint.h>
diff --git a/lib/extras/dec/decode.cc b/lib/extras/dec/decode.cc
index 8712e03..9149208 100644
--- a/lib/extras/dec/decode.cc
+++ b/lib/extras/dec/decode.cc
@@ -7,18 +7,11 @@
#include <locale>
-#if JPEGXL_ENABLE_APNG
#include "lib/extras/dec/apng.h"
-#endif
-#if JPEGXL_ENABLE_EXR
#include "lib/extras/dec/exr.h"
-#endif
-#if JPEGXL_ENABLE_GIF
#include "lib/extras/dec/gif.h"
-#endif
-#if JPEGXL_ENABLE_JPEG
#include "lib/extras/dec/jpg.h"
-#endif
+#include "lib/extras/dec/jxl.h"
#include "lib/extras/dec/pgx.h"
#include "lib/extras/dec/pnm.h"
@@ -29,59 +22,89 @@ namespace {
// Any valid encoding is larger (ensures codecs can read the first few bytes)
constexpr size_t kMinBytes = 9;
-} // namespace
-
-std::vector<Codec> AvailableCodecs() {
- std::vector<Codec> out;
-#if JPEGXL_ENABLE_APNG
- out.push_back(Codec::kPNG);
-#endif
-#if JPEGXL_ENABLE_EXR
- out.push_back(Codec::kEXR);
-#endif
-#if JPEGXL_ENABLE_GIF
- out.push_back(Codec::kGIF);
-#endif
-#if JPEGXL_ENABLE_JPEG
- out.push_back(Codec::kJPG);
-#endif
- out.push_back(Codec::kPGX);
- out.push_back(Codec::kPNM);
- return out;
-}
+void BasenameAndExtension(const std::string& path, std::string* filename,
+ std::string* extension) {
+ // Pattern: "png:name" or "png:-"
+ size_t pos = path.find_first_of(':');
+ if (pos != std::string::npos) {
+ *extension = "." + path.substr(0, pos);
+ *filename = path.substr(pos + 1);
+ //+ ((path.length() == pos + 2 && path.substr(pos + 1, 1) == "-") ? "" :
+ //*extension);
+ return;
+ }
-Codec CodecFromExtension(std::string extension,
- size_t* JXL_RESTRICT bits_per_sample) {
- std::transform(
- extension.begin(), extension.end(), extension.begin(),
- [](char c) { return std::tolower(c, std::locale::classic()); });
- if (extension == ".png") return Codec::kPNG;
+ // Pattern: "name.png"
+ pos = path.find_last_of('.');
+ if (pos != std::string::npos) {
+ *extension = path.substr(pos);
+ *filename = path;
+ return;
+ }
- if (extension == ".jpg") return Codec::kJPG;
- if (extension == ".jpeg") return Codec::kJPG;
+ // Extension not found
+ *filename = path;
+ *extension = "";
+}
- if (extension == ".pgx") return Codec::kPGX;
+} // namespace
- if (extension == ".pam") return Codec::kPNM;
- if (extension == ".pnm") return Codec::kPNM;
- if (extension == ".pgm") return Codec::kPNM;
- if (extension == ".ppm") return Codec::kPNM;
- if (extension == ".pfm") {
+Codec CodecFromPath(std::string path, size_t* JXL_RESTRICT bits_per_sample,
+ std::string* filename, std::string* extension) {
+ std::string base;
+ std::string ext;
+ BasenameAndExtension(path, &base, &ext);
+ if (filename) *filename = base;
+ if (extension) *extension = ext;
+
+ std::transform(ext.begin(), ext.end(), ext.begin(), [](char c) {
+ return std::tolower(c, std::locale::classic());
+ });
+ if (ext == ".png") return Codec::kPNG;
+
+ if (ext == ".jpg") return Codec::kJPG;
+ if (ext == ".jpeg") return Codec::kJPG;
+
+ if (ext == ".pgx") return Codec::kPGX;
+
+ if (ext == ".pam") return Codec::kPNM;
+ if (ext == ".pnm") return Codec::kPNM;
+ if (ext == ".pgm") return Codec::kPNM;
+ if (ext == ".ppm") return Codec::kPNM;
+ if (ext == ".pfm") {
if (bits_per_sample != nullptr) *bits_per_sample = 32;
return Codec::kPNM;
}
- if (extension == ".gif") return Codec::kGIF;
+ if (ext == ".gif") return Codec::kGIF;
- if (extension == ".exr") return Codec::kEXR;
+ if (ext == ".exr") return Codec::kEXR;
return Codec::kUnknown;
}
+bool CanDecode(Codec codec) {
+ switch (codec) {
+ case Codec::kEXR:
+ return CanDecodeEXR();
+ case Codec::kGIF:
+ return CanDecodeGIF();
+ case Codec::kJPG:
+ return CanDecodeJPG();
+ case Codec::kPNG:
+ return CanDecodeAPNG();
+ case Codec::kPNM:
+ case Codec::kPGX:
+ case Codec::kJXL:
+ return true;
+ default:
+ return false;
+ }
+}
+
Status DecodeBytes(const Span<const uint8_t> bytes,
- const ColorHints& color_hints,
- const SizeConstraints& constraints,
- extras::PackedPixelFile* ppf, Codec* orig_codec) {
+ const ColorHints& color_hints, extras::PackedPixelFile* ppf,
+ const SizeConstraints* constraints, Codec* orig_codec) {
if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes");
*ppf = extras::PackedPixelFile();
@@ -90,33 +113,42 @@ Status DecodeBytes(const Span<const uint8_t> bytes,
ppf->info.uses_original_profile = true;
ppf->info.orientation = JXL_ORIENT_IDENTITY;
- Codec codec;
-#if JPEGXL_ENABLE_APNG
- if (DecodeImageAPNG(bytes, color_hints, constraints, ppf)) {
- codec = Codec::kPNG;
- } else
-#endif
- if (DecodeImagePGX(bytes, color_hints, constraints, ppf)) {
- codec = Codec::kPGX;
- } else if (DecodeImagePNM(bytes, color_hints, constraints, ppf)) {
- codec = Codec::kPNM;
- }
-#if JPEGXL_ENABLE_GIF
- else if (DecodeImageGIF(bytes, color_hints, constraints, ppf)) {
- codec = Codec::kGIF;
- }
-#endif
-#if JPEGXL_ENABLE_JPEG
- else if (DecodeImageJPG(bytes, color_hints, constraints, ppf)) {
- codec = Codec::kJPG;
- }
-#endif
-#if JPEGXL_ENABLE_EXR
- else if (DecodeImageEXR(bytes, color_hints, constraints, ppf)) {
- codec = Codec::kEXR;
- }
-#endif
- else {
+ const auto choose_codec = [&]() -> Codec {
+ if (DecodeImageAPNG(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPNG;
+ }
+ if (DecodeImagePGX(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPGX;
+ }
+ if (DecodeImagePNM(bytes, color_hints, ppf, constraints)) {
+ return Codec::kPNM;
+ }
+ JXLDecompressParams dparams = {};
+ for (const uint32_t num_channels : {1, 2, 3, 4}) {
+ dparams.accepted_formats.push_back(
+ {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0});
+ }
+ size_t decoded_bytes;
+ if (DecodeImageJXL(bytes.data(), bytes.size(), dparams, &decoded_bytes,
+ ppf) &&
+ ApplyColorHints(color_hints, true, ppf->info.num_color_channels == 1,
+ ppf)) {
+ return Codec::kJXL;
+ }
+ if (DecodeImageGIF(bytes, color_hints, ppf, constraints)) {
+ return Codec::kGIF;
+ }
+ if (DecodeImageJPG(bytes, color_hints, ppf, constraints)) {
+ return Codec::kJPG;
+ }
+ if (DecodeImageEXR(bytes, color_hints, ppf, constraints)) {
+ return Codec::kEXR;
+ }
+ return Codec::kUnknown;
+ };
+
+ Codec codec = choose_codec();
+ if (codec == Codec::kUnknown) {
return JXL_FAILURE("Codecs failed to decode");
}
if (orig_codec) *orig_codec = codec;
diff --git a/lib/extras/dec/decode.h b/lib/extras/dec/decode.h
index 7f0ff70..0f864dd 100644
--- a/lib/extras/dec/decode.h
+++ b/lib/extras/dec/decode.h
@@ -17,34 +17,40 @@
#include "lib/extras/dec/color_hints.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h"
namespace jxl {
+
+struct SizeConstraints;
+
namespace extras {
-// Codecs supported by CodecInOut::Encode.
+// Codecs supported by DecodeBytes.
enum class Codec : uint32_t {
- kUnknown, // for CodecFromExtension
+ kUnknown, // for CodecFromPath
kPNG,
kPNM,
kPGX,
kJPG,
kGIF,
- kEXR
+ kEXR,
+ kJXL
};
-std::vector<Codec> AvailableCodecs();
+bool CanDecode(Codec codec);
// If and only if extension is ".pfm", *bits_per_sample is updated to 32 so
// that Encode() would encode to PFM instead of PPM.
-Codec CodecFromExtension(std::string extension,
- size_t* JXL_RESTRICT bits_per_sample = nullptr);
+Codec CodecFromPath(std::string path,
+ size_t* JXL_RESTRICT bits_per_sample = nullptr,
+ std::string* filename = nullptr,
+ std::string* extension = nullptr);
// Decodes "bytes" info *ppf.
// color_space_hint may specify the color space, otherwise, defaults to sRGB.
Status DecodeBytes(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints,
- extras::PackedPixelFile* ppf, Codec* orig_codec = nullptr);
+ extras::PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr,
+ Codec* orig_codec = nullptr);
} // namespace extras
} // namespace jxl
diff --git a/lib/extras/dec/exr.cc b/lib/extras/dec/exr.cc
index ddb6d53..821e0f4 100644
--- a/lib/extras/dec/exr.cc
+++ b/lib/extras/dec/exr.cc
@@ -5,20 +5,22 @@
#include "lib/extras/dec/exr.h"
+#if JPEGXL_ENABLE_EXR
#include <ImfChromaticitiesAttribute.h>
#include <ImfIO.h>
#include <ImfRgbaFile.h>
#include <ImfStandardAttributes.h>
+#endif
#include <vector>
namespace jxl {
namespace extras {
+#if JPEGXL_ENABLE_EXR
namespace {
namespace OpenEXR = OPENEXR_IMF_NAMESPACE;
-namespace Imath = IMATH_NAMESPACE;
// OpenEXR::Int64 is deprecated in favor of using uint64_t directly, but using
// uint64_t as recommended causes build failures with previous OpenEXR versions
@@ -60,10 +62,20 @@ class InMemoryIStream : public OpenEXR::IStream {
};
} // namespace
+#endif
+
+bool CanDecodeEXR() {
+#if JPEGXL_ENABLE_EXR
+ return true;
+#else
+ return false;
+#endif
+}
Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints,
- PackedPixelFile* ppf) {
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+#if JPEGXL_ENABLE_EXR
InMemoryIStream is(bytes);
#ifdef __EXCEPTIONS
@@ -71,7 +83,8 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
try {
input_ptr.reset(new OpenEXR::RgbaInputFile(is));
} catch (...) {
- return JXL_FAILURE("OpenEXR failed to parse input");
+ // silently return false if it is not an EXR file
+ return false;
}
OpenEXR::RgbaInputFile& input = *input_ptr;
#else
@@ -87,7 +100,7 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
const float intensity_target = OpenEXR::hasWhiteLuminance(input.header())
? OpenEXR::whiteLuminance(input.header())
- : kDefaultIntensityTarget;
+ : 0;
auto image_size = input.displayWindow().size();
// Size is computed as max - min, but both bounds are inclusive.
@@ -144,6 +157,7 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
std::min(input.dataWindow().max.x, input.displayWindow().max.x);
++exr_x) {
const int image_x = exr_x - input.displayWindow().min.x;
+ // TODO(eustas): UB: OpenEXR::Rgba is not TriviallyCopyable
memcpy(row + image_x * pixel_size,
input_row + (exr_x - input.dataWindow().min.x), pixel_size);
}
@@ -178,6 +192,9 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
}
ppf->info.intensity_target = intensity_target;
return true;
+#else
+ return false;
+#endif
}
} // namespace extras
diff --git a/lib/extras/dec/exr.h b/lib/extras/dec/exr.h
index 6af4e6b..0605cbb 100644
--- a/lib/extras/dec/exr.h
+++ b/lib/extras/dec/exr.h
@@ -11,17 +11,21 @@
#include "lib/extras/dec/color_hints.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h"
namespace jxl {
+
+struct SizeConstraints;
+
namespace extras {
+bool CanDecodeEXR();
+
// Decodes `bytes` into `ppf`. color_hints are ignored.
Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints, PackedPixelFile* ppf);
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
} // namespace extras
} // namespace jxl
diff --git a/lib/extras/dec/gif.cc b/lib/extras/dec/gif.cc
index 5167bf5..3d96394 100644
--- a/lib/extras/dec/gif.cc
+++ b/lib/extras/dec/gif.cc
@@ -5,20 +5,24 @@
#include "lib/extras/dec/gif.h"
+#if JPEGXL_ENABLE_GIF
#include <gif_lib.h>
+#endif
+#include <jxl/codestream_header.h>
#include <string.h>
#include <memory>
#include <utility>
#include <vector>
-#include "jxl/codestream_header.h"
+#include "lib/extras/size_constraints.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/sanitizers.h"
namespace jxl {
namespace extras {
+#if JPEGXL_ENABLE_GIF
namespace {
struct ReadState {
@@ -38,21 +42,6 @@ struct PackedRgb {
uint8_t r, g, b;
};
-// Gif does not support partial transparency, so this considers any nonzero
-// alpha channel value as opaque.
-bool AllOpaque(const PackedImage& color) {
- for (size_t y = 0; y < color.ysize; ++y) {
- const PackedRgba* const JXL_RESTRICT row =
- static_cast<const PackedRgba*>(color.pixels()) + y * color.xsize;
- for (size_t x = 0; x < color.xsize; ++x) {
- if (row[x].a == 0) {
- return false;
- }
- }
- }
- return true;
-}
-
void ensure_have_alpha(PackedFrame* frame) {
if (!frame->extra_channels.empty()) return;
const JxlPixelFormat alpha_format{
@@ -67,12 +56,21 @@ void ensure_have_alpha(PackedFrame* frame) {
std::fill_n(static_cast<uint8_t*>(frame->extra_channels[0].pixels()),
frame->color.xsize * frame->color.ysize, 255u);
}
-
} // namespace
+#endif
+
+bool CanDecodeGIF() {
+#if JPEGXL_ENABLE_GIF
+ return true;
+#else
+ return false;
+#endif
+}
Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints,
- PackedPixelFile* ppf) {
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
+#if JPEGXL_ENABLE_GIF
int error = GIF_OK;
ReadState state = {bytes};
const auto ReadFromSpan = [](GifFileType* const gif, GifByteType* const bytes,
@@ -111,20 +109,20 @@ Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
sizeof(*gif->SavedImages) * gif->ImageCount);
JXL_RETURN_IF_ERROR(
- VerifyDimensions<uint32_t>(&constraints, gif->SWidth, gif->SHeight));
+ VerifyDimensions<uint32_t>(constraints, gif->SWidth, gif->SHeight));
uint64_t total_pixel_count =
static_cast<uint64_t>(gif->SWidth) * gif->SHeight;
for (int i = 0; i < gif->ImageCount; ++i) {
const SavedImage& image = gif->SavedImages[i];
uint32_t w = image.ImageDesc.Width;
uint32_t h = image.ImageDesc.Height;
- JXL_RETURN_IF_ERROR(VerifyDimensions<uint32_t>(&constraints, w, h));
+ JXL_RETURN_IF_ERROR(VerifyDimensions<uint32_t>(constraints, w, h));
uint64_t pixel_count = static_cast<uint64_t>(w) * h;
if (total_pixel_count + pixel_count < total_pixel_count) {
return JXL_FAILURE("Image too big");
}
total_pixel_count += pixel_count;
- if (total_pixel_count > constraints.dec_max_pixels) {
+ if (constraints && (total_pixel_count > constraints->dec_max_pixels)) {
return JXL_FAILURE("Image too big");
}
}
@@ -408,6 +406,9 @@ Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
}
}
return true;
+#else
+ return false;
+#endif
}
} // namespace extras
diff --git a/lib/extras/dec/gif.h b/lib/extras/dec/gif.h
index b359517..4d5be86 100644
--- a/lib/extras/dec/gif.h
+++ b/lib/extras/dec/gif.h
@@ -15,14 +15,19 @@
#include "lib/jxl/base/data_parallel.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h"
namespace jxl {
+
+struct SizeConstraints;
+
namespace extras {
+bool CanDecodeGIF();
+
// Decodes `bytes` into `ppf`. color_hints are ignored.
Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints, PackedPixelFile* ppf);
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
} // namespace extras
} // namespace jxl
diff --git a/lib/extras/dec/jpegli.cc b/lib/extras/dec/jpegli.cc
new file mode 100644
index 0000000..ffa1b79
--- /dev/null
+++ b/lib/extras/dec/jpegli.cc
@@ -0,0 +1,271 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/dec/jpegli.h"
+
+#include <setjmp.h>
+#include <stdint.h>
+
+#include <algorithm>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+#include "lib/jpegli/decode.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/sanitizers.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
+ 0x66, 0x00, 0x00};
+constexpr int kExifMarker = JPEG_APP0 + 1;
+constexpr int kICCMarker = JPEG_APP0 + 2;
+
+static inline bool IsJPG(const std::vector<uint8_t>& bytes) {
+ if (bytes.size() < 2) return false;
+ if (bytes[0] != 0xFF || bytes[1] != 0xD8) return false;
+ return true;
+}
+
+bool MarkerIsExif(const jpeg_saved_marker_ptr marker) {
+ return marker->marker == kExifMarker &&
+ marker->data_length >= sizeof kExifSignature + 2 &&
+ std::equal(std::begin(kExifSignature), std::end(kExifSignature),
+ marker->data);
+}
+
+Status ReadICCProfile(jpeg_decompress_struct* const cinfo,
+ std::vector<uint8_t>* const icc) {
+ uint8_t* icc_data_ptr;
+ unsigned int icc_data_len;
+ if (jpegli_read_icc_profile(cinfo, &icc_data_ptr, &icc_data_len)) {
+ icc->assign(icc_data_ptr, icc_data_ptr + icc_data_len);
+ free(icc_data_ptr);
+ return true;
+ }
+ return false;
+}
+
+void ReadExif(jpeg_decompress_struct* const cinfo,
+ std::vector<uint8_t>* const exif) {
+ constexpr size_t kExifSignatureSize = sizeof kExifSignature;
+ for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr;
+ marker = marker->next) {
+ // marker is initialized by libjpeg, which we are not instrumenting with
+ // msan.
+ msan::UnpoisonMemory(marker, sizeof(*marker));
+ msan::UnpoisonMemory(marker->data, marker->data_length);
+ if (!MarkerIsExif(marker)) continue;
+ size_t marker_length = marker->data_length - kExifSignatureSize;
+ exif->resize(marker_length);
+ std::copy_n(marker->data + kExifSignatureSize, marker_length, exif->data());
+ return;
+ }
+}
+
+JpegliDataType ConvertDataType(JxlDataType type) {
+ switch (type) {
+ case JXL_TYPE_UINT8:
+ return JPEGLI_TYPE_UINT8;
+ case JXL_TYPE_UINT16:
+ return JPEGLI_TYPE_UINT16;
+ case JXL_TYPE_FLOAT:
+ return JPEGLI_TYPE_FLOAT;
+ default:
+ return JPEGLI_TYPE_UINT8;
+ }
+}
+
+JpegliEndianness ConvertEndianness(JxlEndianness type) {
+ switch (type) {
+ case JXL_NATIVE_ENDIAN:
+ return JPEGLI_NATIVE_ENDIAN;
+ case JXL_BIG_ENDIAN:
+ return JPEGLI_BIG_ENDIAN;
+ case JXL_LITTLE_ENDIAN:
+ return JPEGLI_LITTLE_ENDIAN;
+ default:
+ return JPEGLI_NATIVE_ENDIAN;
+ }
+}
+
+JxlColorSpace ConvertColorSpace(J_COLOR_SPACE colorspace) {
+ switch (colorspace) {
+ case JCS_GRAYSCALE:
+ return JXL_COLOR_SPACE_GRAY;
+ case JCS_RGB:
+ return JXL_COLOR_SPACE_RGB;
+ default:
+ return JXL_COLOR_SPACE_UNKNOWN;
+ }
+}
+
+void MyErrorExit(j_common_ptr cinfo) {
+ jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data);
+ (*cinfo->err->output_message)(cinfo);
+ jpegli_destroy_decompress(reinterpret_cast<j_decompress_ptr>(cinfo));
+ longjmp(*env, 1);
+}
+
+void MyOutputMessage(j_common_ptr cinfo) {
+#if JXL_DEBUG_WARNING == 1
+ char buf[JMSG_LENGTH_MAX + 1];
+ (*cinfo->err->format_message)(cinfo, buf);
+ buf[JMSG_LENGTH_MAX] = 0;
+ JXL_WARNING("%s", buf);
+#endif
+}
+
+void UnmapColors(uint8_t* row, size_t xsize, int components,
+ JSAMPARRAY colormap, size_t num_colors) {
+ JXL_CHECK(colormap != nullptr);
+ std::vector<uint8_t> tmp(xsize * components);
+ for (size_t x = 0; x < xsize; ++x) {
+ JXL_CHECK(row[x] < num_colors);
+ for (int c = 0; c < components; ++c) {
+ tmp[x * components + c] = colormap[c][row[x]];
+ }
+ }
+ memcpy(row, tmp.data(), tmp.size());
+}
+
+} // namespace
+
+Status DecodeJpeg(const std::vector<uint8_t>& compressed,
+ const JpegDecompressParams& dparams, ThreadPool* pool,
+ PackedPixelFile* ppf) {
+ // Don't do anything for non-JPEG files (no need to report an error)
+ if (!IsJPG(compressed)) return false;
+
+ // TODO(veluca): use JPEGData also for pixels?
+
+ // We need to declare all the non-trivial destructor local variables before
+ // the call to setjmp().
+ std::unique_ptr<JSAMPLE[]> row;
+
+ jpeg_decompress_struct cinfo;
+ const auto try_catch_block = [&]() -> bool {
+ // Setup error handling in jpeg library so we can deal with broken jpegs in
+ // the fuzzer.
+ jpeg_error_mgr jerr;
+ jmp_buf env;
+ cinfo.err = jpegli_std_error(&jerr);
+ jerr.error_exit = &MyErrorExit;
+ jerr.output_message = &MyOutputMessage;
+ if (setjmp(env)) {
+ return false;
+ }
+ cinfo.client_data = static_cast<void*>(&env);
+
+ jpegli_create_decompress(&cinfo);
+ jpegli_mem_src(&cinfo,
+ reinterpret_cast<const unsigned char*>(compressed.data()),
+ compressed.size());
+ jpegli_save_markers(&cinfo, kICCMarker, 0xFFFF);
+ jpegli_save_markers(&cinfo, kExifMarker, 0xFFFF);
+ const auto failure = [&cinfo](const char* str) -> Status {
+ jpegli_abort_decompress(&cinfo);
+ jpegli_destroy_decompress(&cinfo);
+ return JXL_FAILURE("%s", str);
+ };
+ jpegli_read_header(&cinfo, TRUE);
+ // Might cause CPU-zip bomb.
+ if (cinfo.arith_code) {
+ return failure("arithmetic code JPEGs are not supported");
+ }
+ int nbcomp = cinfo.num_components;
+ if (nbcomp != 1 && nbcomp != 3) {
+ return failure("unsupported number of components in JPEG");
+ }
+ if (dparams.force_rgb) {
+ cinfo.out_color_space = JCS_RGB;
+ } else if (dparams.force_grayscale) {
+ cinfo.out_color_space = JCS_GRAYSCALE;
+ }
+ if (!ReadICCProfile(&cinfo, &ppf->icc)) {
+ ppf->icc.clear();
+ // Default to SRGB
+ ppf->color_encoding.color_space =
+ ConvertColorSpace(cinfo.out_color_space);
+ ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
+ ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
+ ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
+ ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL;
+ }
+ ReadExif(&cinfo, &ppf->metadata.exif);
+
+ ppf->info.xsize = cinfo.image_width;
+ ppf->info.ysize = cinfo.image_height;
+ if (dparams.output_data_type == JXL_TYPE_UINT8) {
+ ppf->info.bits_per_sample = 8;
+ ppf->info.exponent_bits_per_sample = 0;
+ } else if (dparams.output_data_type == JXL_TYPE_UINT16) {
+ ppf->info.bits_per_sample = 16;
+ ppf->info.exponent_bits_per_sample = 0;
+ } else if (dparams.output_data_type == JXL_TYPE_FLOAT) {
+ ppf->info.bits_per_sample = 32;
+ ppf->info.exponent_bits_per_sample = 8;
+ } else {
+ return failure("unsupported data type");
+ }
+ ppf->info.uses_original_profile = true;
+
+ // No alpha in JPG
+ ppf->info.alpha_bits = 0;
+ ppf->info.alpha_exponent_bits = 0;
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+
+ jpegli_set_output_format(&cinfo, ConvertDataType(dparams.output_data_type),
+ ConvertEndianness(dparams.output_endianness));
+
+ if (dparams.num_colors > 0) {
+ cinfo.quantize_colors = TRUE;
+ cinfo.desired_number_of_colors = dparams.num_colors;
+ cinfo.two_pass_quantize = dparams.two_pass_quant;
+ cinfo.dither_mode = (J_DITHER_MODE)dparams.dither_mode;
+ }
+
+ jpegli_start_decompress(&cinfo);
+
+ ppf->info.num_color_channels = cinfo.out_color_components;
+ const JxlPixelFormat format{
+ /*num_channels=*/static_cast<uint32_t>(cinfo.out_color_components),
+ dparams.output_data_type,
+ dparams.output_endianness,
+ /*align=*/0,
+ };
+ ppf->frames.clear();
+ // Allocates the frame buffer.
+ ppf->frames.emplace_back(cinfo.image_width, cinfo.image_height, format);
+ const auto& frame = ppf->frames.back();
+ JXL_ASSERT(sizeof(JSAMPLE) * cinfo.out_color_components *
+ cinfo.image_width <=
+ frame.color.stride);
+
+ for (size_t y = 0; y < cinfo.image_height; ++y) {
+ JSAMPROW rows[] = {reinterpret_cast<JSAMPLE*>(
+ static_cast<uint8_t*>(frame.color.pixels()) +
+ frame.color.stride * y)};
+ jpegli_read_scanlines(&cinfo, rows, 1);
+ if (dparams.num_colors > 0) {
+ UnmapColors(rows[0], cinfo.output_width, cinfo.out_color_components,
+ cinfo.colormap, cinfo.actual_number_of_colors);
+ }
+ }
+
+ jpegli_finish_decompress(&cinfo);
+ return true;
+ };
+ bool success = try_catch_block();
+ jpegli_destroy_decompress(&cinfo);
+ return success;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/lib/extras/dec/jpegli.h b/lib/extras/dec/jpegli.h
new file mode 100644
index 0000000..574df54
--- /dev/null
+++ b/lib/extras/dec/jpegli.h
@@ -0,0 +1,41 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_DEC_JPEGLI_H_
+#define LIB_EXTRAS_DEC_JPEGLI_H_
+
+// Decodes JPG pixels and metadata in memory using the libjpegli library.
+
+#include <jxl/types.h>
+#include <stdint.h>
+
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+struct JpegDecompressParams {
+ JxlDataType output_data_type = JXL_TYPE_UINT8;
+ JxlEndianness output_endianness = JXL_NATIVE_ENDIAN;
+ bool force_rgb = false;
+ bool force_grayscale = false;
+ int num_colors = 0;
+ bool two_pass_quant = true;
+ // 0 = none, 1 = ordered, 2 = Floyd-Steinberg
+ int dither_mode = 2;
+};
+
+Status DecodeJpeg(const std::vector<uint8_t>& compressed,
+ const JpegDecompressParams& dparams, ThreadPool* pool,
+ PackedPixelFile* ppf);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_DEC_JPEGLI_H_
diff --git a/lib/extras/dec/jpg.cc b/lib/extras/dec/jpg.cc
index 6b92f4a..3c8a4bc 100644
--- a/lib/extras/dec/jpg.cc
+++ b/lib/extras/dec/jpg.cc
@@ -5,8 +5,10 @@
#include "lib/extras/dec/jpg.h"
+#if JPEGXL_ENABLE_JPEG
#include <jpeglib.h>
#include <setjmp.h>
+#endif
#include <stdint.h>
#include <algorithm>
@@ -14,12 +16,14 @@
#include <utility>
#include <vector>
+#include "lib/extras/size_constraints.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/sanitizers.h"
namespace jxl {
namespace extras {
+#if JPEGXL_ENABLE_JPEG
namespace {
constexpr unsigned char kICCSignature[12] = {
@@ -160,12 +164,35 @@ void MyOutputMessage(j_common_ptr cinfo) {
#endif
}
+void UnmapColors(uint8_t* row, size_t xsize, int components,
+ JSAMPARRAY colormap, size_t num_colors) {
+ JXL_CHECK(colormap != nullptr);
+ std::vector<uint8_t> tmp(xsize * components);
+ for (size_t x = 0; x < xsize; ++x) {
+ JXL_CHECK(row[x] < num_colors);
+ for (int c = 0; c < components; ++c) {
+ tmp[x * components + c] = colormap[c][row[x]];
+ }
+ }
+ memcpy(row, tmp.data(), tmp.size());
+}
+
} // namespace
+#endif
+
+bool CanDecodeJPG() {
+#if JPEGXL_ENABLE_JPEG
+ return true;
+#else
+ return false;
+#endif
+}
Status DecodeImageJPG(const Span<const uint8_t> bytes,
- const ColorHints& color_hints,
- const SizeConstraints& constraints,
- PackedPixelFile* ppf) {
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints,
+ const JPGDecompressParams* dparams) {
+#if JPEGXL_ENABLE_JPEG
// Don't do anything for non-JPEG files (no need to report an error)
if (!IsJPG(bytes)) return false;
@@ -176,10 +203,7 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes,
std::unique_ptr<JSAMPLE[]> row;
const auto try_catch_block = [&]() -> bool {
- jpeg_decompress_struct cinfo;
- // cinfo is initialized by libjpeg, which we are not instrumenting with
- // msan, therefore we need to initialize cinfo here.
- msan::UnpoisonMemory(&cinfo, sizeof(cinfo));
+ jpeg_decompress_struct cinfo = {};
// Setup error handling in jpeg library so we can deal with broken jpegs in
// the fuzzer.
jpeg_error_mgr jerr;
@@ -207,8 +231,7 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes,
if (read_header_result == JPEG_SUSPENDED) {
return failure("truncated JPEG input");
}
- if (!VerifyDimensions(&constraints, cinfo.image_width,
- cinfo.image_height)) {
+ if (!VerifyDimensions(constraints, cinfo.image_width, cinfo.image_height)) {
return failure("image too big");
}
// Might cause CPU-zip bomb.
@@ -252,12 +275,21 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes,
ppf->info.num_color_channels = nbcomp;
ppf->info.orientation = JXL_ORIENT_IDENTITY;
+ if (dparams && dparams->num_colors > 0) {
+ cinfo.quantize_colors = TRUE;
+ cinfo.desired_number_of_colors = dparams->num_colors;
+ cinfo.two_pass_quantize = dparams->two_pass_quant;
+ cinfo.dither_mode = (J_DITHER_MODE)dparams->dither_mode;
+ }
+
jpeg_start_decompress(&cinfo);
- JXL_ASSERT(cinfo.output_components == nbcomp);
+ JXL_ASSERT(cinfo.out_color_components == nbcomp);
+ JxlDataType data_type =
+ ppf->info.bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16;
const JxlPixelFormat format{
/*num_channels=*/static_cast<uint32_t>(nbcomp),
- /*data_type=*/BITS_IN_JSAMPLE == 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16,
+ data_type,
/*endianness=*/JXL_NATIVE_ENDIAN,
/*align=*/0,
};
@@ -265,9 +297,19 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes,
// Allocates the frame buffer.
ppf->frames.emplace_back(cinfo.image_width, cinfo.image_height, format);
const auto& frame = ppf->frames.back();
- JXL_ASSERT(sizeof(JSAMPLE) * cinfo.output_components * cinfo.image_width <=
+ JXL_ASSERT(sizeof(JSAMPLE) * cinfo.out_color_components *
+ cinfo.image_width <=
frame.color.stride);
+ if (cinfo.quantize_colors) {
+ jxl::msan::UnpoisonMemory(cinfo.colormap, cinfo.out_color_components *
+ sizeof(cinfo.colormap[0]));
+ for (int c = 0; c < cinfo.out_color_components; ++c) {
+ jxl::msan::UnpoisonMemory(
+ cinfo.colormap[c],
+ cinfo.actual_number_of_colors * sizeof(cinfo.colormap[c][0]));
+ }
+ }
for (size_t y = 0; y < cinfo.image_height; ++y) {
JSAMPROW rows[] = {reinterpret_cast<JSAMPLE*>(
static_cast<uint8_t*>(frame.color.pixels()) +
@@ -275,6 +317,10 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes,
jpeg_read_scanlines(&cinfo, rows, 1);
msan::UnpoisonMemory(rows[0], sizeof(JSAMPLE) * cinfo.output_components *
cinfo.image_width);
+ if (dparams && dparams->num_colors > 0) {
+ UnmapColors(rows[0], cinfo.output_width, cinfo.out_color_components,
+ cinfo.colormap, cinfo.actual_number_of_colors);
+ }
}
jpeg_finish_decompress(&cinfo);
@@ -283,6 +329,9 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes,
};
return try_catch_block();
+#else
+ return false;
+#endif
}
} // namespace extras
diff --git a/lib/extras/dec/jpg.h b/lib/extras/dec/jpg.h
index 66b3452..6e7b2f7 100644
--- a/lib/extras/dec/jpg.h
+++ b/lib/extras/dec/jpg.h
@@ -13,19 +13,31 @@
#include "lib/extras/codec.h"
#include "lib/extras/dec/color_hints.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h"
namespace jxl {
+
+struct SizeConstraints;
+
namespace extras {
+bool CanDecodeJPG();
+
+struct JPGDecompressParams {
+ int num_colors = 0;
+ bool two_pass_quant = false;
+ // 0 = none, 1 = ordered, 2 = Floyd-Steinberg
+ int dither_mode = 0;
+};
+
// Decodes `bytes` into `ppf`. color_hints are ignored.
// `elapsed_deinterleave`, if non-null, will be set to the time (in seconds)
// that it took to deinterleave the raw JSAMPLEs to planar floats.
Status DecodeImageJPG(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints, PackedPixelFile* ppf);
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr,
+ const JPGDecompressParams* dparams = nullptr);
} // namespace extras
} // namespace jxl
diff --git a/lib/extras/dec/jxl.cc b/lib/extras/dec/jxl.cc
index 0e10356..1f3a3ff 100644
--- a/lib/extras/dec/jxl.cc
+++ b/lib/extras/dec/jxl.cc
@@ -5,12 +5,15 @@
#include "lib/extras/dec/jxl.h"
-#include "jxl/decode.h"
-#include "jxl/decode_cxx.h"
-#include "jxl/types.h"
+#include <jxl/cms.h>
+#include <jxl/decode.h>
+#include <jxl/decode_cxx.h>
+#include <jxl/types.h>
+
+#include "lib/extras/common.h"
#include "lib/extras/dec/color_description.h"
-#include "lib/extras/enc/encode.h"
#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/exif.h"
namespace jxl {
namespace extras {
@@ -68,11 +71,48 @@ struct BoxProcessor {
}
};
+void SetBitDepthFromDataType(JxlDataType data_type, uint32_t* bits_per_sample,
+ uint32_t* exponent_bits_per_sample) {
+ switch (data_type) {
+ case JXL_TYPE_UINT8:
+ *bits_per_sample = 8;
+ *exponent_bits_per_sample = 0;
+ break;
+ case JXL_TYPE_UINT16:
+ *bits_per_sample = 16;
+ *exponent_bits_per_sample = 0;
+ break;
+ case JXL_TYPE_FLOAT16:
+ *bits_per_sample = 16;
+ *exponent_bits_per_sample = 5;
+ break;
+ case JXL_TYPE_FLOAT:
+ *bits_per_sample = 32;
+ *exponent_bits_per_sample = 8;
+ break;
+ }
+}
+
+template <typename T>
+void UpdateBitDepth(JxlBitDepth bit_depth, JxlDataType data_type, T* info) {
+ if (bit_depth.type == JXL_BIT_DEPTH_FROM_PIXEL_FORMAT) {
+ SetBitDepthFromDataType(data_type, &info->bits_per_sample,
+ &info->exponent_bits_per_sample);
+ } else if (bit_depth.type == JXL_BIT_DEPTH_CUSTOM) {
+ info->bits_per_sample = bit_depth.bits_per_sample;
+ info->exponent_bits_per_sample = bit_depth.exponent_bits_per_sample;
+ }
+}
+
} // namespace
bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
const JXLDecompressParams& dparams, size_t* decoded_bytes,
PackedPixelFile* ppf, std::vector<uint8_t>* jpeg_bytes) {
+ JxlSignature sig = JxlSignatureCheck(bytes, bytes_size);
+ // silently return false if this is not a JXL file
+ if (sig == JXL_SIG_INVALID) return false;
+
auto decoder = JxlDecoderMake(/*memory_manager=*/nullptr);
JxlDecoder* dec = decoder.get();
ppf->frames.clear();
@@ -86,12 +126,7 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
JxlPixelFormat format;
std::vector<JxlPixelFormat> accepted_formats = dparams.accepted_formats;
- if (accepted_formats.empty()) {
- for (const uint32_t num_channels : {1, 2, 3, 4}) {
- accepted_formats.push_back(
- {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0});
- }
- }
+
JxlColorEncoding color_encoding;
size_t num_color_channels = 0;
if (!dparams.color_space.empty()) {
@@ -107,7 +142,9 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
bool can_reconstruct_jpeg = false;
std::vector<uint8_t> jpeg_data_chunk;
if (jpeg_bytes != nullptr) {
- jpeg_data_chunk.resize(16384);
+ // This bound is very likely to be enough to hold the entire
+ // reconstructed JPEG, to avoid having to do expensive retries.
+ jpeg_data_chunk.resize(bytes_size * 3 / 2 + 1024);
jpeg_bytes->resize(0);
}
@@ -128,6 +165,10 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
} else {
events |= (JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_PREVIEW_IMAGE |
JXL_DEC_BOX);
+ if (accepted_formats.empty()) {
+ // decoding just the metadata, not the pixel data
+ events ^= (JXL_DEC_FULL_IMAGE | JXL_DEC_PREVIEW_IMAGE);
+ }
}
if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec, events)) {
fprintf(stderr, "JxlDecoderSubscribeEvents failed\n");
@@ -165,7 +206,7 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
return false;
}
uint32_t progression_index = 0;
- bool codestream_done = false;
+ bool codestream_done = accepted_formats.empty();
BoxProcessor boxes(dec);
for (;;) {
JxlDecoderStatus status = JxlDecoderProcessInput(dec);
@@ -185,8 +226,12 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
}
break;
}
+ size_t released_size = JxlDecoderReleaseInput(dec);
fprintf(stderr,
- "Input file is truncated and allow_partial_input was disabled.");
+ "Input file is truncated (total bytes: %" PRIuS
+ ", processed bytes: %" PRIuS
+ ") and --allow_partial_files is not present.\n",
+ bytes_size, bytes_size - released_size);
return false;
} else if (status == JXL_DEC_BOX) {
boxes.FinalizeOutput();
@@ -240,11 +285,16 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
fprintf(stderr, "JxlDecoderGetBasicInfo failed\n");
return false;
}
+ if (accepted_formats.empty()) continue;
if (num_color_channels != 0) {
// Mark the change in number of color channels due to the requested
// color space.
ppf->info.num_color_channels = num_color_channels;
}
+ if (dparams.output_bitdepth.type == JXL_BIT_DEPTH_CUSTOM) {
+ // Select format based on custom bits per sample.
+ ppf->info.bits_per_sample = dparams.output_bitdepth.bits_per_sample;
+ }
// Select format according to accepted formats.
if (!jxl::extras::SelectFormat(accepted_formats, ppf->info, &format)) {
fprintf(stderr, "SelectFormat failed\n");
@@ -254,9 +304,11 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
if (!have_alpha) {
// Mark in the basic info that alpha channel was dropped.
ppf->info.alpha_bits = 0;
- } else if (dparams.unpremultiply_alpha) {
- // Mark in the basic info that alpha was unpremultiplied.
- ppf->info.alpha_premultiplied = false;
+ } else {
+ if (dparams.unpremultiply_alpha) {
+ // Mark in the basic info that alpha was unpremultiplied.
+ ppf->info.alpha_premultiplied = false;
+ }
}
bool alpha_found = false;
for (uint32_t i = 0; i < ppf->info.num_extra_channels; ++i) {
@@ -287,6 +339,7 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
"Warning: --color_space ignored because the image is "
"not XYB encoded.\n");
} else {
+ JxlDecoderSetCms(dec, *JxlGetDefaultCms());
if (JXL_DEC_SUCCESS !=
JxlDecoderSetPreferredColorProfile(dec, &color_encoding)) {
fprintf(stderr, "Failed to set color space.\n");
@@ -296,34 +349,35 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
}
size_t icc_size = 0;
JxlColorProfileTarget target = JXL_COLOR_PROFILE_TARGET_DATA;
- if (JXL_DEC_SUCCESS !=
- JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) {
- fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
- }
- if (icc_size != 0) {
- ppf->icc.resize(icc_size);
+ ppf->color_encoding.color_space = JXL_COLOR_SPACE_UNKNOWN;
+ if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsEncodedProfile(
+ dec, target, &ppf->color_encoding) ||
+ dparams.need_icc) {
+ // only get ICC if it is not an Enum color encoding
if (JXL_DEC_SUCCESS !=
- JxlDecoderGetColorAsICCProfile(dec, nullptr, target,
- ppf->icc.data(), icc_size)) {
- fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
- return false;
+ JxlDecoderGetICCProfileSize(dec, target, &icc_size)) {
+ fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
+ }
+ if (icc_size != 0) {
+ ppf->icc.resize(icc_size);
+ if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile(
+ dec, target, ppf->icc.data(), icc_size)) {
+ fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
+ return false;
+ }
}
- }
- if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsEncodedProfile(
- dec, nullptr, target, &ppf->color_encoding)) {
- ppf->color_encoding.color_space = JXL_COLOR_SPACE_UNKNOWN;
}
icc_size = 0;
target = JXL_COLOR_PROFILE_TARGET_ORIGINAL;
if (JXL_DEC_SUCCESS !=
- JxlDecoderGetICCProfileSize(dec, nullptr, target, &icc_size)) {
+ JxlDecoderGetICCProfileSize(dec, target, &icc_size)) {
fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
}
if (icc_size != 0) {
ppf->orig_icc.resize(icc_size);
if (JXL_DEC_SUCCESS !=
- JxlDecoderGetColorAsICCProfile(dec, nullptr, target,
- ppf->orig_icc.data(), icc_size)) {
+ JxlDecoderGetColorAsICCProfile(dec, target, ppf->orig_icc.data(),
+ icc_size)) {
fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
return false;
}
@@ -421,9 +475,21 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
return false;
}
}
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetImageOutBitDepth(dec, &dparams.output_bitdepth)) {
+ fprintf(stderr, "JxlDecoderSetImageOutBitDepth failed\n");
+ return false;
+ }
+ UpdateBitDepth(dparams.output_bitdepth, format.data_type, &ppf->info);
+ bool have_alpha = (format.num_channels == 2 || format.num_channels == 4);
+ if (have_alpha) {
+ // Interleaved alpha channels has the same bit depth as color channels.
+ ppf->info.alpha_bits = ppf->info.bits_per_sample;
+ ppf->info.alpha_exponent_bits = ppf->info.exponent_bits_per_sample;
+ }
JxlPixelFormat ec_format = format;
ec_format.num_channels = 1;
- for (const auto& eci : ppf->extra_channels_info) {
+ for (auto& eci : ppf->extra_channels_info) {
frame.extra_channels.emplace_back(jxl::extras::PackedImage(
ppf->info.xsize, ppf->info.ysize, ec_format));
auto& ec = frame.extra_channels.back();
@@ -446,6 +512,8 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
fprintf(stderr, "JxlDecoderSetExtraChannelBuffer failed\n");
return false;
}
+ UpdateBitDepth(dparams.output_bitdepth, ec_format.data_type,
+ &eci.ec_info);
}
} else if (status == JXL_DEC_SUCCESS) {
// Decoding finished successfully.
@@ -463,6 +531,28 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
}
}
boxes.FinalizeOutput();
+ if (!ppf->metadata.exif.empty()) {
+ // Verify that Exif box has a valid TIFF header at the specified offset.
+ // Discard bytes preceding the header.
+ if (ppf->metadata.exif.size() >= 4) {
+ uint32_t offset = LoadBE32(ppf->metadata.exif.data());
+ if (offset <= ppf->metadata.exif.size() - 8) {
+ std::vector<uint8_t> exif(ppf->metadata.exif.begin() + 4 + offset,
+ ppf->metadata.exif.end());
+ bool bigendian;
+ if (IsExif(exif, &bigendian)) {
+ ppf->metadata.exif = std::move(exif);
+ } else {
+ fprintf(stderr, "Warning: invalid TIFF header in Exif\n");
+ }
+ } else {
+ fprintf(stderr, "Warning: invalid Exif offset: %" PRIu32 "\n", offset);
+ }
+ } else {
+ fprintf(stderr, "Warning: invalid Exif length: %" PRIuS "\n",
+ ppf->metadata.exif.size());
+ }
+ }
if (jpeg_bytes != nullptr) {
if (!can_reconstruct_jpeg) return false;
size_t used_jpeg_output =
diff --git a/lib/extras/dec/jxl.h b/lib/extras/dec/jxl.h
index c462fa4..cbada1f 100644
--- a/lib/extras/dec/jxl.h
+++ b/lib/extras/dec/jxl.h
@@ -8,14 +8,14 @@
// Decodes JPEG XL images in memory.
+#include <jxl/parallel_runner.h>
+#include <jxl/types.h>
#include <stdint.h>
#include <limits>
#include <string>
#include <vector>
-#include "jxl/parallel_runner.h"
-#include "jxl/types.h"
#include "lib/extras/packed_image.h"
namespace jxl {
@@ -41,6 +41,10 @@ struct JXLDecompressParams {
// Whether truncated input should be treated as an error.
bool allow_partial_input = false;
+ // Set to true if an ICC profile has to be synthesized for Enum color
+ // encodings
+ bool need_icc = false;
+
// How many passes to decode at most. By default, decode everything.
uint32_t max_passes = std::numeric_limits<uint32_t>::max();
@@ -53,6 +57,9 @@ struct JXLDecompressParams {
bool use_image_callback = true;
// Whether to unpremultiply colors for associated alpha channels.
bool unpremultiply_alpha = false;
+
+ // Controls the effective bit depth of the output pixels.
+ JxlBitDepth output_bitdepth = {JXL_BIT_DEPTH_FROM_PIXEL_FORMAT, 0, 0};
};
bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
diff --git a/lib/extras/dec/pgx.cc b/lib/extras/dec/pgx.cc
index 1417348..a99eb0f 100644
--- a/lib/extras/dec/pgx.cc
+++ b/lib/extras/dec/pgx.cc
@@ -7,6 +7,7 @@
#include <string.h>
+#include "lib/extras/size_constraints.h"
#include "lib/jxl/base/bits.h"
#include "lib/jxl/base/compiler_specific.h"
@@ -145,15 +146,14 @@ class Parser {
} // namespace
Status DecodeImagePGX(const Span<const uint8_t> bytes,
- const ColorHints& color_hints,
- const SizeConstraints& constraints,
- PackedPixelFile* ppf) {
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
Parser parser(bytes);
HeaderPGX header = {};
const uint8_t* pos;
if (!parser.ParseHeader(&header, &pos)) return false;
JXL_RETURN_IF_ERROR(
- VerifyDimensions(&constraints, header.xsize, header.ysize));
+ VerifyDimensions(constraints, header.xsize, header.ysize));
if (header.bits_per_sample == 0 || header.bits_per_sample > 32) {
return JXL_FAILURE("PGX: bits_per_sample invalid");
}
diff --git a/lib/extras/dec/pgx.h b/lib/extras/dec/pgx.h
index 38aedf5..ce852e6 100644
--- a/lib/extras/dec/pgx.h
+++ b/lib/extras/dec/pgx.h
@@ -14,17 +14,19 @@
#include "lib/extras/dec/color_hints.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h"
namespace jxl {
+
+struct SizeConstraints;
+
namespace extras {
// Decodes `bytes` into `ppf`.
Status DecodeImagePGX(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints, PackedPixelFile* ppf);
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
} // namespace extras
} // namespace jxl
diff --git a/lib/extras/dec/pgx_test.cc b/lib/extras/dec/pgx_test.cc
index 41e6bf8..5dbc314 100644
--- a/lib/extras/dec/pgx_test.cc
+++ b/lib/extras/dec/pgx_test.cc
@@ -5,16 +5,18 @@
#include "lib/extras/dec/pgx.h"
-#include "gtest/gtest.h"
+#include <cstring>
+
#include "lib/extras/packed_image_convert.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/testing.h"
namespace jxl {
namespace extras {
namespace {
Span<const uint8_t> MakeSpan(const char* str) {
- return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str),
- strlen(str));
+ return Bytes(reinterpret_cast<const uint8_t*>(str), strlen(str));
}
TEST(CodecPGXTest, Test8bits) {
@@ -23,8 +25,7 @@ TEST(CodecPGXTest, Test8bits) {
PackedPixelFile ppf;
ThreadPool* pool = nullptr;
- EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(),
- SizeConstraints(), &ppf));
+ EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), &ppf));
CodecInOut io;
EXPECT_TRUE(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io));
@@ -51,8 +52,7 @@ TEST(CodecPGXTest, Test16bits) {
PackedPixelFile ppf;
ThreadPool* pool = nullptr;
- EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(),
- SizeConstraints(), &ppf));
+ EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), &ppf));
CodecInOut io;
EXPECT_TRUE(ConvertPackedPixelFileToCodecInOut(ppf, pool, &io));
diff --git a/lib/extras/dec/pnm.cc b/lib/extras/dec/pnm.cc
index 03aecef..c576385 100644
--- a/lib/extras/dec/pnm.cc
+++ b/lib/extras/dec/pnm.cc
@@ -8,6 +8,9 @@
#include <stdlib.h>
#include <string.h>
+#include <cmath>
+
+#include "lib/extras/size_constraints.h"
#include "lib/jxl/base/bits.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/status.h"
@@ -16,16 +19,6 @@ namespace jxl {
namespace extras {
namespace {
-struct HeaderPNM {
- size_t xsize;
- size_t ysize;
- bool is_gray; // PGM
- bool has_alpha; // PAM
- size_t bits_per_sample;
- bool floating_point;
- bool big_endian;
-};
-
class Parser {
public:
explicit Parser(const Span<const uint8_t> bytes)
@@ -183,16 +176,20 @@ class Parser {
Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) {
size_t depth = 3;
size_t max_val = 255;
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
while (!MatchString("ENDHDR", /*skipws=*/false)) {
- JXL_RETURN_IF_ERROR(SkipWhitespace());
if (MatchString("WIDTH")) {
JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
} else if (MatchString("HEIGHT")) {
JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
} else if (MatchString("DEPTH")) {
JXL_RETURN_IF_ERROR(ParseUnsigned(&depth));
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
} else if (MatchString("MAXVAL")) {
JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
+ JXL_RETURN_IF_ERROR(SkipWhitespace());
} else if (MatchString("TUPLTYPE")) {
if (MatchString("RGB_ALPHA")) {
header->has_alpha = true;
@@ -209,6 +206,20 @@ class Parser {
} else if (MatchString("BLACKANDWHITE")) {
header->is_gray = true;
max_val = 1;
+ } else if (MatchString("Alpha")) {
+ header->ec_types.push_back(JXL_CHANNEL_ALPHA);
+ } else if (MatchString("Depth")) {
+ header->ec_types.push_back(JXL_CHANNEL_DEPTH);
+ } else if (MatchString("SpotColor")) {
+ header->ec_types.push_back(JXL_CHANNEL_SPOT_COLOR);
+ } else if (MatchString("SelectionMask")) {
+ header->ec_types.push_back(JXL_CHANNEL_SELECTION_MASK);
+ } else if (MatchString("Black")) {
+ header->ec_types.push_back(JXL_CHANNEL_BLACK);
+ } else if (MatchString("CFA")) {
+ header->ec_types.push_back(JXL_CHANNEL_CFA);
+ } else if (MatchString("Thermal")) {
+ header->ec_types.push_back(JXL_CHANNEL_THERMAL);
} else {
return JXL_FAILURE("PAM: unknown TUPLTYPE");
}
@@ -223,13 +234,13 @@ class Parser {
}
size_t num_channels = header->is_gray ? 1 : 3;
if (header->has_alpha) num_channels++;
- if (num_channels != depth) {
+ if (num_channels + header->ec_types.size() != depth) {
return JXL_FAILURE("PAM: bad DEPTH");
}
if (max_val == 0 || max_val >= 65536) {
return JXL_FAILURE("PAM: bad MAXVAL");
}
- // e.g When `max_val` is 1 , we want 1 bit:
+ // e.g. When `max_val` is 1 , we want 1 bit:
header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
if ((1u << header->bits_per_sample) - 1 != max_val)
return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
@@ -298,30 +309,98 @@ class Parser {
};
Span<const uint8_t> MakeSpan(const char* str) {
- return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str),
- strlen(str));
+ return Bytes(reinterpret_cast<const uint8_t*>(str), strlen(str));
+}
+
+void ReadLinePNM(void* opaque, size_t xpos, size_t ypos, size_t xsize,
+ uint8_t* buffer, size_t len) {
+ ChunkedPNMDecoder* dec = reinterpret_cast<ChunkedPNMDecoder*>(opaque);
+ const size_t bytes_per_channel =
+ DivCeil(dec->header.bits_per_sample, jxl::kBitsPerByte);
+ const size_t pixel_offset = ypos * dec->header.xsize + xpos;
+ const size_t num_channels = dec->header.is_gray ? 1 : 3;
+ const size_t offset = pixel_offset * num_channels * bytes_per_channel;
+ const size_t num_bytes = xsize * num_channels * bytes_per_channel;
+ if (fseek(dec->f, dec->data_start + offset, SEEK_SET) != 0) {
+ return;
+ }
+ JXL_ASSERT(num_bytes == len);
+ if (num_bytes != fread(buffer, 1, num_bytes, dec->f)) {
+ JXL_WARNING("Failed to read from PNM file\n");
+ }
}
} // namespace
-Status DecodeImagePNM(const Span<const uint8_t> bytes,
- const ColorHints& color_hints,
- const SizeConstraints& constraints,
+Status DecodeImagePNM(ChunkedPNMDecoder* dec, const ColorHints& color_hints,
PackedPixelFile* ppf) {
+ std::vector<uint8_t> buffer(10 * 1024);
+ const size_t bytes_read = fread(buffer.data(), 1, buffer.size(), dec->f);
+ if (ferror(dec->f) || bytes_read > buffer.size()) {
+ return false;
+ }
+ Span<const uint8_t> span(buffer);
+ Parser parser(span);
+ HeaderPNM& header = dec->header;
+ const uint8_t* pos = nullptr;
+ if (!parser.ParseHeader(&header, &pos)) {
+ return false;
+ }
+ dec->data_start = pos - &buffer[0];
+
+ if (header.bits_per_sample == 0 || header.bits_per_sample > 16) {
+ return JXL_FAILURE("Invalid bits_per_sample");
+ }
+ if (header.has_alpha || !header.ec_types.empty() || header.floating_point) {
+ return JXL_FAILURE("Only PGM and PPM inputs are supported");
+ }
+
+ // PPM specifies that in the raster, the sample values are "nonlinear"
+ // (BP.709, with gamma number of 2.2). Deviate from the specification and
+ // assume `sRGB` in our implementation.
+ JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
+ header.is_gray, ppf));
+
+ ppf->info.xsize = header.xsize;
+ ppf->info.ysize = header.ysize;
+ ppf->info.bits_per_sample = header.bits_per_sample;
+ ppf->info.exponent_bits_per_sample = 0;
+ ppf->info.orientation = JXL_ORIENT_IDENTITY;
+ ppf->info.alpha_bits = 0;
+ ppf->info.alpha_exponent_bits = 0;
+ ppf->info.num_color_channels = (header.is_gray ? 1 : 3);
+ ppf->info.num_extra_channels = 0;
+
+ const JxlDataType data_type =
+ header.bits_per_sample > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8;
+ const JxlPixelFormat format{
+ /*num_channels=*/ppf->info.num_color_channels,
+ /*data_type=*/data_type,
+ /*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
+ /*align=*/0,
+ };
+ ppf->chunked_frames.emplace_back(header.xsize, header.ysize, format, dec,
+ ReadLinePNM);
+ return true;
+}
+
+Status DecodeImagePNM(const Span<const uint8_t> bytes,
+ const ColorHints& color_hints, PackedPixelFile* ppf,
+ const SizeConstraints* constraints) {
Parser parser(bytes);
HeaderPNM header = {};
const uint8_t* pos = nullptr;
if (!parser.ParseHeader(&header, &pos)) return false;
JXL_RETURN_IF_ERROR(
- VerifyDimensions(&constraints, header.xsize, header.ysize));
+ VerifyDimensions(constraints, header.xsize, header.ysize));
if (header.bits_per_sample == 0 || header.bits_per_sample > 32) {
return JXL_FAILURE("PNM: bits_per_sample invalid");
}
- // PPM specify that in the raster, the sample values are "nonlinear" (BP.709,
- // with gamma number of 2.2). Deviate from the specification and assume
- // `sRGB` in our implementation.
+ // PPM specifies that in the raster, the sample values are "nonlinear"
+ // (BP.709, with gamma number of 2.2). Deviate from the specification and
+ // assume `sRGB` in our implementation.
JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
header.is_gray, ppf));
@@ -341,7 +420,17 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes,
ppf->info.alpha_bits = (header.has_alpha ? ppf->info.bits_per_sample : 0);
ppf->info.alpha_exponent_bits = 0;
ppf->info.num_color_channels = (header.is_gray ? 1 : 3);
- ppf->info.num_extra_channels = (header.has_alpha ? 1 : 0);
+ uint32_t num_alpha_channels = (header.has_alpha ? 1 : 0);
+ uint32_t num_interleaved_channels =
+ ppf->info.num_color_channels + num_alpha_channels;
+ ppf->info.num_extra_channels = num_alpha_channels + header.ec_types.size();
+
+ for (auto type : header.ec_types) {
+ PackedExtraChannel pec;
+ pec.ec_info.bits_per_sample = ppf->info.bits_per_sample;
+ pec.ec_info.type = type;
+ ppf->extra_channels_info.emplace_back(std::move(pec));
+ }
JxlDataType data_type;
if (header.floating_point) {
@@ -356,27 +445,50 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes,
}
const JxlPixelFormat format{
- /*num_channels=*/ppf->info.num_color_channels +
- ppf->info.num_extra_channels,
+ /*num_channels=*/num_interleaved_channels,
/*data_type=*/data_type,
/*endianness=*/header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN,
/*align=*/0,
};
+ const JxlPixelFormat ec_format{1, format.data_type, format.endianness, 0};
ppf->frames.clear();
ppf->frames.emplace_back(header.xsize, header.ysize, format);
auto* frame = &ppf->frames.back();
-
+ for (size_t i = 0; i < header.ec_types.size(); ++i) {
+ frame->extra_channels.emplace_back(header.xsize, header.ysize, ec_format);
+ }
size_t pnm_remaining_size = bytes.data() + bytes.size() - pos;
if (pnm_remaining_size < frame->color.pixels_size) {
return JXL_FAILURE("PNM file too small");
}
- const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped
+
uint8_t* out = reinterpret_cast<uint8_t*>(frame->color.pixels());
- for (size_t y = 0; y < header.ysize; ++y) {
- size_t y_in = flipped_y ? header.ysize - 1 - y : y;
- const uint8_t* row_in = &pos[y_in * frame->color.stride];
- uint8_t* row_out = &out[y * frame->color.stride];
- memcpy(row_out, row_in, frame->color.stride);
+ std::vector<uint8_t*> ec_out(header.ec_types.size());
+ for (size_t i = 0; i < ec_out.size(); ++i) {
+ ec_out[i] = reinterpret_cast<uint8_t*>(frame->extra_channels[i].pixels());
+ }
+ if (ec_out.empty()) {
+ const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped
+ for (size_t y = 0; y < header.ysize; ++y) {
+ size_t y_in = flipped_y ? header.ysize - 1 - y : y;
+ const uint8_t* row_in = &pos[y_in * frame->color.stride];
+ uint8_t* row_out = &out[y * frame->color.stride];
+ memcpy(row_out, row_in, frame->color.stride);
+ }
+ } else {
+ size_t pwidth = PackedImage::BitsPerChannel(data_type) / 8;
+ for (size_t y = 0; y < header.ysize; ++y) {
+ for (size_t x = 0; x < header.xsize; ++x) {
+ memcpy(out, pos, frame->color.pixel_stride());
+ out += frame->color.pixel_stride();
+ pos += frame->color.pixel_stride();
+ for (auto& p : ec_out) {
+ memcpy(p, pos, pwidth);
+ pos += pwidth;
+ p += pwidth;
+ }
+ }
+ }
}
return true;
}
diff --git a/lib/extras/dec/pnm.h b/lib/extras/dec/pnm.h
index f637483..9b68e56 100644
--- a/lib/extras/dec/pnm.h
+++ b/lib/extras/dec/pnm.h
@@ -17,21 +17,43 @@
#include "lib/extras/dec/color_hints.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/data_parallel.h"
-#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/codec_in_out.h"
namespace jxl {
+
+struct SizeConstraints;
+
namespace extras {
// Decodes `bytes` into `ppf`. color_hints may specify "color_space", which
// defaults to sRGB.
Status DecodeImagePNM(Span<const uint8_t> bytes, const ColorHints& color_hints,
- const SizeConstraints& constraints, PackedPixelFile* ppf);
+ PackedPixelFile* ppf,
+ const SizeConstraints* constraints = nullptr);
void TestCodecPNM();
+struct HeaderPNM {
+ size_t xsize;
+ size_t ysize;
+ bool is_gray; // PGM
+ bool has_alpha; // PAM
+ size_t bits_per_sample;
+ bool floating_point;
+ bool big_endian;
+ std::vector<JxlExtraChannelType> ec_types; // PAM
+};
+
+struct ChunkedPNMDecoder {
+ FILE* f;
+ HeaderPNM header = {};
+ size_t data_start;
+};
+
+Status DecodeImagePNM(ChunkedPNMDecoder* dec, const ColorHints& color_hints,
+ PackedPixelFile* ppf);
+
} // namespace extras
} // namespace jxl
diff --git a/lib/extras/enc/apng.cc b/lib/extras/enc/apng.cc
index db6cf9e..f2f8754 100644
--- a/lib/extras/enc/apng.cc
+++ b/lib/extras/enc/apng.cc
@@ -36,7 +36,6 @@
*
*/
-#include <stdio.h>
#include <string.h>
#include <string>
@@ -45,21 +44,29 @@
#include "lib/extras/exif.h"
#include "lib/jxl/base/byte_order.h"
#include "lib/jxl/base/printf_macros.h"
+#if JPEGXL_ENABLE_APNG
#include "png.h" /* original (unpatched) libpng is ok */
+#endif
namespace jxl {
namespace extras {
+#if JPEGXL_ENABLE_APNG
namespace {
+constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
+ 0x66, 0x00, 0x00};
+
class APNGEncoder : public Encoder {
public:
std::vector<JxlPixelFormat> AcceptedFormats() const override {
std::vector<JxlPixelFormat> formats;
for (const uint32_t num_channels : {1, 2, 3, 4}) {
for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) {
- formats.push_back(JxlPixelFormat{num_channels, data_type,
- JXL_BIG_ENDIAN, /*align=*/0});
+ for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
+ formats.push_back(
+ JxlPixelFormat{num_channels, data_type, endianness, /*align=*/0});
+ }
}
}
return formats;
@@ -96,12 +103,24 @@ class BlobsWriterPNG {
// identity to avoid repeated orientation.
std::vector<uint8_t> exif = blobs.exif;
ResetExifOrientation(exif);
+ // By convention, the data is prefixed with "Exif\0\0" when stored in
+ // the legacy (and non-standard) "Raw profile type exif" text chunk
+ // currently used here.
+ // TODO(user): Store Exif data in an eXIf chunk instead, which always
+ // begins with the TIFF header.
+ if (exif.size() >= sizeof kExifSignature &&
+ memcmp(exif.data(), kExifSignature, sizeof kExifSignature) != 0) {
+ exif.insert(exif.begin(), kExifSignature,
+ kExifSignature + sizeof kExifSignature);
+ }
JXL_RETURN_IF_ERROR(EncodeBase16("exif", exif, strings));
}
if (!blobs.iptc.empty()) {
JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings));
}
if (!blobs.xmp.empty()) {
+ // TODO(user): Store XMP data in an "XML:com.adobe.xmp" text chunk
+ // instead.
JXL_RETURN_IF_ERROR(EncodeBase16("xmp", blobs.xmp, strings));
}
return true;
@@ -142,7 +161,7 @@ class BlobsWriterPNG {
}
};
-void MaybeAddCICP(JxlColorEncoding c_enc, png_structp png_ptr,
+void MaybeAddCICP(const JxlColorEncoding& c_enc, png_structp png_ptr,
png_infop info_ptr) {
png_byte cicp_data[4] = {};
png_unknown_chunk cicp_chunk;
@@ -172,13 +191,80 @@ void MaybeAddCICP(JxlColorEncoding c_enc, png_structp png_ptr,
cicp_data[3] = 1;
cicp_chunk.data = cicp_data;
cicp_chunk.size = sizeof(cicp_data);
- cicp_chunk.location = PNG_HAVE_PLTE;
+ cicp_chunk.location = PNG_HAVE_IHDR;
memcpy(cicp_chunk.name, "cICP", 5);
- png_set_keep_unknown_chunks(png_ptr, 3,
+ png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS,
reinterpret_cast<const png_byte*>("cICP"), 1);
png_set_unknown_chunks(png_ptr, info_ptr, &cicp_chunk, 1);
}
+bool MaybeAddSRGB(const JxlColorEncoding& c_enc, png_structp png_ptr,
+ png_infop info_ptr) {
+ if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_SRGB &&
+ (c_enc.color_space == JXL_COLOR_SPACE_GRAY ||
+ (c_enc.color_space == JXL_COLOR_SPACE_RGB &&
+ c_enc.primaries == JXL_PRIMARIES_SRGB &&
+ c_enc.white_point == JXL_WHITE_POINT_D65))) {
+ png_set_sRGB(png_ptr, info_ptr, c_enc.rendering_intent);
+ png_set_cHRM_fixed(png_ptr, info_ptr, 31270, 32900, 64000, 33000, 30000,
+ 60000, 15000, 6000);
+ png_set_gAMA_fixed(png_ptr, info_ptr, 45455);
+ return true;
+ }
+ return false;
+}
+
+void MaybeAddCHRM(const JxlColorEncoding& c_enc, png_structp png_ptr,
+ png_infop info_ptr) {
+ if (c_enc.color_space != JXL_COLOR_SPACE_RGB) return;
+ if (c_enc.primaries == 0) return;
+ png_set_cHRM(png_ptr, info_ptr, c_enc.white_point_xy[0],
+ c_enc.white_point_xy[1], c_enc.primaries_red_xy[0],
+ c_enc.primaries_red_xy[1], c_enc.primaries_green_xy[0],
+ c_enc.primaries_green_xy[1], c_enc.primaries_blue_xy[0],
+ c_enc.primaries_blue_xy[1]);
+}
+
+void MaybeAddGAMA(const JxlColorEncoding& c_enc, png_structp png_ptr,
+ png_infop info_ptr) {
+ switch (c_enc.transfer_function) {
+ case JXL_TRANSFER_FUNCTION_LINEAR:
+ png_set_gAMA_fixed(png_ptr, info_ptr, PNG_FP_1);
+ break;
+ case JXL_TRANSFER_FUNCTION_SRGB:
+ png_set_gAMA_fixed(png_ptr, info_ptr, 45455);
+ break;
+ case JXL_TRANSFER_FUNCTION_GAMMA:
+ png_set_gAMA(png_ptr, info_ptr, c_enc.gamma);
+ break;
+
+ default:;
+ // No gAMA chunk.
+ }
+}
+
+void MaybeAddCLLi(const JxlColorEncoding& c_enc, const float intensity_target,
+ png_structp png_ptr, png_infop info_ptr) {
+ if (c_enc.transfer_function != JXL_TRANSFER_FUNCTION_PQ) return;
+
+ const uint32_t max_cll =
+ static_cast<uint32_t>(10000.f * Clamp1(intensity_target, 0.f, 10000.f));
+ png_byte chunk_data[8] = {};
+ chunk_data[0] = (max_cll >> 24) & 0xFF;
+ chunk_data[1] = (max_cll >> 16) & 0xFF;
+ chunk_data[2] = (max_cll >> 8) & 0xFF;
+ chunk_data[3] = max_cll & 0xFF;
+ // Leave MaxFALL set to 0.
+ png_unknown_chunk chunk;
+ memcpy(chunk.name, "cLLi", 5);
+ chunk.data = chunk_data;
+ chunk.size = sizeof chunk_data;
+ chunk.location = PNG_HAVE_IHDR;
+ png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS,
+ reinterpret_cast<const png_byte*>("cLLi"), 1);
+ png_set_unknown_chunks(png_ptr, info_ptr, &chunk, 1);
+}
+
Status APNGEncoder::EncodePackedPixelFileToAPNG(
const PackedPixelFile& ppf, ThreadPool* pool,
std::vector<uint8_t>* bytes) const {
@@ -233,21 +319,7 @@ Status APNGEncoder::EncodePackedPixelFileToAPNG(
} else {
memcpy(&out[0], in, out_size);
}
- } else if (format.data_type == JXL_TYPE_FLOAT) {
- float mul = 65535.0;
- const uint8_t* p_in = in;
- uint8_t* p_out = out.data();
- for (size_t i = 0; i < num_samples; ++i, p_in += 4, p_out += 2) {
- uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE32(p_in)
- : LoadLE32(p_in));
- float fval;
- memcpy(&fval, &val, 4);
- StoreBE16(static_cast<uint32_t>(fval * mul + 0.5), p_out);
- }
- } else {
- return JXL_FAILURE("Unsupported pixel data type");
}
-
png_structp png_ptr;
png_infop info_ptr;
@@ -272,11 +344,19 @@ Status APNGEncoder::EncodePackedPixelFileToAPNG(
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE,
PNG_FILTER_TYPE_BASE);
if (count == 0) {
- MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr);
- if (!ppf.icc.empty()) {
- png_set_benign_errors(png_ptr, 1);
- png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(), ppf.icc.size());
+ if (!MaybeAddSRGB(ppf.color_encoding, png_ptr, info_ptr)) {
+ MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr);
+ if (!ppf.icc.empty()) {
+ png_set_benign_errors(png_ptr, 1);
+ png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(),
+ ppf.icc.size());
+ }
+ MaybeAddCHRM(ppf.color_encoding, png_ptr, info_ptr);
+ MaybeAddGAMA(ppf.color_encoding, png_ptr, info_ptr);
}
+ MaybeAddCLLi(ppf.color_encoding, ppf.info.intensity_target, png_ptr,
+ info_ptr);
+
std::vector<std::string> textstrings;
JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings));
for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) {
@@ -360,9 +440,14 @@ Status APNGEncoder::EncodePackedPixelFileToAPNG(
}
} // namespace
+#endif
std::unique_ptr<Encoder> GetAPNGEncoder() {
+#if JPEGXL_ENABLE_APNG
return jxl::make_unique<APNGEncoder>();
+#else
+ return nullptr;
+#endif
}
} // namespace extras
diff --git a/lib/extras/enc/encode.cc b/lib/extras/enc/encode.cc
index dc593d2..8c9a148 100644
--- a/lib/extras/enc/encode.cc
+++ b/lib/extras/enc/encode.cc
@@ -7,24 +7,17 @@
#include <locale>
-#if JPEGXL_ENABLE_APNG
#include "lib/extras/enc/apng.h"
-#endif
-#if JPEGXL_ENABLE_EXR
#include "lib/extras/enc/exr.h"
-#endif
-#if JPEGXL_ENABLE_JPEG
#include "lib/extras/enc/jpg.h"
-#endif
#include "lib/extras/enc/npy.h"
#include "lib/extras/enc/pgx.h"
#include "lib/extras/enc/pnm.h"
-#include "lib/jxl/base/printf_macros.h"
namespace jxl {
namespace extras {
-Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) const {
+Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) {
if (info.xsize == 0 || info.ysize == 0) {
return JXL_FAILURE("Empty image");
}
@@ -40,8 +33,34 @@ Status Encoder::VerifyBasicInfo(const JxlBasicInfo& info) const {
return true;
}
-Status Encoder::VerifyPackedImage(const PackedImage& image,
- const JxlBasicInfo& info) const {
+Status Encoder::VerifyFormat(const JxlPixelFormat& format) const {
+ for (auto f : AcceptedFormats()) {
+ if (f.num_channels != format.num_channels) continue;
+ if (f.data_type != format.data_type) continue;
+ if (f.data_type == JXL_TYPE_UINT8 || f.endianness == format.endianness) {
+ return true;
+ }
+ }
+ return JXL_FAILURE("Format is not in the list of accepted formats.");
+}
+
+Status Encoder::VerifyBitDepth(JxlDataType data_type, uint32_t bits_per_sample,
+ uint32_t exponent_bits) {
+ if ((data_type == JXL_TYPE_UINT8 &&
+ (bits_per_sample == 0 || bits_per_sample > 8 || exponent_bits != 0)) ||
+ (data_type == JXL_TYPE_UINT16 &&
+ (bits_per_sample <= 8 || bits_per_sample > 16 || exponent_bits != 0)) ||
+ (data_type == JXL_TYPE_FLOAT16 &&
+ (bits_per_sample > 16 || exponent_bits > 5))) {
+ return JXL_FAILURE(
+ "Incompatible data_type %d and bit depth %u with exponent bits %u",
+ (int)data_type, bits_per_sample, exponent_bits);
+ }
+ return true;
+}
+
+Status Encoder::VerifyImageSize(const PackedImage& image,
+ const JxlBasicInfo& info) {
if (image.pixels() == nullptr) {
return JXL_FAILURE("Invalid image.");
}
@@ -57,77 +76,60 @@ Status Encoder::VerifyPackedImage(const PackedImage& image,
image.format.num_channels != info_num_channels) {
return JXL_FAILURE("Frame size does not match image size");
}
- if (info.bits_per_sample >
- PackedImage::BitsPerChannel(image.format.data_type)) {
- return JXL_FAILURE("Bit depth does not fit pixel data type");
- }
return true;
}
-Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats,
- const JxlBasicInfo& basic_info, JxlPixelFormat* format) {
- const size_t original_bit_depth = basic_info.bits_per_sample;
- size_t current_bit_depth = 0;
- size_t num_alpha_channels = (basic_info.alpha_bits != 0 ? 1 : 0);
- size_t num_channels = basic_info.num_color_channels + num_alpha_channels;
- for (;;) {
- for (const JxlPixelFormat& candidate : accepted_formats) {
- if (candidate.num_channels != num_channels) continue;
- const size_t candidate_bit_depth =
- PackedImage::BitsPerChannel(candidate.data_type);
- if (
- // Candidate bit depth is less than what we have and still enough
- (original_bit_depth <= candidate_bit_depth &&
- candidate_bit_depth < current_bit_depth) ||
- // Or larger than the too-small bit depth we currently have
- (current_bit_depth < candidate_bit_depth &&
- current_bit_depth < original_bit_depth)) {
- *format = candidate;
- current_bit_depth = candidate_bit_depth;
- }
- }
- if (current_bit_depth == 0) {
- if (num_channels > basic_info.num_color_channels) {
- // Try dropping the alpha channel.
- --num_channels;
- continue;
- }
- return JXL_FAILURE("no appropriate format found");
- }
- break;
- }
- if (current_bit_depth < original_bit_depth) {
- JXL_WARNING("encoding %" PRIuS "-bit original to %" PRIuS " bits",
- original_bit_depth, current_bit_depth);
- }
+Status Encoder::VerifyPackedImage(const PackedImage& image,
+ const JxlBasicInfo& info) const {
+ JXL_RETURN_IF_ERROR(VerifyImageSize(image, info));
+ JXL_RETURN_IF_ERROR(VerifyFormat(image.format));
+ JXL_RETURN_IF_ERROR(VerifyBitDepth(image.format.data_type,
+ info.bits_per_sample,
+ info.exponent_bits_per_sample));
return true;
}
+template <int metadata>
+class MetadataEncoder : public Encoder {
+ public:
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ std::vector<JxlPixelFormat> formats;
+ // empty, i.e. no need for actual pixel data
+ return formats;
+ }
+
+ Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded,
+ ThreadPool* pool) const override {
+ JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
+ encoded->icc.clear();
+ encoded->bitstreams.resize(1);
+ if (metadata == 0) encoded->bitstreams.front() = ppf.metadata.exif;
+ if (metadata == 1) encoded->bitstreams.front() = ppf.metadata.xmp;
+ if (metadata == 2) encoded->bitstreams.front() = ppf.metadata.jumbf;
+ return true;
+ }
+};
+
std::unique_ptr<Encoder> Encoder::FromExtension(std::string extension) {
std::transform(
extension.begin(), extension.end(), extension.begin(),
[](char c) { return std::tolower(c, std::locale::classic()); });
-#if JPEGXL_ENABLE_APNG
if (extension == ".png" || extension == ".apng") return GetAPNGEncoder();
-#endif
-
-#if JPEGXL_ENABLE_JPEG
if (extension == ".jpg") return GetJPEGEncoder();
if (extension == ".jpeg") return GetJPEGEncoder();
-#endif
-
if (extension == ".npy") return GetNumPyEncoder();
-
if (extension == ".pgx") return GetPGXEncoder();
-
if (extension == ".pam") return GetPAMEncoder();
if (extension == ".pgm") return GetPGMEncoder();
if (extension == ".ppm") return GetPPMEncoder();
+ if (extension == ".pnm") return GetPNMEncoder();
if (extension == ".pfm") return GetPFMEncoder();
-
-#if JPEGXL_ENABLE_EXR
if (extension == ".exr") return GetEXREncoder();
-#endif
+ if (extension == ".exif") return jxl::make_unique<MetadataEncoder<0>>();
+ if (extension == ".xmp") return jxl::make_unique<MetadataEncoder<1>>();
+ if (extension == ".xml") return jxl::make_unique<MetadataEncoder<1>>();
+ if (extension == ".jumbf") return jxl::make_unique<MetadataEncoder<2>>();
+ if (extension == ".jumb") return jxl::make_unique<MetadataEncoder<2>>();
return nullptr;
}
diff --git a/lib/extras/enc/encode.h b/lib/extras/enc/encode.h
index 92eec50..da5f509 100644
--- a/lib/extras/enc/encode.h
+++ b/lib/extras/enc/encode.h
@@ -8,10 +8,17 @@
// Facade for image encoders.
+#include <jxl/codestream_header.h>
+#include <jxl/types.h>
+
+#include <cstdint>
+#include <memory>
#include <string>
#include <unordered_map>
+#include <utility>
+#include <vector>
-#include "lib/extras/dec/decode.h"
+#include "lib/extras/packed_image.h"
#include "lib/jxl/base/data_parallel.h"
#include "lib/jxl/base/status.h"
@@ -20,7 +27,7 @@ namespace extras {
struct EncodedImage {
// One (if the format supports animations or the image has only one frame) or
- // more sequential bitstreams.
+ // more 1quential bitstreams.
std::vector<std::vector<uint8_t>> bitstreams;
// For each extra channel one or more sequential bitstreams.
@@ -43,6 +50,8 @@ class Encoder {
virtual ~Encoder() = default;
+ // Set of pixel formats that this encoder takes as input.
+ // If empty, the 'encoder' does not need any pixels (it's metadata-only).
virtual std::vector<JxlPixelFormat> AcceptedFormats() const = 0;
// Any existing data in encoded_image is discarded.
@@ -53,12 +62,18 @@ class Encoder {
options_[std::move(name)] = std::move(value);
}
+ static Status VerifyBasicInfo(const JxlBasicInfo& info);
+ static Status VerifyImageSize(const PackedImage& image,
+ const JxlBasicInfo& info);
+ static Status VerifyBitDepth(JxlDataType data_type, uint32_t bits_per_sample,
+ uint32_t exponent_bits);
+
protected:
const std::unordered_map<std::string, std::string>& options() const {
return options_;
}
- Status VerifyBasicInfo(const JxlBasicInfo& info) const;
+ Status VerifyFormat(const JxlPixelFormat& format) const;
Status VerifyPackedImage(const PackedImage& image,
const JxlBasicInfo& info) const;
@@ -67,10 +82,6 @@ class Encoder {
std::unordered_map<std::string, std::string> options_;
};
-// TODO(sboukortt): consider exposing this as part of the C API.
-Status SelectFormat(const std::vector<JxlPixelFormat>& accepted_formats,
- const JxlBasicInfo& basic_info, JxlPixelFormat* format);
-
} // namespace extras
} // namespace jxl
diff --git a/lib/extras/enc/exr.cc b/lib/extras/enc/exr.cc
index 05e05f9..d4005c3 100644
--- a/lib/extras/enc/exr.cc
+++ b/lib/extras/enc/exr.cc
@@ -5,20 +5,23 @@
#include "lib/extras/enc/exr.h"
+#if JPEGXL_ENABLE_EXR
#include <ImfChromaticitiesAttribute.h>
#include <ImfIO.h>
#include <ImfRgbaFile.h>
#include <ImfStandardAttributes.h>
+#endif
+#include <jxl/codestream_header.h>
#include <vector>
-#include "jxl/codestream_header.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/byte_order.h"
namespace jxl {
namespace extras {
+#if JPEGXL_ENABLE_EXR
namespace {
namespace OpenEXR = OPENEXR_IMF_NAMESPACE;
@@ -110,7 +113,7 @@ Status EncodeImageEXR(const PackedImage& image, const JxlBasicInfo& info,
chromaticities.white =
Imath::V2f(c_enc.white_point_xy[0], c_enc.white_point_xy[1]);
OpenEXR::addChromaticities(header, chromaticities);
- OpenEXR::addWhiteLuminance(header, 255.0f);
+ OpenEXR::addWhiteLuminance(header, info.intensity_target);
auto loadFloat =
format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat : LoadLEFloat;
@@ -162,7 +165,7 @@ class EXREncoder : public Encoder {
std::vector<JxlPixelFormat> AcceptedFormats() const override {
std::vector<JxlPixelFormat> formats;
for (const uint32_t num_channels : {1, 2, 3, 4}) {
- for (const JxlDataType data_type : {JXL_TYPE_FLOAT, JXL_TYPE_FLOAT16}) {
+ for (const JxlDataType data_type : {JXL_TYPE_FLOAT}) {
for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
/*data_type=*/data_type,
@@ -191,9 +194,14 @@ class EXREncoder : public Encoder {
};
} // namespace
+#endif
std::unique_ptr<Encoder> GetEXREncoder() {
+#if JPEGXL_ENABLE_EXR
return jxl::make_unique<EXREncoder>();
+#else
+ return nullptr;
+#endif
}
} // namespace extras
diff --git a/lib/extras/enc/jpegli.cc b/lib/extras/enc/jpegli.cc
new file mode 100644
index 0000000..3b78764
--- /dev/null
+++ b/lib/extras/enc/jpegli.cc
@@ -0,0 +1,523 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/jpegli.h"
+
+#include <jxl/cms.h>
+#include <jxl/codestream_header.h>
+#include <setjmp.h>
+#include <stdint.h>
+
+#include "lib/extras/enc/encode.h"
+#include "lib/jpegli/encode.h"
+#include "lib/jxl/enc_xyb.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+void MyErrorExit(j_common_ptr cinfo) {
+ jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data);
+ (*cinfo->err->output_message)(cinfo);
+ jpegli_destroy_compress(reinterpret_cast<j_compress_ptr>(cinfo));
+ longjmp(*env, 1);
+}
+
+Status VerifyInput(const PackedPixelFile& ppf) {
+ const JxlBasicInfo& info = ppf.info;
+ JXL_RETURN_IF_ERROR(Encoder::VerifyBasicInfo(info));
+ if (ppf.frames.size() != 1) {
+ return JXL_FAILURE("JPEG input must have exactly one frame.");
+ }
+ const PackedImage& image = ppf.frames[0].color;
+ JXL_RETURN_IF_ERROR(Encoder::VerifyImageSize(image, info));
+ if (image.format.data_type == JXL_TYPE_FLOAT16) {
+ return JXL_FAILURE("FLOAT16 input is not supported.");
+ }
+ JXL_RETURN_IF_ERROR(Encoder::VerifyBitDepth(image.format.data_type,
+ info.bits_per_sample,
+ info.exponent_bits_per_sample));
+ if ((image.format.data_type == JXL_TYPE_UINT8 && info.bits_per_sample != 8) ||
+ (image.format.data_type == JXL_TYPE_UINT16 &&
+ info.bits_per_sample != 16)) {
+ return JXL_FAILURE("Only full bit depth unsigned types are supported.");
+ }
+ return true;
+}
+
+Status GetColorEncoding(const PackedPixelFile& ppf,
+ ColorEncoding* color_encoding) {
+ if (!ppf.icc.empty()) {
+ IccBytes icc = ppf.icc;
+ JXL_RETURN_IF_ERROR(
+ color_encoding->SetICC(std::move(icc), JxlGetDefaultCms()));
+ } else {
+ JXL_RETURN_IF_ERROR(color_encoding->FromExternal(ppf.color_encoding));
+ }
+ if (color_encoding->ICC().empty()) {
+ return JXL_FAILURE("Invalid color encoding.");
+ }
+ return true;
+}
+
+bool HasICCProfile(const std::vector<uint8_t>& app_data) {
+ size_t pos = 0;
+ while (pos < app_data.size()) {
+ if (pos + 16 > app_data.size()) return false;
+ uint8_t marker = app_data[pos + 1];
+ size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2;
+ if (marker == 0xe2 && memcmp(&app_data[pos + 4], "ICC_PROFILE", 12) == 0) {
+ return true;
+ }
+ pos += marker_len;
+ }
+ return false;
+}
+
+Status WriteAppData(j_compress_ptr cinfo,
+ const std::vector<uint8_t>& app_data) {
+ size_t pos = 0;
+ while (pos < app_data.size()) {
+ if (pos + 4 > app_data.size()) {
+ return JXL_FAILURE("Incomplete APP header.");
+ }
+ uint8_t marker = app_data[pos + 1];
+ size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2;
+ if (app_data[pos] != 0xff || marker < 0xe0 || marker > 0xef) {
+ return JXL_FAILURE("Invalid APP marker %02x %02x", app_data[pos], marker);
+ }
+ if (marker_len <= 4) {
+ return JXL_FAILURE("Invalid APP marker length.");
+ }
+ if (pos + marker_len > app_data.size()) {
+ return JXL_FAILURE("Incomplete APP data");
+ }
+ jpegli_write_marker(cinfo, marker, &app_data[pos + 4], marker_len - 4);
+ pos += marker_len;
+ }
+ return true;
+}
+
+static constexpr int kICCMarker = 0xe2;
+constexpr unsigned char kICCSignature[12] = {
+ 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00};
+static constexpr uint8_t kUnknownTf = 2;
+static constexpr unsigned char kCICPTagSignature[4] = {0x63, 0x69, 0x63, 0x70};
+static constexpr size_t kCICPTagSize = 12;
+
+bool FindCICPTag(const uint8_t* icc_data, size_t len, bool is_first_chunk,
+ size_t* cicp_offset, size_t* cicp_length, uint8_t* cicp_tag,
+ size_t* cicp_pos) {
+ if (is_first_chunk) {
+ // Look up the offset of the CICP tag from the first chunk of ICC data.
+ if (len < 132) {
+ return false;
+ }
+ uint32_t tag_count = LoadBE32(&icc_data[128]);
+ if (len < 132 + 12 * tag_count) {
+ return false;
+ }
+ for (uint32_t i = 0; i < tag_count; ++i) {
+ if (memcmp(&icc_data[132 + 12 * i], kCICPTagSignature, 4) == 0) {
+ *cicp_offset = LoadBE32(&icc_data[136 + 12 * i]);
+ *cicp_length = LoadBE32(&icc_data[140 + 12 * i]);
+ }
+ }
+ if (*cicp_length < kCICPTagSize) {
+ return false;
+ }
+ }
+ if (*cicp_offset < len) {
+ size_t n_bytes = std::min(len - *cicp_offset, kCICPTagSize - *cicp_pos);
+ memcpy(&cicp_tag[*cicp_pos], &icc_data[*cicp_offset], n_bytes);
+ *cicp_pos += n_bytes;
+ *cicp_offset = 0;
+ } else {
+ *cicp_offset -= len;
+ }
+ return true;
+}
+
+uint8_t LookupCICPTransferFunctionFromAppData(const uint8_t* app_data,
+ size_t len) {
+ size_t last_index = 0;
+ size_t cicp_offset = 0;
+ size_t cicp_length = 0;
+ uint8_t cicp_tag[kCICPTagSize] = {};
+ size_t cicp_pos = 0;
+ size_t pos = 0;
+ while (pos < len) {
+ const uint8_t* marker = &app_data[pos];
+ if (pos + 4 > len) {
+ return kUnknownTf;
+ }
+ size_t marker_size = (marker[2] << 8) + marker[3] + 2;
+ if (pos + marker_size > len) {
+ return kUnknownTf;
+ }
+ if (marker_size < 18 || marker[0] != 0xff || marker[1] != kICCMarker ||
+ memcmp(&marker[4], kICCSignature, 12) != 0) {
+ pos += marker_size;
+ continue;
+ }
+ uint8_t index = marker[16];
+ uint8_t total = marker[17];
+ const uint8_t* payload = marker + 18;
+ const size_t payload_size = marker_size - 18;
+ if (index != last_index + 1 || index > total) {
+ return kUnknownTf;
+ }
+ if (!FindCICPTag(payload, payload_size, last_index == 0, &cicp_offset,
+ &cicp_length, &cicp_tag[0], &cicp_pos)) {
+ return kUnknownTf;
+ }
+ if (cicp_pos == kCICPTagSize) {
+ break;
+ }
+ ++last_index;
+ }
+ if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) {
+ return cicp_tag[9];
+ }
+ return kUnknownTf;
+}
+
+uint8_t LookupCICPTransferFunctionFromICCProfile(const uint8_t* icc_data,
+ size_t len) {
+ size_t cicp_offset = 0;
+ size_t cicp_length = 0;
+ uint8_t cicp_tag[kCICPTagSize] = {};
+ size_t cicp_pos = 0;
+ if (!FindCICPTag(icc_data, len, true, &cicp_offset, &cicp_length,
+ &cicp_tag[0], &cicp_pos)) {
+ return kUnknownTf;
+ }
+ if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) {
+ return cicp_tag[9];
+ }
+ return kUnknownTf;
+}
+
+JpegliDataType ConvertDataType(JxlDataType type) {
+ switch (type) {
+ case JXL_TYPE_UINT8:
+ return JPEGLI_TYPE_UINT8;
+ case JXL_TYPE_UINT16:
+ return JPEGLI_TYPE_UINT16;
+ case JXL_TYPE_FLOAT:
+ return JPEGLI_TYPE_FLOAT;
+ default:
+ return JPEGLI_TYPE_UINT8;
+ }
+}
+
+JpegliEndianness ConvertEndianness(JxlEndianness endianness) {
+ switch (endianness) {
+ case JXL_NATIVE_ENDIAN:
+ return JPEGLI_NATIVE_ENDIAN;
+ case JXL_LITTLE_ENDIAN:
+ return JPEGLI_LITTLE_ENDIAN;
+ case JXL_BIG_ENDIAN:
+ return JPEGLI_BIG_ENDIAN;
+ default:
+ return JPEGLI_NATIVE_ENDIAN;
+ }
+}
+
+void ToFloatRow(const uint8_t* row_in, JxlPixelFormat format, size_t len,
+ float* row_out) {
+ bool is_little_endian =
+ (format.endianness == JXL_LITTLE_ENDIAN ||
+ (format.endianness == JXL_NATIVE_ENDIAN && IsLittleEndian()));
+ static constexpr double kMul8 = 1.0 / 255.0;
+ static constexpr double kMul16 = 1.0 / 65535.0;
+ if (format.data_type == JXL_TYPE_UINT8) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = row_in[x] * kMul8;
+ }
+ } else if (format.data_type == JXL_TYPE_UINT16 && is_little_endian) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = LoadLE16(&row_in[2 * x]) * kMul16;
+ }
+ } else if (format.data_type == JXL_TYPE_UINT16 && !is_little_endian) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = LoadBE16(&row_in[2 * x]) * kMul16;
+ }
+ } else if (format.data_type == JXL_TYPE_FLOAT && is_little_endian) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = LoadLEFloat(&row_in[4 * x]);
+ }
+ } else if (format.data_type == JXL_TYPE_FLOAT && !is_little_endian) {
+ for (size_t x = 0; x < len; ++x) {
+ row_out[x] = LoadBEFloat(&row_in[4 * x]);
+ }
+ }
+}
+
+Status EncodeJpegToTargetSize(const PackedPixelFile& ppf,
+ const JpegSettings& jpeg_settings,
+ size_t target_size, ThreadPool* pool,
+ std::vector<uint8_t>* output) {
+ output->clear();
+ size_t best_error = std::numeric_limits<size_t>::max();
+ float distance0 = -1.0f;
+ float distance1 = -1.0f;
+ float distance = 1.0f;
+ for (int step = 0; step < 15; ++step) {
+ JpegSettings settings = jpeg_settings;
+ settings.libjpeg_quality = 0;
+ settings.distance = distance;
+ settings.target_size = 0;
+ std::vector<uint8_t> compressed;
+ JXL_RETURN_IF_ERROR(EncodeJpeg(ppf, settings, pool, &compressed));
+ size_t size = compressed.size();
+ // prefer being under the target size to being over it
+ size_t error = size < target_size
+ ? target_size - size
+ : static_cast<size_t>(1.2f * (size - target_size));
+ if (error < best_error) {
+ best_error = error;
+ std::swap(*output, compressed);
+ }
+ float rel_error = size * 1.0f / target_size;
+ if (std::abs(rel_error - 1.0f) < 0.002f) {
+ break;
+ }
+ if (size < target_size) {
+ distance1 = distance;
+ } else {
+ distance0 = distance;
+ }
+ if (distance1 == -1) {
+ distance *= std::pow(rel_error, 1.5) * 1.05;
+ } else if (distance0 == -1) {
+ distance *= std::pow(rel_error, 1.5) * 0.95;
+ } else {
+ distance = 0.5 * (distance0 + distance1);
+ }
+ }
+ return true;
+}
+
+} // namespace
+
+Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings,
+ ThreadPool* pool, std::vector<uint8_t>* compressed) {
+ if (jpeg_settings.libjpeg_quality > 0) {
+ auto encoder = Encoder::FromExtension(".jpg");
+ encoder->SetOption("q", std::to_string(jpeg_settings.libjpeg_quality));
+ if (!jpeg_settings.libjpeg_chroma_subsampling.empty()) {
+ encoder->SetOption("chroma_subsampling",
+ jpeg_settings.libjpeg_chroma_subsampling);
+ }
+ EncodedImage encoded;
+ JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool));
+ size_t target_size = encoded.bitstreams[0].size();
+ return EncodeJpegToTargetSize(ppf, jpeg_settings, target_size, pool,
+ compressed);
+ }
+ if (jpeg_settings.target_size > 0) {
+ return EncodeJpegToTargetSize(ppf, jpeg_settings, jpeg_settings.target_size,
+ pool, compressed);
+ }
+ JXL_RETURN_IF_ERROR(VerifyInput(ppf));
+
+ ColorEncoding color_encoding;
+ JXL_RETURN_IF_ERROR(GetColorEncoding(ppf, &color_encoding));
+
+ ColorSpaceTransform c_transform(*JxlGetDefaultCms());
+ ColorEncoding xyb_encoding;
+ if (jpeg_settings.xyb) {
+ if (ppf.info.num_color_channels != 3) {
+ return JXL_FAILURE("Only RGB input is supported in XYB mode.");
+ }
+ if (HasICCProfile(jpeg_settings.app_data)) {
+ return JXL_FAILURE("APP data ICC profile is not supported in XYB mode.");
+ }
+ const ColorEncoding& c_desired = ColorEncoding::LinearSRGB(false);
+ JXL_RETURN_IF_ERROR(
+ c_transform.Init(color_encoding, c_desired, 255.0f, ppf.info.xsize, 1));
+ xyb_encoding.SetColorSpace(jxl::ColorSpace::kXYB);
+ xyb_encoding.SetRenderingIntent(jxl::RenderingIntent::kPerceptual);
+ JXL_RETURN_IF_ERROR(xyb_encoding.CreateICC());
+ }
+ const ColorEncoding& output_encoding =
+ jpeg_settings.xyb ? xyb_encoding : color_encoding;
+
+ // We need to declare all the non-trivial destructor local variables
+ // before the call to setjmp().
+ std::vector<uint8_t> pixels;
+ unsigned char* output_buffer = nullptr;
+ unsigned long output_size = 0;
+ std::vector<uint8_t> row_bytes;
+ size_t rowlen = RoundUpTo(ppf.info.xsize, VectorSize());
+ hwy::AlignedFreeUniquePtr<float[]> xyb_tmp =
+ hwy::AllocateAligned<float>(6 * rowlen);
+ hwy::AlignedFreeUniquePtr<float[]> premul_absorb =
+ hwy::AllocateAligned<float>(VectorSize() * 12);
+ ComputePremulAbsorb(255.0f, premul_absorb.get());
+
+ jpeg_compress_struct cinfo;
+ const auto try_catch_block = [&]() -> bool {
+ jpeg_error_mgr jerr;
+ jmp_buf env;
+ cinfo.err = jpegli_std_error(&jerr);
+ jerr.error_exit = &MyErrorExit;
+ if (setjmp(env)) {
+ return false;
+ }
+ cinfo.client_data = static_cast<void*>(&env);
+ jpegli_create_compress(&cinfo);
+ jpegli_mem_dest(&cinfo, &output_buffer, &output_size);
+ const JxlBasicInfo& info = ppf.info;
+ cinfo.image_width = info.xsize;
+ cinfo.image_height = info.ysize;
+ cinfo.input_components = info.num_color_channels;
+ cinfo.in_color_space =
+ cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB;
+ if (jpeg_settings.xyb) {
+ jpegli_set_xyb_mode(&cinfo);
+ } else if (jpeg_settings.use_std_quant_tables) {
+ jpegli_use_standard_quant_tables(&cinfo);
+ }
+ uint8_t cicp_tf = kUnknownTf;
+ if (!jpeg_settings.app_data.empty()) {
+ cicp_tf = LookupCICPTransferFunctionFromAppData(
+ jpeg_settings.app_data.data(), jpeg_settings.app_data.size());
+ } else if (!output_encoding.IsSRGB()) {
+ cicp_tf = LookupCICPTransferFunctionFromICCProfile(
+ output_encoding.ICC().data(), output_encoding.ICC().size());
+ }
+ jpegli_set_cicp_transfer_function(&cinfo, cicp_tf);
+ jpegli_set_defaults(&cinfo);
+ if (!jpeg_settings.chroma_subsampling.empty()) {
+ if (jpeg_settings.chroma_subsampling == "444") {
+ cinfo.comp_info[0].h_samp_factor = 1;
+ cinfo.comp_info[0].v_samp_factor = 1;
+ } else if (jpeg_settings.chroma_subsampling == "440") {
+ cinfo.comp_info[0].h_samp_factor = 1;
+ cinfo.comp_info[0].v_samp_factor = 2;
+ } else if (jpeg_settings.chroma_subsampling == "422") {
+ cinfo.comp_info[0].h_samp_factor = 2;
+ cinfo.comp_info[0].v_samp_factor = 1;
+ } else if (jpeg_settings.chroma_subsampling == "420") {
+ cinfo.comp_info[0].h_samp_factor = 2;
+ cinfo.comp_info[0].v_samp_factor = 2;
+ } else {
+ return false;
+ }
+ for (int i = 1; i < cinfo.num_components; ++i) {
+ cinfo.comp_info[i].h_samp_factor = 1;
+ cinfo.comp_info[i].v_samp_factor = 1;
+ }
+ }
+ jpegli_enable_adaptive_quantization(
+ &cinfo, jpeg_settings.use_adaptive_quantization);
+ if (jpeg_settings.psnr_target > 0.0) {
+ jpegli_set_psnr(&cinfo, jpeg_settings.psnr_target,
+ jpeg_settings.search_tolerance,
+ jpeg_settings.min_distance, jpeg_settings.max_distance);
+ } else if (jpeg_settings.quality > 0.0) {
+ float distance = jpegli_quality_to_distance(jpeg_settings.quality);
+ jpegli_set_distance(&cinfo, distance, TRUE);
+ } else {
+ jpegli_set_distance(&cinfo, jpeg_settings.distance, TRUE);
+ }
+ jpegli_set_progressive_level(&cinfo, jpeg_settings.progressive_level);
+ cinfo.optimize_coding = jpeg_settings.optimize_coding;
+ if (!jpeg_settings.app_data.empty()) {
+ // Make sure jpegli_start_compress() does not write any APP markers.
+ cinfo.write_JFIF_header = false;
+ cinfo.write_Adobe_marker = false;
+ }
+ const PackedImage& image = ppf.frames[0].color;
+ if (jpeg_settings.xyb) {
+ jpegli_set_input_format(&cinfo, JPEGLI_TYPE_FLOAT, JPEGLI_NATIVE_ENDIAN);
+ } else {
+ jpegli_set_input_format(&cinfo, ConvertDataType(image.format.data_type),
+ ConvertEndianness(image.format.endianness));
+ }
+ jpegli_start_compress(&cinfo, TRUE);
+ if (!jpeg_settings.app_data.empty()) {
+ JXL_RETURN_IF_ERROR(WriteAppData(&cinfo, jpeg_settings.app_data));
+ }
+ if ((jpeg_settings.app_data.empty() && !output_encoding.IsSRGB()) ||
+ jpeg_settings.xyb) {
+ jpegli_write_icc_profile(&cinfo, output_encoding.ICC().data(),
+ output_encoding.ICC().size());
+ }
+ const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels());
+ if (jpeg_settings.xyb) {
+ float* src_buf = c_transform.BufSrc(0);
+ float* dst_buf = c_transform.BufDst(0);
+ for (size_t y = 0; y < image.ysize; ++y) {
+ // convert to float
+ ToFloatRow(&pixels[y * image.stride], image.format, 3 * image.xsize,
+ src_buf);
+ // convert to linear srgb
+ if (!c_transform.Run(0, src_buf, dst_buf)) {
+ return false;
+ }
+ // deinterleave channels
+ float* row0 = &xyb_tmp[0];
+ float* row1 = &xyb_tmp[rowlen];
+ float* row2 = &xyb_tmp[2 * rowlen];
+ for (size_t x = 0; x < image.xsize; ++x) {
+ row0[x] = dst_buf[3 * x + 0];
+ row1[x] = dst_buf[3 * x + 1];
+ row2[x] = dst_buf[3 * x + 2];
+ }
+ // convert to xyb
+ LinearRGBRowToXYB(row0, row1, row2, premul_absorb.get(), image.xsize);
+ // scale xyb
+ ScaleXYBRow(row0, row1, row2, image.xsize);
+ // interleave channels
+ float* row_out = &xyb_tmp[3 * rowlen];
+ for (size_t x = 0; x < image.xsize; ++x) {
+ row_out[3 * x + 0] = row0[x];
+ row_out[3 * x + 1] = row1[x];
+ row_out[3 * x + 2] = row2[x];
+ }
+ // feed to jpegli as native endian floats
+ JSAMPROW row[] = {reinterpret_cast<uint8_t*>(row_out)};
+ jpegli_write_scanlines(&cinfo, row, 1);
+ }
+ } else {
+ row_bytes.resize(image.stride);
+ if (cinfo.num_components == (int)image.format.num_channels) {
+ for (size_t y = 0; y < info.ysize; ++y) {
+ memcpy(&row_bytes[0], pixels + y * image.stride, image.stride);
+ JSAMPROW row[] = {row_bytes.data()};
+ jpegli_write_scanlines(&cinfo, row, 1);
+ }
+ } else {
+ for (size_t y = 0; y < info.ysize; ++y) {
+ int bytes_per_channel =
+ PackedImage::BitsPerChannel(image.format.data_type) / 8;
+ int bytes_per_pixel = cinfo.num_components * bytes_per_channel;
+ for (size_t x = 0; x < info.xsize; ++x) {
+ memcpy(&row_bytes[x * bytes_per_pixel],
+ &pixels[y * image.stride + x * image.pixel_stride()],
+ bytes_per_pixel);
+ }
+ JSAMPROW row[] = {row_bytes.data()};
+ jpegli_write_scanlines(&cinfo, row, 1);
+ }
+ }
+ }
+ jpegli_finish_compress(&cinfo);
+ compressed->resize(output_size);
+ std::copy_n(output_buffer, output_size, compressed->data());
+ return true;
+ };
+ bool success = try_catch_block();
+ jpegli_destroy_compress(&cinfo);
+ if (output_buffer) free(output_buffer);
+ return success;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/lib/extras/enc/jpegli.h b/lib/extras/enc/jpegli.h
new file mode 100644
index 0000000..9538b2e
--- /dev/null
+++ b/lib/extras/enc/jpegli.h
@@ -0,0 +1,53 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_JPEGLI_H_
+#define LIB_EXTRAS_ENC_JPEGLI_H_
+
+// Encodes JPG pixels and metadata in memory using the libjpegli library.
+
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+namespace extras {
+
+struct JpegSettings {
+ bool xyb = false;
+ size_t target_size = 0;
+ float quality = 0.0f;
+ float distance = 1.f;
+ bool use_adaptive_quantization = true;
+ bool use_std_quant_tables = false;
+ int progressive_level = 2;
+ bool optimize_coding = true;
+ std::string chroma_subsampling;
+ int libjpeg_quality = 0;
+ std::string libjpeg_chroma_subsampling;
+ // Parameters for selecting distance based on PSNR target.
+ float psnr_target = 0.0f;
+ float search_tolerance = 0.01;
+ float min_distance = 0.1f;
+ float max_distance = 25.0f;
+ // If not empty, must contain concatenated APP marker segments. In this case,
+ // these and only these APP marker segments will be written to the JPEG
+ // output. In xyb mode app_data must not contain an ICC profile, in this
+ // case an additional APP2 ICC profile for the XYB colorspace will be emitted.
+ std::vector<uint8_t> app_data;
+};
+
+Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings,
+ ThreadPool* pool, std::vector<uint8_t>* compressed);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_JPEGLI_H_
diff --git a/lib/extras/enc/jpg.cc b/lib/extras/enc/jpg.cc
index 93a39dd..f1355bb 100644
--- a/lib/extras/enc/jpg.cc
+++ b/lib/extras/enc/jpg.cc
@@ -5,12 +5,18 @@
#include "lib/extras/enc/jpg.h"
+#if JPEGXL_ENABLE_JPEG
#include <jpeglib.h>
#include <setjmp.h>
+#endif
#include <stdint.h>
#include <algorithm>
+#include <array>
+#include <cmath>
+#include <fstream>
#include <iterator>
+#include <memory>
#include <numeric>
#include <sstream>
#include <utility>
@@ -21,11 +27,13 @@
#include "lib/jxl/sanitizers.h"
#if JPEGXL_ENABLE_SJPEG
#include "sjpeg.h"
+#include "sjpegi.h"
#endif
namespace jxl {
namespace extras {
+#if JPEGXL_ENABLE_JPEG
namespace {
constexpr unsigned char kICCSignature[12] = {
@@ -42,6 +50,142 @@ enum class JpegEncoder {
kSJpeg,
};
+#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0]))
+
+// Popular jpeg scan scripts
+// The fields of the individual scans are:
+// comps_in_scan, component_index[], Ss, Se, Ah, Al
+static constexpr jpeg_scan_info kScanScript1[] = {
+ {1, {0}, 0, 0, 0, 0}, //
+ {1, {1}, 0, 0, 0, 0}, //
+ {1, {2}, 0, 0, 0, 0}, //
+ {1, {0}, 1, 8, 0, 0}, //
+ {1, {0}, 9, 63, 0, 0}, //
+ {1, {1}, 1, 63, 0, 0}, //
+ {1, {2}, 1, 63, 0, 0}, //
+};
+static constexpr size_t kNumScans1 = ARRAY_SIZE(kScanScript1);
+
+static constexpr jpeg_scan_info kScanScript2[] = {
+ {1, {0}, 0, 0, 0, 0}, //
+ {1, {1}, 0, 0, 0, 0}, //
+ {1, {2}, 0, 0, 0, 0}, //
+ {1, {0}, 1, 2, 0, 1}, //
+ {1, {0}, 3, 63, 0, 1}, //
+ {1, {0}, 1, 63, 1, 0}, //
+ {1, {1}, 1, 63, 0, 0}, //
+ {1, {2}, 1, 63, 0, 0}, //
+};
+static constexpr size_t kNumScans2 = ARRAY_SIZE(kScanScript2);
+
+static constexpr jpeg_scan_info kScanScript3[] = {
+ {1, {0}, 0, 0, 0, 0}, //
+ {1, {1}, 0, 0, 0, 0}, //
+ {1, {2}, 0, 0, 0, 0}, //
+ {1, {0}, 1, 63, 0, 2}, //
+ {1, {0}, 1, 63, 2, 1}, //
+ {1, {0}, 1, 63, 1, 0}, //
+ {1, {1}, 1, 63, 0, 0}, //
+ {1, {2}, 1, 63, 0, 0}, //
+};
+static constexpr size_t kNumScans3 = ARRAY_SIZE(kScanScript3);
+
+static constexpr jpeg_scan_info kScanScript4[] = {
+ {3, {0, 1, 2}, 0, 0, 0, 1}, //
+ {1, {0}, 1, 5, 0, 2}, //
+ {1, {2}, 1, 63, 0, 1}, //
+ {1, {1}, 1, 63, 0, 1}, //
+ {1, {0}, 6, 63, 0, 2}, //
+ {1, {0}, 1, 63, 2, 1}, //
+ {3, {0, 1, 2}, 0, 0, 1, 0}, //
+ {1, {2}, 1, 63, 1, 0}, //
+ {1, {1}, 1, 63, 1, 0}, //
+ {1, {0}, 1, 63, 1, 0}, //
+};
+static constexpr size_t kNumScans4 = ARRAY_SIZE(kScanScript4);
+
+static constexpr jpeg_scan_info kScanScript5[] = {
+ {3, {0, 1, 2}, 0, 0, 0, 1}, //
+ {1, {0}, 1, 5, 0, 2}, //
+ {1, {1}, 1, 5, 0, 2}, //
+ {1, {2}, 1, 5, 0, 2}, //
+ {1, {1}, 6, 63, 0, 2}, //
+ {1, {2}, 6, 63, 0, 2}, //
+ {1, {0}, 6, 63, 0, 2}, //
+ {1, {0}, 1, 63, 2, 1}, //
+ {1, {1}, 1, 63, 2, 1}, //
+ {1, {2}, 1, 63, 2, 1}, //
+ {3, {0, 1, 2}, 0, 0, 1, 0}, //
+ {1, {0}, 1, 63, 1, 0}, //
+ {1, {1}, 1, 63, 1, 0}, //
+ {1, {2}, 1, 63, 1, 0}, //
+};
+static constexpr size_t kNumScans5 = ARRAY_SIZE(kScanScript5);
+
+// default progressive mode of jpegli
+static constexpr jpeg_scan_info kScanScript6[] = {
+ {3, {0, 1, 2}, 0, 0, 0, 0}, //
+ {1, {0}, 1, 2, 0, 0}, //
+ {1, {1}, 1, 2, 0, 0}, //
+ {1, {2}, 1, 2, 0, 0}, //
+ {1, {0}, 3, 63, 0, 2}, //
+ {1, {1}, 3, 63, 0, 2}, //
+ {1, {2}, 3, 63, 0, 2}, //
+ {1, {0}, 3, 63, 2, 1}, //
+ {1, {1}, 3, 63, 2, 1}, //
+ {1, {2}, 3, 63, 2, 1}, //
+ {1, {0}, 3, 63, 1, 0}, //
+ {1, {1}, 3, 63, 1, 0}, //
+ {1, {2}, 3, 63, 1, 0}, //
+};
+static constexpr size_t kNumScans6 = ARRAY_SIZE(kScanScript6);
+
+// Adapt RGB scan info to grayscale jpegs.
+void FilterScanComponents(const jpeg_compress_struct* cinfo,
+ jpeg_scan_info* si) {
+ const int all_comps_in_scan = si->comps_in_scan;
+ si->comps_in_scan = 0;
+ for (int j = 0; j < all_comps_in_scan; ++j) {
+ const int component = si->component_index[j];
+ if (component < cinfo->input_components) {
+ si->component_index[si->comps_in_scan++] = component;
+ }
+ }
+}
+
+Status SetJpegProgression(int progressive_id,
+ std::vector<jpeg_scan_info>* scan_infos,
+ jpeg_compress_struct* cinfo) {
+ if (progressive_id < 0) {
+ return true;
+ }
+ if (progressive_id == 0) {
+ jpeg_simple_progression(cinfo);
+ return true;
+ }
+ constexpr const jpeg_scan_info* kScanScripts[] = {kScanScript1, kScanScript2,
+ kScanScript3, kScanScript4,
+ kScanScript5, kScanScript6};
+ constexpr size_t kNumScans[] = {kNumScans1, kNumScans2, kNumScans3,
+ kNumScans4, kNumScans5, kNumScans6};
+ if (progressive_id > static_cast<int>(ARRAY_SIZE(kNumScans))) {
+ return JXL_FAILURE("Unknown jpeg scan script id %d", progressive_id);
+ }
+ const jpeg_scan_info* scan_script = kScanScripts[progressive_id - 1];
+ const size_t num_scans = kNumScans[progressive_id - 1];
+ // filter scan script for number of components
+ for (size_t i = 0; i < num_scans; ++i) {
+ jpeg_scan_info scan_info = scan_script[i];
+ FilterScanComponents(cinfo, &scan_info);
+ if (scan_info.comps_in_scan > 0) {
+ scan_infos->emplace_back(std::move(scan_info));
+ }
+ }
+ cinfo->scan_info = scan_infos->data();
+ cinfo->num_scans = scan_infos->size();
+ return true;
+}
+
bool IsSRGBEncoding(const JxlColorEncoding& c) {
return ((c.color_space == JXL_COLOR_SPACE_RGB ||
c.color_space == JXL_COLOR_SPACE_GRAY) &&
@@ -106,18 +250,37 @@ Status SetChromaSubsampling(const std::string& subsampling,
return false;
}
+struct JpegParams {
+ // Common between sjpeg and libjpeg
+ int quality = 100;
+ std::string chroma_subsampling = "444";
+ // Libjpeg parameters
+ int progressive_id = -1;
+ bool optimize_coding = true;
+ bool is_xyb = false;
+ // Sjpeg parameters
+ int libjpeg_quality = 0;
+ std::string libjpeg_chroma_subsampling = "444";
+ float psnr_target = 0;
+ std::string custom_base_quant_fn;
+ float search_q_start = 65.0f;
+ float search_q_min = 1.0f;
+ float search_q_max = 100.0f;
+ int search_max_iters = 20;
+ float search_tolerance = 0.1f;
+ float search_q_precision = 0.01f;
+ float search_first_iter_slope = 3.0f;
+ bool enable_adaptive_quant = true;
+};
+
Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info,
const std::vector<uint8_t>& icc,
- std::vector<uint8_t> exif, size_t quality,
- const std::string& chroma_subsampling,
+ std::vector<uint8_t> exif, const JpegParams& params,
std::vector<uint8_t>* bytes) {
if (BITS_IN_JSAMPLE != 8 || sizeof(JSAMPLE) != 1) {
return JXL_FAILURE("Only 8 bit JSAMPLE is supported.");
}
- jpeg_compress_struct cinfo;
- // cinfo is initialized by libjpeg, which we are not instrumenting with
- // msan.
- msan::UnpoisonMemory(&cinfo, sizeof(cinfo));
+ jpeg_compress_struct cinfo = {};
jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
@@ -129,11 +292,19 @@ Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info,
cinfo.input_components = info.num_color_channels;
cinfo.in_color_space = info.num_color_channels == 1 ? JCS_GRAYSCALE : JCS_RGB;
jpeg_set_defaults(&cinfo);
- cinfo.optimize_coding = TRUE;
+ cinfo.optimize_coding = params.optimize_coding;
if (cinfo.input_components == 3) {
- JXL_RETURN_IF_ERROR(SetChromaSubsampling(chroma_subsampling, &cinfo));
+ JXL_RETURN_IF_ERROR(
+ SetChromaSubsampling(params.chroma_subsampling, &cinfo));
}
- jpeg_set_quality(&cinfo, quality, TRUE);
+ if (params.is_xyb) {
+ // Tell libjpeg not to convert XYB data to YCbCr.
+ jpeg_set_colorspace(&cinfo, JCS_RGB);
+ }
+ jpeg_set_quality(&cinfo, params.quality, TRUE);
+ std::vector<jpeg_scan_info> scan_infos;
+ JXL_RETURN_IF_ERROR(
+ SetJpegProgression(params.progressive_id, &scan_infos, &cinfo));
jpeg_start_compress(&cinfo, TRUE);
if (!icc.empty()) {
WriteICCProfile(&cinfo, icc);
@@ -145,13 +316,39 @@ Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info,
if (cinfo.input_components > 3 || cinfo.input_components < 0)
return JXL_FAILURE("invalid numbers of components");
- std::vector<uint8_t> raw_bytes(image.pixels_size);
- memcpy(&raw_bytes[0], reinterpret_cast<const uint8_t*>(image.pixels()),
- image.pixels_size);
- for (size_t y = 0; y < info.ysize; ++y) {
- JSAMPROW row[] = {raw_bytes.data() + y * image.stride};
-
- jpeg_write_scanlines(&cinfo, row, 1);
+ std::vector<uint8_t> row_bytes(image.stride);
+ const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels());
+ if (cinfo.num_components == (int)image.format.num_channels &&
+ image.format.data_type == JXL_TYPE_UINT8) {
+ for (size_t y = 0; y < info.ysize; ++y) {
+ memcpy(&row_bytes[0], pixels + y * image.stride, image.stride);
+ JSAMPROW row[] = {row_bytes.data()};
+ jpeg_write_scanlines(&cinfo, row, 1);
+ }
+ } else if (image.format.data_type == JXL_TYPE_UINT8) {
+ for (size_t y = 0; y < info.ysize; ++y) {
+ const uint8_t* image_row = pixels + y * image.stride;
+ for (size_t x = 0; x < info.xsize; ++x) {
+ const uint8_t* image_pixel = image_row + x * image.pixel_stride();
+ memcpy(&row_bytes[x * cinfo.num_components], image_pixel,
+ cinfo.num_components);
+ }
+ JSAMPROW row[] = {row_bytes.data()};
+ jpeg_write_scanlines(&cinfo, row, 1);
+ }
+ } else {
+ for (size_t y = 0; y < info.ysize; ++y) {
+ const uint8_t* image_row = pixels + y * image.stride;
+ for (size_t x = 0; x < info.xsize; ++x) {
+ const uint8_t* image_pixel = image_row + x * image.pixel_stride();
+ for (int c = 0; c < cinfo.num_components; ++c) {
+ uint32_t val16 = (image_pixel[2 * c] << 8) + image_pixel[2 * c + 1];
+ row_bytes[x * cinfo.num_components + c] = (val16 + 128) / 257;
+ }
+ }
+ JSAMPROW row[] = {row_bytes.data()};
+ jpeg_write_scanlines(&cinfo, row, 1);
+ }
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
@@ -164,15 +361,93 @@ Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info,
return true;
}
+#if JPEGXL_ENABLE_SJPEG
+struct MySearchHook : public sjpeg::SearchHook {
+ uint8_t base_tables[2][64];
+ float q_start;
+ float q_precision;
+ float first_iter_slope;
+ void ReadBaseTables(const std::string& fn) {
+ const uint8_t kJPEGAnnexKMatrices[2][64] = {
+ {16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55,
+ 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62,
+ 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92,
+ 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99},
+ {17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99,
+ 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99}};
+ memcpy(base_tables[0], kJPEGAnnexKMatrices[0], sizeof(base_tables[0]));
+ memcpy(base_tables[1], kJPEGAnnexKMatrices[1], sizeof(base_tables[1]));
+ if (!fn.empty()) {
+ std::ifstream f(fn);
+ std::string line;
+ int idx = 0;
+ while (idx < 128 && std::getline(f, line)) {
+ if (line.empty() || line[0] == '#') continue;
+ std::istringstream line_stream(line);
+ std::string token;
+ while (idx < 128 && std::getline(line_stream, token, ',')) {
+ uint8_t val = std::stoi(token);
+ base_tables[idx / 64][idx % 64] = val;
+ idx++;
+ }
+ }
+ }
+ }
+ bool Setup(const sjpeg::EncoderParam& param) override {
+ sjpeg::SearchHook::Setup(param);
+ q = q_start;
+ return true;
+ }
+ void NextMatrix(int idx, uint8_t dst[64]) override {
+ float factor = (q <= 0) ? 5000.0f
+ : (q < 50.0f) ? 5000.0f / q
+ : (q < 100.0f) ? 2 * (100.0f - q)
+ : 0.0f;
+ sjpeg::SetQuantMatrix(base_tables[idx], factor, dst);
+ }
+ bool Update(float result) override {
+ value = result;
+ if (fabs(value - target) < tolerance * target) {
+ return true;
+ }
+ if (value > target) {
+ qmax = q;
+ } else {
+ qmin = q;
+ }
+ if (qmin == qmax) {
+ return true;
+ }
+ const float last_q = q;
+ if (pass == 0) {
+ q += first_iter_slope *
+ (for_size ? 0.1 * std::log(target / value) : (target - value));
+ q = std::max(qmin, std::min(qmax, q));
+ } else {
+ q = (qmin + qmax) / 2.;
+ }
+ return (pass > 0 && fabs(q - last_q) < q_precision);
+ }
+ ~MySearchHook() override {}
+};
+#endif
+
Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info,
const std::vector<uint8_t>& icc,
- std::vector<uint8_t> exif, size_t quality,
- const std::string& chroma_subsampling,
+ std::vector<uint8_t> exif, const JpegParams& params,
std::vector<uint8_t>* bytes) {
#if !JPEGXL_ENABLE_SJPEG
return JXL_FAILURE("JPEG XL was built without sjpeg support");
#else
- sjpeg::EncoderParam param(quality);
+ if (image.format.data_type != JXL_TYPE_UINT8) {
+ return JXL_FAILURE("Unsupported pixel data type");
+ }
+ if (info.alpha_bits > 0) {
+ return JXL_FAILURE("alpha is not supported");
+ }
+ sjpeg::EncoderParam param(params.quality);
if (!icc.empty()) {
param.iccp.assign(icc.begin(), icc.end());
}
@@ -180,13 +455,43 @@ Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info,
ResetExifOrientation(exif);
param.exif.assign(exif.begin(), exif.end());
}
- if (chroma_subsampling == "444") {
+ if (params.chroma_subsampling == "444") {
param.yuv_mode = SJPEG_YUV_444;
- } else if (chroma_subsampling == "420") {
+ } else if (params.chroma_subsampling == "420") {
+ param.yuv_mode = SJPEG_YUV_420;
+ } else if (params.chroma_subsampling == "420sharp") {
param.yuv_mode = SJPEG_YUV_SHARP;
} else {
return JXL_FAILURE("sjpeg does not support this chroma subsampling mode");
}
+ param.adaptive_quantization = params.enable_adaptive_quant;
+ std::unique_ptr<MySearchHook> hook;
+ if (params.libjpeg_quality > 0) {
+ JpegParams libjpeg_params;
+ libjpeg_params.quality = params.libjpeg_quality;
+ libjpeg_params.chroma_subsampling = params.libjpeg_chroma_subsampling;
+ std::vector<uint8_t> libjpeg_bytes;
+ JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(image, info, icc, exif,
+ libjpeg_params, &libjpeg_bytes));
+ param.target_mode = sjpeg::EncoderParam::TARGET_SIZE;
+ param.target_value = libjpeg_bytes.size();
+ }
+ if (params.psnr_target > 0) {
+ param.target_mode = sjpeg::EncoderParam::TARGET_PSNR;
+ param.target_value = params.psnr_target;
+ }
+ if (param.target_mode != sjpeg::EncoderParam::TARGET_NONE) {
+ param.passes = params.search_max_iters;
+ param.tolerance = params.search_tolerance;
+ param.qmin = params.search_q_min;
+ param.qmax = params.search_q_max;
+ hook.reset(new MySearchHook());
+ hook->ReadBaseTables(params.custom_base_quant_fn);
+ hook->q_start = params.search_q_start;
+ hook->q_precision = params.search_q_precision;
+ hook->first_iter_slope = params.search_first_iter_slope;
+ param.search_hook = hook.get();
+ }
size_t stride = info.xsize * 3;
const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels());
std::string output;
@@ -202,27 +507,20 @@ Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info,
Status EncodeImageJPG(const PackedImage& image, const JxlBasicInfo& info,
const std::vector<uint8_t>& icc,
std::vector<uint8_t> exif, JpegEncoder encoder,
- size_t quality, const std::string& chroma_subsampling,
- ThreadPool* pool, std::vector<uint8_t>* bytes) {
- if (image.format.data_type != JXL_TYPE_UINT8) {
- return JXL_FAILURE("Unsupported pixel data type");
- }
- if (info.alpha_bits > 0) {
- return JXL_FAILURE("alpha is not supported");
- }
- if (quality > 100) {
+ const JpegParams& params, ThreadPool* pool,
+ std::vector<uint8_t>* bytes) {
+ if (params.quality > 100) {
return JXL_FAILURE("please specify a 0-100 JPEG quality");
}
switch (encoder) {
case JpegEncoder::kLibJpeg:
- JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(image, info, icc, std::move(exif),
- quality, chroma_subsampling,
- bytes));
+ JXL_RETURN_IF_ERROR(
+ EncodeWithLibJpeg(image, info, icc, std::move(exif), params, bytes));
break;
case JpegEncoder::kSJpeg:
- JXL_RETURN_IF_ERROR(EncodeWithSJpeg(image, info, icc, std::move(exif),
- quality, chroma_subsampling, bytes));
+ JXL_RETURN_IF_ERROR(
+ EncodeWithSJpeg(image, info, icc, std::move(exif), params, bytes));
break;
default:
return JXL_FAILURE("tried to use an unknown JPEG encoder");
@@ -234,43 +532,72 @@ Status EncodeImageJPG(const PackedImage& image, const JxlBasicInfo& info,
class JPEGEncoder : public Encoder {
std::vector<JxlPixelFormat> AcceptedFormats() const override {
std::vector<JxlPixelFormat> formats;
- for (const uint32_t num_channels : {1, 3}) {
+ for (const uint32_t num_channels : {1, 2, 3, 4}) {
for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
/*data_type=*/JXL_TYPE_UINT8,
/*endianness=*/endianness,
/*align=*/0});
}
+ formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
+ /*data_type=*/JXL_TYPE_UINT16,
+ /*endianness=*/JXL_BIG_ENDIAN,
+ /*align=*/0});
}
return formats;
}
Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
ThreadPool* pool = nullptr) const override {
JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
- const auto& options = this->options();
- int quality = 100;
- auto it_quality = options.find("q");
- if (it_quality != options.end()) {
- std::istringstream is(it_quality->second);
- JXL_RETURN_IF_ERROR(static_cast<bool>(is >> quality));
- }
- std::string chroma_subsampling = "444";
- auto it_chroma_subsampling = options.find("chroma_subsampling");
- if (it_chroma_subsampling != options.end()) {
- chroma_subsampling = it_chroma_subsampling->second;
- }
JpegEncoder jpeg_encoder = JpegEncoder::kLibJpeg;
- auto it_encoder = options.find("jpeg_encoder");
- if (it_encoder != options.end()) {
- if (it_encoder->second == "libjpeg") {
- jpeg_encoder = JpegEncoder::kLibJpeg;
- } else if (it_encoder->second == "sjpeg") {
- jpeg_encoder = JpegEncoder::kSJpeg;
- } else {
- return JXL_FAILURE("unknown jpeg encoder \"%s\"",
- it_encoder->second.c_str());
+ JpegParams params;
+ for (const auto& it : options()) {
+ if (it.first == "q") {
+ std::istringstream is(it.second);
+ JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.quality));
+ } else if (it.first == "libjpeg_quality") {
+ std::istringstream is(it.second);
+ JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.libjpeg_quality));
+ } else if (it.first == "chroma_subsampling") {
+ params.chroma_subsampling = it.second;
+ } else if (it.first == "libjpeg_chroma_subsampling") {
+ params.libjpeg_chroma_subsampling = it.second;
+ } else if (it.first == "jpeg_encoder") {
+ if (it.second == "libjpeg") {
+ jpeg_encoder = JpegEncoder::kLibJpeg;
+ } else if (it.second == "sjpeg") {
+ jpeg_encoder = JpegEncoder::kSJpeg;
+ } else {
+ return JXL_FAILURE("unknown jpeg encoder \"%s\"", it.second.c_str());
+ }
+ } else if (it.first == "progressive") {
+ std::istringstream is(it.second);
+ JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.progressive_id));
+ } else if (it.first == "optimize" && it.second == "OFF") {
+ params.optimize_coding = false;
+ } else if (it.first == "adaptive_q" && it.second == "OFF") {
+ params.enable_adaptive_quant = false;
+ } else if (it.first == "psnr") {
+ params.psnr_target = std::stof(it.second);
+ } else if (it.first == "base_quant_fn") {
+ params.custom_base_quant_fn = it.second;
+ } else if (it.first == "search_q_start") {
+ params.search_q_start = std::stof(it.second);
+ } else if (it.first == "search_q_min") {
+ params.search_q_min = std::stof(it.second);
+ } else if (it.first == "search_q_max") {
+ params.search_q_max = std::stof(it.second);
+ } else if (it.first == "search_max_iters") {
+ params.search_max_iters = std::stoi(it.second);
+ } else if (it.first == "search_tolerance") {
+ params.search_tolerance = std::stof(it.second);
+ } else if (it.first == "search_q_precision") {
+ params.search_q_precision = std::stof(it.second);
+ } else if (it.first == "search_first_iter_slope") {
+ params.search_first_iter_slope = std::stof(it.second);
}
}
+ params.is_xyb = (ppf.color_encoding.color_space == JXL_COLOR_SPACE_XYB);
std::vector<uint8_t> icc;
if (!IsSRGBEncoding(ppf.color_encoding)) {
icc = ppf.icc;
@@ -281,17 +608,22 @@ class JPEGEncoder : public Encoder {
JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info));
encoded_image->bitstreams.emplace_back();
JXL_RETURN_IF_ERROR(EncodeImageJPG(
- frame.color, ppf.info, icc, ppf.metadata.exif, jpeg_encoder, quality,
- chroma_subsampling, pool, &encoded_image->bitstreams.back()));
+ frame.color, ppf.info, icc, ppf.metadata.exif, jpeg_encoder, params,
+ pool, &encoded_image->bitstreams.back()));
}
return true;
}
};
} // namespace
+#endif
std::unique_ptr<Encoder> GetJPEGEncoder() {
+#if JPEGXL_ENABLE_JPEG
return jxl::make_unique<JPEGEncoder>();
+#else
+ return nullptr;
+#endif
}
} // namespace extras
diff --git a/lib/extras/enc/jxl.cc b/lib/extras/enc/jxl.cc
new file mode 100644
index 0000000..054d15e
--- /dev/null
+++ b/lib/extras/enc/jxl.cc
@@ -0,0 +1,359 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/enc/jxl.h"
+
+#include <jxl/encode.h>
+#include <jxl/encode_cxx.h>
+
+#include "lib/jxl/exif.h"
+
+namespace jxl {
+namespace extras {
+
+JxlEncoderStatus SetOption(const JXLOption& opt,
+ JxlEncoderFrameSettings* settings) {
+ return opt.is_float
+ ? JxlEncoderFrameSettingsSetFloatOption(settings, opt.id, opt.fval)
+ : JxlEncoderFrameSettingsSetOption(settings, opt.id, opt.ival);
+}
+
+bool SetFrameOptions(const std::vector<JXLOption>& options, size_t frame_index,
+ size_t* option_idx, JxlEncoderFrameSettings* settings) {
+ while (*option_idx < options.size()) {
+ const auto& opt = options[*option_idx];
+ if (opt.frame_index > frame_index) {
+ break;
+ }
+ if (JXL_ENC_SUCCESS != SetOption(opt, settings)) {
+ fprintf(stderr, "Setting option id %d failed.\n", opt.id);
+ return false;
+ }
+ (*option_idx)++;
+ }
+ return true;
+}
+
+bool SetupFrame(JxlEncoder* enc, JxlEncoderFrameSettings* settings,
+ const JxlFrameHeader& frame_header,
+ const JXLCompressParams& params, const PackedPixelFile& ppf,
+ size_t frame_index, size_t num_alpha_channels,
+ size_t num_interleaved_alpha, size_t& option_idx) {
+ if (JXL_ENC_SUCCESS != JxlEncoderSetFrameHeader(settings, &frame_header)) {
+ fprintf(stderr, "JxlEncoderSetFrameHeader() failed.\n");
+ return false;
+ }
+ if (!SetFrameOptions(params.options, frame_index, &option_idx, settings)) {
+ return false;
+ }
+ if (num_alpha_channels > 0) {
+ JxlExtraChannelInfo extra_channel_info;
+ JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &extra_channel_info);
+ extra_channel_info.bits_per_sample = ppf.info.alpha_bits;
+ extra_channel_info.exponent_bits_per_sample = ppf.info.alpha_exponent_bits;
+ if (params.premultiply != -1) {
+ if (params.premultiply != 0 && params.premultiply != 1) {
+ fprintf(stderr, "premultiply must be one of: -1, 0, 1.\n");
+ return false;
+ }
+ extra_channel_info.alpha_premultiplied = params.premultiply;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetExtraChannelInfo(enc, 0, &extra_channel_info)) {
+ fprintf(stderr, "JxlEncoderSetExtraChannelInfo() failed.\n");
+ return false;
+ }
+ // We take the extra channel blend info frame_info, but don't do
+ // clamping.
+ JxlBlendInfo extra_channel_blend_info = frame_header.layer_info.blend_info;
+ extra_channel_blend_info.clamp = JXL_FALSE;
+ JxlEncoderSetExtraChannelBlendInfo(settings, 0, &extra_channel_blend_info);
+ }
+ // Add extra channel info for the rest of the extra channels.
+ for (size_t i = 0; i < ppf.info.num_extra_channels; ++i) {
+ if (i < ppf.extra_channels_info.size()) {
+ const auto& ec_info = ppf.extra_channels_info[i].ec_info;
+ if (JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelInfo(
+ enc, num_interleaved_alpha + i, &ec_info)) {
+ fprintf(stderr, "JxlEncoderSetExtraChannelInfo() failed.\n");
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool ReadCompressedOutput(JxlEncoder* enc, std::vector<uint8_t>* compressed) {
+ compressed->clear();
+ compressed->resize(4096);
+ uint8_t* next_out = compressed->data();
+ size_t avail_out = compressed->size() - (next_out - compressed->data());
+ JxlEncoderStatus result = JXL_ENC_NEED_MORE_OUTPUT;
+ while (result == JXL_ENC_NEED_MORE_OUTPUT) {
+ result = JxlEncoderProcessOutput(enc, &next_out, &avail_out);
+ if (result == JXL_ENC_NEED_MORE_OUTPUT) {
+ size_t offset = next_out - compressed->data();
+ compressed->resize(compressed->size() * 2);
+ next_out = compressed->data() + offset;
+ avail_out = compressed->size() - offset;
+ }
+ }
+ compressed->resize(next_out - compressed->data());
+ if (result != JXL_ENC_SUCCESS) {
+ fprintf(stderr, "JxlEncoderProcessOutput failed.\n");
+ return false;
+ }
+ return true;
+}
+
+bool EncodeImageJXL(const JXLCompressParams& params, const PackedPixelFile& ppf,
+ const std::vector<uint8_t>* jpeg_bytes,
+ std::vector<uint8_t>* compressed) {
+ auto encoder = JxlEncoderMake(/*memory_manager=*/nullptr);
+ JxlEncoder* enc = encoder.get();
+
+ if (params.allow_expert_options) {
+ JxlEncoderAllowExpertOptions(enc);
+ }
+
+ if (params.runner_opaque != nullptr &&
+ JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc, params.runner,
+ params.runner_opaque)) {
+ fprintf(stderr, "JxlEncoderSetParallelRunner failed\n");
+ return false;
+ }
+
+ if (params.HasOutputProcessor() &&
+ JXL_ENC_SUCCESS !=
+ JxlEncoderSetOutputProcessor(enc, params.output_processor)) {
+ fprintf(stderr, "JxlEncoderSetOutputProcessorfailed\n");
+ return false;
+ }
+
+ auto settings = JxlEncoderFrameSettingsCreate(enc, nullptr);
+ size_t option_idx = 0;
+ if (!SetFrameOptions(params.options, 0, &option_idx, settings)) {
+ return false;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetFrameDistance(settings, params.distance)) {
+ fprintf(stderr, "Setting frame distance failed.\n");
+ return false;
+ }
+ if (params.debug_image) {
+ JxlEncoderSetDebugImageCallback(settings, params.debug_image,
+ params.debug_image_opaque);
+ }
+ if (params.stats) {
+ JxlEncoderCollectStats(settings, params.stats);
+ }
+
+ bool use_boxes = !ppf.metadata.exif.empty() || !ppf.metadata.xmp.empty() ||
+ !ppf.metadata.jumbf.empty() || !ppf.metadata.iptc.empty();
+ bool use_container = params.use_container || use_boxes ||
+ (jpeg_bytes && params.jpeg_store_metadata);
+
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderUseContainer(enc, static_cast<int>(use_container))) {
+ fprintf(stderr, "JxlEncoderUseContainer failed.\n");
+ return false;
+ }
+
+ if (jpeg_bytes) {
+ if (params.jpeg_store_metadata &&
+ JXL_ENC_SUCCESS != JxlEncoderStoreJPEGMetadata(enc, JXL_TRUE)) {
+ fprintf(stderr, "Storing JPEG metadata failed.\n");
+ return false;
+ }
+ if (!params.jpeg_store_metadata && params.jpeg_strip_exif) {
+ JxlEncoderFrameSettingsSetOption(settings,
+ JXL_ENC_FRAME_SETTING_JPEG_KEEP_EXIF, 0);
+ }
+ if (!params.jpeg_store_metadata && params.jpeg_strip_xmp) {
+ JxlEncoderFrameSettingsSetOption(settings,
+ JXL_ENC_FRAME_SETTING_JPEG_KEEP_XMP, 0);
+ }
+ if (params.jpeg_strip_jumbf) {
+ JxlEncoderFrameSettingsSetOption(
+ settings, JXL_ENC_FRAME_SETTING_JPEG_KEEP_JUMBF, 0);
+ }
+ if (JXL_ENC_SUCCESS != JxlEncoderAddJPEGFrame(settings, jpeg_bytes->data(),
+ jpeg_bytes->size())) {
+ JxlEncoderError error = JxlEncoderGetError(enc);
+ if (error == JXL_ENC_ERR_BAD_INPUT) {
+ fprintf(stderr,
+ "Error while decoding the JPEG image. It may be corrupt (e.g. "
+ "truncated) or of an unsupported type (e.g. CMYK).\n");
+ } else if (error == JXL_ENC_ERR_JBRD) {
+ fprintf(stderr,
+ "JPEG bitstream reconstruction data could not be created. "
+ "Possibly there is too much tail data.\n"
+ "Try using --jpeg_store_metadata 0, to losslessly "
+ "recompress the JPEG image data without bitstream "
+ "reconstruction data.\n");
+ } else {
+ fprintf(stderr, "JxlEncoderAddJPEGFrame() failed.\n");
+ }
+ return false;
+ }
+ } else {
+ size_t num_alpha_channels = 0; // Adjusted below.
+ JxlBasicInfo basic_info = ppf.info;
+ basic_info.xsize *= params.already_downsampled;
+ basic_info.ysize *= params.already_downsampled;
+ if (basic_info.alpha_bits > 0) num_alpha_channels = 1;
+ if (params.intensity_target > 0) {
+ basic_info.intensity_target = params.intensity_target;
+ }
+ basic_info.num_extra_channels =
+ std::max<uint32_t>(num_alpha_channels, ppf.info.num_extra_channels);
+ basic_info.num_color_channels = ppf.info.num_color_channels;
+ const bool lossless = params.distance == 0;
+ basic_info.uses_original_profile = lossless;
+ if (params.override_bitdepth != 0) {
+ basic_info.bits_per_sample = params.override_bitdepth;
+ basic_info.exponent_bits_per_sample =
+ params.override_bitdepth == 32 ? 8 : 0;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetCodestreamLevel(enc, params.codestream_level)) {
+ fprintf(stderr, "Setting --codestream_level failed.\n");
+ return false;
+ }
+ if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc, &basic_info)) {
+ fprintf(stderr, "JxlEncoderSetBasicInfo() failed.\n");
+ return false;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetUpsamplingMode(enc, params.already_downsampled,
+ params.upsampling_mode)) {
+ fprintf(stderr, "JxlEncoderSetUpsamplingMode() failed.\n");
+ return false;
+ }
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetFrameBitDepth(settings, &params.input_bitdepth)) {
+ fprintf(stderr, "JxlEncoderSetFrameBitDepth() failed.\n");
+ return false;
+ }
+ if (num_alpha_channels != 0 &&
+ JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelDistance(
+ settings, 0, params.alpha_distance)) {
+ fprintf(stderr, "Setting alpha distance failed.\n");
+ return false;
+ }
+ if (lossless &&
+ JXL_ENC_SUCCESS != JxlEncoderSetFrameLossless(settings, JXL_TRUE)) {
+ fprintf(stderr, "JxlEncoderSetFrameLossless() failed.\n");
+ return false;
+ }
+ if (!ppf.icc.empty()) {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetICCProfile(enc, ppf.icc.data(), ppf.icc.size())) {
+ fprintf(stderr, "JxlEncoderSetICCProfile() failed.\n");
+ return false;
+ }
+ } else {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetColorEncoding(enc, &ppf.color_encoding)) {
+ fprintf(stderr, "JxlEncoderSetColorEncoding() failed.\n");
+ return false;
+ }
+ }
+
+ if (use_boxes) {
+ if (JXL_ENC_SUCCESS != JxlEncoderUseBoxes(enc)) {
+ fprintf(stderr, "JxlEncoderUseBoxes() failed.\n");
+ return false;
+ }
+ // Prepend 4 zero bytes to exif for tiff header offset
+ std::vector<uint8_t> exif_with_offset;
+ bool bigendian;
+ if (IsExif(ppf.metadata.exif, &bigendian)) {
+ exif_with_offset.resize(ppf.metadata.exif.size() + 4);
+ memcpy(exif_with_offset.data() + 4, ppf.metadata.exif.data(),
+ ppf.metadata.exif.size());
+ }
+ const struct BoxInfo {
+ const char* type;
+ const std::vector<uint8_t>& bytes;
+ } boxes[] = {
+ {"Exif", exif_with_offset},
+ {"xml ", ppf.metadata.xmp},
+ {"jumb", ppf.metadata.jumbf},
+ {"xml ", ppf.metadata.iptc},
+ };
+ for (size_t i = 0; i < sizeof boxes / sizeof *boxes; ++i) {
+ const BoxInfo& box = boxes[i];
+ if (!box.bytes.empty() &&
+ JXL_ENC_SUCCESS != JxlEncoderAddBox(enc, box.type, box.bytes.data(),
+ box.bytes.size(),
+ params.compress_boxes)) {
+ fprintf(stderr, "JxlEncoderAddBox() failed (%s).\n", box.type);
+ return false;
+ }
+ }
+ JxlEncoderCloseBoxes(enc);
+ }
+
+ for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) {
+ const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame];
+ const jxl::extras::PackedImage& pimage = pframe.color;
+ JxlPixelFormat ppixelformat = pimage.format;
+ size_t num_interleaved_alpha =
+ (ppixelformat.num_channels - ppf.info.num_color_channels);
+ if (!SetupFrame(enc, settings, pframe.frame_info, params, ppf, num_frame,
+ num_alpha_channels, num_interleaved_alpha, option_idx)) {
+ return false;
+ }
+ if (JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(settings, &ppixelformat,
+ pimage.pixels(),
+ pimage.pixels_size)) {
+ fprintf(stderr, "JxlEncoderAddImageFrame() failed.\n");
+ return false;
+ }
+ // Only set extra channel buffer if it is provided non-interleaved.
+ for (size_t i = 0; i < pframe.extra_channels.size(); ++i) {
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetExtraChannelBuffer(settings, &ppixelformat,
+ pframe.extra_channels[i].pixels(),
+ pframe.extra_channels[i].stride *
+ pframe.extra_channels[i].ysize,
+ num_interleaved_alpha + i)) {
+ fprintf(stderr, "JxlEncoderSetExtraChannelBuffer() failed.\n");
+ return false;
+ }
+ }
+ }
+ for (size_t fi = 0; fi < ppf.chunked_frames.size(); ++fi) {
+ ChunkedPackedFrame& chunked_frame = ppf.chunked_frames[fi];
+ size_t num_interleaved_alpha =
+ (chunked_frame.format.num_channels - ppf.info.num_color_channels);
+ if (!SetupFrame(enc, settings, chunked_frame.frame_info, params, ppf, fi,
+ num_alpha_channels, num_interleaved_alpha, option_idx)) {
+ return false;
+ }
+ const bool last_frame = fi + 1 == ppf.chunked_frames.size();
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderAddChunkedFrame(settings, last_frame,
+ chunked_frame.GetInputSource())) {
+ fprintf(stderr, "JxlEncoderAddChunkedFrame() failed.\n");
+ return false;
+ }
+ }
+ }
+ JxlEncoderCloseInput(enc);
+ if (params.HasOutputProcessor()) {
+ if (JXL_ENC_SUCCESS != JxlEncoderFlushInput(enc)) {
+ fprintf(stderr, "JxlEncoderAddChunkedFrame() failed.\n");
+ return false;
+ }
+ } else if (!ReadCompressedOutput(enc, compressed)) {
+ return false;
+ }
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl
diff --git a/lib/extras/enc/jxl.h b/lib/extras/enc/jxl.h
new file mode 100644
index 0000000..b8ca5bd
--- /dev/null
+++ b/lib/extras/enc/jxl.h
@@ -0,0 +1,91 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_ENC_JXL_H_
+#define LIB_EXTRAS_ENC_JXL_H_
+
+#include <jxl/encode.h>
+#include <jxl/parallel_runner.h>
+#include <jxl/thread_parallel_runner.h>
+#include <jxl/types.h>
+#include <stdint.h>
+
+#include <vector>
+
+#include "lib/extras/packed_image.h"
+
+namespace jxl {
+namespace extras {
+
+struct JXLOption {
+ JXLOption(JxlEncoderFrameSettingId id, int64_t val, size_t frame_index)
+ : id(id), is_float(false), ival(val), frame_index(frame_index) {}
+ JXLOption(JxlEncoderFrameSettingId id, float val, size_t frame_index)
+ : id(id), is_float(true), fval(val), frame_index(frame_index) {}
+
+ JxlEncoderFrameSettingId id;
+ bool is_float;
+ union {
+ int64_t ival;
+ float fval;
+ };
+ size_t frame_index;
+};
+
+struct JXLCompressParams {
+ std::vector<JXLOption> options;
+ // Target butteraugli distance, 0.0 means lossless.
+ float distance = 1.0f;
+ float alpha_distance = 1.0f;
+ // If set to true, forces container mode.
+ bool use_container = false;
+ // Whether to enable/disable byte-exact jpeg reconstruction for jpeg inputs.
+ bool jpeg_store_metadata = true;
+ bool jpeg_strip_exif = false;
+ bool jpeg_strip_xmp = false;
+ bool jpeg_strip_jumbf = false;
+ // Whether to create brob boxes.
+ bool compress_boxes = true;
+ // Upper bound on the intensity level present in the image in nits (zero means
+ // that the library chooses a default).
+ float intensity_target = 0;
+ int already_downsampled = 1;
+ int upsampling_mode = -1;
+ // Overrides for bitdepth, codestream level and alpha premultiply.
+ size_t override_bitdepth = 0;
+ int32_t codestream_level = -1;
+ int32_t premultiply = -1;
+ // Override input buffer interpretation.
+ JxlBitDepth input_bitdepth = {JXL_BIT_DEPTH_FROM_PIXEL_FORMAT, 0, 0};
+ // If runner_opaque is set, the decoder uses this parallel runner.
+ JxlParallelRunner runner = JxlThreadParallelRunner;
+ void* runner_opaque = nullptr;
+ JxlEncoderOutputProcessor output_processor = {};
+ JxlDebugImageCallback debug_image = nullptr;
+ void* debug_image_opaque = nullptr;
+ JxlEncoderStats* stats = nullptr;
+ bool allow_expert_options = false;
+
+ void AddOption(JxlEncoderFrameSettingId id, int64_t val) {
+ options.emplace_back(JXLOption(id, val, 0));
+ }
+ void AddFloatOption(JxlEncoderFrameSettingId id, float val) {
+ options.emplace_back(JXLOption(id, val, 0));
+ }
+ bool HasOutputProcessor() const {
+ return (output_processor.get_buffer != nullptr &&
+ output_processor.release_buffer != nullptr &&
+ output_processor.set_finalized_position != nullptr);
+ }
+};
+
+bool EncodeImageJXL(const JXLCompressParams& params, const PackedPixelFile& ppf,
+ const std::vector<uint8_t>* jpeg_bytes,
+ std::vector<uint8_t>* compressed);
+
+} // namespace extras
+} // namespace jxl
+
+#endif // LIB_EXTRAS_ENC_JXL_H_
diff --git a/lib/extras/enc/npy.cc b/lib/extras/enc/npy.cc
index 1428e64..ae8cf13 100644
--- a/lib/extras/enc/npy.cc
+++ b/lib/extras/enc/npy.cc
@@ -5,13 +5,12 @@
#include "lib/extras/enc/npy.h"
-#include <stdio.h>
+#include <jxl/types.h>
#include <sstream>
#include <string>
#include <vector>
-#include "jxl/types.h"
#include "lib/extras/packed_image.h"
namespace jxl {
diff --git a/lib/extras/enc/pgx.cc b/lib/extras/enc/pgx.cc
index ef204ad..d4809e3 100644
--- a/lib/extras/enc/pgx.cc
+++ b/lib/extras/enc/pgx.cc
@@ -5,13 +5,11 @@
#include "lib/extras/enc/pgx.h"
-#include <stdio.h>
+#include <jxl/codestream_header.h>
#include <string.h>
-#include "jxl/codestream_header.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/byte_order.h"
-#include "lib/jxl/base/printf_macros.h"
namespace jxl {
namespace extras {
diff --git a/lib/extras/enc/pnm.cc b/lib/extras/enc/pnm.cc
index 9b5f6cb..4183900 100644
--- a/lib/extras/enc/pnm.cc
+++ b/lib/extras/enc/pnm.cc
@@ -5,7 +5,6 @@
#include "lib/extras/enc/pnm.h"
-#include <stdio.h>
#include <string.h>
#include <string>
@@ -14,12 +13,9 @@
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/byte_order.h"
#include "lib/jxl/base/compiler_specific.h"
-#include "lib/jxl/base/file_io.h"
#include "lib/jxl/base/printf_macros.h"
#include "lib/jxl/base/status.h"
-#include "lib/jxl/color_management.h"
#include "lib/jxl/dec_external_image.h"
-#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_external_image.h"
#include "lib/jxl/enc_image_bundle.h"
#include "lib/jxl/fields.h" // AllDefault
@@ -32,66 +28,7 @@ namespace {
constexpr size_t kMaxHeaderSize = 200;
-Status EncodeHeader(const PackedImage& image, size_t bits_per_sample,
- bool little_endian, char* header, int* chars_written) {
- size_t num_channels = image.format.num_channels;
- bool is_gray = num_channels <= 2;
- bool has_alpha = num_channels == 2 || num_channels == 4;
- if (has_alpha) { // PAM
- if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits");
- const uint32_t max_val = (1U << bits_per_sample) - 1;
- *chars_written =
- snprintf(header, kMaxHeaderSize,
- "P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS
- "\nDEPTH %u\nMAXVAL %u\nTUPLTYPE %s\nENDHDR\n",
- image.xsize, image.ysize, is_gray ? 2 : 4, max_val,
- is_gray ? "GRAYSCALE_ALPHA" : "RGB_ALPHA");
- JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) <
- kMaxHeaderSize);
- } else if (bits_per_sample == 32) { // PFM
- const char type = is_gray ? 'f' : 'F';
- const double scale = little_endian ? -1.0 : 1.0;
- *chars_written =
- snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n",
- type, image.xsize, image.ysize, scale);
- JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) <
- kMaxHeaderSize);
- } else { // PGM/PPM
- if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits");
- const uint32_t max_val = (1U << bits_per_sample) - 1;
- const char type = is_gray ? '5' : '6';
- *chars_written =
- snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n",
- type, image.xsize, image.ysize, max_val);
- JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) <
- kMaxHeaderSize);
- }
- return true;
-}
-
-Status EncodeImagePNM(const PackedImage& image, size_t bits_per_sample,
- std::vector<uint8_t>* bytes) {
- // Choose native for PFM; PGM/PPM require big-endian
- bool is_little_endian = bits_per_sample > 16 && IsLittleEndian();
- char header[kMaxHeaderSize];
- int header_size = 0;
- JXL_RETURN_IF_ERROR(EncodeHeader(image, bits_per_sample, is_little_endian,
- header, &header_size));
- bytes->resize(static_cast<size_t>(header_size) + image.pixels_size);
- memcpy(bytes->data(), header, static_cast<size_t>(header_size));
- const bool flipped_y = bits_per_sample == 32; // PFMs are flipped
- const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels());
- uint8_t* out = bytes->data() + header_size;
- for (size_t y = 0; y < image.ysize; ++y) {
- size_t y_out = flipped_y ? image.ysize - 1 - y : y;
- const uint8_t* row_in = &in[y * image.stride];
- uint8_t* row_out = &out[y_out * image.stride];
- memcpy(row_out, row_in, image.stride);
- }
- return true;
-}
-
-class PNMEncoder : public Encoder {
+class BasePNMEncoder : public Encoder {
public:
Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
ThreadPool* pool = nullptr) const override {
@@ -106,8 +43,8 @@ class PNMEncoder : public Encoder {
for (const auto& frame : ppf.frames) {
JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info));
encoded_image->bitstreams.emplace_back();
- JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.color, ppf.info.bits_per_sample,
- &encoded_image->bitstreams.back()));
+ JXL_RETURN_IF_ERROR(
+ EncodeFrame(ppf, frame, &encoded_image->bitstreams.back()));
}
for (size_t i = 0; i < ppf.extra_channels_info.size(); ++i) {
const auto& ec_info = ppf.extra_channels_info[i].ec_info;
@@ -115,85 +52,258 @@ class PNMEncoder : public Encoder {
auto& ec_bitstreams = encoded_image->extra_channel_bitstreams.back();
for (const auto& frame : ppf.frames) {
ec_bitstreams.emplace_back();
- JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.extra_channels[i],
- ec_info.bits_per_sample,
- &ec_bitstreams.back()));
+ JXL_RETURN_IF_ERROR(EncodeExtraChannel(frame.extra_channels[i],
+ ec_info.bits_per_sample,
+ &ec_bitstreams.back()));
}
}
return true;
}
+
+ protected:
+ virtual Status EncodeFrame(const PackedPixelFile& ppf,
+ const PackedFrame& frame,
+ std::vector<uint8_t>* bytes) const = 0;
+ virtual Status EncodeExtraChannel(const PackedImage& image,
+ size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const = 0;
};
+class PNMEncoder : public BasePNMEncoder {
+ public:
+ static const std::vector<JxlPixelFormat> kAcceptedFormats;
+
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ return kAcceptedFormats;
+ }
+
+ Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame,
+ std::vector<uint8_t>* bytes) const override {
+ return EncodeImage(frame.color, ppf.info.bits_per_sample, bytes);
+ }
+ Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const override {
+ return EncodeImage(image, bits_per_sample, bytes);
+ }
+
+ private:
+ Status EncodeImage(const PackedImage& image, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const {
+ uint32_t maxval = (1u << bits_per_sample) - 1;
+ char type = image.format.num_channels == 1 ? '5' : '6';
+ char header[kMaxHeaderSize];
+ size_t header_size =
+ snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n",
+ type, image.xsize, image.ysize, maxval);
+ JXL_RETURN_IF_ERROR(header_size < kMaxHeaderSize);
+ bytes->resize(header_size + image.pixels_size);
+ memcpy(bytes->data(), header, header_size);
+ memcpy(bytes->data() + header_size,
+ reinterpret_cast<uint8_t*>(image.pixels()), image.pixels_size);
+ return true;
+ }
+};
+
+class PGMEncoder : public PNMEncoder {
+ public:
+ static const std::vector<JxlPixelFormat> kAcceptedFormats;
+
+ std::vector<JxlPixelFormat> AcceptedFormats() const override {
+ return kAcceptedFormats;
+ }
+};
+
+const std::vector<JxlPixelFormat> PGMEncoder::kAcceptedFormats = {
+ JxlPixelFormat{1, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0},
+ JxlPixelFormat{1, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}};
+
class PPMEncoder : public PNMEncoder {
public:
+ static const std::vector<JxlPixelFormat> kAcceptedFormats;
+
std::vector<JxlPixelFormat> AcceptedFormats() const override {
- std::vector<JxlPixelFormat> formats;
- for (const uint32_t num_channels : {1, 2, 3, 4}) {
- for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) {
- for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
- formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
- /*data_type=*/data_type,
- /*endianness=*/endianness,
- /*align=*/0});
- }
- }
- }
- return formats;
+ return kAcceptedFormats;
}
};
-class PFMEncoder : public PNMEncoder {
+const std::vector<JxlPixelFormat> PPMEncoder::kAcceptedFormats = {
+ JxlPixelFormat{3, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0},
+ JxlPixelFormat{3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}};
+
+const std::vector<JxlPixelFormat> PNMEncoder::kAcceptedFormats = [] {
+ std::vector<JxlPixelFormat> combined = PPMEncoder::kAcceptedFormats;
+ combined.insert(combined.end(), PGMEncoder::kAcceptedFormats.begin(),
+ PGMEncoder::kAcceptedFormats.end());
+ return combined;
+}();
+
+class PFMEncoder : public BasePNMEncoder {
public:
std::vector<JxlPixelFormat> AcceptedFormats() const override {
std::vector<JxlPixelFormat> formats;
for (const uint32_t num_channels : {1, 3}) {
- for (const JxlDataType data_type : {JXL_TYPE_FLOAT16, JXL_TYPE_FLOAT}) {
- for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
- formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
- /*data_type=*/data_type,
- /*endianness=*/endianness,
- /*align=*/0});
- }
+ for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
+ formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
+ /*data_type=*/JXL_TYPE_FLOAT,
+ /*endianness=*/endianness,
+ /*align=*/0});
}
}
return formats;
}
-};
+ Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame,
+ std::vector<uint8_t>* bytes) const override {
+ return EncodeImage(frame.color, bytes);
+ }
+ Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const override {
+ return EncodeImage(image, bytes);
+ }
-class PGMEncoder : public PPMEncoder {
- public:
- std::vector<JxlPixelFormat> AcceptedFormats() const override {
- std::vector<JxlPixelFormat> formats = PPMEncoder::AcceptedFormats();
- for (auto it = formats.begin(); it != formats.end();) {
- if (it->num_channels > 2) {
- it = formats.erase(it);
- } else {
- ++it;
- }
+ private:
+ Status EncodeImage(const PackedImage& image,
+ std::vector<uint8_t>* bytes) const {
+ char type = image.format.num_channels == 1 ? 'f' : 'F';
+ double scale = image.format.endianness == JXL_LITTLE_ENDIAN ? -1.0 : 1.0;
+ char header[kMaxHeaderSize];
+ size_t header_size =
+ snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n",
+ type, image.xsize, image.ysize, scale);
+ JXL_RETURN_IF_ERROR(header_size < kMaxHeaderSize);
+ bytes->resize(header_size + image.pixels_size);
+ memcpy(bytes->data(), header, header_size);
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels());
+ uint8_t* out = bytes->data() + header_size;
+ for (size_t y = 0; y < image.ysize; ++y) {
+ size_t y_out = image.ysize - 1 - y;
+ const uint8_t* row_in = &in[y * image.stride];
+ uint8_t* row_out = &out[y_out * image.stride];
+ memcpy(row_out, row_in, image.stride);
}
- return formats;
+ return true;
}
};
-class PAMEncoder : public PPMEncoder {
+class PAMEncoder : public BasePNMEncoder {
public:
std::vector<JxlPixelFormat> AcceptedFormats() const override {
- std::vector<JxlPixelFormat> formats = PPMEncoder::AcceptedFormats();
- for (auto it = formats.begin(); it != formats.end();) {
- if (it->num_channels != 2 && it->num_channels != 4) {
- it = formats.erase(it);
- } else {
- ++it;
+ std::vector<JxlPixelFormat> formats;
+ for (const uint32_t num_channels : {1, 2, 3, 4}) {
+ for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) {
+ formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels,
+ /*data_type=*/data_type,
+ /*endianness=*/JXL_BIG_ENDIAN,
+ /*align=*/0});
}
}
return formats;
}
-};
+ Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame,
+ std::vector<uint8_t>* bytes) const override {
+ const PackedImage& color = frame.color;
+ const auto& ec_info = ppf.extra_channels_info;
+ JXL_RETURN_IF_ERROR(frame.extra_channels.size() == ec_info.size());
+ for (const auto& ec : frame.extra_channels) {
+ if (ec.xsize != color.xsize || ec.ysize != color.ysize) {
+ return JXL_FAILURE("Extra channel and color size mismatch.");
+ }
+ if (ec.format.data_type != color.format.data_type ||
+ ec.format.endianness != color.format.endianness) {
+ return JXL_FAILURE("Extra channel and color format mismatch.");
+ }
+ }
+ if (ppf.info.alpha_bits &&
+ (ppf.info.bits_per_sample != ppf.info.alpha_bits)) {
+ return JXL_FAILURE("Alpha bit depth does not match image bit depth");
+ }
+ for (const auto& it : ec_info) {
+ if (it.ec_info.bits_per_sample != ppf.info.bits_per_sample) {
+ return JXL_FAILURE(
+ "Extra channel bit depth does not match image bit depth");
+ }
+ }
+ const char* kColorTypes[4] = {"GRAYSCALE", "GRAYSCALE_ALPHA", "RGB",
+ "RGB_ALPHA"};
+ uint32_t maxval = (1u << ppf.info.bits_per_sample) - 1;
+ uint32_t depth = color.format.num_channels + ec_info.size();
+ char header[kMaxHeaderSize];
+ size_t pos = 0;
+ pos += snprintf(header + pos, kMaxHeaderSize - pos,
+ "P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS
+ "\nDEPTH %u\n"
+ "MAXVAL %u\nTUPLTYPE %s\n",
+ color.xsize, color.ysize, depth, maxval,
+ kColorTypes[color.format.num_channels - 1]);
+ JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize);
+ for (const auto& info : ec_info) {
+ pos += snprintf(header + pos, kMaxHeaderSize - pos, "TUPLTYPE %s\n",
+ ExtraChannelTypeName(info.ec_info.type).c_str());
+ JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize);
+ }
+ pos += snprintf(header + pos, kMaxHeaderSize - pos, "ENDHDR\n");
+ JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize);
+ size_t total_size = color.pixels_size;
+ for (const auto& ec : frame.extra_channels) {
+ total_size += ec.pixels_size;
+ }
+ bytes->resize(pos + total_size);
+ memcpy(bytes->data(), header, pos);
+ // If we have no extra channels, just copy color pixel data over.
+ if (frame.extra_channels.empty()) {
+ memcpy(bytes->data() + pos, reinterpret_cast<uint8_t*>(color.pixels()),
+ color.pixels_size);
+ return true;
+ }
+ // Interleave color and extra channels.
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels());
+ std::vector<const uint8_t*> ec_in(frame.extra_channels.size());
+ for (size_t i = 0; i < frame.extra_channels.size(); ++i) {
+ ec_in[i] =
+ reinterpret_cast<const uint8_t*>(frame.extra_channels[i].pixels());
+ }
+ uint8_t* out = bytes->data() + pos;
+ size_t pwidth = PackedImage::BitsPerChannel(color.format.data_type) / 8;
+ for (size_t y = 0; y < color.ysize; ++y) {
+ for (size_t x = 0; x < color.xsize; ++x) {
+ memcpy(out, in, color.pixel_stride());
+ out += color.pixel_stride();
+ in += color.pixel_stride();
+ for (auto& p : ec_in) {
+ memcpy(out, p, pwidth);
+ out += pwidth;
+ p += pwidth;
+ }
+ }
+ }
+ return true;
+ }
+ Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample,
+ std::vector<uint8_t>* bytes) const override {
+ return true;
+ }
-Span<const uint8_t> MakeSpan(const char* str) {
- return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str),
- strlen(str));
-}
+ private:
+ static std::string ExtraChannelTypeName(JxlExtraChannelType type) {
+ switch (type) {
+ case JXL_CHANNEL_ALPHA:
+ return std::string("Alpha");
+ case JXL_CHANNEL_DEPTH:
+ return std::string("Depth");
+ case JXL_CHANNEL_SPOT_COLOR:
+ return std::string("SpotColor");
+ case JXL_CHANNEL_SELECTION_MASK:
+ return std::string("SelectionMask");
+ case JXL_CHANNEL_BLACK:
+ return std::string("Black");
+ case JXL_CHANNEL_CFA:
+ return std::string("CFA");
+ case JXL_CHANNEL_THERMAL:
+ return std::string("Thermal");
+ default:
+ return std::string("UNKNOWN");
+ }
+ }
+};
} // namespace
@@ -201,6 +311,10 @@ std::unique_ptr<Encoder> GetPPMEncoder() {
return jxl::make_unique<PPMEncoder>();
}
+std::unique_ptr<Encoder> GetPNMEncoder() {
+ return jxl::make_unique<PNMEncoder>();
+}
+
std::unique_ptr<Encoder> GetPFMEncoder() {
return jxl::make_unique<PFMEncoder>();
}
diff --git a/lib/extras/enc/pnm.h b/lib/extras/enc/pnm.h
index 403208c..1e0020c 100644
--- a/lib/extras/enc/pnm.h
+++ b/lib/extras/enc/pnm.h
@@ -19,6 +19,7 @@ namespace extras {
std::unique_ptr<Encoder> GetPAMEncoder();
std::unique_ptr<Encoder> GetPGMEncoder();
+std::unique_ptr<Encoder> GetPNMEncoder();
std::unique_ptr<Encoder> GetPPMEncoder();
std::unique_ptr<Encoder> GetPFMEncoder();
diff --git a/lib/extras/exif.cc b/lib/extras/exif.cc
index 7d92655..aea6327 100644
--- a/lib/extras/exif.cc
+++ b/lib/extras/exif.cc
@@ -23,7 +23,7 @@ void ResetExifOrientation(std::vector<uint8_t>& exif) {
return; // not a valid tiff header
}
t += 4;
- uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t));
+ uint64_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t));
if (exif.size() < 12 + offset + 2 || offset < 8) return;
t += offset - 4;
uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t));
diff --git a/lib/extras/hlg.cc b/lib/extras/hlg.cc
index e39a080..1250055 100644
--- a/lib/extras/hlg.cc
+++ b/lib/extras/hlg.cc
@@ -5,9 +5,9 @@
#include "lib/extras/hlg.h"
-#include <cmath>
+#include <jxl/cms.h>
-#include "lib/jxl/enc_color_management.h"
+#include <cmath>
namespace jxl {
@@ -19,11 +19,12 @@ float GetHlgGamma(const float peak_luminance, const float surround_luminance) {
Status HlgOOTF(ImageBundle* ib, const float gamma, ThreadPool* pool) {
ColorEncoding linear_rec2020;
linear_rec2020.SetColorSpace(ColorSpace::kRGB);
- linear_rec2020.primaries = Primaries::k2100;
- linear_rec2020.white_point = WhitePoint::kD65;
- linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
+ JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100));
+ JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65));
+ linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear);
JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
- JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool));
+ JXL_RETURN_IF_ERROR(
+ ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool));
JXL_RETURN_IF_ERROR(RunOnPool(
pool, 0, ib->ysize(), ThreadPool::NoInit,
diff --git a/lib/extras/jpegli_test.cc b/lib/extras/jpegli_test.cc
new file mode 100644
index 0000000..0ebf0c1
--- /dev/null
+++ b/lib/extras/jpegli_test.cc
@@ -0,0 +1,415 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#if JPEGXL_ENABLE_JPEGLI
+
+#include "lib/extras/dec/jpegli.h"
+
+#include <jxl/color_encoding.h>
+#include <stdint.h>
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "lib/extras/dec/color_hints.h"
+#include "lib/extras/dec/decode.h"
+#include "lib/extras/dec/jpg.h"
+#include "lib/extras/enc/encode.h"
+#include "lib/extras/enc/jpegli.h"
+#include "lib/extras/enc/jpg.h"
+#include "lib/extras/packed_image.h"
+#include "lib/jxl/base/span.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/test_image.h"
+#include "lib/jxl/test_utils.h"
+#include "lib/jxl/testing.h"
+
+namespace jxl {
+namespace extras {
+namespace {
+
+using test::Butteraugli3Norm;
+using test::ButteraugliDistance;
+using test::TestImage;
+
+Status ReadTestImage(const std::string& pathname, PackedPixelFile* ppf) {
+ const std::vector<uint8_t> encoded = jxl::test::ReadTestData(pathname);
+ ColorHints color_hints;
+ if (pathname.find(".ppm") != std::string::npos) {
+ color_hints.Add("color_space", "RGB_D65_SRG_Rel_SRG");
+ } else if (pathname.find(".pgm") != std::string::npos) {
+ color_hints.Add("color_space", "Gra_D65_Rel_SRG");
+ }
+ return DecodeBytes(Bytes(encoded), color_hints, ppf);
+}
+
+std::vector<uint8_t> GetAppData(const std::vector<uint8_t>& compressed) {
+ std::vector<uint8_t> result;
+ size_t pos = 2; // After SOI
+ while (pos + 4 < compressed.size()) {
+ if (compressed[pos] != 0xff || compressed[pos + 1] < 0xe0 ||
+ compressed[pos + 1] > 0xf0) {
+ break;
+ }
+ size_t len = (compressed[pos + 2] << 8) + compressed[pos + 3] + 2;
+ if (pos + len > compressed.size()) {
+ break;
+ }
+ result.insert(result.end(), &compressed[pos], &compressed[pos] + len);
+ pos += len;
+ }
+ return result;
+}
+
+Status DecodeWithLibjpeg(const std::vector<uint8_t>& compressed,
+ PackedPixelFile* ppf,
+ const JPGDecompressParams* dparams = nullptr) {
+ return DecodeImageJPG(Bytes(compressed), ColorHints(), ppf,
+ /*constraints=*/nullptr, dparams);
+}
+
+Status EncodeWithLibjpeg(const PackedPixelFile& ppf, int quality,
+ std::vector<uint8_t>* compressed) {
+ std::unique_ptr<Encoder> encoder = GetJPEGEncoder();
+ encoder->SetOption("q", std::to_string(quality));
+ EncodedImage encoded;
+ JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded));
+ JXL_RETURN_IF_ERROR(!encoded.bitstreams.empty());
+ *compressed = std::move(encoded.bitstreams[0]);
+ return true;
+}
+
+std::string Description(const JxlColorEncoding& color_encoding) {
+ ColorEncoding c_enc;
+ JXL_CHECK(c_enc.FromExternal(color_encoding));
+ return Description(c_enc);
+}
+
+float BitsPerPixel(const PackedPixelFile& ppf,
+ const std::vector<uint8_t>& compressed) {
+ const size_t num_pixels = ppf.info.xsize * ppf.info.ysize;
+ return compressed.size() * 8.0 / num_pixels;
+}
+
+TEST(JpegliTest, JpegliSRGBDecodeTest) {
+ TEST_LIBJPEG_SUPPORT();
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf0;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf0));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding));
+ EXPECT_EQ(8, ppf0.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed));
+
+ PackedPixelFile ppf1;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1));
+ PackedPixelFile ppf2;
+ JpegDecompressParams dparams;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2));
+ EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1));
+}
+
+TEST(JpegliTest, JpegliGrayscaleDecodeTest) {
+ TEST_LIBJPEG_SUPPORT();
+ std::string testimage = "jxl/flower/flower_small.g.depth8.pgm";
+ PackedPixelFile ppf0;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf0));
+ EXPECT_EQ("Gra_D65_Rel_SRG", Description(ppf0.color_encoding));
+ EXPECT_EQ(8, ppf0.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed));
+
+ PackedPixelFile ppf1;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1));
+ PackedPixelFile ppf2;
+ JpegDecompressParams dparams;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2));
+ EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1));
+}
+
+TEST(JpegliTest, JpegliXYBEncodeTest) {
+ TEST_LIBJPEG_SUPPORT();
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.xyb = true;
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out));
+ EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.45f));
+ EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.32f));
+}
+
+TEST(JpegliTest, JpegliDecodeTestLargeSmoothArea) {
+ TEST_LIBJPEG_SUPPORT();
+ TestImage t;
+ const size_t xsize = 2070;
+ const size_t ysize = 1063;
+ t.SetDimensions(xsize, ysize).SetChannels(3);
+ t.SetAllBitDepths(8).SetEndianness(JXL_NATIVE_ENDIAN);
+ TestImage::Frame frame = t.AddFrame();
+ frame.RandomFill();
+ // Create a large smooth area in the top half of the image. This is to test
+ // that the bias statistics calculation can handle many blocks with all-zero
+ // AC coefficients.
+ for (size_t y = 0; y < ysize / 2; ++y) {
+ for (size_t x = 0; x < xsize; ++x) {
+ for (size_t c = 0; c < 3; ++c) {
+ frame.SetValue(y, x, c, 0.5f);
+ }
+ }
+ }
+ const PackedPixelFile& ppf0 = t.ppf();
+
+ std::vector<uint8_t> compressed;
+ ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed));
+
+ PackedPixelFile ppf1;
+ JpegDecompressParams dparams;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf1));
+ EXPECT_LT(ButteraugliDistance(ppf0, ppf1), 3.0f);
+}
+
+TEST(JpegliTest, JpegliYUVEncodeTest) {
+ TEST_LIBJPEG_SUPPORT();
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.xyb = false;
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out));
+ EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.7f));
+ EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.32f));
+}
+
+TEST(JpegliTest, JpegliYUVChromaSubsamplingEncodeTest) {
+ TEST_LIBJPEG_SUPPORT();
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ for (const char* sampling : {"440", "422", "420"}) {
+ settings.xyb = false;
+ settings.chroma_subsampling = std::string(sampling);
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out));
+ EXPECT_LE(BitsPerPixel(ppf_in, compressed), 1.55f);
+ EXPECT_LE(ButteraugliDistance(ppf_in, ppf_out), 1.82f);
+ }
+}
+
+TEST(JpegliTest, JpegliYUVEncodeTestNoAq) {
+ TEST_LIBJPEG_SUPPORT();
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.xyb = false;
+ settings.use_adaptive_quantization = false;
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out));
+ EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.85f));
+ EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.25f));
+}
+
+TEST(JpegliTest, JpegliHDRRoundtripTest) {
+ std::string testimage = "jxl/hdr_room.png";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_202_Rel_HLG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(16, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.xyb = false;
+ ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ PackedPixelFile ppf_out;
+ JpegDecompressParams dparams;
+ dparams.output_data_type = JXL_TYPE_UINT16;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf_out));
+ EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(2.95f));
+ EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.05f));
+}
+
+TEST(JpegliTest, JpegliSetAppData) {
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf_in;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf_in));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding));
+ EXPECT_EQ(8, ppf_in.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ JpegSettings settings;
+ settings.app_data = {0xff, 0xe3, 0, 4, 0, 1};
+ EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+ EXPECT_EQ(settings.app_data, GetAppData(compressed));
+
+ settings.app_data = {0xff, 0xe3, 0, 6, 0, 1, 2, 3, 0xff, 0xef, 0, 4, 0, 1};
+ EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+ EXPECT_EQ(settings.app_data, GetAppData(compressed));
+
+ settings.xyb = true;
+ EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+ EXPECT_EQ(0, memcmp(settings.app_data.data(), GetAppData(compressed).data(),
+ settings.app_data.size()));
+
+ settings.xyb = false;
+ settings.app_data = {0};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xe0};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xe0, 0, 2};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xeb, 0, 4, 0};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xeb, 0, 4, 0, 1, 2, 3};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.app_data = {0xff, 0xab, 0, 4, 0, 1};
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+
+ settings.xyb = false;
+ settings.app_data = {
+ 0xff, 0xeb, 0, 4, 0, 1, //
+ 0xff, 0xe2, 0, 20, 0x49, 0x43, 0x43, 0x5F, 0x50, //
+ 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00, 0, 1, //
+ 0, 0, 0, 0, //
+ };
+ EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+ EXPECT_EQ(settings.app_data, GetAppData(compressed));
+
+ settings.xyb = true;
+ EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed));
+}
+
+struct TestConfig {
+ int num_colors;
+ int passes;
+ int dither;
+};
+
+class JpegliColorQuantTestParam : public ::testing::TestWithParam<TestConfig> {
+};
+
+TEST_P(JpegliColorQuantTestParam, JpegliColorQuantizeTest) {
+ TEST_LIBJPEG_SUPPORT();
+ TestConfig config = GetParam();
+ std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm";
+ PackedPixelFile ppf0;
+ ASSERT_TRUE(ReadTestImage(testimage, &ppf0));
+ EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding));
+ EXPECT_EQ(8, ppf0.info.bits_per_sample);
+
+ std::vector<uint8_t> compressed;
+ ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed));
+
+ PackedPixelFile ppf1;
+ JPGDecompressParams dparams1;
+ dparams1.two_pass_quant = (config.passes == 2);
+ dparams1.num_colors = config.num_colors;
+ dparams1.dither_mode = config.dither;
+ ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1, &dparams1));
+
+ PackedPixelFile ppf2;
+ JpegDecompressParams dparams2;
+ dparams2.two_pass_quant = (config.passes == 2);
+ dparams2.num_colors = config.num_colors;
+ dparams2.dither_mode = config.dither;
+ ASSERT_TRUE(DecodeJpeg(compressed, dparams2, nullptr, &ppf2));
+
+ double dist1 = Butteraugli3Norm(ppf0, ppf1);
+ double dist2 = Butteraugli3Norm(ppf0, ppf2);
+ printf("distance: %f vs %f\n", dist2, dist1);
+ if (config.passes == 1) {
+ if (config.num_colors == 16 && config.dither == 2) {
+ // TODO(szabadka) Fix this case.
+ EXPECT_LT(dist2, dist1 * 1.5);
+ } else {
+ EXPECT_LT(dist2, dist1 * 1.05);
+ }
+ } else if (config.num_colors > 64) {
+ // TODO(szabadka) Fix 2pass quantization for <= 64 colors.
+ EXPECT_LT(dist2, dist1 * 1.1);
+ } else if (config.num_colors > 32) {
+ EXPECT_LT(dist2, dist1 * 1.2);
+ } else {
+ EXPECT_LT(dist2, dist1 * 1.7);
+ }
+}
+
+std::vector<TestConfig> GenerateTests() {
+ std::vector<TestConfig> all_tests;
+ for (int num_colors = 8; num_colors <= 256; num_colors *= 2) {
+ for (int passes = 1; passes <= 2; ++passes) {
+ for (int dither = 0; dither < 3; dither += passes) {
+ TestConfig config;
+ config.num_colors = num_colors;
+ config.passes = passes;
+ config.dither = dither;
+ all_tests.push_back(config);
+ }
+ }
+ }
+ return all_tests;
+}
+
+std::ostream& operator<<(std::ostream& os, const TestConfig& c) {
+ static constexpr const char* kDitherModeStr[] = {"No", "Ordered", "FS"};
+ os << c.passes << "pass";
+ os << c.num_colors << "colors";
+ os << kDitherModeStr[c.dither] << "dither";
+ return os;
+}
+
+std::string TestDescription(const testing::TestParamInfo<TestConfig>& info) {
+ std::stringstream name;
+ name << info.param;
+ return name.str();
+}
+
+JXL_GTEST_INSTANTIATE_TEST_SUITE_P(JpegliColorQuantTest,
+ JpegliColorQuantTestParam,
+ testing::ValuesIn(GenerateTests()),
+ TestDescription);
+
+} // namespace
+} // namespace extras
+} // namespace jxl
+#endif // JPEGXL_ENABLE_JPEGLI
diff --git a/lib/extras/metrics.cc b/lib/extras/metrics.cc
new file mode 100644
index 0000000..4259d3c
--- /dev/null
+++ b/lib/extras/metrics.cc
@@ -0,0 +1,224 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "lib/extras/metrics.h"
+
+#include <math.h>
+#include <stdlib.h>
+
+#include <atomic>
+
+#undef HWY_TARGET_INCLUDE
+#define HWY_TARGET_INCLUDE "lib/extras/metrics.cc"
+#include <hwy/foreach_target.h>
+#include <hwy/highway.h>
+
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/color_encoding_internal.h"
+HWY_BEFORE_NAMESPACE();
+namespace jxl {
+namespace HWY_NAMESPACE {
+
+// These templates are not found via ADL.
+using hwy::HWY_NAMESPACE::Add;
+using hwy::HWY_NAMESPACE::GetLane;
+using hwy::HWY_NAMESPACE::Mul;
+using hwy::HWY_NAMESPACE::Rebind;
+
+double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params,
+ double p) {
+ const double onePerPixels = 1.0 / (distmap.ysize() * distmap.xsize());
+ if (std::abs(p - 3.0) < 1E-6) {
+ double sum1[3] = {0.0};
+
+// Prefer double if possible, but otherwise use float rather than scalar.
+#if HWY_CAP_FLOAT64
+ using T = double;
+ const Rebind<float, HWY_FULL(double)> df;
+#else
+ using T = float;
+#endif
+ const HWY_FULL(T) d;
+ constexpr size_t N = MaxLanes(d);
+ // Manually aligned storage to avoid asan crash on clang-7 due to
+ // unaligned spill.
+ HWY_ALIGN T sum_totals0[N] = {0};
+ HWY_ALIGN T sum_totals1[N] = {0};
+ HWY_ALIGN T sum_totals2[N] = {0};
+
+ for (size_t y = 0; y < distmap.ysize(); ++y) {
+ const float* JXL_RESTRICT row = distmap.ConstRow(y);
+
+ auto sums0 = Zero(d);
+ auto sums1 = Zero(d);
+ auto sums2 = Zero(d);
+
+ size_t x = 0;
+ for (; x + Lanes(d) <= distmap.xsize(); x += Lanes(d)) {
+#if HWY_CAP_FLOAT64
+ const auto d1 = PromoteTo(d, Load(df, row + x));
+#else
+ const auto d1 = Load(d, row + x);
+#endif
+ const auto d2 = Mul(d1, Mul(d1, d1));
+ sums0 = Add(sums0, d2);
+ const auto d3 = Mul(d2, d2);
+ sums1 = Add(sums1, d3);
+ const auto d4 = Mul(d3, d3);
+ sums2 = Add(sums2, d4);
+ }
+
+ Store(Add(sums0, Load(d, sum_totals0)), d, sum_totals0);
+ Store(Add(sums1, Load(d, sum_totals1)), d, sum_totals1);
+ Store(Add(sums2, Load(d, sum_totals2)), d, sum_totals2);
+
+ for (; x < distmap.xsize(); ++x) {
+ const double d1 = row[x];
+ double d2 = d1 * d1 * d1;
+ sum1[0] += d2;
+ d2 *= d2;
+ sum1[1] += d2;
+ d2 *= d2;
+ sum1[2] += d2;
+ }
+ }
+ double v = 0;
+ v += pow(
+ onePerPixels * (sum1[0] + GetLane(SumOfLanes(d, Load(d, sum_totals0)))),
+ 1.0 / (p * 1.0));
+ v += pow(
+ onePerPixels * (sum1[1] + GetLane(SumOfLanes(d, Load(d, sum_totals1)))),
+ 1.0 / (p * 2.0));
+ v += pow(
+ onePerPixels * (sum1[2] + GetLane(SumOfLanes(d, Load(d, sum_totals2)))),
+ 1.0 / (p * 4.0));
+ v /= 3.0;
+ return v;
+ } else {
+ static std::atomic<int> once{0};
+ if (once.fetch_add(1, std::memory_order_relaxed) == 0) {
+ JXL_WARNING("WARNING: using slow ComputeDistanceP");
+ }
+ double sum1[3] = {0.0};
+ for (size_t y = 0; y < distmap.ysize(); ++y) {
+ const float* JXL_RESTRICT row = distmap.ConstRow(y);
+ for (size_t x = 0; x < distmap.xsize(); ++x) {
+ double d2 = std::pow(row[x], p);
+ sum1[0] += d2;
+ d2 *= d2;
+ sum1[1] += d2;
+ d2 *= d2;
+ sum1[2] += d2;
+ }
+ }
+ double v = 0;
+ for (int i = 0; i < 3; ++i) {
+ v += pow(onePerPixels * (sum1[i]), 1.0 / (p * (1 << i)));
+ }
+ v /= 3.0;
+ return v;
+ }
+}
+
+void ComputeSumOfSquares(const ImageBundle& ib1, const ImageBundle& ib2,
+ const JxlCmsInterface& cms, double sum_of_squares[3]) {
+ // Convert to sRGB - closer to perception than linear.
+ const Image3F* srgb1 = &ib1.color();
+ Image3F copy1;
+ if (!ib1.IsSRGB()) {
+ JXL_CHECK(
+ ib1.CopyTo(Rect(ib1), ColorEncoding::SRGB(ib1.IsGray()), cms, &copy1));
+ srgb1 = &copy1;
+ }
+ const Image3F* srgb2 = &ib2.color();
+ Image3F copy2;
+ if (!ib2.IsSRGB()) {
+ JXL_CHECK(
+ ib2.CopyTo(Rect(ib2), ColorEncoding::SRGB(ib2.IsGray()), cms, &copy2));
+ srgb2 = &copy2;
+ }
+
+ JXL_CHECK(SameSize(*srgb1, *srgb2));
+
+ // TODO(veluca): SIMD.
+ float yuvmatrix[3][3] = {{0.299, 0.587, 0.114},
+ {-0.14713, -0.28886, 0.436},
+ {0.615, -0.51499, -0.10001}};
+ for (size_t y = 0; y < srgb1->ysize(); ++y) {
+ const float* JXL_RESTRICT row1[3];
+ const float* JXL_RESTRICT row2[3];
+ for (size_t j = 0; j < 3; j++) {
+ row1[j] = srgb1->ConstPlaneRow(j, y);
+ row2[j] = srgb2->ConstPlaneRow(j, y);
+ }
+ for (size_t x = 0; x < srgb1->xsize(); ++x) {
+ float cdiff[3] = {};
+ // YUV conversion is linear, so we can run it on the difference.
+ for (size_t j = 0; j < 3; j++) {
+ cdiff[j] = row1[j][x] - row2[j][x];
+ }
+ float yuvdiff[3] = {};
+ for (size_t j = 0; j < 3; j++) {
+ for (size_t k = 0; k < 3; k++) {
+ yuvdiff[j] += yuvmatrix[j][k] * cdiff[k];
+ }
+ }
+ for (size_t j = 0; j < 3; j++) {
+ sum_of_squares[j] += yuvdiff[j] * yuvdiff[j];
+ }
+ }
+ }
+}
+
+// NOLINTNEXTLINE(google-readability-namespace-comments)
+} // namespace HWY_NAMESPACE
+} // namespace jxl
+HWY_AFTER_NAMESPACE();
+
+#if HWY_ONCE
+namespace jxl {
+HWY_EXPORT(ComputeDistanceP);
+double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params,
+ double p) {
+ return HWY_DYNAMIC_DISPATCH(ComputeDistanceP)(distmap, params, p);
+}
+
+HWY_EXPORT(ComputeSumOfSquares);
+
+double ComputeDistance2(const ImageBundle& ib1, const ImageBundle& ib2,
+ const JxlCmsInterface& cms) {
+ double sum_of_squares[3] = {};
+ HWY_DYNAMIC_DISPATCH(ComputeSumOfSquares)(ib1, ib2, cms, sum_of_squares);
+ // Weighted PSNR as in JPEG-XL: chroma counts 1/8.
+ const float weights[3] = {6.0f / 8, 1.0f / 8, 1.0f / 8};
+ // Avoid squaring the weight - 1/64 is too extreme.
+ double norm = 0;
+ for (size_t i = 0; i < 3; i++) {
+ norm += std::sqrt(sum_of_squares[i]) * weights[i];
+ }
+ // This function returns distance *squared*.
+ return norm * norm;
+}
+
+double ComputePSNR(const ImageBundle& ib1, const ImageBundle& ib2,
+ const JxlCmsInterface& cms) {
+ if (!SameSize(ib1, ib2)) return 0.0;
+ double sum_of_squares[3] = {};
+ HWY_DYNAMIC_DISPATCH(ComputeSumOfSquares)(ib1, ib2, cms, sum_of_squares);
+ constexpr double kChannelWeights[3] = {6.0 / 8, 1.0 / 8, 1.0 / 8};
+ double avg_psnr = 0;
+ const size_t input_pixels = ib1.xsize() * ib1.ysize();
+ for (int i = 0; i < 3; ++i) {
+ const double rmse = std::sqrt(sum_of_squares[i] / input_pixels);
+ const double psnr =
+ sum_of_squares[i] == 0 ? 99.99 : (20 * std::log10(1 / rmse));
+ avg_psnr += kChannelWeights[i] * psnr;
+ }
+ return avg_psnr;
+}
+
+} // namespace jxl
+#endif
diff --git a/lib/extras/metrics.h b/lib/extras/metrics.h
new file mode 100644
index 0000000..87a69a9
--- /dev/null
+++ b/lib/extras/metrics.h
@@ -0,0 +1,28 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_EXTRAS_METRICS_H_
+#define LIB_EXTRAS_METRICS_H_
+
+#include <stdint.h>
+
+#include "lib/jxl/butteraugli/butteraugli.h"
+#include "lib/jxl/image_bundle.h"
+
+namespace jxl {
+
+// Computes p-norm given the butteraugli distmap.
+double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params,
+ double p);
+
+double ComputeDistance2(const ImageBundle& ib1, const ImageBundle& ib2,
+ const JxlCmsInterface& cms);
+
+double ComputePSNR(const ImageBundle& ib1, const ImageBundle& ib2,
+ const JxlCmsInterface& cms);
+
+} // namespace jxl
+
+#endif // LIB_EXTRAS_METRICS_H_
diff --git a/lib/extras/packed_image.h b/lib/extras/packed_image.h
index 1296472..d3ba9ce 100644
--- a/lib/extras/packed_image.h
+++ b/lib/extras/packed_image.h
@@ -9,20 +9,24 @@
// Helper class for storing external (int or float, interleaved) images. This is
// the common format used by other libraries and in the libjxl API.
+#include <jxl/codestream_header.h>
+#include <jxl/encode.h>
+#include <jxl/types.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
+#include <cmath>
#include <memory>
+#include <set>
#include <string>
#include <vector>
-#include "jxl/codestream_header.h"
-#include "jxl/encode.h"
-#include "jxl/types.h"
-#include "lib/jxl/common.h"
+#include "lib/jxl/base/byte_order.h"
+#include "lib/jxl/base/common.h"
+#include "lib/jxl/base/status.h"
namespace jxl {
namespace extras {
@@ -33,9 +37,26 @@ class PackedImage {
PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format)
: PackedImage(xsize, ysize, format, CalcStride(format, xsize)) {}
+ PackedImage Copy() const {
+ PackedImage copy(xsize, ysize, format);
+ memcpy(reinterpret_cast<uint8_t*>(copy.pixels()),
+ reinterpret_cast<const uint8_t*>(pixels()), pixels_size);
+ return copy;
+ }
+
// The interleaved pixels as defined in the storage format.
void* pixels() const { return pixels_.get(); }
+ uint8_t* pixels(size_t y, size_t x, size_t c) const {
+ return (reinterpret_cast<uint8_t*>(pixels_.get()) + y * stride +
+ x * pixel_stride_ + c * bytes_per_channel_);
+ }
+
+ const uint8_t* const_pixels(size_t y, size_t x, size_t c) const {
+ return (reinterpret_cast<const uint8_t*>(pixels_.get()) + y * stride +
+ x * pixel_stride_ + c * bytes_per_channel_);
+ }
+
// The image size in pixels.
size_t xsize;
size_t ysize;
@@ -47,10 +68,7 @@ class PackedImage {
JxlPixelFormat format;
size_t pixels_size;
- size_t pixel_stride() const {
- return (BitsPerChannel(format.data_type) * format.num_channels /
- jxl::kBitsPerByte);
- }
+ size_t pixel_stride() const { return pixel_stride_; }
static size_t BitsPerChannel(JxlDataType data_type) {
switch (data_type) {
@@ -67,6 +85,52 @@ class PackedImage {
}
}
+ float GetPixelValue(size_t y, size_t x, size_t c) const {
+ const uint8_t* data = const_pixels(y, x, c);
+ switch (format.data_type) {
+ case JXL_TYPE_UINT8:
+ return data[0] * (1.0f / 255);
+ case JXL_TYPE_UINT16: {
+ uint16_t val;
+ memcpy(&val, data, 2);
+ return (swap_endianness_ ? JXL_BSWAP16(val) : val) * (1.0f / 65535);
+ }
+ case JXL_TYPE_FLOAT: {
+ float val;
+ memcpy(&val, data, 4);
+ return swap_endianness_ ? BSwapFloat(val) : val;
+ }
+ default:
+ JXL_ABORT("Unhandled JxlDataType");
+ }
+ }
+
+ void SetPixelValue(size_t y, size_t x, size_t c, float val) {
+ uint8_t* data = pixels(y, x, c);
+ switch (format.data_type) {
+ case JXL_TYPE_UINT8:
+ data[0] = Clamp1(std::round(val * 255), 0.0f, 255.0f);
+ break;
+ case JXL_TYPE_UINT16: {
+ uint16_t val16 = Clamp1(std::round(val * 65535), 0.0f, 65535.0f);
+ if (swap_endianness_) {
+ val16 = JXL_BSWAP16(val16);
+ }
+ memcpy(data, &val16, 2);
+ break;
+ }
+ case JXL_TYPE_FLOAT: {
+ if (swap_endianness_) {
+ val = BSwapFloat(val);
+ }
+ memcpy(data, &val, 4);
+ break;
+ }
+ default:
+ JXL_ABORT("Unhandled JxlDataType");
+ }
+ }
+
private:
PackedImage(size_t xsize, size_t ysize, const JxlPixelFormat& format,
size_t stride)
@@ -75,7 +139,11 @@ class PackedImage {
stride(stride),
format(format),
pixels_size(ysize * stride),
- pixels_(malloc(std::max<size_t>(1, pixels_size)), free) {}
+ pixels_(malloc(std::max<size_t>(1, pixels_size)), free) {
+ bytes_per_channel_ = BitsPerChannel(format.data_type) / jxl::kBitsPerByte;
+ pixel_stride_ = format.num_channels * bytes_per_channel_;
+ swap_endianness_ = SwapEndianness(format.endianness);
+ }
static size_t CalcStride(const JxlPixelFormat& format, size_t xsize) {
size_t stride = xsize * (BitsPerChannel(format.data_type) *
@@ -86,6 +154,9 @@ class PackedImage {
return stride;
}
+ size_t bytes_per_channel_;
+ size_t pixel_stride_;
+ bool swap_endianness_;
std::unique_ptr<void, decltype(free)*> pixels_;
};
@@ -98,6 +169,18 @@ class PackedFrame {
template <typename... Args>
explicit PackedFrame(Args&&... args) : color(std::forward<Args>(args)...) {}
+ PackedFrame Copy() const {
+ PackedFrame copy(color.xsize, color.ysize, color.format);
+ copy.frame_info = frame_info;
+ copy.name = name;
+ copy.color = color.Copy();
+ for (size_t i = 0; i < extra_channels.size(); ++i) {
+ PackedImage ec = extra_channels[i].Copy();
+ copy.extra_channels.emplace_back(std::move(ec));
+ }
+ return copy;
+ }
+
// The Frame metadata.
JxlFrameHeader frame_info = {};
std::string name;
@@ -108,6 +191,85 @@ class PackedFrame {
std::vector<PackedImage> extra_channels;
};
+class ChunkedPackedFrame {
+ public:
+ typedef void (*ReadLine)(void* opaque, size_t xpos, size_t ypos, size_t xsize,
+ uint8_t* buffer, size_t len);
+ ChunkedPackedFrame(size_t xsize, size_t ysize, const JxlPixelFormat& format,
+ void* opaque, ReadLine read_line)
+ : xsize(xsize),
+ ysize(ysize),
+ format(format),
+ opaque_(opaque),
+ read_line_(read_line) {}
+
+ JxlChunkedFrameInputSource GetInputSource() {
+ return JxlChunkedFrameInputSource{this,
+ GetColorChannelsPixelFormat,
+ GetColorChannelDataAt,
+ GetExtraChannelPixelFormat,
+ GetExtraChannelDataAt,
+ ReleaseCurrentData};
+ }
+
+ // The Frame metadata.
+ JxlFrameHeader frame_info = {};
+ std::string name;
+
+ size_t xsize;
+ size_t ysize;
+ JxlPixelFormat format;
+
+ private:
+ static void GetColorChannelsPixelFormat(void* opaque,
+ JxlPixelFormat* pixel_format) {
+ ChunkedPackedFrame* self = reinterpret_cast<ChunkedPackedFrame*>(opaque);
+ *pixel_format = self->format;
+ }
+
+ static const void* GetColorChannelDataAt(void* opaque, size_t xpos,
+ size_t ypos, size_t xsize,
+ size_t ysize, size_t* row_offset) {
+ ChunkedPackedFrame* self = reinterpret_cast<ChunkedPackedFrame*>(opaque);
+ size_t bytes_per_channel =
+ PackedImage::BitsPerChannel(self->format.data_type) / jxl::kBitsPerByte;
+ size_t bytes_per_pixel = bytes_per_channel * self->format.num_channels;
+ *row_offset = xsize * bytes_per_pixel;
+ uint8_t* buffer = reinterpret_cast<uint8_t*>(malloc(ysize * (*row_offset)));
+ for (size_t y = 0; y < ysize; ++y) {
+ self->read_line_(self->opaque_, xpos, ypos + y, xsize,
+ &buffer[y * (*row_offset)], *row_offset);
+ }
+ self->buffers_.insert(buffer);
+ return buffer;
+ }
+
+ static void GetExtraChannelPixelFormat(void* opaque, size_t ec_index,
+ JxlPixelFormat* pixel_format) {
+ JXL_ABORT("Not implemented");
+ }
+
+ static const void* GetExtraChannelDataAt(void* opaque, size_t ec_index,
+ size_t xpos, size_t ypos,
+ size_t xsize, size_t ysize,
+ size_t* row_offset) {
+ JXL_ABORT("Not implemented");
+ }
+
+ static void ReleaseCurrentData(void* opaque, const void* buffer) {
+ ChunkedPackedFrame* self = reinterpret_cast<ChunkedPackedFrame*>(opaque);
+ auto iter = self->buffers_.find(const_cast<void*>(buffer));
+ if (iter != self->buffers_.end()) {
+ free(*iter);
+ self->buffers_.erase(iter);
+ }
+ }
+
+ void* opaque_;
+ ReadLine read_line_;
+ std::set<void*> buffers_;
+};
+
// Optional metadata associated with a file
class PackedMetadata {
public:
@@ -117,17 +279,18 @@ class PackedMetadata {
std::vector<uint8_t> xmp;
};
+// The extra channel metadata information.
+struct PackedExtraChannel {
+ JxlExtraChannelInfo ec_info;
+ size_t index;
+ std::string name;
+};
+
// Helper class representing a JXL image file as decoded to pixels from the API.
class PackedPixelFile {
public:
JxlBasicInfo info = {};
- // The extra channel metadata information.
- struct PackedExtraChannel {
- JxlExtraChannelInfo ec_info;
- size_t index;
- std::string name;
- };
std::vector<PackedExtraChannel> extra_channels_info;
// Color information of the decoded pixels.
@@ -139,9 +302,14 @@ class PackedPixelFile {
std::unique_ptr<PackedFrame> preview_frame;
std::vector<PackedFrame> frames;
+ mutable std::vector<ChunkedPackedFrame> chunked_frames;
PackedMetadata metadata;
PackedPixelFile() { JxlEncoderInitBasicInfo(&info); };
+
+ size_t num_frames() const {
+ return chunked_frames.empty() ? frames.size() : chunked_frames.size();
+ }
};
} // namespace extras
diff --git a/lib/extras/packed_image_convert.cc b/lib/extras/packed_image_convert.cc
index dcdd12a..1bc8f20 100644
--- a/lib/extras/packed_image_convert.cc
+++ b/lib/extras/packed_image_convert.cc
@@ -5,17 +5,18 @@
#include "lib/extras/packed_image_convert.h"
+#include <jxl/cms.h>
+#include <jxl/color_encoding.h>
+#include <jxl/types.h>
+
#include <cstdint>
-#include "jxl/color_encoding.h"
-#include "jxl/types.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/color_encoding_internal.h"
-#include "lib/jxl/color_management.h"
#include "lib/jxl/dec_external_image.h"
-#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_external_image.h"
#include "lib/jxl/enc_image_bundle.h"
+#include "lib/jxl/luminance.h"
namespace jxl {
namespace extras {
@@ -58,10 +59,7 @@ Status ConvertPackedFrameToImageBundle(const JxlBasicInfo& info,
JXL_RETURN_IF_ERROR(ConvertFromExternal(
span, frame.color.xsize, frame.color.ysize, io.metadata.m.color_encoding,
- frame.color.format.num_channels,
- /*alpha_is_premultiplied=*/info.alpha_premultiplied,
- frame_bits_per_sample, frame.color.format.endianness, pool, bundle,
- /*float_in=*/float_in, /*align=*/0));
+ frame_bits_per_sample, frame.color.format, pool, bundle));
bundle->extra_channels().resize(io.metadata.m.extra_channel_info.size());
for (size_t i = 0; i < frame.extra_channels.size(); i++) {
@@ -84,7 +82,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
ppf.info.exponent_bits_per_sample);
}
- const bool is_gray = ppf.info.num_color_channels == 1;
+ const bool is_gray = (ppf.info.num_color_channels == 1);
JXL_ASSERT(ppf.info.num_color_channels == 1 ||
ppf.info.num_color_channels == 3);
@@ -114,20 +112,24 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
// Convert the color encoding.
if (!ppf.icc.empty()) {
- PaddedBytes icc;
- icc.append(ppf.icc);
- if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) {
+ IccBytes icc = ppf.icc;
+ if (!io->metadata.m.color_encoding.SetICC(std::move(icc),
+ JxlGetDefaultCms())) {
fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB\n");
io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
} else {
+ if (io->metadata.m.color_encoding.IsCMYK()) {
+ // We expect gray or tri-color.
+ return JXL_FAILURE("Embedded ICC is CMYK");
+ }
if (io->metadata.m.color_encoding.IsGray() != is_gray) {
// E.g. JPG image has 3 channels, but gray ICC.
return JXL_FAILURE("Embedded ICC does not match image color type");
}
}
} else {
- JXL_RETURN_IF_ERROR(ConvertExternalToInternalColorEncoding(
- ppf.color_encoding, &io->metadata.m.color_encoding));
+ JXL_RETURN_IF_ERROR(
+ io->metadata.m.color_encoding.FromExternal(ppf.color_encoding));
if (io->metadata.m.color_encoding.ICC().empty()) {
return JXL_FAILURE("Failed to serialize ICC");
}
@@ -140,8 +142,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
io->blobs.xmp = ppf.metadata.xmp;
// Append all other extra channels.
- for (const PackedPixelFile::PackedExtraChannel& info :
- ppf.extra_channels_info) {
+ for (const auto& info : ppf.extra_channels_info) {
ExtraChannelInfo out;
out.type = static_cast<jxl::ExtraChannel>(info.ec_info.type);
out.bit_depth.bits_per_sample = info.ec_info.bits_per_sample;
@@ -171,14 +172,12 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
}
// Convert the pixels
- io->dec_pixels = 0;
io->frames.clear();
for (const auto& frame : ppf.frames) {
ImageBundle bundle(&io->metadata.m);
JXL_RETURN_IF_ERROR(
ConvertPackedFrameToImageBundle(ppf.info, frame, *io, pool, &bundle));
io->frames.push_back(std::move(bundle));
- io->dec_pixels += frame.color.xsize * frame.color.ysize;
}
if (ppf.info.exponent_bits_per_sample == 0) {
@@ -188,7 +187,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
if (ppf.info.intensity_target != 0) {
io->metadata.m.SetIntensityTarget(ppf.info.intensity_target);
} else {
- SetIntensityTarget(io);
+ SetIntensityTarget(&io->metadata.m);
}
io->CheckMetadata();
return true;
@@ -221,6 +220,12 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io,
ppf->info.exponent_bits_per_sample =
io.metadata.m.bit_depth.exponent_bits_per_sample;
+ ppf->info.intensity_target = io.metadata.m.tone_mapping.intensity_target;
+ ppf->info.linear_below = io.metadata.m.tone_mapping.linear_below;
+ ppf->info.min_nits = io.metadata.m.tone_mapping.min_nits;
+ ppf->info.relative_to_max_display =
+ io.metadata.m.tone_mapping.relative_to_max_display;
+
ppf->info.alpha_bits = io.metadata.m.GetAlphaBits();
ppf->info.alpha_premultiplied = alpha_premultiplied;
@@ -239,7 +244,7 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io,
// Convert the color encoding
ppf->icc.assign(c_desired.ICC().begin(), c_desired.ICC().end());
- ConvertInternalToExternalColorEncoding(c_desired, &ppf->color_encoding);
+ ppf->color_encoding = c_desired.ToExternal();
// Convert the extra blobs
ppf->metadata.exif = io.blobs.exif;
@@ -276,12 +281,8 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io,
const ImageBundle* transformed;
// TODO(firsching): handle the transform here.
JXL_RETURN_IF_ERROR(TransformIfNeeded(*to_color_transform, c_desired,
- GetJxlCms(), pool, &store,
+ *JxlGetDefaultCms(), pool, &store,
&transformed));
- size_t stride = ib.oriented_xsize() *
- (c_desired.Channels() * ppf->info.bits_per_sample) /
- kBitsPerByte;
- PaddedBytes pixels(stride * ib.oriented_ysize());
JXL_RETURN_IF_ERROR(ConvertToExternal(
*transformed, bits_per_sample, float_out, format.num_channels,
diff --git a/lib/extras/packed_image_convert.h b/lib/extras/packed_image_convert.h
index cada660..100adcc 100644
--- a/lib/extras/packed_image_convert.h
+++ b/lib/extras/packed_image_convert.h
@@ -9,7 +9,8 @@
// Helper functions to convert from the external image types to the internal
// CodecInOut to help transitioning to the external types.
-#include "jxl/types.h"
+#include <jxl/types.h>
+
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/codec_in_out.h"
diff --git a/lib/extras/render_hdr.cc b/lib/extras/render_hdr.cc
deleted file mode 100644
index b247699..0000000
--- a/lib/extras/render_hdr.cc
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (c) the JPEG XL Project Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-#include "lib/extras/render_hdr.h"
-
-#include "lib/extras/hlg.h"
-#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/enc_color_management.h"
-
-namespace jxl {
-
-Status RenderHDR(CodecInOut* io, float display_nits, ThreadPool* pool) {
- const ColorEncoding& original_color_encoding = io->metadata.m.color_encoding;
- if (!(original_color_encoding.tf.IsPQ() ||
- original_color_encoding.tf.IsHLG())) {
- // Nothing to do.
- return true;
- }
-
- if (original_color_encoding.tf.IsPQ()) {
- JXL_RETURN_IF_ERROR(ToneMapTo({0, display_nits}, io, pool));
- JXL_RETURN_IF_ERROR(GamutMap(io, /*preserve_saturation=*/0.1, pool));
- } else {
- const float intensity_target = io->metadata.m.IntensityTarget();
- const float gamma_hlg_to_display = GetHlgGamma(display_nits);
- // If the image is already in display space, we need to account for the
- // already-applied OOTF.
- const float gamma_display_to_display =
- gamma_hlg_to_display / GetHlgGamma(intensity_target);
- // Ensures that conversions to linear in HlgOOTF below will not themselves
- // include the OOTF.
- io->metadata.m.SetIntensityTarget(300);
-
- bool need_gamut_mapping = false;
- for (ImageBundle& ib : io->frames) {
- const float gamma = ib.c_current().tf.IsHLG() ? gamma_hlg_to_display
- : gamma_display_to_display;
- if (gamma < 1) need_gamut_mapping = true;
- JXL_RETURN_IF_ERROR(HlgOOTF(&ib, gamma, pool));
- }
- io->metadata.m.SetIntensityTarget(display_nits);
-
- if (need_gamut_mapping) {
- JXL_RETURN_IF_ERROR(GamutMap(io, /*preserve_saturation=*/0.1, pool));
- }
- }
-
- ColorEncoding rec2020_pq;
- rec2020_pq.SetColorSpace(ColorSpace::kRGB);
- rec2020_pq.white_point = WhitePoint::kD65;
- rec2020_pq.primaries = Primaries::k2100;
- rec2020_pq.tf.SetTransferFunction(TransferFunction::kPQ);
- JXL_RETURN_IF_ERROR(rec2020_pq.CreateICC());
- io->metadata.m.color_encoding = rec2020_pq;
- return io->TransformTo(rec2020_pq, GetJxlCms(), pool);
-}
-
-} // namespace jxl
diff --git a/lib/extras/render_hdr.h b/lib/extras/render_hdr.h
deleted file mode 100644
index 95127e0..0000000
--- a/lib/extras/render_hdr.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) the JPEG XL Project Authors. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-#ifndef LIB_EXTRAS_RENDER_HDR_H_
-#define LIB_EXTRAS_RENDER_HDR_H_
-
-#include "lib/jxl/codec_in_out.h"
-
-namespace jxl {
-
-// If `io` has an original color space using PQ or HLG, this renders it
-// appropriately for a display with a peak luminance of `display_nits` and
-// converts the result to a Rec. 2020 / PQ image. Otherwise, leaves the image as
-// is.
-// PQ images are tone-mapped using the method described in Rep. ITU-R BT.2408-5
-// annex 5, while HLG images are rendered using the HLG OOTF with a gamma
-// appropriate for the given target luminance.
-// With a sufficiently bright SDR display, converting the output of this
-// function to an SDR colorspace may look decent.
-Status RenderHDR(CodecInOut* io, float display_nits,
- ThreadPool* pool = nullptr);
-
-} // namespace jxl
-
-#endif // LIB_EXTRAS_RENDER_HDR_H_
diff --git a/lib/extras/size_constraints.h b/lib/extras/size_constraints.h
new file mode 100644
index 0000000..cf06f8c
--- /dev/null
+++ b/lib/extras/size_constraints.h
@@ -0,0 +1,43 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#ifndef LIB_JXL_SIZE_CONSTRAINTS_H_
+#define LIB_JXL_SIZE_CONSTRAINTS_H_
+
+#include <cstdint>
+#include <type_traits>
+
+#include "lib/jxl/base/status.h"
+
+namespace jxl {
+
+struct SizeConstraints {
+ // Upper limit on pixel dimensions/area, enforced by VerifyDimensions
+ // (called from decoders). Fuzzers set smaller values to limit memory use.
+ uint32_t dec_max_xsize = 0xFFFFFFFFu;
+ uint32_t dec_max_ysize = 0xFFFFFFFFu;
+ uint64_t dec_max_pixels = 0xFFFFFFFFu; // Might be up to ~0ull
+};
+
+template <typename T,
+ class = typename std::enable_if<std::is_unsigned<T>::value>::type>
+Status VerifyDimensions(const SizeConstraints* constraints, T xs, T ys) {
+ if (!constraints) return true;
+
+ if (xs == 0 || ys == 0) return JXL_FAILURE("Empty image.");
+ if (xs > constraints->dec_max_xsize) return JXL_FAILURE("Image too wide.");
+ if (ys > constraints->dec_max_ysize) return JXL_FAILURE("Image too tall.");
+
+ const uint64_t num_pixels = static_cast<uint64_t>(xs) * ys;
+ if (num_pixels > constraints->dec_max_pixels) {
+ return JXL_FAILURE("Image too big.");
+ }
+
+ return true;
+}
+
+} // namespace jxl
+
+#endif // LIB_JXL_SIZE_CONSTRAINTS_H_
diff --git a/lib/extras/time.cc b/lib/extras/time.cc
index 73d1b8f..d4f4175 100644
--- a/lib/extras/time.cc
+++ b/lib/extras/time.cc
@@ -6,7 +6,6 @@
#include "lib/extras/time.h"
#include <stdint.h>
-#include <stdio.h>
#include <stdlib.h>
#include <ctime>
diff --git a/lib/extras/tone_mapping.cc b/lib/extras/tone_mapping.cc
index 1ed1b29..3d02695 100644
--- a/lib/extras/tone_mapping.cc
+++ b/lib/extras/tone_mapping.cc
@@ -7,11 +7,13 @@
#undef HWY_TARGET_INCLUDE
#define HWY_TARGET_INCLUDE "lib/extras/tone_mapping.cc"
+#include <jxl/cms.h>
+
#include <hwy/foreach_target.h>
#include <hwy/highway.h>
-#include "lib/jxl/dec_tone_mapping-inl.h"
-#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/cms/tone_mapping-inl.h"
+#include "lib/jxl/image_bundle.h"
HWY_BEFORE_NAMESPACE();
namespace jxl {
@@ -30,11 +32,12 @@ Status ToneMapFrame(const std::pair<float, float> display_nits,
ColorEncoding linear_rec2020;
linear_rec2020.SetColorSpace(ColorSpace::kRGB);
- linear_rec2020.primaries = Primaries::k2100;
- linear_rec2020.white_point = WhitePoint::kD65;
- linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
+ JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100));
+ JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65));
+ linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear);
JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
- JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool));
+ JXL_RETURN_IF_ERROR(
+ ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool));
Rec2408ToneMapper<decltype(df)> tone_mapper(
{ib->metadata()->tone_mapping.min_nits,
@@ -67,11 +70,12 @@ Status GamutMapFrame(ImageBundle* const ib, float preserve_saturation,
ColorEncoding linear_rec2020;
linear_rec2020.SetColorSpace(ColorSpace::kRGB);
- linear_rec2020.primaries = Primaries::k2100;
- linear_rec2020.white_point = WhitePoint::kD65;
- linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
+ JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100));
+ JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65));
+ linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear);
JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
- JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, GetJxlCms(), pool));
+ JXL_RETURN_IF_ERROR(
+ ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool));
JXL_RETURN_IF_ERROR(RunOnPool(
pool, 0, ib->ysize(), ThreadPool::NoInit,
diff --git a/lib/extras/tone_mapping_gbench.cc b/lib/extras/tone_mapping_gbench.cc
index 2f97b88..34cbdde 100644
--- a/lib/extras/tone_mapping_gbench.cc
+++ b/lib/extras/tone_mapping_gbench.cc
@@ -6,39 +6,35 @@
#include "benchmark/benchmark.h"
#include "lib/extras/codec.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/enc_color_management.h"
-#include "lib/jxl/testdata.h"
namespace jxl {
static void BM_ToneMapping(benchmark::State& state) {
- CodecInOut image;
- const PaddedBytes image_bytes = ReadTestData("jxl/flower/flower.png");
- JXL_CHECK(SetFromBytes(Span<const uint8_t>(image_bytes), &image));
+ Image3F color(2268, 1512);
+ FillImage(0.5f, &color);
- // Convert to linear Rec. 2020 so that `ToneMapTo` doesn't have to and we
- // mainly measure the tone mapping itself.
+ // Use linear Rec. 2020 so that `ToneMapTo` doesn't have to convert to it and
+ // we mainly measure the tone mapping itself.
ColorEncoding linear_rec2020;
linear_rec2020.SetColorSpace(ColorSpace::kRGB);
- linear_rec2020.primaries = Primaries::k2100;
- linear_rec2020.white_point = WhitePoint::kD65;
- linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
+ JXL_CHECK(linear_rec2020.SetPrimariesType(Primaries::k2100));
+ JXL_CHECK(linear_rec2020.SetWhitePointType(WhitePoint::kD65));
+ linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear);
JXL_CHECK(linear_rec2020.CreateICC());
- JXL_CHECK(image.TransformTo(linear_rec2020, GetJxlCms()));
for (auto _ : state) {
state.PauseTiming();
CodecInOut tone_mapping_input;
- tone_mapping_input.SetFromImage(CopyImage(*image.Main().color()),
- image.Main().c_current());
- tone_mapping_input.metadata.m.SetIntensityTarget(
- image.metadata.m.IntensityTarget());
+ Image3F color2(color.xsize(), color.ysize());
+ CopyImageTo(color, &color2);
+ tone_mapping_input.SetFromImage(std::move(color2), linear_rec2020);
+ tone_mapping_input.metadata.m.SetIntensityTarget(255);
state.ResumeTiming();
JXL_CHECK(ToneMapTo({0.1, 100}, &tone_mapping_input));
}
- state.SetItemsProcessed(state.iterations() * image.xsize() * image.ysize());
+ state.SetItemsProcessed(state.iterations() * color.xsize() * color.ysize());
}
BENCHMARK(BM_ToneMapping);