diff --git a/engine/src/flutter/lib/ui/fixtures/dispose_op_background.apng b/engine/src/flutter/lib/ui/fixtures/dispose_op_background.apng new file mode 100644 index 00000000000..94ce56c783f Binary files /dev/null and b/engine/src/flutter/lib/ui/fixtures/dispose_op_background.apng differ diff --git a/engine/src/flutter/lib/ui/painting/image_generator.h b/engine/src/flutter/lib/ui/painting/image_generator.h index dcfe4b1f069..e461eae5b30 100644 --- a/engine/src/flutter/lib/ui/painting/image_generator.h +++ b/engine/src/flutter/lib/ui/painting/image_generator.h @@ -44,6 +44,9 @@ class ImageGenerator { /// How this frame should be modified before decoding the next one. SkCodecAnimation::DisposalMethod disposal_method; + /// The region of the frame that is affected by the disposal method. + std::optional disposal_rect; + /// How this frame should be blended with the previous frame. SkCodecAnimation::Blend blend_mode; }; diff --git a/engine/src/flutter/lib/ui/painting/image_generator_apng.cc b/engine/src/flutter/lib/ui/painting/image_generator_apng.cc index 5179461d1bb..159d87638ae 100644 --- a/engine/src/flutter/lib/ui/painting/image_generator_apng.cc +++ b/engine/src/flutter/lib/ui/painting/image_generator_apng.cc @@ -404,6 +404,10 @@ APNGImageGenerator::DemuxNextImage(const void* buffer_p, 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; @@ -411,6 +415,7 @@ APNGImageGenerator::DemuxNextImage(const void* buffer_p, 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 = @@ -547,8 +552,10 @@ bool APNGImageGenerator::DemuxNextImageInternal() { } if (images_.size() > first_frame_index_ && - last_frame_info->disposal_method == - SkCodecAnimation::DisposalMethod::kKeep) { + (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) && diff --git a/engine/src/flutter/lib/ui/painting/multi_frame_codec.cc b/engine/src/flutter/lib/ui/painting/multi_frame_codec.cc index ca0654d3299..4a4d5d27ac1 100644 --- a/engine/src/flutter/lib/ui/painting/multi_frame_codec.cc +++ b/engine/src/flutter/lib/ui/painting/multi_frame_codec.cc @@ -94,6 +94,9 @@ MultiFrameCodec::State::GetNextFrameImage( // Copy the previous frame's output buffer into the current frame as the // starting point. bitmap.writePixels(lastRequiredFrame_->pixmap()); + if (restoreBGColorRect_.has_value()) { + bitmap.erase(SK_ColorTRANSPARENT, restoreBGColorRect_.value()); + } } } @@ -133,6 +136,13 @@ MultiFrameCodec::State::GetNextFrameImage( lastRequiredFrameIndex_ = nextFrameIndex_; } + if (frameInfo.disposal_method == + SkCodecAnimation::DisposalMethod::kRestoreBGColor) { + restoreBGColorRect_ = frameInfo.disposal_rect; + } else { + restoreBGColorRect_.reset(); + } + #if IMPELLER_SUPPORTS_RENDERING if (is_impeller_enabled_) { // This is safe regardless of whether the GPU is available or not because diff --git a/engine/src/flutter/lib/ui/painting/multi_frame_codec.h b/engine/src/flutter/lib/ui/painting/multi_frame_codec.h index 107d23bca9f..ef288f27bf4 100644 --- a/engine/src/flutter/lib/ui/painting/multi_frame_codec.h +++ b/engine/src/flutter/lib/ui/painting/multi_frame_codec.h @@ -54,10 +54,13 @@ class MultiFrameCodec : public Codec { int nextFrameIndex_; // The last decoded frame that's required to decode any subsequent frames. std::optional lastRequiredFrame_; - // The index of the last decoded required frame. int lastRequiredFrameIndex_ = -1; + // The rectangle that should be cleared if the previous frame's disposal + // method was kRestoreBGColor. + std::optional restoreBGColorRect_; + std::pair, std::string> GetNextFrameImage( fml::WeakPtr resourceContext, const std::shared_ptr& gpu_disable_sync_switch, diff --git a/engine/src/flutter/testing/dart/codec_test.dart b/engine/src/flutter/testing/dart/codec_test.dart index 1bf58a26de1..fb07a63f61a 100644 --- a/engine/src/flutter/testing/dart/codec_test.dart +++ b/engine/src/flutter/testing/dart/codec_test.dart @@ -204,8 +204,6 @@ void main() { }); test('Animated apng alpha type handling', () async { - // https://github.com/flutter/engine/pull/42153 - final Uint8List data = File( path.join('flutter', 'lib', 'ui', 'fixtures', 'alpha_animated.apng'), ).readAsBytesSync(); @@ -220,6 +218,29 @@ void main() { imageData = (await image.toByteData())!; expect(imageData.getUint32(0), 0x99000099); }); + + test('Animated apng background color restore', () async { + final Uint8List data = File( + path.join('flutter', 'lib', 'ui', 'fixtures', 'dispose_op_background.apng'), + ).readAsBytesSync(); + final ui.Codec codec = await ui.instantiateImageCodec(data); + + // First frame is solid red + ui.Image image = (await codec.getNextFrame()).image; + ByteData imageData = (await image.toByteData())!; + expect(imageData.getUint32(0), 0xFF0000FF); + + // Third frame is blue in the lower right corner. + await codec.getNextFrame(); + image = (await codec.getNextFrame()).image; + imageData = (await image.toByteData())!; + expect(imageData.getUint32(imageData.lengthInBytes - 4), 0x0000FFFF); + + // Fourth frame is transparent in the lower right corner + image = (await codec.getNextFrame()).image; + imageData = (await image.toByteData())!; + expect(imageData.getUint32(imageData.lengthInBytes - 4), 0x00000000); + }); } /// Returns a File handle to a file in the skia/resources directory.