/src/serenity/Userland/Libraries/LibWeb/HTML/HTMLCanvasElement.cpp
Line | Count | Source |
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 <AK/MemoryStream.h> |
10 | | #include <LibGfx/Bitmap.h> |
11 | | #include <LibGfx/ImageFormats/JPEGWriter.h> |
12 | | #include <LibGfx/ImageFormats/PNGWriter.h> |
13 | | #include <LibWeb/Bindings/ExceptionOrUtils.h> |
14 | | #include <LibWeb/Bindings/HTMLCanvasElementPrototype.h> |
15 | | #include <LibWeb/CSS/StyleComputer.h> |
16 | | #include <LibWeb/CSS/StyleValues/CSSKeywordValue.h> |
17 | | #include <LibWeb/CSS/StyleValues/RatioStyleValue.h> |
18 | | #include <LibWeb/CSS/StyleValues/StyleValueList.h> |
19 | | #include <LibWeb/DOM/Document.h> |
20 | | #include <LibWeb/HTML/CanvasRenderingContext2D.h> |
21 | | #include <LibWeb/HTML/HTMLCanvasElement.h> |
22 | | #include <LibWeb/HTML/Numbers.h> |
23 | | #include <LibWeb/HTML/Scripting/ExceptionReporter.h> |
24 | | #include <LibWeb/Layout/CanvasBox.h> |
25 | | #include <LibWeb/Platform/EventLoopPlugin.h> |
26 | | #include <LibWeb/WebIDL/AbstractOperations.h> |
27 | | |
28 | | namespace Web::HTML { |
29 | | |
30 | | JS_DEFINE_ALLOCATOR(HTMLCanvasElement); |
31 | | |
32 | | static constexpr auto max_canvas_area = 16384 * 16384; |
33 | | |
34 | | HTMLCanvasElement::HTMLCanvasElement(DOM::Document& document, DOM::QualifiedName qualified_name) |
35 | 0 | : HTMLElement(document, move(qualified_name)) |
36 | 0 | { |
37 | 0 | } |
38 | | |
39 | 0 | HTMLCanvasElement::~HTMLCanvasElement() = default; |
40 | | |
41 | | void HTMLCanvasElement::initialize(JS::Realm& realm) |
42 | 0 | { |
43 | 0 | Base::initialize(realm); |
44 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLCanvasElement); |
45 | 0 | } |
46 | | |
47 | | void HTMLCanvasElement::visit_edges(Cell::Visitor& visitor) |
48 | 0 | { |
49 | 0 | Base::visit_edges(visitor); |
50 | 0 | m_context.visit( |
51 | 0 | [&](JS::NonnullGCPtr<CanvasRenderingContext2D>& context) { |
52 | 0 | visitor.visit(context); |
53 | 0 | }, |
54 | 0 | [&](JS::NonnullGCPtr<WebGL::WebGLRenderingContext>& context) { |
55 | 0 | visitor.visit(context); |
56 | 0 | }, |
57 | 0 | [](Empty) { |
58 | 0 | }); |
59 | 0 | } |
60 | | |
61 | | void HTMLCanvasElement::apply_presentational_hints(CSS::StyleProperties& style) const |
62 | 0 | { |
63 | | // https://html.spec.whatwg.org/multipage/rendering.html#attributes-for-embedded-content-and-images |
64 | | // The width and height attributes map to the aspect-ratio property on canvas elements. |
65 | | |
66 | | // FIXME: Multiple elements have aspect-ratio presentational hints, make this into a helper function |
67 | | |
68 | | // https://html.spec.whatwg.org/multipage/rendering.html#map-to-the-aspect-ratio-property |
69 | | // if element has both attributes w and h, and parsing those attributes' values using the rules for parsing non-negative integers doesn't generate an error for either |
70 | 0 | auto w = parse_non_negative_integer(get_attribute_value(HTML::AttributeNames::width)); |
71 | 0 | auto h = parse_non_negative_integer(get_attribute_value(HTML::AttributeNames::height)); |
72 | |
|
73 | 0 | if (w.has_value() && h.has_value()) |
74 | | // then the user agent is expected to use the parsed integers as a presentational hint for the 'aspect-ratio' property of the form auto w / h. |
75 | 0 | style.set_property(CSS::PropertyID::AspectRatio, |
76 | 0 | CSS::StyleValueList::create(CSS::StyleValueVector { |
77 | 0 | CSS::CSSKeywordValue::create(CSS::Keyword::Auto), |
78 | 0 | CSS::RatioStyleValue::create(CSS::Ratio { static_cast<double>(w.value()), static_cast<double>(h.value()) }) }, |
79 | |
|
80 | 0 | CSS::StyleValueList::Separator::Space)); |
81 | 0 | } |
82 | | |
83 | | unsigned HTMLCanvasElement::width() const |
84 | 0 | { |
85 | | // https://html.spec.whatwg.org/multipage/canvas.html#obtain-numeric-values |
86 | | // The rules for parsing non-negative integers must be used to obtain their numeric values. |
87 | | // If an attribute is missing, or if parsing its value returns an error, then the default value must be used instead. |
88 | | // The width attribute defaults to 300 |
89 | 0 | return parse_non_negative_integer(get_attribute_value(HTML::AttributeNames::width)).value_or(300); |
90 | 0 | } |
91 | | |
92 | | unsigned HTMLCanvasElement::height() const |
93 | 0 | { |
94 | | // https://html.spec.whatwg.org/multipage/canvas.html#obtain-numeric-values |
95 | | // The rules for parsing non-negative integers must be used to obtain their numeric values. |
96 | | // If an attribute is missing, or if parsing its value returns an error, then the default value must be used instead. |
97 | | // the height attribute defaults to 150 |
98 | 0 | return parse_non_negative_integer(get_attribute_value(HTML::AttributeNames::height)).value_or(150); |
99 | 0 | } |
100 | | |
101 | | void HTMLCanvasElement::reset_context_to_default_state() |
102 | 0 | { |
103 | 0 | m_context.visit( |
104 | 0 | [](JS::NonnullGCPtr<CanvasRenderingContext2D>& context) { |
105 | 0 | context->reset_to_default_state(); |
106 | 0 | }, |
107 | 0 | [](JS::NonnullGCPtr<WebGL::WebGLRenderingContext>&) { |
108 | 0 | TODO(); |
109 | 0 | }, |
110 | 0 | [](Empty) { |
111 | | // Do nothing. |
112 | 0 | }); |
113 | 0 | } |
114 | | |
115 | | WebIDL::ExceptionOr<void> HTMLCanvasElement::set_width(unsigned value) |
116 | 0 | { |
117 | 0 | TRY(set_attribute(HTML::AttributeNames::width, String::number(value))); |
118 | 0 | m_bitmap = nullptr; |
119 | 0 | reset_context_to_default_state(); |
120 | 0 | return {}; |
121 | 0 | } |
122 | | |
123 | | WebIDL::ExceptionOr<void> HTMLCanvasElement::set_height(unsigned value) |
124 | 0 | { |
125 | 0 | TRY(set_attribute(HTML::AttributeNames::height, String::number(value))); |
126 | 0 | m_bitmap = nullptr; |
127 | 0 | reset_context_to_default_state(); |
128 | 0 | return {}; |
129 | 0 | } |
130 | | |
131 | | JS::GCPtr<Layout::Node> HTMLCanvasElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style) |
132 | 0 | { |
133 | 0 | return heap().allocate_without_realm<Layout::CanvasBox>(document(), *this, move(style)); |
134 | 0 | } |
135 | | |
136 | | HTMLCanvasElement::HasOrCreatedContext HTMLCanvasElement::create_2d_context() |
137 | 0 | { |
138 | 0 | if (!m_context.has<Empty>()) |
139 | 0 | return m_context.has<JS::NonnullGCPtr<CanvasRenderingContext2D>>() ? HasOrCreatedContext::Yes : HasOrCreatedContext::No; |
140 | | |
141 | 0 | m_context = CanvasRenderingContext2D::create(realm(), *this); |
142 | 0 | return HasOrCreatedContext::Yes; |
143 | 0 | } |
144 | | |
145 | | JS::ThrowCompletionOr<HTMLCanvasElement::HasOrCreatedContext> HTMLCanvasElement::create_webgl_context(JS::Value options) |
146 | 0 | { |
147 | 0 | if (!m_context.has<Empty>()) |
148 | 0 | return m_context.has<JS::NonnullGCPtr<WebGL::WebGLRenderingContext>>() ? HasOrCreatedContext::Yes : HasOrCreatedContext::No; |
149 | | |
150 | 0 | auto maybe_context = TRY(WebGL::WebGLRenderingContext::create(realm(), *this, options)); |
151 | 0 | if (!maybe_context) |
152 | 0 | return HasOrCreatedContext::No; |
153 | | |
154 | 0 | m_context = JS::NonnullGCPtr<WebGL::WebGLRenderingContext>(*maybe_context); |
155 | 0 | return HasOrCreatedContext::Yes; |
156 | 0 | } |
157 | | |
158 | | // https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext |
159 | | JS::ThrowCompletionOr<HTMLCanvasElement::RenderingContext> HTMLCanvasElement::get_context(String const& type, JS::Value options) |
160 | 0 | { |
161 | | // 1. If options is not an object, then set options to null. |
162 | 0 | if (!options.is_object()) |
163 | 0 | options = JS::js_null(); |
164 | | |
165 | | // 2. Set options to the result of converting options to a JavaScript value. |
166 | | // NOTE: No-op. |
167 | | |
168 | | // 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: |
169 | | // NOTE: See the spec for the full table. |
170 | 0 | if (type == "2d"sv) { |
171 | 0 | if (create_2d_context() == HasOrCreatedContext::Yes) |
172 | 0 | return JS::make_handle(*m_context.get<JS::NonnullGCPtr<HTML::CanvasRenderingContext2D>>()); |
173 | | |
174 | 0 | return Empty {}; |
175 | 0 | } |
176 | | |
177 | | // NOTE: The WebGL spec says "experimental-webgl" is also acceptable and must be equivalent to "webgl". Other engines accept this, so we do too. |
178 | 0 | if (type.is_one_of("webgl"sv, "experimental-webgl"sv)) { |
179 | 0 | if (TRY(create_webgl_context(options)) == HasOrCreatedContext::Yes) |
180 | 0 | return JS::make_handle(*m_context.get<JS::NonnullGCPtr<WebGL::WebGLRenderingContext>>()); |
181 | | |
182 | 0 | return Empty {}; |
183 | 0 | } |
184 | | |
185 | 0 | return Empty {}; |
186 | 0 | } |
187 | | |
188 | | static Gfx::IntSize bitmap_size_for_canvas(HTMLCanvasElement const& canvas, size_t minimum_width, size_t minimum_height) |
189 | 0 | { |
190 | 0 | auto width = max(canvas.width(), minimum_width); |
191 | 0 | auto height = max(canvas.height(), minimum_height); |
192 | |
|
193 | 0 | Checked<size_t> area = width; |
194 | 0 | area *= height; |
195 | |
|
196 | 0 | if (area.has_overflow()) { |
197 | 0 | dbgln("Refusing to create {}x{} canvas (overflow)", width, height); |
198 | 0 | return {}; |
199 | 0 | } |
200 | 0 | if (area.value() > max_canvas_area) { |
201 | 0 | dbgln("Refusing to create {}x{} canvas (exceeds maximum size)", width, height); |
202 | 0 | return {}; |
203 | 0 | } |
204 | 0 | return Gfx::IntSize(width, height); |
205 | 0 | } |
206 | | |
207 | | bool HTMLCanvasElement::create_bitmap(size_t minimum_width, size_t minimum_height) |
208 | 0 | { |
209 | 0 | auto size = bitmap_size_for_canvas(*this, minimum_width, minimum_height); |
210 | 0 | if (size.is_empty()) { |
211 | 0 | m_bitmap = nullptr; |
212 | 0 | return false; |
213 | 0 | } |
214 | 0 | if (!m_bitmap || m_bitmap->size() != size) { |
215 | 0 | auto bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, size); |
216 | 0 | if (bitmap_or_error.is_error()) |
217 | 0 | return false; |
218 | 0 | m_bitmap = bitmap_or_error.release_value_but_fixme_should_propagate_errors(); |
219 | 0 | } |
220 | 0 | return m_bitmap; |
221 | 0 | } |
222 | | |
223 | | struct SerializeBitmapResult { |
224 | | ByteBuffer buffer; |
225 | | StringView mime_type; |
226 | | }; |
227 | | |
228 | | // https://html.spec.whatwg.org/multipage/canvas.html#a-serialisation-of-the-bitmap-as-a-file |
229 | | static ErrorOr<SerializeBitmapResult> serialize_bitmap(Gfx::Bitmap const& bitmap, StringView type, Optional<double> quality) |
230 | 0 | { |
231 | | // If type is an image format that supports variable quality (such as "image/jpeg"), quality is given, and type is not "image/png", then, |
232 | | // if quality is a Number in the range 0.0 to 1.0 inclusive, the user agent must treat quality as the desired quality level. |
233 | | // Otherwise, the user agent must use its default quality value, as if the quality argument had not been given. |
234 | 0 | if (quality.has_value() && !(*quality >= 0.0 && *quality <= 1.0)) |
235 | 0 | quality = OptionalNone {}; |
236 | |
|
237 | 0 | if (type.equals_ignoring_ascii_case("image/jpeg"sv)) { |
238 | 0 | AllocatingMemoryStream file; |
239 | 0 | Gfx::JPEGWriter::Options jpeg_options; |
240 | 0 | if (quality.has_value()) |
241 | 0 | jpeg_options.quality = static_cast<int>(quality.value() * 100); |
242 | 0 | TRY(Gfx::JPEGWriter::encode(file, bitmap, jpeg_options)); |
243 | 0 | return SerializeBitmapResult { TRY(file.read_until_eof()), "image/jpeg"sv }; |
244 | 0 | } |
245 | | |
246 | | // User agents must support PNG ("image/png"). User agents may support other types. |
247 | | // If the user agent does not support the requested type, then it must create the file using the PNG format. [PNG] |
248 | 0 | return SerializeBitmapResult { TRY(Gfx::PNGWriter::encode(bitmap)), "image/png"sv }; |
249 | 0 | } |
250 | | |
251 | | // https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-todataurl |
252 | | String HTMLCanvasElement::to_data_url(StringView type, Optional<double> quality) |
253 | 0 | { |
254 | | // It is possible the the canvas doesn't have a associated bitmap so create one |
255 | 0 | if (!bitmap()) |
256 | 0 | create_bitmap(); |
257 | | |
258 | | // FIXME: 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException. |
259 | | |
260 | | // 2. If this canvas element's bitmap has no pixels (i.e. either its horizontal dimension or its vertical dimension is zero) |
261 | | // then return the string "data:,". (This is the shortest data: URL; it represents the empty string in a text/plain resource.) |
262 | 0 | if (!m_bitmap) |
263 | 0 | return "data:,"_string; |
264 | | |
265 | | // 3. Let file be a serialization of this canvas element's bitmap as a file, passing type and quality if given. |
266 | 0 | auto file = serialize_bitmap(*m_bitmap, type, move(quality)); |
267 | | |
268 | | // 4. If file is null then return "data:,". |
269 | 0 | if (file.is_error()) { |
270 | 0 | dbgln("HTMLCanvasElement: Failed to encode canvas bitmap to {}: {}", type, file.error()); |
271 | 0 | return "data:,"_string; |
272 | 0 | } |
273 | | |
274 | | // 5. Return a data: URL representing file. [RFC2397] |
275 | 0 | auto base64_encoded_or_error = encode_base64(file.value().buffer); |
276 | 0 | if (base64_encoded_or_error.is_error()) { |
277 | 0 | return "data:,"_string; |
278 | 0 | } |
279 | 0 | return MUST(URL::create_with_data(file.value().mime_type, base64_encoded_or_error.release_value(), true).to_string()); |
280 | 0 | } |
281 | | |
282 | | // https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-toblob |
283 | | WebIDL::ExceptionOr<void> HTMLCanvasElement::to_blob(JS::NonnullGCPtr<WebIDL::CallbackType> callback, StringView type, Optional<double> quality) |
284 | 0 | { |
285 | | // It is possible the the canvas doesn't have a associated bitmap so create one |
286 | 0 | if (!bitmap()) |
287 | 0 | create_bitmap(); |
288 | | |
289 | | // FIXME: 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException. |
290 | | |
291 | | // 2. Let result be null. |
292 | 0 | RefPtr<Gfx::Bitmap> bitmap_result; |
293 | | |
294 | | // 3. If this canvas element's bitmap has pixels (i.e., neither its horizontal dimension nor its vertical dimension is zero), |
295 | | // then set result to a copy of this canvas element's bitmap. |
296 | 0 | if (m_bitmap) |
297 | 0 | bitmap_result = TRY_OR_THROW_OOM(vm(), m_bitmap->clone()); |
298 | | |
299 | | // 4. Run these steps in parallel: |
300 | 0 | Platform::EventLoopPlugin::the().deferred_invoke([this, callback, bitmap_result, type, quality] { |
301 | | // 1. If result is non-null, then set result to a serialization of result as a file with type and quality if given. |
302 | 0 | Optional<SerializeBitmapResult> file_result; |
303 | 0 | if (bitmap_result) { |
304 | 0 | if (auto result = serialize_bitmap(*bitmap_result, type, move(quality)); !result.is_error()) |
305 | 0 | file_result = result.release_value(); |
306 | 0 | } |
307 | | |
308 | | // 2. Queue an element task on the canvas blob serialization task source given the canvas element to run these steps: |
309 | 0 | queue_an_element_task(Task::Source::CanvasBlobSerializationTask, [this, callback, file_result = move(file_result)] { |
310 | 0 | auto maybe_error = Bindings::throw_dom_exception_if_needed(vm(), [&]() -> WebIDL::ExceptionOr<void> { |
311 | | // 1. If result is non-null, then set result to a new Blob object, created in the relevant realm of this canvas element, representing result. [FILEAPI] |
312 | 0 | JS::GCPtr<FileAPI::Blob> blob_result; |
313 | 0 | if (file_result.has_value()) |
314 | 0 | blob_result = FileAPI::Blob::create(realm(), file_result->buffer, TRY_OR_THROW_OOM(vm(), String::from_utf8(file_result->mime_type))); |
315 | | |
316 | | // 2. Invoke callback with « result ». |
317 | 0 | TRY(WebIDL::invoke_callback(*callback, {}, move(blob_result))); |
318 | 0 | return {}; |
319 | 0 | }); |
320 | 0 | if (maybe_error.is_throw_completion()) |
321 | 0 | report_exception(maybe_error.throw_completion(), realm()); |
322 | 0 | }); |
323 | 0 | }); |
324 | 0 | return {}; |
325 | 0 | } |
326 | | |
327 | | void HTMLCanvasElement::present() |
328 | 0 | { |
329 | 0 | m_context.visit( |
330 | 0 | [](JS::NonnullGCPtr<CanvasRenderingContext2D>&) { |
331 | | // Do nothing, CRC2D writes directly to the canvas bitmap. |
332 | 0 | }, |
333 | 0 | [](JS::NonnullGCPtr<WebGL::WebGLRenderingContext>& context) { |
334 | 0 | context->present(); |
335 | 0 | }, |
336 | 0 | [](Empty) { |
337 | | // Do nothing. |
338 | 0 | }); |
339 | 0 | } |
340 | | |
341 | | } |