Unverified Commit e8fa87e3 authored by Dan Field's avatar Dan Field Committed by GitHub

Make upscaling images opt-in (#59856)

* Make upscaling images opt-in
parent 65550d0f
......@@ -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<ui.Codec> 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,
);
}
......
......@@ -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<ui.Codec> Function(Uint8List bytes, {int cacheWidth, int cacheHeight});
/// * [ResizeImage], which uses this to override the `cacheWidth`,
/// `cacheHeight`, and `allowUpscaling` parameters.
typedef DecoderCallback = Future<ui.Codec> 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);
}
......
......@@ -4,6 +4,21 @@
// @dart = 2.8
/// A 50x50 blue square png.
const List<int> kBlueSquare = <int>[
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<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,
......
......@@ -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;
......
......@@ -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;
......
......@@ -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);
......
......@@ -17,7 +17,7 @@ class PaintingBindingSpy extends BindingBase with SchedulerBinding, ServicesBind
int get instantiateImageCodecCalledCount => counter;
@override
Future<ui.Codec> instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight}) {
Future<ui.Codec> instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight, bool allowUpscaling = false}) {
counter++;
return ui.instantiateImageCodec(list);
}
......
......@@ -323,11 +323,12 @@ Future<void> 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<ResizeImage>());
......@@ -345,9 +346,10 @@ Future<void> 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);
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment