Serenity Operating System
at master 213 lines 7.2 kB view raw
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}