diff --git a/dev/benchmarks/macrobenchmarks/lib/src/animated_placeholder.dart b/dev/benchmarks/macrobenchmarks/lib/src/animated_placeholder.dart index c940bf255a..f6f071d72e 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/animated_placeholder.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/animated_placeholder.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:ui' as ui show Codec; +import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -57,11 +58,11 @@ class DelayedBase64Image extends ImageProvider { } @override - ImageStreamCompleter load(int key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer(int key, DecoderBufferCallback decode) { return MultiFrameImageStreamCompleter( codec: Future.delayed( delay, - () => decode(base64.decode(data)), + () async => decode(await ImmutableBuffer.fromUint8List(base64.decode(data))), ), scale: 1.0, ); diff --git a/dev/benchmarks/microbenchmarks/lib/ui/image_bench.dart b/dev/benchmarks/microbenchmarks/lib/ui/image_bench.dart index 8c4609debe..ac3ba0bc54 100644 --- a/dev/benchmarks/microbenchmarks/lib/ui/image_bench.dart +++ b/dev/benchmarks/microbenchmarks/lib/ui/image_bench.dart @@ -87,9 +87,7 @@ Future main() async { for (int i = 0; i < 10; i += 1) { await Future.wait(>[ for (String asset in assets) - rootBundle.load(asset).then((ByteData data) { - return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List()); - }) + rootBundle.loadBuffer(asset) ]); } watch.stop(); diff --git a/packages/flutter/lib/src/painting/_network_image_io.dart b/packages/flutter/lib/src/painting/_network_image_io.dart index 77c39dd377..e2ea5f7cd0 100644 --- a/packages/flutter/lib/src/painting/_network_image_io.dart +++ b/packages/flutter/lib/src/painting/_network_image_io.dart @@ -46,7 +46,26 @@ class NetworkImage extends image_provider.ImageProvider chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( - codec: _loadAsync(key as NetworkImage, chunkEvents, decode), + codec: _loadAsync(key as NetworkImage, chunkEvents, null, decode), + chunkEvents: chunkEvents.stream, + scale: key.scale, + debugLabel: key.url, + informationCollector: () => [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('Image key', key), + ], + ); + } + + @override + ImageStreamCompleter loadBuffer(image_provider.NetworkImage key, image_provider.DecoderBufferCallback decode) { + // Ownership of this controller is handed off to [_loadAsync]; it is that + // method's responsibility to close the controller's stream when the image + // has been loaded or an error is thrown. + final StreamController chunkEvents = StreamController(); + + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key as NetworkImage, chunkEvents, decode, null), chunkEvents: chunkEvents.stream, scale: key.scale, debugLabel: key.url, @@ -77,7 +96,8 @@ class NetworkImage extends image_provider.ImageProvider _loadAsync( NetworkImage key, StreamController chunkEvents, - image_provider.DecoderCallback decode, + image_provider.DecoderBufferCallback? decode, + image_provider.DecoderCallback? decodeDepreacted, ) async { try { assert(key == this); @@ -111,7 +131,13 @@ class NetworkImage extends image_provider.ImageProvider chunkEvents = + StreamController(); + + return MultiFrameImageStreamCompleter( + chunkEvents: chunkEvents.stream, + codec: _loadAsync(key as NetworkImage, decode, null, chunkEvents), scale: key.scale, debugLabel: key.url, informationCollector: _imageStreamInformationCollector(key), @@ -93,7 +110,8 @@ class NetworkImage // directly in place of the typical `instantiateImageCodec` method. Future _loadAsync( NetworkImage key, - image_provider.DecoderCallback decode, + image_provider.DecoderBufferCallback? decode, + image_provider.DecoderCallback? decodeDepreacted, StreamController chunkEvents, ) async { assert(key == this); @@ -144,7 +162,13 @@ class NetworkImage statusCode: request.status!, uri: resolved); } - return decode(bytes); + if (decode != null) { + final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + return decode(buffer); + } else { + assert(decodeDepreacted != null); + return decodeDepreacted!(bytes); + } } else { // This API only exists in the web engine implementation and is not // contained in the analyzer summary for Flutter. diff --git a/packages/flutter/lib/src/painting/binding.dart b/packages/flutter/lib/src/painting/binding.dart index fd6c3bd1df..d69e3e9c3c 100644 --- a/packages/flutter/lib/src/painting/binding.dart +++ b/packages/flutter/lib/src/painting/binding.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:typed_data' show Uint8List; -import 'dart:ui' as ui show instantiateImageCodec, Codec; +import 'dart:ui' as ui show instantiateImageCodec, instantiateImageCodecFromBuffer, Codec, ImmutableBuffer; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show ServicesBinding; @@ -81,6 +81,9 @@ mixin PaintingBinding on BindingBase, ServicesBinding { /// Calls through to [dart:ui.instantiateImageCodec] from [ImageCache]. /// + /// This method is deprecated. use [instantiateImageCodecFromBuffer] with an + /// [ImmutableBuffer] instance instead of this method. + /// /// The `cacheWidth` and `cacheHeight` parameters, when specified, indicate /// the size to decode the image to. /// @@ -97,6 +100,10 @@ mixin PaintingBinding on BindingBase, ServicesBinding { /// unnecessary memory usage for images. Callers that wish to display an image /// above its native resolution should prefer scaling the canvas the image is /// drawn into. + @Deprecated( + 'Use instantiateImageCodecFromBuffer with an ImmutableBuffer instance instead. ' + 'This feature was deprecated after v2.13.0-1.0.pre.', + ) Future instantiateImageCodec( Uint8List bytes, { int? cacheWidth, @@ -114,6 +121,44 @@ mixin PaintingBinding on BindingBase, ServicesBinding { ); } + /// Calls through to [dart:ui.instantiateImageCodecFromBuffer] from [ImageCache]. + /// + /// The [buffer] parameter should be an [ui.ImmutableBuffer] instance which can + /// be acquired from [ui.ImmutableBuffer.fromUint8List] or [ui.ImmutableBuffer.fromAsset]. + /// + /// The [cacheWidth] and [cacheHeight] parameters, when specified, indicate + /// the size to decode the image to. + /// + /// Both [cacheWidth] and [cacheHeight] must be positive values greater than + /// or equal to 1, or null. It is valid to specify only one of `cacheWidth` + /// and [cacheHeight] with the other remaining null, in which case the omitted + /// dimension will be scaled to maintain the aspect ratio of the original + /// dimensions. When both are null or omitted, the image will be decoded at + /// its native resolution. + /// + /// The [allowUpscaling] parameter determines whether the `cacheWidth` or + /// [cacheHeight] parameters are clamped to the intrinsic width and height of + /// the original image. By default, the dimensions are clamped to avoid + /// unnecessary memory usage for images. Callers that wish to display an image + /// above its native resolution should prefer scaling the canvas the image is + /// drawn into. + Future instantiateImageCodecFromBuffer( + ui.ImmutableBuffer buffer, { + int? cacheWidth, + int? cacheHeight, + bool allowUpscaling = false, + }) { + assert(cacheWidth == null || cacheWidth > 0); + assert(cacheHeight == null || cacheHeight > 0); + assert(allowUpscaling != null); + return ui.instantiateImageCodecFromBuffer( + buffer, + targetWidth: cacheWidth, + targetHeight: cacheHeight, + allowUpscaling: allowUpscaling, + ); + } + @override void evict(String asset) { super.evict(asset); diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index 7652abd1d7..3b2cd566a5 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; -import 'dart:ui' as ui show Codec; +import 'dart:ui' as ui show Codec, ImmutableBuffer; import 'dart:ui' show Size, Locale, TextDirection; import 'package:flutter/foundation.dart'; @@ -164,6 +164,9 @@ class ImageConfiguration { /// Performs the decode process for use in [ImageProvider.load]. /// +/// This typedef is deprecated. Use [DecoderBufferCallback] with +/// [ImageProvider.loadBuffer] instead. +/// /// This callback allows decoupling of the `cacheWidth`, `cacheHeight`, and /// `allowUpscaling` parameters from implementations of [ImageProvider] that do /// not expose them. @@ -172,7 +175,24 @@ class ImageConfiguration { /// /// * [ResizeImage], which uses this to override the `cacheWidth`, /// `cacheHeight`, and `allowUpscaling` parameters. -typedef DecoderCallback = Future Function(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool allowUpscaling}); +@Deprecated( + 'Use DecoderBufferCallback with ImageProvider.loadBuffer instead. ' + 'This feature was deprecated after v2.13.0-1.0.pre.', +) +typedef DecoderCallback = Future Function(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling}); + + +/// Performs the decode process for use in [ImageProvider.loadBuffer]. +/// +/// This callback allows decoupling of the `cacheWidth`, `cacheHeight`, and +/// `allowUpscaling` parameters from implementations of [ImageProvider] that do +/// not expose them. +/// +/// See also: +/// +/// * [ResizeImage], which uses this to override the `cacheWidth`, +/// `cacheHeight`, and `allowUpscaling` parameters. +typedef DecoderBufferCallback = Future Function(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling}); /// Identifies an image without committing to the precise final asset. This /// allows a set of images to be identified and for the precise image to later @@ -213,16 +233,16 @@ typedef DecoderCallback = Future Function(Uint8List bytes, {int? cache /// using that key. This is handled by [resolveStreamForKey]. That method /// may fizzle if it determines the image is no longer necessary, use the /// provided [ImageErrorListener] to report an error, set the completer -/// from the cache if possible, or call [load] to fetch the encoded image +/// from the cache if possible, or call [loadBuffer] to fetch the encoded image /// bytes and schedule decoding. -/// 4. The [load] method is responsible for both fetching the encoded bytes +/// 4. The [loadBuffer] method is responsible for both fetching the encoded bytes /// and decoding them using the provided [DecoderCallback]. It is called /// in a context that uses the [ImageErrorListener] to report errors back. /// -/// Subclasses normally only have to implement the [load] and [obtainKey] +/// Subclasses normally only have to implement the [loadBuffer] and [obtainKey] /// methods. A subclass that needs finer grained control over the [ImageStream] /// type must override [createStream]. A subclass that needs finer grained -/// control over the resolution, such as delaying calling [load], must override +/// control over the resolution, such as delaying calling [loadBuffer], must override /// [resolveStreamForKey]. /// /// The [resolve] method is marked as [nonVirtual] so that [ImageProvider]s can @@ -491,7 +511,7 @@ abstract class ImageProvider { } final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent( key, - () => load(key, PaintingBinding.instance.instantiateImageCodec), + () => loadBuffer(key, PaintingBinding.instance.instantiateImageCodecFromBuffer), onError: handleError, ); if (completer != null) { @@ -563,6 +583,10 @@ abstract class ImageProvider { /// Converts a key into an [ImageStreamCompleter], and begins fetching the /// image. /// + /// This method is deprecated. Implement [loadBuffer] for faster image + /// loading. Only one of [load] and [loadBuffer] must be implemented, and + /// [loadBuffer] is preferred. + /// /// The [decode] callback provides the logic to obtain the codec for the /// image. /// @@ -570,7 +594,31 @@ abstract class ImageProvider { /// /// * [ResizeImage], for modifying the key to account for cache dimensions. @protected - ImageStreamCompleter load(T key, DecoderCallback decode); + @Deprecated( + 'Implement loadBuffer for faster image loading. ' + 'This feature was deprecated after v2.13.0-1.0.pre.', + ) + ImageStreamCompleter load(T key, DecoderCallback decode) { + throw UnsupportedError('Implement loadBuffer for faster image loading'); + } + + /// Converts a key into an [ImageStreamCompleter], and begins fetching the + /// image. + /// + /// For backwards-compatibility the default implementation of this method calls + /// through to [ImageProvider.load]. However, implementors of this interface should + /// only override this method and not [ImageProvider.load], which is deprecated. + /// + /// The [decode] callback provides the logic to obtain the codec for the + /// image. + /// + /// See also: + /// + /// * [ResizeImage], for modifying the key to account for cache dimensions. + @protected + ImageStreamCompleter loadBuffer(T key, DecoderBufferCallback decode) { + return load(key, PaintingBinding.instance.instantiateImageCodec); + } @override String toString() => '${objectRuntimeType(this, 'ImageConfiguration')}()'; @@ -634,6 +682,24 @@ abstract class AssetBundleImageProvider extends ImageProvider [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('Image key', key), + ]; + return true; + }()); + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode, null), + scale: key.scale, + debugLabel: key.name, + informationCollector: collector, + ); + } + @override ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) { InformationCollector? collector; @@ -645,7 +711,7 @@ abstract class AssetBundleImageProvider extends ImageProvider _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async { - ByteData? data; + Future _loadAsync(AssetBundleImageKey key, DecoderBufferCallback? decode, DecoderCallback? decodeDepreacted) async { + if (decode != null) { + ui.ImmutableBuffer? buffer; + // Hot reload/restart could change whether an asset bundle or key in a + // bundle are available, or if it is a network backed bundle. + try { + buffer = await key.bundle.loadBuffer(key.name); + } on FlutterError { + PaintingBinding.instance.imageCache.evict(key); + rethrow; + } + if (buffer == null) { + PaintingBinding.instance.imageCache.evict(key); + throw StateError('Unable to read data'); + } + return decode(buffer); + } + ByteData data; // Hot reload/restart could change whether an asset bundle or key in a // bundle are available, or if it is a network backed bundle. try { @@ -671,7 +753,7 @@ abstract class AssetBundleImageProvider extends ImageProvider { @override ImageStreamCompleter load(ResizeImageKey key, DecoderCallback decode) { - Future decodeResize(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { + Future decodeResize(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { assert( cacheWidth == null && cacheHeight == null && allowUpscaling == null, 'ResizeImage cannot be composed with another ImageProvider that applies ' 'cacheWidth, cacheHeight, or allowUpscaling.', ); - return decode(bytes, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling); + return decode(buffer, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling); } final ImageStreamCompleter completer = imageProvider.load(key._providerCacheKey, decodeResize); if (!kReleaseMode) { @@ -772,6 +854,23 @@ class ResizeImage extends ImageProvider { return completer; } + @override + ImageStreamCompleter loadBuffer(ResizeImageKey key, DecoderBufferCallback decode) { + Future decodeResize(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { + assert( + cacheWidth == null && cacheHeight == null && allowUpscaling == null, + 'ResizeImage cannot be composed with another ImageProvider that applies ' + 'cacheWidth, cacheHeight, or allowUpscaling.', + ); + return decode(buffer, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling); + } + final ImageStreamCompleter completer = imageProvider.loadBuffer(key._providerCacheKey, decodeResize); + if (!kReleaseMode) { + completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})'; + } + return completer; + } + @override Future obtainKey(ImageConfiguration configuration) { Completer? completer; @@ -832,6 +931,9 @@ abstract class NetworkImage extends ImageProvider { @override ImageStreamCompleter load(NetworkImage key, DecoderCallback decode); + + @override + ImageStreamCompleter loadBuffer(NetworkImage key, DecoderBufferCallback decode); } /// Decodes the given [File] object as an image, associating it with the given @@ -866,7 +968,7 @@ class FileImage extends ImageProvider { @override ImageStreamCompleter load(FileImage key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), + codec: _loadAsync(key, null, decode), scale: key.scale, debugLabel: key.file.path, informationCollector: () => [ @@ -875,18 +977,32 @@ class FileImage extends ImageProvider { ); } - Future _loadAsync(FileImage key, DecoderCallback decode) async { + @override + ImageStreamCompleter loadBuffer(FileImage key, DecoderBufferCallback decode) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode, null), + scale: key.scale, + debugLabel: key.file.path, + informationCollector: () => [ + ErrorDescription('Path: ${file.path}'), + ], + ); + } + + Future _loadAsync(FileImage key, DecoderBufferCallback? decode, DecoderCallback? decodeDeprecated) async { assert(key == this); final Uint8List bytes = await file.readAsBytes(); - if (bytes.lengthInBytes == 0) { // The file may become available later. PaintingBinding.instance.imageCache.evict(key); throw StateError('$file is empty and cannot be loaded as an image.'); } - return decode(bytes); + if (decode != null) { + return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); + } + return decodeDeprecated!(bytes); } @override @@ -953,16 +1069,28 @@ class MemoryImage extends ImageProvider { @override ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), + codec: _loadAsync(key, null, decode), scale: key.scale, debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})', ); } - Future _loadAsync(MemoryImage key, DecoderCallback decode) { - assert(key == this); + @override + ImageStreamCompleter loadBuffer(MemoryImage key, DecoderBufferCallback decode) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode, null), + scale: key.scale, + debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})', + ); + } - return decode(bytes); + Future _loadAsync(MemoryImage key, DecoderBufferCallback? decode, DecoderCallback? decodeDepreacted) async { + assert(key == this); + if (decode != null) { + final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + return decode(buffer); + } + return decodeDepreacted!(bytes); } @override diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index bac48e1b9d..0461eac87a 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -2,11 +2,11 @@ // 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:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -55,6 +55,15 @@ abstract class AssetBundle { /// Throws an exception if the asset is not found. Future load(String key); + /// Retrieve a binary resource from the asset bundle as an immutable + /// buffer. + /// + /// Throws an exception if the asset is not found. + Future loadBuffer(String key) async { + final ByteData data = await load(key); + return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List()); + } + /// Retrieve a string from the asset bundle. /// /// Throws an exception if the asset is not found. @@ -228,6 +237,12 @@ abstract class CachingAssetBundle extends AssetBundle { _stringCache.clear(); _structuredDataCache.clear(); } + + @override + Future loadBuffer(String key) async { + final ByteData data = await load(key); + return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List()); + } } /// An [AssetBundle] that loads resources using platform messages. @@ -242,6 +257,19 @@ class PlatformAssetBundle extends CachingAssetBundle { } return asset; } + + @override + Future loadBuffer(String key) async { + if (kIsWeb) { + final ByteData bytes = await load(key); + return ui.ImmutableBuffer.fromUint8List(bytes.buffer.asUint8List()); + } + try { + return await ui.ImmutableBuffer.fromAsset(key); + } on Exception { + throw FlutterError('Unable to load asset: $key.'); + } + } } AssetBundle _initRootBundle() { diff --git a/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart b/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart index 99be5bbb81..af0cbbf24f 100644 --- a/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart +++ b/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart @@ -109,6 +109,9 @@ class ScrollAwareImageProvider extends ImageProvider { @override ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode); + @override + ImageStreamCompleter loadBuffer(T key, DecoderBufferCallback decode) => imageProvider.loadBuffer(key, decode); + @override Future obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration); } diff --git a/packages/flutter/test/painting/image_cache_test.dart b/packages/flutter/test/painting/image_cache_test.dart index 6e1542f53f..483049807f 100644 --- a/packages/flutter/test/painting/image_cache_test.dart +++ b/packages/flutter/test/painting/image_cache_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -136,15 +135,15 @@ void main() { }); test('Returns null if an error is caught resolving an image', () { - Future basicDecoder(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false); + Future basicDecoder(ui.ImmutableBuffer bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { + return PaintingBinding.instance.instantiateImageCodecFromBuffer(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false); } final ErrorImageProvider errorImage = ErrorImageProvider(); - expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, basicDecoder)), throwsA(isA())); + expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.loadBuffer(errorImage, basicDecoder)), throwsA(isA())); bool caughtError = false; final ImageStreamCompleter? result = imageCache.putIfAbsent( errorImage, - () => errorImage.load(errorImage, basicDecoder), + () => errorImage.loadBuffer(errorImage, basicDecoder), onError: (dynamic error, StackTrace? stackTrace) { caughtError = true; }, diff --git a/packages/flutter/test/painting/image_provider_and_image_cache_test.dart b/packages/flutter/test/painting/image_provider_and_image_cache_test.dart index d1121e56fe..af14532c5c 100644 --- a/packages/flutter/test/painting/image_provider_and_image_cache_test.dart +++ b/packages/flutter/test/painting/image_provider_and_image_cache_test.dart @@ -17,8 +17,8 @@ import 'mocks_for_image_cache.dart'; void main() { TestRenderingFlutterBinding.ensureInitialized(); - Future basicDecoder(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false); + Future basicDecoder(ui.ImmutableBuffer bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { + return PaintingBinding.instance.instantiateImageCodecFromBuffer(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false); } FlutterExceptionHandler? oldError; @@ -76,7 +76,7 @@ void main() { final Uint8List bytes = Uint8List.fromList(kTransparentImage); final MemoryImage imageProvider = MemoryImage(bytes); final ImageStreamCompleter cacheStream = otherCache.putIfAbsent( - imageProvider, () => imageProvider.load(imageProvider, basicDecoder), + imageProvider, () => imageProvider.loadBuffer(imageProvider, basicDecoder), )!; final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final Completer completer = Completer(); diff --git a/packages/flutter/test/painting/image_provider_network_image_test.dart b/packages/flutter/test/painting/image_provider_network_image_test.dart index 807e6b0568..f65760ea4a 100644 --- a/packages/flutter/test/painting/image_provider_network_image_test.dart +++ b/packages/flutter/test/painting/image_provider_network_image_test.dart @@ -6,7 +6,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math' as math; import 'dart:typed_data'; -import 'dart:ui' show Codec, FrameInfo; +import 'dart:ui' show Codec, FrameInfo, ImmutableBuffer; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; @@ -18,8 +18,8 @@ import '../rendering/rendering_tester.dart'; void main() { TestRenderingFlutterBinding.ensureInitialized(); - Future basicDecoder(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false); + Future basicDecoder(ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { + return PaintingBinding.instance.instantiateImageCodecFromBuffer(buffer, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false); } late _FakeHttpClient httpClient; @@ -77,7 +77,7 @@ void main() { Future loadNetworkImage() async { final NetworkImage networkImage = NetworkImage(nonconst('foo')); - final ImageStreamCompleter completer = networkImage.load(networkImage, basicDecoder); + final ImageStreamCompleter completer = networkImage.loadBuffer(networkImage, basicDecoder); completer.addListener(ImageStreamListener( (ImageInfo image, bool synchronousCall) { }, onError: (dynamic error, StackTrace? stackTrace) { @@ -189,7 +189,7 @@ void main() { debugNetworkImageHttpClientProvider = null; }, skip: isBrowser); // [intended] Browser does not resolve images this way. - Future decoder(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) async { + Future decoder(ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) async { return FakeCodec(); } @@ -207,7 +207,7 @@ void main() { const NetworkImage provider = NetworkImage(url); - final MultiFrameImageStreamCompleter completer = provider.load(provider, decoder) as MultiFrameImageStreamCompleter; + final MultiFrameImageStreamCompleter completer = provider.loadBuffer(provider, decoder) as MultiFrameImageStreamCompleter; expect(completer.debugLabel, url); }); diff --git a/packages/flutter/test/painting/image_provider_resize_image_test.dart b/packages/flutter/test/painting/image_provider_resize_image_test.dart index ecf79e2dad..3607618624 100644 --- a/packages/flutter/test/painting/image_provider_resize_image_test.dart +++ b/packages/flutter/test/painting/image_provider_resize_image_test.dart @@ -99,14 +99,14 @@ void main() { final MemoryImage memoryImage = MemoryImage(bytes); final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321); - Future decode(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) { + Future decode(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) { expect(cacheWidth, 123); expect(cacheHeight, 321); expect(allowUpscaling, false); - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling); + return PaintingBinding.instance.instantiateImageCodecFromBuffer(buffer, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling); } - resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode); + resizeImage.loadBuffer(await resizeImage.obtainKey(ImageConfiguration.empty), decode); }); test('ResizeImage handles sync obtainKey', () async { diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart index 4e19577855..eebf8a8d0f 100644 --- a/packages/flutter/test/painting/image_provider_test.dart +++ b/packages/flutter/test/painting/image_provider_test.dart @@ -88,14 +88,14 @@ void main() { final File file = fs.file('/empty.png')..createSync(recursive: true); final FileImage provider = FileImage(file); - expect(provider.load(provider, (Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) async { + expect(provider.loadBuffer(provider, (ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) async { return Future.value(FakeCodec()); }), isA()); expect(await error.future, isStateError); }); - Future decoder(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) async { + Future decoder(ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) async { return FakeCodec(); } @@ -104,7 +104,7 @@ void main() { final File file = fs.file('/blue.png')..createSync(recursive: true)..writeAsBytesSync(kBlueSquarePng); final FileImage provider = FileImage(file); - final MultiFrameImageStreamCompleter completer = provider.load(provider, decoder) as MultiFrameImageStreamCompleter; + final MultiFrameImageStreamCompleter completer = provider.loadBuffer(provider, decoder) as MultiFrameImageStreamCompleter; expect(completer.debugLabel, file.path); }); @@ -113,7 +113,7 @@ void main() { final Uint8List bytes = Uint8List.fromList(kBlueSquarePng); final MemoryImage provider = MemoryImage(bytes); - final MultiFrameImageStreamCompleter completer = provider.load(provider, decoder) as MultiFrameImageStreamCompleter; + final MultiFrameImageStreamCompleter completer = provider.loadBuffer(provider, decoder) as MultiFrameImageStreamCompleter; expect(completer.debugLabel, 'MemoryImage(${describeIdentity(bytes)})'); }); @@ -122,7 +122,7 @@ void main() { const String asset = 'images/blue.png'; final ExactAssetImage provider = ExactAssetImage(asset, bundle: _TestAssetBundle()); final AssetBundleImageKey key = await provider.obtainKey(ImageConfiguration.empty); - final MultiFrameImageStreamCompleter completer = provider.load(key, decoder) as MultiFrameImageStreamCompleter; + final MultiFrameImageStreamCompleter completer = provider.loadBuffer(key, decoder) as MultiFrameImageStreamCompleter; expect(completer.debugLabel, asset); }); @@ -130,7 +130,7 @@ void main() { test('Resize image sets tag', () async { final Uint8List bytes = Uint8List.fromList(kBlueSquarePng); final ResizeImage provider = ResizeImage(MemoryImage(bytes), width: 40, height: 40); - final MultiFrameImageStreamCompleter completer = provider.load( + final MultiFrameImageStreamCompleter completer = provider.loadBuffer( await provider.obtainKey(ImageConfiguration.empty), decoder, ) as MultiFrameImageStreamCompleter; diff --git a/packages/flutter/test/painting/image_resolution_test.dart b/packages/flutter/test/painting/image_resolution_test.dart index fa9e90dda7..c447eb4397 100644 --- a/packages/flutter/test/painting/image_resolution_test.dart +++ b/packages/flutter/test/painting/image_resolution_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; @@ -34,6 +35,12 @@ class TestAssetBundle extends CachingAssetBundle { } throw FlutterError('key not found'); } + + @override + Future loadBuffer(String key) async { + final ByteData data = await load(key); + return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List()); + } } void main() { diff --git a/packages/flutter/test/painting/image_test_utils.dart b/packages/flutter/test/painting/image_test_utils.dart index ca4832718d..5894efe98d 100644 --- a/packages/flutter/test/painting/image_test_utils.dart +++ b/packages/flutter/test/painting/image_test_utils.dart @@ -30,6 +30,11 @@ class TestImageProvider extends ImageProvider { @override ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { + throw UnsupportedError('Use ImageProvider.loadBuffer instead.'); + } + + @override + ImageStreamCompleter loadBuffer(TestImageProvider key, DecoderBufferCallback decode) { loadCallCount += 1; return OneFrameImageStreamCompleter(_completer.future); } diff --git a/packages/flutter/test/painting/mocks_for_image_cache.dart b/packages/flutter/test/painting/mocks_for_image_cache.dart index 885bf04d69..4cb5ac6961 100644 --- a/packages/flutter/test/painting/mocks_for_image_cache.dart +++ b/packages/flutter/test/painting/mocks_for_image_cache.dart @@ -86,6 +86,11 @@ Future extractOneFrame(ImageStream stream) { } class ErrorImageProvider extends ImageProvider { + @override + ImageStreamCompleter loadBuffer(ErrorImageProvider key, DecoderBufferCallback decode) { + throw Error(); + } + @override ImageStreamCompleter load(ErrorImageProvider key, DecoderCallback decode) { throw Error(); @@ -99,7 +104,7 @@ class ErrorImageProvider extends ImageProvider { class ObtainKeyErrorImageProvider extends ImageProvider { @override - ImageStreamCompleter load(ObtainKeyErrorImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer(ObtainKeyErrorImageProvider key, DecoderBufferCallback decode) { throw Error(); } @@ -107,11 +112,16 @@ class ObtainKeyErrorImageProvider extends ImageProvider obtainKey(ImageConfiguration configuration) { throw Error(); } + + @override + ImageStreamCompleter load(ObtainKeyErrorImageProvider key, DecoderCallback decode) { + throw UnimplementedError(); + } } class LoadErrorImageProvider extends ImageProvider { @override - ImageStreamCompleter load(LoadErrorImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer(LoadErrorImageProvider key, DecoderBufferCallback decode) { throw Error(); } @@ -119,6 +129,11 @@ class LoadErrorImageProvider extends ImageProvider { Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture(this); } + + @override + ImageStreamCompleter load(LoadErrorImageProvider key, DecoderCallback decode) { + throw UnimplementedError(); + } } class LoadErrorCompleterImageProvider extends ImageProvider { diff --git a/packages/flutter/test/widgets/fade_in_image_test.dart b/packages/flutter/test/widgets/fade_in_image_test.dart index 8eb603e24b..d0f3cd56f8 100644 --- a/packages/flutter/test/widgets/fade_in_image_test.dart +++ b/packages/flutter/test/widgets/fade_in_image_test.dart @@ -2,6 +2,7 @@ // 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 'dart:ui' as ui; @@ -58,8 +59,8 @@ class LoadTestImageProvider extends ImageProvider { final ImageProvider provider; - ImageStreamCompleter testLoad(Object key, DecoderCallback decode) { - return provider.load(key, decode); + ImageStreamCompleter testLoad(Object key, DecoderBufferCallback decode) { + return provider.loadBuffer(key, decode); } @override @@ -339,7 +340,7 @@ Future main() async { group('ImageProvider', () { - testWidgets('memory placeholder cacheWidth and cacheHeight is passed through', (WidgetTester tester) async { + test('memory placeholder cacheWidth and cacheHeight is passed through', () async { final Uint8List testBytes = Uint8List.fromList(kTransparentImage); final FadeInImage image = FadeInImage.memoryNetwork( placeholder: testBytes, @@ -351,22 +352,29 @@ Future main() async { ); bool called = false; - Future decode(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) { + Future decode(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) { expect(cacheWidth, 20); expect(cacheHeight, 30); expect(allowUpscaling, false); called = true; - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling); + return PaintingBinding.instance.instantiateImageCodecFromBuffer(buffer, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling); } final ImageProvider resizeImage = image.placeholder; expect(image.placeholder, isA()); expect(called, false); final LoadTestImageProvider testProvider = LoadTestImageProvider(image.placeholder); - testProvider.testLoad(await resizeImage.obtainKey(ImageConfiguration.empty), decode); + final ImageStreamCompleter streamCompleter = testProvider.testLoad(await resizeImage.obtainKey(ImageConfiguration.empty), decode); + + final Completer completer = Completer(); + streamCompleter.addListener(ImageStreamListener((ImageInfo imageInfo, bool syncCall) { + completer.complete(); + })); + await completer.future; + expect(called, true); }); - testWidgets('do not resize when null cache dimensions', (WidgetTester tester) async { + test('do not resize when null cache dimensions', () async { final Uint8List testBytes = Uint8List.fromList(kTransparentImage); final FadeInImage image = FadeInImage.memoryNetwork( placeholder: testBytes, @@ -374,19 +382,26 @@ Future main() async { ); bool called = false; - Future decode(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) { + Future decode(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) { expect(cacheWidth, null); expect(cacheHeight, null); expect(allowUpscaling, false); called = true; - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + return PaintingBinding.instance.instantiateImageCodecFromBuffer(buffer, cacheWidth: cacheWidth, cacheHeight: cacheHeight); } // image.placeholder should be an instance of MemoryImage instead of ResizeImage final ImageProvider memoryImage = image.placeholder; expect(image.placeholder, isA()); expect(called, false); final LoadTestImageProvider testProvider = LoadTestImageProvider(image.placeholder); - testProvider.testLoad(await memoryImage.obtainKey(ImageConfiguration.empty), decode); + final ImageStreamCompleter streamCompleter = testProvider.testLoad(await memoryImage.obtainKey(ImageConfiguration.empty), decode); + + final Completer completer = Completer(); + streamCompleter.addListener(ImageStreamListener((ImageInfo imageInfo, bool syncCall) { + completer.complete(); + })); + await completer.future; + expect(called, true); }); }); diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart index c4756c9ca1..180e2ae627 100644 --- a/packages/flutter/test/widgets/image_resolution_test.dart +++ b/packages/flutter/test/widgets/image_resolution_test.dart @@ -84,7 +84,7 @@ class TestAssetImage extends AssetImage { final Map images; @override - ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer(AssetBundleImageKey key, DecoderBufferCallback decode) { late ImageInfo imageInfo; key.bundle.load(key.name).then((ByteData data) { final ui.Image image = images[scaleOf(data)]!; diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart index 8bdbc1b32e..c67baf1e7b 100644 --- a/packages/flutter/test/widgets/image_test.dart +++ b/packages/flutter/test/widgets/image_test.dart @@ -2122,7 +2122,7 @@ class _DebouncingImageProvider extends ImageProvider { Future obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration); @override - ImageStreamCompleter load(Object key, DecoderCallback decode) => imageProvider.load(key, decode); + ImageStreamCompleter loadBuffer(Object key, DecoderBufferCallback decode) => imageProvider.loadBuffer(key, decode); } class _FailingImageProvider extends ImageProvider { diff --git a/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart b/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart index 2b6213c0b0..919082c9fa 100644 --- a/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart +++ b/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart @@ -322,7 +322,7 @@ void main() { // If we miss the early return, we will fail. testImageProvider.complete(); - imageCache.putIfAbsent(testImageProvider, () => testImageProvider.load(testImageProvider, PaintingBinding.instance.instantiateImageCodec)); + imageCache.putIfAbsent(testImageProvider, () => testImageProvider.loadBuffer(testImageProvider, PaintingBinding.instance.instantiateImageCodecFromBuffer)); // We've stopped scrolling fast. physics.recommendDeferredLoadingValue = false; await tester.idle(); @@ -377,7 +377,7 @@ void main() { // Complete the original image while we're still scrolling fast. testImageProvider.complete(); - stream.setCompleter(testImageProvider.load(testImageProvider, PaintingBinding.instance.instantiateImageCodec)); + stream.setCompleter(testImageProvider.loadBuffer(testImageProvider, PaintingBinding.instance.instantiateImageCodecFromBuffer)); // Verify that this hasn't changed the cache state yet expect(imageCache.containsKey(testImageProvider), false);