Expose API for custom image decode and cache sizes (#41415)
diff --git a/packages/flutter/lib/src/painting/_network_image_io.dart b/packages/flutter/lib/src/painting/_network_image_io.dart index 2b2c4a7..d976b93 100644 --- a/packages/flutter/lib/src/painting/_network_image_io.dart +++ b/packages/flutter/lib/src/painting/_network_image_io.dart
@@ -9,7 +9,6 @@ import 'package:flutter/foundation.dart'; -import 'binding.dart'; import 'debug.dart'; import 'image_provider.dart' as image_provider; import 'image_stream.dart'; @@ -38,14 +37,14 @@ } @override - ImageStreamCompleter load(image_provider.NetworkImage key) { + ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback 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<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, chunkEvents), + codec: _loadAsync(key, chunkEvents, decode), chunkEvents: chunkEvents.stream, scale: key.scale, informationCollector: () { @@ -76,6 +75,7 @@ Future<ui.Codec> _loadAsync( NetworkImage key, StreamController<ImageChunkEvent> chunkEvents, + image_provider.DecoderCallback decode, ) async { try { assert(key == this); @@ -101,7 +101,7 @@ if (bytes.lengthInBytes == 0) throw Exception('NetworkImage is an empty file: $resolved'); - return PaintingBinding.instance.instantiateImageCodec(bytes); + return decode(bytes); } finally { chunkEvents.close(); }
diff --git a/packages/flutter/lib/src/painting/_network_image_web.dart b/packages/flutter/lib/src/painting/_network_image_web.dart index 6b566de..ba28c8a 100644 --- a/packages/flutter/lib/src/painting/_network_image_web.dart +++ b/packages/flutter/lib/src/painting/_network_image_web.dart
@@ -11,6 +11,8 @@ import 'image_stream.dart'; /// The dart:html implemenation of [image_provider.NetworkImage]. +/// +/// NetworkImage on the web does not support decoding to a specified size. class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage { /// Creates an object that fetches the image at the given URL. /// @@ -34,9 +36,9 @@ } @override - ImageStreamCompleter load(image_provider.NetworkImage key) { + ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) { return MultiFrameImageStreamCompleter( - codec: _loadAsync(key), + codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () { return <DiagnosticsNode>[ @@ -47,7 +49,10 @@ ); } - Future<ui.Codec> _loadAsync(NetworkImage key) async { + // Web does not support decoding network images to a specified size. The decode parameter + // here is ignored and the web-only `ui.webOnlyInstantiateImageCodecFromUrl` will be used + // directly in place of the typical `instantiateImageCodec` method. + Future<ui.Codec> _loadAsync(NetworkImage key, image_provider.DecoderCallback decode) async { assert(key == this); final Uri resolved = Uri.base.resolve(key.url);
diff --git a/packages/flutter/lib/src/painting/binding.dart b/packages/flutter/lib/src/painting/binding.dart index 4eb915d..c022373 100644 --- a/packages/flutter/lib/src/painting/binding.dart +++ b/packages/flutter/lib/src/painting/binding.dart
@@ -70,8 +70,26 @@ ImageCache createImageCache() => ImageCache(); /// Calls through to [dart:ui] with [decodedCacheRatioCap] from [ImageCache]. - Future<ui.Codec> instantiateImageCodec(Uint8List list) { - return ui.instantiateImageCodec(list); + /// + /// 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 decode to its original size. When both are null or omitted, + /// the image will be decoded at its native resolution. + Future<ui.Codec> instantiateImageCodec(Uint8List bytes, { + int cacheWidth, + int cacheHeight, + }) { + assert(cacheWidth == null || cacheWidth > 0); + assert(cacheHeight == null || cacheHeight > 0); + return ui.instantiateImageCodec( + bytes, + targetWidth: cacheWidth, + targetHeight: cacheHeight, + ); } @override
diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index a08e4c9..69790b2 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart
@@ -152,6 +152,16 @@ } } +/// Performs the decode process for use in [ImageProvider.load]. +/// +/// This callback allows decoupling of the `cacheWidth` and `cacheHeight` +/// parameters from implementations of [ImageProvider] that do not use them. +/// +/// See also: +/// +/// * [ResizeImage], which uses this to override the `cacheWidth` and `cacheHeight` parameters. +typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int cacheWidth, int cacheHeight}); + /// 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 /// be resolved based on the environment, e.g. the device pixel ratio. @@ -312,8 +322,11 @@ } key.then<void>((T key) { obtainedKey = key; - final ImageStreamCompleter completer = PaintingBinding.instance - .imageCache.putIfAbsent(key, () => load(key), onError: handleError); + final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent( + key, + () => load(key, PaintingBinding.instance.instantiateImageCodec), + onError: handleError, + ); if (completer != null) { stream.setCompleter(completer); } @@ -379,8 +392,15 @@ /// Converts a key into an [ImageStreamCompleter], and begins fetching the /// image. + /// + /// 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 load(T key); + ImageStreamCompleter load(T key, DecoderCallback decode); @override String toString() => '$runtimeType()'; @@ -444,9 +464,9 @@ /// Converts a key into an [ImageStreamCompleter], and begins fetching the /// image using [loadAsync]. @override - ImageStreamCompleter load(AssetBundleImageKey key) { + ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( - codec: _loadAsync(key), + codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { yield DiagnosticsProperty<ImageProvider>('Image provider', this); @@ -460,11 +480,82 @@ /// /// This function is used by [load]. @protected - Future<ui.Codec> _loadAsync(AssetBundleImageKey key) async { + Future<ui.Codec> _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async { final ByteData data = await key.bundle.load(key.name); if (data == null) throw 'Unable to read data'; - return await PaintingBinding.instance.instantiateImageCodec(data.buffer.asUint8List()); + return await decode(data.buffer.asUint8List()); + } +} + +class _SizeAwareCacheKey { + const _SizeAwareCacheKey(this.providerCacheKey, this.width, this.height); + + final Object providerCacheKey; + + final int width; + + final int height; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) + return false; + final _SizeAwareCacheKey typedOther = other; + return providerCacheKey == typedOther.providerCacheKey + && width == typedOther.width + && height == typedOther.height; + } + + @override + int get hashCode => hashValues(providerCacheKey, width, height); +} + +/// Instructs Flutter to decode the image at the specified dimensions +/// instead of at its native size. +/// +/// This allows finer control of the size of the image in [ImageCache] and is +/// generally used to reduce the memory footprint of [ImageCache]. +/// +/// The decoded image may still be displayed at sizes other than the +/// cached size provided here. +class ResizeImage extends ImageProvider<_SizeAwareCacheKey> { + /// Creates an ImageProvider that decodes the image to the specified size. + /// + /// The cached image will be directly decoded and stored at the resolution + /// defined by `width` and `height`. The image will lose detail and + /// use less memory if resized to a size smaller than the native size. + const ResizeImage( + this.imageProvider, { + this.width, + this.height, + }) : assert(width != null || height != null); + + /// The [ImageProvider] that this class wraps. + final ImageProvider imageProvider; + + /// The width the image should decode to and cache. + final int width; + + /// The height the image should decode to and cache. + final int height; + + @override + ImageStreamCompleter load(_SizeAwareCacheKey key, DecoderCallback decode) { + final DecoderCallback decodeResize = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + assert( + cacheWidth == null && cacheHeight == null, + 'ResizeImage cannot be composed with another ImageProvider that applies cacheWidth or cacheHeight.' + ); + return decode(bytes, cacheWidth: width, cacheHeight: height); + }; + return imageProvider.load(key.providerCacheKey, decodeResize); + } + + @override + Future<_SizeAwareCacheKey> obtainKey(ImageConfiguration configuration) async { + final Object providerCacheKey = await imageProvider.obtainKey(configuration); + return _SizeAwareCacheKey(providerCacheKey, width, height); } } @@ -472,6 +563,11 @@ /// /// The image will be cached regardless of cache headers from the server. /// +/// When a network image is used on the Web platform, the [cacheWidth] and +/// [cacheHeight] parameters of the [DecoderCallback] are ignored as the Web +/// engine delegates image decoding of network images to the Web, which does +/// not support custom decode sizes. +/// /// See also: /// /// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage]. @@ -496,7 +592,7 @@ Map<String, String> get headers; @override - ImageStreamCompleter load(NetworkImage key); + ImageStreamCompleter load(NetworkImage key, DecoderCallback decode); } /// Decodes the given [File] object as an image, associating it with the given @@ -525,9 +621,9 @@ } @override - ImageStreamCompleter load(FileImage key) { + ImageStreamCompleter load(FileImage key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( - codec: _loadAsync(key), + codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { yield ErrorDescription('Path: ${file?.path}'); @@ -535,14 +631,14 @@ ); } - Future<ui.Codec> _loadAsync(FileImage key) async { + Future<ui.Codec> _loadAsync(FileImage key, DecoderCallback decode) async { assert(key == this); final Uint8List bytes = await file.readAsBytes(); if (bytes.lengthInBytes == 0) return null; - return await PaintingBinding.instance.instantiateImageCodec(bytes); + return await decode(bytes); } @override @@ -593,17 +689,17 @@ } @override - ImageStreamCompleter load(MemoryImage key) { + ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( - codec: _loadAsync(key), + codec: _loadAsync(key, decode), scale: key.scale, ); } - Future<ui.Codec> _loadAsync(MemoryImage key) { + Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) { assert(key == this); - return PaintingBinding.instance.instantiateImageCodec(bytes); + return decode(bytes); } @override
diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart index 9dfa4a5..83f8a9e 100644 --- a/packages/flutter/lib/src/widgets/image.dart +++ b/packages/flutter/lib/src/widgets/image.dart
@@ -55,6 +55,13 @@ ); } +ImageProvider<dynamic> _resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider<dynamic> provider) { + if (cacheWidth != null || cacheHeight != null) { + return ResizeImage(provider, width: cacheWidth, height: cacheHeight); + } + return provider; +} + /// Prefetches an image into the image cache. /// /// Returns a [Future] that will complete when the first image yielded by the @@ -232,6 +239,17 @@ /// ``` /// {@end-tool} /// +/// The [Image.asset], [Image.network], [Image.file], and [Image.memory] +/// constructors allow a custom decode size to be specified through +/// [cacheWidth] and [cacheHeight] parameters. The engine will decode the +/// image to the specified size, which is primarily intended to reduce the +/// memory usage of [ImageCache]. +/// +/// In the case where a network image is used on the Web platform, the +/// [cacheWidth] and [cacheHeight] parameters are ignored as the Web engine +/// delegates image decoding of network images to the Web, which does not support +/// custom decode sizes. +/// /// See also: /// /// * [Icon], which shows an image from a font. @@ -305,6 +323,16 @@ /// [FilterQuality.none] which corresponds to nearest-neighbor. /// /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. + /// + /// If [cacheWidth] or [cacheHeight] are provided, it indicates to the + /// engine that the image should be decoded at the specified size. The image + /// will be rendered to the constraints of the layout or [width] and [height] + /// regardless of these parameters. These parameters are primarily intended + /// to reduce the memory usage of [ImageCache]. + /// + /// In the case where the network image is on the Web platform, the [cacheWidth] + /// and [cacheHeight] parameters are ignored as the web engine delegates + /// image decoding to the web which does not support custom decode sizes. Image.network( String src, { Key key, @@ -325,10 +353,14 @@ this.gaplessPlayback = false, this.filterQuality = FilterQuality.low, Map<String, String> headers, - }) : image = NetworkImage(src, scale: scale, headers: headers), + int cacheWidth, + int cacheHeight, + }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)), assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), + assert(cacheWidth == null || cacheWidth > 0), + assert(cacheHeight == null || cacheHeight > 0), super(key: key); /// Creates a widget that displays an [ImageStream] obtained from a [File]. @@ -349,6 +381,12 @@ /// [FilterQuality.none] which corresponds to nearest-neighbor. /// /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. + /// + /// If [cacheWidth] or [cacheHeight] are provided, it indicates to the + /// engine that the image must be decoded at the specified size. The image + /// will be rendered to the constraints of the layout or [width] and [height] + /// regardless of these parameters. These parameters are primarily intended + /// to reduce the memory usage of [ImageCache]. Image.file( File file, { Key key, @@ -367,12 +405,16 @@ this.matchTextDirection = false, this.gaplessPlayback = false, this.filterQuality = FilterQuality.low, - }) : image = FileImage(file, scale: scale), + int cacheWidth, + int cacheHeight, + }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, FileImage(file, scale: scale)), loadingBuilder = null, assert(alignment != null), assert(repeat != null), assert(filterQuality != null), assert(matchTextDirection != null), + assert(cacheWidth == null || cacheWidth > 0), + assert(cacheHeight == null || cacheHeight > 0), super(key: key); @@ -404,6 +446,12 @@ /// /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. /// + /// If [cacheWidth] or [cacheHeight] are provided, it indicates to the + /// engine that the image must be decoded at the specified size. The image + /// will be rendered to the constraints of the layout or [width] and [height] + /// regardless of these parameters. These parameters are primarily intended + /// to reduce the memory usage of [ImageCache]. + /// /// The [name] and [repeat] arguments must not be null. /// /// Either the [width] and [height] arguments should be specified, or the @@ -520,13 +568,18 @@ this.gaplessPlayback = false, String package, this.filterQuality = FilterQuality.low, - }) : image = scale != null + int cacheWidth, + int cacheHeight, + }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, scale != null ? ExactAssetImage(name, bundle: bundle, scale: scale, package: package) - : AssetImage(name, bundle: bundle, package: package), + : AssetImage(name, bundle: bundle, package: package) + ), loadingBuilder = null, assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), + assert(cacheWidth == null || cacheWidth > 0), + assert(cacheHeight == null || cacheHeight > 0), super(key: key); /// Creates a widget that displays an [ImageStream] obtained from a [Uint8List]. @@ -548,6 +601,12 @@ /// [FilterQuality.none] which corresponds to nearest-neighbor. /// /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. + /// + /// If [cacheWidth] or [cacheHeight] are provided, it indicates to the + /// engine that the image must be decoded at the specified size. The image + /// will be rendered to the constraints of the layout or [width] and [height] + /// regardless of these parameters. These parameters are primarily intended + /// to reduce the memory usage of [ImageCache]. Image.memory( Uint8List bytes, { Key key, @@ -566,11 +625,15 @@ this.matchTextDirection = false, this.gaplessPlayback = false, this.filterQuality = FilterQuality.low, - }) : image = MemoryImage(bytes, scale: scale), + int cacheWidth, + int cacheHeight, + }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale)), loadingBuilder = null, assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), + assert(cacheWidth == null || cacheWidth > 0), + assert(cacheHeight == null || cacheHeight > 0), super(key: key); /// The image to display.
diff --git a/packages/flutter/test/painting/binding_test.dart b/packages/flutter/test/painting/binding_test.dart index ea46e60..d849abd 100644 --- a/packages/flutter/test/painting/binding_test.dart +++ b/packages/flutter/test/painting/binding_test.dart
@@ -19,7 +19,9 @@ final Uint8List bytes = Uint8List.fromList(kTransparentImage); final MemoryImage memoryImage = MemoryImage(bytes); - memoryImage.load(memoryImage); + memoryImage.load(memoryImage, (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + }); expect(binding.instantiateImageCodecCalledCount, 1); }); }
diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart index c5ae1e5..872a8ca 100644 --- a/packages/flutter/test/painting/decoration_test.dart +++ b/packages/flutter/test/painting/decoration_test.dart
@@ -33,7 +33,7 @@ } @override - ImageStreamCompleter load(int key) { + ImageStreamCompleter load(int key, DecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture<ImageInfo>(TestImageInfo(key, image: TestImage(), scale: 1.0)) ); @@ -47,7 +47,7 @@ } @override - ImageStreamCompleter load(int key) { + ImageStreamCompleter load(int key, DecoderCallback decode) { return OneFrameImageStreamCompleter( Future<ImageInfo>.value(TestImageInfo(key)) ); @@ -63,7 +63,7 @@ } @override - ImageStreamCompleter load(DelayedImageProvider key) { + ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) { return OneFrameImageStreamCompleter(_completer.future); }
diff --git a/packages/flutter/test/painting/fake_image_provider.dart b/packages/flutter/test/painting/fake_image_provider.dart index 58ebb1a..a8937e2 100644 --- a/packages/flutter/test/painting/fake_image_provider.dart +++ b/packages/flutter/test/painting/fake_image_provider.dart
@@ -26,7 +26,7 @@ } @override - ImageStreamCompleter load(FakeImageProvider key) { + ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) { assert(key == this); return MultiFrameImageStreamCompleter( codec: SynchronousFuture<ui.Codec>(_codec),
diff --git a/packages/flutter/test/painting/image_cache_test.dart b/packages/flutter/test/painting/image_cache_test.dart index 13b14bf..da8758e 100644 --- a/packages/flutter/test/painting/image_cache_test.dart +++ b/packages/flutter/test/painting/image_cache_test.dart
@@ -133,9 +133,9 @@ test('Returns null if an error is caught resolving an image', () { final ErrorImageProvider errorImage = ErrorImageProvider(); - expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage)), throwsA(isInstanceOf<Error>())); + expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null)), throwsA(isInstanceOf<Error>())); bool caughtError = false; - final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage), onError: (dynamic error, StackTrace stackTrace) { + final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null), onError: (dynamic error, StackTrace stackTrace) { caughtError = true; }); expect(result, null);
diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart index 3571dcf..de638f4 100644 --- a/packages/flutter/test/painting/image_provider_test.dart +++ b/packages/flutter/test/painting/image_provider_test.dart
@@ -18,6 +18,11 @@ import 'mocks_for_image_cache.dart'; void main() { + + final DecoderCallback basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + }; + group(ImageProvider, () { setUpAll(() { TestRenderingFlutterBinding(); // initializes the imageCache @@ -46,7 +51,7 @@ final Uint8List bytes = Uint8List.fromList(kTransparentImage); final MemoryImage imageProvider = MemoryImage(bytes); final ImageStreamCompleter cacheStream = otherCache.putIfAbsent( - imageProvider, () => imageProvider.load(imageProvider), + imageProvider, () => imageProvider.load(imageProvider, basicDecoder), ); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final Completer<void> completer = Completer<void>(); @@ -195,7 +200,7 @@ Future<void> loadNetworkImage() async { final NetworkImage networkImage = NetworkImage(nonconst('foo')); - final ImageStreamCompleter completer = networkImage.load(networkImage); + final ImageStreamCompleter completer = networkImage.load(networkImage, basicDecoder); completer.addListener(ImageStreamListener( (ImageInfo image, bool synchronousCall) { }, onError: (dynamic error, StackTrace stackTrace) { @@ -293,6 +298,83 @@ }, skip: isBrowser); }); }); + + test('ResizeImage resizes to the correct dimensions', () async { + final Uint8List bytes = Uint8List.fromList(kTransparentImage); + final MemoryImage imageProvider = MemoryImage(bytes); + final Size rawImageSize = await _resolveAndGetSize(imageProvider); + expect(rawImageSize, const Size(1, 1)); + + const Size resizeDims = Size(14, 7); + final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round()); + const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims); + final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig); + expect(resizedImageSize, resizeDims); + }); + + test('ResizeImage does not resize when no size is passed', () async { + final Uint8List bytes = Uint8List.fromList(kTransparentImage); + final MemoryImage imageProvider = MemoryImage(bytes); + final Size rawImageSize = await _resolveAndGetSize(imageProvider); + expect(rawImageSize, const Size(1, 1)); + + // Cannot pass in two null arguments for cache dimensions, so will use the regular + // MemoryImage + final MemoryImage resizedImage = MemoryImage(bytes); + final Size resizedImageSize = await _resolveAndGetSize(resizedImage); + expect(resizedImageSize, const Size(1, 1)); + }); + + test('ResizeImage stores values', () async { + final Uint8List bytes = Uint8List.fromList(kTransparentImage); + final MemoryImage memoryImage = MemoryImage(bytes); + final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: 20); + expect(resizeImage.width, 10); + expect(resizeImage.height, 20); + expect(resizeImage.imageProvider, memoryImage); + + expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true); + }); + + test('ResizeImage takes one dim', () async { + final Uint8List bytes = Uint8List.fromList(kTransparentImage); + final MemoryImage memoryImage = MemoryImage(bytes); + final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: null); + expect(resizeImage.width, 10); + expect(resizeImage.height, null); + expect(resizeImage.imageProvider, memoryImage); + + expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true); + }); + + test('ResizeImage forms closure', () async { + final Uint8List bytes = Uint8List.fromList(kTransparentImage); + final MemoryImage memoryImage = MemoryImage(bytes); + final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321); + + final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + expect(cacheWidth, 123); + expect(cacheHeight, 321); + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + }; + + resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode); + }); +} + +Future<Size> _resolveAndGetSize(ImageProvider imageProvider, + {ImageConfiguration configuration = ImageConfiguration.empty}) async { + final ImageStream stream = imageProvider.resolve(configuration); + final Completer<Size> completer = Completer<Size>(); + final ImageStreamListener listener = + ImageStreamListener((ImageInfo image, bool synchronousCall) { + final int height = image.image.height; + final int width = image.image.width; + completer.complete(Size(width.toDouble(), height.toDouble())); + } + ); + stream.addListener(listener); + return await completer.future; } class MockHttpClient extends Mock implements HttpClient {}
diff --git a/packages/flutter/test/painting/image_test_utils.dart b/packages/flutter/test/painting/image_test_utils.dart index bce8798..923fd14 100644 --- a/packages/flutter/test/painting/image_test_utils.dart +++ b/packages/flutter/test/painting/image_test_utils.dart
@@ -31,7 +31,7 @@ } @override - ImageStreamCompleter load(TestImageProvider key) => + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) => OneFrameImageStreamCompleter(_completer.future); ImageInfo complete() {
diff --git a/packages/flutter/test/painting/mocks_for_image_cache.dart b/packages/flutter/test/painting/mocks_for_image_cache.dart index 656967e..dfc8901 100644 --- a/packages/flutter/test/painting/mocks_for_image_cache.dart +++ b/packages/flutter/test/painting/mocks_for_image_cache.dart
@@ -37,7 +37,7 @@ } @override - ImageStreamCompleter load(int key) { + ImageStreamCompleter load(int key, DecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture<ImageInfo>(TestImageInfo(imageValue, image: image)) ); @@ -51,7 +51,7 @@ const FailingTestImageProvider(int key, int imageValue, { ui.Image image }) : super(key, imageValue, image: image); @override - ImageStreamCompleter load(int key) { + ImageStreamCompleter load(int key, DecoderCallback decode) { return OneFrameImageStreamCompleter(Future<ImageInfo>.sync(() => Future<ImageInfo>.error('loading failed!'))); } } @@ -85,7 +85,7 @@ class ErrorImageProvider extends ImageProvider<ErrorImageProvider> { @override - ImageStreamCompleter load(ErrorImageProvider key) { + ImageStreamCompleter load(ErrorImageProvider key, DecoderCallback decode) { throw Error(); } @@ -97,7 +97,7 @@ class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvider> { @override - ImageStreamCompleter load(ObtainKeyErrorImageProvider key) { + ImageStreamCompleter load(ObtainKeyErrorImageProvider key, DecoderCallback decode) { throw Error(); } @@ -109,7 +109,7 @@ class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> { @override - ImageStreamCompleter load(LoadErrorImageProvider key) { + ImageStreamCompleter load(LoadErrorImageProvider key, DecoderCallback decode) { throw Error(); } @@ -121,7 +121,7 @@ class LoadErrorCompleterImageProvider extends ImageProvider<LoadErrorCompleterImageProvider> { @override - ImageStreamCompleter load(LoadErrorCompleterImageProvider key) { + ImageStreamCompleter load(LoadErrorCompleterImageProvider key, DecoderCallback decode) { final Completer<void> completer = Completer<void>.sync(); completer.completeError(Error()); return OneFrameImageStreamCompleter(completer.future);
diff --git a/packages/flutter/test/painting/painting_utils.dart b/packages/flutter/test/painting/painting_utils.dart index 378fb0d..ba6f953 100644 --- a/packages/flutter/test/painting/painting_utils.dart +++ b/packages/flutter/test/painting/painting_utils.dart
@@ -14,7 +14,7 @@ int get instantiateImageCodecCalledCount => counter; @override - Future<ui.Codec> instantiateImageCodec(Uint8List list) { + Future<ui.Codec> instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight}) { counter++; return ui.instantiateImageCodec(list); }
diff --git a/packages/flutter/test/painting/shape_decoration_test.dart b/packages/flutter/test/painting/shape_decoration_test.dart index dbdcb62..c3f02d8 100644 --- a/packages/flutter/test/painting/shape_decoration_test.dart +++ b/packages/flutter/test/painting/shape_decoration_test.dart
@@ -107,7 +107,7 @@ } @override - ImageStreamCompleter load(TestImageProvider key) { + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage(), scale: 1.0)), );
diff --git a/packages/flutter/test/widgets/box_decoration_test.dart b/packages/flutter/test/widgets/box_decoration_test.dart index 110fd3d..be7cd3e 100644 --- a/packages/flutter/test/widgets/box_decoration_test.dart +++ b/packages/flutter/test/widgets/box_decoration_test.dart
@@ -28,7 +28,7 @@ } @override - ImageStreamCompleter load(TestImageProvider key) { + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { return OneFrameImageStreamCompleter( future.then<ImageInfo>((void value) => ImageInfo(image: image)) );
diff --git a/packages/flutter/test/widgets/image_data.dart b/packages/flutter/test/widgets/image_data.dart new file mode 100644 index 0000000..83a83af --- /dev/null +++ b/packages/flutter/test/widgets/image_data.dart
@@ -0,0 +1,25 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const List<int> kTransparentImage = <int>[ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, + 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, +]; + +/// An animated GIF image with 3 1x1 pixel frames (a red, green, and blue +/// frames). The gif animates forever, and each frame has a 100ms delay. +const List<int> kAnimatedGif = <int> [ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xa1, 0x03, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x21, + 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, + 0x03, 0x01, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c, + 0x01, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x54, 0x01, 0x00, 0x21, + 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b, +];
diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart index ebf2db3..4508e6d 100644 --- a/packages/flutter/test/widgets/image_resolution_test.dart +++ b/packages/flutter/test/widgets/image_resolution_test.dart
@@ -13,6 +13,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'image_data.dart'; + class TestImage implements ui.Image { TestImage(this.scale); final double scale; @@ -104,7 +106,7 @@ TestAssetImage(String name) : super(name); @override - ImageStreamCompleter load(AssetBundleImageKey key) { + ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) { ImageInfo imageInfo; key.bundle.load(key.name).then<void>((ByteData data) { final TestByteData testData = data; @@ -150,6 +152,30 @@ ); } +Widget buildImageCacheResized(String name, Key key, int width, int height, int cacheWidth, int cacheHeight) { + return Center( + child: RepaintBoundary( + child: Container( + width: 250, + height: 250, + child: Center( + child: Image.memory( + Uint8List.fromList(kTransparentImage), + key: key, + excludeFromSemantics: true, + color: const Color(0xFF00FFFF), + colorBlendMode: BlendMode.plus, + width: width.toDouble(), + height: height.toDouble(), + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ), + ), + ), + ), + ); +} + RenderImage getRenderImage(WidgetTester tester, Key key) { return tester.renderObject<RenderImage>(find.byKey(key)); } @@ -303,4 +329,22 @@ expect(getTestImage(tester, key).scale, 10.0); }); + testWidgets('Image cache resize upscale display 5', (WidgetTester tester) async { + final Key key = GlobalKey(); + await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 20, 20)); + expect(getRenderImage(tester, key).size, const Size(5.0, 5.0)); + }); + + testWidgets('Image cache resize upscale display 50', (WidgetTester tester) async { + final Key key = GlobalKey(); + await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 50, 50, 20, 20)); + expect(getRenderImage(tester, key).size, const Size(50.0, 50.0)); + }); + + testWidgets('Image cache resize downscale display 5', (WidgetTester tester) async { + final Key key = GlobalKey(); + await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 1, 1)); + expect(getRenderImage(tester, key).size, const Size(5.0, 5.0)); + }); + }
diff --git a/packages/flutter/test/widgets/image_rtl_test.dart b/packages/flutter/test/widgets/image_rtl_test.dart index c2e6daf..cd7222e 100644 --- a/packages/flutter/test/widgets/image_rtl_test.dart +++ b/packages/flutter/test/widgets/image_rtl_test.dart
@@ -19,7 +19,7 @@ } @override - ImageStreamCompleter load(TestImageProvider key) { + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage())) );
diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart index da2a2f0..5a400fe 100644 --- a/packages/flutter/test/widgets/image_test.dart +++ b/packages/flutter/test/widgets/image_test.dart
@@ -1199,7 +1199,7 @@ } @override - ImageStreamCompleter load(TestImageProvider key) => _streamCompleter; + ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) => _streamCompleter; void complete() { _completer.complete(ImageInfo(image: TestImage()));