imagebitmap: Add missing basic functionality (#37025)

Add missing basic functionality for ImageBitmap
https://html.spec.whatwg.org/multipage/#imagebitmap
including new variant of creation bitmap with source rectangle
https://html.spec.whatwg.org/multipage/#dom-createimagebitmap
but without support of cropping bitmap data with formatting.

Add ImageBitmap to CanvasImageSource union type
https://html.spec.whatwg.org/multipage/#canvasimagesource

Add ImageBitmap to TexImageSource for WebGL
https://registry.khronos.org/webgl/specs/latest/1.0/index.html

Testing: Improvements in the following WPT tests
 - html/canvas/element/manual/imagebitmap/*
 - html/canvas/element/manual/wide-gamut-canvas/*
 - html/semantics/embedded-content/the-canvas-element/*
 - webgl/tests/conformance/textures/image_bitmap_from*
 - webmessaging/postMessage_cross_domain_image_transfer_2d.sub.htm

Fixes: https://github.com/servo/servo/issues/34112

Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
This commit is contained in:
Andrei Volykhin 2025-06-09 17:28:30 +03:00 committed by GitHub
parent a3c792e5aa
commit 7f536e8092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 653 additions and 1232 deletions

View file

@ -55,6 +55,7 @@ use crate::dom::element::{Element, cors_setting_for_element};
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlcanvaselement::HTMLCanvasElement;
use crate::dom::htmlvideoelement::HTMLVideoElement;
use crate::dom::imagebitmap::ImageBitmap;
use crate::dom::imagedata::ImageData;
use crate::dom::node::{Node, NodeTraits};
use crate::dom::offscreencanvas::OffscreenCanvas;
@ -319,6 +320,7 @@ impl CanvasState {
},
CanvasImageSource::HTMLVideoElement(video) => video.origin_is_clean(),
CanvasImageSource::HTMLCanvasElement(canvas) => canvas.origin_is_clean(),
CanvasImageSource::ImageBitmap(bitmap) => bitmap.origin_is_clean(),
CanvasImageSource::OffscreenCanvas(canvas) => canvas.origin_is_clean(),
CanvasImageSource::CSSStyleValue(_) => true,
}
@ -459,6 +461,15 @@ impl CanvasState {
self.draw_html_canvas_element(canvas, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh)
},
CanvasImageSource::ImageBitmap(ref bitmap) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if bitmap.is_detached() {
return Err(Error::InvalidState);
}
self.draw_image_bitmap(bitmap, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh);
Ok(())
},
CanvasImageSource::OffscreenCanvas(ref canvas) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if canvas.get_size().is_empty() {
@ -728,6 +739,52 @@ impl CanvasState {
Ok(())
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
#[allow(clippy::too_many_arguments)]
fn draw_image_bitmap(
&self,
bitmap: &ImageBitmap,
canvas: Option<&HTMLCanvasElement>,
sx: f64,
sy: f64,
sw: Option<f64>,
sh: Option<f64>,
dx: f64,
dy: f64,
dw: Option<f64>,
dh: Option<f64>,
) {
let Some(snapshot) = bitmap.bitmap_data().clone() else {
return;
};
// Step 4. Establish the source and destination rectangles.
let bitmap_size = snapshot.size();
let dw = dw.unwrap_or(bitmap_size.width as f64);
let dh = dh.unwrap_or(bitmap_size.height as f64);
let sw = sw.unwrap_or(bitmap_size.width as f64);
let sh = sh.unwrap_or(bitmap_size.height as f64);
let (source_rect, dest_rect) =
self.adjust_source_dest_rects(bitmap_size, sx, sy, sw, sh, dx, dy, dw, dh);
// Step 5. If one of the sw or sh arguments is zero, then return. Nothing is painted.
if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
return;
}
let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
snapshot.as_ipc(),
dest_rect,
source_rect,
smoothing_enabled,
));
self.mark_as_dirty(canvas);
}
pub(crate) fn mark_as_dirty(&self, canvas: Option<&HTMLCanvasElement>) {
if let Some(canvas) = canvas {
canvas.mark_as_dirty();
@ -1063,6 +1120,14 @@ impl CanvasState {
canvas.get_image_data().ok_or(Error::InvalidState)?
},
CanvasImageSource::ImageBitmap(ref bitmap) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if bitmap.is_detached() {
return Err(Error::InvalidState);
}
bitmap.bitmap_data().clone().ok_or(Error::InvalidState)?
},
CanvasImageSource::OffscreenCanvas(ref canvas) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if canvas.get_size().is_empty() {

View file

@ -29,9 +29,10 @@ use crossbeam_channel::Sender;
use devtools_traits::{PageError, ScriptToDevtoolsControlMsg};
use dom_struct::dom_struct;
use embedder_traits::EmbedderMsg;
use euclid::default::Size2D;
use http::HeaderMap;
use hyper_serde::Serde;
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::ipc::{self, IpcSender, IpcSharedMemory};
use ipc_channel::router::ROUTER;
use js::glue::{IsWrapper, UnwrapObjectDynamic};
use js::jsapi::{
@ -59,9 +60,11 @@ use net_traits::{
CoreResourceMsg, CoreResourceThread, FetchResponseListener, IpcSend, ReferrerPolicy,
ResourceThreads, fetch_async,
};
use pixels::PixelFormat;
use profile_traits::{ipc as profile_ipc, mem as profile_mem, time as profile_time};
use script_bindings::interfaces::GlobalScopeHelpers;
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
use snapshot::Snapshot;
use timers::{TimerEventId, TimerEventRequest, TimerSource};
use url::Origin;
use uuid::Uuid;
@ -2956,64 +2959,209 @@ impl GlobalScope {
result == CheckResult::Blocked
}
/// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap>
#[allow(clippy::too_many_arguments)]
pub(crate) fn create_image_bitmap(
&self,
image: ImageBitmapSource,
_sx: i32,
_sy: i32,
sw: Option<i32>,
sh: Option<i32>,
options: &ImageBitmapOptions,
can_gc: CanGc,
) -> Rc<Promise> {
let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
let p = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
// Step 1. If either sw or sh is given and is 0, then return a promise rejected with a RangeError.
if sw.is_some_and(|w| w == 0) {
p.reject_error(
Error::Range("'sw' must be a non-zero value".to_owned()),
can_gc,
);
return p;
}
if sh.is_some_and(|h| h == 0) {
p.reject_error(
Error::Range("'sh' must be a non-zero value".to_owned()),
can_gc,
);
return p;
}
// Step 2. If either options's resizeWidth or options's resizeHeight is present and is 0,
// then return a promise rejected with an "InvalidStateError" DOMException.
if options.resizeWidth.is_some_and(|w| w == 0) {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
if options.resizeHeight.is_some_and(|w| w == 0) {
if options.resizeHeight.is_some_and(|h| h == 0) {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
// Step 3. Check the usability of the image argument. If this throws an exception or returns bad,
// then return a promise rejected with an "InvalidStateError" DOMException.
// Step 6. Switch on image:
match image {
ImageBitmapSource::HTMLCanvasElement(ref canvas) => {
// https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument
if !canvas.is_valid() {
ImageBitmapSource::HTMLImageElement(ref image) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if !image.is_usable().is_ok_and(|u| u) {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
match canvas.get_image_data() {
Some(snapshot) => {
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(canvas.origin_is_clean());
p.resolve_native(&(image_bitmap), can_gc);
// If no ImageBitmap object can be constructed, then the promise is rejected instead.
let Some(img) = image.image_data() else {
p.reject_error(Error::InvalidState, can_gc);
return p;
};
let Some(img) = img.as_raster_image() else {
// Vector HTMLImageElement are not yet supported.
p.reject_error(Error::InvalidState, can_gc);
return p;
};
let size = Size2D::new(img.metadata.width, img.metadata.height);
let format = match img.format {
PixelFormat::BGRA8 => snapshot::PixelFormat::BGRA,
PixelFormat::RGBA8 => snapshot::PixelFormat::RGBA,
pixel_format => {
unimplemented!("unsupported pixel format ({:?})", pixel_format)
},
None => p.reject_error(Error::InvalidState, can_gc),
};
let alpha_mode = snapshot::AlphaMode::Transparent {
premultiplied: false,
};
let snapshot = Snapshot::from_shared_memory(
size.cast(),
format,
alpha_mode,
IpcSharedMemory::from_bytes(img.first_frame().bytes),
);
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(image.same_origin(GlobalScope::entry().origin()));
p.resolve_native(&image_bitmap, can_gc);
},
ImageBitmapSource::HTMLVideoElement(ref video) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if !video.is_usable() {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
p
if video.is_network_state_empty() {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
// If no ImageBitmap object can be constructed, then the promise is rejected instead.
let Some(snapshot) = video.get_current_frame_data() else {
p.reject_error(Error::InvalidState, can_gc);
return p;
};
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(video.origin_is_clean());
p.resolve_native(&image_bitmap, can_gc);
},
ImageBitmapSource::HTMLCanvasElement(ref canvas) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if canvas.get_size().is_empty() {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
// If no ImageBitmap object can be constructed, then the promise is rejected instead.
let Some(snapshot) = canvas.get_image_data() else {
p.reject_error(Error::InvalidState, can_gc);
return p;
};
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(canvas.origin_is_clean());
p.resolve_native(&image_bitmap, can_gc);
},
ImageBitmapSource::ImageBitmap(ref bitmap) => {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if bitmap.is_detached() {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
// If no ImageBitmap object can be constructed, then the promise is rejected instead.
let Some(snapshot) = bitmap.bitmap_data().clone() else {
p.reject_error(Error::InvalidState, can_gc);
return p;
};
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(bitmap.origin_is_clean());
p.resolve_native(&image_bitmap, can_gc);
},
ImageBitmapSource::OffscreenCanvas(ref canvas) => {
// https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument
if !canvas.is_valid() {
// <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
if canvas.get_size().is_empty() {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
match canvas.get_image_data() {
Some(snapshot) => {
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(canvas.origin_is_clean());
p.resolve_native(&(image_bitmap), can_gc);
},
None => p.reject_error(Error::InvalidState, can_gc),
}
p
// If no ImageBitmap object can be constructed, then the promise is rejected instead.
let Some(snapshot) = canvas.get_image_data() else {
p.reject_error(Error::InvalidState, can_gc);
return p;
};
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
image_bitmap.set_origin_clean(canvas.origin_is_clean());
p.resolve_native(&image_bitmap, can_gc);
},
_ => {
ImageBitmapSource::Blob(_) => {
// TODO: implement support of Blob object as ImageBitmapSource
p.reject_error(Error::InvalidState, can_gc);
},
ImageBitmapSource::ImageData(ref image_data) => {
// <https://html.spec.whatwg.org/multipage/#the-imagebitmap-interface:imagedata-4>
if image_data.is_detached() {
p.reject_error(Error::InvalidState, can_gc);
return p;
}
let alpha_mode = snapshot::AlphaMode::Transparent {
premultiplied: false,
};
let snapshot = Snapshot::from_shared_memory(
image_data.get_size().cast(),
snapshot::PixelFormat::RGBA,
alpha_mode,
image_data.to_shared_memory(),
);
let image_bitmap = ImageBitmap::new(self, snapshot, can_gc);
p.resolve_native(&image_bitmap, can_gc);
},
ImageBitmapSource::CSSStyleValue(_) => {
// TODO: CSSStyleValue is not part of ImageBitmapSource
// <https://html.spec.whatwg.org/multipage/#imagebitmapsource>
p.reject_error(Error::NotSupported, can_gc);
p
},
}
// Step 7. Return promise.
p
}
pub(crate) fn fire_timer(&self, handle: TimerEventId, can_gc: CanGc) {

View file

@ -520,6 +520,10 @@ impl HTMLMediaElement {
}
}
pub(crate) fn network_state(&self) -> NetworkState {
self.network_state.get()
}
pub(crate) fn get_ready_state(&self) -> ReadyState {
self.ready_state.get()
}

View file

@ -37,7 +37,7 @@ use crate::dom::bindings::str::DOMString;
use crate::dom::document::Document;
use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers};
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlmediaelement::{HTMLMediaElement, ReadyState};
use crate::dom::htmlmediaelement::{HTMLMediaElement, NetworkState, ReadyState};
use crate::dom::node::{Node, NodeTraits};
use crate::dom::performanceresourcetiming::InitiatorType;
use crate::dom::virtualmethods::VirtualMethods;
@ -294,6 +294,10 @@ impl HTMLVideoElement {
pub(crate) fn origin_is_clean(&self) -> bool {
self.htmlmediaelement.origin_is_clean()
}
pub(crate) fn is_network_state_empty(&self) -> bool {
self.htmlmediaelement.network_state() == NetworkState::Empty
}
}
impl HTMLVideoElementMethods<crate::DomTypeHolder> for HTMLVideoElement {

View file

@ -68,7 +68,7 @@ impl ImageBitmap {
/// Return the value of the [`[[Detached]]`](https://html.spec.whatwg.org/multipage/#detached)
/// internal slot
fn is_detached(&self) -> bool {
pub(crate) fn is_detached(&self) -> bool {
self.bitmap_data.borrow().is_none()
}
}
@ -109,9 +109,9 @@ impl Serializable for ImageBitmap {
}
fn serialized_storage<'a>(
reader: StructuredData<'a, '_>,
data: StructuredData<'a, '_>,
) -> &'a mut Option<HashMap<ImageBitmapId, Self::Data>> {
match reader {
match data {
StructuredData::Reader(r) => &mut r.image_bitmaps,
StructuredData::Writer(w) => &mut w.image_bitmaps,
}

View file

@ -148,6 +148,11 @@ impl ImageData {
imagedata, global, proto, can_gc,
))
}
pub(crate) fn is_detached(&self) -> bool {
self.data.is_detached_buffer(GlobalScope::get_cx())
}
#[allow(unsafe_code)]
pub(crate) fn to_shared_memory(&self) -> IpcSharedMemory {
IpcSharedMemory::from_bytes(unsafe { self.as_slice() })

View file

@ -126,10 +126,6 @@ impl OffscreenCanvas {
Some(context)
}
pub(crate) fn is_valid(&self) -> bool {
self.Width() != 0 && self.Height() != 0
}
pub(crate) fn placeholder(&self) -> Option<&HTMLCanvasElement> {
self.placeholder.as_deref()
}

View file

@ -575,6 +575,23 @@ impl WebGLRenderingContext {
pub(crate) fn get_image_pixels(&self, source: TexImageSource) -> Fallible<Option<TexPixels>> {
Ok(Some(match source {
TexImageSource::ImageBitmap(bitmap) => {
if !bitmap.origin_is_clean() {
return Err(Error::Security);
}
let Some(snapshot) = bitmap.bitmap_data().clone() else {
return Ok(None);
};
let snapshot = snapshot.as_ipc();
let size = snapshot.size().cast();
let format = match snapshot.format() {
snapshot::PixelFormat::RGBA => PixelFormat::RGBA8,
snapshot::PixelFormat::BGRA => PixelFormat::BGRA8,
};
let premultiply = snapshot.alpha_mode().is_premultiplied();
TexPixels::new(snapshot.to_ipc_shared_memory(), size, format, premultiply)
},
TexImageSource::ImageData(image_data) => TexPixels::new(
image_data.to_shared_memory(),
image_data.get_size(),

View file

@ -1214,7 +1214,7 @@ impl WindowMethods<crate::DomTypeHolder> for Window {
self.as_global_scope().queue_function_as_microtask(callback);
}
// https://html.spec.whatwg.org/multipage/#dom-createimagebitmap
/// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap>
fn CreateImageBitmap(
&self,
image: ImageBitmapSource,
@ -1223,7 +1223,30 @@ impl WindowMethods<crate::DomTypeHolder> for Window {
) -> Rc<Promise> {
let p = self
.as_global_scope()
.create_image_bitmap(image, options, can_gc);
.create_image_bitmap(image, 0, 0, None, None, options, can_gc);
p
}
/// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap>
fn CreateImageBitmap_(
&self,
image: ImageBitmapSource,
sx: i32,
sy: i32,
sw: i32,
sh: i32,
options: &ImageBitmapOptions,
can_gc: CanGc,
) -> Rc<Promise> {
let p = self.as_global_scope().create_image_bitmap(
image,
sx,
sy,
Some(sw),
Some(sh),
options,
can_gc,
);
p
}

View file

@ -446,7 +446,7 @@ impl WorkerGlobalScopeMethods<crate::DomTypeHolder> for WorkerGlobalScope {
.queue_function_as_microtask(callback);
}
// https://html.spec.whatwg.org/multipage/#dom-createimagebitmap
/// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap>
fn CreateImageBitmap(
&self,
image: ImageBitmapSource,
@ -455,7 +455,30 @@ impl WorkerGlobalScopeMethods<crate::DomTypeHolder> for WorkerGlobalScope {
) -> Rc<Promise> {
let p = self
.upcast::<GlobalScope>()
.create_image_bitmap(image, options, can_gc);
.create_image_bitmap(image, 0, 0, None, None, options, can_gc);
p
}
/// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap>
fn CreateImageBitmap_(
&self,
image: ImageBitmapSource,
sx: i32,
sy: i32,
sw: i32,
sh: i32,
options: &ImageBitmapOptions,
can_gc: CanGc,
) -> Rc<Promise> {
let p = self.upcast::<GlobalScope>().create_image_bitmap(
image,
sx,
sy,
Some(sw),
Some(sh),
options,
can_gc,
);
p
}