[canvaskit] Fix image decoding in CPU-only mode (#180706)

In CPU-only rendering mode, we were using
`MakeLazyImageFromTextureSource` which only works if a WebGL context is
available (which it isn't in CPU-only mode). This fixes the problem by
using `MakeImageFromCanvasImageSource` which uses a 2d canvas when we
have fallen back to software rendering.

Fixes https://github.com/flutter/flutter/issues/175423

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [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.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[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/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Harry Terkelsen 2026-01-13 17:07:10 -08:00 committed by GitHub
parent 93d36ef85a
commit 3b7caba768
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 171 additions and 46 deletions

View File

@ -170,11 +170,23 @@ extension type CanvasKit(JSObject _) implements JSObject {
bool srcIsPremultiplied,
);
SkImage? MakeLazyImageFromTextureSourceWithInfo(Object src, SkPartialImageInfo info) =>
_MakeLazyImageFromTextureSource2(src.toJSAnyShallow, info);
SkImage? MakeLazyImageFromTextureSourceWithInfo(Object src, SkPartialImageInfo info) {
assert(
!CanvasKitRenderer.instance.isSoftware,
'Cannot use `MakeLazyImageFromTextureSourceWithInfo` in CPU-only mode.',
);
return _MakeLazyImageFromTextureSource2(src.toJSAnyShallow, info);
}
SkImage? MakeLazyImageFromImageBitmap(DomImageBitmap imageBitmap, bool hasPremultipliedAlpha) =>
_MakeLazyImageFromTextureSource3(imageBitmap, 0, hasPremultipliedAlpha);
SkImage? MakeLazyImageFromImageBitmap(DomImageBitmap imageBitmap, bool hasPremultipliedAlpha) {
assert(
!CanvasKitRenderer.instance.isSoftware,
'Cannot use `MakeLazyImageFromImageBitmap` in CPU-only mode.',
);
return _MakeLazyImageFromTextureSource3(imageBitmap, 0, hasPremultipliedAlpha);
}
external SkImage? MakeImageFromCanvasImageSource(JSAny src);
}
extension type CanvasKitModule(JSObject _) implements JSObject {

View File

@ -115,7 +115,12 @@ class CkResizingCodec extends ResizingCodec {
scaledHeight,
);
final DomImageBitmap bitmap = offscreenCanvas.transferToImageBitmap();
final SkImage? skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true);
SkImage? skImage;
if (CanvasKitRenderer.instance.isSoftware) {
skImage = canvasKit.MakeImageFromCanvasImageSource(bitmap);
} else {
skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true);
}
// Resize the canvas to 0x0 to cause the browser to eagerly reclaim its
// memory.
@ -137,16 +142,21 @@ ui.Image createCkImageFromImageElement(
int naturalWidth,
int naturalHeight,
) {
final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo(
image,
SkPartialImageInfo(
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
width: naturalWidth.toDouble(),
height: naturalHeight.toDouble(),
),
);
SkImage? skImage;
if (CanvasKitRenderer.instance.isSoftware) {
skImage = canvasKit.MakeImageFromCanvasImageSource(image);
} else {
skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo(
image,
SkPartialImageInfo(
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
width: naturalWidth.toDouble(),
height: naturalHeight.toDouble(),
),
);
}
if (skImage == null) {
throw ImageCodecException('Failed to create image from Image.decode');
}
@ -334,16 +344,7 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
debugSource: url,
);
} else {
final DomBlob blob = createDomBlob(<ByteBuffer>[list.buffer]);
final codec = CkImageBlobCodec(blob, chunkCallback: chunkCallback);
try {
await codec.decode();
return codec;
} on ImageCodecException {
codec.dispose();
return CkAnimatedImage.decodeFromBytes(list, url);
}
return CkAnimatedImage.decodeFromBytes(list, url);
}
}
}

View File

@ -42,16 +42,26 @@ class CkBrowserImageDecoder extends BrowserImageDecoder {
@override
ui.Image generateImageFromVideoFrame(VideoFrame frame) {
final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo(
frame,
SkPartialImageInfo(
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
width: frame.displayWidth,
height: frame.displayHeight,
),
);
SkImage? skImage;
if (CanvasKitRenderer.instance.isSoftware) {
final int width = frame.displayWidth.toInt();
final int height = frame.displayHeight.toInt();
final DomHTMLCanvasElement canvas = createDomCanvasElement(width: width, height: height);
final DomCanvasRenderingContext2D ctx = canvas.context2D;
ctx.drawImage(frame, 0, 0);
skImage = canvasKit.MakeImageFromCanvasImageSource(canvas);
} else {
skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo(
frame,
SkPartialImageInfo(
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
width: frame.displayWidth,
height: frame.displayHeight,
),
);
}
if (skImage == null) {
throw ImageCodecException(
"Failed to create image from pixel data decoded using the browser's ImageDecoder.",

View File

@ -23,6 +23,9 @@ class CanvasKitRenderer extends Renderer {
@override
String get rendererTag => 'canvaskit';
/// Whether the renderer is using software rendering.
bool get isSoftware => _pictureToImageSurface.isSoftware;
late final FlutterFontCollection _fontCollection = isExperimentalWebParagraph
? WebFontCollection()
: SkiaFontCollection();
@ -211,7 +214,12 @@ class CanvasKitRenderer extends Renderer {
@override
ui.Image createImageFromImageBitmap(DomImageBitmap imageBitmap) {
final SkImage? skImage = canvasKit.MakeLazyImageFromImageBitmap(imageBitmap, true);
SkImage? skImage;
if (isSoftware) {
skImage = canvasKit.MakeImageFromCanvasImageSource(imageBitmap);
} else {
skImage = canvasKit.MakeLazyImageFromImageBitmap(imageBitmap, true);
}
if (skImage == null) {
throw Exception('Failed to convert image bitmap to an SkImage.');
}
@ -234,16 +242,31 @@ class CanvasKitRenderer extends Renderer {
));
return createImageFromImageBitmap(bitmap);
}
final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo(
object,
SkPartialImageInfo(
width: width.toDouble(),
height: height.toDouble(),
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
),
);
SkImage? skImage;
if (isSoftware) {
if (object.isA<VideoFrame>()) {
// If the object is a VideoFrame, we need to draw it to a canvas first to
// avoid a bug in CanvasKit where MakeImageFromCanvasImageSource doesn't
// work with VideoFrames.
final DomHTMLCanvasElement canvas = createDomCanvasElement(width: width, height: height);
final DomCanvasRenderingContext2D ctx = canvas.context2D;
ctx.drawImage(object as VideoFrame, 0, 0);
skImage = canvasKit.MakeImageFromCanvasImageSource(canvas);
} else {
skImage = canvasKit.MakeImageFromCanvasImageSource(object);
}
} else {
skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo(
object,
SkPartialImageInfo(
width: width.toDouble(),
height: height.toDouble(),
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
),
);
}
if (skImage == null) {
throw Exception('Failed to convert image bitmap to an SkImage.');

View File

@ -52,6 +52,9 @@ abstract class CkSurface extends Surface {
return true;
}
/// Whether this surface is using software rendering.
bool get isSoftware => !supportsWebGl;
String? _fallbackToSoftwareReason;
/// When true, the surface will fail to create a GL context and fall back to

View File

@ -0,0 +1,76 @@
// 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.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import 'package:web_engine_tester/golden_tester.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false);
test('Renders image from encoded bytes in CPU-only mode', () async {
debugOverrideJsConfiguration(JsFlutterConfiguration(canvasKitForceCpuOnly: true));
// A 1x1 yellow PNG image.
const kBase64Png =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
final Uint8List bytes = base64.decode(kBase64Png);
final ui.Codec codec = await ui.instantiateImageCodec(bytes);
final ui.FrameInfo frameInfo = await codec.getNextFrame();
final ui.Image image = frameInfo.image;
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder, const ui.Rect.fromLTWH(0, 0, 10, 10));
// Draw the image scaled up so we can see it
canvas.drawImageRect(
image,
const ui.Rect.fromLTWH(0, 0, 1, 1),
const ui.Rect.fromLTWH(0, 0, 10, 10),
ui.Paint(),
);
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('image_cpu_only.png', region: const ui.Rect.fromLTWH(0, 0, 10, 10));
debugOverrideJsConfiguration(null); // Reset configuration
});
test('Renders image from URL in CPU-only mode', () async {
debugOverrideJsConfiguration(JsFlutterConfiguration(canvasKitForceCpuOnly: true));
final Uri uri = Uri.base.resolve('/test/ui/image/sample_image1.png');
final ui.Codec codec = await ui_web.createImageCodecFromUrl(uri);
final ui.FrameInfo frameInfo = await codec.getNextFrame();
final ui.Image image = frameInfo.image;
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder, const ui.Rect.fromLTWH(0, 0, 100, 100));
// Draw the image
canvas.drawImage(image, ui.Offset.zero, ui.Paint());
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('image_cpu_only_url.png', region: const ui.Rect.fromLTWH(0, 0, 100, 100));
debugOverrideJsConfiguration(null); // Reset configuration
});
}