diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 6acdcd4176e186ad95621c6712c5f34a845b3057..4a25b43546a8410ddf3d6eebe2c8ab5a2c4ce8f1 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -15,24 +15,33 @@ void skiaInstantiateImageCodec(Uint8List list, Callback callback, callback(codec); } -/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after requesting from URI. -void skiaInstantiateWebImageCodec(String src, Callback callback, - WebOnlyImageCodecChunkCallback? chunkCallback) { - chunkCallback?.call(0, 100); +/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after +/// requesting from URI. +Future skiaInstantiateWebImageCodec( + String src, WebOnlyImageCodecChunkCallback? chunkCallback) { + Completer completer = Completer(); //TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported. - html.HttpRequest.request( - src, - responseType: "arraybuffer", - ).then((html.HttpRequest response) { - chunkCallback?.call(100, 100); + html.HttpRequest.request(src, responseType: "arraybuffer", + onProgress: (html.ProgressEvent event) { + if (event.lengthComputable) { + chunkCallback?.call(event.loaded!, event.total!); + } + }).then((html.HttpRequest response) { + if (response.status != 200) { + completer.completeError(Exception( + 'Network image request failed with status: ${response.status}')); + } final Uint8List list = new Uint8List.view((response.response as ByteBuffer)); final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(list); final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); - callback(codec); + completer.complete(codec); + }, onError: (dynamic error) { + completer.completeError(error); }); + return completer.future; } /// A wrapper for `SkAnimatedImage`. @@ -43,7 +52,8 @@ class CkAnimatedImage implements ui.Image { // being garbage-collected, or by an explicit call to [delete]. late final SkiaObjectBox box; - CkAnimatedImage(SkAnimatedImage skAnimatedImage) : this._(skAnimatedImage, null); + CkAnimatedImage(SkAnimatedImage skAnimatedImage) + : this._(skAnimatedImage, null); CkAnimatedImage._(this._skAnimatedImage, SkiaObjectBox? boxToClone) { if (boxToClone != null) { @@ -66,19 +76,21 @@ class CkAnimatedImage implements ui.Image { if (assertionsEnabled) { return _disposed; } - throw StateError('Image.debugDisposed is only available when asserts are enabled.'); + throw StateError( + 'Image.debugDisposed is only available when asserts are enabled.'); } ui.Image clone() => CkAnimatedImage._(_skAnimatedImage, box); @override bool isCloneOf(ui.Image other) { - return other is CkAnimatedImage - && other._skAnimatedImage.isAliasOf(_skAnimatedImage); + return other is CkAnimatedImage && + other._skAnimatedImage.isAliasOf(_skAnimatedImage); } @override - List? debugGetOpenHandleStackTraces() => box.debugGetStackTraces(); + List? debugGetOpenHandleStackTraces() => + box.debugGetStackTraces(); int get frameCount => _skAnimatedImage.getFrameCount(); @@ -115,8 +127,9 @@ class CkAnimatedImage implements ui.Image { ); bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); } else { - final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% - // make a copy that we can return + // Defaults to PNG 100%. + final SkData skData = _skAnimatedImage.encodeToData(); + // Make a copy that we can return. bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); } @@ -162,7 +175,8 @@ class CkImage implements ui.Image { if (assertionsEnabled) { return _disposed; } - throw StateError('Image.debugDisposed is only available when asserts are enabled.'); + throw StateError( + 'Image.debugDisposed is only available when asserts are enabled.'); } @override @@ -170,12 +184,12 @@ class CkImage implements ui.Image { @override bool isCloneOf(ui.Image other) { - return other is CkImage - && other.skImage.isAliasOf(skImage); + return other is CkImage && other.skImage.isAliasOf(skImage); } @override - List? debugGetOpenHandleStackTraces() => box.debugGetStackTraces(); + List? debugGetOpenHandleStackTraces() => + box.debugGetStackTraces(); @override int get width => skImage.width(); diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 45c1f8a78673861ad459dd124ea8993687933f72..695bcd9362da51c9ce4bb306b32eb05c548c28d3 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -462,9 +462,14 @@ String? _instantiateImageCodec(Uint8List list, engine.Callback callback) } Future webOnlyInstantiateImageCodecFromUrl(Uri uri, - {engine.WebOnlyImageCodecChunkCallback? chunkCallback}) { - return _futurize((engine.Callback callback) => + {engine.WebOnlyImageCodecChunkCallback? chunkCallback}) { + if (engine.useCanvasKit) { + return engine.skiaInstantiateWebImageCodec( + uri.toString(), chunkCallback); + } else { + return _futurize((engine.Callback callback) => _instantiateImageCodecFromUrl(uri, chunkCallback, callback)); + } } String? _instantiateImageCodecFromUrl( @@ -472,13 +477,8 @@ String? _instantiateImageCodecFromUrl( engine.WebOnlyImageCodecChunkCallback? chunkCallback, engine.Callback callback, ) { - if (engine.useCanvasKit) { - engine.skiaInstantiateWebImageCodec(uri.toString(), callback, chunkCallback); - return null; - } else { - callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); - return null; - } + callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); + return null; } void decodeImageFromList(Uint8List list, ImageDecoderCallback callback) { diff --git a/lib/web_ui/test/canvaskit/image_test.dart b/lib/web_ui/test/canvaskit/image_test.dart index 8451ddccb9651fec8230b8ed3c4de3145702ec0f..2487efab5f5a5a3caf44c8c6c950970e1f4c31f8 100644 --- a/lib/web_ui/test/canvaskit/image_test.dart +++ b/lib/web_ui/test/canvaskit/image_test.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. // @dart = 2.6 +import 'dart:html' show ProgressEvent; + import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -22,14 +24,16 @@ void testMain() { }); test('CkAnimatedImage toString', () { - final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage); expect(image.toString(), '[1×1]'); image.dispose(); }); test('CkAnimatedImage can be explicitly disposed of', () { - final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage); expect(image.box.isDeleted, false); expect(image.debugDisposed, false); @@ -42,7 +46,8 @@ void testMain() { }); test('CkAnimatedImage can be cloned and explicitly disposed of', () async { - final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage); final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage); final CkAnimatedImage imageClone = image.clone(); @@ -63,14 +68,18 @@ void testMain() { }); test('CkImage toString', () { - final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame(); + final SkImage skImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage) + .getCurrentFrame(); final CkImage image = CkImage(skImage); expect(image.toString(), '[1×1]'); image.dispose(); }); test('CkImage can be explicitly disposed of', () { - final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame(); + final SkImage skImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage) + .getCurrentFrame(); final CkImage image = CkImage(skImage); expect(image.debugDisposed, false); expect(image.box.isDeleted, false); @@ -83,7 +92,9 @@ void testMain() { }); test('CkImage can be explicitly disposed of when cloned', () async { - final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame(); + final SkImage skImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage) + .getCurrentFrame(); final CkImage image = CkImage(skImage); final CkImage imageClone = image.clone(); @@ -102,6 +113,12 @@ void testMain() { await Future.delayed(Duration.zero); expect(skImage.isDeleted(), true); }); - // TODO: https://github.com/flutter/flutter/issues/60040 + + test('skiaInstantiateWebImageCodec throws exception if given invalid URL', + () async { + expect(skiaInstantiateWebImageCodec('invalid-url', null), + throwsA(isA())); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 }, skip: isIosSafari); }