| // Copyright 2013 The Flutter 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 "image_generator_apng.h" |
| #include <cstddef> |
| #include <cstring> |
| |
| #include "flutter/fml/logging.h" |
| #include "third_party/skia/include/codec/SkCodec.h" |
| #include "third_party/skia/include/codec/SkCodecAnimation.h" |
| #include "third_party/skia/include/core/SkAlphaType.h" |
| #include "third_party/skia/include/core/SkColorType.h" |
| #include "third_party/skia/include/core/SkImageInfo.h" |
| #include "third_party/skia/include/core/SkStream.h" |
| #include "third_party/zlib/zlib.h" // For crc32 |
| |
| namespace flutter { |
| |
| APNGImageGenerator::~APNGImageGenerator() = default; |
| |
| APNGImageGenerator::APNGImageGenerator(sk_sp<SkData>& data, |
| SkImageInfo& image_info, |
| APNGImage&& default_image, |
| unsigned int frame_count, |
| unsigned int play_count, |
| const void* next_chunk_p, |
| const std::vector<uint8_t>& header) |
| : data_(data), |
| image_info_(image_info), |
| frame_count_(frame_count), |
| play_count_(play_count), |
| first_frame_index_(default_image.frame_info.has_value() ? 0 : 1), |
| next_chunk_p_(next_chunk_p), |
| header_(header) { |
| images_.push_back(std::move(default_image)); |
| } |
| |
| const SkImageInfo& APNGImageGenerator::GetInfo() { |
| return image_info_; |
| } |
| |
| unsigned int APNGImageGenerator::GetFrameCount() const { |
| return frame_count_; |
| } |
| |
| unsigned int APNGImageGenerator::GetPlayCount() const { |
| return frame_count_ > 1 ? play_count_ : 1; |
| } |
| |
| const ImageGenerator::FrameInfo APNGImageGenerator::GetFrameInfo( |
| unsigned int frame_index) { |
| unsigned int image_index = first_frame_index_ + frame_index; |
| if (!DemuxToImageIndex(image_index)) { |
| return {}; |
| } |
| |
| auto frame_info = images_[image_index].frame_info; |
| if (frame_info.has_value()) { |
| return frame_info.value(); |
| } |
| return {}; |
| } |
| |
| SkISize APNGImageGenerator::GetScaledDimensions(float desired_scale) { |
| return image_info_.dimensions(); |
| } |
| |
| bool APNGImageGenerator::GetPixels(const SkImageInfo& info, |
| void* pixels, |
| size_t row_bytes, |
| unsigned int frame_index, |
| std::optional<unsigned int> prior_frame) { |
| FML_DCHECK(images_.size() > 0); |
| unsigned int image_index = first_frame_index_ + frame_index; |
| |
| //---------------------------------------------------------------------------- |
| /// 1. Demux the frame from the APNG stream. |
| /// |
| |
| if (!DemuxToImageIndex(image_index)) { |
| FML_DLOG(ERROR) << "Couldn't demux image at index " << image_index |
| << " (frame index: " << frame_index |
| << ") from APNG stream."; |
| return RenderDefaultImage(info, pixels, row_bytes); |
| } |
| |
| //---------------------------------------------------------------------------- |
| /// 2. Decode the frame. |
| /// |
| |
| APNGImage& frame = images_[image_index]; |
| SkImageInfo frame_info = frame.codec->getInfo(); |
| auto frame_row_bytes = frame_info.bytesPerPixel() * frame_info.width(); |
| |
| if (frame.pixels.empty()) { |
| frame.pixels.resize(frame_row_bytes * frame_info.height()); |
| SkCodec::Result result = frame.codec->getPixels( |
| frame.codec->getInfo(), frame.pixels.data(), frame_row_bytes); |
| if (result != SkCodec::kSuccess) { |
| FML_DLOG(ERROR) << "Failed to decode image at index " << image_index |
| << " (frame index: " << frame_index |
| << ") of APNG. SkCodec::Result: " << result; |
| return RenderDefaultImage(info, pixels, row_bytes); |
| } |
| } |
| if (!frame.frame_info.has_value()) { |
| FML_DLOG(ERROR) << "Failed to decode image at index " << image_index |
| << " (frame index: " << frame_index |
| << ") of APNG due to the frame missing data (frame_info)."; |
| return false; |
| } |
| |
| //---------------------------------------------------------------------------- |
| /// 3. Composite the frame onto the canvas. |
| /// |
| |
| if (info.colorType() != kN32_SkColorType) { |
| FML_DLOG(ERROR) << "Failed to composite image at index " << image_index |
| << " (frame index: " << frame_index |
| << ") of APNG due to the destination surface having an " |
| "unsupported color type."; |
| return false; |
| } |
| if (frame_info.colorType() != kN32_SkColorType) { |
| FML_DLOG(ERROR) |
| << "Failed to composite image at index " << image_index |
| << " (frame index: " << frame_index |
| << ") of APNG due to the frame having an unsupported color type."; |
| return false; |
| } |
| |
| // Regardless of the byte order (RGBA vs BGRA), the blending operations are |
| // the same. |
| struct Pixel { |
| uint8_t channel[4]; |
| |
| uint8_t GetAlpha() { return channel[3]; } |
| |
| void Premultiply() { |
| for (int i = 0; i < 3; i++) { |
| channel[i] = channel[i] * GetAlpha() / 0xFF; |
| } |
| } |
| |
| void Unpremultiply() { |
| if (GetAlpha() == 0) { |
| channel[0] = channel[1] = channel[2] = 0; |
| return; |
| } |
| for (int i = 0; i < 3; i++) { |
| channel[i] = channel[i] * 0xFF / GetAlpha(); |
| } |
| } |
| }; |
| |
| FML_DCHECK(frame_info.bytesPerPixel() == sizeof(Pixel)); |
| |
| bool result = true; |
| |
| if (frame.frame_info->blend_mode == SkCodecAnimation::Blend::kSrc) { |
| SkPixmap src_pixmap(frame_info, frame.pixels.data(), frame_row_bytes); |
| uint8_t* dst_pixels = static_cast<uint8_t*>(pixels) + |
| frame.y_offset * row_bytes + |
| frame.x_offset * frame_info.bytesPerPixel(); |
| result = src_pixmap.readPixels(info, dst_pixels, row_bytes); |
| if (!result) { |
| FML_DLOG(ERROR) << "Failed to copy pixels at index " << image_index |
| << " (frame index: " << frame_index << ") of APNG."; |
| } |
| } else if (frame.frame_info->blend_mode == |
| SkCodecAnimation::Blend::kSrcOver) { |
| for (int y = 0; y < frame_info.height(); y++) { |
| auto src_row = frame.pixels.data() + y * frame_row_bytes; |
| auto dst_row = static_cast<uint8_t*>(pixels) + |
| (y + frame.y_offset) * row_bytes + |
| frame.x_offset * frame_info.bytesPerPixel(); |
| |
| for (int x = 0; x < frame_info.width(); x++) { |
| auto x_offset_bytes = x * frame_info.bytesPerPixel(); |
| |
| Pixel src = *reinterpret_cast<Pixel*>(src_row + x_offset_bytes); |
| Pixel* dst_p = reinterpret_cast<Pixel*>(dst_row + x_offset_bytes); |
| Pixel dst = *dst_p; |
| |
| // Ensure both colors are premultiplied for the blending operation. |
| if (info.alphaType() == kUnpremul_SkAlphaType) { |
| dst.Premultiply(); |
| } |
| if (frame_info.alphaType() == kUnpremul_SkAlphaType) { |
| src.Premultiply(); |
| } |
| |
| for (int i = 0; i < 4; i++) { |
| dst.channel[i] = |
| src.channel[i] + dst.channel[i] * (0xFF - src.GetAlpha()) / 0xFF; |
| } |
| |
| // The final color is premultiplied. Unpremultiply to match the |
| // backdrop surface if necessary. |
| if (info.alphaType() == kUnpremul_SkAlphaType) { |
| dst.Unpremultiply(); |
| } |
| |
| *dst_p = dst; |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| std::unique_ptr<ImageGenerator> APNGImageGenerator::MakeFromData( |
| sk_sp<SkData> data) { |
| // Ensure the buffer is large enough to at least contain the PNG signature |
| // and a chunk header. |
| if (data->size() < sizeof(kPngSignature) + sizeof(ChunkHeader)) { |
| return nullptr; |
| } |
| // Validate the full PNG signature. |
| const uint8_t* data_p = static_cast<const uint8_t*>(data.get()->data()); |
| if (memcmp(data_p, kPngSignature, sizeof(kPngSignature))) { |
| return nullptr; |
| } |
| |
| // Validate the header chunk. |
| const ChunkHeader* chunk = reinterpret_cast<const ChunkHeader*>(data_p + 8); |
| if (!IsValidChunkHeader(data_p, data->size(), chunk) || |
| chunk->get_data_length() != sizeof(ImageHeaderChunkData) || |
| chunk->get_type() != kImageHeaderChunkType) { |
| return nullptr; |
| } |
| |
| // Walk the chunks to find the "animation control" chunk. If an "image data" |
| // chunk is found first, this PNG is not animated. |
| while (true) { |
| chunk = GetNextChunk(data_p, data->size(), chunk); |
| |
| if (chunk == nullptr) { |
| return nullptr; |
| } |
| if (chunk->get_type() == kImageDataChunkType) { |
| return nullptr; |
| } |
| if (chunk->get_type() == kAnimationControlChunkType) { |
| break; |
| } |
| } |
| |
| const AnimationControlChunkData* animation_data = |
| CastChunkData<AnimationControlChunkData>(chunk); |
| |
| // Extract the header signature and chunks to prepend when demuxing images. |
| std::optional<std::vector<uint8_t>> header; |
| const void* first_chunk_p; |
| std::tie(header, first_chunk_p) = ExtractHeader(data_p, data->size()); |
| if (!header.has_value()) { |
| return nullptr; |
| } |
| |
| // Demux the first image in the APNG chunk stream in order to interpret |
| // extent and blending info immediately. |
| std::optional<APNGImage> default_image; |
| const void* next_chunk_p; |
| std::tie(default_image, next_chunk_p) = |
| DemuxNextImage(data_p, data->size(), header.value(), first_chunk_p); |
| if (!default_image.has_value()) { |
| return nullptr; |
| } |
| |
| unsigned int play_count = animation_data->get_num_plays(); |
| if (play_count == 0) { |
| play_count = kInfinitePlayCount; |
| } |
| |
| SkImageInfo image_info = default_image.value().codec->getInfo(); |
| return std::unique_ptr<APNGImageGenerator>( |
| new APNGImageGenerator(data, image_info, std::move(default_image.value()), |
| animation_data->get_num_frames(), play_count, |
| next_chunk_p, header.value())); |
| } |
| |
| bool APNGImageGenerator::IsValidChunkHeader(const void* buffer, |
| size_t size, |
| const ChunkHeader* chunk) { |
| // Ensure the chunk doesn't start before the beginning of the buffer. |
| if (reinterpret_cast<const uint8_t*>(chunk) < |
| static_cast<const uint8_t*>(buffer)) { |
| return false; |
| } |
| |
| // Ensure the buffer is large enough to contain at least the chunk header. |
| if (reinterpret_cast<const uint8_t*>(chunk) + sizeof(ChunkHeader) > |
| static_cast<const uint8_t*>(buffer) + size) { |
| return false; |
| } |
| |
| // Ensure the buffer is large enough to contain the chunk's given data size |
| // and CRC. |
| const uint8_t* chunk_end = |
| reinterpret_cast<const uint8_t*>(chunk) + GetChunkSize(chunk); |
| if (chunk_end > static_cast<const uint8_t*>(buffer) + size) { |
| return false; |
| } |
| |
| // Ensure the 4-byte type only contains ISO 646 letters. |
| uint32_t type = chunk->get_type(); |
| for (int i = 0; i < 4; i++) { |
| uint8_t c = type >> i * 8 & 0xFF; |
| if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| const APNGImageGenerator::ChunkHeader* APNGImageGenerator::GetNextChunk( |
| const void* buffer, |
| size_t size, |
| const ChunkHeader* current_chunk) { |
| FML_DCHECK((uint8_t*)current_chunk + sizeof(ChunkHeader) <= |
| (uint8_t*)buffer + size); |
| |
| const ChunkHeader* next_chunk = reinterpret_cast<const ChunkHeader*>( |
| reinterpret_cast<const uint8_t*>(current_chunk) + |
| GetChunkSize(current_chunk)); |
| if (!IsValidChunkHeader(buffer, size, next_chunk)) { |
| return nullptr; |
| } |
| |
| return next_chunk; |
| } |
| |
| std::pair<std::optional<std::vector<uint8_t>>, const void*> |
| APNGImageGenerator::ExtractHeader(const void* buffer_p, size_t buffer_size) { |
| std::vector<uint8_t> result(sizeof(kPngSignature)); |
| memcpy(result.data(), kPngSignature, sizeof(kPngSignature)); |
| |
| const ChunkHeader* chunk = reinterpret_cast<const ChunkHeader*>( |
| static_cast<const uint8_t*>(buffer_p) + sizeof(kPngSignature)); |
| // Validate the first chunk to ensure it's safe to read. |
| if (!IsValidChunkHeader(buffer_p, buffer_size, chunk)) { |
| return std::make_pair(std::nullopt, nullptr); |
| } |
| |
| // Walk the chunks and copy in the non-APNG chunks until we come across a |
| // frame or image chunk. |
| do { |
| if (chunk->get_type() != kAnimationControlChunkType) { |
| size_t chunk_size = GetChunkSize(chunk); |
| result.resize(result.size() + chunk_size); |
| memcpy(result.data() + result.size() - chunk_size, chunk, chunk_size); |
| } |
| |
| chunk = GetNextChunk(buffer_p, buffer_size, chunk); |
| } while (chunk != nullptr && chunk->get_type() != kFrameControlChunkType && |
| chunk->get_type() != kImageDataChunkType && |
| chunk->get_type() != kFrameDataChunkType); |
| |
| // nullptr means the end of the buffer was reached, which means there's no |
| // frame or image data, so just return nothing because the PNG isn't even |
| // valid. |
| if (chunk == nullptr) { |
| return std::make_pair(std::nullopt, nullptr); |
| } |
| |
| return std::make_pair(result, chunk); |
| } |
| |
| std::pair<std::optional<APNGImageGenerator::APNGImage>, const void*> |
| APNGImageGenerator::DemuxNextImage(const void* buffer_p, |
| size_t buffer_size, |
| const std::vector<uint8_t>& header, |
| const void* chunk_p) { |
| const ChunkHeader* chunk = reinterpret_cast<const ChunkHeader*>(chunk_p); |
| // Validate the given chunk to ensure it's safe to read. |
| if (!IsValidChunkHeader(buffer_p, buffer_size, chunk)) { |
| return std::make_pair(std::nullopt, nullptr); |
| } |
| |
| // Expect frame data to begin at fdAT or IDAT |
| if (chunk->get_type() != kFrameControlChunkType && |
| chunk->get_type() != kImageDataChunkType) { |
| return std::make_pair(std::nullopt, nullptr); |
| } |
| |
| APNGImage result; |
| const FrameControlChunkData* control_data = nullptr; |
| |
| // The presence of an fcTL chunk is optional for the first (default) image |
| // of a PNG. Both cases are handled in APNGImage. |
| if (chunk->get_type() == kFrameControlChunkType) { |
| control_data = CastChunkData<FrameControlChunkData>(chunk); |
| |
| ImageGenerator::FrameInfo frame_info; |
| switch (control_data->get_blend_op()) { |
| case 0: // APNG_BLEND_OP_SOURCE |
| frame_info.blend_mode = SkCodecAnimation::Blend::kSrc; |
| break; |
| case 1: // APNG_BLEND_OP_OVER |
| frame_info.blend_mode = SkCodecAnimation::Blend::kSrcOver; |
| break; |
| default: |
| return std::make_pair(std::nullopt, nullptr); |
| } |
| |
| SkIRect frame_rect = SkIRect::MakeXYWH( |
| control_data->get_x_offset(), control_data->get_y_offset(), |
| control_data->get_width(), control_data->get_height()); |
| switch (control_data->get_dispose_op()) { |
| case 0: // APNG_DISPOSE_OP_NONE |
| frame_info.disposal_method = SkCodecAnimation::DisposalMethod::kKeep; |
| break; |
| case 1: // APNG_DISPOSE_OP_BACKGROUND |
| frame_info.disposal_method = |
| SkCodecAnimation::DisposalMethod::kRestoreBGColor; |
| frame_info.disposal_rect = frame_rect; |
| break; |
| case 2: // APNG_DISPOSE_OP_PREVIOUS |
| frame_info.disposal_method = |
| SkCodecAnimation::DisposalMethod::kRestorePrevious; |
| break; |
| default: |
| return std::make_pair(std::nullopt, nullptr); |
| } |
| uint16_t denominator = control_data->get_delay_den() == 0 |
| ? 100 |
| : control_data->get_delay_den(); |
| frame_info.duration = |
| static_cast<int>(control_data->get_delay_num() * 1000.f / denominator); |
| |
| result.frame_info = frame_info; |
| result.x_offset = control_data->get_x_offset(); |
| result.y_offset = control_data->get_y_offset(); |
| } |
| |
| std::vector<const ChunkHeader*> image_chunks; |
| size_t chunk_space = 0; |
| |
| // Walk the chunks until the next frame, end chunk, or an invalid chunk is |
| // reached, recording the chunks to copy along with their required space. |
| // TODO(bdero): Validate that IDAT/fdAT chunks are contiguous. |
| // TODO(bdero): Validate the acTL/fcTL/fdAT sequence number ordering. |
| do { |
| if (chunk->get_type() != kFrameControlChunkType) { |
| image_chunks.push_back(chunk); |
| chunk_space += GetChunkSize(chunk); |
| |
| // fdAT chunks are converted into IDAT chunks when demuxed. The only |
| // difference between these chunk types is that fdAT has a 4 byte |
| // sequence number prepended to its data, so subtract that space from |
| // the buffer. |
| if (chunk->get_type() == kFrameDataChunkType) { |
| chunk_space -= 4; |
| } |
| } |
| |
| chunk = GetNextChunk(buffer_p, buffer_size, chunk); |
| } while (chunk != nullptr && chunk->get_type() != kFrameControlChunkType && |
| chunk->get_type() != kImageTrailerChunkType); |
| |
| const uint8_t end_chunk[] = {0, 0, 0, 0, 'I', 'E', |
| 'N', 'D', 0xAE, 0x42, 0x60, 0x82}; |
| |
| // Form a buffer for the new encoded PNG and copy the chunks in. |
| sk_sp<SkData> new_png_buffer = SkData::MakeUninitialized( |
| header.size() + chunk_space + sizeof(end_chunk)); |
| |
| { |
| uint8_t* write_cursor = |
| static_cast<uint8_t*>(new_png_buffer->writable_data()); |
| |
| // Copy the signature/header chunks |
| memcpy(write_cursor, header.data(), header.size()); |
| // If this is a frame, override the width/height in the IHDR chunk. |
| if (control_data) { |
| ChunkHeader* ihdr_header = |
| reinterpret_cast<ChunkHeader*>(write_cursor + sizeof(kPngSignature)); |
| ImageHeaderChunkData* ihdr_data = const_cast<ImageHeaderChunkData*>( |
| CastChunkData<ImageHeaderChunkData>(ihdr_header)); |
| ihdr_data->set_width(control_data->get_width()); |
| ihdr_data->set_height(control_data->get_height()); |
| ihdr_header->UpdateChunkCrc32(); |
| } |
| write_cursor += header.size(); |
| |
| // Copy the image data/ancillary chunks. |
| for (const ChunkHeader* c : image_chunks) { |
| if (c->get_type() == kFrameDataChunkType) { |
| // Write a new IDAT chunk header. |
| ChunkHeader* write_header = |
| reinterpret_cast<ChunkHeader*>(write_cursor); |
| write_header->set_data_length(c->get_data_length() - 4); |
| write_header->set_type(kImageDataChunkType); |
| write_cursor += sizeof(ChunkHeader); |
| |
| // Copy all of the data except for the 4 byte sequence number at the |
| // beginning of the fdAT data. |
| memcpy(write_cursor, |
| reinterpret_cast<const uint8_t*>(c) + sizeof(ChunkHeader) + 4, |
| write_header->get_data_length()); |
| write_cursor += write_header->get_data_length(); |
| |
| // Recompute the chunk CRC. |
| write_header->UpdateChunkCrc32(); |
| write_cursor += 4; |
| } else { |
| size_t chunk_size = GetChunkSize(c); |
| memcpy(write_cursor, c, chunk_size); |
| write_cursor += chunk_size; |
| } |
| } |
| |
| // Copy the trailer chunk. |
| memcpy(write_cursor, &end_chunk, sizeof(end_chunk)); |
| } |
| |
| SkCodec::Result header_parse_result; |
| result.codec = SkCodec::MakeFromStream(SkMemoryStream::Make(new_png_buffer), |
| &header_parse_result); |
| if (header_parse_result != SkCodec::Result::kSuccess) { |
| FML_DLOG(ERROR) |
| << "Failed to parse image header during APNG demux. SkCodec::Result: " |
| << header_parse_result; |
| return std::make_pair(std::nullopt, nullptr); |
| } |
| |
| if (chunk->get_type() == kImageTrailerChunkType) { |
| chunk = nullptr; |
| } |
| |
| return std::make_pair(std::optional<APNGImage>{std::move(result)}, chunk); |
| } |
| |
| bool APNGImageGenerator::DemuxNextImageInternal() { |
| if (next_chunk_p_ == nullptr) { |
| return false; |
| } |
| |
| std::optional<APNGImage> image; |
| const void* data_p = const_cast<void*>(data_.get()->data()); |
| std::tie(image, next_chunk_p_) = |
| DemuxNextImage(data_p, data_->size(), header_, next_chunk_p_); |
| if (!image.has_value() || !image->frame_info.has_value()) { |
| return false; |
| } |
| |
| auto last_frame_info = images_.back().frame_info; |
| if (!last_frame_info.has_value()) { |
| return false; |
| } |
| |
| if (images_.size() > first_frame_index_ && |
| (last_frame_info->disposal_method == |
| SkCodecAnimation::DisposalMethod::kKeep || |
| last_frame_info->disposal_method == |
| SkCodecAnimation::DisposalMethod::kRestoreBGColor)) { |
| // Mark the required frame as the previous frame in all cases. |
| image->frame_info->required_frame = images_.size() - 1; |
| } else if (images_.size() > (first_frame_index_ + 1) && |
| last_frame_info->disposal_method == |
| SkCodecAnimation::DisposalMethod::kRestorePrevious) { |
| // Mark the required frame as the last previous frame |
| // It is not valid if there are 2 or above frames set |disposal_method| to |
| // |kRestorePrevious|. But it also works in MultiFrameCodec. |
| image->frame_info->required_frame = images_.size() - 2; |
| } |
| |
| // Calling SkCodec::getInfo at least once prior to decoding is mandatory. |
| SkImageInfo info = image.value().codec->getInfo(); |
| FML_DCHECK(info.colorInfo() == image_info_.colorInfo()); |
| |
| images_.push_back(std::move(image.value())); |
| |
| auto default_info = images_[0].codec->getInfo(); |
| if (info.colorType() != default_info.colorType()) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool APNGImageGenerator::DemuxToImageIndex(unsigned int image_index) { |
| // If the requested image doesn't exist yet, demux more frames from the APNG |
| // stream. |
| if (image_index >= images_.size()) { |
| while (DemuxNextImageInternal() && image_index >= images_.size()) { |
| } |
| |
| if (image_index >= images_.size()) { |
| // The chunk stream was exhausted before the image was found. |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| void APNGImageGenerator::ChunkHeader::UpdateChunkCrc32() { |
| uint32_t* crc_p = |
| reinterpret_cast<uint32_t*>(reinterpret_cast<uint8_t*>(this) + |
| sizeof(ChunkHeader) + get_data_length()); |
| *crc_p = fml::BigEndianToArch(ComputeChunkCrc32()); |
| } |
| |
| uint32_t APNGImageGenerator::ChunkHeader::ComputeChunkCrc32() { |
| // Exclude the length field at the beginning of the chunk header. |
| size_t length = sizeof(ChunkHeader) - 4 + get_data_length(); |
| uint8_t* chunk_data_p = reinterpret_cast<uint8_t*>(this) + 4; |
| uint32_t crc = 0; |
| |
| // zlib's crc32 can only take 16 bits at a time for the length, but PNG |
| // supports a 32 bit chunk length, so looping is necessary here. |
| // Note that crc32 is always called at least once, even if the chunk has an |
| // empty data section. |
| do { |
| uint16_t length16 = length; |
| if (length16 == 0 && length > 0) { |
| length16 = std::numeric_limits<uint16_t>::max(); |
| } |
| |
| crc = crc32(crc, chunk_data_p, length16); |
| length -= length16; |
| chunk_data_p += length16; |
| } while (length > 0); |
| |
| return crc; |
| } |
| |
| bool APNGImageGenerator::RenderDefaultImage(const SkImageInfo& info, |
| void* pixels, |
| size_t row_bytes) { |
| SkCodec::Result result = images_[0].codec->getPixels(info, pixels, row_bytes); |
| if (result != SkCodec::kSuccess) { |
| FML_DLOG(ERROR) << "Failed to decode the APNG's default/fallback image. " |
| "SkCodec::Result: " |
| << result; |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace flutter |