diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 61ed3c13c866d..85ac7971e8b41 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -5,15 +5,36 @@ // @dart = 2.10 part of engine; -/// Instantiates a [ui.Codec] backed by an `SkImage` from Skia. +/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia. void skiaInstantiateImageCodec(Uint8List list, Callback callback, [int? width, int? height, int? format, int? rowBytes]) { - final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(list); + final SkAnimatedImage skAnimatedImage = + canvasKit.MakeAnimatedImageFromEncoded(list); final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage); final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage); 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); + //TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported. + html.HttpRequest.request( + src, + responseType: "arraybuffer", + ).then((html.HttpRequest response) { + chunkCallback?.call(100, 100); + 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); + }); +} + /// A wrapper for `SkAnimatedImage`. class CkAnimatedImage implements ui.Image { final SkAnimatedImage _skAnimatedImage; diff --git a/lib/web_ui/lib/src/engine/html_image_codec.dart b/lib/web_ui/lib/src/engine/html_image_codec.dart index cc8b30c1d27ef..a42f4b4f1c746 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -32,17 +32,13 @@ class HtmlCodec implements ui.Codec { // Currently there is no way to watch decode progress, so // we add 0/100 , 100/100 progress callbacks to enable loading progress // builders to create UI. - if (chunkCallback != null) { - chunkCallback!(0, 100); - } + chunkCallback?.call(0, 100); if (_supportsDecode) { final html.ImageElement imgElement = html.ImageElement(); imgElement.src = src; js_util.setProperty(imgElement, 'decoding', 'async'); imgElement.decode().then((dynamic _) { - if (chunkCallback != null) { - chunkCallback!(100, 100); - } + chunkCallback?.call(100, 100); final HtmlImage image = HtmlImage( imgElement, imgElement.naturalWidth, diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 6e8ee2975f963..ecfc7a1a84ec5 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -7,7 +7,8 @@ part of ui; // ignore: unused_element, Used in Shader assert. bool _offsetIsValid(Offset offset) { - assert(offset != null, 'Offset argument was null.'); // ignore: unnecessary_null_comparison + assert(offset != null, + 'Offset argument was null.'); // ignore: unnecessary_null_comparison assert(!offset.dx.isNaN && !offset.dy.isNaN, 'Offset argument contained a NaN value.'); return true; @@ -15,7 +16,8 @@ bool _offsetIsValid(Offset offset) { // ignore: unused_element, Used in Shader assert. bool _matrix4IsValid(Float32List matrix4) { - assert(matrix4 != null, 'Matrix4 argument was null.'); // ignore: unnecessary_null_comparison + assert(matrix4 != null, + 'Matrix4 argument was null.'); // ignore: unnecessary_null_comparison assert(matrix4.length == 16, 'Matrix4 must have 16 entries.'); return true; } @@ -246,8 +248,7 @@ class Color { if (other.runtimeType != runtimeType) { return false; } - return other is Color - && other.value == value; + return other is Color && other.value == value; } @override @@ -1141,7 +1142,8 @@ abstract class Gradient extends Shader { _validateColorStops(colors, colorStops); // If focal is null or focal radius is null, this should be treated as a regular radial gradient // If focal == center and the focal radius is 0.0, it's still a regular radial gradient - final Float32List? matrix32 = matrix4 != null ? engine.toMatrix32(matrix4) : null; + final Float32List? matrix32 = + matrix4 != null ? engine.toMatrix32(matrix4) : null; if (focal == null || (focal == center && focalRadius == 0.0)) { return engine.GradientRadial( center, radius, colors, colorStops, tileMode, matrix32); @@ -1184,12 +1186,12 @@ abstract class Gradient extends Shader { List colors, [ List? colorStops, TileMode tileMode = TileMode.clamp, - double startAngle/*?*/ = 0.0, - double endAngle/*!*/ = math.pi * 2, + double startAngle /*?*/ = 0.0, + double endAngle /*!*/ = math.pi * 2, Float64List? matrix4, ]) => - engine.GradientSweep( - center, colors, colorStops, tileMode, startAngle, endAngle, matrix4 != null ? engine.toMatrix32(matrix4) : null); + engine.GradientSweep(center, colors, colorStops, tileMode, startAngle, + endAngle, matrix4 != null ? engine.toMatrix32(matrix4) : null); } /// Opaque handle to raw decoded image data (pixels). @@ -1383,9 +1385,9 @@ class MaskFilter { @override bool operator ==(Object other) { - return other is MaskFilter - && other._style == _style - && other._sigma == _sigma; + return other is MaskFilter && + other._style == _style && + other._sigma == _sigma; } @override @@ -1594,7 +1596,8 @@ String? _instantiateImageCodec( } else { assert(height != null); assert(format != null); - engine.skiaInstantiateImageCodec(list, callback, width, height, format!.index, rowBytes); + engine.skiaInstantiateImageCodec( + list, callback, width, height, format!.index, rowBytes); } return null; } @@ -1613,8 +1616,14 @@ String? _instantiateImageCodecFromUrl( Uri uri, engine.WebOnlyImageCodecChunkCallback? chunkCallback, engine.Callback callback) { - callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); - return null; + if (engine.experimentalUseSkia) { + engine.skiaInstantiateWebImageCodec( + uri.toString(), callback, chunkCallback); + return null; + } else { + callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); + return null; + } } /// Loads a single image frame from a byte array into an [Image] object. @@ -1650,16 +1659,16 @@ void decodeImageFromPixels( int? targetHeight, bool allowUpscaling = true, }) { - final Future codecFuture = _futurize( - (engine.Callback callback) { - return _instantiateImageCodec( - pixels, - callback, - width: width, - height: height, - format: format, - rowBytes: rowBytes, - ); + final Future codecFuture = + _futurize((engine.Callback callback) { + return _instantiateImageCodec( + pixels, + callback, + width: width, + height: height, + format: format, + rowBytes: rowBytes, + ); }); codecFuture .then((Codec codec) => codec.getNextFrame()) @@ -1684,8 +1693,10 @@ class Shadow { this.color = const Color(_kColorDefault), this.offset = Offset.zero, this.blurRadius = 0.0, - }) : assert(color != null, 'Text shadow color was null.'), // ignore: unnecessary_null_comparison - assert(offset != null, 'Text shadow offset was null.'), // ignore: unnecessary_null_comparison + }) : assert(color != null, + 'Text shadow color was null.'), // ignore: unnecessary_null_comparison + assert(offset != null, + 'Text shadow offset was null.'), // ignore: unnecessary_null_comparison assert(blurRadius >= 0.0, 'Text shadow blur radius should be non-negative.'); @@ -1816,10 +1827,10 @@ class Shadow { if (identical(this, other)) { return true; } - return other is Shadow - && other.color == color - && other.offset == offset - && other.blurRadius == blurRadius; + return other is Shadow && + other.color == color && + other.offset == offset && + other.blurRadius == blurRadius; } @override @@ -1829,7 +1840,6 @@ class Shadow { String toString() => 'TextShadow($color, $offset, $blurRadius)'; } - /// A shader (as used by [Paint.shader]) that tiles an image. class ImageShader extends Shader { /// Creates an image-tiling shader. The first argument specifies the image to @@ -1838,19 +1848,14 @@ class ImageShader extends Shader { /// matrix to apply to the effect. All the arguments are required and must not /// be null. factory ImageShader( - Image image, - TileMode tmx, - TileMode tmy, - Float64List matrix4) { + Image image, TileMode tmx, TileMode tmy, Float64List matrix4) { if (engine.experimentalUseSkia) { return engine.EngineImageShader(image, tmx, tmy, matrix4); } - throw UnsupportedError( - 'ImageShader not implemented for web platform.'); + throw UnsupportedError('ImageShader not implemented for web platform.'); } } - /// A handle to a read-only byte buffer that is managed by the engine. class ImmutableBuffer { ImmutableBuffer._(this.length); @@ -1878,7 +1883,11 @@ class ImmutableBuffer { /// Use this class to determine the height, width, and byte size of image data /// before decoding it. class ImageDescriptor { - ImageDescriptor._() : _width = null, _height = null, _rowBytes = null, _format = null; + ImageDescriptor._() + : _width = null, + _height = null, + _rowBytes = null, + _format = null; /// Creates an image descriptor from encoded data in a supported format. static Future encoded(ImmutableBuffer buffer) async { @@ -1902,7 +1911,10 @@ class ImageDescriptor { required int height, int? rowBytes, required PixelFormat pixelFormat, - }) : _width = width, _height = height, _rowBytes = rowBytes, _format = pixelFormat { + }) : _width = width, + _height = height, + _rowBytes = rowBytes, + _format = pixelFormat { _data = buffer._list; } @@ -1913,7 +1925,8 @@ class ImageDescriptor { final PixelFormat? _format; Never _throw(String parameter) { - throw UnsupportedError('ImageDescriptor.$parameter is not supported on web.'); + throw UnsupportedError( + 'ImageDescriptor.$parameter is not supported on web.'); } /// The width, in pixels, of the image. @@ -1923,7 +1936,8 @@ class ImageDescriptor { int get height => _height ?? _throw('height'); /// The number of bytes per pixel in the image. - int get bytesPerPixel => throw UnsupportedError('ImageDescriptor.bytesPerPixel is not supported on web.'); + int get bytesPerPixel => throw UnsupportedError( + 'ImageDescriptor.bytesPerPixel is not supported on web.'); /// Release the resources used by this object. The object is no longer usable /// after this method is called. @@ -1943,16 +1957,15 @@ class ImageDescriptor { allowUpscaling: false, ); } - return _futurize( - (engine.Callback callback) { - return _instantiateImageCodec( - _data!, - callback, - width: _width, - height: _height, - format: _format, - rowBytes: _rowBytes, - ); - }); + return _futurize((engine.Callback callback) { + return _instantiateImageCodec( + _data!, + callback, + width: _width, + height: _height, + format: _format, + rowBytes: _rowBytes, + ); + }); } } diff --git a/lib/web_ui/test/engine/image/html_image_codec_test.dart b/lib/web_ui/test/engine/image/html_image_codec_test.dart index 38cd5602d32f4..853e102f82e23 100644 --- a/lib/web_ui/test/engine/image/html_image_codec_test.dart +++ b/lib/web_ui/test/engine/image/html_image_codec_test.dart @@ -19,7 +19,27 @@ Future main() async { test('provides image loading progress', () async { StringBuffer buffer = new StringBuffer(); final HtmlCodec codec = HtmlCodec('sample_image1.png', - chunkCallback: (int loaded, int total) { + chunkCallback: (int loaded, int total) { + buffer.write('$loaded/$total,'); + }); + await codec.getNextFrame(); + expect(buffer.toString(), '0/100,100/100,'); + }); + }); + + group('ImageCodecUrl', () { + test('loads sample image from web', () async { + final Uri uri = Uri.base.resolve('sample_image1.png'); + final HtmlCodec codec = await ui.webOnlyInstantiateImageCodecFromUrl(uri); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + 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'); + StringBuffer buffer = new StringBuffer(); + final HtmlCodec codec = await ui.webOnlyInstantiateImageCodecFromUrl(uri, + chunkCallback: (int loaded, int total) { buffer.write('$loaded/$total,'); }); await codec.getNextFrame();