2025-03-07 00:19:45 +01:00
/*
* Copyright ( c ) 2025 , Ladybird contributors
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <AK/Tuple.h>
# include <LibWeb/Bindings/OffscreenCanvasPrototype.h>
# include <LibWeb/HTML/Canvas/SerializeBitmap.h>
# include <LibWeb/HTML/OffscreenCanvas.h>
# include <LibWeb/HTML/OffscreenCanvasRenderingContext2D.h>
# include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
# include <LibWeb/HTML/Window.h>
# include <LibWeb/Platform/EventLoopPlugin.h>
# include <LibWeb/WebGL/WebGL2RenderingContext.h>
# include <LibWeb/WebGL/WebGLRenderingContext.h>
namespace Web : : HTML {
GC_DEFINE_ALLOCATOR ( OffscreenCanvas ) ;
GC : : Ref < OffscreenCanvas > OffscreenCanvas : : create ( JS : : Realm & realm , WebIDL : : UnsignedLong width ,
WebIDL : : UnsignedLong height )
{
return MUST ( OffscreenCanvas : : construct_impl ( realm , width , height ) ) ;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-offscreencanvas
WebIDL : : ExceptionOr < GC : : Ref < OffscreenCanvas > > OffscreenCanvas : : construct_impl (
JS : : Realm & realm ,
WebIDL : : UnsignedLong width ,
WebIDL : : UnsignedLong height )
{
2025-07-01 01:19:08 +01:00
RefPtr < Gfx : : Bitmap > bitmap ;
if ( width > 0 & & height > 0 ) {
// The new OffscreenCanvas(width, height) constructor steps are:
auto bitmap_or_error = Gfx : : Bitmap : : create ( Gfx : : BitmapFormat : : RGBA8888 , Gfx : : IntSize { width , height } ) ;
2025-03-07 00:19:45 +01:00
2025-07-01 01:19:08 +01:00
if ( bitmap_or_error . is_error ( ) ) {
2025-08-07 19:31:52 -04:00
return WebIDL : : InvalidStateError : : create ( realm , Utf16String : : formatted ( " Error in allocating bitmap: {} " , bitmap_or_error . error ( ) ) ) ;
2025-07-01 01:19:08 +01:00
}
bitmap = bitmap_or_error . release_value ( ) ;
2025-03-07 00:19:45 +01:00
}
// 1. Initialize the bitmap of this to a rectangular array of transparent black pixels of the dimensions specified by width and height.
// noop, the pixel value to set is equal to 0x00000000, which the bitmap already contains
// 2. Initialize the width of this to width.
// 3. Initialize the height of this to height.
// noop, we use the height and width from the bitmap
// FIXME: 4. Set this's inherited language to explicitly unknown.
// FIXME: 5. Set this's inherited direction to "ltr".
// 6. Let global be the relevant global object of this.
auto & global = realm . global_object ( ) ;
// 7. If global is a Window object:
if ( is < HTML : : Window > ( global ) ) {
auto & window = as < HTML : : Window > ( global ) ;
// 1.Let element be the document element of global's associated Document.
auto * element = window . associated_document ( ) . document_element ( ) ;
// 2. If element is not null :
if ( element ) {
// FIXME: 1. Set the inherited language of this to element's language.
// FIXME: 2. Set the inherited direction of this to element's directionality.
}
}
return realm . create < OffscreenCanvas > ( realm , bitmap ) ;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-offscreencanvas
2025-07-01 01:19:08 +01:00
OffscreenCanvas : : OffscreenCanvas ( JS : : Realm & realm , RefPtr < Gfx : : Bitmap > bitmap )
2025-03-07 00:19:45 +01:00
: EventTarget ( realm )
, m_bitmap { move ( bitmap ) }
{
}
OffscreenCanvas : : ~ OffscreenCanvas ( ) = default ;
2025-07-17 09:51:04 -04:00
WebIDL : : ExceptionOr < void > OffscreenCanvas : : transfer_steps ( HTML : : TransferDataEncoder & )
2025-03-07 00:19:45 +01:00
{
// FIXME: Implement this
2025-07-17 09:51:04 -04:00
dbgln ( " (STUBBED) OffscreenCanvas::transfer_steps(HTML::TransferDataEncoder&) " ) ;
2025-03-07 00:19:45 +01:00
return { } ;
}
2025-07-17 09:51:04 -04:00
WebIDL : : ExceptionOr < void > OffscreenCanvas : : transfer_receiving_steps ( HTML : : TransferDataDecoder & )
2025-03-07 00:19:45 +01:00
{
// FIXME: Implement this
2025-07-17 09:51:04 -04:00
dbgln ( " (STUBBED) OffscreenCanvas::transfer_receiving_steps(HTML::TransferDataDecoder&) " ) ;
2025-03-07 00:19:45 +01:00
return { } ;
}
HTML : : TransferType OffscreenCanvas : : primary_interface ( ) const
{
// FIXME: Implement this
dbgln ( " (STUBBED) OffscreenCanvas::primary_interface() " ) ;
return { } ;
}
WebIDL : : UnsignedLong OffscreenCanvas : : width ( ) const
{
2025-07-01 01:19:08 +01:00
if ( ! m_bitmap )
return 0 ;
2025-03-07 00:19:45 +01:00
return m_bitmap - > size ( ) . width ( ) ;
}
WebIDL : : UnsignedLong OffscreenCanvas : : height ( ) const
{
2025-07-01 01:19:08 +01:00
if ( ! m_bitmap )
return 0 ;
2025-03-07 00:19:45 +01:00
return m_bitmap - > size ( ) . height ( ) ;
}
void OffscreenCanvas : : reset_context_to_default_state ( )
{
m_context . visit (
[ ] ( GC : : Ref < OffscreenCanvasRenderingContext2D > & context ) {
context - > reset_to_default_state ( ) ;
} ,
[ ] ( GC : : Ref < WebGL : : WebGLRenderingContext > & context ) {
context - > reset_to_default_state ( ) ;
} ,
[ ] ( GC : : Ref < WebGL : : WebGL2RenderingContext > & context ) {
context - > reset_to_default_state ( ) ;
} ,
[ ] ( Empty ) {
// Do nothing.
} ) ;
}
2025-09-20 18:36:10 +02:00
WebIDL : : ExceptionOr < void > OffscreenCanvas : : set_new_bitmap_size ( Gfx : : IntSize new_size )
2025-03-07 00:19:45 +01:00
{
2025-07-01 01:19:08 +01:00
if ( new_size . width ( ) = = 0 | | new_size . height ( ) = = 0 )
m_bitmap = nullptr ;
else {
2025-09-20 18:36:10 +02:00
// FIXME: Other browsers appear to not throw for unreasonable sizes being set. We could consider deferring allocation of the bitmap until it is used,
// but for now, lets just allocate it here and throw if it fails instead of crashing.
auto bitmap_or_error = Gfx : : Bitmap : : create ( Gfx : : BitmapFormat : : RGBA8888 , Gfx : : IntSize { new_size . width ( ) , new_size . height ( ) } ) ;
if ( bitmap_or_error . is_error ( ) ) {
return WebIDL : : InvalidStateError : : create ( realm ( ) , Utf16String : : formatted ( " Error in allocating bitmap: {} " , bitmap_or_error . error ( ) ) ) ;
}
m_bitmap = bitmap_or_error . release_value ( ) ;
2025-07-01 01:19:08 +01:00
}
2025-03-07 00:19:45 +01:00
m_context . visit (
[ & ] ( GC : : Ref < OffscreenCanvasRenderingContext2D > & context ) {
context - > set_size ( new_size ) ;
} ,
[ & ] ( GC : : Ref < WebGL : : WebGLRenderingContext > & context ) {
context - > set_size ( new_size ) ;
} ,
[ & ] ( GC : : Ref < WebGL : : WebGL2RenderingContext > & context ) {
context - > set_size ( new_size ) ;
} ,
[ ] ( Empty ) {
// Do nothing.
} ) ;
2025-09-20 18:36:10 +02:00
return { } ;
2025-03-07 00:19:45 +01:00
}
2025-09-20 18:36:10 +02:00
2025-07-01 01:19:08 +01:00
RefPtr < Gfx : : Bitmap > OffscreenCanvas : : bitmap ( ) const
2025-03-07 00:19:45 +01:00
{
return m_bitmap ;
}
WebIDL : : ExceptionOr < void > OffscreenCanvas : : set_width ( WebIDL : : UnsignedLong value )
{
Gfx : : IntSize current_size = bitmap_size_for_canvas ( ) ;
current_size . set_width ( value ) ;
2025-09-20 18:36:10 +02:00
TRY ( set_new_bitmap_size ( current_size ) ) ;
2025-03-07 00:19:45 +01:00
reset_context_to_default_state ( ) ;
return { } ;
}
WebIDL : : ExceptionOr < void > OffscreenCanvas : : set_height ( WebIDL : : UnsignedLong value )
{
Gfx : : IntSize current_size = bitmap_size_for_canvas ( ) ;
current_size . set_height ( value ) ;
2025-09-20 18:36:10 +02:00
TRY ( set_new_bitmap_size ( current_size ) ) ;
2025-03-07 00:19:45 +01:00
reset_context_to_default_state ( ) ;
return { } ;
}
Gfx : : IntSize OffscreenCanvas : : bitmap_size_for_canvas ( ) const
{
2025-07-01 01:19:08 +01:00
if ( ! m_bitmap )
return { 0 , 0 } ;
2025-03-07 00:19:45 +01:00
return m_bitmap - > size ( ) ;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-offscreencanvas-getcontext
JS : : ThrowCompletionOr < OffscreenRenderingContext > OffscreenCanvas : : get_context ( Bindings : : OffscreenRenderingContextId contextId , JS : : Value options )
{
// 1. If options is not an object, then set options to null.
if ( ! options . is_object ( ) )
options = JS : : js_null ( ) ;
// 2. Set options to the result of converting options to a JavaScript value.
// NOTE: No-op.
// 3. Run the steps in the cell of the following table whose column header matches this OffscreenCanvas object's context mode and whose row header matches contextId:
// NOTE: See the spec for the full table.
if ( contextId = = Bindings : : OffscreenRenderingContextId : : _2d ) {
if ( TRY ( create_2d_context ( options ) ) = = HasOrCreatedContext : : Yes )
return GC : : make_root ( * m_context . get < GC : : Ref < HTML : : OffscreenCanvasRenderingContext2D > > ( ) ) ;
return Empty { } ;
}
if ( contextId = = Bindings : : OffscreenRenderingContextId : : Webgl ) {
dbgln ( " (STUBBED) OffscreenCanvas::get_context(Webgl) " ) ;
return Empty { } ;
}
if ( contextId = = Bindings : : OffscreenRenderingContextId : : Webgl2 ) {
dbgln ( " (STUBBED) OffscreenCanvas::get_context(Webgl2) " ) ;
return Empty { } ;
}
return Empty { } ;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-offscreencanvas-transfertoimagebitmap
WebIDL : : ExceptionOr < GC : : Ref < ImageBitmap > > OffscreenCanvas : : transfer_to_image_bitmap ( )
{
// The transferToImageBitmap() method, when invoked, must run the following steps :
// FIXME: 1. If the value of this OffscreenCanvas object's [[Detached]] internal slot is set to true, then throw an "InvalidStateError" DOMException.
// 2. If this OffscreenCanvas object's context mode is set to none, then throw an "InvalidStateError" DOMException.
if ( m_context . has < Empty > ( ) ) {
2025-08-07 19:31:52 -04:00
return WebIDL : : InvalidStateError : : create ( realm ( ) , " OffscreenCanvas has no context " _utf16 ) ;
2025-03-07 00:19:45 +01:00
}
// 3. Let image be a newly created ImageBitmap object that references the same underlying bitmap data as this OffscreenCanvas object's bitmap.
auto image = ImageBitmap : : create ( realm ( ) ) ;
image - > set_bitmap ( m_bitmap ) ;
2025-07-01 01:19:08 +01:00
// 4. Set this OffscreenCanvas object's bitmap to reference a newly created bitmap of the same dimensions and color space as the previous bitmap, and with its pixels initialized to transparent black, or opaque black if the rendering context' s alpha is false.
2025-03-07 00:19:45 +01:00
// FIXME: implement the checking of the alpha from the context
auto size = bitmap_size_for_canvas ( ) ;
2025-07-01 01:19:08 +01:00
if ( size . is_empty ( ) ) {
m_bitmap = nullptr ;
} else {
m_bitmap = MUST ( Gfx : : Bitmap : : create ( Gfx : : BitmapFormat : : RGBA8888 , size ) ) ;
}
2025-03-07 00:19:45 +01:00
// 5. Return image.
return image ;
}
static Tuple < FlyString , Optional < double > > options_convert_or_default ( Optional < ImageEncodeOptions > options )
{
if ( ! options . has_value ( ) ) {
return { " image/png " _fly_string , { } } ;
}
return { options - > type , options - > quality } ;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-offscreencanvas-converttoblob
GC : : Ref < WebIDL : : Promise > OffscreenCanvas : : convert_to_blob ( Optional < ImageEncodeOptions > maybe_options )
{
// The convertToBlob(options) method, when invoked, must run the following steps:
// FIXME: 1. If the value of this OffscreenCanvas object's [[Detached]] internal slot is set to true, then return a promise rejected with an "InvalidStateError" DOMException.
// FIXME 2. If this OffscreenCanvas object's context mode is 2d and the rendering context's output bitmap's origin-clean flag is set to false, then return a promise rejected with a "SecurityError" DOMException.
auto size = bitmap_size_for_canvas ( ) ;
// 3. If this OffscreenCanvas object's bitmap has no pixels (i.e., either its horizontal dimension or its vertical dimension is zero) then return a promise rejected with an "IndexSizeError" DOMException.
if ( size . height ( ) = = 0 or size . width ( ) = = 0 ) {
2025-08-07 19:31:52 -04:00
auto error = WebIDL : : IndexSizeError : : create ( realm ( ) , " OffscreenCanvas has invalid dimensions. The bitmap has no pixels " _utf16 ) ;
2025-03-07 00:19:45 +01:00
return WebIDL : : create_rejected_promise_from_exception ( realm ( ) , error ) ;
}
// 4. Let bitmap be a copy of this OffscreenCanvas object's bitmap.
2025-07-01 01:19:08 +01:00
RefPtr < Gfx : : Bitmap > bitmap ;
if ( m_bitmap )
bitmap = MUST ( m_bitmap - > clone ( ) ) ;
2025-03-07 00:19:45 +01:00
// 5. Let result be a new promise object.
auto result_promise = WebIDL : : create_promise ( realm ( ) ) ;
// 6. Run these steps in parallel:
Platform : : EventLoopPlugin : : the ( ) . deferred_invoke ( GC : : create_function ( heap ( ) , [ this , result_promise , bitmap , maybe_options ] {
// 1. Let file be a serialization of bitmap as a file, with options's type and quality if present.
Optional < SerializeBitmapResult > file_result { } ;
auto options = options_convert_or_default ( maybe_options ) ;
if ( auto result = serialize_bitmap ( * bitmap , options . get < 0 > ( ) , options . get < 1 > ( ) ) ; ! result . is_error ( ) )
file_result = result . release_value ( ) ;
// 2. Queue an element task on the canvas blob serialization task source given the canvas element to run these steps:
// FIXME: wait for spec bug to be resolve: https://github.com/whatwg/html/issues/11101
// AD-HOC: queue the task in an appropiate queue. This depends if the global object is a window or a worker
Function < void ( ) > task_to_queue = [ this , result_promise , file_result = move ( file_result ) ] - > void {
HTML : : TemporaryExecutionContext context ( realm ( ) , HTML : : TemporaryExecutionContext : : CallbacksEnabled : : Yes ) ;
// 1. If file is null, then reject result with an "EncodingError" DOMException.
if ( ! file_result . has_value ( ) ) {
2025-08-07 19:31:52 -04:00
auto error = WebIDL : : EncodingError : : create ( realm ( ) , " Failed to convert OffscreenCanvas to Blob " _utf16 ) ;
2025-03-07 00:19:45 +01:00
WebIDL : : reject_promise ( realm ( ) , result_promise , error ) ;
} else {
// 1. If result is non-null, resolve result with a new Blob object, created in the relevant realm of this OffscreenCanvas object, representing file. [FILEAPI]
auto type = String : : from_utf8 ( file_result - > mime_type ) ;
if ( type . is_error ( ) ) {
2025-08-07 19:31:52 -04:00
auto error = WebIDL : : EncodingError : : create ( realm ( ) , Utf16String : : formatted ( " OOM Error while converting string in OffscreenCanvas to blob: {} " , type . error ( ) ) ) ;
2025-03-07 00:19:45 +01:00
WebIDL : : reject_promise ( realm ( ) , result_promise , error ) ;
return ;
}
GC : : Ptr < FileAPI : : Blob > blob_result = FileAPI : : Blob : : create ( realm ( ) , file_result - > buffer , type . release_value ( ) ) ;
WebIDL : : resolve_promise ( realm ( ) , result_promise , blob_result ) ;
}
} ;
auto & global_object = HTML : : relevant_global_object ( * this ) ;
// AD-HOC: if the global_object is a window, queue an element task on the canvas blob serialization task source
if ( is < HTML : : Window > ( global_object ) ) {
auto & window = as < HTML : : Window > ( global_object ) ;
window . associated_document ( ) . document_element ( ) - > queue_an_element_task ( Task : : Source : : CanvasBlobSerializationTask , move ( task_to_queue ) ) ;
return ;
}
// AD-HOC: the global object only can be a worker or a window
VERIFY ( is < HTML : : WorkerGlobalScope > ( global_object ) ) ;
auto & worker = as < HTML : : WorkerGlobalScope > ( global_object ) ;
// AD-HOC: if the global_object is a worker, queue a global task on the canvas blob serialization task source
HTML : : queue_global_task ( Task : : Source : : CanvasBlobSerializationTask , worker , GC : : create_function ( heap ( ) , move ( task_to_queue ) ) ) ;
} ) ) ;
// 7. Return result.
return result_promise ;
}
void OffscreenCanvas : : set_oncontextlost ( GC : : Ptr < WebIDL : : CallbackType > event_handler )
{
set_event_handler_attribute ( HTML : : EventNames : : contextlost , event_handler ) ;
}
GC : : Ptr < WebIDL : : CallbackType > OffscreenCanvas : : oncontextlost ( )
{
return event_handler_attribute ( HTML : : EventNames : : contextlost ) ;
}
void OffscreenCanvas : : set_oncontextrestored ( GC : : Ptr < WebIDL : : CallbackType > event_handler )
{
set_event_handler_attribute ( HTML : : EventNames : : contextrestored , event_handler ) ;
}
GC : : Ptr < WebIDL : : CallbackType > OffscreenCanvas : : oncontextrestored ( )
{
return event_handler_attribute ( HTML : : EventNames : : contextrestored ) ;
}
void OffscreenCanvas : : initialize ( JS : : Realm & realm )
{
Base : : initialize ( realm ) ;
WEB_SET_PROTOTYPE_FOR_INTERFACE ( OffscreenCanvas ) ;
}
void OffscreenCanvas : : visit_edges ( Cell : : Visitor & visitor )
{
Base : : visit_edges ( visitor ) ;
m_context . visit (
[ & ] ( GC : : Ref < OffscreenCanvasRenderingContext2D > & context ) {
visitor . visit ( context ) ;
} ,
[ & ] ( GC : : Ref < WebGL : : WebGLRenderingContext > & context ) {
visitor . visit ( context ) ;
} ,
[ & ] ( GC : : Ref < WebGL : : WebGL2RenderingContext > & context ) {
visitor . visit ( context ) ;
} ,
[ ] ( Empty ) {
} ) ;
}
JS : : ThrowCompletionOr < OffscreenCanvas : : HasOrCreatedContext > OffscreenCanvas : : create_2d_context ( JS : : Value options )
{
if ( ! m_context . has < Empty > ( ) )
return m_context . has < GC : : Ref < OffscreenCanvasRenderingContext2D > > ( ) ? HasOrCreatedContext : : Yes : HasOrCreatedContext : : No ;
m_context = TRY ( OffscreenCanvasRenderingContext2D : : create ( realm ( ) , * this , options ) ) ;
return HasOrCreatedContext : : Yes ;
}
}