Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 1e4e11a

Browse files
authored
Add more flexible image loading API (#38905)
* Add more flexible image loading API This adds a new `instantiateImageCodecWithSize` method, which can be used to decode an image into a size, where the target size isn't known until the caller is allowed to inspect the image descriptor. This enables the use case of loading an image whose aspect ratio isn't known at load time, and which needs to be resized to fit within a bounding box while also maintaining its original aspect ratio. flutter/flutter#118543 * Add test * Fixed test failure * Update * Respond to review comments * Add web implementation * Fixed typo * Review comments Also changed the TargetImageSizeCallback to just take intrinsic width & height, rather than the full image descriptor. * Forgot to remove the _SizeOnlyImageDescriptor class - it's no longer needed * Forgot to update test
1 parent b3da52d commit 1e4e11a

File tree

3 files changed

+198
-12
lines changed

3 files changed

+198
-12
lines changed

lib/ui/painting.dart

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,6 +2124,10 @@ Future<Codec> instantiateImageCodec(
21242124
/// The data can be for either static or animated images. The following image
21252125
/// formats are supported: {@macro dart.ui.imageFormats}
21262126
///
2127+
/// The [buffer] will be disposed by this method once the codec has been created,
2128+
/// so the caller must relinquish ownership of the [buffer] when they call this
2129+
/// method.
2130+
///
21272131
/// The [targetWidth] and [targetHeight] arguments specify the size of the
21282132
/// output image, in image pixels. If they are not equal to the intrinsic
21292133
/// dimensions of the image, then the image will be scaled after being decoded.
@@ -2141,26 +2145,145 @@ Future<Codec> instantiateImageCodec(
21412145
///
21422146
/// The returned future can complete with an error if the image decoding has
21432147
/// failed.
2148+
///
2149+
/// ## Compatibility note on the web
2150+
///
2151+
/// When running Flutter on the web, only the CanvasKit renderer supports image
2152+
/// resizing capabilities (not the HTML renderer). So if image resizing is
2153+
/// critical to your use case, and you're deploying to the web, you should
2154+
/// build using the CanvasKit renderer.
21442155
Future<Codec> instantiateImageCodecFromBuffer(
21452156
ImmutableBuffer buffer, {
21462157
int? targetWidth,
21472158
int? targetHeight,
21482159
bool allowUpscaling = true,
2160+
}) {
2161+
return instantiateImageCodecWithSize(
2162+
buffer,
2163+
getTargetSize: (int intrinsicWidth, int intrinsicHeight) {
2164+
if (!allowUpscaling) {
2165+
if (targetWidth != null && targetWidth! > intrinsicWidth) {
2166+
targetWidth = intrinsicWidth;
2167+
}
2168+
if (targetHeight != null && targetHeight! > intrinsicHeight) {
2169+
targetHeight = intrinsicHeight;
2170+
}
2171+
}
2172+
return TargetImageSize(width: targetWidth, height: targetHeight);
2173+
},
2174+
);
2175+
}
2176+
2177+
/// Instantiates an image [Codec].
2178+
///
2179+
/// This method is a convenience wrapper around the [ImageDescriptor] API.
2180+
///
2181+
/// The [buffer] parameter is the binary image data (e.g a PNG or GIF binary
2182+
/// data). The data can be for either static or animated images. The following
2183+
/// image formats are supported: {@macro dart.ui.imageFormats}
2184+
///
2185+
/// The [buffer] will be disposed by this method once the codec has been
2186+
/// created, so the caller must relinquish ownership of the [buffer] when they
2187+
/// call this method.
2188+
///
2189+
/// The [getTargetSize] parameter, when specified, will be invoked and passed
2190+
/// the image's intrinsic size to determine the size to decode the image to.
2191+
/// The width and the height of the size it returns must be positive values
2192+
/// greater than or equal to 1, or null. It is valid to return a
2193+
/// [TargetImageSize] that specifies only one of `width` and `height` with the
2194+
/// other remaining null, in which case the omitted dimension will be scaled to
2195+
/// maintain the aspect ratio of the original dimensions. When both are null or
2196+
/// omitted, the image will be decoded at its native resolution (as will be the
2197+
/// case if the [getTargetSize] parameter is omitted).
2198+
///
2199+
/// Scaling the image to larger than its intrinsic size should usually be
2200+
/// avoided, since it causes the image to use more memory than necessary.
2201+
/// Instead, prefer scaling the [Canvas] transform.
2202+
///
2203+
/// The returned future can complete with an error if the image decoding has
2204+
/// failed.
2205+
///
2206+
/// ## Compatibility note on the web
2207+
///
2208+
/// When running Flutter on the web, only the CanvasKit renderer supports image
2209+
/// resizing capabilities (not the HTML renderer). So if image resizing is
2210+
/// critical to your use case, and you're deploying to the web, you should
2211+
/// build using the CanvasKit renderer.
2212+
Future<Codec> instantiateImageCodecWithSize(
2213+
ImmutableBuffer buffer, {
2214+
TargetImageSizeCallback? getTargetSize,
21492215
}) async {
2216+
getTargetSize ??= _getDefaultImageSize;
21502217
final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);
2151-
if (!allowUpscaling) {
2152-
if (targetWidth != null && targetWidth > descriptor.width) {
2153-
targetWidth = descriptor.width;
2154-
}
2155-
if (targetHeight != null && targetHeight > descriptor.height) {
2156-
targetHeight = descriptor.height;
2157-
}
2218+
try {
2219+
final TargetImageSize targetSize = getTargetSize(descriptor.width, descriptor.height);
2220+
assert(targetSize.width == null || targetSize.width! > 0);
2221+
assert(targetSize.height == null || targetSize.height! > 0);
2222+
return descriptor.instantiateCodec(
2223+
targetWidth: targetSize.width,
2224+
targetHeight: targetSize.height,
2225+
);
2226+
} finally {
2227+
buffer.dispose();
21582228
}
2159-
buffer.dispose();
2160-
return descriptor.instantiateCodec(
2161-
targetWidth: targetWidth,
2162-
targetHeight: targetHeight,
2163-
);
2229+
}
2230+
2231+
TargetImageSize _getDefaultImageSize(int intrinsicWidth, int intrinsicHeight) {
2232+
return const TargetImageSize();
2233+
}
2234+
2235+
/// Signature for a callback that determines the size to which an image should
2236+
/// be decoded given its intrinsic size.
2237+
///
2238+
/// See also:
2239+
///
2240+
/// * [instantiateImageCodecWithSize], which used this signature for its
2241+
/// `getTargetSize` argument.
2242+
typedef TargetImageSizeCallback = TargetImageSize Function(
2243+
int intrinsicWidth,
2244+
int intrinsicHeight,
2245+
);
2246+
2247+
/// A specification of the size to which an image should be decoded.
2248+
///
2249+
/// See also:
2250+
///
2251+
/// * [TargetImageSizeCallback], a callback that returns instances of this
2252+
/// class when consulted by image decoding methods such as
2253+
/// [instantiateImageCodecWithSize].
2254+
class TargetImageSize {
2255+
/// Creates a new instance of this class.
2256+
///
2257+
/// The `width` and `height` may both be null, but if they're non-null, they
2258+
/// must be positive.
2259+
const TargetImageSize({this.width, this.height})
2260+
: assert(width == null || width > 0),
2261+
assert(height == null || height > 0);
2262+
2263+
/// The width into which to load the image.
2264+
///
2265+
/// If this is non-null, the image will be decoded into the specified width.
2266+
/// If this is null and [height] is also null, the image will be decoded into
2267+
/// its intrinsic size. If this is null and [height] is non-null, the image
2268+
/// will be decoded into a width that maintains its intrinsic aspect ratio
2269+
/// while respecting the [height] value.
2270+
///
2271+
/// If this value is non-null, it must be positive.
2272+
final int? width;
2273+
2274+
/// The height into which to load the image.
2275+
///
2276+
/// If this is non-null, the image will be decoded into the specified height.
2277+
/// If this is null and [width] is also null, the image will be decoded into
2278+
/// its intrinsic size. If this is null and [width] is non-null, the image
2279+
/// will be decoded into a height that maintains its intrinsic aspect ratio
2280+
/// while respecting the [width] value.
2281+
///
2282+
/// If this value is non-null, it must be positive.
2283+
final int? height;
2284+
2285+
@override
2286+
String toString() => 'TargetImageSize($width x $height)';
21642287
}
21652288

21662289
/// Loads a single image frame from a byte array into an [Image] object.

lib/web_ui/lib/painting.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,42 @@ Future<Codec> instantiateImageCodecFromBuffer(
491491
targetHeight: targetHeight,
492492
allowUpscaling: allowUpscaling);
493493

494+
Future<Codec> instantiateImageCodecWithSize(
495+
ImmutableBuffer buffer, {
496+
TargetImageSizeCallback? getTargetSize,
497+
}) async {
498+
if (getTargetSize == null) {
499+
return engine.renderer.instantiateImageCodec(buffer._list!);
500+
} else {
501+
final Codec codec = await engine.renderer.instantiateImageCodec(buffer._list!);
502+
try {
503+
final FrameInfo info = await codec.getNextFrame();
504+
try {
505+
final int width = info.image.width;
506+
final int height = info.image.height;
507+
final TargetImageSize targetSize = getTargetSize(width, height);
508+
return engine.renderer.instantiateImageCodec(buffer._list!,
509+
targetWidth: targetSize.width, targetHeight: targetSize.height, allowUpscaling: false);
510+
} finally {
511+
info.image.dispose();
512+
}
513+
} finally {
514+
codec.dispose();
515+
}
516+
}
517+
}
518+
519+
typedef TargetImageSizeCallback = TargetImageSize Function(int intrinsicWidth, int intrinsicHeight);
520+
521+
class TargetImageSize {
522+
const TargetImageSize({this.width, this.height})
523+
: assert(width == null || width > 0),
524+
assert(height == null || height > 0);
525+
526+
final int? width;
527+
final int? height;
528+
}
529+
494530
Future<Codec> webOnlyInstantiateImageCodecFromUrl(Uri uri,
495531
{engine.WebOnlyImageCodecChunkCallback? chunkCallback}) =>
496532
engine.renderer.instantiateImageCodecFromUrl(uri, chunkCallback: chunkCallback);

testing/dart/codec_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,33 @@ void main() {
8686
]));
8787
});
8888

89+
test('with size', () async {
90+
final Uint8List data = await _getSkiaResource('baby_tux.png').readAsBytes();
91+
final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(data);
92+
final ui.Codec codec = await ui.instantiateImageCodecWithSize(
93+
buffer,
94+
getTargetSize: (int intrinsicWidth, int intrinsicHeight) {
95+
return ui.TargetImageSize(
96+
width: intrinsicWidth ~/ 2,
97+
height: intrinsicHeight ~/ 2,
98+
);
99+
},
100+
);
101+
final List<List<int>> decodedFrameInfos = <List<int>>[];
102+
for (int i = 0; i < 2; i++) {
103+
final ui.FrameInfo frameInfo = await codec.getNextFrame();
104+
decodedFrameInfos.add(<int>[
105+
frameInfo.duration.inMilliseconds,
106+
frameInfo.image.width,
107+
frameInfo.image.height,
108+
]);
109+
}
110+
expect(decodedFrameInfos, equals(<List<int>>[
111+
<int>[0, 120, 123],
112+
<int>[0, 120, 123],
113+
]));
114+
});
115+
89116
test('disposed decoded image', () async {
90117
final Uint8List data = await _getSkiaResource('flutter_logo.jpg').readAsBytes();
91118
final ui.Codec codec = await ui.instantiateImageCodec(data);

0 commit comments

Comments
 (0)