diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 7a7634f2c0..dc03cf9bca 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -291,9 +291,10 @@ pub(crate) use self::broadcast_channel::{ pub(crate) use self::canvas::{ BitmapRejection, BitmapTaskResult, CanvasBlobCallbackTask, CanvasBlobCallbackTaskEffect, CanvasBlobEncodeJob, CanvasContextKind, attach_canvas_like_context_object, - build_canvas_rendering_context_2d_object, build_offscreen_canvas_object, - build_webgl_context_object, build_webgl2_context_object, canvas_like_to_data_url, - reset_html_canvas_backing_store_for_dimension_assignment, settle_bitmap_task_result, + build_bitmap_renderer_context, build_canvas_rendering_context_2d_object, + build_offscreen_canvas_object, build_webgl_context_object, build_webgl2_context_object, + canvas_like_to_data_url, reset_html_canvas_backing_store_for_dimension_assignment, + settle_bitmap_task_result, }; use self::constructors::illegal_constructor_callback; pub(crate) use self::constructors::{ diff --git a/moli-renderer-v8/src/context_bootstrap/canvas.rs b/moli-renderer-v8/src/context_bootstrap/canvas.rs index 9961295b0b..7a832d9ae7 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas.rs @@ -147,9 +147,13 @@ struct HtmlCanvasElementPrototypeAccessorsDeclaration { pub(crate) enum CanvasContextKind { #[strum(serialize = "2d")] TwoD, + #[strum(serialize = "bitmaprenderer")] + BitmapRenderer, WebGl, #[strum(serialize = "webgl2")] WebGl2, + #[strum(serialize = "webgpu")] + WebGpu, } impl CanvasContextKind { @@ -181,11 +185,16 @@ mod canvas_context_kind_tests { Some(CanvasContextKind::WebGl2) ); assert_eq!(CanvasContextKind::parse("WebGL"), None); - assert_eq!(CanvasContextKind::parse("bitmaprenderer"), None); + assert_eq!( + CanvasContextKind::parse("bitmaprenderer"), + Some(CanvasContextKind::BitmapRenderer) + ); } } mod backing_store; +mod bitmap_renderer; +pub(crate) use bitmap_renderer::build_bitmap_renderer_context; mod blob_serialization; pub(crate) use blob_serialization::{ CanvasBlobCallbackTask, CanvasBlobCallbackTaskEffect, CanvasBlobEncodeJob, @@ -296,6 +305,7 @@ pub(super) fn install_canvas_template_bindings<'s>( "ImageBitmap" => { image_bitmap::install_image_bitmap_template_bindings(scope, template); } + "ImageBitmapRenderingContext" => bitmap_renderer::install(scope, template), "TextMetrics" => { context2d::install_text_metrics_template_bindings(scope, template); } diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs b/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs index 03610804cd..4e47c12c8d 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs @@ -10,9 +10,14 @@ use moli_canvas::{byte_len as canvas_byte_len, encode_data_url}; use moli_webapi_declare::WebApiObject; const CANVAS_BACKING_STORE_SLOT: &str = "__moliCanvasBackingStore"; -const CANVAS_OWNER_SLOT: &str = "__moliCanvasOwner"; +pub(super) const CANVAS_OWNER_SLOT: &str = "__moliCanvasOwner"; const CANVAS_HAS_CONTEXT_SLOT: &str = "__moliCanvasHasContext"; const CANVAS_2D_CONTEXT_SLOT: &str = "__moliCanvas2DContext"; +const CANVAS_BITMAP_VALID_SLOT: &str = "__moliCanvasBitmapValid"; +const CANVAS_BITMAP_WIDTH_SLOT: &str = "__moliCanvasBitmapWidth"; +const CANVAS_BITMAP_HEIGHT_SLOT: &str = "__moliCanvasBitmapHeight"; +const CANVAS_BITMAP_PREMULTIPLIED_SLOT: &str = "__moliCanvasBitmapPremultiplied"; +const CANVAS_BITMAP_OPAQUE_SLOT: &str = "__moliCanvasBitmapOpaque"; #[derive(WebApiObject)] #[webapi(plain)] @@ -47,6 +52,23 @@ pub(crate) fn reset_canvas_like_backing_store<'s>( scope: &mut v8::PinScope<'s, '_>, canvas: v8::Local<'s, v8::Object>, ) { + // HTML width/height attributes resize only a blank bitmaprenderer output. + // A transferred bitmap retains its own natural dimensions and pixels. + if has_valid_bitmap_output(scope, canvas) && html_canvas_identity(scope, canvas).is_some() { + return; + } + set_private_value( + scope, + canvas, + CANVAS_BITMAP_VALID_SLOT, + v8::Boolean::new(scope, false).into(), + ); + set_private_value( + scope, + canvas, + CANVAS_BITMAP_PREMULTIPLIED_SLOT, + v8::Boolean::new(scope, false).into(), + ); if let Some(context) = canvas_2d_context(scope, canvas) { super::context2d::reset_canvas_context_state(scope, context); } @@ -58,12 +80,107 @@ pub(crate) fn reset_canvas_like_backing_store<'s>( remove_html_canvas_pixels(scope, canvas); return; }; - let Some(bytes) = new_uint8_clamped_array_from_bytes(scope, vec![0; len]) else { + let pixels = blank_canvas_pixels(scope, canvas, len); + let Some(bytes) = new_uint8_clamped_array_from_bytes(scope, pixels.clone()) else { remove_html_canvas_pixels(scope, canvas); return; }; set_private_value(scope, canvas, CANVAS_BACKING_STORE_SLOT, bytes.into()); - replace_html_canvas_pixels(scope, canvas, width, height, vec![0; len]); + replace_html_canvas_pixels(scope, canvas, width, height, pixels); +} + +pub(super) fn initialize_canvas_bitmap_renderer<'s>( + scope: &mut v8::PinScope<'s, '_>, + canvas: v8::Local<'s, v8::Object>, + alpha: bool, +) { + set_private_value( + scope, + canvas, + CANVAS_BITMAP_OPAQUE_SLOT, + v8::Boolean::new(scope, !alpha).into(), + ); + set_private_value( + scope, + canvas, + CANVAS_HAS_CONTEXT_SLOT, + v8::Boolean::new(scope, true).into(), + ); + clear_canvas_bitmap_output(scope, canvas); +} + +pub(super) fn clear_canvas_bitmap_output<'s>( + scope: &mut v8::PinScope<'s, '_>, + canvas: v8::Local<'s, v8::Object>, +) { + set_private_value( + scope, + canvas, + CANVAS_BITMAP_VALID_SLOT, + v8::Boolean::new(scope, false).into(), + ); + reset_canvas_like_backing_store(scope, canvas); +} + +pub(super) fn set_canvas_bitmap_output<'s>( + scope: &mut v8::PinScope<'s, '_>, + canvas: v8::Local<'s, v8::Object>, + data: super::image_bitmap::BitmapData<'s>, +) { + set_private_value(scope, canvas, CANVAS_BACKING_STORE_SLOT, data.pixels.into()); + set_private_value( + scope, + canvas, + CANVAS_BITMAP_WIDTH_SLOT, + v8::Integer::new_from_unsigned(scope, data.width).into(), + ); + set_private_value( + scope, + canvas, + CANVAS_BITMAP_HEIGHT_SLOT, + v8::Integer::new_from_unsigned(scope, data.height).into(), + ); + set_private_value( + scope, + canvas, + CANVAS_BITMAP_PREMULTIPLIED_SLOT, + v8::Boolean::new(scope, data.premultiplied).into(), + ); + set_private_value( + scope, + canvas, + CANVAS_BITMAP_VALID_SLOT, + v8::Boolean::new(scope, true).into(), + ); + if html_canvas_identity(scope, canvas).is_some() + && let Some((pixels, width, height)) = canvas_like_pixels_copy(scope, canvas) + { + replace_html_canvas_pixels(scope, canvas, width, height, pixels); + } +} + +fn has_valid_bitmap_output<'s>( + scope: &mut v8::PinScope<'s, '_>, + canvas: v8::Local<'s, v8::Object>, +) -> bool { + get_private_value(scope, canvas, CANVAS_BITMAP_VALID_SLOT) + .is_some_and(|value| value.boolean_value(scope)) +} + +fn blank_canvas_pixels<'s>( + scope: &mut v8::PinScope<'s, '_>, + canvas: v8::Local<'s, v8::Object>, + len: usize, +) -> Vec { + let mut pixels = vec![0; len]; + if get_private_value(scope, canvas, CANVAS_BITMAP_OPAQUE_SLOT) + .is_some_and(|value| value.boolean_value(scope)) + { + for pixel in pixels.chunks_exact_mut(4) { + pixel[3] = 255; + } + } + pixels } pub(super) fn canvas_2d_context<'s>( @@ -144,6 +261,20 @@ pub(super) fn canvas_like_pixels_copy<'s>( let mut bytes = vec![0; view.byte_length()]; let written = view.copy_contents(&mut bytes); bytes.truncate(written); + let premultiplied = get_private_value(scope, canvas, CANVAS_BITMAP_PREMULTIPLIED_SLOT) + .is_some_and(|value| value.boolean_value(scope)); + let opaque = get_private_value(scope, canvas, CANVAS_BITMAP_OPAQUE_SLOT) + .is_some_and(|value| value.boolean_value(scope)); + if opaque { + if !premultiplied { + moli_canvas::premultiply_rgba8_in_place(&mut bytes)?; + } + for pixel in bytes.chunks_exact_mut(4) { + pixel[3] = 255; + } + } else { + super::image_bitmap::unpremultiply_bitmap_pixels(&mut bytes, premultiplied); + } Some((bytes, width, height)) } @@ -168,9 +299,10 @@ fn ensure_canvas_like_backing_store<'s>( { return Some(existing); } - let bytes = new_uint8_clamped_array_from_bytes(scope, vec![0; expected_len])?; + let pixels = blank_canvas_pixels(scope, canvas, expected_len); + let bytes = new_uint8_clamped_array_from_bytes(scope, pixels.clone())?; set_private_value(scope, canvas, CANVAS_BACKING_STORE_SLOT, bytes.into()); - replace_html_canvas_pixels(scope, canvas, width, height, vec![0; expected_len]); + replace_html_canvas_pixels(scope, canvas, width, height, pixels); Some(bytes) } @@ -213,6 +345,13 @@ pub(super) fn canvas_like_dimensions<'s>( scope: &mut v8::PinScope<'s, '_>, canvas: v8::Local<'s, v8::Object>, ) -> Option<(u32, u32)> { + if has_valid_bitmap_output(scope, canvas) { + let width = + get_private_value(scope, canvas, CANVAS_BITMAP_WIDTH_SLOT)?.uint32_value(scope)?; + let height = + get_private_value(scope, canvas, CANVAS_BITMAP_HEIGHT_SLOT)?.uint32_value(scope)?; + return Some((width, height)); + } if html_canvas_identity(scope, canvas).is_some() { let width = crate::native_bridge::element::canvas_dimension_value(scope, canvas, "width", 300); diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/bitmap_renderer.rs b/moli-renderer-v8/src/context_bootstrap/canvas/bitmap_renderer.rs new file mode 100644 index 0000000000..32efb6a2bf --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/canvas/bitmap_renderer.rs @@ -0,0 +1,125 @@ +//! Ownership transfer into a canvas's ImageBitmap output surface. + +use super::{ + backing_store::{ + CANVAS_OWNER_SLOT, canvas_owner_from_context, clear_canvas_bitmap_output, + initialize_canvas_bitmap_renderer, set_canvas_bitmap_output, + }, + image_bitmap::{detach_image_bitmap, image_bitmap_data}, +}; +use crate::{ + context_bootstrap::{ensure_intrinsic_interface_prototype, throw_dom_exception_value}, + util::new_null_prototype_object, + web_api_interfaces, webidl, +}; +use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject, web_api_object_target}; + +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "ImageBitmapRenderingContextSettings")] +struct Settings { + #[webidl(default = true)] + alpha: bool, +} + +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::ImageBitmapRenderingContext)] +struct ContextSlots<'s> { + #[webapi(slot = CANVAS_OWNER_SLOT)] + canvas: v8::Local<'s, v8::Object>, +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::ImageBitmapRenderingContext, enumerable, receiver)] +struct Prototype { + #[webapi(accessor_property, getter = canvas_getter)] + canvas: (), + #[webapi(method, callback = transfer_from_image_bitmap, length = 1)] + transfer_from_image_bitmap: (), +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "ImageBitmapRenderingContext.transferFromImageBitmap")] +struct TransferArgs<'s> { + #[webidl(required, nullable, interface = web_api_interfaces::ImageBitmap)] + bitmap: Option>, +} + +pub(super) fn install<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let prototype = template.prototype_template(scope); + Prototype::initialize_prototype_template(scope, prototype); +} + +pub(crate) fn build_bitmap_renderer_context<'s>( + scope: &mut v8::PinScope<'s, '_>, + canvas: v8::Local<'s, v8::Object>, + options: v8::Local<'s, v8::Value>, +) -> Option> { + // getContext takes `any`: primitive settings are ignored before converting + // the API-specific dictionary. Reacquiring a context never reaches here. + let options = v8::Local::::try_from(options) + .unwrap_or_else(|_| new_null_prototype_object(scope)); + let settings = match webidl::parse_dictionary_object::(scope, options) { + Ok(settings) => settings, + Err(error) => { + webidl::throw_error(scope, &error); + return None; + } + }; + let realm = canvas + .get_creation_context(scope) + .unwrap_or_else(|| scope.get_current_context()); + let scope = &mut v8::ContextScope::new(scope, realm); + let prototype = + ensure_intrinsic_interface_prototype(scope, "ImageBitmapRenderingContext").ok()?; + let context = v8::Object::new(scope); + if context.set_prototype(scope, prototype.into()) != Some(true) { + return None; + } + ContextSlots::new(canvas).initialize(scope, context).ok()?; + initialize_canvas_bitmap_renderer(scope, canvas, settings.alpha); + Some(context) +} + +fn canvas_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, +) { + let Some(target) = web_api_object_target(scope, args.this()) else { + return; + }; + if let Some(canvas) = canvas_owner_from_context(scope, target) { + rv.set(canvas.into()); + } +} + +fn transfer_from_image_bitmap<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'s>, +) { + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(target) = web_api_object_target(scope, args.this()) else { + return; + }; + let Some(canvas) = canvas_owner_from_context(scope, target) else { + return; + }; + let Some(bitmap) = parsed.bitmap else { + clear_canvas_bitmap_output(scope, canvas); + return; + }; + let Some(data) = image_bitmap_data(scope, bitmap) else { + throw_dom_exception_value(scope, "The ImageBitmap is detached.", "InvalidStateError"); + return; + }; + // Move the native pixel view, retaining its premultiplication metadata. + // The source is detached only after the destination owns that view. + set_canvas_bitmap_output(scope, canvas, data); + detach_image_bitmap(scope, bitmap); +} diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap.rs b/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap.rs index f218bb2da4..7974120750 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap.rs @@ -42,7 +42,7 @@ struct ImageBitmapObjectDeclaration { } #[derive(Default, WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::ImageBitmap, enumerable)] +#[webapi(interface = web_api_interfaces::ImageBitmap, enumerable, receiver)] struct ImageBitmapPrototypeDeclaration { #[webapi( accessor_property, @@ -327,9 +327,25 @@ pub(super) fn image_bitmap_pixels_copy<'s>( scope: &mut v8::PinScope<'s, '_>, bitmap: v8::Local<'s, v8::Object>, ) -> Option<(Vec, u32, u32)> { - if !image_bitmap_receiver_branded(scope, bitmap) { - return None; - } + let data = image_bitmap_data(scope, bitmap)?; + let mut pixels = vec![0; data.pixels.byte_length()]; + data.pixels.copy_contents(&mut pixels); + unpremultiply_bitmap_pixels(&mut pixels, data.premultiplied); + Some((pixels, data.width, data.height)) +} + +pub(super) struct BitmapData<'s> { + pub(super) pixels: v8::Local<'s, v8::Uint8ClampedArray>, + pub(super) width: u32, + pub(super) height: u32, + pub(super) premultiplied: bool, +} + +pub(super) fn image_bitmap_data<'s>( + scope: &mut v8::PinScope<'s, '_>, + bitmap: v8::Local<'s, v8::Object>, +) -> Option> { + let bitmap = moli_webapi_declare::web_api_object_target(scope, bitmap)?; let width = get_private_value(scope, bitmap, IMAGE_BITMAP_WIDTH_SLOT)?.uint32_value(scope)?; let height = get_private_value(scope, bitmap, IMAGE_BITMAP_HEIGHT_SLOT)?.uint32_value(scope)?; let view = v8::Local::::try_from(get_private_value( @@ -338,11 +354,18 @@ pub(super) fn image_bitmap_pixels_copy<'s>( IMAGE_BITMAP_PIXELS_SLOT, )?) .ok()?; - let mut pixels = vec![0; view.byte_length()]; - view.copy_contents(&mut pixels); - if get_private_value(scope, bitmap, IMAGE_BITMAP_PREMULTIPLIED_SLOT) - .is_some_and(|value| value.boolean_value(scope)) - { + let premultiplied = get_private_value(scope, bitmap, IMAGE_BITMAP_PREMULTIPLIED_SLOT) + .is_some_and(|value| value.boolean_value(scope)); + Some(BitmapData { + pixels: view, + width, + height, + premultiplied, + }) +} + +pub(super) fn unpremultiply_bitmap_pixels(pixels: &mut [u8], premultiplied: bool) { + if premultiplied { for pixel in pixels.chunks_exact_mut(4) { let alpha = u32::from(pixel[3]); for channel in &mut pixel[..3] { @@ -353,7 +376,6 @@ pub(super) fn image_bitmap_pixels_copy<'s>( } } } - Some((pixels, width, height)) } fn build_image_bitmap_object<'s>( @@ -377,10 +399,9 @@ fn image_bitmap_dimension_getter<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if !image_bitmap_receiver_branded(scope, args.this()) { - throw_type_error(scope, "Illegal invocation"); + let Some(bitmap) = moli_webapi_declare::web_api_object_target(scope, args.this()) else { return; - } + }; let Some(slot) = callback_data_item( scope, &args, @@ -390,7 +411,7 @@ fn image_bitmap_dimension_getter<'s>( rv.set_uint32(0); return; }; - let value = get_private_value(scope, args.this(), slot) + let value = get_private_value(scope, bitmap, slot) .and_then(|value| value.number_value(scope)) .unwrap_or(0.0); rv.set_uint32(value.max(0.0) as u32); @@ -401,20 +422,26 @@ fn image_bitmap_close_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if !image_bitmap_receiver_branded(scope, args.this()) { - throw_type_error(scope, "Illegal invocation"); + detach_image_bitmap(scope, args.this()); + rv.set_undefined(); +} + +pub(super) fn detach_image_bitmap<'s>( + scope: &mut v8::PinScope<'s, '_>, + bitmap: v8::Local<'s, v8::Object>, +) { + let Some(bitmap) = moli_webapi_declare::web_api_object_target(scope, bitmap) else { return; - } + }; for slot in IMAGE_BITMAP_DIMENSION_SLOTS { - set_private_value(scope, args.this(), slot, v8::Number::new(scope, 0.0).into()); + set_private_value(scope, bitmap, slot, v8::Number::new(scope, 0.0).into()); } set_private_value( scope, - args.this(), + bitmap, IMAGE_BITMAP_PIXELS_SLOT, v8::undefined(scope).into(), ); - rv.set_undefined(); } pub(super) fn image_bitmap_receiver_branded<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/offscreen.rs b/moli-renderer-v8/src/context_bootstrap/canvas/offscreen.rs index 6cbad8d035..d124c4fff1 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/offscreen.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/offscreen.rs @@ -143,13 +143,10 @@ pub(crate) fn offscreen_canvas_get_context_callback<'s>( throw_type_error(scope, "Illegal invocation"); return; } - let Some(kind) = webidl::try_parse_args::(scope, &args) - .ok() - .map(|parsed| parsed.kind) - else { - rv.set_null(); + let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; + let kind = parsed.kind; if let Some(context) = get_private_value(scope, args.this(), OFFSCREEN_CANVAS_CONTEXT_SLOT) { let same_kind = get_private_value(scope, args.this(), OFFSCREEN_CANVAS_CONTEXT_KIND_SLOT) .and_then(|value| value.to_string(scope)) @@ -163,8 +160,12 @@ pub(crate) fn offscreen_canvas_get_context_callback<'s>( } let context = match kind { CanvasContextKind::TwoD => build_offscreen_2d_context_object(scope), + CanvasContextKind::BitmapRenderer => { + build_bitmap_renderer_context(scope, args.this(), args.get(1)) + } CanvasContextKind::WebGl => build_webgl_context_object(scope), CanvasContextKind::WebGl2 => build_webgl2_context_object(scope), + CanvasContextKind::WebGpu => None, }; let Some(context) = context else { rv.set_null(); @@ -178,14 +179,16 @@ pub(crate) fn offscreen_canvas_get_context_callback<'s>( OFFSCREEN_CANVAS_CONTEXT_SLOT, context.into(), ); - let kind = v8str(scope, kind.label()); + let kind_label = v8str(scope, kind.label()); set_private_value( scope, args.this(), OFFSCREEN_CANVAS_CONTEXT_KIND_SLOT, - kind.into(), + kind_label.into(), ); - attach_canvas_like_context_object(scope, args.this(), context); + if kind != CanvasContextKind::BitmapRenderer { + attach_canvas_like_context_object(scope, args.this(), context); + } rv.set(context.into()); } diff --git a/moli-renderer-v8/src/native_bridge/element/canvas.rs b/moli-renderer-v8/src/native_bridge/element/canvas.rs index 85f9a966d6..08d0094eac 100644 --- a/moli-renderer-v8/src/native_bridge/element/canvas.rs +++ b/moli-renderer-v8/src/native_bridge/element/canvas.rs @@ -1,10 +1,10 @@ use crate::{ context_bootstrap::{ - CanvasContextKind, attach_canvas_like_context_object, + CanvasContextKind, attach_canvas_like_context_object, build_bitmap_renderer_context, build_canvas_rendering_context_2d_object, build_offscreen_canvas_object, build_webgl_context_object, build_webgl2_context_object, canvas_like_to_data_url, }, - util::{get_own_static_property, object_own_static_string_property, v8_string, v8str}, + util::{get_private_value, set_private_value, v8_string}, webidl, }; @@ -15,32 +15,41 @@ use super::{reflected_attribute, set_reflected_attribute}; const CANVAS_CONTEXT_KIND_SLOT: &str = "__moliCanvasContextKind"; const CANVAS_CONTEXT_2D_SLOT: &str = "__moliCanvasContext2D"; +const CANVAS_CONTEXT_BITMAP_RENDERER_SLOT: &str = "__moliCanvasContextBitmapRenderer"; const CANVAS_CONTEXT_WEBGL_SLOT: &str = "__moliCanvasContextWebGL"; const CANVAS_CONTEXT_WEBGL2_SLOT: &str = "__moliCanvasContextWebGL2"; #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "HTMLCanvasElement.getContext")] struct HtmlCanvasGetContextArgs { - #[webidl(required, converter = "enum")] - kind: CanvasContextKind, + #[webidl(required, converter = "dom_string")] + context_id: String, } fn canvas_context_slot(kind: CanvasContextKind) -> &'static str { match kind { CanvasContextKind::TwoD => CANVAS_CONTEXT_2D_SLOT, + CanvasContextKind::BitmapRenderer => CANVAS_CONTEXT_BITMAP_RENDERER_SLOT, CanvasContextKind::WebGl => CANVAS_CONTEXT_WEBGL_SLOT, CanvasContextKind::WebGl2 => CANVAS_CONTEXT_WEBGL2_SLOT, + CanvasContextKind::WebGpu => "__moliCanvasContextWebGPU", } } fn build_canvas_context_for_kind<'s>( scope: &mut v8::PinScope<'s, '_>, kind: CanvasContextKind, + canvas: v8::Local<'s, v8::Object>, + options: v8::Local<'s, v8::Value>, ) -> Option> { match kind { CanvasContextKind::TwoD => build_canvas_rendering_context_2d_object(scope).map(Into::into), + CanvasContextKind::BitmapRenderer => { + build_bitmap_renderer_context(scope, canvas, options).map(Into::into) + } CanvasContextKind::WebGl => build_webgl_context_object(scope).map(Into::into), CanvasContextKind::WebGl2 => build_webgl2_context_object(scope).map(Into::into), + CanvasContextKind::WebGpu => None, } } @@ -169,24 +178,24 @@ pub(crate) fn canvas_get_context_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { - let Some(kind) = webidl::try_parse_args::(scope, &args) - .ok() - .map(|parsed| parsed.kind) - else { + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(kind) = CanvasContextKind::parse(&parsed.context_id) else { rv.set_null(); return; }; let canvas = args.this(); - if let Some(existing_kind) = - object_own_static_string_property(scope, canvas, CANVAS_CONTEXT_KIND_SLOT) - .and_then(|value| CanvasContextKind::parse(&value)) + if let Some(existing_kind) = get_private_value(scope, canvas, CANVAS_CONTEXT_KIND_SLOT) + .and_then(|value| value.to_string(scope)) + .and_then(|value| CanvasContextKind::parse(&value.to_rust_string_lossy(scope))) { if existing_kind != kind { rv.set_null(); return; } let slot = canvas_context_slot(kind); - if let Some(existing) = get_own_static_property(scope, canvas, slot) { + if let Some(existing) = get_private_value(scope, canvas, slot) { rv.set(existing); return; } @@ -200,7 +209,7 @@ pub(crate) fn canvas_get_context_callback<'s>( .or_else(|| canvas.get_creation_context(scope)) .unwrap_or_else(|| scope.get_current_context()); let target_scope = &mut v8::ContextScope::new(scope, relevant_context); - let Some(value) = build_canvas_context_for_kind(target_scope, kind) else { + let Some(value) = build_canvas_context_for_kind(target_scope, kind, canvas, args.get(1)) else { rv.set_null(); return; }; @@ -208,19 +217,14 @@ pub(crate) fn canvas_get_context_callback<'s>( rv.set_null(); return; }; - let _ = canvas.define_own_property( + set_private_value( target_scope, - v8str(target_scope, CANVAS_CONTEXT_KIND_SLOT).into(), + canvas, + CANVAS_CONTEXT_KIND_SLOT, kind_value.into(), - v8::PropertyAttribute::DONT_ENUM, ); let slot = canvas_context_slot(kind); - let _ = canvas.define_own_property( - target_scope, - v8str(target_scope, slot).into(), - value, - v8::PropertyAttribute::DONT_ENUM, - ); + set_private_value(target_scope, canvas, slot, value); if matches!( kind, CanvasContextKind::TwoD | CanvasContextKind::WebGl | CanvasContextKind::WebGl2 diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/bitmap_renderer.rs b/moli-renderer-v8/src/runtime/page_vm/tests/bitmap_renderer.rs new file mode 100644 index 0000000000..6de634078d --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/tests/bitmap_renderer.rs @@ -0,0 +1,89 @@ +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn bitmap_renderer_transfers_real_pixels_and_exports_snapshots_across_realms() { + run_page_vm_async_test(async move { + let mut page = test_page_vm(); + let loader = page.request_client.clone(); + page.vm_mut() + .eval("document.body.innerHTML = '';")?; + page.vm_mut().eval(&format!( + "globalThis.__bitmapRendererSurface = {};", + include_str!("../../../../tests/fixtures/canvas-bitmap-renderer-surface.js"), + ))?; + page.vm_mut().eval(include_str!( + "../../../../tests/fixtures/canvas-bitmap-renderer.js" + ))?; + let deadline = Instant::now() + Duration::from_secs(20); + while page.vm_mut().eval("typeof __uiEventResults")? == "undefined" { + for selector in [ + PageSelectedTaskTestSelector::BitmapTask, + PageSelectedTaskTestSelector::CanvasBlobSerialization, + ] { + let _ = page + .run_exact_selected_page_task_for_test(selector, &loader) + .await?; + } + anyhow::ensure!( + Instant::now() < deadline, + "bitmap renderer fixture did not finish" + ); + tokio::time::sleep(Duration::from_millis(1)).await; + } + let result: serde_json::Value = + serde_json::from_str(&page.vm_mut().eval("JSON.stringify(__uiEventResults)")?)?; + let failures: Vec<_> = result["checks"] + .as_array() + .unwrap() + .iter() + .filter(|row| row["passed"] != true) + .collect(); + assert!(failures.is_empty(), "{failures:#?}"); + assert_eq!( + result["total"], 340, + "all realm and canvas combinations must run" + ); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("bitmap renderer ownership and export fixture"); +} + +#[tokio::test(flavor = "current_thread")] +async fn bitmap_renderer_transfers_registered_native_bitmap_proxies_and_shares_detachment() { + run_page_vm_async_test(async move { + let mut page = test_page_vm(); + let loader = page.request_client.clone(); + page.vm_mut().eval(r#" + document.body.innerHTML = ''; + globalThis.nativeRenderer = new OffscreenCanvas(3, 2).getContext('bitmaprenderer'); + createImageBitmap(new ImageData(new Uint8ClampedArray([255, 0, 0, 255]), 1, 1)).then(bitmap => globalThis.nativeBitmap = bitmap); + "#)?; + let deadline = Instant::now() + Duration::from_secs(5); + while page.vm_mut().eval("typeof nativeBitmap")? == "undefined" { + let _ = page.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader).await?; + anyhow::ensure!(Instant::now() < deadline, "native bitmap was not decoded"); + tokio::time::sleep(Duration::from_millis(1)).await; + } + page.vm_mut().register_bitmap_renderer_proxies_for_test()?; + assert_eq!(page.vm_mut().eval(r#"(() => { + const other = document.getElementById('child').contentWindow; + const get = Object.getOwnPropertyDescriptor(other.ImageBitmapRenderingContext.prototype, 'canvas').get; + const transfer = other.ImageBitmapRenderingContext.prototype.transferFromImageBitmap; + const width = Object.getOwnPropertyDescriptor(other.ImageBitmap.prototype, 'width').get; + if (get.call(rendererProxy) !== nativeRenderer.canvas || width.call(bitmapProxy) !== 1) throw Error('native receiver identity'); + let traps = 0, caught; + try { transfer.call(rendererProxy, new Proxy(bitmapProxy, {getPrototypeOf() { traps++; }})); } catch(error) { caught = error; } + if (!(caught instanceof other.TypeError) || traps !== 0 || nativeBitmap.width !== 1) throw Error('author argument proxy'); + transfer.call(rendererProxy, bitmapProxy); + if (nativeBitmap.width !== 0 || width.call(bitmapProxy) !== 0) throw Error('shared source detachment'); + const read = new OffscreenCanvas(1, 1).getContext('2d'); + read.drawImage(get.call(rendererProxy), 0, 0); + if (JSON.stringify(Array.from(read.getImageData(0, 0, 1, 1).data)) !== '[255,0,0,255]') throw Error('native bitmap data'); + other.ImageBitmap.prototype.close.call(bitmapProxy); + transfer.call(rendererProxy, null); + return true; + })()"#)?, "true"); + Ok::<_, anyhow::Error>(()) + }).await.expect("registered native context and ImageBitmap proxy transfer"); +} diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index e68bc1c8f8..8a692c6dbc 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -1,4 +1,5 @@ mod bitmap; +mod bitmap_renderer; use std::{ sync::Arc, time::{Duration, Instant}, diff --git a/moli-renderer-v8/src/script_vm/tests/bitmap_renderer.rs b/moli-renderer-v8/src/script_vm/tests/bitmap_renderer.rs new file mode 100644 index 0000000000..d41ce55a9f --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/bitmap_renderer.rs @@ -0,0 +1,30 @@ +use super::*; + +impl ScriptVm { + // Register wrappers around objects produced by the real page task executor. + // Author-created Proxy objects in the fixtures remain unregistered. + pub(crate) fn register_bitmap_renderer_proxies_for_test(&mut self) -> anyhow::Result<()> { + let context_ptr = &self.page_default_context as *const _; + self.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| { + let global = scope.get_current_context().global(scope); + for (target_name, proxy_name) in [ + ("nativeRenderer", "rendererProxy"), + ("nativeBitmap", "bitmapProxy"), + ] { + let key = crate::util::v8str(scope, target_name); + let target = + v8::Local::::try_from(global.get(scope, key.into()).unwrap()) + .unwrap(); + let handler = crate::util::new_null_prototype_object(scope); + let proxy = v8::Proxy::new(scope, target, handler).unwrap(); + moli_webapi_declare::register_web_api_proxy(scope, proxy)?; + let key = crate::util::v8str(scope, proxy_name); + assert_eq!( + global.create_data_property(scope, key.into(), proxy.into()), + Some(true) + ); + } + Ok(()) + }) + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 5afce47cfc..61244e825c 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2215,6 +2215,7 @@ mod audio_event_interfaces; mod audio_param_node_interfaces; mod audio_source_interfaces; mod bitmap_rendering_context_interface_interfaces; +mod bitmap_renderer; mod blob_range; mod blob_response_headers; mod blob_storage_key; diff --git a/moli-renderer-v8/src/util.rs b/moli-renderer-v8/src/util.rs index 2851e28b43..60cf356de2 100644 --- a/moli-renderer-v8/src/util.rs +++ b/moli-renderer-v8/src/util.rs @@ -9,8 +9,8 @@ pub use moli_v8_util::{ get_private_value, get_property, global_constructor_object, global_constructor_prototype, initialize_ecmascript_intrinsic_registry, new_null_prototype_object, object_bool_property, object_chain_contains, object_defined_string_property, object_non_empty_string_property, - object_number_property, object_own_static_bool_property, object_own_static_string_property, - object_property_as_object, object_string_property, private_key, register_ecmascript_intrinsic, + object_number_property, object_own_static_bool_property, object_property_as_object, + object_string_property, private_key, register_ecmascript_intrinsic, registered_ecmascript_constructor, registered_ecmascript_prototype, set_null_prototype, set_private_value, throw_range_error, throw_type_error, v8_json_parse, v8_string, v8str, walk_object_chain, diff --git a/moli-renderer-v8/src/worker/thread/tests/bitmap_renderer.rs b/moli-renderer-v8/src/worker/thread/tests/bitmap_renderer.rs new file mode 100644 index 0000000000..25b7b99f1a --- /dev/null +++ b/moli-renderer-v8/src/worker/thread/tests/bitmap_renderer.rs @@ -0,0 +1,19 @@ +use super::*; + +#[tokio::test] +async fn worker_bitmap_renderer_exposes_native_surface_and_nullable_transfer() { + ensure_v8(); + let source = format!( + r#"const probe = {}; + const checks = probe(globalThis, globalThis, 'offscreen', 'worker'); + postMessage({{total: checks.length, failures: checks.filter(row => !row.passed)}}); + close();"#, + include_str!("../../../../tests/fixtures/canvas-bitmap-renderer-surface.js"), + ); + let mut worker = spawn_worker(source, "test://bitmap_renderer".into()); + let message = timeout(TIMEOUT, worker.recv()) + .await + .expect("timed out") + .expect("channel closed"); + assert_eq!(expect_post_json(message), r#"{"total":31,"failures":[]}"#); +} diff --git a/moli-renderer-v8/src/worker/thread/tests/mod.rs b/moli-renderer-v8/src/worker/thread/tests/mod.rs index 9e45208d76..9c0c4dd089 100644 --- a/moli-renderer-v8/src/worker/thread/tests/mod.rs +++ b/moli-renderer-v8/src/worker/thread/tests/mod.rs @@ -1530,6 +1530,7 @@ fn service_worker_storage_apis_use_explicit_registration_storage_key() { mod abort_signal_events; mod abort_signal_statics; +mod bitmap_renderer; mod canvas_fill_rect; mod canvas_transform_snapshots; mod cors_redirects; diff --git a/moli-renderer-v8/tests/fixtures/canvas-bitmap-renderer-surface.js b/moli-renderer-v8/tests/fixtures/canvas-bitmap-renderer-surface.js new file mode 100644 index 0000000000..486da8d78f --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/canvas-bitmap-renderer-surface.js @@ -0,0 +1,77 @@ +(owner, callee, kind, label) => { + const checks = []; + const check = (name, run) => { + try { + const actual = run(); + checks.push({name: `${label}: ${name}`, passed: actual === true, actual}); + } catch (error) { + checks.push({name: `${label}: ${name}`, passed: false, error: String(error)}); + } + }; + const throws = (run, Constructor) => { + try { run(); } catch (error) { return error instanceof Constructor; } + return false; + }; + const canvas = () => { + const value = kind === 'element' ? owner.document.createElement('canvas') : new owner.OffscreenCanvas(2, 3); + value.width = 2; value.height = 3; + return value; + }; + const getContext = (kind === 'element' ? callee.HTMLCanvasElement : callee.OffscreenCanvas).prototype.getContext; + const C = owner.ImageBitmapRenderingContext; + const prototype = callee.ImageBitmapRenderingContext.prototype; + const descriptor = Object.getOwnPropertyDescriptor(prototype, 'canvas'); + const transfer = prototype.transferFromImageBitmap; + const value = canvas(); + const context = getContext.call(value, 'bitmaprenderer'); + check('context creation and owner realm', () => context !== null && Object.getPrototypeOf(context) === C.prototype); + check('illegal constructor', () => C.length === 0 && throws(() => new C(), owner.TypeError)); + check('readonly canvas accessor shape', () => descriptor?.enumerable && descriptor.configurable && typeof descriptor.get === 'function' && descriptor.get.length === 0 && descriptor.set === undefined); + check('operation shape', () => typeof transfer === 'function' && transfer.length === 1 && transfer.name === 'transferFromImageBitmap' && !Object.hasOwn(transfer, 'prototype')); + check('canvas identity', () => descriptor.get.call(context) === value && !Object.hasOwn(context, 'canvas')); + check('stable context without reading options again', () => getContext.call(value, 'bitmaprenderer', {get alpha() { throw Error('reacquired options'); }}) === context); + check('other modes cannot read options or replace context', () => getContext.call(value, '2d', {get alpha() { throw Error('mode switch'); }}) === null && getContext.call(value, 'webgpu') === null && getContext.call(value, 'bitmaprenderer') === context); + check('2d mode excludes bitmaprenderer', () => getContext.call(canvasWith2d(), 'bitmaprenderer') === null); + function canvasWith2d() { const result = canvas(); getContext.call(result, '2d'); return result; } + check('context ID conversion preserves exception identity', () => { + const marker = {}; try { getContext.call(canvas(), {toString() { throw marker; }}); } catch (error) { return error === marker; } + return false; + }); + check('required context ID and Symbol conversion', () => throws(() => getContext.call(canvas()), callee.TypeError) && throws(() => getContext.call(canvas(), Symbol()), callee.TypeError)); + check('primitive settings are ignored', () => [null, undefined, true, 42, 'value', Symbol(), 1n].every(options => getContext.call(canvas(), 'bitmaprenderer', options) !== null)); + check('settings getter once and inherited', () => { + let reads = 0; + const options = Object.create({get alpha() { reads++; return true; }}); + return getContext.call(canvas(), 'bitmaprenderer', options) !== null && reads === 1; + }); + check('settings failure keeps canvas uninitialized', () => { + const marker = {}; const target = canvas(); let caught; + try { getContext.call(target, 'bitmaprenderer', {get alpha() { throw marker; }}); } catch (error) { caught = error; } + return caught === marker && getContext.call(target, '2d') !== null; + }); + check('public cache names cannot replace the context', () => { + value.__moliCanvasContextKind = '2d'; value.__moliCanvasContextBitmapRenderer = {}; + value.__moliOffscreenCanvasContextKind = '2d'; value.__moliOffscreenCanvasContext = {}; + return getContext.call(value, 'bitmaprenderer') === context && getContext.call(value, '2d') === null; + }); + check('null and undefined clear output', () => transfer.call(context, null) === undefined && transfer.call(context, undefined) === undefined); + check('missing bitmap argument', () => typeof transfer === 'function' && context !== null && throws(() => transfer.call(context), callee.TypeError)); + const revoked = Proxy.revocable(context || {}, {}); revoked.revoke(); + let traps = 0; + const trap = () => { traps++; throw Error('author trap'); }; + const invalid = [null, {}, Object.create(C.prototype), Object.create(context || {}), new Proxy(context || {}, {get: trap, getPrototypeOf: trap}), revoked.proxy]; + invalid.forEach((receiver, index) => { + check(`canvas receiver ${index}`, () => typeof descriptor?.get === 'function' && throws(() => descriptor.get.call(receiver), callee.TypeError)); + check(`transfer receiver ${index} before argument inspection`, () => typeof transfer === 'function' && throws(() => transfer.call(receiver, new Proxy({}, {get: trap, getPrototypeOf: trap})), callee.TypeError)); + }); + check('invalid bitmap values', () => typeof transfer === 'function' && context !== null && [0, false, Symbol(), {}, value, new Proxy({}, {get: trap, getPrototypeOf: trap})].every(bitmap => throws(() => transfer.call(context, bitmap), callee.TypeError))); + check('author proxy traps were not invoked', () => traps === 0); + check('public constructor replacement keeps intrinsic prototype', () => { + const saved = owner.ImageBitmapRenderingContext; + try { + owner.ImageBitmapRenderingContext = function Replaced() {}; + return Object.getPrototypeOf(getContext.call(canvas(), 'bitmaprenderer')) === saved.prototype; + } finally { owner.ImageBitmapRenderingContext = saved; } + }); + return checks; +} diff --git a/moli-renderer-v8/tests/fixtures/canvas-bitmap-renderer.js b/moli-renderer-v8/tests/fixtures/canvas-bitmap-renderer.js new file mode 100644 index 0000000000..95fff45e3e --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/canvas-bitmap-renderer.js @@ -0,0 +1,112 @@ +(async () => { + const checks = []; + const check = async (name, run) => { + try { + const actual = await run(); + checks.push({name, passed: actual === true, actual}); + } catch (error) { checks.push({name, passed: false, error: String(error)}); } + }; + const throws = (run, Constructor, name) => { + try { run(); } catch (error) { return error instanceof Constructor && (!name || error.name === name); } + return false; + }; + const equal = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected); + const realms = [globalThis, document.getElementById('child').contentWindow]; + const surface = globalThis.__bitmapRendererSurface; + for (let owner = 0; owner < realms.length; owner++) for (let callee = 0; callee < realms.length; callee++) { + const O = realms[owner], C = realms[callee]; + for (const kind of ['element', 'offscreen']) { + const prefix = `${owner}/${callee}/${kind}`; + checks.push(...surface(O, C, kind, prefix)); + const canvas = (width, height) => { + const target = kind === 'element' ? O.document.createElement('canvas') : new O.OffscreenCanvas(width, height); + target.width = width; target.height = height; return target; + }; + const transfer = C.ImageBitmapRenderingContext.prototype.transferFromImageBitmap; + const pixels = source => { + const read = new O.OffscreenCanvas(1, 1).getContext('2d'); + read.drawImage(source, 0, 0); return Array.from(read.getImageData(0, 0, 1, 1).data); + }; + const data = new O.ImageData(new O.Uint8ClampedArray([0, 255, 0, 128, 255, 0, 0, 255]), 2, 1); + for (const alpha of [true, false]) for (const premultiplyAlpha of ['none', 'premultiply', 'default']) { + await check(`${prefix}: ownership and pixels alpha=${alpha} premultiply=${premultiplyAlpha}`, async () => { + const source = await O.createImageBitmap(data, {premultiplyAlpha}); + const target = canvas(4, 3); const ctx = target.getContext('bitmaprenderer', {alpha}); + if (!ctx) return false; + transfer.call(ctx, source); + if (source.width !== 0 || source.height !== 0 || !equal(pixels(target), alpha ? [0, 255, 0, 128] : [0, 128, 0, 255])) return false; + const snapshot = await O.createImageBitmap(target); + const result = snapshot.width === 2 && snapshot.height === 1 && equal(pixels(snapshot), alpha ? [0, 255, 0, 128] : [0, 128, 0, 255]); + snapshot.close(); source.close(); source.close(); + return result && equal(pixels(target), alpha ? [0, 255, 0, 128] : [0, 128, 0, 255]); + }); + } + await check(`${prefix}: consumed source cannot be reused`, async () => { + const source = await O.createImageBitmap(data); + const target = canvas(2, 1), ctx = target.getContext('bitmaprenderer'); + if (!ctx) return false; + transfer.call(ctx, source); + const draw = new C.OffscreenCanvas(2, 1).getContext('2d'); + return throws(() => transfer.call(ctx, source), C.DOMException, 'InvalidStateError') && + throws(() => draw.drawImage(source, 0, 0), C.DOMException, 'InvalidStateError') && + await O.createImageBitmap(source).then(() => false, error => error instanceof O.DOMException && error.name === 'InvalidStateError'); + }); + await check(`${prefix}: author bitmap proxies reject without traps or detaching source`, async () => { + const source = await O.createImageBitmap(data), ctx = canvas(2, 1).getContext('bitmaprenderer'); + if (!ctx) return false; + let traps = 0; const trap = () => { traps++; throw Error('bitmap trap'); }; + const revoked = Proxy.revocable(source, {}); revoked.revoke(); + const invalid = [Object.create(source), new Proxy(source, {get: trap, getPrototypeOf: trap}), revoked.proxy]; + const result = invalid.every(value => throws(() => transfer.call(ctx, value), C.TypeError)) && traps === 0 && source.width === 2 && source.height === 1; + source.close(); return result; + }); + for (const alpha of [true, false]) { + await check(`${prefix}: null clears to current attribute size alpha=${alpha}`, async () => { + const target = canvas(4, 3), ctx = target.getContext('bitmaprenderer', {alpha}); + if (!ctx) return false; + transfer.call(ctx, await O.createImageBitmap(data)); + target.width = 5; target.height = 4; + transfer.call(ctx, null); + const result = await O.createImageBitmap(target); + const success = result.width === 5 && result.height === 4 && equal(pixels(result), [0, 0, 0, alpha ? 0 : 255]); + result.close(); return success; + }); + } + if (kind === 'element') { + await check(`${prefix}: HTML attribute resize preserves valid output`, async () => { + const target = canvas(4, 3), ctx = target.getContext('bitmaprenderer'); + if (!ctx) return false; + transfer.call(ctx, await O.createImageBitmap(data)); + target.setAttribute('width', '7'); target.height = 8; + const result = await O.createImageBitmap(target); + const success = target.width === 7 && target.height === 8 && result.width === 2 && result.height === 1 && equal(pixels(result), [0, 255, 0, 128]); + result.close(); return success; + }); + await check(`${prefix}: PNG toDataURL serializes output dimensions`, async () => { + const target = canvas(4, 3), ctx = target.getContext('bitmaprenderer'); + if (!ctx) return false; + transfer.call(ctx, await O.createImageBitmap(data)); + const encoded = O.atob(target.toDataURL().split(',')[1]); + const uint32 = index => [...encoded.slice(index, index + 4)].reduce((value, byte) => value * 256 + byte.charCodeAt(0), 0); + return encoded.slice(1, 4) === 'PNG' && uint32(16) === 2 && uint32(20) === 1; + }); + await check(`${prefix}: toBlob snapshots pixels asynchronously before clearing`, async () => { + const target = canvas(4, 3), ctx = target.getContext('bitmaprenderer'); + if (!ctx) return false; + transfer.call(ctx, await O.createImageBitmap(data)); + let called = false; + const encoded = new Promise(resolve => target.toBlob(blob => { called = true; resolve(blob); })); + transfer.call(ctx, null); + await Promise.resolve(); if (called) return false; + const blob = await encoded; + if (!(blob instanceof O.Blob) || blob.type !== 'image/png' || blob.size === 0) return false; + const result = await O.createImageBitmap(blob); + const success = result.width === 2 && result.height === 1 && equal(pixels(result), [0, 255, 0, 128]); + result.close(); return success; + }); + } + } + } + globalThis.__uiEventResults = {complete: true, total: checks.length, passed: checks.filter(row => row.passed).length, checks}; + return checks.every(row => row.passed); +})()