/* * Copyright (c) 2025, Ladybird contributors * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include namespace Web::HTML { GC_DEFINE_ALLOCATOR(OffscreenCanvas); GC::Ref 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> OffscreenCanvas::construct_impl( JS::Realm& realm, WebIDL::UnsignedLong width, WebIDL::UnsignedLong height) { RefPtr 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 }); if (bitmap_or_error.is_error()) { return WebIDL::InvalidStateError::create(realm, MUST(String::formatted("Error in allocating bitmap: {}", bitmap_or_error.error()))); } bitmap = bitmap_or_error.release_value(); } // 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(global)) { auto& window = as(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(realm, bitmap); } // https://html.spec.whatwg.org/multipage/canvas.html#dom-offscreencanvas OffscreenCanvas::OffscreenCanvas(JS::Realm& realm, RefPtr bitmap) : EventTarget(realm) , m_bitmap { move(bitmap) } { } OffscreenCanvas::~OffscreenCanvas() = default; WebIDL::ExceptionOr OffscreenCanvas::transfer_steps(HTML::TransferDataHolder&) { // FIXME: Implement this dbgln("(STUBBED) OffscreenCanvas::transfer_steps(HTML::TransferDataHolder&)"); return {}; } WebIDL::ExceptionOr OffscreenCanvas::transfer_receiving_steps(HTML::TransferDataHolder&) { // FIXME: Implement this dbgln("(STUBBED) OffscreenCanvas::transfer_receiving_steps(HTML::TransferDataHolder&)"); return {}; } HTML::TransferType OffscreenCanvas::primary_interface() const { // FIXME: Implement this dbgln("(STUBBED) OffscreenCanvas::primary_interface()"); return {}; } WebIDL::UnsignedLong OffscreenCanvas::width() const { if (!m_bitmap) return 0; return m_bitmap->size().width(); } WebIDL::UnsignedLong OffscreenCanvas::height() const { if (!m_bitmap) return 0; return m_bitmap->size().height(); } void OffscreenCanvas::reset_context_to_default_state() { m_context.visit( [](GC::Ref& context) { context->reset_to_default_state(); }, [](GC::Ref& context) { context->reset_to_default_state(); }, [](GC::Ref& context) { context->reset_to_default_state(); }, [](Empty) { // Do nothing. }); } void OffscreenCanvas::set_new_bitmap_size(Gfx::IntSize new_size) { if (new_size.width() == 0 || new_size.height() == 0) m_bitmap = nullptr; else { m_bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::RGBA8888, Gfx::IntSize { new_size.width(), new_size.height() })); } m_context.visit( [&](GC::Ref& context) { context->set_size(new_size); }, [&](GC::Ref& context) { context->set_size(new_size); }, [&](GC::Ref& context) { context->set_size(new_size); }, [](Empty) { // Do nothing. }); } RefPtr OffscreenCanvas::bitmap() const { return m_bitmap; } WebIDL::ExceptionOr OffscreenCanvas::set_width(WebIDL::UnsignedLong value) { Gfx::IntSize current_size = bitmap_size_for_canvas(); current_size.set_width(value); set_new_bitmap_size(current_size); reset_context_to_default_state(); return {}; } WebIDL::ExceptionOr OffscreenCanvas::set_height(WebIDL::UnsignedLong value) { Gfx::IntSize current_size = bitmap_size_for_canvas(); current_size.set_height(value); set_new_bitmap_size(current_size); reset_context_to_default_state(); return {}; } Gfx::IntSize OffscreenCanvas::bitmap_size_for_canvas() const { if (!m_bitmap) return { 0, 0 }; return m_bitmap->size(); } // https://html.spec.whatwg.org/multipage/canvas.html#dom-offscreencanvas-getcontext JS::ThrowCompletionOr 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>()); 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> 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()) { return WebIDL::InvalidStateError::create(realm(), "OffscreenCanvas has no context"_string); } // 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); // 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. // FIXME: implement the checking of the alpha from the context auto size = bitmap_size_for_canvas(); if (size.is_empty()) { m_bitmap = nullptr; } else { m_bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::RGBA8888, size)); } // 5. Return image. return image; } static Tuple> options_convert_or_default(Optional 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 OffscreenCanvas::convert_to_blob(Optional 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) { auto error = WebIDL::IndexSizeError::create(realm(), "OffscreenCanvas has invalid dimensions. The bitmap has no pixels"_string); return WebIDL::create_rejected_promise_from_exception(realm(), error); } // 4. Let bitmap be a copy of this OffscreenCanvas object's bitmap. RefPtr bitmap; if (m_bitmap) bitmap = MUST(m_bitmap->clone()); // 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 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 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()) { auto error = WebIDL::EncodingError::create(realm(), "Failed to convert OffscreenCanvas to Blob"_string); 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()) { auto error = WebIDL::EncodingError::create(realm(), MUST(String::formatted("OOM Error while converting string in OffscreenCanvas to blob: {}"_string, type.error()))); WebIDL::reject_promise(realm(), result_promise, error); return; } GC::Ptr 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(global_object)) { auto& window = as(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(global_object)); auto& worker = as(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 event_handler) { set_event_handler_attribute(HTML::EventNames::contextlost, event_handler); } GC::Ptr OffscreenCanvas::oncontextlost() { return event_handler_attribute(HTML::EventNames::contextlost); } void OffscreenCanvas::set_oncontextrestored(GC::Ptr event_handler) { set_event_handler_attribute(HTML::EventNames::contextrestored, event_handler); } GC::Ptr 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& context) { visitor.visit(context); }, [&](GC::Ref& context) { visitor.visit(context); }, [&](GC::Ref& context) { visitor.visit(context); }, [](Empty) { }); } JS::ThrowCompletionOr OffscreenCanvas::create_2d_context(JS::Value options) { if (!m_context.has()) return m_context.has>() ? HasOrCreatedContext::Yes : HasOrCreatedContext::No; m_context = TRY(OffscreenCanvasRenderingContext2D::create(realm(), *this, options)); return HasOrCreatedContext::Yes; } }