diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 4d518c29f3bae..b2250fc45b381 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1793,6 +1793,8 @@ ORIGIN: ../../../flutter/lib/ui/painting/image_filter.cc + ../../../flutter/LICE ORIGIN: ../../../flutter/lib/ui/painting/image_filter.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/image_generator.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/image_generator.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/ui/painting/image_generator_apng.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/ui/painting/image_generator_apng.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/image_generator_registry.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/image_generator_registry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/image_shader.cc + ../../../flutter/LICENSE @@ -4273,6 +4275,8 @@ FILE: ../../../flutter/lib/ui/painting/image_filter.cc FILE: ../../../flutter/lib/ui/painting/image_filter.h FILE: ../../../flutter/lib/ui/painting/image_generator.cc FILE: ../../../flutter/lib/ui/painting/image_generator.h +FILE: ../../../flutter/lib/ui/painting/image_generator_apng.cc +FILE: ../../../flutter/lib/ui/painting/image_generator_apng.h FILE: ../../../flutter/lib/ui/painting/image_generator_registry.cc FILE: ../../../flutter/lib/ui/painting/image_generator_registry.h FILE: ../../../flutter/lib/ui/painting/image_shader.cc diff --git a/fml/endianness.h b/fml/endianness.h index 269b461492184..eff90f9cafb3c 100644 --- a/fml/endianness.h +++ b/fml/endianness.h @@ -26,9 +26,17 @@ namespace fml { +template +struct IsByteSwappable + : public std:: + integral_constant || std::is_enum_v> { +}; +template +constexpr bool IsByteSwappableV = IsByteSwappable::value; + /// @brief Flips the endianness of the given value. /// The given value must be an integral type of size 1, 2, 4, or 8. -template >> +template >> constexpr T ByteSwap(T n) { if constexpr (sizeof(T) == 1) { return n; @@ -47,7 +55,7 @@ constexpr T ByteSwap(T n) { /// current architecture. This is effectively a cross platform /// ntohl/ntohs (as network byte order is always Big Endian). /// The given value must be an integral type of size 1, 2, 4, or 8. -template >> +template >> constexpr T BigEndianToArch(T n) { #if FML_ARCH_CPU_LITTLE_ENDIAN return ByteSwap(n); @@ -59,7 +67,7 @@ constexpr T BigEndianToArch(T n) { /// @brief Convert a known little endian value to match the endianness of the /// current architecture. /// The given value must be an integral type of size 1, 2, 4, or 8. -template >> +template >> constexpr T LittleEndianToArch(T n) { #if !FML_ARCH_CPU_LITTLE_ENDIAN return ByteSwap(n); diff --git a/lib/ui/BUILD.gn b/lib/ui/BUILD.gn index adaec644f017b..1556126b2d5e4 100644 --- a/lib/ui/BUILD.gn +++ b/lib/ui/BUILD.gn @@ -59,6 +59,8 @@ source_set("ui") { "painting/image_filter.h", "painting/image_generator.cc", "painting/image_generator.h", + "painting/image_generator_apng.cc", + "painting/image_generator_apng.h", "painting/image_generator_registry.cc", "painting/image_generator_registry.h", "painting/image_shader.cc", @@ -161,6 +163,7 @@ source_set("ui") { "//third_party/dart/runtime/bin:dart_io_api", "//third_party/rapidjson", "//third_party/skia", + "//third_party/zlib:zlib", ] if (impeller_supports_rendering) { diff --git a/lib/ui/painting/image_decoder_unittests.cc b/lib/ui/painting/image_decoder_unittests.cc index 7a5e3cfd0aa00..4f91282281b28 100644 --- a/lib/ui/painting/image_decoder_unittests.cc +++ b/lib/ui/painting/image_decoder_unittests.cc @@ -162,7 +162,7 @@ class UnknownImageGenerator : public ImageGenerator { unsigned int GetPlayCount() const { return 1; } - const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) const { + const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) { return {std::nullopt, 0, SkCodecAnimation::DisposalMethod::kKeep}; } diff --git a/lib/ui/painting/image_generator.cc b/lib/ui/painting/image_generator.cc index e5648fc24a97b..3db7625c741df 100644 --- a/lib/ui/painting/image_generator.cc +++ b/lib/ui/painting/image_generator.cc @@ -51,7 +51,7 @@ unsigned int BuiltinSkiaImageGenerator::GetPlayCount() const { } const ImageGenerator::FrameInfo BuiltinSkiaImageGenerator::GetFrameInfo( - unsigned int frame_index) const { + unsigned int frame_index) { return {.required_frame = std::nullopt, .duration = 0, .disposal_method = SkCodecAnimation::DisposalMethod::kKeep}; @@ -105,7 +105,7 @@ unsigned int BuiltinSkiaCodecImageGenerator::GetPlayCount() const { } const ImageGenerator::FrameInfo BuiltinSkiaCodecImageGenerator::GetFrameInfo( - unsigned int frame_index) const { + unsigned int frame_index) { SkCodec::FrameInfo info = {}; codec_generator_->getFrameInfo(frame_index, &info); return { diff --git a/lib/ui/painting/image_generator.h b/lib/ui/painting/image_generator.h index 4792f3b374b92..a5515c8057319 100644 --- a/lib/ui/painting/image_generator.h +++ b/lib/ui/painting/image_generator.h @@ -37,11 +37,15 @@ class ImageGenerator { /// blended with. std::optional required_frame; - /// Number of milliseconds to show this frame. + /// Number of milliseconds to show this frame. 0 means only show it for one + /// frame. unsigned int duration; /// How this frame should be modified before decoding the next one. SkCodecAnimation::DisposalMethod disposal_method; + + /// How this frame should be blended with the previous frame. + SkCodecAnimation::Blend blend_mode; }; virtual ~ImageGenerator(); @@ -80,7 +84,7 @@ class ImageGenerator { /// @return Information about the given frame. If the image is /// single-frame, a default result is returned. /// @see `GetFrameCount` - virtual const FrameInfo GetFrameInfo(unsigned int frame_index) const = 0; + virtual const FrameInfo GetFrameInfo(unsigned int frame_index) = 0; /// @brief Given a scale value, find the closest image size that can be /// used for efficiently decoding the image. If subpixel image @@ -152,7 +156,7 @@ class BuiltinSkiaImageGenerator : public ImageGenerator { // |ImageGenerator| const ImageGenerator::FrameInfo GetFrameInfo( - unsigned int frame_index) const override; + unsigned int frame_index) override; // |ImageGenerator| SkISize GetScaledDimensions(float desired_scale) override; @@ -192,7 +196,7 @@ class BuiltinSkiaCodecImageGenerator : public ImageGenerator { // |ImageGenerator| const ImageGenerator::FrameInfo GetFrameInfo( - unsigned int frame_index) const override; + unsigned int frame_index) override; // |ImageGenerator| SkISize GetScaledDimensions(float desired_scale) override; diff --git a/lib/ui/painting/image_generator_apng.cc b/lib/ui/painting/image_generator_apng.cc new file mode 100644 index 0000000000000..943d7752015db --- /dev/null +++ b/lib/ui/painting/image_generator_apng.cc @@ -0,0 +1,616 @@ +// 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 +#include + +#include "flutter/fml/logging.h" +#include "third_party/libpng/png.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/SkStream.h" +#include "third_party/skia/src/codec/SkPngCodec.h" +#include "third_party/zlib/zlib.h" // For crc32 + +namespace flutter { + +APNGImageGenerator::~APNGImageGenerator() = default; + +APNGImageGenerator::APNGImageGenerator(sk_sp& data, + SkImageInfo& image_info, + APNGImage&& default_image, + unsigned int frame_count, + unsigned int play_count, + const void* next_chunk_p, + const std::vector& 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 {}; + } + + return images_[image_index].frame_info.value(); +} + +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 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]; + auto 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); + } + } + + //---------------------------------------------------------------------------- + /// 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)); + + for (int y = 0; y < frame_info.height(); y++) { + auto src_row = frame.pixels.data() + y * frame_row_bytes; + auto dst_row = static_cast(pixels) + + (y + frame.y_offset) * row_bytes + + frame.x_offset * frame_info.bytesPerPixel(); + + switch (frame.frame_info->blend_mode) { + case SkCodecAnimation::Blend::kSrcOver: { + for (int x = 0; x < frame_info.width(); x++) { + auto x_offset_bytes = x * frame_info.bytesPerPixel(); + + Pixel src = *reinterpret_cast(src_row + x_offset_bytes); + Pixel* dst_p = reinterpret_cast(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; + } + break; + } + case SkCodecAnimation::Blend::kSrc: + memcpy(dst_row, src_row, frame_row_bytes); + break; + } + } + + return true; +} + +std::unique_ptr APNGImageGenerator::MakeFromData( + sk_sp 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(data.get()->data()); + if (png_sig_cmp(static_cast(data_p), 0, + sizeof(kPngSignature))) { + return nullptr; + } + + // Validate the header chunk. + const ChunkHeader* chunk = reinterpret_cast(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(chunk); + + // Extract the header signature and chunks to prepend when demuxing images. + std::optional> 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 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( + 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(chunk) < + static_cast(buffer)) { + return false; + } + + // Ensure the buffer is large enough to contain at least the chunk header. + if (reinterpret_cast(chunk) + sizeof(ChunkHeader) > + static_cast(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(chunk) + GetChunkSize(chunk); + if (chunk_end > static_cast(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( + reinterpret_cast(current_chunk) + + GetChunkSize(current_chunk)); + if (!IsValidChunkHeader(buffer, size, next_chunk)) { + return nullptr; + } + + return next_chunk; +} + +std::pair>, const void*> +APNGImageGenerator::ExtractHeader(const void* buffer_p, size_t buffer_size) { + std::vector result(sizeof(kPngSignature)); + memcpy(result.data(), kPngSignature, sizeof(kPngSignature)); + + const ChunkHeader* chunk = reinterpret_cast( + static_cast(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, const void*> +APNGImageGenerator::DemuxNextImage(const void* buffer_p, + size_t buffer_size, + const std::vector& header, + const void* chunk_p) { + const ChunkHeader* chunk = reinterpret_cast(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(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); + } + 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; + 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(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 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 new_png_buffer = SkData::MakeUninitialized( + header.size() + chunk_space + sizeof(end_chunk)); + + { + uint8_t* write_cursor = + static_cast(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(write_cursor + sizeof(kPngSignature)); + ImageHeaderChunkData* ihdr_data = const_cast( + CastChunkData(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(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(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 = SkPngCodec::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{std::move(result)}, chunk); +} + +bool APNGImageGenerator::DemuxNextImageInternal() { + if (next_chunk_p_ == nullptr) { + return false; + } + + std::optional image; + const void* data_p = const_cast(data_.get()->data()); + std::tie(image, next_chunk_p_) = + DemuxNextImage(data_p, data_->size(), header_, next_chunk_p_); + if (!image.has_value()) { + return false; + } + + if (images_.back().frame_info->disposal_method == + SkCodecAnimation::DisposalMethod::kRestorePrevious) { + FML_DLOG(INFO) + << "DisposalMethod::kRestorePrevious is not supported by the " + "MultiFrameCodec. Falling back to DisposalMethod::kRestoreBGColor " + " behavior instead."; + } + + if (images_.size() > first_frame_index_ && + images_.back().frame_info->disposal_method == + SkCodecAnimation::DisposalMethod::kKeep) { + // Mark the required frame as the previous frame in all cases. + image->frame_info->required_frame = images_.size() - 1; + } + + // 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(reinterpret_cast(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(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::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 diff --git a/lib/ui/painting/image_generator_apng.h b/lib/ui/painting/image_generator_apng.h new file mode 100644 index 0000000000000..a18ef58cf008c --- /dev/null +++ b/lib/ui/painting/image_generator_apng.h @@ -0,0 +1,216 @@ +// 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.h" + +#include "flutter/fml/endianness.h" +#include "flutter/fml/logging.h" + +#define PNG_FIELD(T, name) \ + private: \ + T name; \ + \ + public: \ + T get_##name() const { \ + return fml::BigEndianToArch(name); \ + } \ + void set_##name(T n) { \ + name = fml::BigEndianToArch(n); \ + } + +namespace flutter { + +class APNGImageGenerator : public ImageGenerator { + public: + ~APNGImageGenerator(); + + // |ImageGenerator| + const SkImageInfo& GetInfo() override; + + // |ImageGenerator| + unsigned int GetFrameCount() const override; + + // |ImageGenerator| + unsigned int GetPlayCount() const override; + + // |ImageGenerator| + const ImageGenerator::FrameInfo GetFrameInfo( + unsigned int frame_index) override; + + // |ImageGenerator| + SkISize GetScaledDimensions(float desired_scale) override; + + // |ImageGenerator| + bool GetPixels(const SkImageInfo& info, + void* pixels, + size_t row_bytes, + unsigned int frame_index, + std::optional prior_frame) override; + + static std::unique_ptr MakeFromData(sk_sp data); + + private: + static constexpr uint8_t kPngSignature[8] = {137, 80, 78, 71, 13, 10, 26, 10}; + static constexpr size_t kChunkCrcSize = 4; + + enum ChunkType { + kImageHeaderChunkType = 'IHDR', + kAnimationControlChunkType = 'acTL', + kImageDataChunkType = 'IDAT', + kFrameControlChunkType = 'fcTL', + kFrameDataChunkType = 'fdAT', + kImageTrailerChunkType = 'IEND', + }; + + class __attribute__((packed, aligned(1))) ChunkHeader { + PNG_FIELD(uint32_t, data_length) + PNG_FIELD(ChunkType, type) + + public: + void UpdateChunkCrc32(); + + private: + uint32_t ComputeChunkCrc32(); + }; + + class __attribute__((packed, aligned(1))) ImageHeaderChunkData { + PNG_FIELD(uint32_t, width) + PNG_FIELD(uint32_t, height) + PNG_FIELD(uint8_t, bit_depth) + PNG_FIELD(uint8_t, color_type) + PNG_FIELD(uint8_t, compression_method) + PNG_FIELD(uint8_t, filter_method) + PNG_FIELD(uint8_t, interlace_method) + }; + + class __attribute__((packed, aligned(1))) AnimationControlChunkData { + PNG_FIELD(uint32_t, num_frames) + PNG_FIELD(uint32_t, num_plays) + }; + + class __attribute__((packed, aligned(1))) FrameControlChunkData { + PNG_FIELD(uint32_t, sequence_number) + PNG_FIELD(uint32_t, width) + PNG_FIELD(uint32_t, height) + PNG_FIELD(uint32_t, x_offset) + PNG_FIELD(uint32_t, y_offset) + PNG_FIELD(uint16_t, delay_num) + PNG_FIELD(uint16_t, delay_den) + PNG_FIELD(uint8_t, dispose_op) + PNG_FIELD(uint8_t, blend_op) + }; + + /// @brief The first PNG frame is always the "default" PNG frame. Absence of + /// `frame_info` is only possible on the "default" PNG frame. + /// Each frame goes through two decoding stages: + /// 1. Demuxing stage: An individual PNG codec is created for a frame + /// while walking through the APNG chunk stream -- this is placed + /// in the `codec` field. + /// 2. Decoding stage: When a frame is requested for the first time, + /// the decoded image is requested from the `SkCodec` and then + /// (depending on the `frame_info`) composited with a previous + /// frame. The final "canvas" frame is placed in the + /// `composited_image` field. At this point, the `codec` is freed + /// and the `composited_image` is handed to the caller for drawing. + struct APNGImage { + std::unique_ptr codec; + + // The rendered frame pixels. + std::vector pixels; + + // Absence of frame info is possible on the "default" image. + std::optional frame_info; + + // X offset of this image when composited. Only applicable to frames. + unsigned int x_offset; + + // X offset of this image when composited. Only applicable to frames. + unsigned int y_offset; + }; + + APNGImageGenerator(sk_sp& data, + SkImageInfo& image_info, + APNGImage&& default_image, + unsigned int frame_count, + unsigned int play_count, + const void* next_chunk_p, + const std::vector& header); + + static bool IsValidChunkHeader(const void* buffer, + size_t size, + const ChunkHeader* chunk); + + static const ChunkHeader* GetNextChunk(const void* buffer, + size_t size, + const ChunkHeader* current_chunk); + + /// @brief This is a utility template for casting a png buffer pointer to a + /// chunk header. Its primary purpose is to statically insert runtime + /// debug checks that detect invalid decoding behavior. + template + static constexpr const T* CastChunkData(const ChunkHeader* chunk) { + if constexpr (std::is_same_v) { + FML_DCHECK(chunk->get_type() == kImageHeaderChunkType); + } else if constexpr (std::is_same_v) { + FML_DCHECK(chunk->get_type() == kAnimationControlChunkType); + } else if constexpr (std::is_same_v) { + FML_DCHECK(chunk->get_type() == kFrameControlChunkType); + } else { + static_assert(!sizeof(T), "Invalid chunk struct"); + } + + return reinterpret_cast(reinterpret_cast(chunk) + + sizeof(ChunkHeader)); + } + + static constexpr size_t GetChunkSize(const ChunkHeader* chunk) { + return sizeof(ChunkHeader) + chunk->get_data_length() + kChunkCrcSize; + } + + static constexpr bool IsChunkCopySafe(const ChunkHeader* chunk) { + // The safe-to-copy bit is the 5th bit of the chunk name's 4th byte. This is + // the same as checking that the 4th byte is lowercase. + return (chunk->get_type() & 0x20) != 0; + } + + /// @brief Extract a header that's safe to use for both the "default" image + /// and individual PNG frames. Strip the animation control chunk. + static std::pair>, const void*> + ExtractHeader(const void* buffer_p, size_t buffer_size); + + /// @brief Takes a chunk pointer to a chunk and demuxes/interprets the next + /// image in the APNG sequence. It also provides the next `chunk_p` + /// to use. + /// @see `APNGImage` + static std::pair, const void*> DemuxNextImage( + const void* buffer_p, + size_t buffer_size, + const std::vector& header, + const void* chunk_p); + + bool DemuxNextImageInternal(); + + bool DemuxToImageIndex(unsigned int image_index); + + bool RenderDefaultImage(const SkImageInfo& info, + void* pixels, + size_t row_bytes); + + FML_DISALLOW_COPY_ASSIGN_AND_MOVE(APNGImageGenerator); + sk_sp data_; + SkImageInfo image_info_; + unsigned int frame_count_; + unsigned int play_count_; + + // The first image is always the default image, which may or may not be a + // frame. All subsequent images are guaranteed to have frame data. + std::vector images_; + + unsigned int first_frame_index_; + + const void* next_chunk_p_; + std::vector header_; +}; + +} // namespace flutter diff --git a/lib/ui/painting/image_generator_registry.cc b/lib/ui/painting/image_generator_registry.cc index 4460182f0a673..ca5d83a36d359 100644 --- a/lib/ui/painting/image_generator_registry.cc +++ b/lib/ui/painting/image_generator_registry.cc @@ -14,9 +14,17 @@ #include "third_party/skia/include/ports/SkImageGeneratorWIC.h" #endif +#include "image_generator_apng.h" + namespace flutter { ImageGeneratorRegistry::ImageGeneratorRegistry() : weak_factory_(this) { + AddFactory( + [](sk_sp buffer) { + return APNGImageGenerator::MakeFromData(std::move(buffer)); + }, + 0); + AddFactory( [](sk_sp buffer) { return BuiltinSkiaCodecImageGenerator::MakeFromData(std::move(buffer)); diff --git a/lib/ui/painting/image_generator_registry_unittests.cc b/lib/ui/painting/image_generator_registry_unittests.cc index 3051e42410644..54eb45309e3a0 100644 --- a/lib/ui/painting/image_generator_registry_unittests.cc +++ b/lib/ui/painting/image_generator_registry_unittests.cc @@ -62,7 +62,7 @@ class FakeImageGenerator : public ImageGenerator { unsigned int GetPlayCount() const { return 1; } - const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) const { + const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) { return {std::nullopt, 0, SkCodecAnimation::DisposalMethod::kKeep}; } diff --git a/lib/ui/painting/multi_frame_codec.cc b/lib/ui/painting/multi_frame_codec.cc index 76af13ba161fa..a380656ac1c04 100644 --- a/lib/ui/painting/multi_frame_codec.cc +++ b/lib/ui/painting/multi_frame_codec.cc @@ -107,24 +107,30 @@ sk_sp MultiFrameCodec::State::GetNextFrameImage( std::optional prior_frame_index = std::nullopt; if (requiredFrameIndex != SkCodec::kNoFrame) { + // We currently assume that frames can only ever depend on the immediately + // previous frame, if any. This means that + // `DisposalMethod::kRestorePrevious` is not supported. if (lastRequiredFrame_ == nullptr) { - FML_LOG(ERROR) << "Frame " << nextFrameIndex_ << " depends on frame " - << requiredFrameIndex - << " and no required frames are cached."; - return nullptr; + FML_DLOG(INFO) + << "Frame " << nextFrameIndex_ << " depends on frame " + << requiredFrameIndex + << " and no required frames are cached. Using blank slate instead."; } else if (lastRequiredFrameIndex_ != requiredFrameIndex) { FML_DLOG(INFO) << "Required frame " << requiredFrameIndex - << " is not cached. Using " << lastRequiredFrameIndex_ - << " instead"; - } - - if (lastRequiredFrame_->getPixels() && - CopyToBitmap(&bitmap, lastRequiredFrame_->colorType(), - *lastRequiredFrame_)) { - prior_frame_index = requiredFrameIndex; + << " is not cached. Using blank slate instead."; + } else { + // Copy the previous frame's output buffer into the current frame as the + // starting point. + if (lastRequiredFrame_->getPixels() && + CopyToBitmap(&bitmap, lastRequiredFrame_->colorType(), + *lastRequiredFrame_)) { + prior_frame_index = requiredFrameIndex; + } } } + // Write the new frame to the output buffer. The bitmap pixels as supplied + // are already set in accordance with the previous frame's disposal policy. if (!generator_->GetPixels(info, bitmap.getPixels(), bitmap.rowBytes(), nextFrameIndex_, requiredFrameIndex)) { FML_LOG(ERROR) << "Could not getPixels for frame " << nextFrameIndex_; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index d850eab688012..9f775f87d1047 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -2238,7 +2238,7 @@ class SinglePixelImageGenerator : public ImageGenerator { unsigned int GetPlayCount() const { return 1; } - const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) const { + const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) { return {std::nullopt, 0, SkCodecAnimation::DisposalMethod::kKeep}; } diff --git a/shell/platform/android/android_image_generator.cc b/shell/platform/android/android_image_generator.cc index 714eef933b145..9ba4c050c9a9a 100644 --- a/shell/platform/android/android_image_generator.cc +++ b/shell/platform/android/android_image_generator.cc @@ -38,7 +38,7 @@ unsigned int AndroidImageGenerator::GetPlayCount() const { } const ImageGenerator::FrameInfo AndroidImageGenerator::GetFrameInfo( - unsigned int frame_index) const { + unsigned int frame_index) { return {.required_frame = std::nullopt, .duration = 0, .disposal_method = SkCodecAnimation::DisposalMethod::kKeep}; diff --git a/shell/platform/android/android_image_generator.h b/shell/platform/android/android_image_generator.h index 6d13c94116492..53cbadde10afc 100644 --- a/shell/platform/android/android_image_generator.h +++ b/shell/platform/android/android_image_generator.h @@ -32,7 +32,7 @@ class AndroidImageGenerator : public ImageGenerator { // |ImageGenerator| const ImageGenerator::FrameInfo GetFrameInfo( - unsigned int frame_index) const override; + unsigned int frame_index) override; // |ImageGenerator| SkISize GetScaledDimensions(float desired_scale) override;