summaryrefslogtreecommitdiff
path: root/lib/extras/enc
diff options
context:
space:
mode:
Diffstat (limited to 'lib/extras/enc')
-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
13 files changed, 1848 insertions, 272 deletions
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();