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

Conversation

tvolkert
Copy link
Contributor

@tvolkert tvolkert commented Jan 16, 2023

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

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides].
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See [testing the engine] for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the [CLA].
  • All existing and new tests are passing.

@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@tvolkert tvolkert added Work in progress (WIP) Not ready (yet) for review! and removed Work in progress (WIP) Not ready (yet) for review! needs tests labels Jan 16, 2023
@tvolkert tvolkert requested review from dnfield and yjbanov January 16, 2023 05:33
@tvolkert
Copy link
Contributor Author

@yjbanov I'm wondering how we can support this API in the web engine. Thoughts?

///
/// * [instantiateImageCodecWithSize], which used this signature for its
/// `getTargetSize` argument.
typedef TargetImageSizeProducer = TargetImageSize Function(ImageDescriptor descriptor);
Copy link
Member

Choose a reason for hiding this comment

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

I think we generally use "Callback" as the suffix for function types

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

}) async {
getTargetSize ??= (ImageDescriptor descriptor) => const TargetImageSize();
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using ??= here, create a static function and make it the argument default value:

TargetImageSize _defaultGetTargetSize(ImageDescriptor descriptor) {
  return const TargetImageSize();
}

TargetImageSizeProducer getTargetSize = _defaultGetTargetSize,


Copy link
Contributor Author

Choose a reason for hiding this comment

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

Heh - I did that at first, but then the package:flutter/painting.dart API wrapper has to do something like this:

  Future<ui.Codec> instantiateImageCodecWithSize(
    ui.ImmutableBuffer buffer, {
    ui.TargetImageSizeProducer? getTargetSize,
  }) {
    if (getTargetSize == null) {
      return ui.instantiateImageCodecWithSize(buffer);
    } else {
      return ui.instantiateImageCodecWithSize(buffer, getTargetSize: getTargetSize);
    }
  }

Either that, or the static function has to be public (or worse: copied). After discussing with Bob, he suggested that he frequently resorts to the method I've done here. What do you think given those tradeoffs?

Copy link
Member

Choose a reason for hiding this comment

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

ahh yeah, that makes sense. I think the approach is fine. I would still use a static function instead of creating a new closure on each run though

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

Copy link
Member

@jonahwilliams jonahwilliams left a comment

Choose a reason for hiding this comment

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

I think overall this approach LGTM. How does this look on the web side of things?

@tvolkert
Copy link
Contributor Author

How does this look on the web side of things?

I added a web impl. It's not pretty.... definitely interested in knowing if there's a better way.

final int height;

@override
dynamic noSuchMethod(Invocation invocation) => throw UnsupportedError('ImageDescriptor.${invocation.memberName} is not supported on web within a TargetImageSizeCallback.');
Copy link
Member

Choose a reason for hiding this comment

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

Nooooooooo, can we avoid using noSuchMethod? :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, this can really blow up the JS bundle size.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TIL! I'll update it. Reason I went with this is because it was forcing me to implement all the private methods too, which was a bummer.

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 - I got rid of the need for the ImageDescriptor at all.

@yjbanov yjbanov requested a review from alanwutang11 January 19, 2023 18:57
@jonahwilliams
Copy link
Member

Talked about this a bit offline with @chinmaygarde . I think we can already accomplish this with our existing API, but its not very obvious how to do so. For example, you can set one of targetWidth and targetHeight to a maximum value, but don't set the other one - this will preserve the aspect ratio. Then set allowUpscalling to false, this will prevent the image from being resized to be larger.

Given that, I suspect you could probably implement this API in the framework without any dart:ui changes

/// must be positive.
const TargetImageSize({this.width, this.height})
: assert(width == null || width > 0),
assert(width == null || width > 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think you meant for this to be height

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

Choose a reason for hiding this comment

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

nit: height here too

/// 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

@tvolkert
Copy link
Contributor Author

Talked about this a bit offline with @chinmaygarde . I think we can already accomplish this with our existing API, but its not very obvious how to do so. For example, you can set one of targetWidth and targetHeight to a maximum value, but don't set the other one - this will preserve the aspect ratio. Then set allowUpscalling to false, this will prevent the image from being resized to be larger.

Given that, I suspect you could probably implement this API in the framework without any dart:ui changes

That only works if you at least know the aspect ratio of the image. If I want to size an image to fit within e.g. a square bounding box of 100x100, but I don't know if the image is portrait or landscape orientation, then I don't know whether to say "load the image with width=100 and let height be flexible" or "load the image at height=100 and let the width be flexible".

This was the use case that drove this change. It comes about when you load a stream of random images over the network via a public API (e.g. Google Photos, Flickr, etc.).

@jonahwilliams
Copy link
Member

so in that case you'd have to say maxWidth = 100, maxHeight = 100. but then that doesn't preserve the aspect ratio?

@tvolkert
Copy link
Contributor Author

Correct - that stretches the image to a square.

@tvolkert
Copy link
Contributor Author

Note that the current API doesn't specify maxWidth & maxHeight -- it specifies targetWidth & targetHeight

@jonahwilliams
Copy link
Member

with allowUpscaling= false then targetSise becomes maxSize essentially

@jonahwilliams
Copy link
Member

Could we accomplish something similar with a preserveAspectRatio flag that behaved like you expected?

@tvolkert
Copy link
Contributor Author

No, allowUpscaling=false only ensures that you don't increase the target width beyond the intrinsic width (or the target height beyond the intrinsic height)

engine/lib/ui/painting.dart

Lines 2151 to 2158 in 7d40e77

if (!allowUpscaling) {
if (targetWidth != null && targetWidth > descriptor.width) {
targetWidth = descriptor.width;
}
if (targetHeight != null && targetHeight > descriptor.height) {
targetHeight = descriptor.height;
}
}

@tvolkert
Copy link
Contributor Author

tvolkert commented Jan 19, 2023

Could we accomplish something similar with a preserveAspectRatio flag that behaved like you expected?

We already do preserve the aspect ratio if you only specify one of the target width or height. But the problem is, you don't know which one to pass if you don't already know its aspect ratio.

We could (and I considered) add a new parameter (or new signature) that had the caller pass a bounding box, and we'd fit the target image within the bounding box while maintaining the aspect ratio. But such an API change would be just as disruptive as the change in this PR, but less flexible.

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
* Respond to review comments
Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

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

Overall LGTM, and looks like all comments have been addressed. One nit about whether we could generalise TargetImageSize to avoid adding more such classes later elsewhere.


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.

@jonahwilliams
Copy link
Member

I think that this is a good feature, but I think it should actually be implemented in foundation instead of dart:ui. It does not require any new native bindings as far as I can tell.

@tvolkert
Copy link
Contributor Author

There's an optimization I've discussed with @yjbanov and @alanwutang11 that will need to be done in the web engine, so it's probably best left here.

@jonahwilliams
Copy link
Member

Which one?

@tvolkert
Copy link
Contributor Author

If the user passes a callback, but it returns a TargetImageSize with either both width & height null or the same size as the intrinsic size, we can pass the existing codec without discarding it and having to re-create one. It relies on a web engine implementation detail whereby we can avoid getting the first frame in order to know the intrinsic size of the image.

@jonahwilliams
Copy link
Member

Interesting! Please file a bug/todo for tracking this

Copy link
Member

@jonahwilliams jonahwilliams left a comment

Choose a reason for hiding this comment

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

LGTM

@tvolkert tvolkert added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 23, 2023
@tvolkert
Copy link
Contributor Author

flutter/flutter#118985

@auto-submit auto-submit bot merged commit 1e4e11a into flutter:main Jan 23, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jan 23, 2023
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Jan 23, 2023
sourcegraph-bot pushed a commit to sgtest/megarepo that referenced this pull request Jan 23, 2023
…e#38905) (#118989)

Commit: 696a84b1ed7da6d8edc69e334a9ec1f3d4ad478f
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Jan 23, 2023
* 044e344 a8522271c Roll Fuchsia Mac SDK from 5TQ9IL4-Yu3KHCR-H... to R4F4q-h902yt4s7ow... (flutter/engine#39058) (flutter/flutter#118984)

* b974eac b3da52d8c Roll Fuchsia Linux SDK from l3c_b-vRr-o6ZFX_M... to f613tOkDB282hW2tA... (flutter/engine#39061) (flutter/flutter#118987)

* 696a84b 1e4e11ad1 Add more flexible image loading API (flutter/engine#38905) (flutter/flutter#118989)

* 6ae7ad7 92313596d Roll Dart SDK from 548678dd684c to 608a0691a1d7 (1 revision) (flutter/engine#39063) (flutter/flutter#118990)

* bd7bee0 Roll Flutter Engine from 92313596d77b to 8e7bc509e0d7 (3 revisions) (flutter/flutter#119004)
auto-submit bot pushed a commit to flutter/plugins that referenced this pull request Jan 23, 2023
* 044e344 a8522271c Roll Fuchsia Mac SDK from 5TQ9IL4-Yu3KHCR-H... to R4F4q-h902yt4s7ow... (flutter/engine#39058) (flutter/flutter#118984)

* b974eac b3da52d8c Roll Fuchsia Linux SDK from l3c_b-vRr-o6ZFX_M... to f613tOkDB282hW2tA... (flutter/engine#39061) (flutter/flutter#118987)

* 696a84b 1e4e11ad1 Add more flexible image loading API (flutter/engine#38905) (flutter/flutter#118989)

* 6ae7ad7 92313596d Roll Dart SDK from 548678dd684c to 608a0691a1d7 (1 revision) (flutter/engine#39063) (flutter/flutter#118990)

* bd7bee0 Roll Flutter Engine from 92313596d77b to 8e7bc509e0d7 (3 revisions) (flutter/flutter#119004)
ricardoamador pushed a commit to ricardoamador/engine that referenced this pull request Jan 25, 2023
* 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
mauricioluz pushed a commit to mauricioluz/plugins that referenced this pull request Jan 26, 2023
* 044e344 a8522271c Roll Fuchsia Mac SDK from 5TQ9IL4-Yu3KHCR-H... to R4F4q-h902yt4s7ow... (flutter/engine#39058) (flutter/flutter#118984)

* b974eac b3da52d8c Roll Fuchsia Linux SDK from l3c_b-vRr-o6ZFX_M... to f613tOkDB282hW2tA... (flutter/engine#39061) (flutter/flutter#118987)

* 696a84b 1e4e11ad1 Add more flexible image loading API (flutter/engine#38905) (flutter/flutter#118989)

* 6ae7ad7 92313596d Roll Dart SDK from 548678dd684c to 608a0691a1d7 (1 revision) (flutter/engine#39063) (flutter/flutter#118990)

* bd7bee0 Roll Flutter Engine from 92313596d77b to 8e7bc509e0d7 (3 revisions) (flutter/flutter#119004)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
autosubmit Merge PR when tree becomes green via auto submit App
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants