mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[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:
parent
93d36ef85a
commit
3b7caba768
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.');
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user