diff --git a/packages/flutter/lib/src/painting/binding.dart b/packages/flutter/lib/src/painting/binding.dart index 47c968c95e..9b578cdb2b 100644 --- a/packages/flutter/lib/src/painting/binding.dart +++ b/packages/flutter/lib/src/painting/binding.dart @@ -71,26 +71,37 @@ mixin PaintingBinding on BindingBase, ServicesBinding { @protected ImageCache createImageCache() => ImageCache(); - /// Calls through to [dart:ui] with [decodedCacheRatioCap] from [ImageCache]. + /// Calls through to [dart:ui] from [ImageCache]. /// - /// The [cacheWidth] and [cacheHeight] parameters, when specified, indicate the - /// size to decode the image to. + /// 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. + /// 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 instantiateImageCodec(Uint8List bytes, { int cacheWidth, int cacheHeight, + bool allowUpscaling = false, }) { assert(cacheWidth == null || cacheWidth > 0); assert(cacheHeight == null || cacheHeight > 0); + assert(allowUpscaling != null); return ui.instantiateImageCodec( bytes, targetWidth: cacheWidth, targetHeight: cacheHeight, + allowUpscaling: allowUpscaling, ); } diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index 370f854063..0d7646f5fd 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart @@ -162,13 +162,15 @@ class ImageConfiguration { /// 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. +/// 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` and `cacheHeight` parameters. -typedef DecoderCallback = Future Function(Uint8List bytes, {int cacheWidth, int cacheHeight}); +/// * [ResizeImage], which uses this to override the `cacheWidth`, +/// `cacheHeight`, and `allowUpscaling` parameters. +typedef DecoderCallback = Future Function(Uint8List bytes, {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 @@ -718,7 +720,9 @@ class ResizeImage extends ImageProvider<_SizeAwareCacheKey> { this.imageProvider, { this.width, this.height, - }) : assert(width != null || height != null); + this.allowUpscaling = false, + }) : assert(width != null || height != null), + assert(allowUpscaling != null); /// The [ImageProvider] that this class wraps. final ImageProvider imageProvider; @@ -729,6 +733,15 @@ class ResizeImage extends ImageProvider<_SizeAwareCacheKey> { /// The height the image should decode to and cache. final int height; + /// Whether the [width] and [height] parameters should be clamped to the + /// intrinsic width and height of the image. + /// + /// In general, it is better for memory usage to avoid scaling the image + /// beyond its intrinsic dimensions when decoding it. If there is a need to + /// scale an image larger, it is better to apply a scale to the canvas, or + /// to use an appropriate [Image.fit]. + final bool allowUpscaling; + /// Composes the `provider` in a [ResizeImage] only when `cacheWidth` and /// `cacheHeight` are not both null. /// @@ -743,12 +756,13 @@ class ResizeImage extends ImageProvider<_SizeAwareCacheKey> { @override ImageStreamCompleter load(_SizeAwareCacheKey key, DecoderCallback decode) { - final DecoderCallback decodeResize = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + final DecoderCallback decodeResize = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) { assert( - cacheWidth == null && cacheHeight == null, - 'ResizeImage cannot be composed with another ImageProvider that applies cacheWidth or cacheHeight.' + 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); + return decode(bytes, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling); }; return imageProvider.load(key.providerCacheKey, decodeResize); } diff --git a/packages/flutter/test/painting/image_data.dart b/packages/flutter/test/painting/image_data.dart index fc36144464..37359438dc 100644 --- a/packages/flutter/test/painting/image_data.dart +++ b/packages/flutter/test/painting/image_data.dart @@ -4,6 +4,21 @@ // @dart = 2.8 + +/// A 50x50 blue square png. +const List kBlueSquare = [ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1e, 0x3f, 0x88, 0xb1, 0x00, 0x00, 0x00, 0x48, 0x49, 0x44, + 0x41, 0x54, 0x78, 0xda, 0xed, 0xcf, 0x31, 0x0d, 0x00, 0x30, 0x08, 0x00, 0xb0, + 0x61, 0x63, 0x2f, 0xfe, 0x2d, 0x61, 0x05, 0x34, 0xf0, 0x92, 0xd6, 0x41, 0x23, + 0x7f, 0xf5, 0x3b, 0x20, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, + 0x44, 0x44, 0x44, 0x36, 0x06, 0x03, 0x6e, 0x69, 0x47, 0x12, 0x8e, 0xea, 0xaa, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, +]; + const List kTransparentImage = [ 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, 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 07e87b4873..6195f99a46 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 @@ -19,8 +19,8 @@ import 'mocks_for_image_cache.dart'; void main() { TestRenderingFlutterBinding(); - final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) { + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false); }; FlutterExceptionHandler oldError; 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 d350296c61..1b978d8d98 100644 --- a/packages/flutter/test/painting/image_provider_network_image_test.dart +++ b/packages/flutter/test/painting/image_provider_network_image_test.dart @@ -20,8 +20,8 @@ import 'image_data.dart'; void main() { TestRenderingFlutterBinding(); - final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) { + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling); }; _MockHttpClient httpClient; 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 675b32eebf..29afff69ed 100644 --- a/packages/flutter/test/painting/image_provider_resize_image_test.dart +++ b/packages/flutter/test/painting/image_provider_resize_image_test.dart @@ -21,13 +21,40 @@ void main() { PaintingBinding.instance.imageCache.clearLiveImages(); }); - test('ResizeImage resizes to the correct dimensions', () async { + test('ResizeImage resizes to the correct dimensions (up)', () 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(), allowUpscaling: true); + const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims); + final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig); + expect(resizedImageSize, resizeDims); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56312 + + + test('ResizeImage resizes to the correct dimensions (down)', () async { + final Uint8List bytes = Uint8List.fromList(kBlueSquare); + final MemoryImage imageProvider = MemoryImage(bytes); + final Size rawImageSize = await _resolveAndGetSize(imageProvider); + expect(rawImageSize, const Size(50, 50)); + + const Size resizeDims = Size(25, 25); + final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round(), allowUpscaling: true); + const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims); + final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig); + expect(resizedImageSize, resizeDims); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56312 + + test('ResizeImage resizes to the correct dimensions - no upscaling', () 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(1, 1); 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); @@ -73,10 +100,11 @@ void main() { final MemoryImage memoryImage = MemoryImage(bytes); final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321); - final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) { expect(cacheWidth, 123); expect(cacheHeight, 321); - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + expect(allowUpscaling, false); + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling); }; resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode); diff --git a/packages/flutter/test/painting/painting_utils.dart b/packages/flutter/test/painting/painting_utils.dart index 28db994f7a..fcd1f4748c 100644 --- a/packages/flutter/test/painting/painting_utils.dart +++ b/packages/flutter/test/painting/painting_utils.dart @@ -17,7 +17,7 @@ class PaintingBindingSpy extends BindingBase with SchedulerBinding, ServicesBind int get instantiateImageCodecCalledCount => counter; @override - Future instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight}) { + Future instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight, bool allowUpscaling = false}) { counter++; return ui.instantiateImageCodec(list); } diff --git a/packages/flutter/test/widgets/fade_in_image_test.dart b/packages/flutter/test/widgets/fade_in_image_test.dart index a8103e7080..c9d430fa35 100644 --- a/packages/flutter/test/widgets/fade_in_image_test.dart +++ b/packages/flutter/test/widgets/fade_in_image_test.dart @@ -323,11 +323,12 @@ Future main() async { ); bool called = false; - final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) { expect(cacheWidth, 20); expect(cacheHeight, 30); + expect(allowUpscaling, false); called = true; - return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); + return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling); }; final ImageProvider resizeImage = image.placeholder; expect(image.placeholder, isA()); @@ -345,9 +346,10 @@ Future main() async { ); bool called = false; - final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) { + final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) { expect(cacheWidth, null); expect(cacheHeight, null); + expect(allowUpscaling, null); called = true; return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight); };