LibWeb: Delegate painting surface allocation to canvas's active context

This change prepares for the addition of WebGL support, where painting
surface allocation process will differ from that of context2d.
This commit is contained in:
Aliaksandr Kalenik 2024-11-29 20:17:25 +01:00 committed by Alexander Kalenik
parent e683700fe6
commit f719b05ab9
Notes: github-actions[bot] 2024-12-03 22:37:53 +00:00
8 changed files with 116 additions and 67 deletions

View file

@ -21,6 +21,7 @@
#include <LibWeb/HTML/ImageData.h>
#include <LibWeb/HTML/Path2D.h>
#include <LibWeb/HTML/TextMetrics.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Painting/Paintable.h>
@ -41,6 +42,7 @@ CanvasRenderingContext2D::CanvasRenderingContext2D(JS::Realm& realm, HTMLCanvasE
: PlatformObject(realm)
, CanvasPath(static_cast<Bindings::PlatformObject&>(*this), *this)
, m_element(element)
, m_size(element.bitmap_size_for_canvas())
{
}
@ -198,14 +200,29 @@ void CanvasRenderingContext2D::did_draw(Gfx::FloatRect const&)
Gfx::Painter* CanvasRenderingContext2D::painter()
{
if (!canvas_element().surface()) {
if (!canvas_element().allocate_painting_surface())
return nullptr;
allocate_painting_surface_if_needed();
canvas_element().document().invalidate_display_list();
m_painter = make<Gfx::PainterSkia>(*canvas_element().surface());
}
return m_painter.ptr();
}
void CanvasRenderingContext2D::set_size(Gfx::IntSize const& size)
{
if (m_size == size)
return;
m_size = size;
m_surface = nullptr;
}
void CanvasRenderingContext2D::allocate_painting_surface_if_needed()
{
if (m_surface)
return;
auto skia_backend_context = canvas_element().navigable()->traversable_navigable()->skia_backend_context();
m_surface = Gfx::PaintingSurface::create_with_size(skia_backend_context, canvas_element().bitmap_size_for_canvas(), Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied);
}
Gfx::Path CanvasRenderingContext2D::text_path(StringView text, float x, float y, Optional<double> max_width)
{
if (max_width.has_value() && max_width.value() <= 0)

View file

@ -113,6 +113,11 @@ public:
[[nodiscard]] Gfx::Painter* painter();
void set_size(Gfx::IntSize const&);
RefPtr<Gfx::PaintingSurface> surface() { return m_surface; }
void allocate_painting_surface_if_needed();
private:
explicit CanvasRenderingContext2D(JS::Realm&, HTMLCanvasElement&);
@ -148,6 +153,9 @@ private:
// https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-origin-clean
bool m_origin_clean { true };
Gfx::IntSize m_size;
RefPtr<Gfx::PaintingSurface> m_surface;
};
enum class CanvasImageSourceUsability {

View file

@ -25,6 +25,7 @@
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/Layout/CanvasBox.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/WebGL/WebGLRenderingContext.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
namespace Web::HTML {
@ -128,13 +129,27 @@ void HTMLCanvasElement::reset_context_to_default_state()
});
}
WebIDL::ExceptionOr<void> HTMLCanvasElement::set_width(WebIDL::UnsignedLong value)
void HTMLCanvasElement::notify_context_about_canvas_size_change()
{
m_context.visit(
[&](GC::Ref<CanvasRenderingContext2D>& context) {
context->set_size(bitmap_size_for_canvas());
},
[&](GC::Ref<WebGL::WebGLRenderingContext>&) {
TODO();
},
[](Empty) {
// Do nothing.
});
}
WebIDL::ExceptionOr<void> HTMLCanvasElement::set_width(unsigned value)
{
if (value > 2147483647)
value = 300;
TRY(set_attribute(HTML::AttributeNames::width, String::number(value)));
m_surface = nullptr;
notify_context_about_canvas_size_change();
reset_context_to_default_state();
return {};
}
@ -145,7 +160,7 @@ WebIDL::ExceptionOr<void> HTMLCanvasElement::set_height(WebIDL::UnsignedLong val
value = 150;
TRY(set_attribute(HTML::AttributeNames::height, String::number(value)));
m_surface = nullptr;
notify_context_about_canvas_size_change();
reset_context_to_default_state();
return {};
}
@ -214,10 +229,10 @@ JS::ThrowCompletionOr<HTMLCanvasElement::RenderingContext> HTMLCanvasElement::ge
return Empty {};
}
static Gfx::IntSize bitmap_size_for_canvas(HTMLCanvasElement const& canvas, size_t minimum_width, size_t minimum_height)
Gfx::IntSize HTMLCanvasElement::bitmap_size_for_canvas(size_t minimum_width, size_t minimum_height) const
{
auto width = max(canvas.width(), minimum_width);
auto height = max(canvas.height(), minimum_height);
auto width = max(this->width(), minimum_width);
auto height = max(this->height(), minimum_height);
Checked<size_t> area = width;
area *= height;
@ -233,25 +248,6 @@ static Gfx::IntSize bitmap_size_for_canvas(HTMLCanvasElement const& canvas, size
return Gfx::IntSize(width, height);
}
bool HTMLCanvasElement::allocate_painting_surface(size_t minimum_width, size_t minimum_height)
{
if (m_surface)
return true;
auto traversable = document().navigable()->traversable_navigable();
VERIFY(traversable);
auto size = bitmap_size_for_canvas(*this, minimum_width, minimum_height);
if (size.is_empty()) {
m_surface = nullptr;
return false;
}
if (!m_surface || m_surface->size() != size) {
m_surface = Gfx::PaintingSurface::create_with_size(traversable->skia_backend_context(), size, Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied);
}
return m_surface;
}
struct SerializeBitmapResult {
ByteBuffer buffer;
StringView mime_type;
@ -283,21 +279,26 @@ static ErrorOr<SerializeBitmapResult> serialize_bitmap(Gfx::Bitmap const& bitmap
String HTMLCanvasElement::to_data_url(StringView type, JS::Value quality)
{
// It is possible the the canvas doesn't have a associated bitmap so create one
if (!m_surface) {
allocate_painting_surface();
allocate_painting_surface_if_needed();
auto surface = this->surface();
auto size = bitmap_size_for_canvas();
if (!size.is_empty()) {
// If the context is not initialized yet, we need to allocate transparent surface for serialization
auto skia_backend_context = navigable()->traversable_navigable()->skia_backend_context();
surface = Gfx::PaintingSurface::create_with_size(skia_backend_context, size, Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied);
}
// FIXME: 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
// 2. If this canvas element's bitmap has no pixels (i.e. either its horizontal dimension or its vertical dimension is zero)
// then return the string "data:,". (This is the shortest data: URL; it represents the empty string in a text/plain resource.)
if (!m_surface)
if (!surface)
return "data:,"_string;
// 3. Let file be a serialization of this canvas element's bitmap as a file, passing type and quality if given.
auto snapshot = Gfx::ImmutableBitmap::create_snapshot_from_painting_surface(*m_surface);
auto bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, m_surface->size()));
m_surface->read_into_bitmap(*bitmap);
auto snapshot = Gfx::ImmutableBitmap::create_snapshot_from_painting_surface(*surface);
auto bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, surface->size()));
surface->read_into_bitmap(*bitmap);
auto file = serialize_bitmap(bitmap, type, move(quality));
// 4. If file is null then return "data:,".
@ -318,8 +319,13 @@ String HTMLCanvasElement::to_data_url(StringView type, JS::Value quality)
WebIDL::ExceptionOr<void> HTMLCanvasElement::to_blob(GC::Ref<WebIDL::CallbackType> callback, StringView type, JS::Value quality)
{
// It is possible the the canvas doesn't have a associated bitmap so create one
if (!m_surface) {
allocate_painting_surface();
allocate_painting_surface_if_needed();
auto surface = this->surface();
auto size = bitmap_size_for_canvas();
if (!size.is_empty()) {
// If the context is not initialized yet, we need to allocate transparent surface for serialization
auto skia_backend_context = navigable()->traversable_navigable()->skia_backend_context();
surface = Gfx::PaintingSurface::create_with_size(skia_backend_context, size, Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied);
}
// FIXME: 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
@ -329,9 +335,9 @@ WebIDL::ExceptionOr<void> HTMLCanvasElement::to_blob(GC::Ref<WebIDL::CallbackTyp
// 3. If this canvas element's bitmap has pixels (i.e., neither its horizontal dimension nor its vertical dimension is zero),
// then set result to a copy of this canvas element's bitmap.
if (m_surface) {
bitmap_result = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, m_surface->size()));
m_surface->read_into_bitmap(*bitmap_result);
if (surface) {
bitmap_result = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, surface->size()));
surface->read_into_bitmap(*bitmap_result);
}
// 4. Run these steps in parallel:
@ -364,9 +370,8 @@ WebIDL::ExceptionOr<void> HTMLCanvasElement::to_blob(GC::Ref<WebIDL::CallbackTyp
void HTMLCanvasElement::present()
{
if (m_surface) {
m_surface->flush();
}
if (auto surface = this->surface())
surface->flush();
m_context.visit(
[](GC::Ref<CanvasRenderingContext2D>&) {
@ -380,4 +385,32 @@ void HTMLCanvasElement::present()
});
}
RefPtr<Gfx::PaintingSurface> HTMLCanvasElement::surface() const
{
return m_context.visit(
[&](GC::Ref<CanvasRenderingContext2D> const& context) {
return context->surface();
},
[&](GC::Ref<WebGL::WebGLRenderingContext> const&) -> RefPtr<Gfx::PaintingSurface> {
TODO();
},
[](Empty) -> RefPtr<Gfx::PaintingSurface> {
return {};
});
}
void HTMLCanvasElement::allocate_painting_surface_if_needed()
{
m_context.visit(
[&](GC::Ref<CanvasRenderingContext2D>& context) {
context->allocate_painting_surface_if_needed();
},
[&](GC::Ref<WebGL::WebGLRenderingContext>&) {
TODO();
},
[](Empty) {
// Do nothing.
});
}
}

View file

@ -24,11 +24,14 @@ public:
virtual ~HTMLCanvasElement() override;
bool allocate_painting_surface(size_t minimum_width = 0, size_t minimum_height = 0);
RefPtr<Gfx::PaintingSurface> surface() { return m_surface; }
RefPtr<Gfx::PaintingSurface const> surface() const { return m_surface; }
Gfx::IntSize bitmap_size_for_canvas(size_t minimum_width = 0, size_t minimum_height = 0) const;
JS::ThrowCompletionOr<RenderingContext> get_context(String const& type, JS::Value options);
enum class HasOrCreatedContext {
No,
Yes,
};
HasOrCreatedContext create_2d_context();
WebIDL::UnsignedLong width() const;
WebIDL::UnsignedLong height() const;
@ -41,6 +44,9 @@ public:
void present();
RefPtr<Gfx::PaintingSurface> surface() const;
void allocate_painting_surface_if_needed();
private:
HTMLCanvasElement(DOM::Document&, DOM::QualifiedName);
@ -52,16 +58,9 @@ private:
virtual GC::Ptr<Layout::Node> create_layout_node(CSS::StyleProperties) override;
virtual void adjust_computed_style(CSS::StyleProperties&) override;
enum class HasOrCreatedContext {
No,
Yes,
};
HasOrCreatedContext create_2d_context();
JS::ThrowCompletionOr<HasOrCreatedContext> create_webgl_context(JS::Value options);
void reset_context_to_default_state();
RefPtr<Gfx::PaintingSurface> m_surface;
void notify_context_about_canvas_size_change();
Variant<GC::Ref<HTML::CanvasRenderingContext2D>, GC::Ref<WebGL::WebGLRenderingContext>, Empty> m_context;
};

View file

@ -42,8 +42,10 @@ ErrorOr<GC::Ref<HTML::HTMLCanvasElement>, WebDriver::Error> draw_bounding_box_fr
MUST(canvas.set_height(paint_height));
// FIXME: 5. Let context, a canvas context mode, be the result of invoking the 2D context creation algorithm given canvas as the target.
if (!canvas.allocate_painting_surface(paint_width, paint_height))
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Unable to create a screenshot bitmap"sv);
canvas.create_2d_context();
canvas.allocate_painting_surface_if_needed();
if (!canvas.surface())
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Failed to allocate painting surface"sv);
// 6. Complete implementation specific steps equivalent to drawing the region of the framebuffer specified by the following coordinates onto context:
// - X coordinate: rectangle x coordinate

View file

@ -5,15 +5,12 @@
*/
#include <AK/OwnPtr.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Forward.h>
#include <LibWeb/WebGL/OpenGLContext.h>
namespace Web::WebGL {
OwnPtr<OpenGLContext> OpenGLContext::create(Gfx::PaintingSurface& bitmap)
OwnPtr<OpenGLContext> OpenGLContext::create()
{
(void)bitmap;
return {};
}

View file

@ -13,7 +13,7 @@ namespace Web::WebGL {
class OpenGLContext {
public:
static OwnPtr<OpenGLContext> create(Gfx::PaintingSurface&);
static OwnPtr<OpenGLContext> create();
virtual void present() = 0;
void clear_buffer_to_default_values();

View file

@ -39,14 +39,7 @@ JS::ThrowCompletionOr<GC::Ptr<WebGLRenderingContext>> WebGLRenderingContext::cre
// We should be coming here from getContext being called on a wrapped <canvas> element.
auto context_attributes = TRY(convert_value_to_context_attributes_dictionary(canvas_element.vm(), options));
bool created_surface = canvas_element.allocate_painting_surface(/* minimum_width= */ 1, /* minimum_height= */ 1);
if (!created_surface) {
fire_webgl_context_creation_error(canvas_element);
return GC::Ptr<WebGLRenderingContext> { nullptr };
}
VERIFY(canvas_element.surface());
auto context = OpenGLContext::create(*canvas_element.surface());
auto context = OpenGLContext::create();
if (!context) {
fire_webgl_context_creation_error(canvas_element);