Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[canvaskit] Decode images using <img> tag decoding #53201

Merged

Conversation

harryterkelsen
Copy link
Contributor

@harryterkelsen harryterkelsen commented Jun 4, 2024

Prefer to decode images using the browser API rather than with CanvasKit to avoid jank when decoding.

Part of deprecating the HTML renderer: flutter/flutter#145954

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

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@harryterkelsen harryterkelsen added the Work in progress (WIP) Not ready (yet) for review! label Jun 4, 2024
@github-actions github-actions bot added the platform-web Code specifically for the web engine label Jun 4, 2024
@flutter-dashboard
Copy link

This pull request executed golden file tests, but it has not been updated in a while (20 days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold.

@harryterkelsen harryterkelsen removed the Work in progress (WIP) Not ready (yet) for review! label Jul 11, 2024
@harryterkelsen
Copy link
Contributor Author

This change causes the CanvasKit renderer to default to decoding using ImageDecoder when it's available, or using an ImageElement otherwise. To facilitate this, some helper methods were added which can read the pixels and resize ImageElement images using a DOM canvas, as a workaround to a bug in CanvasKit where getting pixel data from images created from textures returns an array of all 0s.

lib/web_ui/lib/src/engine/canvaskit/image.dart Outdated Show resolved Hide resolved
}

final int scaledWidth = scaledSize.width.toInt();
final int scaledHeight = scaledSize.height.toInt();
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe scaledImageSize should switch to BitmapSize to avoid rounding? There's also the issue that we check for allowUpscaling before calling toInt, which may end up upscaling anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to BitmapSize to avoid ugly .toInt() and .toDouble() calls where possible. Checking for allowUpscaling before calling toInt() should be okay here since we're only working with ints for the width and height.

scaledHeight,
);
final DomImageData imageData =
ctx.getImageData(0, 0, scaledWidth, scaledHeight);
Copy link
Contributor

Choose a reason for hiding this comment

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

Safari can run out 2D canvas memory easily. To prevent that set the size of the htmlCanvas to 0x0. That forces Safari to reclaim the underlying image buffer eagerly. Alternatively, maybe the htmlCanvas could be reused. If the app scaled an image once, it may do it again, but that's speculative.

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.

height: scaledHeight,
);
final DomCanvasRenderingContext2D ctx =
htmlCanvas.getContext('2d')! as DomCanvasRenderingContext2D;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm guessing that despite the slowness (due to https://bugs.webkit.org/show_bug.cgi?id=267291) a 2d OffscreenCanvas with transferToImageBitmap would still be faster than an on-screen 2d canvas getImageData. The latter is guaranteed to always copy, in addition to other copying that happens in canvasKit.MakeImage.

However, if use ImageBitmap as the source, then we'd have to use MakeLazyImageFromTextureSource, which has been a bit unreliable.

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
Contributor

Choose a reason for hiding this comment

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

Unreliable how? I think we should try to avoid explicitly pulling the bytes out of the image. Is there a reason we can't just create an ImageBitmap and use that, instead of grabbing the image data? I also should mention that createImageBitmap actually allows you to scale the image while extracting it from the image source: https://developer.mozilla.org/en-US/docs/Web/API/createImageBitmap

/// For images which are decoded via an HTML Image element, this field holds
/// the image element from which this image was created.
///
/// Skia owns the image element and will close it when it's no longer used.
Copy link
Contributor

Choose a reason for hiding this comment

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

How does Skia close the image element? VideoFrame and ImageBitmap provide the close() method, but is there something equivalent for <img> tag?

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 checked the Skia code and it only automatically disposes the VideoFrame. We should manually dispose of the ImageBitmap when dispose is called. As for the <img> tag, I think the best we can do is let it be garbage collected with this Image.

return CkImage.cloneOf(
box,
videoFrame: videoFrame?.clone(),
imageElement: imageElement,
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to do anything with bitmapImage? If not, it would be good to explain why not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it should. I overlooked the clone call when adding ImageBitmap here.

lib/web_ui/lib/src/engine/canvaskit/image.dart Outdated Show resolved Hide resolved
lib/web_ui/lib/src/engine/canvaskit/picture.dart Outdated Show resolved Hide resolved
lib/web_ui/lib/src/engine/html_image_element_codec.dart Outdated Show resolved Hide resolved
@harryterkelsen harryterkelsen force-pushed the canvaskit-html-img-element-decode branch from 0b426fe to b207ee7 Compare July 18, 2024 21:01
final int scaledWidth = scaledSize.width;
final int scaledHeight = scaledSize.height;

final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can avoid creating an offscreen canvas and so on here by just using createImageBitmap instead. https://developer.mozilla.org/en-US/docs/Web/API/createImageBitmap It has resizeWidth and resizeHeight options which will to the rescaling for you.

Copy link
Contributor

Choose a reason for hiding this comment

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

Harry pointed out that createImageBitmap is async, which makes this much less clean. So we should leave it as is.

Copy link
Contributor

@eyebrowsoffire eyebrowsoffire left a comment

Choose a reason for hiding this comment

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

LGTM!

final int scaledWidth = scaledSize.width;
final int scaledHeight = scaledSize.height;

final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas(
Copy link
Contributor

Choose a reason for hiding this comment

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

Harry pointed out that createImageBitmap is async, which makes this much less clean. So we should leave it as is.

@flutter-dashboard
Copy link

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

Changes reported for pull request #53201 at sha e7aff46

@harryterkelsen harryterkelsen added the autosubmit Merge PR when tree becomes green via auto submit App label Jul 19, 2024
@auto-submit auto-submit bot merged commit 6312dfc into flutter:main Jul 19, 2024
32 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jul 19, 2024
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Jul 19, 2024
…152046)

flutter/engine@9739d92...6312dfc

2024-07-19 1961493 [email protected] [canvaskit] Decode images using <img> tag decoding (flutter/engine#53201)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-engine-flutter-autoroll
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/ doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App platform-web Code specifically for the web engine will affect goldens
Projects
None yet
3 participants