Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 6a75cb6

Browse files
authored
[web] Reland: (Add crossOrigin property to <img> tag used for decoding)++ (#57228)
Relands #54961 with a few more changes and tests. Fixes flutter/flutter#160127
1 parent 8535ce3 commit 6a75cb6

File tree

6 files changed

+71
-15
lines changed

6 files changed

+71
-15
lines changed

lib/web_ui/lib/src/engine/canvaskit/image.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ ui.Image createCkImageFromImageElement(
161161
}
162162

163163
class CkImageElementCodec extends HtmlImageElementCodec {
164-
CkImageElementCodec(super.src);
164+
CkImageElementCodec(super.src, {super.chunkCallback});
165165

166166
@override
167167
ui.Image createImageFromHTMLImageElement(
@@ -170,7 +170,7 @@ class CkImageElementCodec extends HtmlImageElementCodec {
170170
}
171171

172172
class CkImageBlobCodec extends HtmlBlobCodec {
173-
CkImageBlobCodec(super.blob);
173+
CkImageBlobCodec(super.blob, {super.chunkCallback});
174174

175175
@override
176176
ui.Image createImageFromHTMLImageElement(
@@ -326,7 +326,7 @@ const String _kNetworkImageMessage = 'Failed to load network image.';
326326
/// requesting from URI.
327327
Future<ui.Codec> skiaInstantiateWebImageCodec(
328328
String url, ui_web.ImageCodecChunkCallback? chunkCallback) async {
329-
final CkImageElementCodec imageElementCodec = CkImageElementCodec(url);
329+
final CkImageElementCodec imageElementCodec = CkImageElementCodec(url, chunkCallback: chunkCallback);
330330
try {
331331
await imageElementCodec.decode();
332332
return imageElementCodec;
@@ -339,7 +339,7 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
339339
data: list, contentType: imageType.mimeType, debugSource: url);
340340
} else {
341341
final DomBlob blob = createDomBlob(<ByteBuffer>[list.buffer]);
342-
final CkImageBlobCodec codec = CkImageBlobCodec(blob);
342+
final CkImageBlobCodec codec = CkImageBlobCodec(blob, chunkCallback: chunkCallback);
343343

344344
try {
345345
await codec.decode();

lib/web_ui/lib/src/engine/dom.dart

+16
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,22 @@ extension DomHTMLImageElementExtension on DomHTMLImageElement {
990990
external set _height(JSNumber? value);
991991
set height(double? value) => _height = value?.toJS;
992992

993+
@JS('crossOrigin')
994+
external JSString? get _crossOrigin;
995+
String? get crossOrigin => _crossOrigin?.toDart;
996+
997+
@JS('crossOrigin')
998+
external set _crossOrigin(JSString? value);
999+
set crossOrigin(String? value) => _crossOrigin = value?.toJS;
1000+
1001+
@JS('decoding')
1002+
external JSString? get _decoding;
1003+
String? get decoding => _decoding?.toDart;
1004+
1005+
@JS('decoding')
1006+
external set _decoding(JSString? value);
1007+
set decoding(String? value) => _decoding = value?.toJS;
1008+
9931009
@JS('decode')
9941010
external JSPromise<JSAny?> _decode();
9951011
Future<Object?> decode() => js_util.promiseToFuture<Object?>(_decode());

lib/web_ui/lib/src/engine/html_image_element_codec.dart

+8-3
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,13 @@ abstract class HtmlImageElementCodec implements ui.Codec {
4343
// builders to create UI.
4444
chunkCallback?.call(0, 100);
4545
imgElement = createDomHTMLImageElement();
46-
imgElement!.src = src;
47-
setJsProperty<String>(imgElement!, 'decoding', 'async');
46+
if (renderer is! HtmlRenderer) {
47+
imgElement!.crossOrigin = 'anonymous';
48+
}
49+
imgElement!
50+
..decoding = 'async'
51+
..src = src;
52+
4853

4954
// Ignoring the returned future on purpose because we're communicating
5055
// through the `completer`.
@@ -91,7 +96,7 @@ abstract class HtmlImageElementCodec implements ui.Codec {
9196
}
9297

9398
abstract class HtmlBlobCodec extends HtmlImageElementCodec {
94-
HtmlBlobCodec(this.blob)
99+
HtmlBlobCodec(this.blob, {super.chunkCallback})
95100
: super(
96101
domWindow.URL.createObjectURL(blob),
97102
debugSource: 'encoded image bytes',

lib/web_ui/test/canvaskit/image_golden_test.dart

+13
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,19 @@ Future<void> testMain() async {
253253
}
254254
});
255255

256+
test('crossOrigin requests cause an error', () async {
257+
final String otherOrigin =
258+
domWindow.location.origin.replaceAll('localhost', '127.0.0.1');
259+
bool gotError = false;
260+
try {
261+
final ui.Codec _ = await renderer.instantiateImageCodecFromUrl(
262+
Uri.parse('$otherOrigin/test_images/1x1.png'));
263+
} catch (e) {
264+
gotError = true;
265+
}
266+
expect(gotError, isTrue, reason: 'Should have got CORS error');
267+
});
268+
256269
_testCkAnimatedImage();
257270

258271
test('isAvif', () {

lib/web_ui/test/html/image/html_image_element_codec_test.dart renamed to lib/web_ui/test/ui/image/html_image_element_codec_test.dart

+30-8
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import 'dart:typed_data';
77

88
import 'package:test/bootstrap/browser.dart';
99
import 'package:test/test.dart';
10+
import 'package:ui/src/engine/canvaskit/image.dart';
11+
import 'package:ui/src/engine/dom.dart';
1012
import 'package:ui/src/engine/html/image.dart';
1113
import 'package:ui/src/engine/html_image_element_codec.dart';
1214
import 'package:ui/ui.dart' as ui;
1315
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
1416

1517
import '../../common/test_initialization.dart';
18+
import '../../ui/utils.dart';
1619

1720
void main() {
1821
internalBootstrapBrowserTest(() => testMain);
@@ -60,16 +63,20 @@ Future<void> testMain() async {
6063
expect(image.height, height);
6164
});
6265
test('loads sample image', () async {
63-
final HtmlImageElementCodec codec =
64-
HtmlRendererImageCodec('sample_image1.png');
66+
final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png');
6567
final ui.FrameInfo frameInfo = await codec.getNextFrame();
68+
69+
expect(codec.imgElement, isNotNull);
70+
expect(codec.imgElement!.src, contains('sample_image1.png'));
71+
expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous');
72+
expect(codec.imgElement!.decoding, 'async');
73+
6674
expect(frameInfo.image, isNotNull);
6775
expect(frameInfo.image.width, 100);
6876
expect(frameInfo.image.toString(), '[100×100]');
6977
});
7078
test('dispose image image', () async {
71-
final HtmlImageElementCodec codec =
72-
HtmlRendererImageCodec('sample_image1.png');
79+
final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png');
7380
final ui.FrameInfo frameInfo = await codec.getNextFrame();
7481
expect(frameInfo.image, isNotNull);
7582
expect(frameInfo.image.debugDisposed, isFalse);
@@ -78,7 +85,7 @@ Future<void> testMain() async {
7885
});
7986
test('provides image loading progress', () async {
8087
final StringBuffer buffer = StringBuffer();
81-
final HtmlImageElementCodec codec = HtmlRendererImageCodec(
88+
final HtmlImageElementCodec codec = createImageElementCodec(
8289
'sample_image1.png', chunkCallback: (int loaded, int total) {
8390
buffer.write('$loaded/$total,');
8491
});
@@ -89,7 +96,7 @@ Future<void> testMain() async {
8996
/// Regression test for Firefox
9097
/// https://github.com/flutter/flutter/issues/66412
9198
test('Returns nonzero natural width/height', () async {
92-
final HtmlImageElementCodec codec = HtmlRendererImageCodec(
99+
final HtmlImageElementCodec codec = createImageElementCodec(
93100
'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I'
94101
'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG'
95102
'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx'
@@ -103,14 +110,20 @@ Future<void> testMain() async {
103110
final ui.FrameInfo frameInfo = await codec.getNextFrame();
104111
expect(frameInfo.image.width, isNot(0));
105112
});
106-
});
113+
}, skip: isSkwasm);
107114

108115
group('ImageCodecUrl', () {
109116
test('loads sample image from web', () async {
110117
final Uri uri = Uri.base.resolve('sample_image1.png');
111118
final HtmlImageElementCodec codec =
112119
await ui_web.createImageCodecFromUrl(uri) as HtmlImageElementCodec;
113120
final ui.FrameInfo frameInfo = await codec.getNextFrame();
121+
122+
expect(codec.imgElement, isNotNull);
123+
expect(codec.imgElement!.src, contains('sample_image1.png'));
124+
expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous');
125+
expect(codec.imgElement!.decoding, 'async');
126+
114127
expect(frameInfo.image, isNotNull);
115128
expect(frameInfo.image.width, 100);
116129
});
@@ -124,5 +137,14 @@ Future<void> testMain() async {
124137
await codec.getNextFrame();
125138
expect(buffer.toString(), '0/100,100/100,');
126139
});
127-
});
140+
}, skip: isSkwasm);
141+
}
142+
143+
HtmlImageElementCodec createImageElementCodec(
144+
String src, {
145+
ui_web.ImageCodecChunkCallback? chunkCallback,
146+
}) {
147+
return isHtml
148+
? HtmlRendererImageCodec(src, chunkCallback: chunkCallback)
149+
: CkImageElementCodec(src, chunkCallback: chunkCallback);
128150
}

0 commit comments

Comments
 (0)