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

Fixes https://github.com/flutter/flutter/issues/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
====================================================================================================
```
<details>
<summary>Code sample and visual demo with the files used, since the
issue has no screenshots and author did not provide his code
sample:</summary>

<img width="1491" height="819" alt="image"
src="https://github.com/user-attachments/assets/04e7dc17-bc7f-453f-ba28-5621d47c3f23"
/>

```dart
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,),
        ])),
      ),
    );
  }
}
```

<details>
<summary>Webp and gif Images used for this sample:</summary>

<img width="200" height="356" alt="200w"
src="https://github.com/user-attachments/assets/abcbdd6e-d4d0-4e23-b88d-04c0e3d4a79e"
/>
<img width="56" height="100" alt="200w"
src="https://github.com/user-attachments/assets/8df4d356-1297-4aad-af85-a2b4bd23b606"
/>

<img width="480" height="480" alt="giphy"
src="https://github.com/user-attachments/assets/13b6952e-1bf9-49ec-b58f-52c5caf6fa04"
/>
<img width="100" height="100" alt="giphy"
src="https://github.com/user-attachments/assets/4d3bb3dc-0f8c-4384-9b1d-86e3a4474206"
/>
<img width="200" height="200" alt="opaque_first_frame_bug"
src="https://github.com/user-attachments/assets/5e41e321-04f4-4ad5-a9cd-90c0164e4c6a"
/>
</details>

</details>

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 (e619960004d, 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.

<details>
<summary>Screenshot after the fix, both with impeller enabled and
disabled:</summary>
<img width="1329" height="760" alt="image"
src="https://github.com/user-attachments/assets/a01da7d8-3c40-49a1-a312-4941d23128a2"
/>
</details>


## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant in-code documentation (doc comments with
`///`).
- [ ] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/engine/src/flutter/lib/ui/fixtures/opaque_first_frame_then_alpha.gif b/engine/src/flutter/lib/ui/fixtures/opaque_first_frame_then_alpha.gif
new file mode 100644
index 0000000..52803e9
--- /dev/null
+++ b/engine/src/flutter/lib/ui/fixtures/opaque_first_frame_then_alpha.gif
Binary files differ
diff --git a/engine/src/flutter/lib/ui/fixtures/opaque_first_frame_then_alpha.webp b/engine/src/flutter/lib/ui/fixtures/opaque_first_frame_then_alpha.webp
new file mode 100644
index 0000000..3aa43d0
--- /dev/null
+++ b/engine/src/flutter/lib/ui/fixtures/opaque_first_frame_then_alpha.webp
Binary files differ
diff --git a/engine/src/flutter/lib/ui/painting/image_generator.cc b/engine/src/flutter/lib/ui/painting/image_generator.cc
index 1ba1c4f..8574a94 100644
--- a/engine/src/flutter/lib/ui/painting/image_generator.cc
+++ b/engine/src/flutter/lib/ui/painting/image_generator.cc
@@ -92,6 +92,20 @@
     // Prefer premul over unpremul (this produces better filtering in general)
     info = info.makeAlphaType(kPremul_SkAlphaType);
   }
+  if (kOpaque_SkAlphaType == info.alphaType()) {
+    // `SkCodec::getInfo` reports the alpha type of the first frame only, so a
+    // later frame with alpha fails with `kInvalidConversion` when decoded into
+    // an opaque bitmap. See https://github.com/flutter/flutter/issues/85831.
+    const int frame_count = codec->getFrameCount();
+    for (int i = 1; i < frame_count; i++) {
+      SkCodec::FrameInfo frame_info = {};
+      if (codec->getFrameInfo(i, &frame_info) &&
+          frame_info.fAlphaType != kOpaque_SkAlphaType) {
+        info = info.makeAlphaType(kPremul_SkAlphaType);
+        break;
+      }
+    }
+  }
   return info;
 }
 
diff --git a/engine/src/flutter/testing/dart/codec_test.dart b/engine/src/flutter/testing/dart/codec_test.dart
index c75145b..3ec59a7 100644
--- a/engine/src/flutter/testing/dart/codec_test.dart
+++ b/engine/src/flutter/testing/dart/codec_test.dart
@@ -172,6 +172,44 @@
     expect(imageData.buffer.asUint8List(), goldenData);
   });
 
+  test('Animated webp decodes a frame with alpha after an opaque first frame', () async {
+    // Regression test for https://github.com/flutter/flutter/issues/85831
+    final Uint8List data = File(
+      path.join('flutter', 'lib', 'ui', 'fixtures', 'opaque_first_frame_then_alpha.webp'),
+    ).readAsBytesSync();
+    final ui.Codec codec = await ui.instantiateImageCodec(data);
+
+    // The test image contains two solid frames. The first is opaque red, and
+    // the second is blue with alpha=128.
+    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();
+  });
+
+  test('Animated gif decodes a frame with alpha after an opaque first frame', () async {
+    // Regression test for https://github.com/flutter/flutter/issues/85831
+    final Uint8List data = File(
+      path.join('flutter', 'lib', 'ui', 'fixtures', 'opaque_first_frame_then_alpha.gif'),
+    ).readAsBytesSync();
+    final ui.Codec codec = await ui.instantiateImageCodec(data);
+
+    // The test image contains two frames that restore to the background. The
+    // first is opaque red, and the second is transparent except for one blue
+    // pixel in the top left corner.
+    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();
+  });
+
   test('Animated webp can reuse across multiple frames', () async {
     // Regression test for https://github.com/flutter/flutter/issues/61150#issuecomment-679055858