tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

apng.cc (18932B)


      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 <cstring>
     40 #include <string>
     41 #include <vector>
     42 
     43 #include "lib/extras/exif.h"
     44 #include "lib/jxl/base/byte_order.h"
     45 #include "lib/jxl/base/printf_macros.h"
     46 #if JPEGXL_ENABLE_APNG
     47 #include "png.h" /* original (unpatched) libpng is ok */
     48 #endif
     49 
     50 namespace jxl {
     51 namespace extras {
     52 
     53 #if JPEGXL_ENABLE_APNG
     54 namespace {
     55 
     56 constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
     57                                             0x66, 0x00, 0x00};
     58 
     59 class APNGEncoder : public Encoder {
     60 public:
     61  std::vector<JxlPixelFormat> AcceptedFormats() const override {
     62    std::vector<JxlPixelFormat> formats;
     63    for (const uint32_t num_channels : {1, 2, 3, 4}) {
     64      for (const JxlDataType data_type :
     65           {JXL_TYPE_UINT8, JXL_TYPE_UINT16, JXL_TYPE_FLOAT}) {
     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    // Encode main image frames
     77    JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
     78    encoded_image->icc.clear();
     79    encoded_image->bitstreams.resize(1);
     80    JXL_RETURN_IF_ERROR(EncodePackedPixelFileToAPNG(
     81        ppf, pool, &encoded_image->bitstreams.front()));
     82 
     83    // Encode extra channels
     84    for (size_t i = 0; i < ppf.extra_channels_info.size(); ++i) {
     85      encoded_image->extra_channel_bitstreams.emplace_back();
     86      auto& ec_bitstreams = encoded_image->extra_channel_bitstreams.back();
     87      ec_bitstreams.emplace_back();
     88      JXL_RETURN_IF_ERROR(EncodePackedPixelFileToAPNG(
     89          ppf, pool, &ec_bitstreams.back(), true, i));
     90    }
     91    return true;
     92  }
     93 
     94 private:
     95  Status EncodePackedPixelFileToAPNG(const PackedPixelFile& ppf,
     96                                     ThreadPool* pool,
     97                                     std::vector<uint8_t>* bytes,
     98                                     bool encode_extra_channels = false,
     99                                     size_t extra_channel_index = 0) const;
    100 };
    101 
    102 void PngWrite(png_structp png_ptr, png_bytep data, png_size_t length) {
    103  std::vector<uint8_t>* bytes =
    104      static_cast<std::vector<uint8_t>*>(png_get_io_ptr(png_ptr));
    105  bytes->insert(bytes->end(), data, data + length);
    106 }
    107 
    108 // Stores XMP and EXIF/IPTC into key/value strings for PNG
    109 class BlobsWriterPNG {
    110 public:
    111  static Status Encode(const PackedMetadata& blobs,
    112                       std::vector<std::string>* strings) {
    113    if (!blobs.exif.empty()) {
    114      // PNG viewers typically ignore Exif orientation but not all of them do
    115      // (and e.g. cjxl doesn't), so we overwrite the Exif orientation to the
    116      // identity to avoid repeated orientation.
    117      std::vector<uint8_t> exif = blobs.exif;
    118      ResetExifOrientation(exif);
    119      // By convention, the data is prefixed with "Exif\0\0" when stored in
    120      // the legacy (and non-standard) "Raw profile type exif" text chunk
    121      // currently used here.
    122      // TODO(user): Store Exif data in an eXIf chunk instead, which always
    123      //             begins with the TIFF header.
    124      if (exif.size() >= sizeof kExifSignature &&
    125          memcmp(exif.data(), kExifSignature, sizeof kExifSignature) != 0) {
    126        exif.insert(exif.begin(), kExifSignature,
    127                    kExifSignature + sizeof kExifSignature);
    128      }
    129      JXL_RETURN_IF_ERROR(EncodeBase16("exif", exif, strings));
    130    }
    131    if (!blobs.iptc.empty()) {
    132      JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings));
    133    }
    134    if (!blobs.xmp.empty()) {
    135      // TODO(user): Store XMP data in an "XML:com.adobe.xmp" text chunk
    136      //             instead.
    137      JXL_RETURN_IF_ERROR(EncodeBase16("xmp", blobs.xmp, strings));
    138    }
    139    return true;
    140  }
    141 
    142 private:
    143  // TODO(eustas): use array
    144  static JXL_INLINE char EncodeNibble(const uint8_t nibble) {
    145    if (nibble < 16) {
    146      return (nibble < 10) ? '0' + nibble : 'a' + nibble - 10;
    147    } else {
    148      JXL_DEBUG_ABORT("Internal logic error");
    149      return 0;
    150    }
    151  }
    152 
    153  static Status EncodeBase16(const std::string& type,
    154                             const std::vector<uint8_t>& bytes,
    155                             std::vector<std::string>* strings) {
    156    // Encoding: base16 with newline after 72 chars.
    157    const size_t base16_size =
    158        2 * bytes.size() + DivCeil(bytes.size(), static_cast<size_t>(36)) + 1;
    159    std::string base16;
    160    base16.reserve(base16_size);
    161    for (size_t i = 0; i < bytes.size(); ++i) {
    162      if (i % 36 == 0) base16.push_back('\n');
    163      base16.push_back(EncodeNibble(bytes[i] >> 4));
    164      base16.push_back(EncodeNibble(bytes[i] & 0x0F));
    165    }
    166    base16.push_back('\n');
    167    JXL_ENSURE(base16.length() == base16_size);
    168 
    169    char key[30];
    170    snprintf(key, sizeof(key), "Raw profile type %s", type.c_str());
    171 
    172    char header[30];
    173    snprintf(header, sizeof(header), "\n%s\n%8" PRIuS, type.c_str(),
    174             bytes.size());
    175 
    176    strings->emplace_back(key);
    177    strings->push_back(std::string(header) + base16);
    178    return true;
    179  }
    180 };
    181 
    182 void MaybeAddCICP(const JxlColorEncoding& c_enc, png_structp png_ptr,
    183                  png_infop info_ptr) {
    184  png_byte cicp_data[4] = {};
    185  png_unknown_chunk cicp_chunk;
    186  if (c_enc.color_space != JXL_COLOR_SPACE_RGB) {
    187    return;
    188  }
    189  if (c_enc.primaries == JXL_PRIMARIES_P3) {
    190    if (c_enc.white_point == JXL_WHITE_POINT_D65) {
    191      cicp_data[0] = 12;
    192    } else if (c_enc.white_point == JXL_WHITE_POINT_DCI) {
    193      cicp_data[0] = 11;
    194    } else {
    195      return;
    196    }
    197  } else if (c_enc.primaries != JXL_PRIMARIES_CUSTOM &&
    198             c_enc.white_point == JXL_WHITE_POINT_D65) {
    199    cicp_data[0] = static_cast<png_byte>(c_enc.primaries);
    200  } else {
    201    return;
    202  }
    203  if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_UNKNOWN ||
    204      c_enc.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) {
    205    return;
    206  }
    207  cicp_data[1] = static_cast<png_byte>(c_enc.transfer_function);
    208  cicp_data[2] = 0;
    209  cicp_data[3] = 1;
    210  cicp_chunk.data = cicp_data;
    211  cicp_chunk.size = sizeof(cicp_data);
    212  cicp_chunk.location = PNG_HAVE_IHDR;
    213  memcpy(cicp_chunk.name, "cICP", 5);
    214  png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS,
    215                              reinterpret_cast<const png_byte*>("cICP"), 1);
    216  png_set_unknown_chunks(png_ptr, info_ptr, &cicp_chunk, 1);
    217 }
    218 
    219 bool MaybeAddSRGB(const JxlColorEncoding& c_enc, png_structp png_ptr,
    220                  png_infop info_ptr) {
    221  if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_SRGB &&
    222      (c_enc.color_space == JXL_COLOR_SPACE_GRAY ||
    223       (c_enc.color_space == JXL_COLOR_SPACE_RGB &&
    224        c_enc.primaries == JXL_PRIMARIES_SRGB &&
    225        c_enc.white_point == JXL_WHITE_POINT_D65))) {
    226    png_set_sRGB(png_ptr, info_ptr, c_enc.rendering_intent);
    227    png_set_cHRM_fixed(png_ptr, info_ptr, 31270, 32900, 64000, 33000, 30000,
    228                       60000, 15000, 6000);
    229    png_set_gAMA_fixed(png_ptr, info_ptr, 45455);
    230    return true;
    231  }
    232  return false;
    233 }
    234 
    235 void MaybeAddCHRM(const JxlColorEncoding& c_enc, png_structp png_ptr,
    236                  png_infop info_ptr) {
    237  if (c_enc.color_space != JXL_COLOR_SPACE_RGB) return;
    238  if (c_enc.primaries == 0) return;
    239  png_set_cHRM(png_ptr, info_ptr, c_enc.white_point_xy[0],
    240               c_enc.white_point_xy[1], c_enc.primaries_red_xy[0],
    241               c_enc.primaries_red_xy[1], c_enc.primaries_green_xy[0],
    242               c_enc.primaries_green_xy[1], c_enc.primaries_blue_xy[0],
    243               c_enc.primaries_blue_xy[1]);
    244 }
    245 
    246 void MaybeAddGAMA(const JxlColorEncoding& c_enc, png_structp png_ptr,
    247                  png_infop info_ptr) {
    248  switch (c_enc.transfer_function) {
    249    case JXL_TRANSFER_FUNCTION_LINEAR:
    250      png_set_gAMA_fixed(png_ptr, info_ptr, PNG_FP_1);
    251      break;
    252    case JXL_TRANSFER_FUNCTION_SRGB:
    253      png_set_gAMA_fixed(png_ptr, info_ptr, 45455);
    254      break;
    255    case JXL_TRANSFER_FUNCTION_GAMMA:
    256      png_set_gAMA(png_ptr, info_ptr, c_enc.gamma);
    257      break;
    258 
    259    default:;
    260      // No gAMA chunk.
    261  }
    262 }
    263 
    264 void MaybeAddCLLi(const JxlColorEncoding& c_enc, const float intensity_target,
    265                  png_structp png_ptr, png_infop info_ptr) {
    266  if (c_enc.transfer_function != JXL_TRANSFER_FUNCTION_PQ) return;
    267  if (intensity_target == 10'000) return;
    268 
    269  const uint32_t max_content_light_level =
    270      static_cast<uint32_t>(10'000.f * Clamp1(intensity_target, 0.f, 10'000.f));
    271  png_byte chunk_data[8] = {};
    272  png_save_uint_32(chunk_data, max_content_light_level);
    273  // Leave MaxFALL set to 0.
    274  png_unknown_chunk chunk;
    275  memcpy(chunk.name, "cLLi", 5);
    276  chunk.data = chunk_data;
    277  chunk.size = sizeof chunk_data;
    278  chunk.location = PNG_HAVE_IHDR;
    279  png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS,
    280                              reinterpret_cast<const png_byte*>("cLLi"), 1);
    281  png_set_unknown_chunks(png_ptr, info_ptr, &chunk, 1);
    282 }
    283 
    284 Status APNGEncoder::EncodePackedPixelFileToAPNG(
    285    const PackedPixelFile& ppf, ThreadPool* pool, std::vector<uint8_t>* bytes,
    286    bool encode_extra_channels, size_t extra_channel_index) const {
    287  JxlExtraChannelInfo ec_info{};
    288  if (encode_extra_channels) {
    289    if (ppf.extra_channels_info.size() <= extra_channel_index) {
    290      return JXL_FAILURE("Invalid index for extra channel");
    291    }
    292    ec_info = ppf.extra_channels_info[extra_channel_index].ec_info;
    293  }
    294 
    295  bool has_alpha = !encode_extra_channels && (ppf.info.alpha_bits != 0);
    296  bool is_gray = encode_extra_channels || (ppf.info.num_color_channels == 1);
    297  size_t color_channels =
    298      encode_extra_channels ? 1 : ppf.info.num_color_channels;
    299  size_t num_channels = color_channels + (has_alpha ? 1 : 0);
    300 
    301  if (!ppf.info.have_animation && ppf.frames.size() != 1) {
    302    return JXL_FAILURE("Invalid number of frames");
    303  }
    304 
    305  size_t count = 0;
    306  size_t anim_chunks = 0;
    307 
    308  for (const auto& frame : ppf.frames) {
    309    const PackedImage& color = encode_extra_channels
    310                                   ? frame.extra_channels[extra_channel_index]
    311                                   : frame.color;
    312 
    313    size_t xsize = color.xsize;
    314    size_t ysize = color.ysize;
    315    size_t num_samples = num_channels * xsize * ysize;
    316 
    317    uint32_t bits_per_sample = encode_extra_channels ? ec_info.bits_per_sample
    318                                                     : ppf.info.bits_per_sample;
    319    if (!encode_extra_channels) {
    320      JXL_RETURN_IF_ERROR(VerifyPackedImage(color, ppf.info));
    321    } else {
    322      JXL_RETURN_IF_ERROR(VerifyFormat(color.format));
    323      JXL_RETURN_IF_ERROR(VerifyBitDepth(color.format.data_type,
    324                                         bits_per_sample,
    325                                         ec_info.exponent_bits_per_sample));
    326    }
    327    const JxlPixelFormat format = color.format;
    328    const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels());
    329    JXL_RETURN_IF_ERROR(PackedImage::ValidateDataType(format.data_type));
    330    size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type);
    331    size_t bytes_per_sample = data_bits_per_sample / 8;
    332    size_t out_bytes_per_sample = bytes_per_sample > 1 ? 2 : 1;
    333    size_t out_stride = xsize * num_channels * out_bytes_per_sample;
    334    size_t out_size = ysize * out_stride;
    335    std::vector<uint8_t> out(out_size);
    336 
    337    if (format.data_type == JXL_TYPE_UINT8) {
    338      if (bits_per_sample < 8) {
    339        float mul = 255.0 / ((1u << bits_per_sample) - 1);
    340        for (size_t i = 0; i < num_samples; ++i) {
    341          out[i] = static_cast<uint8_t>(std::lroundf(in[i] * mul));
    342        }
    343      } else {
    344        memcpy(out.data(), in, out_size);
    345      }
    346    } else if (format.data_type == JXL_TYPE_UINT16) {
    347      if (bits_per_sample < 16 || format.endianness != JXL_BIG_ENDIAN) {
    348        float mul = 65535.0 / ((1u << bits_per_sample) - 1);
    349        const uint8_t* p_in = in;
    350        uint8_t* p_out = out.data();
    351        for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) {
    352          uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE16(p_in)
    353                                                              : LoadLE16(p_in));
    354          StoreBE16(static_cast<uint32_t>(std::lroundf(val * mul)), p_out);
    355        }
    356      } else {
    357        memcpy(out.data(), in, out_size);
    358      }
    359    } else if (format.data_type == JXL_TYPE_FLOAT) {
    360      constexpr float kMul = 65535.0;
    361      const uint8_t* p_in = in;
    362      uint8_t* p_out = out.data();
    363      for (size_t i = 0; i < num_samples;
    364           ++i, p_in += sizeof(float), p_out += 2) {
    365        float val =
    366            Clamp1(format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat(p_in)
    367                   : format.endianness == JXL_LITTLE_ENDIAN
    368                       ? LoadLEFloat(p_in)
    369                       : *reinterpret_cast<const float*>(p_in),
    370                   0.f, 1.f);
    371        StoreBE16(static_cast<uint32_t>(std::lroundf(val * kMul)), p_out);
    372      }
    373    }
    374    png_structp png_ptr;
    375    png_infop info_ptr;
    376 
    377    png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr,
    378                                      nullptr);
    379 
    380    if (!png_ptr) return JXL_FAILURE("Could not init png encoder");
    381 
    382    info_ptr = png_create_info_struct(png_ptr);
    383    if (!info_ptr) return JXL_FAILURE("Could not init png info struct");
    384    png_set_compression_level(png_ptr, 1);
    385 
    386    png_set_write_fn(png_ptr, bytes, PngWrite, nullptr);
    387    png_set_flush(png_ptr, 0);
    388 
    389    int width = xsize;
    390    int height = ysize;
    391 
    392    png_byte color_type = (is_gray ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB);
    393    if (has_alpha) color_type |= PNG_COLOR_MASK_ALPHA;
    394    png_byte bit_depth = out_bytes_per_sample * 8;
    395 
    396    png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type,
    397                 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE,
    398                 PNG_FILTER_TYPE_BASE);
    399    if (count == 0 && !encode_extra_channels) {
    400      if (!MaybeAddSRGB(ppf.color_encoding, png_ptr, info_ptr)) {
    401        if (ppf.primary_color_representation !=
    402            PackedPixelFile::kIccIsPrimary) {
    403          MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr);
    404        }
    405        if (!ppf.icc.empty()) {
    406          png_set_benign_errors(png_ptr, 1);
    407          png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(),
    408                       ppf.icc.size());
    409        }
    410        MaybeAddCHRM(ppf.color_encoding, png_ptr, info_ptr);
    411        MaybeAddGAMA(ppf.color_encoding, png_ptr, info_ptr);
    412      }
    413      MaybeAddCLLi(ppf.color_encoding, ppf.info.intensity_target, png_ptr,
    414                   info_ptr);
    415 
    416      std::vector<std::string> textstrings;
    417      JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings));
    418      for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) {
    419        png_text text;
    420        text.key = const_cast<png_charp>(textstrings[kk].c_str());
    421        text.text = const_cast<png_charp>(textstrings[kk + 1].c_str());
    422        text.compression = PNG_TEXT_COMPRESSION_zTXt;
    423        png_set_text(png_ptr, info_ptr, &text, 1);
    424      }
    425 
    426      png_write_info(png_ptr, info_ptr);
    427    } else {
    428      // fake writing a header, otherwise libpng gets confused
    429      size_t pos = bytes->size();
    430      png_write_info(png_ptr, info_ptr);
    431      bytes->resize(pos);
    432    }
    433 
    434    if (ppf.info.have_animation) {
    435      if (count == 0) {
    436        png_byte adata[8];
    437        png_save_uint_32(adata, ppf.frames.size());
    438        png_save_uint_32(adata + 4, ppf.info.animation.num_loops);
    439        png_byte actl[5] = "acTL";
    440        png_write_chunk(png_ptr, actl, adata, 8);
    441      }
    442      png_byte fdata[26];
    443      // TODO(jon): also make this work for the non-coalesced case
    444      png_save_uint_32(fdata, anim_chunks++);
    445      png_save_uint_32(fdata + 4, width);
    446      png_save_uint_32(fdata + 8, height);
    447      png_save_uint_32(fdata + 12, 0);
    448      png_save_uint_32(fdata + 16, 0);
    449      png_save_uint_16(fdata + 20, frame.frame_info.duration *
    450                                       ppf.info.animation.tps_denominator);
    451      png_save_uint_16(fdata + 22, ppf.info.animation.tps_numerator);
    452      fdata[24] = 1;
    453      fdata[25] = 0;
    454      png_byte fctl[5] = "fcTL";
    455      png_write_chunk(png_ptr, fctl, fdata, 26);
    456    }
    457 
    458    std::vector<uint8_t*> rows(height);
    459    for (int y = 0; y < height; ++y) {
    460      rows[y] = out.data() + y * out_stride;
    461    }
    462 
    463    png_write_flush(png_ptr);
    464    const size_t pos = bytes->size();
    465    png_write_image(png_ptr, rows.data());
    466    png_write_flush(png_ptr);
    467    if (count > 0) {
    468      std::vector<uint8_t> fdata(4);
    469      png_save_uint_32(fdata.data(), anim_chunks++);
    470      size_t p = pos;
    471      while (p + 8 < bytes->size()) {
    472        size_t len = png_get_uint_32(bytes->data() + p);
    473        JXL_ENSURE(bytes->operator[](p + 4) == 'I');
    474        JXL_ENSURE(bytes->operator[](p + 5) == 'D');
    475        JXL_ENSURE(bytes->operator[](p + 6) == 'A');
    476        JXL_ENSURE(bytes->operator[](p + 7) == 'T');
    477        fdata.insert(fdata.end(), bytes->data() + p + 8,
    478                     bytes->data() + p + 8 + len);
    479        p += len + 12;
    480      }
    481      bytes->resize(pos);
    482 
    483      png_byte fdat[5] = "fdAT";
    484      png_write_chunk(png_ptr, fdat, fdata.data(), fdata.size());
    485    }
    486 
    487    count++;
    488    if (count == ppf.frames.size() || !ppf.info.have_animation) {
    489      png_write_end(png_ptr, nullptr);
    490    }
    491 
    492    png_destroy_write_struct(&png_ptr, &info_ptr);
    493  }
    494 
    495  return true;
    496 }
    497 
    498 }  // namespace
    499 #endif
    500 
    501 std::unique_ptr<Encoder> GetAPNGEncoder() {
    502 #if JPEGXL_ENABLE_APNG
    503  return jxl::make_unique<APNGEncoder>();
    504 #else
    505  return nullptr;
    506 #endif
    507 }
    508 
    509 }  // namespace extras
    510 }  // namespace jxl