From d2a5b9dcb14e5013952f96e6158b5040c13c670c Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 17 Dec 2024 10:35:17 -0500 Subject: [PATCH] [web] Reland: (Add `crossOrigin` property to `` tag used for decoding)++ (flutter/engine#57228) Relands https://github.com/flutter/engine/pull/54961 with a few more changes and tests. Fixes https://github.com/flutter/flutter/issues/160127 --- .../lib/src/engine/canvaskit/image.dart | 8 ++-- .../lib/web_ui/lib/src/engine/dom.dart | 16 ++++++++ .../src/engine/html_image_element_codec.dart | 11 +++-- .../test/canvaskit/image_golden_test.dart | 13 ++++++ .../image/html_image_element_codec_test.dart | 38 ++++++++++++++---- .../test/{html => ui}/image/sample_image1.png | Bin 6 files changed, 71 insertions(+), 15 deletions(-) rename engine/src/flutter/lib/web_ui/test/{html => ui}/image/html_image_element_codec_test.dart (79%) rename engine/src/flutter/lib/web_ui/test/{html => ui}/image/sample_image1.png (100%) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart index 5b4edb6a29..306ff752c2 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -161,7 +161,7 @@ ui.Image createCkImageFromImageElement( } class CkImageElementCodec extends HtmlImageElementCodec { - CkImageElementCodec(super.src); + CkImageElementCodec(super.src, {super.chunkCallback}); @override ui.Image createImageFromHTMLImageElement( @@ -170,7 +170,7 @@ class CkImageElementCodec extends HtmlImageElementCodec { } class CkImageBlobCodec extends HtmlBlobCodec { - CkImageBlobCodec(super.blob); + CkImageBlobCodec(super.blob, {super.chunkCallback}); @override ui.Image createImageFromHTMLImageElement( @@ -326,7 +326,7 @@ const String _kNetworkImageMessage = 'Failed to load network image.'; /// requesting from URI. Future skiaInstantiateWebImageCodec( String url, ui_web.ImageCodecChunkCallback? chunkCallback) async { - final CkImageElementCodec imageElementCodec = CkImageElementCodec(url); + final CkImageElementCodec imageElementCodec = CkImageElementCodec(url, chunkCallback: chunkCallback); try { await imageElementCodec.decode(); return imageElementCodec; @@ -339,7 +339,7 @@ Future skiaInstantiateWebImageCodec( data: list, contentType: imageType.mimeType, debugSource: url); } else { final DomBlob blob = createDomBlob([list.buffer]); - final CkImageBlobCodec codec = CkImageBlobCodec(blob); + final CkImageBlobCodec codec = CkImageBlobCodec(blob, chunkCallback: chunkCallback); try { await codec.decode(); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart index caf4888bbc..336ddadbab 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart @@ -990,6 +990,22 @@ extension DomHTMLImageElementExtension on DomHTMLImageElement { external set _height(JSNumber? value); set height(double? value) => _height = value?.toJS; + @JS('crossOrigin') + external JSString? get _crossOrigin; + String? get crossOrigin => _crossOrigin?.toDart; + + @JS('crossOrigin') + external set _crossOrigin(JSString? value); + set crossOrigin(String? value) => _crossOrigin = value?.toJS; + + @JS('decoding') + external JSString? get _decoding; + String? get decoding => _decoding?.toDart; + + @JS('decoding') + external set _decoding(JSString? value); + set decoding(String? value) => _decoding = value?.toJS; + @JS('decode') external JSPromise _decode(); Future decode() => js_util.promiseToFuture(_decode()); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_element_codec.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_element_codec.dart index 2bd6ccabc5..9784345e99 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_element_codec.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_element_codec.dart @@ -43,8 +43,13 @@ abstract class HtmlImageElementCodec implements ui.Codec { // builders to create UI. chunkCallback?.call(0, 100); imgElement = createDomHTMLImageElement(); - imgElement!.src = src; - setJsProperty(imgElement!, 'decoding', 'async'); + if (renderer is! HtmlRenderer) { + imgElement!.crossOrigin = 'anonymous'; + } + imgElement! + ..decoding = 'async' + ..src = src; + // Ignoring the returned future on purpose because we're communicating // through the `completer`. @@ -91,7 +96,7 @@ abstract class HtmlImageElementCodec implements ui.Codec { } abstract class HtmlBlobCodec extends HtmlImageElementCodec { - HtmlBlobCodec(this.blob) + HtmlBlobCodec(this.blob, {super.chunkCallback}) : super( domWindow.URL.createObjectURL(blob), debugSource: 'encoded image bytes', diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart index b5c5c6085b..e0681ce6e1 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart @@ -253,6 +253,19 @@ Future testMain() async { } }); + test('crossOrigin requests cause an error', () async { + final String otherOrigin = + domWindow.location.origin.replaceAll('localhost', '127.0.0.1'); + bool gotError = false; + try { + final ui.Codec _ = await renderer.instantiateImageCodecFromUrl( + Uri.parse('$otherOrigin/test_images/1x1.png')); + } catch (e) { + gotError = true; + } + expect(gotError, isTrue, reason: 'Should have got CORS error'); + }); + _testCkAnimatedImage(); test('isAvif', () { diff --git a/engine/src/flutter/lib/web_ui/test/html/image/html_image_element_codec_test.dart b/engine/src/flutter/lib/web_ui/test/ui/image/html_image_element_codec_test.dart similarity index 79% rename from engine/src/flutter/lib/web_ui/test/html/image/html_image_element_codec_test.dart rename to engine/src/flutter/lib/web_ui/test/ui/image/html_image_element_codec_test.dart index bbbf9ed2a5..772aeeb31c 100644 --- a/engine/src/flutter/lib/web_ui/test/html/image/html_image_element_codec_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/image/html_image_element_codec_test.dart @@ -7,12 +7,15 @@ import 'dart:typed_data'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; +import 'package:ui/src/engine/canvaskit/image.dart'; +import 'package:ui/src/engine/dom.dart'; import 'package:ui/src/engine/html/image.dart'; import 'package:ui/src/engine/html_image_element_codec.dart'; import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; import '../../common/test_initialization.dart'; +import '../../ui/utils.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -60,16 +63,20 @@ Future testMain() async { expect(image.height, height); }); test('loads sample image', () async { - final HtmlImageElementCodec codec = - HtmlRendererImageCodec('sample_image1.png'); + final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png'); final ui.FrameInfo frameInfo = await codec.getNextFrame(); + + expect(codec.imgElement, isNotNull); + expect(codec.imgElement!.src, contains('sample_image1.png')); + expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous'); + expect(codec.imgElement!.decoding, 'async'); + expect(frameInfo.image, isNotNull); expect(frameInfo.image.width, 100); expect(frameInfo.image.toString(), '[100×100]'); }); test('dispose image image', () async { - final HtmlImageElementCodec codec = - HtmlRendererImageCodec('sample_image1.png'); + final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png'); final ui.FrameInfo frameInfo = await codec.getNextFrame(); expect(frameInfo.image, isNotNull); expect(frameInfo.image.debugDisposed, isFalse); @@ -78,7 +85,7 @@ Future testMain() async { }); test('provides image loading progress', () async { final StringBuffer buffer = StringBuffer(); - final HtmlImageElementCodec codec = HtmlRendererImageCodec( + final HtmlImageElementCodec codec = createImageElementCodec( 'sample_image1.png', chunkCallback: (int loaded, int total) { buffer.write('$loaded/$total,'); }); @@ -89,7 +96,7 @@ Future testMain() async { /// Regression test for Firefox /// https://github.com/flutter/flutter/issues/66412 test('Returns nonzero natural width/height', () async { - final HtmlImageElementCodec codec = HtmlRendererImageCodec( + final HtmlImageElementCodec codec = createImageElementCodec( 'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I' 'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG' 'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx' @@ -103,7 +110,7 @@ Future testMain() async { final ui.FrameInfo frameInfo = await codec.getNextFrame(); expect(frameInfo.image.width, isNot(0)); }); - }); + }, skip: isSkwasm); group('ImageCodecUrl', () { test('loads sample image from web', () async { @@ -111,6 +118,12 @@ Future testMain() async { final HtmlImageElementCodec codec = await ui_web.createImageCodecFromUrl(uri) as HtmlImageElementCodec; final ui.FrameInfo frameInfo = await codec.getNextFrame(); + + expect(codec.imgElement, isNotNull); + expect(codec.imgElement!.src, contains('sample_image1.png')); + expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous'); + expect(codec.imgElement!.decoding, 'async'); + expect(frameInfo.image, isNotNull); expect(frameInfo.image.width, 100); }); @@ -124,5 +137,14 @@ Future testMain() async { await codec.getNextFrame(); expect(buffer.toString(), '0/100,100/100,'); }); - }); + }, skip: isSkwasm); +} + +HtmlImageElementCodec createImageElementCodec( + String src, { + ui_web.ImageCodecChunkCallback? chunkCallback, +}) { + return isHtml + ? HtmlRendererImageCodec(src, chunkCallback: chunkCallback) + : CkImageElementCodec(src, chunkCallback: chunkCallback); } diff --git a/engine/src/flutter/lib/web_ui/test/html/image/sample_image1.png b/engine/src/flutter/lib/web_ui/test/ui/image/sample_image1.png similarity index 100% rename from engine/src/flutter/lib/web_ui/test/html/image/sample_image1.png rename to engine/src/flutter/lib/web_ui/test/ui/image/sample_image1.png