diff options
Diffstat (limited to 'lib/extras/enc')
-rw-r--r-- | lib/extras/enc/apng.cc | 133 | ||||
-rw-r--r-- | lib/extras/enc/encode.cc | 124 | ||||
-rw-r--r-- | lib/extras/enc/encode.h | 25 | ||||
-rw-r--r-- | lib/extras/enc/exr.cc | 14 | ||||
-rw-r--r-- | lib/extras/enc/jpegli.cc | 523 | ||||
-rw-r--r-- | lib/extras/enc/jpegli.h | 53 | ||||
-rw-r--r-- | lib/extras/enc/jpg.cc | 450 | ||||
-rw-r--r-- | lib/extras/enc/jxl.cc | 359 | ||||
-rw-r--r-- | lib/extras/enc/jxl.h | 91 | ||||
-rw-r--r-- | lib/extras/enc/npy.cc | 3 | ||||
-rw-r--r-- | lib/extras/enc/pgx.cc | 4 | ||||
-rw-r--r-- | lib/extras/enc/pnm.cc | 340 | ||||
-rw-r--r-- | lib/extras/enc/pnm.h | 1 |
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, ¶ms.input_bitdepth)) { + fprintf(stderr, "JxlEncoderSetFrameBitDepth() failed.\n"); + return false; + } + if (num_alpha_channels != 0 && + JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelDistance( + settings, 0, params.alpha_distance)) { + fprintf(stderr, "Setting alpha distance failed.\n"); + return false; + } + if (lossless && + JXL_ENC_SUCCESS != JxlEncoderSetFrameLossless(settings, JXL_TRUE)) { + fprintf(stderr, "JxlEncoderSetFrameLossless() failed.\n"); + return false; + } + if (!ppf.icc.empty()) { + if (JXL_ENC_SUCCESS != + JxlEncoderSetICCProfile(enc, ppf.icc.data(), ppf.icc.size())) { + fprintf(stderr, "JxlEncoderSetICCProfile() failed.\n"); + return false; + } + } else { + if (JXL_ENC_SUCCESS != + JxlEncoderSetColorEncoding(enc, &ppf.color_encoding)) { + fprintf(stderr, "JxlEncoderSetColorEncoding() failed.\n"); + return false; + } + } + + if (use_boxes) { + if (JXL_ENC_SUCCESS != JxlEncoderUseBoxes(enc)) { + fprintf(stderr, "JxlEncoderUseBoxes() failed.\n"); + return false; + } + // Prepend 4 zero bytes to exif for tiff header offset + std::vector<uint8_t> exif_with_offset; + bool bigendian; + if (IsExif(ppf.metadata.exif, &bigendian)) { + exif_with_offset.resize(ppf.metadata.exif.size() + 4); + memcpy(exif_with_offset.data() + 4, ppf.metadata.exif.data(), + ppf.metadata.exif.size()); + } + const struct BoxInfo { + const char* type; + const std::vector<uint8_t>& bytes; + } boxes[] = { + {"Exif", exif_with_offset}, + {"xml ", ppf.metadata.xmp}, + {"jumb", ppf.metadata.jumbf}, + {"xml ", ppf.metadata.iptc}, + }; + for (size_t i = 0; i < sizeof boxes / sizeof *boxes; ++i) { + const BoxInfo& box = boxes[i]; + if (!box.bytes.empty() && + JXL_ENC_SUCCESS != JxlEncoderAddBox(enc, box.type, box.bytes.data(), + box.bytes.size(), + params.compress_boxes)) { + fprintf(stderr, "JxlEncoderAddBox() failed (%s).\n", box.type); + return false; + } + } + JxlEncoderCloseBoxes(enc); + } + + for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) { + const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame]; + const jxl::extras::PackedImage& pimage = pframe.color; + JxlPixelFormat ppixelformat = pimage.format; + size_t num_interleaved_alpha = + (ppixelformat.num_channels - ppf.info.num_color_channels); + if (!SetupFrame(enc, settings, pframe.frame_info, params, ppf, num_frame, + num_alpha_channels, num_interleaved_alpha, option_idx)) { + return false; + } + if (JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(settings, &ppixelformat, + pimage.pixels(), + pimage.pixels_size)) { + fprintf(stderr, "JxlEncoderAddImageFrame() failed.\n"); + return false; + } + // Only set extra channel buffer if it is provided non-interleaved. + for (size_t i = 0; i < pframe.extra_channels.size(); ++i) { + if (JXL_ENC_SUCCESS != + JxlEncoderSetExtraChannelBuffer(settings, &ppixelformat, + pframe.extra_channels[i].pixels(), + pframe.extra_channels[i].stride * + pframe.extra_channels[i].ysize, + num_interleaved_alpha + i)) { + fprintf(stderr, "JxlEncoderSetExtraChannelBuffer() failed.\n"); + return false; + } + } + } + for (size_t fi = 0; fi < ppf.chunked_frames.size(); ++fi) { + ChunkedPackedFrame& chunked_frame = ppf.chunked_frames[fi]; + size_t num_interleaved_alpha = + (chunked_frame.format.num_channels - ppf.info.num_color_channels); + if (!SetupFrame(enc, settings, chunked_frame.frame_info, params, ppf, fi, + num_alpha_channels, num_interleaved_alpha, option_idx)) { + return false; + } + const bool last_frame = fi + 1 == ppf.chunked_frames.size(); + if (JXL_ENC_SUCCESS != + JxlEncoderAddChunkedFrame(settings, last_frame, + chunked_frame.GetInputSource())) { + fprintf(stderr, "JxlEncoderAddChunkedFrame() failed.\n"); + return false; + } + } + } + JxlEncoderCloseInput(enc); + if (params.HasOutputProcessor()) { + if (JXL_ENC_SUCCESS != JxlEncoderFlushInput(enc)) { + fprintf(stderr, "JxlEncoderAddChunkedFrame() failed.\n"); + return false; + } + } else if (!ReadCompressedOutput(enc, compressed)) { + return false; + } + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/enc/jxl.h b/lib/extras/enc/jxl.h new file mode 100644 index 0000000..b8ca5bd --- /dev/null +++ b/lib/extras/enc/jxl.h @@ -0,0 +1,91 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_EXTRAS_ENC_JXL_H_ +#define LIB_EXTRAS_ENC_JXL_H_ + +#include <jxl/encode.h> +#include <jxl/parallel_runner.h> +#include <jxl/thread_parallel_runner.h> +#include <jxl/types.h> +#include <stdint.h> + +#include <vector> + +#include "lib/extras/packed_image.h" + +namespace jxl { +namespace extras { + +struct JXLOption { + JXLOption(JxlEncoderFrameSettingId id, int64_t val, size_t frame_index) + : id(id), is_float(false), ival(val), frame_index(frame_index) {} + JXLOption(JxlEncoderFrameSettingId id, float val, size_t frame_index) + : id(id), is_float(true), fval(val), frame_index(frame_index) {} + + JxlEncoderFrameSettingId id; + bool is_float; + union { + int64_t ival; + float fval; + }; + size_t frame_index; +}; + +struct JXLCompressParams { + std::vector<JXLOption> options; + // Target butteraugli distance, 0.0 means lossless. + float distance = 1.0f; + float alpha_distance = 1.0f; + // If set to true, forces container mode. + bool use_container = false; + // Whether to enable/disable byte-exact jpeg reconstruction for jpeg inputs. + bool jpeg_store_metadata = true; + bool jpeg_strip_exif = false; + bool jpeg_strip_xmp = false; + bool jpeg_strip_jumbf = false; + // Whether to create brob boxes. + bool compress_boxes = true; + // Upper bound on the intensity level present in the image in nits (zero means + // that the library chooses a default). + float intensity_target = 0; + int already_downsampled = 1; + int upsampling_mode = -1; + // Overrides for bitdepth, codestream level and alpha premultiply. + size_t override_bitdepth = 0; + int32_t codestream_level = -1; + int32_t premultiply = -1; + // Override input buffer interpretation. + JxlBitDepth input_bitdepth = {JXL_BIT_DEPTH_FROM_PIXEL_FORMAT, 0, 0}; + // If runner_opaque is set, the decoder uses this parallel runner. + JxlParallelRunner runner = JxlThreadParallelRunner; + void* runner_opaque = nullptr; + JxlEncoderOutputProcessor output_processor = {}; + JxlDebugImageCallback debug_image = nullptr; + void* debug_image_opaque = nullptr; + JxlEncoderStats* stats = nullptr; + bool allow_expert_options = false; + + void AddOption(JxlEncoderFrameSettingId id, int64_t val) { + options.emplace_back(JXLOption(id, val, 0)); + } + void AddFloatOption(JxlEncoderFrameSettingId id, float val) { + options.emplace_back(JXLOption(id, val, 0)); + } + bool HasOutputProcessor() const { + return (output_processor.get_buffer != nullptr && + output_processor.release_buffer != nullptr && + output_processor.set_finalized_position != nullptr); + } +}; + +bool EncodeImageJXL(const JXLCompressParams& params, const PackedPixelFile& ppf, + const std::vector<uint8_t>* jpeg_bytes, + std::vector<uint8_t>* compressed); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_ENC_JXL_H_ diff --git a/lib/extras/enc/npy.cc b/lib/extras/enc/npy.cc index 1428e64..ae8cf13 100644 --- a/lib/extras/enc/npy.cc +++ b/lib/extras/enc/npy.cc @@ -5,13 +5,12 @@ #include "lib/extras/enc/npy.h" -#include <stdio.h> +#include <jxl/types.h> #include <sstream> #include <string> #include <vector> -#include "jxl/types.h" #include "lib/extras/packed_image.h" namespace jxl { diff --git a/lib/extras/enc/pgx.cc b/lib/extras/enc/pgx.cc index ef204ad..d4809e3 100644 --- a/lib/extras/enc/pgx.cc +++ b/lib/extras/enc/pgx.cc @@ -5,13 +5,11 @@ #include "lib/extras/enc/pgx.h" -#include <stdio.h> +#include <jxl/codestream_header.h> #include <string.h> -#include "jxl/codestream_header.h" #include "lib/extras/packed_image.h" #include "lib/jxl/base/byte_order.h" -#include "lib/jxl/base/printf_macros.h" namespace jxl { namespace extras { diff --git a/lib/extras/enc/pnm.cc b/lib/extras/enc/pnm.cc index 9b5f6cb..4183900 100644 --- a/lib/extras/enc/pnm.cc +++ b/lib/extras/enc/pnm.cc @@ -5,7 +5,6 @@ #include "lib/extras/enc/pnm.h" -#include <stdio.h> #include <string.h> #include <string> @@ -14,12 +13,9 @@ #include "lib/extras/packed_image.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/compiler_specific.h" -#include "lib/jxl/base/file_io.h" #include "lib/jxl/base/printf_macros.h" #include "lib/jxl/base/status.h" -#include "lib/jxl/color_management.h" #include "lib/jxl/dec_external_image.h" -#include "lib/jxl/enc_color_management.h" #include "lib/jxl/enc_external_image.h" #include "lib/jxl/enc_image_bundle.h" #include "lib/jxl/fields.h" // AllDefault @@ -32,66 +28,7 @@ namespace { constexpr size_t kMaxHeaderSize = 200; -Status EncodeHeader(const PackedImage& image, size_t bits_per_sample, - bool little_endian, char* header, int* chars_written) { - size_t num_channels = image.format.num_channels; - bool is_gray = num_channels <= 2; - bool has_alpha = num_channels == 2 || num_channels == 4; - if (has_alpha) { // PAM - if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits"); - const uint32_t max_val = (1U << bits_per_sample) - 1; - *chars_written = - snprintf(header, kMaxHeaderSize, - "P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS - "\nDEPTH %u\nMAXVAL %u\nTUPLTYPE %s\nENDHDR\n", - image.xsize, image.ysize, is_gray ? 2 : 4, max_val, - is_gray ? "GRAYSCALE_ALPHA" : "RGB_ALPHA"); - JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < - kMaxHeaderSize); - } else if (bits_per_sample == 32) { // PFM - const char type = is_gray ? 'f' : 'F'; - const double scale = little_endian ? -1.0 : 1.0; - *chars_written = - snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n", - type, image.xsize, image.ysize, scale); - JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < - kMaxHeaderSize); - } else { // PGM/PPM - if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits"); - const uint32_t max_val = (1U << bits_per_sample) - 1; - const char type = is_gray ? '5' : '6'; - *chars_written = - snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n", - type, image.xsize, image.ysize, max_val); - JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) < - kMaxHeaderSize); - } - return true; -} - -Status EncodeImagePNM(const PackedImage& image, size_t bits_per_sample, - std::vector<uint8_t>* bytes) { - // Choose native for PFM; PGM/PPM require big-endian - bool is_little_endian = bits_per_sample > 16 && IsLittleEndian(); - char header[kMaxHeaderSize]; - int header_size = 0; - JXL_RETURN_IF_ERROR(EncodeHeader(image, bits_per_sample, is_little_endian, - header, &header_size)); - bytes->resize(static_cast<size_t>(header_size) + image.pixels_size); - memcpy(bytes->data(), header, static_cast<size_t>(header_size)); - const bool flipped_y = bits_per_sample == 32; // PFMs are flipped - const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels()); - uint8_t* out = bytes->data() + header_size; - for (size_t y = 0; y < image.ysize; ++y) { - size_t y_out = flipped_y ? image.ysize - 1 - y : y; - const uint8_t* row_in = &in[y * image.stride]; - uint8_t* row_out = &out[y_out * image.stride]; - memcpy(row_out, row_in, image.stride); - } - return true; -} - -class PNMEncoder : public Encoder { +class BasePNMEncoder : public Encoder { public: Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, ThreadPool* pool = nullptr) const override { @@ -106,8 +43,8 @@ class PNMEncoder : public Encoder { for (const auto& frame : ppf.frames) { JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); encoded_image->bitstreams.emplace_back(); - JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.color, ppf.info.bits_per_sample, - &encoded_image->bitstreams.back())); + JXL_RETURN_IF_ERROR( + EncodeFrame(ppf, frame, &encoded_image->bitstreams.back())); } for (size_t i = 0; i < ppf.extra_channels_info.size(); ++i) { const auto& ec_info = ppf.extra_channels_info[i].ec_info; @@ -115,85 +52,258 @@ class PNMEncoder : public Encoder { auto& ec_bitstreams = encoded_image->extra_channel_bitstreams.back(); for (const auto& frame : ppf.frames) { ec_bitstreams.emplace_back(); - JXL_RETURN_IF_ERROR(EncodeImagePNM(frame.extra_channels[i], - ec_info.bits_per_sample, - &ec_bitstreams.back())); + JXL_RETURN_IF_ERROR(EncodeExtraChannel(frame.extra_channels[i], + ec_info.bits_per_sample, + &ec_bitstreams.back())); } } return true; } + + protected: + virtual Status EncodeFrame(const PackedPixelFile& ppf, + const PackedFrame& frame, + std::vector<uint8_t>* bytes) const = 0; + virtual Status EncodeExtraChannel(const PackedImage& image, + size_t bits_per_sample, + std::vector<uint8_t>* bytes) const = 0; }; +class PNMEncoder : public BasePNMEncoder { + public: + static const std::vector<JxlPixelFormat> kAcceptedFormats; + + std::vector<JxlPixelFormat> AcceptedFormats() const override { + return kAcceptedFormats; + } + + Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame, + std::vector<uint8_t>* bytes) const override { + return EncodeImage(frame.color, ppf.info.bits_per_sample, bytes); + } + Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) const override { + return EncodeImage(image, bits_per_sample, bytes); + } + + private: + Status EncodeImage(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) const { + uint32_t maxval = (1u << bits_per_sample) - 1; + char type = image.format.num_channels == 1 ? '5' : '6'; + char header[kMaxHeaderSize]; + size_t header_size = + snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%u\n", + type, image.xsize, image.ysize, maxval); + JXL_RETURN_IF_ERROR(header_size < kMaxHeaderSize); + bytes->resize(header_size + image.pixels_size); + memcpy(bytes->data(), header, header_size); + memcpy(bytes->data() + header_size, + reinterpret_cast<uint8_t*>(image.pixels()), image.pixels_size); + return true; + } +}; + +class PGMEncoder : public PNMEncoder { + public: + static const std::vector<JxlPixelFormat> kAcceptedFormats; + + std::vector<JxlPixelFormat> AcceptedFormats() const override { + return kAcceptedFormats; + } +}; + +const std::vector<JxlPixelFormat> PGMEncoder::kAcceptedFormats = { + JxlPixelFormat{1, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0}, + JxlPixelFormat{1, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}}; + class PPMEncoder : public PNMEncoder { public: + static const std::vector<JxlPixelFormat> kAcceptedFormats; + std::vector<JxlPixelFormat> AcceptedFormats() const override { - std::vector<JxlPixelFormat> formats; - for (const uint32_t num_channels : {1, 2, 3, 4}) { - for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { - for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { - formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, - /*data_type=*/data_type, - /*endianness=*/endianness, - /*align=*/0}); - } - } - } - return formats; + return kAcceptedFormats; } }; -class PFMEncoder : public PNMEncoder { +const std::vector<JxlPixelFormat> PPMEncoder::kAcceptedFormats = { + JxlPixelFormat{3, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0}, + JxlPixelFormat{3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}}; + +const std::vector<JxlPixelFormat> PNMEncoder::kAcceptedFormats = [] { + std::vector<JxlPixelFormat> combined = PPMEncoder::kAcceptedFormats; + combined.insert(combined.end(), PGMEncoder::kAcceptedFormats.begin(), + PGMEncoder::kAcceptedFormats.end()); + return combined; +}(); + +class PFMEncoder : public BasePNMEncoder { public: std::vector<JxlPixelFormat> AcceptedFormats() const override { std::vector<JxlPixelFormat> formats; for (const uint32_t num_channels : {1, 3}) { - for (const JxlDataType data_type : {JXL_TYPE_FLOAT16, JXL_TYPE_FLOAT}) { - for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { - formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, - /*data_type=*/data_type, - /*endianness=*/endianness, - /*align=*/0}); - } + for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/JXL_TYPE_FLOAT, + /*endianness=*/endianness, + /*align=*/0}); } } return formats; } -}; + Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame, + std::vector<uint8_t>* bytes) const override { + return EncodeImage(frame.color, bytes); + } + Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) const override { + return EncodeImage(image, bytes); + } -class PGMEncoder : public PPMEncoder { - public: - std::vector<JxlPixelFormat> AcceptedFormats() const override { - std::vector<JxlPixelFormat> formats = PPMEncoder::AcceptedFormats(); - for (auto it = formats.begin(); it != formats.end();) { - if (it->num_channels > 2) { - it = formats.erase(it); - } else { - ++it; - } + private: + Status EncodeImage(const PackedImage& image, + std::vector<uint8_t>* bytes) const { + char type = image.format.num_channels == 1 ? 'f' : 'F'; + double scale = image.format.endianness == JXL_LITTLE_ENDIAN ? -1.0 : 1.0; + char header[kMaxHeaderSize]; + size_t header_size = + snprintf(header, kMaxHeaderSize, "P%c\n%" PRIuS " %" PRIuS "\n%.1f\n", + type, image.xsize, image.ysize, scale); + JXL_RETURN_IF_ERROR(header_size < kMaxHeaderSize); + bytes->resize(header_size + image.pixels_size); + memcpy(bytes->data(), header, header_size); + const uint8_t* in = reinterpret_cast<const uint8_t*>(image.pixels()); + uint8_t* out = bytes->data() + header_size; + for (size_t y = 0; y < image.ysize; ++y) { + size_t y_out = image.ysize - 1 - y; + const uint8_t* row_in = &in[y * image.stride]; + uint8_t* row_out = &out[y_out * image.stride]; + memcpy(row_out, row_in, image.stride); } - return formats; + return true; } }; -class PAMEncoder : public PPMEncoder { +class PAMEncoder : public BasePNMEncoder { public: std::vector<JxlPixelFormat> AcceptedFormats() const override { - std::vector<JxlPixelFormat> formats = PPMEncoder::AcceptedFormats(); - for (auto it = formats.begin(); it != formats.end();) { - if (it->num_channels != 2 && it->num_channels != 4) { - it = formats.erase(it); - } else { - ++it; + std::vector<JxlPixelFormat> formats; + for (const uint32_t num_channels : {1, 2, 3, 4}) { + for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) { + formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, + /*data_type=*/data_type, + /*endianness=*/JXL_BIG_ENDIAN, + /*align=*/0}); } } return formats; } -}; + Status EncodeFrame(const PackedPixelFile& ppf, const PackedFrame& frame, + std::vector<uint8_t>* bytes) const override { + const PackedImage& color = frame.color; + const auto& ec_info = ppf.extra_channels_info; + JXL_RETURN_IF_ERROR(frame.extra_channels.size() == ec_info.size()); + for (const auto& ec : frame.extra_channels) { + if (ec.xsize != color.xsize || ec.ysize != color.ysize) { + return JXL_FAILURE("Extra channel and color size mismatch."); + } + if (ec.format.data_type != color.format.data_type || + ec.format.endianness != color.format.endianness) { + return JXL_FAILURE("Extra channel and color format mismatch."); + } + } + if (ppf.info.alpha_bits && + (ppf.info.bits_per_sample != ppf.info.alpha_bits)) { + return JXL_FAILURE("Alpha bit depth does not match image bit depth"); + } + for (const auto& it : ec_info) { + if (it.ec_info.bits_per_sample != ppf.info.bits_per_sample) { + return JXL_FAILURE( + "Extra channel bit depth does not match image bit depth"); + } + } + const char* kColorTypes[4] = {"GRAYSCALE", "GRAYSCALE_ALPHA", "RGB", + "RGB_ALPHA"}; + uint32_t maxval = (1u << ppf.info.bits_per_sample) - 1; + uint32_t depth = color.format.num_channels + ec_info.size(); + char header[kMaxHeaderSize]; + size_t pos = 0; + pos += snprintf(header + pos, kMaxHeaderSize - pos, + "P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS + "\nDEPTH %u\n" + "MAXVAL %u\nTUPLTYPE %s\n", + color.xsize, color.ysize, depth, maxval, + kColorTypes[color.format.num_channels - 1]); + JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize); + for (const auto& info : ec_info) { + pos += snprintf(header + pos, kMaxHeaderSize - pos, "TUPLTYPE %s\n", + ExtraChannelTypeName(info.ec_info.type).c_str()); + JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize); + } + pos += snprintf(header + pos, kMaxHeaderSize - pos, "ENDHDR\n"); + JXL_RETURN_IF_ERROR(pos < kMaxHeaderSize); + size_t total_size = color.pixels_size; + for (const auto& ec : frame.extra_channels) { + total_size += ec.pixels_size; + } + bytes->resize(pos + total_size); + memcpy(bytes->data(), header, pos); + // If we have no extra channels, just copy color pixel data over. + if (frame.extra_channels.empty()) { + memcpy(bytes->data() + pos, reinterpret_cast<uint8_t*>(color.pixels()), + color.pixels_size); + return true; + } + // Interleave color and extra channels. + const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels()); + std::vector<const uint8_t*> ec_in(frame.extra_channels.size()); + for (size_t i = 0; i < frame.extra_channels.size(); ++i) { + ec_in[i] = + reinterpret_cast<const uint8_t*>(frame.extra_channels[i].pixels()); + } + uint8_t* out = bytes->data() + pos; + size_t pwidth = PackedImage::BitsPerChannel(color.format.data_type) / 8; + for (size_t y = 0; y < color.ysize; ++y) { + for (size_t x = 0; x < color.xsize; ++x) { + memcpy(out, in, color.pixel_stride()); + out += color.pixel_stride(); + in += color.pixel_stride(); + for (auto& p : ec_in) { + memcpy(out, p, pwidth); + out += pwidth; + p += pwidth; + } + } + } + return true; + } + Status EncodeExtraChannel(const PackedImage& image, size_t bits_per_sample, + std::vector<uint8_t>* bytes) const override { + return true; + } -Span<const uint8_t> MakeSpan(const char* str) { - return Span<const uint8_t>(reinterpret_cast<const uint8_t*>(str), - strlen(str)); -} + private: + static std::string ExtraChannelTypeName(JxlExtraChannelType type) { + switch (type) { + case JXL_CHANNEL_ALPHA: + return std::string("Alpha"); + case JXL_CHANNEL_DEPTH: + return std::string("Depth"); + case JXL_CHANNEL_SPOT_COLOR: + return std::string("SpotColor"); + case JXL_CHANNEL_SELECTION_MASK: + return std::string("SelectionMask"); + case JXL_CHANNEL_BLACK: + return std::string("Black"); + case JXL_CHANNEL_CFA: + return std::string("CFA"); + case JXL_CHANNEL_THERMAL: + return std::string("Thermal"); + default: + return std::string("UNKNOWN"); + } + } +}; } // namespace @@ -201,6 +311,10 @@ std::unique_ptr<Encoder> GetPPMEncoder() { return jxl::make_unique<PPMEncoder>(); } +std::unique_ptr<Encoder> GetPNMEncoder() { + return jxl::make_unique<PNMEncoder>(); +} + std::unique_ptr<Encoder> GetPFMEncoder() { return jxl::make_unique<PFMEncoder>(); } diff --git a/lib/extras/enc/pnm.h b/lib/extras/enc/pnm.h index 403208c..1e0020c 100644 --- a/lib/extras/enc/pnm.h +++ b/lib/extras/enc/pnm.h @@ -19,6 +19,7 @@ namespace extras { std::unique_ptr<Encoder> GetPAMEncoder(); std::unique_ptr<Encoder> GetPGMEncoder(); +std::unique_ptr<Encoder> GetPNMEncoder(); std::unique_ptr<Encoder> GetPPMEncoder(); std::unique_ptr<Encoder> GetPFMEncoder(); |