Skip to content

[engine] Decode animation frames with alpha after an opaque first frame - #193113

Merged
auto-submit[bot] merged 5 commits into
flutter:masterfrom
krll-kov:webp-opaque-first-frame-alpha
Sep 29, 2026
Merged

auto-submit[bot] merged 5 commits into
flutter:masterfrom
krll-kov:webp-opaque-first-frame-alpha

Conversation

@krll-kov

@krll-kov krll-kov commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #85831

While working on webp encoder for image package i noticed that flutter does not render some animated webp and gifs with following exception:

======== Exception caught by image resource service ================================================
The following _Exception was thrown resolving an image frame:
Exception: Could not getPixels for frame 1

When the exception was thrown, this was the stack: 
Path: \\Mac\Downloads\gif\200w.webp
====================================================================================================
Code sample and visual demo with the files used, since the issue has no screenshots and author did not provide his code sample: image
import 'dart:io';

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      home: Scaffold(
        appBar: AppBar(title: Text('Material App Bar')),
        body: Center(child: Column(children: <Widget>[
          Image.file(File(r'\\Mac\Downloads\gif\200w.gif'), height: 100,),
          Image.file(File(r'\\Mac\Downloads\gif\200w.webp'), height: 100,),

          Image.file(File(r'\\Mac\Downloads\gif\giphy.gif'), height: 100,),
          Image.file(File(r'\\Mac\Downloads\gif\giphy.webp'), height: 100,),

          Image.file(File(r'\\Mac\Downloads\gif\opaque_first_frame_bug.gif'), height: 100,),
        ])),
      ),
    );
  }
}
Webp and gif Images used for this sample: 200w 200w giphy giphy opaque_first_frame_bug

It appears that this is an old issue in how Flutter uses Skia, because for animated WebP and GIF, Skia's SkCodec::getInfo() reports the alpha type of the first frame only, and its documentation doesn't mention that (or at least i did not find it). Flutter allocates every frame's bitmap from that info without checking getFrameInfo(i).fAlphaType, so Skia rejects any later frame with alpha after an opaque first frame with kInvalidConversion.

Tested on an engine built from master (e619960, Windows x64 release) with an app that decodes every frame through ui.instantiateImageCodec:

file without the change with the change
WebP, 59 frames, frames 46 and 53 have alpha frames 46, 53 fail all decode
3 WebP files, 15 frames each, frames 1-14 have alpha (lossy and lossless) frames 1-14 fail all decode
GIF, opaque frame 0, frame 1 with transparency, disposal to background frame 1 fails all decode
the same WebP frames with one alpha pixel in frame 0, and a GIF with disposal none all decode all decode

Fix: when SkCodec::getInfo() reports kOpaque_SkAlphaType for an animation, BuiltinSkiaCodecImageGenerator now reports kPremul_SkAlphaType if any later frame reports a non-opaque SkCodec::FrameInfo::fAlphaType. Animations whose later frames all report kOpaque_SkAlphaType stay opaque. APNG is decoded by APNGImageGenerator, which this change doesn't touch.

The new tests in testing/dart/codec_test.dart use a 140-byte WebP and a 70-byte GIF fixture. I ran codec_test.dart with flutter_tester from a host_debug_unopt_x64 build (Windows x64) with the Skia and Impeller Vulkan backends, single and multithreaded: all 16 tests pass. Without the change the two new tests fail with Could not getPixels for frame 1. The Impeller OpenGL ES tests were not tested locally.

Screenshot after the fix, both with impeller enabled and disabled: image

Pre-launch Checklist

@github-actions github-actions Bot added the engine flutter/engine related. See also e: labels. label Sep 21, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses an issue where decoding animated images with an opaque first frame followed by frames with alpha fails due to an invalid conversion. The fix updates image_generator.cc to inspect all frames and set the alpha type to premultiplied if any subsequent frame contains alpha, and adds corresponding regression tests. The review feedback recommends explicitly disposing of the instantiated ui.Image objects in the new tests to prevent potential native memory leaks.

Comment on lines +184 to +190
ui.Image image = (await codec.getNextFrame()).image;
ByteData imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0xFF0000FF);
image = (await codec.getNextFrame()).image;
imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0x00008080);
codec.dispose();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The instantiated ui.Image objects are not disposed, which can lead to native memory leaks during test execution. It is a best practice to explicitly call dispose() on ui.Image instances once they are no longer needed.

Suggested change
ui.Image image = (await codec.getNextFrame()).image;
ByteData imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0xFF0000FF);
image = (await codec.getNextFrame()).image;
imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0x00008080);
codec.dispose();
ui.Image image = (await codec.getNextFrame()).image;
ByteData imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0xFF0000FF);
image.dispose();
image = (await codec.getNextFrame()).image;
imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0x00008080);
image.dispose();
codec.dispose();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other codec tests in this file only dispose the codec, so I kept the same pattern

Comment on lines +203 to +210
ui.Image image = (await codec.getNextFrame()).image;
ByteData imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0xFF0000FF);
image = (await codec.getNextFrame()).image;
imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0x0000FFFF);
expect(imageData.getUint32(4), 0x00000000);
codec.dispose();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The instantiated ui.Image objects are not disposed, which can lead to native memory leaks during test execution. It is a best practice to explicitly call dispose() on ui.Image instances once they are no longer needed.

Suggested change
ui.Image image = (await codec.getNextFrame()).image;
ByteData imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0xFF0000FF);
image = (await codec.getNextFrame()).image;
imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0x0000FFFF);
expect(imageData.getUint32(4), 0x00000000);
codec.dispose();
ui.Image image = (await codec.getNextFrame()).image;
ByteData imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0xFF0000FF);
image.dispose();
image = (await codec.getNextFrame()).image;
imageData = (await image.toByteData())!;
expect(imageData.getUint32(0), 0x0000FFFF);
expect(imageData.getUint32(4), 0x00000000);
image.dispose();
codec.dispose();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other codec tests in this file only dispose the codec, so I kept the same pattern

@gaaclarke gaaclarke added the CICD Run CI/CD label Sep 21, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 21, 2026
@krll-kov

Copy link
Copy Markdown
Contributor Author

@gaaclarke The previous run had one infra failure: Linux mac_android_aot_engine hit the 60m timeout on the Mac after all build steps had passed (it was cancelled while uploading logs, no failed tests): dashboard

I've merged the latest master just in case, so the CICD label was removed. Could you please add it back?

@b-luk b-luk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks, @krll-kov

@krll-kov

Copy link
Copy Markdown
Contributor Author

LGTM. Thanks, @krll-kov

Thanks for approval!

@b-luk
b-luk requested a review from gaaclarke September 28, 2026 20:49

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm! thanks

@gaaclarke gaaclarke added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 28, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 28, 2026
@auto-submit

auto-submit Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/193113, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR.

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 28, 2026
@krll-kov

Copy link
Copy Markdown
Contributor Author

@gaaclarke bot asked to merge main, can you add label again?

@b-luk b-luk added the CICD Run CI/CD label Sep 28, 2026
@gaaclarke gaaclarke added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 28, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into flutter:master with commit 9c3ea4e Sep 29, 2026
30 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 29, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Sep 29, 2026
flutter/flutter@c5a061b...55b8f88

2026-09-29 [email protected] [Android 17] Added Android 17 device to Firebase tests (flutter/flutter#193251)
2026-09-29 [email protected] [engine] Fix remaining `unawaited_futures` lints (flutter/flutter#193508)
2026-09-29 [email protected] shell: Rename first-frame tests AddFirstFrameCallback* (flutter/flutter#193418)
2026-09-28 [email protected] Enable `unawaited_futures` lint across the Flutter repository (flutter/flutter#193125)
2026-09-28 [email protected] Upgrade flutter/flutter to Roblectric 4.17 (flutter/flutter#193369)
2026-09-28 [email protected] [engine] Decode animation frames with alpha after an opaque first frame (flutter/flutter#193113)
2026-09-28 [email protected] Allow macOS tests to use macOS 15 or 26 (flutter/flutter#193317)
2026-09-28 [email protected] Cleanup DDC SDK build targets (flutter/flutter#193193)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some webp fails to load, throws multi_frame_codec.cc(112)] Could not getPixels for frame 2

3 participants