libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

apng.cc (16417B)


      1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
      2 //
      3 // Use of this source code is governed by a BSD-style
      4 // license that can be found in the LICENSE file.
      5 
      6 #include "lib/extras/enc/apng.h"
      7 
      8 // Parts of this code are taken from apngdis, which has the following license:
      9 /* APNG Disassembler 2.8
     10  *
     11  * Deconstructs APNG files into individual frames.
     12  *
     13  * http://apngdis.sourceforge.net
     14  *
     15  * Copyright (c) 2010-2015 Max Stepin
     16  * maxst at users.sourceforge.net
     17  *
     18  * zlib license
     19  * ------------
     20  *
     21  * This software is provided 'as-is', without any express or implied
     22  * warranty.  In no event will the authors be held liable for any damages
     23  * arising from the use of this software.
     24  *
     25  * Permission is granted to anyone to use this software for any purpose,
     26  * including commercial applications, and to alter it and redistribute it
     27  * freely, subject to the following restrictions:
     28  *
     29  * 1. The origin of this software must not be misrepresented; you must not
     30  *    claim that you wrote the original software. If you use this software
     31  *    in a product, an acknowledgment in the product documentation would be
     32  *    appreciated but is not required.
     33  * 2. Altered source versions must be plainly marked as such, and must not be
     34  *    misrepresented as being the original software.
     35  * 3. This notice may not be removed or altered from any source distribution.
     36  *
     37  */
     38 
     39 #include <string.h>
     40 
     41 #include <string>
     42 #include <vector>
     43 
     44 #include "lib/extras/exif.h"
     45 #include "lib/jxl/base/byte_order.h"
     46 #include "lib/jxl/base/printf_macros.h"
     47 #if JPEGXL_ENABLE_APNG
     48 #include "png.h" /* original (unpatched) libpng is ok */
     49 #endif
     50 
     51 namespace jxl {
     52 namespace extras {
     53 
     54 #if JPEGXL_ENABLE_APNG
     55 namespace {
     56 
     57 constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
     58                                              0x66, 0x00, 0x00};
     59 
     60 class APNGEncoder : public Encoder {
     61  public:
     62   std::vector<JxlPixelFormat> AcceptedFormats() const override {
     63     std::vector<JxlPixelFormat> formats;
     64     for (const uint32_t num_channels : {1, 2, 3, 4}) {
     65       for (const JxlDataType data_type : {JXL_TYPE_UINT8, JXL_TYPE_UINT16}) {
     66         for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) {
     67           formats.push_back(
     68               JxlPixelFormat{num_channels, data_type, endianness, /*align=*/0});
     69         }
     70       }
     71     }
     72     return formats;
     73   }
     74   Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
     75                 ThreadPool* pool) const override {
     76     JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
     77     encoded_image->icc.clear();
     78     encoded_image->bitstreams.resize(1);
     79     return EncodePackedPixelFileToAPNG(ppf, pool,
     80                                        &encoded_image->bitstreams.front());
     81   }
     82 
     83  private:
     84   Status EncodePackedPixelFileToAPNG(const PackedPixelFile& ppf,
     85                                      ThreadPool* pool,
     86                                      std::vector<uint8_t>* bytes) const;
     87 };
     88 
     89 void PngWrite(png_structp png_ptr, png_bytep data, png_size_t length) {
     90   std::vector<uint8_t>* bytes =
     91       static_cast<std::vector<uint8_t>*>(png_get_io_ptr(png_ptr));
     92   bytes->insert(bytes->end(), data, data + length);
     93 }
     94 
     95 // Stores XMP and EXIF/IPTC into key/value strings for PNG
     96 class BlobsWriterPNG {
     97  public:
     98   static Status Encode(const PackedMetadata& blobs,
     99                        std::vector<std::string>* strings) {
    100     if (!blobs.exif.empty()) {
    101       // PNG viewers typically ignore Exif orientation but not all of them do
    102       // (and e.g. cjxl doesn't), so we overwrite the Exif orientation to the
    103       // identity to avoid repeated orientation.
    104       std::vector<uint8_t> exif = blobs.exif;
    105       ResetExifOrientation(exif);
    106       // By convention, the data is prefixed with "Exif\0\0" when stored in
    107       // the legacy (and non-standard) "Raw profile type exif" text chunk
    108       // currently used here.
    109       // TODO(user): Store Exif data in an eXIf chunk instead, which always
    110       //             begins with the TIFF header.
    111       if (exif.size() >= sizeof kExifSignature &&
    112           memcmp(exif.data(), kExifSignature, sizeof kExifSignature) != 0) {
    113         exif.insert(exif.begin(), kExifSignature,
    114                     kExifSignature + sizeof kExifSignature);
    115       }
    116       JXL_RETURN_IF_ERROR(EncodeBase16("exif", exif, strings));
    117     }
    118     if (!blobs.iptc.empty()) {
    119       JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings));
    120     }
    121     if (!blobs.xmp.empty()) {
    122       // TODO(user): Store XMP data in an "XML:com.adobe.xmp" text chunk
    123       //             instead.
    124       JXL_RETURN_IF_ERROR(EncodeBase16("xmp", blobs.xmp, strings));
    125     }
    126     return true;
    127   }
    128 
    129  private:
    130   static JXL_INLINE char EncodeNibble(const uint8_t nibble) {
    131     JXL_ASSERT(nibble < 16);
    132     return (nibble < 10) ? '0' + nibble : 'a' + nibble - 10;
    133   }
    134 
    135   static Status EncodeBase16(const std::string& type,
    136                              const std::vector<uint8_t>& bytes,
    137                              std::vector<std::string>* strings) {
    138     // Encoding: base16 with newline after 72 chars.
    139     const size_t base16_size =
    140         2 * bytes.size() + DivCeil(bytes.size(), static_cast<size_t>(36)) + 1;
    141     std::string base16;
    142     base16.reserve(base16_size);
    143     for (size_t i = 0; i < bytes.size(); ++i) {
    144       if (i % 36 == 0) base16.push_back('\n');
    145       base16.push_back(EncodeNibble(bytes[i] >> 4));
    146       base16.push_back(EncodeNibble(bytes[i] & 0x0F));
    147     }
    148     base16.push_back('\n');
    149     JXL_ASSERT(base16.length() == base16_size);
    150 
    151     char key[30];
    152     snprintf(key, sizeof(key), "Raw profile type %s", type.c_str());
    153 
    154     char header[30];
    155     snprintf(header, sizeof(header), "\n%s\n%8" PRIuS, type.c_str(),
    156              bytes.size());
    157 
    158     strings->emplace_back(key);
    159     strings->push_back(std::string(header) + base16);
    160     return true;
    161   }
    162 };
    163 
    164 void MaybeAddCICP(const JxlColorEncoding& c_enc, png_structp png_ptr,
    165                   png_infop info_ptr) {
    166   png_byte cicp_data[4] = {};
    167   png_unknown_chunk cicp_chunk;
    168   if (c_enc.color_space != JXL_COLOR_SPACE_RGB) {
    169     return;
    170   }
    171   if (c_enc.primaries == JXL_PRIMARIES_P3) {
    172     if (c_enc.white_point == JXL_WHITE_POINT_D65) {
    173       cicp_data[0] = 12;
    174     } else if (c_enc.white_point == JXL_WHITE_POINT_DCI) {
    175       cicp_data[0] = 11;
    176     } else {
    177       return;
    178     }
    179   } else if (c_enc.primaries != JXL_PRIMARIES_CUSTOM &&
    180              c_enc.white_point == JXL_WHITE_POINT_D65) {
    181     cicp_data[0] = static_cast<png_byte>(c_enc.primaries);
    182   } else {
    183     return;
    184   }
    185   if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_UNKNOWN ||
    186       c_enc.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) {
    187     return;
    188   }
    189   cicp_data[1] = static_cast<png_byte>(c_enc.transfer_function);
    190   cicp_data[2] = 0;
    191   cicp_data[3] = 1;
    192   cicp_chunk.data = cicp_data;
    193   cicp_chunk.size = sizeof(cicp_data);
    194   cicp_chunk.location = PNG_HAVE_IHDR;
    195   memcpy(cicp_chunk.name, "cICP", 5);
    196   png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS,
    197                               reinterpret_cast<const png_byte*>("cICP"), 1);
    198   png_set_unknown_chunks(png_ptr, info_ptr, &cicp_chunk, 1);
    199 }
    200 
    201 bool MaybeAddSRGB(const JxlColorEncoding& c_enc, png_structp png_ptr,
    202                   png_infop info_ptr) {
    203   if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_SRGB &&
    204       (c_enc.color_space == JXL_COLOR_SPACE_GRAY ||
    205        (c_enc.color_space == JXL_COLOR_SPACE_RGB &&
    206         c_enc.primaries == JXL_PRIMARIES_SRGB &&
    207         c_enc.white_point == JXL_WHITE_POINT_D65))) {
    208     png_set_sRGB(png_ptr, info_ptr, c_enc.rendering_intent);
    209     png_set_cHRM_fixed(png_ptr, info_ptr, 31270, 32900, 64000, 33000, 30000,
    210                        60000, 15000, 6000);
    211     png_set_gAMA_fixed(png_ptr, info_ptr, 45455);
    212     return true;
    213   }
    214   return false;
    215 }
    216 
    217 void MaybeAddCHRM(const JxlColorEncoding& c_enc, png_structp png_ptr,
    218                   png_infop info_ptr) {
    219   if (c_enc.color_space != JXL_COLOR_SPACE_RGB) return;
    220   if (c_enc.primaries == 0) return;
    221   png_set_cHRM(png_ptr, info_ptr, c_enc.white_point_xy[0],
    222                c_enc.white_point_xy[1], c_enc.primaries_red_xy[0],
    223                c_enc.primaries_red_xy[1], c_enc.primaries_green_xy[0],
    224                c_enc.primaries_green_xy[1], c_enc.primaries_blue_xy[0],
    225                c_enc.primaries_blue_xy[1]);
    226 }
    227 
    228 void MaybeAddGAMA(const JxlColorEncoding& c_enc, png_structp png_ptr,
    229                   png_infop info_ptr) {
    230   switch (c_enc.transfer_function) {
    231     case JXL_TRANSFER_FUNCTION_LINEAR:
    232       png_set_gAMA_fixed(png_ptr, info_ptr, PNG_FP_1);
    233       break;
    234     case JXL_TRANSFER_FUNCTION_SRGB:
    235       png_set_gAMA_fixed(png_ptr, info_ptr, 45455);
    236       break;
    237     case JXL_TRANSFER_FUNCTION_GAMMA:
    238       png_set_gAMA(png_ptr, info_ptr, c_enc.gamma);
    239       break;
    240 
    241     default:;
    242       // No gAMA chunk.
    243   }
    244 }
    245 
    246 void MaybeAddCLLi(const JxlColorEncoding& c_enc, const float intensity_target,
    247                   png_structp png_ptr, png_infop info_ptr) {
    248   if (c_enc.transfer_function != JXL_TRANSFER_FUNCTION_PQ) return;
    249 
    250   const uint32_t max_cll =
    251       static_cast<uint32_t>(10000.f * Clamp1(intensity_target, 0.f, 10000.f));
    252   png_byte chunk_data[8] = {};
    253   chunk_data[0] = (max_cll >> 24) & 0xFF;
    254   chunk_data[1] = (max_cll >> 16) & 0xFF;
    255   chunk_data[2] = (max_cll >> 8) & 0xFF;
    256   chunk_data[3] = max_cll & 0xFF;
    257   // Leave MaxFALL set to 0.
    258   png_unknown_chunk chunk;
    259   memcpy(chunk.name, "cLLi", 5);
    260   chunk.data = chunk_data;
    261   chunk.size = sizeof chunk_data;
    262   chunk.location = PNG_HAVE_IHDR;
    263   png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS,
    264                               reinterpret_cast<const png_byte*>("cLLi"), 1);
    265   png_set_unknown_chunks(png_ptr, info_ptr, &chunk, 1);
    266 }
    267 
    268 Status APNGEncoder::EncodePackedPixelFileToAPNG(
    269     const PackedPixelFile& ppf, ThreadPool* pool,
    270     std::vector<uint8_t>* bytes) const {
    271   size_t xsize = ppf.info.xsize;
    272   size_t ysize = ppf.info.ysize;
    273   bool has_alpha = ppf.info.alpha_bits != 0;
    274   bool is_gray = ppf.info.num_color_channels == 1;
    275   size_t color_channels = ppf.info.num_color_channels;
    276   size_t num_channels = color_channels + (has_alpha ? 1 : 0);
    277   size_t num_samples = num_channels * xsize * ysize;
    278 
    279   if (!ppf.info.have_animation && ppf.frames.size() != 1) {
    280     return JXL_FAILURE("Invalid number of frames");
    281   }
    282 
    283   size_t count = 0;
    284   size_t anim_chunks = 0;
    285 
    286   for (const auto& frame : ppf.frames) {
    287     JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info));
    288 
    289     const PackedImage& color = frame.color;
    290     const JxlPixelFormat format = color.format;
    291     const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels());
    292     size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type);
    293     size_t bytes_per_sample = data_bits_per_sample / 8;
    294     size_t out_bytes_per_sample = bytes_per_sample > 1 ? 2 : 1;
    295     size_t out_stride = xsize * num_channels * out_bytes_per_sample;
    296     size_t out_size = ysize * out_stride;
    297     std::vector<uint8_t> out(out_size);
    298 
    299     if (format.data_type == JXL_TYPE_UINT8) {
    300       if (ppf.info.bits_per_sample < 8) {
    301         float mul = 255.0 / ((1u << ppf.info.bits_per_sample) - 1);
    302         for (size_t i = 0; i < num_samples; ++i) {
    303           out[i] = static_cast<uint8_t>(in[i] * mul + 0.5);
    304         }
    305       } else {
    306         memcpy(out.data(), in, out_size);
    307       }
    308     } else if (format.data_type == JXL_TYPE_UINT16) {
    309       if (ppf.info.bits_per_sample < 16 ||
    310           format.endianness != JXL_BIG_ENDIAN) {
    311         float mul = 65535.0 / ((1u << ppf.info.bits_per_sample) - 1);
    312         const uint8_t* p_in = in;
    313         uint8_t* p_out = out.data();
    314         for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) {
    315           uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE16(p_in)
    316                                                               : LoadLE16(p_in));
    317           StoreBE16(static_cast<uint32_t>(val * mul + 0.5), p_out);
    318         }
    319       } else {
    320         memcpy(out.data(), in, out_size);
    321       }
    322     }
    323     png_structp png_ptr;
    324     png_infop info_ptr;
    325 
    326     png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr,
    327                                       nullptr);
    328 
    329     if (!png_ptr) return JXL_FAILURE("Could not init png encoder");
    330 
    331     info_ptr = png_create_info_struct(png_ptr);
    332     if (!info_ptr) return JXL_FAILURE("Could not init png info struct");
    333 
    334     png_set_write_fn(png_ptr, bytes, PngWrite, nullptr);
    335     png_set_flush(png_ptr, 0);
    336 
    337     int width = xsize;
    338     int height = ysize;
    339 
    340     png_byte color_type = (is_gray ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB);
    341     if (has_alpha) color_type |= PNG_COLOR_MASK_ALPHA;
    342     png_byte bit_depth = out_bytes_per_sample * 8;
    343 
    344     png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type,
    345                  PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE,
    346                  PNG_FILTER_TYPE_BASE);
    347     if (count == 0) {
    348       if (!MaybeAddSRGB(ppf.color_encoding, png_ptr, info_ptr)) {
    349         MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr);
    350         if (!ppf.icc.empty()) {
    351           png_set_benign_errors(png_ptr, 1);
    352           png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(),
    353                        ppf.icc.size());
    354         }
    355         MaybeAddCHRM(ppf.color_encoding, png_ptr, info_ptr);
    356         MaybeAddGAMA(ppf.color_encoding, png_ptr, info_ptr);
    357       }
    358       MaybeAddCLLi(ppf.color_encoding, ppf.info.intensity_target, png_ptr,
    359                    info_ptr);
    360 
    361       std::vector<std::string> textstrings;
    362       JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings));
    363       for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) {
    364         png_text text;
    365         text.key = const_cast<png_charp>(textstrings[kk].c_str());
    366         text.text = const_cast<png_charp>(textstrings[kk + 1].c_str());
    367         text.compression = PNG_TEXT_COMPRESSION_zTXt;
    368         png_set_text(png_ptr, info_ptr, &text, 1);
    369       }
    370 
    371       png_write_info(png_ptr, info_ptr);
    372     } else {
    373       // fake writing a header, otherwise libpng gets confused
    374       size_t pos = bytes->size();
    375       png_write_info(png_ptr, info_ptr);
    376       bytes->resize(pos);
    377     }
    378 
    379     if (ppf.info.have_animation) {
    380       if (count == 0) {
    381         png_byte adata[8];
    382         png_save_uint_32(adata, ppf.frames.size());
    383         png_save_uint_32(adata + 4, ppf.info.animation.num_loops);
    384         png_byte actl[5] = "acTL";
    385         png_write_chunk(png_ptr, actl, adata, 8);
    386       }
    387       png_byte fdata[26];
    388       // TODO(jon): also make this work for the non-coalesced case
    389       png_save_uint_32(fdata, anim_chunks++);
    390       png_save_uint_32(fdata + 4, width);
    391       png_save_uint_32(fdata + 8, height);
    392       png_save_uint_32(fdata + 12, 0);
    393       png_save_uint_32(fdata + 16, 0);
    394       png_save_uint_16(fdata + 20, frame.frame_info.duration *
    395                                        ppf.info.animation.tps_denominator);
    396       png_save_uint_16(fdata + 22, ppf.info.animation.tps_numerator);
    397       fdata[24] = 1;
    398       fdata[25] = 0;
    399       png_byte fctl[5] = "fcTL";
    400       png_write_chunk(png_ptr, fctl, fdata, 26);
    401     }
    402 
    403     std::vector<uint8_t*> rows(height);
    404     for (int y = 0; y < height; ++y) {
    405       rows[y] = out.data() + y * out_stride;
    406     }
    407 
    408     png_write_flush(png_ptr);
    409     const size_t pos = bytes->size();
    410     png_write_image(png_ptr, rows.data());
    411     png_write_flush(png_ptr);
    412     if (count > 0) {
    413       std::vector<uint8_t> fdata(4);
    414       png_save_uint_32(fdata.data(), anim_chunks++);
    415       size_t p = pos;
    416       while (p + 8 < bytes->size()) {
    417         size_t len = png_get_uint_32(bytes->data() + p);
    418         JXL_ASSERT(bytes->operator[](p + 4) == 'I');
    419         JXL_ASSERT(bytes->operator[](p + 5) == 'D');
    420         JXL_ASSERT(bytes->operator[](p + 6) == 'A');
    421         JXL_ASSERT(bytes->operator[](p + 7) == 'T');
    422         fdata.insert(fdata.end(), bytes->data() + p + 8,
    423                      bytes->data() + p + 8 + len);
    424         p += len + 12;
    425       }
    426       bytes->resize(pos);
    427 
    428       png_byte fdat[5] = "fdAT";
    429       png_write_chunk(png_ptr, fdat, fdata.data(), fdata.size());
    430     }
    431 
    432     count++;
    433     if (count == ppf.frames.size() || !ppf.info.have_animation) {
    434       png_write_end(png_ptr, nullptr);
    435     }
    436 
    437     png_destroy_write_struct(&png_ptr, &info_ptr);
    438   }
    439 
    440   return true;
    441 }
    442 
    443 }  // namespace
    444 #endif
    445 
    446 std::unique_ptr<Encoder> GetAPNGEncoder() {
    447 #if JPEGXL_ENABLE_APNG
    448   return jxl::make_unique<APNGEncoder>();
    449 #else
    450   return nullptr;
    451 #endif
    452 }
    453 
    454 }  // namespace extras
    455 }  // namespace jxl