// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include "lib/extras/dec/apng.h"

// Parts of this code are taken from apngdis, which has the following license:
/* APNG Disassembler 2.8
 *
 * Deconstructs APNG files into individual frames.
 *
 * http://apngdis.sourceforge.net
 *
 * Copyright (c) 2010-2015 Max Stepin
 * maxst at users.sourceforge.net
 *
 * zlib license
 * ------------
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 *
 */

#include <jxl/codestream_header.h>
#include <jxl/encode.h>

#include <array>
#include <atomic>
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "lib/extras/packed_image.h"
#include "lib/extras/size_constraints.h"
#include "lib/jxl/base/byte_order.h"
#include "lib/jxl/base/common.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/printf_macros.h"
#include "lib/jxl/base/rect.h"
#include "lib/jxl/base/sanitizers.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
#if JPEGXL_ENABLE_APNG
#include "png.h" /* original (unpatched) libpng is ok */
#endif

namespace jxl {
namespace extras {

#if !JPEGXL_ENABLE_APNG

bool CanDecodeAPNG() { return false; }
Status DecodeImageAPNG(const Span<const uint8_t> bytes,
                       const ColorHints& color_hints, PackedPixelFile* ppf,
                       const SizeConstraints* constraints) {
  return false;
}

#else  // JPEGXL_ENABLE_APNG

namespace {

constexpr std::array<uint8_t, 8> kPngSignature = {137,  'P',  'N', 'G',
                                                  '\r', '\n', 26,  '\n'};

// Returns floating-point value from the PNG encoding (times 10^5).
double F64FromU32(const uint32_t x) { return static_cast<int32_t>(x) * 1E-5; }

/** Extract information from 'sRGB' chunk. */
Status DecodeSrgbChunk(const Bytes payload, JxlColorEncoding* color_encoding) {
  if (payload.size() != 1) return JXL_FAILURE("Wrong sRGB size");
  uint8_t ri = payload[0];
  // (PNG uses the same values as ICC.)
  if (ri >= 4) return JXL_FAILURE("Invalid Rendering Intent");
  color_encoding->white_point = JXL_WHITE_POINT_D65;
  color_encoding->primaries = JXL_PRIMARIES_SRGB;
  color_encoding->transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
  color_encoding->rendering_intent = static_cast<JxlRenderingIntent>(ri);
  return true;
}

/**
 * Extract information from 'cICP' chunk.
 *
 * If the cICP profile is not fully supported, return `false` and leave
 * `color_encoding` unmodified.
 */
Status DecodeCicpChunk(const Bytes payload, JxlColorEncoding* color_encoding) {
  if (payload.size() != 4) return JXL_FAILURE("Wrong cICP size");
  JxlColorEncoding color_enc = *color_encoding;

  // From https://www.itu.int/rec/T-REC-H.273-202107-I/en
  if (payload[0] == 1) {
    // IEC 61966-2-1 sRGB
    color_enc.primaries = JXL_PRIMARIES_SRGB;
    color_enc.white_point = JXL_WHITE_POINT_D65;
  } else if (payload[0] == 4) {
    // Rec. ITU-R BT.470-6 System M
    color_enc.primaries = JXL_PRIMARIES_CUSTOM;
    color_enc.primaries_red_xy[0] = 0.67;
    color_enc.primaries_red_xy[1] = 0.33;
    color_enc.primaries_green_xy[0] = 0.21;
    color_enc.primaries_green_xy[1] = 0.71;
    color_enc.primaries_blue_xy[0] = 0.14;
    color_enc.primaries_blue_xy[1] = 0.08;
    color_enc.white_point = JXL_WHITE_POINT_CUSTOM;
    color_enc.white_point_xy[0] = 0.310;
    color_enc.white_point_xy[1] = 0.316;
  } else if (payload[0] == 5) {
    // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
    color_enc.primaries = JXL_PRIMARIES_CUSTOM;
    color_enc.primaries_red_xy[0] = 0.64;
    color_enc.primaries_red_xy[1] = 0.33;
    color_enc.primaries_green_xy[0] = 0.29;
    color_enc.primaries_green_xy[1] = 0.60;
    color_enc.primaries_blue_xy[0] = 0.15;
    color_enc.primaries_blue_xy[1] = 0.06;
    color_enc.white_point = JXL_WHITE_POINT_D65;
  } else if (payload[0] == 6 || payload[0] == 7) {
    // SMPTE ST 170 (2004) / SMPTE ST 240 (1999)
    color_enc.primaries = JXL_PRIMARIES_CUSTOM;
    color_enc.primaries_red_xy[0] = 0.630;
    color_enc.primaries_red_xy[1] = 0.340;
    color_enc.primaries_green_xy[0] = 0.310;
    color_enc.primaries_green_xy[1] = 0.595;
    color_enc.primaries_blue_xy[0] = 0.155;
    color_enc.primaries_blue_xy[1] = 0.070;
    color_enc.white_point = JXL_WHITE_POINT_D65;
  } else if (payload[0] == 8) {
    // Generic film (colour filters using Illuminant C)
    color_enc.primaries = JXL_PRIMARIES_CUSTOM;
    color_enc.primaries_red_xy[0] = 0.681;
    color_enc.primaries_red_xy[1] = 0.319;
    color_enc.primaries_green_xy[0] = 0.243;
    color_enc.primaries_green_xy[1] = 0.692;
    color_enc.primaries_blue_xy[0] = 0.145;
    color_enc.primaries_blue_xy[1] = 0.049;
    color_enc.white_point = JXL_WHITE_POINT_CUSTOM;
    color_enc.white_point_xy[0] = 0.310;
    color_enc.white_point_xy[1] = 0.316;
  } else if (payload[0] == 9) {
    // Rec. ITU-R BT.2100-2
    color_enc.primaries = JXL_PRIMARIES_2100;
    color_enc.white_point = JXL_WHITE_POINT_D65;
  } else if (payload[0] == 10) {
    // CIE 1931 XYZ
    color_enc.primaries = JXL_PRIMARIES_CUSTOM;
    color_enc.primaries_red_xy[0] = 1;
    color_enc.primaries_red_xy[1] = 0;
    color_enc.primaries_green_xy[0] = 0;
    color_enc.primaries_green_xy[1] = 1;
    color_enc.primaries_blue_xy[0] = 0;
    color_enc.primaries_blue_xy[1] = 0;
    color_enc.white_point = JXL_WHITE_POINT_E;
  } else if (payload[0] == 11) {
    // SMPTE RP 431-2 (2011)
    color_enc.primaries = JXL_PRIMARIES_P3;
    color_enc.white_point = JXL_WHITE_POINT_DCI;
  } else if (payload[0] == 12) {
    // SMPTE EG 432-1 (2010)
    color_enc.primaries = JXL_PRIMARIES_P3;
    color_enc.white_point = JXL_WHITE_POINT_D65;
  } else if (payload[0] == 22) {
    color_enc.primaries = JXL_PRIMARIES_CUSTOM;
    color_enc.primaries_red_xy[0] = 0.630;
    color_enc.primaries_red_xy[1] = 0.340;
    color_enc.primaries_green_xy[0] = 0.295;
    color_enc.primaries_green_xy[1] = 0.605;
    color_enc.primaries_blue_xy[0] = 0.155;
    color_enc.primaries_blue_xy[1] = 0.077;
    color_enc.white_point = JXL_WHITE_POINT_D65;
  } else {
    JXL_WARNING("Unsupported primaries specified in cICP chunk: %d",
                static_cast<int>(payload[0]));
    return false;
  }

  if (payload[1] == 1 || payload[1] == 6 || payload[1] == 14 ||
      payload[1] == 15) {
    // Rec. ITU-R BT.709-6
    color_enc.transfer_function = JXL_TRANSFER_FUNCTION_709;
  } else if (payload[1] == 4) {
    // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
    color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
    color_enc.gamma = 1 / 2.2;
  } else if (payload[1] == 5) {
    // Rec. ITU-R BT.470-6 System B, G
    color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
    color_enc.gamma = 1 / 2.8;
  } else if (payload[1] == 8 || payload[1] == 13 || payload[1] == 16 ||
             payload[1] == 17 || payload[1] == 18) {
    // These codes all match the corresponding JXL enum values
    color_enc.transfer_function = static_cast<JxlTransferFunction>(payload[1]);
  } else {
    JXL_WARNING("Unsupported transfer function specified in cICP chunk: %d",
                static_cast<int>(payload[1]));
    return false;
  }

  if (payload[2] != 0) {
    JXL_WARNING("Unsupported color space specified in cICP chunk: %d",
                static_cast<int>(payload[2]));
    return false;
  }
  if (payload[3] != 1) {
    JXL_WARNING("Unsupported full-range flag specified in cICP chunk: %d",
                static_cast<int>(payload[3]));
    return false;
  }
  // cICP has no rendering intent, so use the default
  color_enc.rendering_intent = JXL_RENDERING_INTENT_RELATIVE;
  *color_encoding = color_enc;
  return true;
}

/** Extract information from 'gAMA' chunk. */
Status DecodeGamaChunk(Bytes payload, JxlColorEncoding* color_encoding) {
  if (payload.size() != 4) return JXL_FAILURE("Wrong gAMA size");
  color_encoding->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
  color_encoding->gamma = F64FromU32(LoadBE32(payload.data()));
  return true;
}

/** Extract information from 'cHTM' chunk. */
Status DecodeChrmChunk(Bytes payload, JxlColorEncoding* color_encoding) {
  if (payload.size() != 32) return JXL_FAILURE("Wrong cHRM size");
  const uint8_t* data = payload.data();
  color_encoding->white_point = JXL_WHITE_POINT_CUSTOM;
  color_encoding->white_point_xy[0] = F64FromU32(LoadBE32(data + 0));
  color_encoding->white_point_xy[1] = F64FromU32(LoadBE32(data + 4));

  color_encoding->primaries = JXL_PRIMARIES_CUSTOM;
  color_encoding->primaries_red_xy[0] = F64FromU32(LoadBE32(data + 8));
  color_encoding->primaries_red_xy[1] = F64FromU32(LoadBE32(data + 12));
  color_encoding->primaries_green_xy[0] = F64FromU32(LoadBE32(data + 16));
  color_encoding->primaries_green_xy[1] = F64FromU32(LoadBE32(data + 20));
  color_encoding->primaries_blue_xy[0] = F64FromU32(LoadBE32(data + 24));
  color_encoding->primaries_blue_xy[1] = F64FromU32(LoadBE32(data + 28));
  return true;
}

/** Extracts information from 'cLLi' chunk. */
Status DecodeClliChunk(Bytes payload, float* max_content_light_level) {
  if (payload.size() != 8) return JXL_FAILURE("Wrong cLLi size");
  const uint8_t* data = payload.data();
  const uint32_t maxcll_png =
      Clamp1(png_get_uint_32(data), uint32_t{0}, uint32_t{10000 * 10000});
  // Ignore MaxFALL value.
  *max_content_light_level = static_cast<float>(maxcll_png) / 10000.f;
  return true;
}

/** Returns false if invalid. */
JXL_INLINE Status DecodeHexNibble(const char c, uint32_t* JXL_RESTRICT nibble) {
  if ('a' <= c && c <= 'f') {
    *nibble = 10 + c - 'a';
  } else if ('0' <= c && c <= '9') {
    *nibble = c - '0';
  } else {
    *nibble = 0;
    return JXL_FAILURE("Invalid metadata nibble");
  }
  JXL_ENSURE(*nibble < 16);
  return true;
}

/** Returns false if invalid. */
JXL_INLINE Status DecodeDecimal(const char** pos, const char* end,
                                uint32_t* JXL_RESTRICT value) {
  size_t len = 0;
  *value = 0;
  while (*pos < end) {
    char next = **pos;
    if (next >= '0' && next <= '9') {
      *value = (*value * 10) + static_cast<uint32_t>(next - '0');
      len++;
      if (len > 8) {
        break;
      }
    } else {
      // Do not consume terminator (non-decimal digit).
      break;
    }
    (*pos)++;
  }
  if (len == 0 || len > 8) {
    return JXL_FAILURE("Failed to parse decimal");
  }
  return true;
}

/**
 * Parses a PNG text chunk with key of the form "Raw profile type ####", with
 * #### a type.
 *
 * Returns whether it could successfully parse the content.
 * We trust key and encoded are null-terminated because they come from
 * libpng.
 */
Status MaybeDecodeBase16(const char* key, const char* encoded,
                         std::string* type, std::vector<uint8_t>* bytes) {
  const char* encoded_end = encoded + strlen(encoded);

  const char* kKey = "Raw profile type ";
  if (strncmp(key, kKey, strlen(kKey)) != 0) return false;
  *type = key + strlen(kKey);
  const size_t kMaxTypeLen = 20;
  if (type->length() > kMaxTypeLen) return false;  // Type too long

  // Header: freeform string and number of bytes
  // Expected format is:
  // \n
  // profile name/description\n
  //       40\n               (the number of bytes after hex-decoding)
  // 01234566789abcdef....\n  (72 bytes per line max).
  // 012345667\n              (last line)
  const char* pos = encoded;

  if (*(pos++) != '\n') return false;
  while (pos < encoded_end && *pos != '\n') {
    pos++;
  }
  if (pos == encoded_end) return false;
  // We parsed so far a \n, some number of non \n characters and are now
  // pointing at a \n.
  if (*(pos++) != '\n') return false;
  // Skip leading spaces
  while (pos < encoded_end && *pos == ' ') {
    pos++;
  }
  uint32_t bytes_to_decode = 0;
  JXL_RETURN_IF_ERROR(DecodeDecimal(&pos, encoded_end, &bytes_to_decode));

  // We need 2*bytes for the hex values plus 1 byte every 36 values,
  // plus terminal \n for length.
  size_t tail = static_cast<size_t>(encoded_end - pos);
  bool ok = ((tail / 2) >= bytes_to_decode);
  if (ok) tail -= 2 * static_cast<size_t>(bytes_to_decode);
  ok = ok && (tail == 1 + DivCeil(bytes_to_decode, 36));
  if (!ok) {
    return JXL_FAILURE("Not enough bytes to parse %d bytes in hex",
                       bytes_to_decode);
  }
  JXL_ENSURE(bytes->empty());
  bytes->reserve(bytes_to_decode);

  // Encoding: base16 with newline after 72 chars.
  // pos points to the \n before the first line of hex values.
  for (size_t i = 0; i < bytes_to_decode; ++i) {
    if (i % 36 == 0) {
      if (pos + 1 >= encoded_end) return false;  // Truncated base16 1
      if (*pos != '\n') return false;            // Expected newline
      ++pos;
    }

    if (pos + 2 >= encoded_end) return false;  // Truncated base16 2;
    uint32_t nibble0;
    uint32_t nibble1;
    JXL_RETURN_IF_ERROR(DecodeHexNibble(pos[0], &nibble0));
    JXL_RETURN_IF_ERROR(DecodeHexNibble(pos[1], &nibble1));
    bytes->push_back(static_cast<uint8_t>((nibble0 << 4) + nibble1));
    pos += 2;
  }
  if (pos + 1 != encoded_end) return false;  // Too many encoded bytes
  if (pos[0] != '\n') return false;          // Incorrect metadata terminator
  return true;
}

/** Retrieves XMP and EXIF/IPTC from itext and text. */
Status DecodeBlob(const png_text_struct& info, PackedMetadata* metadata) {
  // We trust these are properly null-terminated by libpng.
  const char* key = info.key;
  const char* value = info.text;
  if (strstr(key, "XML:com.adobe.xmp")) {
    metadata->xmp.resize(strlen(value));  // safe, see above
    memcpy(metadata->xmp.data(), value, metadata->xmp.size());
  }

  std::string type;
  std::vector<uint8_t> bytes;

  // Handle text chunks annotated with key "Raw profile type ####", with
  // #### a type, which may contain metadata.
  const char* kKey = "Raw profile type ";
  if (strncmp(key, kKey, strlen(kKey)) != 0) return false;

  if (!MaybeDecodeBase16(key, value, &type, &bytes)) {
    JXL_WARNING("Couldn't parse 'Raw format type' text chunk");
    return false;
  }
  if (type == "exif") {
    // Remove prefix if present.
    constexpr std::array<uint8_t, 6> kExifPrefix = {'E', 'x', 'i', 'f', 0, 0};
    if (bytes.size() >= kExifPrefix.size() &&
        memcmp(bytes.data(), kExifPrefix.data(), kExifPrefix.size()) == 0) {
      bytes.erase(bytes.begin(), bytes.begin() + kExifPrefix.size());
    }
    if (!metadata->exif.empty()) {
      JXL_DEBUG_V(2,
                  "overwriting EXIF (%" PRIuS " bytes) with base16 (%" PRIuS
                  " bytes)",
                  metadata->exif.size(), bytes.size());
    }
    metadata->exif = std::move(bytes);
  } else if (type == "iptc") {
    // TODO(jon): Deal with IPTC in some way
  } else if (type == "8bim") {
    // TODO(jon): Deal with 8bim in some way
  } else if (type == "xmp") {
    if (!metadata->xmp.empty()) {
      JXL_DEBUG_V(2,
                  "overwriting XMP (%" PRIuS " bytes) with base16 (%" PRIuS
                  " bytes)",
                  metadata->xmp.size(), bytes.size());
    }
    metadata->xmp = std::move(bytes);
  } else {
    JXL_DEBUG_V(
        2, "Unknown type in 'Raw format type' text chunk: %s: %" PRIuS " bytes",
        type.c_str(), bytes.size());
  }
  return true;
}

constexpr bool isAbc(char c) {
  return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}

/** Wrap 4-char tag name into ID. */
constexpr uint32_t MakeTag(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
  return a | (b << 8) | (c << 16) | (d << 24);
}

/** Reusable image data container. */
struct Pixels {
  // Use array instead of vector to avoid memory initialization.
  std::unique_ptr<uint8_t[]> pixels;
  size_t pixels_size = 0;
  std::vector<uint8_t*> rows;
  std::atomic<bool> has_error{false};

  Status Resize(size_t row_bytes, size_t num_rows) {
    size_t new_size = row_bytes * num_rows;  // it is assumed size is sane
    if (new_size > pixels_size) {
      pixels.reset(new uint8_t[new_size]);
      if (!pixels) {
        // TODO(szabadka): use specialized OOM error code
        return JXL_FAILURE("Failed to allocate memory for image buffer");
      }
      pixels_size = new_size;
    }
    rows.resize(num_rows);
    for (size_t y = 0; y < num_rows; y++) {
      rows[y] = pixels.get() + y * row_bytes;
    }
    return true;
  }
};

/**
 * Helper that chunks in-memory input.
 */
struct Reader {
  explicit Reader(Span<const uint8_t> data) : data_(data) {}

  const Span<const uint8_t> data_;
  size_t offset_ = 0;

  Bytes Peek(size_t len) const {
    size_t cap = data_.size() - offset_;
    size_t to_copy = std::min(cap, len);
    return {data_.data() + offset_, to_copy};
  }

  Bytes Read(size_t len) {
    Bytes result = Peek(len);
    offset_ += result.size();
    return result;
  }

  /* Returns empty Span on error. */
  Bytes ReadChunk() {
    Bytes len = Peek(4);
    if (len.size() != 4) {
      return Bytes();
    }
    const auto size = png_get_uint_32(len.data());
    // NB: specification allows 2^31 - 1
    constexpr size_t kMaxPNGChunkSize = 1u << 30;  // 1 GB
    // Check first, to avoid overflow.
    if (size > kMaxPNGChunkSize) {
      JXL_WARNING("APNG chunk size is too big");
      return Bytes();
    }
    size_t full_size = size + 12;  // size does not include itself, tag and CRC.
    Bytes result = Read(full_size);
    return (result.size() == full_size) ? result : Bytes();
  }

  bool Eof() const { return offset_ == data_.size(); }
};

void ProgressiveRead_OnInfo(png_structp png_ptr, png_infop info_ptr) {
  png_set_expand(png_ptr);
  png_set_palette_to_rgb(png_ptr);
  png_set_tRNS_to_alpha(png_ptr);
  (void)png_set_interlace_handling(png_ptr);
  png_read_update_info(png_ptr, info_ptr);
}

void ProgressiveRead_OnRow(png_structp png_ptr, png_bytep new_row,
                           png_uint_32 row_num, int pass) {
  Pixels* frame = reinterpret_cast<Pixels*>(png_get_progressive_ptr(png_ptr));
  if (!frame) {
    JXL_DEBUG_ABORT("Internal logic error");
    return;
  }
  if (row_num >= frame->rows.size()) {
    frame->has_error = true;
    return;
  }
  png_progressive_combine_row(png_ptr, frame->rows[row_num], new_row);
}

// Holds intermediate state during parsing APNG file.
struct Context {
  ~Context() {
    // Make sure png memory is released in any case.
    ResetPngDecoder();
  }

  bool CreatePngDecoder() {
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr,
                                     nullptr);
    info_ptr = png_create_info_struct(png_ptr);
    return (png_ptr != nullptr && info_ptr != nullptr);
  }

  /**
   * Initialize PNG decoder.
   *
   * TODO(eustas): add details
   */
  bool InitPngDecoder(const std::vector<Bytes>& chunksInfo,
                      const RectT<uint64_t>& viewport) {
    ResetPngDecoder();

    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr,
                                     nullptr);
    info_ptr = png_create_info_struct(png_ptr);
    if (png_ptr == nullptr || info_ptr == nullptr) {
      return false;
    }

    if (setjmp(png_jmpbuf(png_ptr))) {
      return false;
    }

    /* hIST chunk tail is not processed properly; skip this chunk completely;
       see https://github.com/glennrp/libpng/pull/413 */
    constexpr std::array<uint8_t, 5> kIgnoredChunks = {'h', 'I', 'S', 'T', 0};
    png_set_keep_unknown_chunks(png_ptr, 1, kIgnoredChunks.data(),
                                static_cast<int>(kIgnoredChunks.size() / 5));

    png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE);
    png_set_progressive_read_fn(png_ptr, static_cast<void*>(&frameRaw),
                                ProgressiveRead_OnInfo, ProgressiveRead_OnRow,
                                nullptr);

    png_process_data(png_ptr, info_ptr,
                     const_cast<uint8_t*>(kPngSignature.data()),
                     kPngSignature.size());

    // Patch dimensions.
    png_save_uint_32(ihdr.data() + 8, static_cast<uint32_t>(viewport.xsize()));
    png_save_uint_32(ihdr.data() + 12, static_cast<uint32_t>(viewport.ysize()));
    png_process_data(png_ptr, info_ptr, ihdr.data(), ihdr.size());

    for (const auto& chunk : chunksInfo) {
      png_process_data(png_ptr, info_ptr, const_cast<uint8_t*>(chunk.data()),
                       chunk.size());
    }

    return true;
  }

  /**
   * Pass chunk to PNG decoder.
   */
  bool FeedChunks(const Bytes& chunk1, const Bytes& chunk2 = Bytes()) {
    // TODO(eustas): turn to DCHECK
    if (!png_ptr || !info_ptr) return false;

    if (setjmp(png_jmpbuf(png_ptr))) {
      return false;
    }

    for (const auto& chunk : {chunk1, chunk2}) {
      if (!chunk.empty()) {
        png_process_data(png_ptr, info_ptr, const_cast<uint8_t*>(chunk.data()),
                         chunk.size());
      }
    }
    return true;
  }

  bool FinalizeStream(PackedMetadata* metadata) {
    // TODO(eustas): turn to DCHECK
    if (!png_ptr || !info_ptr) return false;

    if (setjmp(png_jmpbuf(png_ptr))) {
      return false;
    }

    const std::array<uint8_t, 12> kFooter = {0,  0,  0,   0,  73, 69,
                                             78, 68, 174, 66, 96, 130};
    png_process_data(png_ptr, info_ptr, const_cast<uint8_t*>(kFooter.data()),
                     kFooter.size());
    // before destroying: check if we encountered any metadata chunks
    png_textp text_ptr = nullptr;
    int num_text = 0;
    if (png_get_text(png_ptr, info_ptr, &text_ptr, &num_text) != 0) {
      msan::UnpoisonMemory(text_ptr, sizeof(png_text_struct) * num_text);
      for (int i = 0; i < num_text; i++) {
        Status result = DecodeBlob(text_ptr[i], metadata);
        // Ignore unknown / malformed blob.
        (void)result;
      }
    }

    return true;
  }

  void ResetPngDecoder() {
    png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
    // Just in case. Not all versions on libpng wipe-out the pointers.
    png_ptr = nullptr;
    info_ptr = nullptr;
  }

  std::array<uint8_t, 25> ihdr;  // (modified) copy of file IHDR chunk
  png_structp png_ptr = nullptr;
  png_infop info_ptr = nullptr;
  Pixels frameRaw = {};
};

enum class DisposeOp : uint8_t { NONE = 0, BACKGROUND = 1, PREVIOUS = 2 };

constexpr uint8_t kLastDisposeOp = static_cast<uint32_t>(DisposeOp::PREVIOUS);

enum class BlendOp : uint8_t { SOURCE = 0, OVER = 1 };

constexpr uint8_t kLastBlendOp = static_cast<uint32_t>(BlendOp::OVER);

// fcTL
struct FrameControl {
  uint32_t delay_num;
  uint32_t delay_den;
  RectT<uint64_t> viewport;
  DisposeOp dispose_op;
  BlendOp blend_op;
};

struct Frame {
  PackedImage pixels;
  FrameControl metadata;
};

bool ValidateViewport(const RectT<uint64_t>& r) {
  constexpr uint32_t kMaxPngDim = 1000000UL;
  return (r.xsize() <= kMaxPngDim) && (r.ysize() <= kMaxPngDim);
}

/**
 * Setup #channels, bpp, colorspace, etc. from PNG values.
 */
void SetColorData(PackedPixelFile* ppf, uint8_t color_type, uint8_t bit_depth,
                  png_color_8p sig_bits, uint32_t has_transparency) {
  bool palette_used = ((color_type & 1) != 0);
  bool color_used = ((color_type & 2) != 0);
  bool alpha_channel_used = ((color_type & 4) != 0);
  if (palette_used) {
    if (!color_used || alpha_channel_used) {
      JXL_DEBUG_V(2, "Unexpected PNG color type");
    }
  }

  ppf->info.bits_per_sample = bit_depth;

  if (palette_used) {
    // palette will actually be 8-bit regardless of the index bitdepth
    ppf->info.bits_per_sample = 8;
  }
  if (color_used) {
    ppf->info.num_color_channels = 3;
    ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB;
    if (sig_bits) {
      if (sig_bits->red == sig_bits->green &&
          sig_bits->green == sig_bits->blue) {
        ppf->info.bits_per_sample = sig_bits->red;
      } else {
        int maxbps =
            std::max(sig_bits->red, std::max(sig_bits->green, sig_bits->blue));
        JXL_DEBUG_V(2,
                    "sBIT chunk: bit depths for R, G, and B are not the same "
                    "(%i %i %i), while in JPEG XL they have to be the same. "
                    "Setting RGB bit depth to %i.",
                    sig_bits->red, sig_bits->green, sig_bits->blue, maxbps);
        ppf->info.bits_per_sample = maxbps;
      }
    }
  } else {
    ppf->info.num_color_channels = 1;
    ppf->color_encoding.color_space = JXL_COLOR_SPACE_GRAY;
    if (sig_bits) ppf->info.bits_per_sample = sig_bits->gray;
  }
  if (alpha_channel_used || has_transparency) {
    ppf->info.alpha_bits = ppf->info.bits_per_sample;
    if (sig_bits && sig_bits->alpha != ppf->info.bits_per_sample) {
      JXL_DEBUG_V(2,
                  "sBIT chunk: bit depths for RGBA are inconsistent "
                  "(%i %i %i %i). Setting A bitdepth to %i.",
                  sig_bits->red, sig_bits->green, sig_bits->blue,
                  sig_bits->alpha, ppf->info.bits_per_sample);
    }
  } else {
    ppf->info.alpha_bits = 0;
  }
  ppf->color_encoding.color_space = (ppf->info.num_color_channels == 1)
                                        ? JXL_COLOR_SPACE_GRAY
                                        : JXL_COLOR_SPACE_RGB;
}

// Color profile chunks: cICP has the highest priority, followed by
// iCCP and sRGB (which shouldn't co-exist, but if they do, we use
// iCCP), followed finally by gAMA and cHRM.
enum class ColorInfoType {
  NONE = 0,
  GAMA_OR_CHRM = 1,
  ICCP_OR_SRGB = 2,
  CICP = 3
};

}  // namespace

bool CanDecodeAPNG() { return true; }

/**
 * Parse and decode PNG file.
 *
 * Useful PNG chunks:
 *   acTL : animation control (#frames, loop count)
 *   fcTL : frame control (seq#, viewport, delay, disposal blending)
 *   bKGD : preferable background
 *   IDAT : "default image"
 *          if single fcTL goes before IDAT, then it is also first frame
 *   fdAT : seq# + IDAT-like content
 *   PLTE : palette
 *   cICP : coding-independent code points for video signal type identification
 *   iCCP : embedded ICC profile
 *   sRGB : standard RGB colour space
 *   eXIf : exchangeable image file profile
 *   gAMA : image gamma
 *   cHRM : primary chromaticities and white point
 *   tRNS : transparency
 *
 * PNG chunk ordering:
 *  - IHDR first
 *  - IEND last
 *  - acTL, cHRM, cICP, gAMA, iCCP, sRGB, bKGD, eXIf, PLTE before IDAT
 *  - fdAT after IDAT
 *
 * More rules:
 *  - iCCP and sRGB are exclusive
 *  - fcTL and fdAT seq# must be in order fro 0, with no gaps or duplicates
 *  - fcTL before corresponding IDAT / fdAT
 */
Status DecodeImageAPNG(const Span<const uint8_t> bytes,
                       const ColorHints& color_hints, PackedPixelFile* ppf,
                       const SizeConstraints* constraints) {
  // Initialize output (default settings in case e.g. only gAMA is given).
  ppf->frames.clear();
  ppf->info.exponent_bits_per_sample = 0;
  ppf->info.alpha_exponent_bits = 0;
  ppf->info.orientation = JXL_ORIENT_IDENTITY;
  ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB;
  ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
  ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
  ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
  ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_RELATIVE;

  Reader input(bytes);

  // Check signature.
  Bytes sig = input.Read(kPngSignature.size());
  if (sig.size() != 8 ||
      memcmp(sig.data(), kPngSignature.data(), kPngSignature.size()) != 0) {
    return false;  // Return silently if it is not a PNG
  }

  // Check IHDR chunk.
  Context ctx;
  Bytes ihdr = input.ReadChunk();
  if (ihdr.size() != ctx.ihdr.size()) {
    return JXL_FAILURE("Unexpected first chunk payload size");
  }
  memcpy(ctx.ihdr.data(), ihdr.data(), ihdr.size());
  uint32_t id = LoadLE32(ihdr.data() + 4);
  if (id != MakeTag('I', 'H', 'D', 'R')) {
    return JXL_FAILURE("First chunk is not IHDR");
  }
  const RectT<uint64_t> image_rect(0, 0, png_get_uint_32(ihdr.data() + 8),
                                   png_get_uint_32(ihdr.data() + 12));
  if (!ValidateViewport(image_rect)) {
    return JXL_FAILURE("PNG image dimensions are too large");
  }

  // Chunks we supply to PNG decoder for every animation frame.
  std::vector<Bytes> passthrough_chunks;
  if (!ctx.InitPngDecoder(passthrough_chunks, image_rect)) {
    return JXL_FAILURE("Failed to initialize PNG decoder");
  }

  // Marker that this PNG is animated.
  bool seen_actl = false;
  // First IDAT is a very important milestone; at this moment we freeze
  // gathered metadata.
  bool seen_idat = false;
  // fCTL can occur multiple times, but only once before IDAT.
  bool seen_fctl = false;
  // Logical EOF.
  bool seen_iend = false;

  ColorInfoType color_info_type = ColorInfoType::NONE;

  // Flag that we processed some IDAT / fDAT after image / frame start.
  bool seen_pixel_data = false;

  uint32_t num_channels;
  JxlPixelFormat format = {};
  size_t bytes_per_pixel = 0;
  std::vector<Frame> frames;
  FrameControl current_frame = {/*delay_num=*/1, /*delay_den=*/10, image_rect,
                                DisposeOp::NONE, BlendOp::SOURCE};

  // Copies frame pixels / metadata from temporary storage.
  // TODO(eustas): avoid copying.
  const auto finalize_frame = [&]() -> Status {
    if (!seen_pixel_data) {
      return JXL_FAILURE("Frame / image without fdAT / IDAT chunks");
    }
    if (!ctx.FinalizeStream(&ppf->metadata)) {
      return JXL_FAILURE("Failed to finalize PNG substream");
    }
    if (ctx.frameRaw.has_error) {
      return JXL_FAILURE("Internal error");
    }
    // Allocates the frame buffer.
    const RectT<uint64_t>& vp = current_frame.viewport;
    size_t xsize = static_cast<size_t>(vp.xsize());
    size_t ysize = static_cast<size_t>(vp.ysize());
    JXL_ASSIGN_OR_RETURN(PackedImage image,
                         PackedImage::Create(xsize, ysize, format));
    for (size_t y = 0; y < ysize; ++y) {
      // TODO(eustas): ensure multiplication is safe
      memcpy(static_cast<uint8_t*>(image.pixels()) + image.stride * y,
             ctx.frameRaw.rows[y], bytes_per_pixel * xsize);
    }
    frames.push_back(Frame{std::move(image), current_frame});
    seen_pixel_data = false;
    return true;
  };

  while (!input.Eof()) {
    if (seen_iend) {
      return JXL_FAILURE("Exuberant input after IEND chunk");
    }
    Bytes chunk = input.ReadChunk();
    if (chunk.empty()) {
      return JXL_FAILURE("Malformed chunk");
    }
    Bytes type(chunk.data() + 4, 4);
    id = LoadLE32(type.data());
    // Cut 'size' and 'type' at front and 'CRC' at the end.
    Bytes payload(chunk.data() + 8, chunk.size() - 12);

    if (!isAbc(type[0]) || !isAbc(type[1]) || !isAbc(type[2]) ||
        !isAbc(type[3])) {
      return JXL_FAILURE("Exotic PNG chunk");
    }

    switch (id) {
      case MakeTag('a', 'c', 'T', 'L'):
        if (seen_idat) {
          JXL_DEBUG_V(2, "aCTL after IDAT ignored");
          continue;
        }
        if (seen_actl) {
          JXL_DEBUG_V(2, "Duplicate aCTL chunk ignored");
          continue;
        }
        seen_actl = true;
        ppf->info.have_animation = JXL_TRUE;
        // TODO(eustas): decode from chunk?
        ppf->info.animation.tps_numerator = 1000;
        ppf->info.animation.tps_denominator = 1;
        continue;

      case MakeTag('I', 'E', 'N', 'D'):
        seen_iend = true;
        JXL_RETURN_IF_ERROR(finalize_frame());
        continue;

      case MakeTag('f', 'c', 'T', 'L'): {
        if (payload.size() != 26) {
          return JXL_FAILURE("Unexpected fcTL payload size: %u",
                             static_cast<uint32_t>(payload.size()));
        }
        if (seen_fctl && !seen_idat) {
          return JXL_FAILURE("More than one fcTL before IDAT");
        }
        if (seen_idat && !seen_actl) {
          return JXL_FAILURE("fcTL after IDAT, but without acTL");
        }
        seen_fctl = true;

        // TODO(eustas): check order?
        // sequence_number = png_get_uint_32(payload.data());
        RectT<uint64_t> raw_viewport(png_get_uint_32(payload.data() + 12),
                                     png_get_uint_32(payload.data() + 16),
                                     png_get_uint_32(payload.data() + 4),
                                     png_get_uint_32(payload.data() + 8));
        uint8_t dispose_op = payload[24];
        if (dispose_op > kLastDisposeOp) {
          return JXL_FAILURE("Invalid DisposeOp");
        }
        uint8_t blend_op = payload[25];
        if (blend_op > kLastBlendOp) {
          return JXL_FAILURE("Invalid BlendOp");
        }
        FrameControl next_frame = {
            /*delay_num=*/png_get_uint_16(payload.data() + 20),
            /*delay_den=*/png_get_uint_16(payload.data() + 22), raw_viewport,
            static_cast<DisposeOp>(dispose_op), static_cast<BlendOp>(blend_op)};

        if (!raw_viewport.Intersection(image_rect).IsSame(raw_viewport)) {
          // Cropping happened.
          return JXL_FAILURE("PNG frame is outside of image rect");
        }

        if (!seen_idat) {
          // "Default" image is the first animation frame. Its viewport must
          // cover the whole image area.
          if (!raw_viewport.IsSame(image_rect)) {
            return JXL_FAILURE(
                "If the first animation frame is default image, its viewport "
                "must cover full image");
          }
        } else {
          JXL_RETURN_IF_ERROR(finalize_frame());
          if (!ctx.InitPngDecoder(passthrough_chunks, next_frame.viewport)) {
            return JXL_FAILURE("Failed to initialize PNG decoder");
          }
        }
        current_frame = next_frame;
        continue;
      }

      case MakeTag('I', 'D', 'A', 'T'): {
        if (!frames.empty()) {
          return JXL_FAILURE("IDAT after default image is over");
        }
        if (!seen_idat) {
          // First IDAT means that all metadata is ready.
          seen_idat = true;
          JXL_ENSURE(image_rect.xsize() ==
                     png_get_image_width(ctx.png_ptr, ctx.info_ptr));
          JXL_ENSURE(image_rect.ysize() ==
                     png_get_image_height(ctx.png_ptr, ctx.info_ptr));
          JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, image_rect.xsize(),
                                               image_rect.ysize()));
          ppf->info.xsize = image_rect.xsize();
          ppf->info.ysize = image_rect.ysize();

          png_color_8p sig_bits = nullptr;
          // Error is OK -> sig_bits remains nullptr.
          png_get_sBIT(ctx.png_ptr, ctx.info_ptr, &sig_bits);
          SetColorData(ppf, png_get_color_type(ctx.png_ptr, ctx.info_ptr),
                       png_get_bit_depth(ctx.png_ptr, ctx.info_ptr), sig_bits,
                       png_get_valid(ctx.png_ptr, ctx.info_ptr, PNG_INFO_tRNS));
          num_channels =
              ppf->info.num_color_channels + (ppf->info.alpha_bits ? 1 : 0);
          format = {
              /*num_channels=*/num_channels,
              /*data_type=*/ppf->info.bits_per_sample > 8 ? JXL_TYPE_UINT16
                                                          : JXL_TYPE_UINT8,
              /*endianness=*/JXL_BIG_ENDIAN,
              /*align=*/0,
          };
          bytes_per_pixel =
              num_channels * (format.data_type == JXL_TYPE_UINT16 ? 2 : 1);
          // TODO(eustas): ensure multiplication is safe
          uint64_t row_bytes =
              static_cast<uint64_t>(image_rect.xsize()) * bytes_per_pixel;
          uint64_t max_rows = std::numeric_limits<size_t>::max() / row_bytes;
          if (image_rect.ysize() > max_rows) {
            return JXL_FAILURE("Image too big.");
          }
          // TODO(eustas): drop frameRaw
          JXL_RETURN_IF_ERROR(
              ctx.frameRaw.Resize(row_bytes, image_rect.ysize()));
        }

        if (!ctx.FeedChunks(chunk)) {
          return JXL_FAILURE("Decoding IDAT failed");
        }
        seen_pixel_data = true;
        continue;
      }

      case MakeTag('f', 'd', 'A', 'T'): {
        if (!seen_idat) {
          return JXL_FAILURE("fdAT chunk before IDAT");
        }
        if (!seen_actl) {
          return JXL_FAILURE("fdAT chunk before acTL");
        }
        /* The 'fdAT' chunk has... the same structure as an 'IDAT' chunk,
         * except preceded by a sequence number. */
        if (payload.size() < 4) {
          return JXL_FAILURE("Corrupted fdAT chunk");
        }
        // Turn 'fdAT' to 'IDAT' by cutting sequence number and replacing tag.
        std::array<uint8_t, 8> preamble;
        png_save_uint_32(preamble.data(), payload.size() - 4);
        memcpy(preamble.data() + 4, "IDAT", 4);
        // Cut-off 'size', 'type' and 'sequence_number'
        Bytes chunk_tail(chunk.data() + 12, chunk.size() - 12);
        if (!ctx.FeedChunks(Bytes(preamble), chunk_tail)) {
          return JXL_FAILURE("Decoding fdAT failed");
        }
        seen_pixel_data = true;
        continue;
      }

      case MakeTag('c', 'I', 'C', 'P'):
        if (color_info_type == ColorInfoType::CICP) {
          JXL_DEBUG_V(2, "Excessive colorspace definition; cICP chunk ignored");
          continue;
        }
        JXL_RETURN_IF_ERROR(DecodeCicpChunk(payload, &ppf->color_encoding));
        ppf->icc.clear();
        ppf->primary_color_representation =
            PackedPixelFile::kColorEncodingIsPrimary;
        color_info_type = ColorInfoType::CICP;
        continue;

      case MakeTag('i', 'C', 'C', 'P'): {
        if (color_info_type == ColorInfoType::ICCP_OR_SRGB) {
          return JXL_FAILURE("Repeated iCCP / sRGB chunk");
        }
        if (color_info_type > ColorInfoType::ICCP_OR_SRGB) {
          JXL_DEBUG_V(2, "Excessive colorspace definition; iCCP chunk ignored");
          continue;
        }
        // Let PNG decoder deal with chunk processing.
        if (!ctx.FeedChunks(chunk)) {
          return JXL_FAILURE("Corrupt iCCP chunk");
        }

        // TODO(jon): catch special case of PQ and synthesize color encoding
        // in that case
        int compression_type = 0;
        png_bytep profile = nullptr;
        png_charp name = nullptr;
        png_uint_32 profile_len = 0;
        png_uint_32 ok =
            png_get_iCCP(ctx.png_ptr, ctx.info_ptr, &name, &compression_type,
                         &profile, &profile_len);
        if (!ok || !profile_len) {
          return JXL_FAILURE("Malformed / incomplete iCCP chunk");
        }
        ppf->icc.assign(profile, profile + profile_len);
        ppf->primary_color_representation = PackedPixelFile::kIccIsPrimary;
        color_info_type = ColorInfoType::ICCP_OR_SRGB;
        continue;
      }

      case MakeTag('s', 'R', 'G', 'B'):
        if (color_info_type == ColorInfoType::ICCP_OR_SRGB) {
          return JXL_FAILURE("Repeated iCCP / sRGB chunk");
        }
        if (color_info_type > ColorInfoType::ICCP_OR_SRGB) {
          JXL_DEBUG_V(2, "Excessive colorspace definition; sRGB chunk ignored");
          continue;
        }
        JXL_RETURN_IF_ERROR(DecodeSrgbChunk(payload, &ppf->color_encoding));
        color_info_type = ColorInfoType::ICCP_OR_SRGB;
        continue;

      case MakeTag('g', 'A', 'M', 'A'):
        if (color_info_type >= ColorInfoType::GAMA_OR_CHRM) {
          JXL_DEBUG_V(2, "Excessive colorspace definition; gAMA chunk ignored");
          continue;
        }
        JXL_RETURN_IF_ERROR(DecodeGamaChunk(payload, &ppf->color_encoding));
        color_info_type = ColorInfoType::GAMA_OR_CHRM;
        continue;

      case MakeTag('c', 'H', 'R', 'M'):
        if (color_info_type >= ColorInfoType::GAMA_OR_CHRM) {
          JXL_DEBUG_V(2, "Excessive colorspace definition; cHRM chunk ignored");
          continue;
        }
        JXL_RETURN_IF_ERROR(DecodeChrmChunk(payload, &ppf->color_encoding));
        color_info_type = ColorInfoType::GAMA_OR_CHRM;
        continue;

      case MakeTag('c', 'L', 'L', 'i'):
        JXL_RETURN_IF_ERROR(
            DecodeClliChunk(payload, &ppf->info.intensity_target));
        continue;

      case MakeTag('e', 'X', 'I', 'f'):
        // TODO(eustas): next eXIF chunk overwrites current; is it ok?
        ppf->metadata.exif.resize(payload.size());
        memcpy(ppf->metadata.exif.data(), payload.data(), payload.size());
        continue;

      default:
        // We don't know what is that, just pass through.
        if (!ctx.FeedChunks(chunk)) {
          return JXL_FAILURE("PNG decoder failed to process chunk");
        }
        // If it happens before IDAT, we consider it metadata and pass to all
        // sub-decoders.
        if (!seen_idat) {
          passthrough_chunks.push_back(chunk);
        }
        continue;
    }
  }

  bool color_is_already_set = (color_info_type != ColorInfoType::NONE);
  bool is_gray = (ppf->info.num_color_channels == 1);
  JXL_RETURN_IF_ERROR(
      ApplyColorHints(color_hints, color_is_already_set, is_gray, ppf));

  if (ppf->color_encoding.transfer_function != JXL_TRANSFER_FUNCTION_PQ) {
    // Reset intensity target, in case we set it from cLLi but TF is not PQ.
    ppf->info.intensity_target = 0.f;
  }

  bool has_nontrivial_background = false;
  bool previous_frame_should_be_cleared = false;
  for (size_t i = 0; i < frames.size(); i++) {
    Frame& frame = frames[i];
    const FrameControl& fc = frame.metadata;
    const RectT<uint64_t> vp = fc.viewport;
    const auto& pixels = frame.pixels;
    size_t xsize = pixels.xsize;
    size_t ysize = pixels.ysize;
    JXL_ENSURE(xsize == vp.xsize());
    JXL_ENSURE(ysize == vp.ysize());

    // Before encountering a DISPOSE_OP_NONE frame, the canvas is filled with
    // 0, so DISPOSE_OP_BACKGROUND and DISPOSE_OP_PREVIOUS are equivalent.
    if (fc.dispose_op == DisposeOp::NONE) {
      has_nontrivial_background = true;
    }
    bool should_blend = fc.blend_op == BlendOp::OVER;
    bool use_for_next_frame =
        has_nontrivial_background && fc.dispose_op != DisposeOp::PREVIOUS;
    size_t x0 = vp.x0();
    size_t y0 = vp.y0();
    if (previous_frame_should_be_cleared) {
      const auto& pvp = frames[i - 1].metadata.viewport;
      size_t px0 = pvp.x0();
      size_t py0 = pvp.y0();
      size_t pxs = pvp.xsize();
      size_t pys = pvp.ysize();
      if (px0 >= x0 && py0 >= y0 && px0 + pxs <= x0 + xsize &&
          py0 + pys <= y0 + ysize && fc.blend_op == BlendOp::SOURCE &&
          use_for_next_frame) {
        // If the previous frame is entirely contained in the current frame
        // and we are using BLEND_OP_SOURCE, nothing special needs to be done.
        ppf->frames.emplace_back(std::move(frame.pixels));
      } else if (px0 == x0 && py0 == y0 && px0 + pxs == x0 + xsize &&
                 py0 + pys == y0 + ysize && use_for_next_frame) {
        // If the new frame has the same size as the old one, but we are
        // blending, we can instead just not blend.
        should_blend = false;
        ppf->frames.emplace_back(std::move(frame.pixels));
      } else if (px0 <= x0 && py0 <= y0 && px0 + pxs >= x0 + xsize &&
                 py0 + pys >= y0 + ysize && use_for_next_frame) {
        // If the new frame is contained within the old frame, we can pad the
        // new frame with zeros and not blend.
        JXL_ASSIGN_OR_RETURN(PackedImage new_data,
                             PackedImage::Create(pxs, pys, pixels.format));
        memset(new_data.pixels(), 0, new_data.pixels_size);
        for (size_t y = 0; y < ysize; y++) {
          JXL_RETURN_IF_ERROR(
              PackedImage::ValidateDataType(new_data.format.data_type));
          size_t bytes_per_pixel =
              PackedImage::BitsPerChannel(new_data.format.data_type) *
              new_data.format.num_channels / 8;
          memcpy(
              static_cast<uint8_t*>(new_data.pixels()) +
                  new_data.stride * (y + y0 - py0) +
                  bytes_per_pixel * (x0 - px0),
              static_cast<const uint8_t*>(pixels.pixels()) + pixels.stride * y,
              xsize * bytes_per_pixel);
        }

        x0 = px0;
        y0 = py0;
        xsize = pxs;
        ysize = pys;
        should_blend = false;
        ppf->frames.emplace_back(std::move(new_data));
      } else {
        // If all else fails, insert a placeholder blank frame with kReplace.
        JXL_ASSIGN_OR_RETURN(PackedImage blank,
                             PackedImage::Create(pxs, pys, pixels.format));
        memset(blank.pixels(), 0, blank.pixels_size);
        ppf->frames.emplace_back(std::move(blank));
        auto& pframe = ppf->frames.back();
        pframe.frame_info.layer_info.crop_x0 = px0;
        pframe.frame_info.layer_info.crop_y0 = py0;
        pframe.frame_info.layer_info.xsize = pxs;
        pframe.frame_info.layer_info.ysize = pys;
        pframe.frame_info.duration = 0;
        bool is_full_size = px0 == 0 && py0 == 0 && pxs == ppf->info.xsize &&
                            pys == ppf->info.ysize;
        pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1;
        pframe.frame_info.layer_info.blend_info.blendmode = JXL_BLEND_REPLACE;
        pframe.frame_info.layer_info.blend_info.source = 1;
        pframe.frame_info.layer_info.save_as_reference = 1;
        ppf->frames.emplace_back(std::move(frame.pixels));
      }
    } else {
      ppf->frames.emplace_back(std::move(frame.pixels));
    }

    auto& pframe = ppf->frames.back();
    pframe.frame_info.layer_info.crop_x0 = x0;
    pframe.frame_info.layer_info.crop_y0 = y0;
    pframe.frame_info.layer_info.xsize = xsize;
    pframe.frame_info.layer_info.ysize = ysize;
    pframe.frame_info.duration =
        fc.delay_num * 1000 / (fc.delay_den ? fc.delay_den : 100);
    pframe.frame_info.layer_info.blend_info.blendmode =
        should_blend ? JXL_BLEND_BLEND : JXL_BLEND_REPLACE;
    bool is_full_size = x0 == 0 && y0 == 0 && xsize == ppf->info.xsize &&
                        ysize == ppf->info.ysize;
    pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1;
    pframe.frame_info.layer_info.blend_info.source = 1;
    pframe.frame_info.layer_info.blend_info.alpha = 0;
    pframe.frame_info.layer_info.save_as_reference = use_for_next_frame ? 1 : 0;

    previous_frame_should_be_cleared =
        has_nontrivial_background && (fc.dispose_op == DisposeOp::BACKGROUND);
  }

  if (ppf->frames.empty()) return JXL_FAILURE("No frames decoded");
  ppf->frames.back().frame_info.is_last = JXL_TRUE;

  return true;
}

#endif  // JPEGXL_ENABLE_APNG

}  // namespace extras
}  // namespace jxl
