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

Add more flexible image loading API #38905

Merged
merged 9 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 135 additions & 12 deletions lib/ui/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2124,6 +2124,10 @@ Future<Codec> instantiateImageCodec(
/// The data can be for either static or animated images. The following image
/// formats are supported: {@macro dart.ui.imageFormats}
///
/// The [buffer] will be disposed by this method once the codec has been created,
/// so the caller must relinquish ownership of the [buffer] when they call this
/// method.
///
/// The [targetWidth] and [targetHeight] arguments specify the size of the
/// output image, in image pixels. If they are not equal to the intrinsic
/// dimensions of the image, then the image will be scaled after being decoded.
Expand All @@ -2141,26 +2145,145 @@ Future<Codec> instantiateImageCodec(
///
/// The returned future can complete with an error if the image decoding has
/// failed.
///
/// ## Compatibility note on the web
///
/// When running Flutter on the web, only the CanvasKit renderer supports image
/// resizing capabilities (not the HTML renderer). So if image resizing is
/// critical to your use case, and you're deploying to the web, you should
/// build using the CanvasKit renderer.
Future<Codec> instantiateImageCodecFromBuffer(
ImmutableBuffer buffer, {
int? targetWidth,
int? targetHeight,
bool allowUpscaling = true,
}) {
return instantiateImageCodecWithSize(
buffer,
getTargetSize: (int intrinsicWidth, int intrinsicHeight) {
if (!allowUpscaling) {
if (targetWidth != null && targetWidth! > intrinsicWidth) {
targetWidth = intrinsicWidth;
}
if (targetHeight != null && targetHeight! > intrinsicHeight) {
targetHeight = intrinsicHeight;
}
}
return TargetImageSize(width: targetWidth, height: targetHeight);
},
);
}

/// Instantiates an image [Codec].
///
/// This method is a convenience wrapper around the [ImageDescriptor] API.
///
/// The [buffer] parameter is the binary image data (e.g a PNG or GIF binary
/// data). The data can be for either static or animated images. The following
/// image formats are supported: {@macro dart.ui.imageFormats}
///
/// The [buffer] will be disposed by this method once the codec has been
/// created, so the caller must relinquish ownership of the [buffer] when they
/// call this method.
///
/// The [getTargetSize] parameter, when specified, will be invoked and passed
/// the image's intrinsic size to determine the size to decode the image to.
/// The width and the height of the size it returns must be positive values
/// greater than or equal to 1, or null. It is valid to return a
/// [TargetImageSize] that specifies only one of `width` and `height` 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 (as will be the
/// case if the [getTargetSize] parameter is omitted).
///
/// Scaling the image to larger than its intrinsic size should usually be
/// avoided, since it causes the image to use more memory than necessary.
/// Instead, prefer scaling the [Canvas] transform.
///
/// The returned future can complete with an error if the image decoding has
/// failed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe note here that image resizing capabilities are only available with the canvaskit renderer and not html renderer (we should probably update the api docs for instantiateImageCodec to note this too)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

///
/// ## Compatibility note on the web
///
/// When running Flutter on the web, only the CanvasKit renderer supports image
/// resizing capabilities (not the HTML renderer). So if image resizing is
/// critical to your use case, and you're deploying to the web, you should
/// build using the CanvasKit renderer.
Future<Codec> instantiateImageCodecWithSize(
ImmutableBuffer buffer, {
TargetImageSizeCallback? getTargetSize,
}) async {
getTargetSize ??= _getDefaultImageSize;
final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);
if (!allowUpscaling) {
if (targetWidth != null && targetWidth > descriptor.width) {
targetWidth = descriptor.width;
}
if (targetHeight != null && targetHeight > descriptor.height) {
targetHeight = descriptor.height;
}
try {
final TargetImageSize targetSize = getTargetSize(descriptor.width, descriptor.height);
assert(targetSize.width == null || targetSize.width! > 0);
assert(targetSize.height == null || targetSize.height! > 0);
return descriptor.instantiateCodec(
targetWidth: targetSize.width,
targetHeight: targetSize.height,
);
} finally {
buffer.dispose();
}
buffer.dispose();
return descriptor.instantiateCodec(
targetWidth: targetWidth,
targetHeight: targetHeight,
);
}

TargetImageSize _getDefaultImageSize(int intrinsicWidth, int intrinsicHeight) {
return const TargetImageSize();
}

/// Signature for a callback that determines the size to which an image should
/// be decoded given its intrinsic size.
///
/// See also:
///
/// * [instantiateImageCodecWithSize], which used this signature for its
/// `getTargetSize` argument.
typedef TargetImageSizeCallback = TargetImageSize Function(
int intrinsicWidth,
int intrinsicHeight,
);

/// A specification of the size to which an image should be decoded.
///
/// See also:
///
/// * [TargetImageSizeCallback], a callback that returns instances of this
/// class when consulted by image decoding methods such as
/// [instantiateImageCodecWithSize].
class TargetImageSize {
/// Creates a new instance of this class.
///
/// The `width` and `height` may both be null, but if they're non-null, they
/// must be positive.
const TargetImageSize({this.width, this.height})
: assert(width == null || width > 0),
assert(height == null || height > 0);

/// The width into which to load the image.
///
/// If this is non-null, the image will be decoded into the specified width.
/// If this is null and [height] is also null, the image will be decoded into
/// its intrinsic size. If this is null and [height] is non-null, the image
/// will be decoded into a width that maintains its intrinsic aspect ratio
/// while respecting the [height] value.
///
/// If this value is non-null, it must be positive.
final int? width;

/// The height into which to load the image.
///
/// If this is non-null, the image will be decoded into the specified height.
/// If this is null and [width] is also null, the image will be decoded into
/// its intrinsic size. If this is null and [width] is non-null, the image
/// will be decoded into a height that maintains its intrinsic aspect ratio
/// while respecting the [width] value.
///
/// If this value is non-null, it must be positive.
final int? height;

@override
String toString() => 'TargetImageSize($width x $height)';
}

/// Loads a single image frame from a byte array into an [Image] object.
Expand Down
36 changes: 36 additions & 0 deletions lib/web_ui/lib/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,42 @@ Future<Codec> instantiateImageCodecFromBuffer(
targetHeight: targetHeight,
allowUpscaling: allowUpscaling);

Future<Codec> instantiateImageCodecWithSize(
ImmutableBuffer buffer, {
TargetImageSizeCallback? getTargetSize,
}) async {
if (getTargetSize == null) {
return engine.renderer.instantiateImageCodec(buffer._list!);
} else {
final Codec codec = await engine.renderer.instantiateImageCodec(buffer._list!);
try {
final FrameInfo info = await codec.getNextFrame();
try {
final int width = info.image.width;
final int height = info.image.height;
final TargetImageSize targetSize = getTargetSize(width, height);
return engine.renderer.instantiateImageCodec(buffer._list!,
targetWidth: targetSize.width, targetHeight: targetSize.height, allowUpscaling: false);
} finally {
info.image.dispose();
}
} finally {
codec.dispose();
}
}
}

typedef TargetImageSizeCallback = TargetImageSize Function(int intrinsicWidth, int intrinsicHeight);

class TargetImageSize {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there other places in dart:ui we could use an int-based size? Might make sense to generalise the name so others could use it later, if so.

I can’t think of a great name though. Maybe something like PhysicalSize.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that, but a real PhysicalSize class would want both width & height to be non-null. This is unique in how it allows one or both of those to be null.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More similar to an IntConstraints type, not quite a size. We do really need an ISize type, but probably unrelated.

const TargetImageSize({this.width, this.height})
: assert(width == null || width > 0),
assert(height == null || height > 0);

final int? width;
final int? height;
}

Future<Codec> webOnlyInstantiateImageCodecFromUrl(Uri uri,
{engine.WebOnlyImageCodecChunkCallback? chunkCallback}) =>
engine.renderer.instantiateImageCodecFromUrl(uri, chunkCallback: chunkCallback);
Expand Down
27 changes: 27 additions & 0 deletions testing/dart/codec_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,33 @@ void main() {
]));
});

test('with size', () async {
final Uint8List data = await _getSkiaResource('baby_tux.png').readAsBytes();
final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(data);
final ui.Codec codec = await ui.instantiateImageCodecWithSize(
buffer,
getTargetSize: (int intrinsicWidth, int intrinsicHeight) {
return ui.TargetImageSize(
width: intrinsicWidth ~/ 2,
height: intrinsicHeight ~/ 2,
);
},
);
final List<List<int>> decodedFrameInfos = <List<int>>[];
for (int i = 0; i < 2; i++) {
final ui.FrameInfo frameInfo = await codec.getNextFrame();
decodedFrameInfos.add(<int>[
frameInfo.duration.inMilliseconds,
frameInfo.image.width,
frameInfo.image.height,
]);
}
expect(decodedFrameInfos, equals(<List<int>>[
<int>[0, 120, 123],
<int>[0, 120, 123],
]));
});

test('disposed decoded image', () async {
final Uint8List data = await _getSkiaResource('flutter_logo.jpg').readAsBytes();
final ui.Codec codec = await ui.instantiateImageCodec(data);
Expand Down