Skip to content

Commit 0852c51

Browse files
dnfieldmingwandroid
authored andcommitted
Make upscaling images opt-in (flutter#59856)
* Make upscaling images opt-in
1 parent 6ed5f3d commit 0852c51

8 files changed

Lines changed: 98 additions & 28 deletions

packages/flutter/lib/src/painting/binding.dart

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,26 +71,37 @@ mixin PaintingBinding on BindingBase, ServicesBinding {
7171
@protected
7272
ImageCache createImageCache() => ImageCache();
7373

74-
/// Calls through to [dart:ui] with [decodedCacheRatioCap] from [ImageCache].
74+
/// Calls through to [dart:ui] from [ImageCache].
7575
///
76-
/// The [cacheWidth] and [cacheHeight] parameters, when specified, indicate the
77-
/// size to decode the image to.
76+
/// The `cacheWidth` and `cacheHeight` parameters, when specified, indicate
77+
/// the size to decode the image to.
7878
///
79-
/// Both [cacheWidth] and [cacheHeight] must be positive values greater than or
80-
/// equal to 1 or null. It is valid to specify only one of [cacheWidth] and
81-
/// [cacheHeight] with the other remaining null, in which case the omitted
82-
/// dimension will decode to its original size. When both are null or omitted,
83-
/// the image will be decoded at its native resolution.
79+
/// Both `cacheWidth` and `cacheHeight` must be positive values greater than
80+
/// or equal to 1, or null. It is valid to specify only one of `cacheWidth`
81+
/// and `cacheHeight` with the other remaining null, in which case the omitted
82+
/// dimension will be scaled to maintain the aspect ratio of the original
83+
/// dimensions. When both are null or omitted, the image will be decoded at
84+
/// its native resolution.
85+
///
86+
/// The `allowUpscaling` parameter determines whether the `cacheWidth` or
87+
/// `cacheHeight` parameters are clamped to the intrinsic width and height of
88+
/// the original image. By default, the dimensions are clamped to avoid
89+
/// unnecessary memory usage for images. Callers that wish to display an image
90+
/// above its native resolution should prefer scaling the canvas the image is
91+
/// drawn into.
8492
Future<ui.Codec> instantiateImageCodec(Uint8List bytes, {
8593
int cacheWidth,
8694
int cacheHeight,
95+
bool allowUpscaling = false,
8796
}) {
8897
assert(cacheWidth == null || cacheWidth > 0);
8998
assert(cacheHeight == null || cacheHeight > 0);
99+
assert(allowUpscaling != null);
90100
return ui.instantiateImageCodec(
91101
bytes,
92102
targetWidth: cacheWidth,
93103
targetHeight: cacheHeight,
104+
allowUpscaling: allowUpscaling,
94105
);
95106
}
96107

packages/flutter/lib/src/painting/image_provider.dart

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,15 @@ class ImageConfiguration {
162162

163163
/// Performs the decode process for use in [ImageProvider.load].
164164
///
165-
/// This callback allows decoupling of the `cacheWidth` and `cacheHeight`
166-
/// parameters from implementations of [ImageProvider] that do not use them.
165+
/// This callback allows decoupling of the `cacheWidth`, `cacheHeight`, and
166+
/// `allowUpscaling` parameters from implementations of [ImageProvider] that do
167+
/// not expose them.
167168
///
168169
/// See also:
169170
///
170-
/// * [ResizeImage], which uses this to override the `cacheWidth` and `cacheHeight` parameters.
171-
typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int cacheWidth, int cacheHeight});
171+
/// * [ResizeImage], which uses this to override the `cacheWidth`,
172+
/// `cacheHeight`, and `allowUpscaling` parameters.
173+
typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling});
172174

173175
/// Identifies an image without committing to the precise final asset. This
174176
/// allows a set of images to be identified and for the precise image to later
@@ -718,7 +720,9 @@ class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
718720
this.imageProvider, {
719721
this.width,
720722
this.height,
721-
}) : assert(width != null || height != null);
723+
this.allowUpscaling = false,
724+
}) : assert(width != null || height != null),
725+
assert(allowUpscaling != null);
722726

723727
/// The [ImageProvider] that this class wraps.
724728
final ImageProvider imageProvider;
@@ -729,6 +733,15 @@ class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
729733
/// The height the image should decode to and cache.
730734
final int height;
731735

736+
/// Whether the [width] and [height] parameters should be clamped to the
737+
/// intrinsic width and height of the image.
738+
///
739+
/// In general, it is better for memory usage to avoid scaling the image
740+
/// beyond its intrinsic dimensions when decoding it. If there is a need to
741+
/// scale an image larger, it is better to apply a scale to the canvas, or
742+
/// to use an appropriate [Image.fit].
743+
final bool allowUpscaling;
744+
732745
/// Composes the `provider` in a [ResizeImage] only when `cacheWidth` and
733746
/// `cacheHeight` are not both null.
734747
///
@@ -743,12 +756,13 @@ class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
743756

744757
@override
745758
ImageStreamCompleter load(_SizeAwareCacheKey key, DecoderCallback decode) {
746-
final DecoderCallback decodeResize = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
759+
final DecoderCallback decodeResize = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) {
747760
assert(
748-
cacheWidth == null && cacheHeight == null,
749-
'ResizeImage cannot be composed with another ImageProvider that applies cacheWidth or cacheHeight.'
761+
cacheWidth == null && cacheHeight == null && allowUpscaling == null,
762+
'ResizeImage cannot be composed with another ImageProvider that applies '
763+
'cacheWidth, cacheHeight, or allowUpscaling.'
750764
);
751-
return decode(bytes, cacheWidth: width, cacheHeight: height);
765+
return decode(bytes, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
752766
};
753767
return imageProvider.load(key.providerCacheKey, decodeResize);
754768
}

packages/flutter/test/painting/image_data.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44

55
// @dart = 2.8
66

7+
8+
/// A 50x50 blue square png.
9+
const List<int> kBlueSquare = <int>[
10+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
11+
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x08, 0x06,
12+
0x00, 0x00, 0x00, 0x1e, 0x3f, 0x88, 0xb1, 0x00, 0x00, 0x00, 0x48, 0x49, 0x44,
13+
0x41, 0x54, 0x78, 0xda, 0xed, 0xcf, 0x31, 0x0d, 0x00, 0x30, 0x08, 0x00, 0xb0,
14+
0x61, 0x63, 0x2f, 0xfe, 0x2d, 0x61, 0x05, 0x34, 0xf0, 0x92, 0xd6, 0x41, 0x23,
15+
0x7f, 0xf5, 0x3b, 0x20, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
16+
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
17+
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
18+
0x44, 0x44, 0x44, 0x36, 0x06, 0x03, 0x6e, 0x69, 0x47, 0x12, 0x8e, 0xea, 0xaa,
19+
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
20+
];
21+
722
const List<int> kTransparentImage = <int>[
823
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
924
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,

packages/flutter/test/painting/image_provider_and_image_cache_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import 'mocks_for_image_cache.dart';
1919
void main() {
2020
TestRenderingFlutterBinding();
2121

22-
final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
23-
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
22+
final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) {
23+
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false);
2424
};
2525

2626
FlutterExceptionHandler oldError;

packages/flutter/test/painting/image_provider_network_image_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import 'image_data.dart';
2020
void main() {
2121
TestRenderingFlutterBinding();
2222

23-
final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
24-
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
23+
final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) {
24+
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling);
2525
};
2626

2727
_MockHttpClient httpClient;

packages/flutter/test/painting/image_provider_resize_image_test.dart

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,40 @@ void main() {
2121
PaintingBinding.instance.imageCache.clearLiveImages();
2222
});
2323

24-
test('ResizeImage resizes to the correct dimensions', () async {
24+
test('ResizeImage resizes to the correct dimensions (up)', () async {
2525
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
2626
final MemoryImage imageProvider = MemoryImage(bytes);
2727
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
2828
expect(rawImageSize, const Size(1, 1));
2929

3030
const Size resizeDims = Size(14, 7);
31+
final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round(), allowUpscaling: true);
32+
const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims);
33+
final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig);
34+
expect(resizedImageSize, resizeDims);
35+
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56312
36+
37+
38+
test('ResizeImage resizes to the correct dimensions (down)', () async {
39+
final Uint8List bytes = Uint8List.fromList(kBlueSquare);
40+
final MemoryImage imageProvider = MemoryImage(bytes);
41+
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
42+
expect(rawImageSize, const Size(50, 50));
43+
44+
const Size resizeDims = Size(25, 25);
45+
final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round(), allowUpscaling: true);
46+
const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims);
47+
final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig);
48+
expect(resizedImageSize, resizeDims);
49+
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56312
50+
51+
test('ResizeImage resizes to the correct dimensions - no upscaling', () async {
52+
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
53+
final MemoryImage imageProvider = MemoryImage(bytes);
54+
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
55+
expect(rawImageSize, const Size(1, 1));
56+
57+
const Size resizeDims = Size(1, 1);
3158
final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round());
3259
const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims);
3360
final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig);
@@ -73,10 +100,11 @@ void main() {
73100
final MemoryImage memoryImage = MemoryImage(bytes);
74101
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
75102

76-
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
103+
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) {
77104
expect(cacheWidth, 123);
78105
expect(cacheHeight, 321);
79-
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
106+
expect(allowUpscaling, false);
107+
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling);
80108
};
81109

82110
resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode);

packages/flutter/test/painting/painting_utils.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class PaintingBindingSpy extends BindingBase with SchedulerBinding, ServicesBind
1717
int get instantiateImageCodecCalledCount => counter;
1818

1919
@override
20-
Future<ui.Codec> instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight}) {
20+
Future<ui.Codec> instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight, bool allowUpscaling = false}) {
2121
counter++;
2222
return ui.instantiateImageCodec(list);
2323
}

packages/flutter/test/widgets/fade_in_image_test.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,11 +323,12 @@ Future<void> main() async {
323323
);
324324

325325
bool called = false;
326-
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
326+
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) {
327327
expect(cacheWidth, 20);
328328
expect(cacheHeight, 30);
329+
expect(allowUpscaling, false);
329330
called = true;
330-
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
331+
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling);
331332
};
332333
final ImageProvider resizeImage = image.placeholder;
333334
expect(image.placeholder, isA<ResizeImage>());
@@ -345,9 +346,10 @@ Future<void> main() async {
345346
);
346347

347348
bool called = false;
348-
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
349+
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) {
349350
expect(cacheWidth, null);
350351
expect(cacheHeight, null);
352+
expect(allowUpscaling, null);
351353
called = true;
352354
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
353355
};

0 commit comments

Comments
 (0)