ladybird/Libraries/LibWeb/HTML/OffscreenCanvas.cpp
Timothy Flynn 64abc6101d LibWeb+WebWorker: Use IPC mechanics for structured serialization
Our structured serialization implementation had its own bespoke encoder
and decoder to serialize JS values. It also used a u32 buffer under the
hood, which made using its structures a bit awkward. We had previously
worked around its data structures in transferable streams, which nested
transfers of MessagePort instances. We basically had to add hooks into
the MessagePort to route to the correct transfer receiving steps, and
we could not invoke the correct AOs directly as the spec dictates.

We now use IPC mechanics to encode and decode data. This works because,
although we are encoding JS values, we are only ultimately encoding
primitive and basic AK types. The resulting data structures actually
enforce that we implement transferable streams exactly as the spec is
worded (I had planned to do that in a separate commit, but the fallout
of this patch actually required that change).
2025-07-18 10:09:02 -04:00

397 lines
15 KiB
C++

/*
* 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)
{
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 });
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<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
OffscreenCanvas::OffscreenCanvas(JS::Realm& realm, RefPtr<Gfx::Bitmap> bitmap)
: EventTarget(realm)
, m_bitmap { move(bitmap) }
{
}
OffscreenCanvas::~OffscreenCanvas() = default;
WebIDL::ExceptionOr<void> OffscreenCanvas::transfer_steps(HTML::TransferDataEncoder&)
{
// FIXME: Implement this
dbgln("(STUBBED) OffscreenCanvas::transfer_steps(HTML::TransferDataEncoder&)");
return {};
}
WebIDL::ExceptionOr<void> OffscreenCanvas::transfer_receiving_steps(HTML::TransferDataDecoder&)
{
// FIXME: Implement this
dbgln("(STUBBED) OffscreenCanvas::transfer_receiving_steps(HTML::TransferDataDecoder&)");
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<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.
});
}
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<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.
});
}
RefPtr<Gfx::Bitmap> OffscreenCanvas::bitmap() const
{
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);
set_new_bitmap_size(current_size);
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);
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<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>()) {
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<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) {
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<Gfx::Bitmap> 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<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()) {
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<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;
}
}