Serenity Operating System
1/*
2 * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <LibGfx/Bitmap.h>
8#include <LibWeb/ARIA/Roles.h>
9#include <LibWeb/CSS/Parser/Parser.h>
10#include <LibWeb/CSS/StyleComputer.h>
11#include <LibWeb/DOM/Document.h>
12#include <LibWeb/DOM/Event.h>
13#include <LibWeb/HTML/EventNames.h>
14#include <LibWeb/HTML/HTMLImageElement.h>
15#include <LibWeb/HTML/Parser/HTMLParser.h>
16#include <LibWeb/Layout/ImageBox.h>
17#include <LibWeb/Loader/ResourceLoader.h>
18#include <LibWeb/Painting/PaintableBox.h>
19
20namespace Web::HTML {
21
22HTMLImageElement::HTMLImageElement(DOM::Document& document, DOM::QualifiedName qualified_name)
23 : HTMLElement(document, move(qualified_name))
24 , m_image_loader(*this)
25{
26 m_image_loader.on_load = [this] {
27 set_needs_style_update(true);
28 this->document().set_needs_layout();
29 queue_an_element_task(HTML::Task::Source::DOMManipulation, [this] {
30 dispatch_event(DOM::Event::create(this->realm(), EventNames::load).release_value_but_fixme_should_propagate_errors());
31 });
32 };
33
34 m_image_loader.on_fail = [this] {
35 dbgln("HTMLImageElement: Resource did fail: {}", src());
36 set_needs_style_update(true);
37 this->document().set_needs_layout();
38 queue_an_element_task(HTML::Task::Source::DOMManipulation, [this] {
39 dispatch_event(DOM::Event::create(this->realm(), EventNames::error).release_value_but_fixme_should_propagate_errors());
40 });
41 };
42
43 m_image_loader.on_animate = [this] {
44 if (layout_node())
45 layout_node()->set_needs_display();
46 };
47}
48
49HTMLImageElement::~HTMLImageElement() = default;
50
51JS::ThrowCompletionOr<void> HTMLImageElement::initialize(JS::Realm& realm)
52{
53 MUST_OR_THROW_OOM(Base::initialize(realm));
54 set_prototype(&Bindings::ensure_web_prototype<Bindings::HTMLImageElementPrototype>(realm, "HTMLImageElement"));
55
56 return {};
57}
58
59void HTMLImageElement::apply_presentational_hints(CSS::StyleProperties& style) const
60{
61 for_each_attribute([&](auto& name, auto& value) {
62 if (name == HTML::AttributeNames::width) {
63 if (auto parsed_value = parse_dimension_value(value))
64 style.set_property(CSS::PropertyID::Width, parsed_value.release_nonnull());
65 } else if (name == HTML::AttributeNames::height) {
66 if (auto parsed_value = parse_dimension_value(value))
67 style.set_property(CSS::PropertyID::Height, parsed_value.release_nonnull());
68 } else if (name == HTML::AttributeNames::hspace) {
69 if (auto parsed_value = parse_dimension_value(value)) {
70 style.set_property(CSS::PropertyID::MarginLeft, *parsed_value);
71 style.set_property(CSS::PropertyID::MarginRight, *parsed_value);
72 }
73 } else if (name == HTML::AttributeNames::vspace) {
74 if (auto parsed_value = parse_dimension_value(value)) {
75 style.set_property(CSS::PropertyID::MarginTop, *parsed_value);
76 style.set_property(CSS::PropertyID::MarginBottom, *parsed_value);
77 }
78 }
79 });
80}
81
82void HTMLImageElement::parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value)
83{
84 HTMLElement::parse_attribute(name, value);
85
86 if (name == HTML::AttributeNames::src && !value.is_empty())
87 m_image_loader.load(document().parse_url(value));
88
89 if (name == HTML::AttributeNames::alt) {
90 if (layout_node())
91 verify_cast<Layout::ImageBox>(*layout_node()).dom_node_did_update_alt_text({});
92 }
93}
94
95JS::GCPtr<Layout::Node> HTMLImageElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
96{
97 return heap().allocate_without_realm<Layout::ImageBox>(document(), *this, move(style), m_image_loader);
98}
99
100Gfx::Bitmap const* HTMLImageElement::bitmap() const
101{
102 return m_image_loader.bitmap(m_image_loader.current_frame_index());
103}
104
105// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-width
106unsigned HTMLImageElement::width() const
107{
108 const_cast<DOM::Document&>(document()).update_layout();
109
110 // Return the rendered width of the image, in CSS pixels, if the image is being rendered.
111 if (auto* paint_box = this->paint_box())
112 return paint_box->content_width().value();
113
114 // NOTE: This step seems to not be in the spec, but all browsers do it.
115 auto width_attr = get_attribute(HTML::AttributeNames::width);
116 if (auto converted = width_attr.to_uint(); converted.has_value())
117 return *converted;
118
119 // ...or else the density-corrected intrinsic width and height of the image, in CSS pixels,
120 // if the image has intrinsic dimensions and is available but not being rendered.
121 if (m_image_loader.has_image())
122 return m_image_loader.width();
123
124 // ...or else 0, if the image is not available or does not have intrinsic dimensions.
125 return 0;
126}
127
128void HTMLImageElement::set_width(unsigned width)
129{
130 MUST(set_attribute(HTML::AttributeNames::width, DeprecatedString::number(width)));
131}
132
133// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-height
134unsigned HTMLImageElement::height() const
135{
136 const_cast<DOM::Document&>(document()).update_layout();
137
138 // Return the rendered height of the image, in CSS pixels, if the image is being rendered.
139 if (auto* paint_box = this->paint_box())
140 return paint_box->content_height().value();
141
142 // NOTE: This step seems to not be in the spec, but all browsers do it.
143 auto height_attr = get_attribute(HTML::AttributeNames::height);
144 if (auto converted = height_attr.to_uint(); converted.has_value())
145 return *converted;
146
147 // ...or else the density-corrected intrinsic height and height of the image, in CSS pixels,
148 // if the image has intrinsic dimensions and is available but not being rendered.
149 if (m_image_loader.has_image())
150 return m_image_loader.height();
151
152 // ...or else 0, if the image is not available or does not have intrinsic dimensions.
153 return 0;
154}
155
156void HTMLImageElement::set_height(unsigned height)
157{
158 MUST(set_attribute(HTML::AttributeNames::height, DeprecatedString::number(height)));
159}
160
161// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-naturalwidth
162unsigned HTMLImageElement::natural_width() const
163{
164 // Return the density-corrected intrinsic width of the image, in CSS pixels,
165 // if the image has intrinsic dimensions and is available.
166 if (m_image_loader.has_image())
167 return m_image_loader.width();
168
169 // ...or else 0.
170 return 0;
171}
172
173// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-naturalheight
174unsigned HTMLImageElement::natural_height() const
175{
176 // Return the density-corrected intrinsic height of the image, in CSS pixels,
177 // if the image has intrinsic dimensions and is available.
178 if (m_image_loader.has_image())
179 return m_image_loader.height();
180
181 // ...or else 0.
182 return 0;
183}
184
185// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-complete
186bool HTMLImageElement::complete() const
187{
188 // The IDL attribute complete must return true if any of the following conditions is true:
189
190 // - Both the src attribute and the srcset attribute are omitted.
191 if (!has_attribute(HTML::AttributeNames::src) && !has_attribute(HTML::AttributeNames::srcset))
192 return true;
193
194 // - The srcset attribute is omitted and the src attribute's value is the empty string.
195 if (!has_attribute(HTML::AttributeNames::srcset) && attribute(HTML::AttributeNames::src) == ""sv)
196 return true;
197
198 // - The img element's current request's state is completely available and its pending request is null.
199 // - The img element's current request's state is broken and its pending request is null.
200 // FIXME: This is ad-hoc and should be updated once we are loading images via the Fetch mechanism.
201 if (m_image_loader.has_loaded_or_failed())
202 return true;
203
204 return false;
205}
206
207Optional<ARIA::Role> HTMLImageElement::default_role() const
208{
209 // https://www.w3.org/TR/html-aria/#el-img
210 // https://www.w3.org/TR/html-aria/#el-img-no-alt
211 if (alt().is_null() || !alt().is_empty())
212 return ARIA::Role::img;
213 // https://www.w3.org/TR/html-aria/#el-img-empty-alt
214 return ARIA::Role::presentation;
215}
216
217}