[web] Reland: (Add crossOrigin property to <img> 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
This commit is contained in:
Mouad Debbar 2024-12-17 10:35:17 -05:00 committed by GitHub
parent 37a7c44c11
commit d2a5b9dcb1
6 changed files with 71 additions and 15 deletions

View File

@ -161,7 +161,7 @@ ui.Image createCkImageFromImageElement(
} }
class CkImageElementCodec extends HtmlImageElementCodec { class CkImageElementCodec extends HtmlImageElementCodec {
CkImageElementCodec(super.src); CkImageElementCodec(super.src, {super.chunkCallback});
@override @override
ui.Image createImageFromHTMLImageElement( ui.Image createImageFromHTMLImageElement(
@ -170,7 +170,7 @@ class CkImageElementCodec extends HtmlImageElementCodec {
} }
class CkImageBlobCodec extends HtmlBlobCodec { class CkImageBlobCodec extends HtmlBlobCodec {
CkImageBlobCodec(super.blob); CkImageBlobCodec(super.blob, {super.chunkCallback});
@override @override
ui.Image createImageFromHTMLImageElement( ui.Image createImageFromHTMLImageElement(
@ -326,7 +326,7 @@ const String _kNetworkImageMessage = 'Failed to load network image.';
/// requesting from URI. /// requesting from URI.
Future<ui.Codec> skiaInstantiateWebImageCodec( Future<ui.Codec> skiaInstantiateWebImageCodec(
String url, ui_web.ImageCodecChunkCallback? chunkCallback) async { String url, ui_web.ImageCodecChunkCallback? chunkCallback) async {
final CkImageElementCodec imageElementCodec = CkImageElementCodec(url); final CkImageElementCodec imageElementCodec = CkImageElementCodec(url, chunkCallback: chunkCallback);
try { try {
await imageElementCodec.decode(); await imageElementCodec.decode();
return imageElementCodec; return imageElementCodec;
@ -339,7 +339,7 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
data: list, contentType: imageType.mimeType, debugSource: url); data: list, contentType: imageType.mimeType, debugSource: url);
} else { } else {
final DomBlob blob = createDomBlob(<ByteBuffer>[list.buffer]); final DomBlob blob = createDomBlob(<ByteBuffer>[list.buffer]);
final CkImageBlobCodec codec = CkImageBlobCodec(blob); final CkImageBlobCodec codec = CkImageBlobCodec(blob, chunkCallback: chunkCallback);
try { try {
await codec.decode(); await codec.decode();

View File

@ -990,6 +990,22 @@ extension DomHTMLImageElementExtension on DomHTMLImageElement {
external set _height(JSNumber? value); external set _height(JSNumber? value);
set height(double? value) => _height = value?.toJS; 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') @JS('decode')
external JSPromise<JSAny?> _decode(); external JSPromise<JSAny?> _decode();
Future<Object?> decode() => js_util.promiseToFuture<Object?>(_decode()); Future<Object?> decode() => js_util.promiseToFuture<Object?>(_decode());

View File

@ -43,8 +43,13 @@ abstract class HtmlImageElementCodec implements ui.Codec {
// builders to create UI. // builders to create UI.
chunkCallback?.call(0, 100); chunkCallback?.call(0, 100);
imgElement = createDomHTMLImageElement(); imgElement = createDomHTMLImageElement();
imgElement!.src = src; if (renderer is! HtmlRenderer) {
setJsProperty<String>(imgElement!, 'decoding', 'async'); imgElement!.crossOrigin = 'anonymous';
}
imgElement!
..decoding = 'async'
..src = src;
// Ignoring the returned future on purpose because we're communicating // Ignoring the returned future on purpose because we're communicating
// through the `completer`. // through the `completer`.
@ -91,7 +96,7 @@ abstract class HtmlImageElementCodec implements ui.Codec {
} }
abstract class HtmlBlobCodec extends HtmlImageElementCodec { abstract class HtmlBlobCodec extends HtmlImageElementCodec {
HtmlBlobCodec(this.blob) HtmlBlobCodec(this.blob, {super.chunkCallback})
: super( : super(
domWindow.URL.createObjectURL(blob), domWindow.URL.createObjectURL(blob),
debugSource: 'encoded image bytes', debugSource: 'encoded image bytes',

View File

@ -253,6 +253,19 @@ Future<void> 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(); _testCkAnimatedImage();
test('isAvif', () { test('isAvif', () {

View File

@ -7,12 +7,15 @@ import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart'; import 'package:test/bootstrap/browser.dart';
import 'package:test/test.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.dart';
import 'package:ui/src/engine/html_image_element_codec.dart'; import 'package:ui/src/engine/html_image_element_codec.dart';
import 'package:ui/ui.dart' as ui; import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web; import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import '../../common/test_initialization.dart'; import '../../common/test_initialization.dart';
import '../../ui/utils.dart';
void main() { void main() {
internalBootstrapBrowserTest(() => testMain); internalBootstrapBrowserTest(() => testMain);
@ -60,16 +63,20 @@ Future<void> testMain() async {
expect(image.height, height); expect(image.height, height);
}); });
test('loads sample image', () async { test('loads sample image', () async {
final HtmlImageElementCodec codec = final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png');
HtmlRendererImageCodec('sample_image1.png');
final ui.FrameInfo frameInfo = await codec.getNextFrame(); 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, isNotNull);
expect(frameInfo.image.width, 100); expect(frameInfo.image.width, 100);
expect(frameInfo.image.toString(), '[100×100]'); expect(frameInfo.image.toString(), '[100×100]');
}); });
test('dispose image image', () async { test('dispose image image', () async {
final HtmlImageElementCodec codec = final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png');
HtmlRendererImageCodec('sample_image1.png');
final ui.FrameInfo frameInfo = await codec.getNextFrame(); final ui.FrameInfo frameInfo = await codec.getNextFrame();
expect(frameInfo.image, isNotNull); expect(frameInfo.image, isNotNull);
expect(frameInfo.image.debugDisposed, isFalse); expect(frameInfo.image.debugDisposed, isFalse);
@ -78,7 +85,7 @@ Future<void> testMain() async {
}); });
test('provides image loading progress', () async { test('provides image loading progress', () async {
final StringBuffer buffer = StringBuffer(); final StringBuffer buffer = StringBuffer();
final HtmlImageElementCodec codec = HtmlRendererImageCodec( final HtmlImageElementCodec codec = createImageElementCodec(
'sample_image1.png', chunkCallback: (int loaded, int total) { 'sample_image1.png', chunkCallback: (int loaded, int total) {
buffer.write('$loaded/$total,'); buffer.write('$loaded/$total,');
}); });
@ -89,7 +96,7 @@ Future<void> testMain() async {
/// Regression test for Firefox /// Regression test for Firefox
/// https://github.com/flutter/flutter/issues/66412 /// https://github.com/flutter/flutter/issues/66412
test('Returns nonzero natural width/height', () async { test('Returns nonzero natural width/height', () async {
final HtmlImageElementCodec codec = HtmlRendererImageCodec( final HtmlImageElementCodec codec = createImageElementCodec(
'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I' 'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I'
'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG' 'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG'
'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx' 'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx'
@ -103,7 +110,7 @@ Future<void> testMain() async {
final ui.FrameInfo frameInfo = await codec.getNextFrame(); final ui.FrameInfo frameInfo = await codec.getNextFrame();
expect(frameInfo.image.width, isNot(0)); expect(frameInfo.image.width, isNot(0));
}); });
}); }, skip: isSkwasm);
group('ImageCodecUrl', () { group('ImageCodecUrl', () {
test('loads sample image from web', () async { test('loads sample image from web', () async {
@ -111,6 +118,12 @@ Future<void> testMain() async {
final HtmlImageElementCodec codec = final HtmlImageElementCodec codec =
await ui_web.createImageCodecFromUrl(uri) as HtmlImageElementCodec; await ui_web.createImageCodecFromUrl(uri) as HtmlImageElementCodec;
final ui.FrameInfo frameInfo = await codec.getNextFrame(); 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, isNotNull);
expect(frameInfo.image.width, 100); expect(frameInfo.image.width, 100);
}); });
@ -124,5 +137,14 @@ Future<void> testMain() async {
await codec.getNextFrame(); await codec.getNextFrame();
expect(buffer.toString(), '0/100,100/100,'); 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);
} }

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB