diff --git a/lib/ui/fixtures/four_frame_with_reuse.gif b/lib/ui/fixtures/four_frame_with_reuse.gif new file mode 100644 index 0000000000000..c2844f1ae0202 Binary files /dev/null and b/lib/ui/fixtures/four_frame_with_reuse.gif differ diff --git a/lib/ui/fixtures/four_frame_with_reuse_end.png b/lib/ui/fixtures/four_frame_with_reuse_end.png new file mode 100644 index 0000000000000..b0433f9c2b8d3 Binary files /dev/null and b/lib/ui/fixtures/four_frame_with_reuse_end.png differ diff --git a/lib/ui/fixtures/heart.webp b/lib/ui/fixtures/heart.webp new file mode 100644 index 0000000000000..e71cb118f0c8c Binary files /dev/null and b/lib/ui/fixtures/heart.webp differ diff --git a/lib/ui/fixtures/heart_end.png b/lib/ui/fixtures/heart_end.png new file mode 100644 index 0000000000000..5fd9a7061d0b3 Binary files /dev/null and b/lib/ui/fixtures/heart_end.png differ diff --git a/lib/ui/painting/multi_frame_codec.cc b/lib/ui/painting/multi_frame_codec.cc index a380656ac1c04..d2c38fcf15890 100644 --- a/lib/ui/painting/multi_frame_codec.cc +++ b/lib/ui/painting/multi_frame_codec.cc @@ -115,9 +115,6 @@ sk_sp MultiFrameCodec::State::GetNextFrameImage( << "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 blank slate instead."; } else { // Copy the previous frame's output buffer into the current frame as the // starting point. @@ -138,7 +135,8 @@ sk_sp MultiFrameCodec::State::GetNextFrameImage( } // Hold onto this if we need it to decode future frames. - if (frameInfo.disposal_method == SkCodecAnimation::DisposalMethod::kKeep) { + if (frameInfo.disposal_method == SkCodecAnimation::DisposalMethod::kKeep || + lastRequiredFrame_) { lastRequiredFrame_ = std::make_unique(bitmap); lastRequiredFrameIndex_ = nextFrameIndex_; } @@ -161,9 +159,9 @@ sk_sp MultiFrameCodec::State::GetNextFrameImage( gpu_disable_sync_switch->Execute( fml::SyncSwitch::Handlers() .SetIfTrue([&skImage, &bitmap] { - // Defer decoding until time of draw later on the raster thread. Can - // happen when GL operations are currently forbidden such as in the - // background on iOS. + // Defer decoding until time of draw later on the raster thread. + // Can happen when GL operations are currently forbidden such as + // in the background on iOS. skImage = SkImage::MakeFromBitmap(bitmap); }) .SetIfFalse([&skImage, &resourceContext, &bitmap] { @@ -257,8 +255,8 @@ Dart_Handle MultiFrameCodec::getNextFrame(Dart_Handle callback_handle) { })); return Dart_Null(); - // The static leak checker gets confused by the control flow, unique pointers - // and closures in this function. + // The static leak checker gets confused by the control flow, unique + // pointers and closures in this function. // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) } diff --git a/testing/dart/codec_test.dart b/testing/dart/codec_test.dart index 08b38b0ce0ccf..ba63a592a36ba 100644 --- a/testing/dart/codec_test.dart +++ b/testing/dart/codec_test.dart @@ -126,6 +126,57 @@ void main() { expect(e.toString(), contains('Decoded image has been disposed')); } }); + + test('Animated gif can reuse across multiple frames', () async { + // Regression test for b/271947267 and https://github.com/flutter/flutter/issues/122134 + + final Uint8List data = File( + path.join('flutter', 'lib', 'ui', 'fixtures', 'four_frame_with_reuse.gif'), + ).readAsBytesSync(); + final ui.Codec codec = await ui.instantiateImageCodec(data); + + // Capture the final frame of animation. If we have not composited + // correctly, it will be clipped strangely. + late ui.FrameInfo frameInfo; + for (int i = 0; i < 4; i++) { + frameInfo = await codec.getNextFrame(); + } + + final ui.Image image = frameInfo.image; + final ByteData imageData = (await image.toByteData(format: ui.ImageByteFormat.png))!; + + final Uint8List goldenData = File( + path.join('flutter', 'lib', 'ui', 'fixtures', 'four_frame_with_reuse_end.png'), + ).readAsBytesSync(); + + expect(imageData.buffer.asUint8List(), goldenData); + }); + + test('Animated webp can reuse across multiple frames', () async { + // Regression test for https://github.com/flutter/flutter/issues/61150#issuecomment-679055858 + + final Uint8List data = File( + path.join('flutter', 'lib', 'ui', 'fixtures', 'heart.webp'), + ).readAsBytesSync(); + final ui.Codec codec = await ui.instantiateImageCodec(data); + + // Capture the final frame of animation. If we have not composited + // correctly, the hearts will be incorrectly repeated in the image. + late ui.FrameInfo frameInfo; + for (int i = 0; i < 69; i++) { + frameInfo = await codec.getNextFrame(); + } + + final ui.Image image = frameInfo.image; + final ByteData imageData = (await image.toByteData(format: ui.ImageByteFormat.png))!; + + final Uint8List goldenData = File( + path.join('flutter', 'lib', 'ui', 'fixtures', 'heart_end.png'), + ).readAsBytesSync(); + + expect(imageData.buffer.asUint8List(), goldenData); + + }); } /// Returns a File handle to a file in the skia/resources directory.