[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,
+      );
+    });
+  });
+}