Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/Base64.h>
8#include <AK/Checked.h>
9#include <LibGfx/Bitmap.h>
10#include <LibGfx/PNGWriter.h>
11#include <LibWeb/CSS/StyleComputer.h>
12#include <LibWeb/DOM/Document.h>
13#include <LibWeb/HTML/CanvasRenderingContext2D.h>
14#include <LibWeb/HTML/HTMLCanvasElement.h>
15#include <LibWeb/Layout/CanvasBox.h>
16
17namespace Web::HTML {
18
19static constexpr auto max_canvas_area = 16384 * 16384;
20
21HTMLCanvasElement::HTMLCanvasElement(DOM::Document& document, DOM::QualifiedName qualified_name)
22 : HTMLElement(document, move(qualified_name))
23{
24}
25
26HTMLCanvasElement::~HTMLCanvasElement() = default;
27
28JS::ThrowCompletionOr<void> HTMLCanvasElement::initialize(JS::Realm& realm)
29{
30 MUST_OR_THROW_OOM(Base::initialize(realm));
31 set_prototype(&Bindings::ensure_web_prototype<Bindings::HTMLCanvasElementPrototype>(realm, "HTMLCanvasElement"));
32
33 return {};
34}
35
36void HTMLCanvasElement::visit_edges(Cell::Visitor& visitor)
37{
38 Base::visit_edges(visitor);
39 m_context.visit(
40 [&](JS::NonnullGCPtr<CanvasRenderingContext2D>& context) {
41 visitor.visit(context.ptr());
42 },
43 [&](JS::NonnullGCPtr<WebGL::WebGLRenderingContext>& context) {
44 visitor.visit(context.ptr());
45 },
46 [](Empty) {
47 });
48}
49
50unsigned HTMLCanvasElement::width() const
51{
52 return attribute(HTML::AttributeNames::width).to_uint().value_or(300);
53}
54
55unsigned HTMLCanvasElement::height() const
56{
57 return attribute(HTML::AttributeNames::height).to_uint().value_or(150);
58}
59
60void HTMLCanvasElement::reset_context_to_default_state()
61{
62 m_context.visit(
63 [](JS::NonnullGCPtr<CanvasRenderingContext2D>& context) {
64 context->reset_to_default_state();
65 },
66 [](JS::NonnullGCPtr<WebGL::WebGLRenderingContext>&) {
67 TODO();
68 },
69 [](Empty) {
70 // Do nothing.
71 });
72}
73
74void HTMLCanvasElement::set_width(unsigned value)
75{
76 MUST(set_attribute(HTML::AttributeNames::width, DeprecatedString::number(value)));
77 m_bitmap = nullptr;
78 reset_context_to_default_state();
79}
80
81void HTMLCanvasElement::set_height(unsigned value)
82{
83 MUST(set_attribute(HTML::AttributeNames::height, DeprecatedString::number(value)));
84 m_bitmap = nullptr;
85 reset_context_to_default_state();
86}
87
88JS::GCPtr<Layout::Node> HTMLCanvasElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
89{
90 return heap().allocate_without_realm<Layout::CanvasBox>(document(), *this, move(style));
91}
92
93HTMLCanvasElement::HasOrCreatedContext HTMLCanvasElement::create_2d_context()
94{
95 if (!m_context.has<Empty>())
96 return m_context.has<JS::NonnullGCPtr<CanvasRenderingContext2D>>() ? HasOrCreatedContext::Yes : HasOrCreatedContext::No;
97
98 m_context = CanvasRenderingContext2D::create(realm(), *this).release_value_but_fixme_should_propagate_errors();
99 return HasOrCreatedContext::Yes;
100}
101
102JS::ThrowCompletionOr<HTMLCanvasElement::HasOrCreatedContext> HTMLCanvasElement::create_webgl_context(JS::Value options)
103{
104 if (!m_context.has<Empty>())
105 return m_context.has<JS::NonnullGCPtr<WebGL::WebGLRenderingContext>>() ? HasOrCreatedContext::Yes : HasOrCreatedContext::No;
106
107 auto maybe_context = TRY(WebGL::WebGLRenderingContext::create(realm(), *this, options));
108 if (!maybe_context)
109 return HasOrCreatedContext::No;
110
111 m_context = JS::NonnullGCPtr<WebGL::WebGLRenderingContext>(*maybe_context);
112 return HasOrCreatedContext::Yes;
113}
114
115// https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext
116JS::ThrowCompletionOr<HTMLCanvasElement::RenderingContext> HTMLCanvasElement::get_context(DeprecatedString const& type, JS::Value options)
117{
118 // 1. If options is not an object, then set options to null.
119 if (!options.is_object())
120 options = JS::js_null();
121
122 // 2. Set options to the result of converting options to a JavaScript value.
123 // NOTE: No-op.
124
125 // 3. Run the steps in the cell of the following table whose column header matches this canvas element's canvas context mode and whose row header matches contextId:
126 // NOTE: See the spec for the full table.
127 if (type == "2d"sv) {
128 if (create_2d_context() == HasOrCreatedContext::Yes)
129 return JS::make_handle(*m_context.get<JS::NonnullGCPtr<HTML::CanvasRenderingContext2D>>());
130
131 return Empty {};
132 }
133
134 // NOTE: The WebGL spec says "experimental-webgl" is also acceptable and must be equivalent to "webgl". Other engines accept this, so we do too.
135 if (type.is_one_of("webgl"sv, "experimental-webgl"sv)) {
136 if (TRY(create_webgl_context(options)) == HasOrCreatedContext::Yes)
137 return JS::make_handle(*m_context.get<JS::NonnullGCPtr<WebGL::WebGLRenderingContext>>());
138
139 return Empty {};
140 }
141
142 return Empty {};
143}
144
145static Gfx::IntSize bitmap_size_for_canvas(HTMLCanvasElement const& canvas, size_t minimum_width, size_t minimum_height)
146{
147 auto width = max(canvas.width(), minimum_width);
148 auto height = max(canvas.height(), minimum_height);
149
150 Checked<size_t> area = width;
151 area *= height;
152
153 if (area.has_overflow()) {
154 dbgln("Refusing to create {}x{} canvas (overflow)", width, height);
155 return {};
156 }
157 if (area.value() > max_canvas_area) {
158 dbgln("Refusing to create {}x{} canvas (exceeds maximum size)", width, height);
159 return {};
160 }
161 return Gfx::IntSize(width, height);
162}
163
164bool HTMLCanvasElement::create_bitmap(size_t minimum_width, size_t minimum_height)
165{
166 auto size = bitmap_size_for_canvas(*this, minimum_width, minimum_height);
167 if (size.is_empty()) {
168 m_bitmap = nullptr;
169 return false;
170 }
171 if (!m_bitmap || m_bitmap->size() != size) {
172 auto bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, size);
173 if (bitmap_or_error.is_error())
174 return false;
175 m_bitmap = bitmap_or_error.release_value_but_fixme_should_propagate_errors();
176 }
177 return m_bitmap;
178}
179
180DeprecatedString HTMLCanvasElement::to_data_url(DeprecatedString const& type, [[maybe_unused]] Optional<double> quality) const
181{
182 if (!m_bitmap)
183 return {};
184 if (type != "image/png")
185 return {};
186 auto encoded_bitmap_or_error = Gfx::PNGWriter::encode(*m_bitmap);
187 if (encoded_bitmap_or_error.is_error()) {
188 dbgln("Gfx::PNGWriter failed to encode the HTMLCanvasElement: {}", encoded_bitmap_or_error.error());
189 return {};
190 }
191 auto base64_encoded_or_error = encode_base64(encoded_bitmap_or_error.value());
192 if (base64_encoded_or_error.is_error()) {
193 // FIXME: propagate error
194 return {};
195 }
196 return AK::URL::create_with_data(type, base64_encoded_or_error.release_value().to_deprecated_string(), true).to_deprecated_string();
197}
198
199void HTMLCanvasElement::present()
200{
201 m_context.visit(
202 [](JS::NonnullGCPtr<CanvasRenderingContext2D>&) {
203 // Do nothing, CRC2D writes directly to the canvas bitmap.
204 },
205 [](JS::NonnullGCPtr<WebGL::WebGLRenderingContext>& context) {
206 context->present();
207 },
208 [](Empty) {
209 // Do nothing.
210 });
211}
212
213}