canvas: Add OffscreenCanvas 'transferToImageBitmap' method (#37880)

Follow the HTML speficication and add missing 'transferToImageBitmap'
method to OffscreenCanvas interface.

https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-transfertoimagebitmap

Testing: Improvements in the following tests
- html/canvas/offscreen/compositing/2d.composite.grid*
- html/canvas/offscreen/fill-and-stroke-styles/2d.gradient*
- html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas*
- html/canvas/offscreen/reset/2d.reset*
- html/canvas/offscreen/text/2d.text*

Fixes (partially): #34111

Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
This commit is contained in:
Andrei Volykhin 2025-07-04 19:25:36 +03:00 committed by GitHub
parent 70b0fb840e
commit 9bd8d4f026
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 110 additions and 84 deletions

View file

@ -39,6 +39,11 @@ pub(crate) trait CanvasContext {
fn resize(&self);
// Resets the backing bitmap (to transparent or opaque black) without the
// context state reset.
// Used by OffscreenCanvas.transferToImageBitmap.
fn reset_bitmap(&self);
/// Returns none if area of canvas is zero.
///
/// In case of other errors it returns cleared snapshot
@ -145,6 +150,21 @@ impl CanvasContext for RenderingContext {
}
}
fn reset_bitmap(&self) {
match self {
RenderingContext::Placeholder(offscreen_canvas) => {
if let Some(context) = offscreen_canvas.context() {
context.reset_bitmap()
}
},
RenderingContext::Context2d(context) => context.reset_bitmap(),
RenderingContext::WebGL(context) => context.reset_bitmap(),
RenderingContext::WebGL2(context) => context.reset_bitmap(),
#[cfg(feature = "webgpu")]
RenderingContext::WebGPU(context) => context.reset_bitmap(),
}
}
fn get_image_data(&self) -> Option<Snapshot> {
match self {
RenderingContext::Placeholder(offscreen_canvas) => {
@ -257,6 +277,12 @@ impl CanvasContext for OffscreenRenderingContext {
}
}
fn reset_bitmap(&self) {
match self {
OffscreenRenderingContext::Context2d(context) => context.reset_bitmap(),
}
}
fn get_image_data(&self) -> Option<Snapshot> {
match self {
OffscreenRenderingContext::Context2d(context) => context.get_image_data(),

View file

@ -292,6 +292,14 @@ impl CanvasState {
*self.state.borrow_mut() = CanvasContextState::new();
}
pub(crate) fn reset_bitmap(&self) {
if !self.is_paintable() {
return;
}
self.send_canvas_2d_msg(Canvas2dMsg::ClearRect(self.size.get().to_f32().into()));
}
fn create_drawable_rect(&self, x: f64, y: f64, w: f64, h: f64) -> Option<Rect<f32>> {
if !([x, y, w, h].iter().all(|val| val.is_finite())) {
return None;

View file

@ -153,6 +153,10 @@ impl CanvasContext for CanvasRenderingContext2D {
self.set_canvas_bitmap_dimensions(self.size().cast())
}
fn reset_bitmap(&self) {
self.canvas_state.reset_bitmap()
}
fn get_image_data(&self) -> Option<Snapshot> {
if !self.canvas_state.is_paintable() {
return None;

View file

@ -27,6 +27,7 @@ use crate::dom::blob::Blob;
use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlcanvaselement::HTMLCanvasElement;
use crate::dom::imagebitmap::ImageBitmap;
use crate::dom::offscreencanvasrenderingcontext2d::OffscreenCanvasRenderingContext2D;
use crate::dom::promise::Promise;
use crate::realms::{AlreadyInRealm, InRealm};
@ -214,6 +215,40 @@ impl OffscreenCanvasMethods<crate::DomTypeHolder> for OffscreenCanvas {
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-transfertoimagebitmap>
fn TransferToImageBitmap(&self, can_gc: CanGc) -> Fallible<DomRoot<ImageBitmap>> {
// TODO Step 1. If the value of this OffscreenCanvas object's
// [[Detached]] internal slot is set to true, then throw an
// "InvalidStateError" DOMException.
// Step 2. If this OffscreenCanvas object's context mode is set to none,
// then throw an "InvalidStateError" DOMException.
if self.context.borrow().is_none() {
return Err(Error::InvalidState);
}
// Step 3. Let image be a newly created ImageBitmap object that
// references the same underlying bitmap data as this OffscreenCanvas
// object's bitmap.
let Some(snapshot) = self.get_image_data() else {
return Err(Error::InvalidState);
};
let image_bitmap = ImageBitmap::new(&self.global(), snapshot, can_gc);
image_bitmap.set_origin_clean(self.origin_is_clean());
// Step 4. Set this OffscreenCanvas object's bitmap to reference a newly
// created bitmap of the same dimensions and color space as the previous
// bitmap, and with its pixels initialized to transparent black, or
// opaque black if the rendering context's alpha is false.
if let Some(canvas_context) = self.context() {
canvas_context.reset_bitmap();
}
// Step 5. Return image.
Ok(image_bitmap)
}
/// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-converttoblob>
fn ConvertToBlob(&self, options: &ImageEncodeOptions, can_gc: CanGc) -> Rc<Promise> {
// Step 5. Let result be a new promise object.

View file

@ -84,6 +84,10 @@ impl CanvasContext for OffscreenCanvasRenderingContext2D {
self.context.resize()
}
fn reset_bitmap(&self) {
self.context.reset_bitmap()
}
fn get_image_data(&self) -> Option<Snapshot> {
self.context.get_image_data()
}

View file

@ -916,6 +916,10 @@ impl CanvasContext for WebGL2RenderingContext {
self.base.resize();
}
fn reset_bitmap(&self) {
self.base.reset_bitmap();
}
fn get_image_data(&self) -> Option<Snapshot> {
self.base.get_image_data()
}

View file

@ -1974,6 +1974,10 @@ impl CanvasContext for WebGLRenderingContext {
}
}
fn reset_bitmap(&self) {
warn!("The WebGLRenderingContext 'reset_bitmap' is not implemented yet");
}
// Used by HTMLCanvasElement.toDataURL
//
// This emits errors quite liberally, but the spec says that this operation

View file

@ -276,6 +276,10 @@ impl CanvasContext for GPUCanvasContext {
}
}
fn reset_bitmap(&self) {
warn!("The GPUCanvasContext 'reset_bitmap' is not implemented yet");
}
/// <https://gpuweb.github.io/gpuweb/#ref-for-abstract-opdef-get-a-copy-of-the-image-contents-of-a-context%E2%91%A5>
fn get_image_data(&self) -> Option<Snapshot> {
// 1. Return a copy of the image contents of context.

View file

@ -519,7 +519,7 @@ DOMInterfaces = {
},
'OffscreenCanvas': {
'canGc': ['ConvertToBlob', 'GetContext', 'SetHeight', 'SetWidth'],
'canGc': ['ConvertToBlob', 'GetContext', 'SetHeight', 'SetWidth', 'TransferToImageBitmap'],
},
'OffscreenCanvasRenderingContext2D': {

View file

@ -20,6 +20,6 @@ interface OffscreenCanvas : EventTarget {
attribute [EnforceRange] unsigned long long height;
[Throws] OffscreenRenderingContext? getContext(DOMString contextId, optional any options = null);
//ImageBitmap transferToImageBitmap();
[Throws] ImageBitmap transferToImageBitmap();
Promise<Blob> convertToBlob(optional ImageEncodeOptions options = {});
};

View file

@ -1,2 +1,2 @@
[2d.composite.grid.filter.no_shadow.drawImage.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.filter.no_shadow.fillRect.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.filter.no_shadow.pattern.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.filter.shadow.drawImage.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.filter.shadow.fillRect.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.filter.shadow.pattern.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.no_filter.no_shadow.drawImage.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.no_filter.no_shadow.fillRect.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.no_filter.no_shadow.pattern.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.no_filter.shadow.drawImage.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.no_filter.shadow.fillRect.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.composite.grid.no_filter.shadow.pattern.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.gradient.colorInterpolationMethod.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.gradient.hueInterpolationMethod.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.filter.drop-shadow-globalAlpha.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,4 +0,0 @@
[offscreencanvas.filter.w.html]
expected: ERROR
[offscreencanvas]
expected: TIMEOUT

View file

@ -1,7 +1,4 @@
[offscreencanvas.resize.html]
[Verify that writing to the width and height attributes of an OffscreenCanvas works when there is a 2d context attached.]
expected: FAIL
[Verify that resizing an OffscreenCanvas with a webgl context propagates the new size to its placeholder canvas asynchronously.]
expected: FAIL

View file

@ -2,18 +2,8 @@
[Test that transferToImageBitmap returns an ImageBitmap with correct color]
expected: FAIL
[Test that call transferToImageBitmap on a detached OffscreenCanvas throws an exception]
expected: FAIL
[Test that transferToImageBitmap returns an ImageBitmap with correct width and height]
expected: FAIL
[Test that transferToImageBitmap without a context throws an exception]
[Test that call transferToImageBitmap on a detached OffscreenCanvas throws an exception]
expected: FAIL
[Test that transferToImageBitmap preserves transform]
expected: FAIL
[Test that transferToImageBitmap won't change context's property]
expected: FAIL

View file

@ -1,26 +1,10 @@
[offscreencanvas.transfer.to.imagebitmap.w.html]
expected: ERROR
[Test that call transferToImageBitmap twice returns an ImageBitmap with correct color in a worker]
expected: FAIL
[Test that transferToImageBitmap returns an ImageBitmap with correct width and height in a worker]
expected: FAIL
[Test that transferToImageBitmap returns an ImageBitmap with correct color in a worker]
expected: FAIL
[Test that call transferToImageBitmap on a detached OffscreenCanvas throws an exception in a worker]
expected: FAIL
[Test that call transferToImageBitmap without a context throws an exception in a worker]
expected: FAIL
[Test that call transferToImageBitmap preserves transform in a worker]
expected: FAIL
[Test that transferToImageBitmap won't change context's property in a worker]
expected: FAIL
[Test that call transferToImageBitmap twice on a alpha-disabled context returns an ImageBitmap with correct color in a worker]
expected: FAIL

View file

@ -1,2 +0,0 @@
[2d.reset.after-rasterization.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.reset.render.drop_shadow.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.reset.render.global_composite_operation.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.reset.render.line.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.reset.render.misc.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.reset.render.miter_limit.w.html]
expected: TIMEOUT

View file

@ -1,2 +1,2 @@
[2d.reset.render.text.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +0,0 @@
[2d.reset.state.clip.w.html]
expected: TIMEOUT

View file

@ -1,2 +1,2 @@
[2d.text.drawing.style.reset.fontKerning.none2.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +1,2 @@
[2d.text.fontVariantCaps.after.reset.font.w.html]
expected: TIMEOUT
expected: FAIL

View file

@ -1,2 +0,0 @@
[2d.text.fontVariantCaps1.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.text.fontVariantCaps3.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.text.fontVariantCaps4.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.text.fontVariantCaps5.w.html]
expected: TIMEOUT

View file

@ -1,2 +0,0 @@
[2d.text.fontVariantCaps6.w.html]
expected: TIMEOUT

View file

@ -26,9 +26,6 @@
[ImageBitmapRenderingContext interface: operation transferFromImageBitmap(ImageBitmap?)]
expected: FAIL
[OffscreenCanvas interface: operation transferToImageBitmap()]
expected: FAIL
[OffscreenCanvas interface: attribute oncontextlost]
expected: FAIL

View file

@ -4370,9 +4370,6 @@
[ImageBitmapRenderingContext interface: operation transferFromImageBitmap(ImageBitmap?)]
expected: FAIL
[OffscreenCanvas interface: operation transferToImageBitmap()]
expected: FAIL
[OffscreenCanvas interface: attribute oncontextlost]
expected: FAIL