Skip to content

Do not derive a decode size of 0 for very thin images - #194669

Merged
auto-submit[bot] merged 4 commits into
flutter:masterfrom
YeungKC:fix-thin-image-zero-side
Oct 20, 2026
Merged

auto-submit[bot] merged 4 commits into
flutter:masterfrom
YeungKC:fix-thin-image-zero-side

Conversation

@YeungKC

@YeungKC YeungKC commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

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

  • 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, including Features we expect every widget to implement.
  • I signed the CLA.
  • 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.
  • 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.

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Oct 18, 2026
@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. engine flutter/engine related. See also e: labels. platform-web Web applications specifically team-web Owned by Web platform team labels Oct 18, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 ResizeImage constructor now asserts width == null || width > 0 and height == null || height > 0. This is the same check that Image already does for cacheWidth and cacheHeight. 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 fit branch changes only a derived side.
  • Test: ResizeImage with width or height of 0 or -1, with exact and fit, throws an AssertionError.

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
harryterkelsen previously approved these changes Oct 19, 2026

@harryterkelsen harryterkelsen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks! LGTM now with the ResizeImage fix

…onstructor

An explicit width or height of 0 now fails at decode time, as before this change.
@YeungKC

YeungKC commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

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 ResizeImage with a width or height of 0, also when the image is never decoded. Some callers can pass 0 without an earlier check. For example, FadeInImage does not assert that imageCacheWidth or placeholderCacheWidth is greater than 0. On master, such a test passes if it does not decode the image.

I prepared an alternative commit. It removes the constructor asserts. It clamps to 1 only the sides that ResizeImagePolicy.fit derives from the aspect ratio, and never a side that the caller gives. Thus, an explicit ResizeImage(width: 0) with fit or exact fails at decode time, as on master. The thin image cases still decode to 100x1 and 1x100. The new tests check both behaviors.

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.

@harryterkelsen

Copy link
Copy Markdown
Contributor

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 ResizeImage with a width or height of 0, also when the image is never decoded. Some callers can pass 0 without an earlier check. For example, FadeInImage does not assert that imageCacheWidth or placeholderCacheWidth is greater than 0. On master, such a test passes if it does not decode the image.

I prepared an alternative commit. It removes the constructor asserts. It clamps to 1 only the sides that ResizeImagePolicy.fit derives from the aspect ratio, and never a side that the caller gives. Thus, an explicit ResizeImage(width: 0) with fit or exact fails at decode time, as on master. The thin image cases still decode to 100x1 and 1x100. The new tests check both behaviors.

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

@YeungKC

YeungKC commented Oct 20, 2026

Copy link
Copy Markdown
Member Author

Thanks. I pushed b84ce82. It removes the new constructor asserts and clamps only the derived sides in the fit branch. A width or height of 0 that the caller gives is not changed, so it still fails at decode time, as before. The assert test is replaced with a test for this case. The painting, image and fade tests pass locally.

@harryterkelsen harryterkelsen added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 20, 2026

@victorsanni victorsanni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM for framework side

@auto-submit
auto-submit Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into flutter:master with commit 0af6449 Oct 20, 2026
24 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 20, 2026
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
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/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. framework flutter/packages/flutter repository. See also f: labels. platform-web Web applications specifically team-web Owned by Web platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image fails to decode when cacheWidth or ResizeImage derives a side of 0 for a very thin image

3 participants