[web] Reland: (Add `crossOrigin` property to `<img>` tag used for decoding)++ (#57228)
Relands https://github.com/flutter/engine/pull/54961 with a few more changes and tests.
Fixes https://github.com/flutter/flutter/issues/160127
diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart
index 5b4edb6..306ff75 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/image.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart
@@ -161,7 +161,7 @@
}
class CkImageElementCodec extends HtmlImageElementCodec {
- CkImageElementCodec(super.src);
+ CkImageElementCodec(super.src, {super.chunkCallback});
@override
ui.Image createImageFromHTMLImageElement(
@@ -170,7 +170,7 @@
}
class CkImageBlobCodec extends HtmlBlobCodec {
- CkImageBlobCodec(super.blob);
+ CkImageBlobCodec(super.blob, {super.chunkCallback});
@override
ui.Image createImageFromHTMLImageElement(
@@ -326,7 +326,7 @@
/// requesting from URI.
Future<ui.Codec> 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 @@
data: list, contentType: imageType.mimeType, debugSource: url);
} else {
final DomBlob blob = createDomBlob(<ByteBuffer>[list.buffer]);
- final CkImageBlobCodec codec = CkImageBlobCodec(blob);
+ final CkImageBlobCodec codec = CkImageBlobCodec(blob, chunkCallback: chunkCallback);
try {
await codec.decode();
diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart
index caf4888..336ddad 100644
--- a/lib/web_ui/lib/src/engine/dom.dart
+++ b/lib/web_ui/lib/src/engine/dom.dart
@@ -990,6 +990,22 @@
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<JSAny?> _decode();
Future<Object?> decode() => js_util.promiseToFuture<Object?>(_decode());
diff --git a/lib/web_ui/lib/src/engine/html_image_element_codec.dart b/lib/web_ui/lib/src/engine/html_image_element_codec.dart
index 2bd6cca..9784345 100644
--- a/lib/web_ui/lib/src/engine/html_image_element_codec.dart
+++ b/lib/web_ui/lib/src/engine/html_image_element_codec.dart
@@ -43,8 +43,13 @@
// builders to create UI.
chunkCallback?.call(0, 100);
imgElement = createDomHTMLImageElement();
- imgElement!.src = src;
- setJsProperty<String>(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 HtmlBlobCodec extends HtmlImageElementCodec {
- HtmlBlobCodec(this.blob)
+ HtmlBlobCodec(this.blob, {super.chunkCallback})
: super(
domWindow.URL.createObjectURL(blob),
debugSource: 'encoded image bytes',
diff --git a/lib/web_ui/test/canvaskit/image_golden_test.dart b/lib/web_ui/test/canvaskit/image_golden_test.dart
index b5c5c60..e0681ce 100644
--- a/lib/web_ui/test/canvaskit/image_golden_test.dart
+++ b/lib/web_ui/test/canvaskit/image_golden_test.dart
@@ -253,6 +253,19 @@
}
});
+ 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/lib/web_ui/test/html/image/html_image_element_codec_test.dart b/lib/web_ui/test/ui/image/html_image_element_codec_test.dart
similarity index 79%
rename from lib/web_ui/test/html/image/html_image_element_codec_test.dart
rename to lib/web_ui/test/ui/image/html_image_element_codec_test.dart
index bbbf9ed..772aeeb 100644
--- a/lib/web_ui/test/html/image/html_image_element_codec_test.dart
+++ b/lib/web_ui/test/ui/image/html_image_element_codec_test.dart
@@ -7,12 +7,15 @@
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 @@
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 @@
});
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 @@
/// 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 @@
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 @@
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 @@
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/lib/web_ui/test/html/image/sample_image1.png b/lib/web_ui/test/ui/image/sample_image1.png
similarity index 100%
rename from lib/web_ui/test/html/image/sample_image1.png
rename to lib/web_ui/test/ui/image/sample_image1.png
Binary files differ