Repository navigation
Do not derive a decode size of 0 for very thin images - #194669
Conversation
There was a problem hiding this comment.
Code Review
This pull request prevents image scaling calculations from resulting in a width or height of 0 for very thin images by enforcing a minimum dimension of 1 in painting.dart, image_decoder.dart, and image_provider.dart. It also adds corresponding unit tests to verify this behavior. Feedback on the changes points out a potential native memory leak in codec_test.dart because the ui.Image returned by frameInfo.image is not disposed.
| } | ||
|
|
||
| return ui.TargetImageSize(width: targetWidth, height: targetHeight); | ||
| // A very thin image can derive a side of 0, which cannot be decoded. |
There was a problem hiding this comment.
This change mostly looks good to me, but I think we are changing behavior when a user explicitly passes a size of 0. Currently if you do ResizeImage(width: 0, ResizeImagePolicy.fit) it will throw an error, as it should. With this code, I think it will silently just create a 1x1 image. Let's change the ResizeImage constructor to assert that the width and height are > 0 and add a test for this case.
There was a problem hiding this comment.
Thanks, you are correct. I checked it: before this PR, ResizeImage(width: 0, policy: ResizeImagePolicy.fit) failed the TargetImageSize assertion. With the first version of this PR, it decoded a 1x1 image.
6ff762f changes this:
- The
ResizeImageconstructor now assertswidth == null || width > 0andheight == null || height > 0. This is the same check thatImagealready does forcacheWidthandcacheHeight. An explicit 0 or a negative value now fails at construction, for all policies. - Because the given sides are always greater than 0, the clamp in the
fitbranch changes only a derived side. - Test:
ResizeImagewithwidthorheightof 0 or -1, withexactandfit, throws anAssertionError.
The engine change does not change an explicit 0. instantiateCodec already sets a target side that is <= 0 to null before it derives a side, and the clamp is only in the branches that derive a side.
test/painting/, test/widgets/image_test.dart, and flutter analyze pass locally.
harryterkelsen
left a comment
There was a problem hiding this comment.
Thanks! LGTM now with the ResizeImage fix
…onstructor An explicit width or height of 0 now fails at decode time, as before this change.
|
Thank you for the approval. After it, the "Google testing" check failed. I cannot see the details of that check. I suspect that the new constructor asserts cause the failure. They fail as soon as a caller constructs I prepared an alternative commit. It removes the constructor asserts. It clamps to 1 only the sides that Can you tell me if the internal failure is in fact this assert? If it is, I can push the alternative commit to this PR. |
Yes, I took a look and the Google testing failure is caused by the assert in ResizeImage. I think your fix of only adjusting the "derived" sides sounds correct. Please push it here and I'll take a look |
|
Thanks. I pushed b84ce82. It removes the new constructor asserts and clamps only the derived sides in the |
victorsanni
left a comment
There was a problem hiding this comment.
LGTM for framework side
Fixes flutter#194668 A very thin image does not decode when the decode size is derived from one side. Example: a 4000x1 PNG with `cacheWidth: 100`. The derived height is `100 ~/ 4000 = 0`, and the decode fails. Observed on stable 3.47.7 (the same code is on master): | Call (4000x1 or 1x4000 PNG) | Result | | --- | --- | | `Image.memory(..., cacheWidth: 100)` / `ResizeImage(width: 100)` | `Exception: Codec failed to produce an image` (log: `Could not resize to empty dimensions.`) | | `ResizeImage(height: 100)` on 1x4000 | same exception | | `ResizeImage(width: 100, policy: ResizeImagePolicy.fit)` (with or without `allowUpscaling`, with or without `height`) | debug: the assertion `height == null \|\| height > 0` (or `width == null \|\| width > 0`) in `TargetImageSize` fails. By reading the code, in release mode the 0 becomes null, `instantiateCodec` derives 0 again, and the codec fails. | | `ImageDescriptor.instantiateCodec(targetWidth: 100)` / `ui.instantiateImageCodec(targetWidth: 100)` | same exception | This change clamps each derived side to at least 1 pixel: * `ImageDescriptor.instantiateCodec` in `dart:ui` (`lib/ui/painting.dart`). This is the path for `cacheWidth` or `cacheHeight` with one side, and for `instantiateImageCodec` with one target side. * `scaledImageSize` in the web engine, which has the same derivation. * `ResizeImage` with `ResizeImagePolicy.fit`. The clamp is on each side that a branch derives from the aspect ratio. A side that the caller gives is not changed, so an explicit 0 still fails at decode time, as before, and is not changed to 1. `useLogicalSize` uses `ceil`, so it cannot give 0. ## Tests * `packages/flutter/test/painting/image_provider_resize_image_test.dart`: 4000x1 and 1x4000 images with `policy: fit` decode to 100x1 and 1x100. This test fails without the change (assertion in `TargetImageSize`) and passes with it. The painting, image and fade tests pass locally. * `packages/flutter/test/painting/image_provider_resize_image_test.dart`: `ResizeImage` with `width` or `height` of 0 still fails to decode with `policy: exact` and `policy: fit`. * `engine/src/flutter/testing/dart/codec_test.dart`: `instantiateImageCodec(targetWidth: 100)` on 4000x1 gives 100x1, and `targetHeight: 100` on 1x4000 gives 1x100. * `engine/src/flutter/lib/web_ui/test/ui/image_test.dart`: `scaledImageSize` returns 100x1 and 1x100 for the same inputs. I did not build the engine locally, so the `codec_test.dart` and web tests run only in CI. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). No doc change is necessary. - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] All existing and new tests are passing. The framework tests pass locally. The engine and web tests run only in CI. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/
Fixes #194668
A very thin image does not decode when the decode size is derived from one side.
Example: a 4000x1 PNG with
cacheWidth: 100. The derived height is100 ~/ 4000 = 0, and the decode fails.Observed on stable 3.47.7 (the same code is on master):
Image.memory(..., cacheWidth: 100)/ResizeImage(width: 100)Exception: Codec failed to produce an image(log:Could not resize to empty dimensions.)ResizeImage(height: 100)on 1x4000ResizeImage(width: 100, policy: ResizeImagePolicy.fit)(with or withoutallowUpscaling, with or withoutheight)height == null || height > 0(orwidth == null || width > 0) inTargetImageSizefails. By reading the code, in release mode the 0 becomes null,instantiateCodecderives 0 again, and the codec fails.ImageDescriptor.instantiateCodec(targetWidth: 100)/ui.instantiateImageCodec(targetWidth: 100)This change clamps each derived side to at least 1 pixel:
ImageDescriptor.instantiateCodecindart:ui(lib/ui/painting.dart).This is the path for
cacheWidthorcacheHeightwith one side, and forinstantiateImageCodecwith one target side.scaledImageSizein the web engine, which has the same derivation.ResizeImagewithResizeImagePolicy.fit. The clamp is on each side thata branch derives from the aspect ratio.
A side that the caller gives is not changed, so an explicit 0 still fails at
decode time, as before, and is not changed to 1.
useLogicalSizeusesceil, so it cannot give 0.Tests
packages/flutter/test/painting/image_provider_resize_image_test.dart:4000x1 and 1x4000 images with
policy: fitdecode to 100x1 and 1x100.This test fails without the change (assertion in
TargetImageSize) andpasses with it. The painting, image and fade tests pass locally.
packages/flutter/test/painting/image_provider_resize_image_test.dart:ResizeImagewithwidthorheightof 0 still fails to decode withpolicy: exactandpolicy: fit.engine/src/flutter/testing/dart/codec_test.dart:instantiateImageCodec(targetWidth: 100)on 4000x1 gives 100x1, andtargetHeight: 100on 1x4000 gives 1x100.engine/src/flutter/lib/web_ui/test/ui/image_test.dart:scaledImageSizereturns 100x1 and 1x100 for the same inputs.
I did not build the engine locally, so the
codec_test.dartand web tests runonly in CI.
Pre-launch Checklist
///). No doc change is necessary.