From 3f0875a32d321a19bda308c0ccc8c14917ff094b Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 9 Oct 2026 15:23:49 +0800 Subject: [PATCH] fix(canvas): support native bitmaprenderer transfers Expose ImageBitmapRenderingContext through HTML and Offscreen canvas modes. Transfer the native pixel backing, detach the source, preserve natural output dimensions, and share canvas readback and PNG/Blob exports. Use generated WebIDL receiver and argument checks, including registered native Proxies. Protect context caches with private slots and apply alpha settings through the shared dictionary derive. Validate actual bitmap tasks, cross-realm pixel and export behavior, and worker surface contracts. Full fmt, workspace clippy and nextest pass. --- moli-renderer-v8/src/context_bootstrap.rs | 7 +- .../src/context_bootstrap/canvas.rs | 12 +- .../context_bootstrap/canvas/backing_store.rs | 149 +++++++++++++++++- .../canvas/bitmap_renderer.rs | 125 +++++++++++++++ .../context_bootstrap/canvas/image_bitmap.rs | 67 +++++--- .../src/context_bootstrap/canvas/offscreen.rs | 19 ++- .../src/native_bridge/element/canvas.rs | 48 +++--- .../runtime/page_vm/tests/bitmap_renderer.rs | 89 +++++++++++ .../src/runtime/page_vm/tests/mod.rs | 1 + .../src/script_vm/tests/bitmap_renderer.rs | 30 ++++ moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + moli-renderer-v8/src/util.rs | 4 +- .../worker/thread/tests/bitmap_renderer.rs | 19 +++ .../src/worker/thread/tests/mod.rs | 1 + .../canvas-bitmap-renderer-surface.js | 77 +++++++++ .../tests/fixtures/canvas-bitmap-renderer.js | 112 +++++++++++++ 16 files changed, 700 insertions(+), 61 deletions(-) create mode 100644 moli-renderer-v8/src/context_bootstrap/canvas/bitmap_renderer.rs create mode 100644 moli-renderer-v8/src/runtime/page_vm/tests/bitmap_renderer.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/bitmap_renderer.rs create mode 100644 moli-renderer-v8/src/worker/thread/tests/bitmap_renderer.rs create mode 100644 moli-renderer-v8/tests/fixtures/canvas-bitmap-renderer-surface.js create mode 100644 moli-renderer-v8/tests/fixtures/canvas-bitmap-renderer.js 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); +})()