[canvaskit] Fall back to `drawImage` for browsers that don't support `createImageBitmap` (#48336)
Safari 14 doesn't have the `createImageBitmap` API available. This
change allows us to render into `RenderCanvas` without using
`createImageBitmap` in that case.
Fixes https://github.com/flutter/flutter/issues/138910
## 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] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
diff --git a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart
index 7793cfb..9c44492 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart
@@ -53,6 +53,9 @@
late final DomCanvasRenderingContextBitmapRenderer renderContext =
canvasElement.contextBitmapRenderer;
+ late final DomCanvasRenderingContext2D renderContext2d =
+ canvasElement.context2D;
+
double _currentDevicePixelRatio = -1;
/// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device
@@ -82,6 +85,25 @@
renderContext.transferFromImageBitmap(bitmap);
}
+ void renderWithNoBitmapSupport(
+ DomCanvasImageSource imageSource,
+ int sourceHeight,
+ ui.Size size,
+ ) {
+ _ensureSize(size);
+ renderContext2d.drawImage(
+ imageSource,
+ 0,
+ sourceHeight - size.height,
+ size.width,
+ size.height,
+ 0,
+ 0,
+ size.width,
+ size.height,
+ );
+ }
+
/// Ensures that this canvas can draw a frame of the given [size].
void _ensureSize(ui.Size size) {
// Check if the frame is the same size as before, and if so, we don't need
diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart
index a479005..f86998f 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart
@@ -114,23 +114,35 @@
pictures.forEach(skCanvas.drawPicture);
_surface!.flush();
- DomImageBitmap bitmap;
- if (Surface.offscreenCanvasSupported) {
- bitmap = (await createImageBitmap(_offscreenCanvas! as JSObject, (
- x: 0,
- y: _pixelHeight - frameSize.height.toInt(),
- width: frameSize.width.toInt(),
- height: frameSize.height.toInt(),
- )).toDart)! as DomImageBitmap;
+ if (browserSupportsCreateImageBitmap) {
+ DomImageBitmap bitmap;
+ if (Surface.offscreenCanvasSupported) {
+ bitmap = (await createImageBitmap(_offscreenCanvas! as JSObject, (
+ x: 0,
+ y: _pixelHeight - frameSize.height.toInt(),
+ width: frameSize.width.toInt(),
+ height: frameSize.height.toInt(),
+ )).toDart)! as DomImageBitmap;
+ } else {
+ bitmap = (await createImageBitmap(_canvasElement! as JSObject, (
+ x: 0,
+ y: _pixelHeight - frameSize.height.toInt(),
+ width: frameSize.width.toInt(),
+ height: frameSize.height.toInt()
+ )).toDart)! as DomImageBitmap;
+ }
+ canvas.render(bitmap);
} else {
- bitmap = (await createImageBitmap(_canvasElement! as JSObject, (
- x: 0,
- y: _pixelHeight - frameSize.height.toInt(),
- width: frameSize.width.toInt(),
- height: frameSize.height.toInt()
- )).toDart)! as DomImageBitmap;
+ // If the browser doesn't support `createImageBitmap` (e.g. Safari 14)
+ // then render using `drawImage` instead.
+ DomCanvasImageSource imageSource;
+ if (Surface.offscreenCanvasSupported) {
+ imageSource = _offscreenCanvas! as DomCanvasImageSource;
+ } else {
+ imageSource = _canvasElement! as DomCanvasImageSource;
+ }
+ canvas.renderWithNoBitmapSupport(imageSource, _pixelHeight, frameSize);
}
- canvas.render(bitmap);
}
/// Acquire a frame of the given [size] containing a drawable canvas.
diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart
index 25030af..284cd72 100644
--- a/lib/web_ui/lib/src/engine/dom.dart
+++ b/lib/web_ui/lib/src/engine/dom.dart
@@ -1226,10 +1226,53 @@
x0.toJS, y0.toJS, r0.toJS, x1.toJS, y1.toJS, r1.toJS);
@JS('drawImage')
- external JSVoid _drawImage(
- DomCanvasImageSource source, JSNumber destX, JSNumber destY);
- void drawImage(DomCanvasImageSource source, num destX, num destY) =>
- _drawImage(source, destX.toJS, destY.toJS);
+ external JSVoid _drawImage1(
+ DomCanvasImageSource source, JSNumber dx, JSNumber dy);
+ @JS('drawImage')
+ external JSVoid _drawImage2(
+ DomCanvasImageSource source,
+ JSNumber sx,
+ JSNumber sy,
+ JSNumber sWidth,
+ JSNumber sHeight,
+ JSNumber dx,
+ JSNumber dy,
+ JSNumber dWidth,
+ JSNumber dHeight,
+ );
+ void drawImage(
+ DomCanvasImageSource source,
+ num srcxOrDstX,
+ num srcyOrDstY, [
+ num? srcWidth,
+ num? srcHeight,
+ num? dstX,
+ num? dstY,
+ num? dstWidth,
+ num? dstHeight,
+ ]) {
+ if (srcWidth == null) {
+ // In this case the numbers provided are the destination x and y offset.
+ return _drawImage1(source, srcxOrDstX.toJS, srcyOrDstY.toJS);
+ } else {
+ assert(srcHeight != null &&
+ dstX != null &&
+ dstY != null &&
+ dstWidth != null &&
+ dstHeight != null);
+ return _drawImage2(
+ source,
+ srcxOrDstX.toJS,
+ srcyOrDstY.toJS,
+ srcWidth.toJS,
+ srcHeight!.toJS,
+ dstX!.toJS,
+ dstY!.toJS,
+ dstWidth!.toJS,
+ dstHeight!.toJS,
+ );
+ }
+ }
@JS('fill')
external JSVoid _fill1();
@@ -3623,6 +3666,15 @@
bool browserSupportsOffscreenCanvas = _offscreenCanvasConstructor != null;
+@JS('window.createImageBitmap')
+external JSAny? get _createImageBitmapFunction;
+
+/// Set to `true` to disable `createImageBitmap` support. Used in tests.
+bool debugDisableCreateImageBitmapSupport = false;
+
+bool browserSupportsCreateImageBitmap =
+ !debugDisableCreateImageBitmapSupport || _createImageBitmapFunction != null;
+
@JS()
@staticInterop
extension JSArrayExtension on JSArray {
diff --git a/lib/web_ui/test/canvaskit/no_create_image_bitmap_test.dart b/lib/web_ui/test/canvaskit/no_create_image_bitmap_test.dart
new file mode 100644
index 0000000..4ee6b09
--- /dev/null
+++ b/lib/web_ui/test/canvaskit/no_create_image_bitmap_test.dart
@@ -0,0 +1,65 @@
+// 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 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+import 'package:ui/src/engine.dart';
+import 'package:ui/ui.dart' as ui;
+
+import 'common.dart';
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+const ui.Rect region = ui.Rect.fromLTRB(0, 0, 500, 250);
+
+/// Test that we can render even if `createImageBitmap` is not supported.
+void testMain() {
+ group('CanvasKit', () {
+ setUpCanvasKitTest();
+ setUp(() async {
+ EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(1.0);
+ debugDisableCreateImageBitmapSupport = true;
+ });
+
+ tearDown(() {
+ debugDisableCreateImageBitmapSupport = false;
+ });
+
+ test('can render without createImageBitmap', () async {
+ final CkPictureRecorder recorder = CkPictureRecorder();
+ final CkCanvas canvas = recorder.beginRecording(region);
+
+ final CkGradientLinear gradient = CkGradientLinear(
+ ui.Offset(region.left + region.width / 4, region.height / 2),
+ ui.Offset(region.right - region.width / 8, region.height / 2),
+ const <ui.Color>[
+ ui.Color(0xFF4285F4),
+ ui.Color(0xFF34A853),
+ ui.Color(0xFFFBBC05),
+ ui.Color(0xFFEA4335),
+ ui.Color(0xFF4285F4),
+ ],
+ const <double>[
+ 0.0,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1.0,
+ ],
+ ui.TileMode.clamp,
+ null);
+
+ final CkPaint paint = CkPaint()..shader = gradient;
+
+ canvas.drawRect(region, paint);
+
+ await matchPictureGolden(
+ 'canvaskit_linear_gradient_no_create_image_bitmap.png',
+ recorder.endRecording(),
+ region: region,
+ );
+ });
+ });
+}