Skip to content

Commit

Permalink
[CP-stable][web] Reland: Add crossOrigin property to <img> tag us…
Browse files Browse the repository at this point in the history
…ed for decoding (#57272)

Original PR: #57228

@christopherfujino 

Approved for hotfix to stable by @yjbanov in #57228 (comment)
  • Loading branch information
Reprevise authored Jan 9, 2025
1 parent 9c0d322 commit 41bb2f4
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 15 deletions.
8 changes: 4 additions & 4 deletions lib/web_ui/lib/src/engine/canvaskit/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ ui.Image createCkImageFromImageElement(
}

class CkImageElementCodec extends HtmlImageElementCodec {
CkImageElementCodec(super.src);
CkImageElementCodec(super.src, {super.chunkCallback});

@override
ui.Image createImageFromHTMLImageElement(
Expand All @@ -170,7 +170,7 @@ class CkImageElementCodec extends HtmlImageElementCodec {
}

class CkImageBlobCodec extends HtmlBlobCodec {
CkImageBlobCodec(super.blob);
CkImageBlobCodec(super.blob, {super.chunkCallback});

@override
ui.Image createImageFromHTMLImageElement(
Expand Down Expand Up @@ -326,7 +326,7 @@ const String _kNetworkImageMessage = 'Failed to load network image.';
/// 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;
Expand All @@ -339,7 +339,7 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
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();
Expand Down
16 changes: 16 additions & 0 deletions lib/web_ui/lib/src/engine/dom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSAny?> _decode();
Future<Object?> decode() => js_util.promiseToFuture<Object?>(_decode());
Expand Down
11 changes: 8 additions & 3 deletions lib/web_ui/lib/src/engine/html_image_element_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,13 @@ abstract class HtmlImageElementCodec implements ui.Codec {
// 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`.
Expand Down Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions lib/web_ui/test/canvaskit/image_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();

test('isAvif', () {
Expand Down
38 changes: 30 additions & 8 deletions lib/web_ui/test/engine/image/html_image_element_codec_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -60,16 +63,20 @@ Future<void> 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);
Expand All @@ -78,7 +85,7 @@ Future<void> 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,');
});
Expand All @@ -89,7 +96,7 @@ Future<void> 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'
Expand All @@ -103,14 +110,20 @@ Future<void> 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 {
final Uri uri = Uri.base.resolve('sample_image1.png');
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);
});
Expand All @@ -124,5 +137,14 @@ Future<void> 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);
}
150 changes: 150 additions & 0 deletions lib/web_ui/test/ui/image/html_image_element_codec_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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 'dart:async';
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);
}

Future<void> testMain() async {
setUpUnitTests();
group('$HtmlImageElementCodec', () {
test('supports raw images - RGBA8888', () async {
final Completer<ui.Image> completer = Completer<ui.Image>();
const int width = 200;
const int height = 300;
final Uint32List list = Uint32List(width * height);
for (int index = 0; index < list.length; index += 1) {
list[index] = 0xFF0000FF;
}
ui.decodeImageFromPixels(
list.buffer.asUint8List(),
width,
height,
ui.PixelFormat.rgba8888,
(ui.Image image) => completer.complete(image),
);
final ui.Image image = await completer.future;
expect(image.width, width);
expect(image.height, height);
});
test('supports raw images - BGRA8888', () async {
final Completer<ui.Image> completer = Completer<ui.Image>();
const int width = 200;
const int height = 300;
final Uint32List list = Uint32List(width * height);
for (int index = 0; index < list.length; index += 1) {
list[index] = 0xFF0000FF;
}
ui.decodeImageFromPixels(
list.buffer.asUint8List(),
width,
height,
ui.PixelFormat.bgra8888,
(ui.Image image) => completer.complete(image),
);
final ui.Image image = await completer.future;
expect(image.width, width);
expect(image.height, height);
});
test('loads sample image', () async {
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 = createImageElementCodec('sample_image1.png');
final ui.FrameInfo frameInfo = await codec.getNextFrame();
expect(frameInfo.image, isNotNull);
expect(frameInfo.image.debugDisposed, isFalse);
frameInfo.image.dispose();
expect(frameInfo.image.debugDisposed, isTrue);
});
test('provides image loading progress', () async {
final StringBuffer buffer = StringBuffer();
final HtmlImageElementCodec codec = createImageElementCodec(
'sample_image1.png', chunkCallback: (int loaded, int total) {
buffer.write('$loaded/$total,');
});
await codec.getNextFrame();
expect(buffer.toString(), '0/100,100/100,');
});

/// Regression test for Firefox
/// https://github.com/flutter/flutter/issues/66412
test('Returns nonzero natural width/height', () async {
final HtmlImageElementCodec codec = createImageElementCodec(
'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I'
'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG'
'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx'
'MiAyLjM5OSAxMiAxMiAwIDkuNjAxLTIuMzk5IDEyLTEyIDEyLTkuNjAxIDAtMTItMi'
'4zOTktMTItMTJDMCAyLjM5OSAyLjM5OSAwIDEyIDB6bS0xLjk2OSAxOC41NjRjMi41'
'MjQuMDAzIDQuNjA0LTIuMDcgNC42MDktNC41OTUgMC0yLjUyMS0yLjA3NC00LjU5NS'
'00LjU5NS00LjU5NVM1LjQ1IDExLjQ0OSA1LjQ1IDEzLjk2OWMwIDIuNTE2IDIuMDY1'
'IDQuNTg4IDQuNTgxIDQuNTk1em04LjM0NC0uMTg5VjUuNjI1SDUuNjI1djIuMjQ3aD'
'EwLjQ5OHYxMC41MDNoMi4yNTJ6bS04LjM0NC02Ljc0OGEyLjM0MyAyLjM0MyAwIDEx'
'LS4wMDIgNC42ODYgMi4zNDMgMi4zNDMgMCAwMS4wMDItNC42ODZ6Ii8+PC9zdmc+');
final ui.FrameInfo frameInfo = await codec.getNextFrame();
expect(frameInfo.image.width, isNot(0));
});
}, skip: isSkwasm);

group('ImageCodecUrl', () {
test('loads sample image from web', () async {
final Uri uri = Uri.base.resolve('sample_image1.png');
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);
});
test('provides image loading progress from web', () async {
final Uri uri = Uri.base.resolve('sample_image1.png');
final StringBuffer buffer = StringBuffer();
final HtmlImageElementCodec codec = await ui_web
.createImageCodecFromUrl(uri, chunkCallback: (int loaded, int total) {
buffer.write('$loaded/$total,');
}) as HtmlImageElementCodec;
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);
}
Binary file added lib/web_ui/test/ui/image/sample_image1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 41bb2f4

Please sign in to comment.