/src/serenity/Userland/Libraries/LibWeb/SVG/SVGImageElement.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2024, Tim Ledbetter <tim.ledbetter@ladybird.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include "SVGImageElement.h" |
8 | | #include <LibCore/Timer.h> |
9 | | #include <LibJS/Heap/Heap.h> |
10 | | #include <LibWeb/Bindings/SVGImageElementPrototype.h> |
11 | | #include <LibWeb/DOM/DocumentObserver.h> |
12 | | #include <LibWeb/DOM/Event.h> |
13 | | #include <LibWeb/HTML/PotentialCORSRequest.h> |
14 | | #include <LibWeb/HTML/SharedResourceRequest.h> |
15 | | #include <LibWeb/Layout/SVGImageBox.h> |
16 | | #include <LibWeb/Painting/Paintable.h> |
17 | | #include <LibWeb/SVG/SVGDecodedImageData.h> |
18 | | |
19 | | namespace Web::SVG { |
20 | | |
21 | | SVGImageElement::SVGImageElement(DOM::Document& document, DOM::QualifiedName qualified_name) |
22 | 0 | : SVGGraphicsElement(document, move(qualified_name)) |
23 | 0 | { |
24 | 0 | m_animation_timer = Core::Timer::try_create().release_value_but_fixme_should_propagate_errors(); |
25 | 0 | m_animation_timer->on_timeout = [this] { animate(); }; |
26 | 0 | } |
27 | | |
28 | | void SVGImageElement::initialize(JS::Realm& realm) |
29 | 0 | { |
30 | 0 | Base::initialize(realm); |
31 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGImageElement); |
32 | 0 | } |
33 | | |
34 | | void SVGImageElement::visit_edges(Cell::Visitor& visitor) |
35 | 0 | { |
36 | 0 | Base::visit_edges(visitor); |
37 | 0 | SVGURIReferenceMixin::visit_edges(visitor); |
38 | 0 | visitor.visit(m_x); |
39 | 0 | visitor.visit(m_y); |
40 | 0 | visitor.visit(m_width); |
41 | 0 | visitor.visit(m_height); |
42 | 0 | visitor.visit(m_resource_request); |
43 | 0 | } |
44 | | |
45 | | void SVGImageElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value) |
46 | 0 | { |
47 | 0 | SVGGraphicsElement::attribute_changed(name, old_value, value); |
48 | 0 | if (name == SVG::AttributeNames::x) { |
49 | 0 | auto parsed_value = AttributeParser::parse_coordinate(value.value_or(String {})); |
50 | 0 | MUST(x()->base_val()->set_value(parsed_value.value_or(0))); |
51 | 0 | } else if (name == SVG::AttributeNames::y) { |
52 | 0 | auto parsed_value = AttributeParser::parse_coordinate(value.value_or(String {})); |
53 | 0 | MUST(y()->base_val()->set_value(parsed_value.value_or(0))); |
54 | 0 | } else if (name == SVG::AttributeNames::width) { |
55 | 0 | auto parsed_value = AttributeParser::parse_coordinate(value.value_or(String {})); |
56 | 0 | MUST(width()->base_val()->set_value(parsed_value.value_or(0))); |
57 | 0 | } else if (name == SVG::AttributeNames::height) { |
58 | 0 | auto parsed_value = AttributeParser::parse_coordinate(value.value_or(String {})); |
59 | 0 | MUST(height()->base_val()->set_value(parsed_value.value_or(0))); |
60 | 0 | } else if (name == SVG::AttributeNames::href) { |
61 | 0 | process_the_url(value); |
62 | 0 | } |
63 | 0 | } |
64 | | |
65 | | // https://svgwg.org/svg2-draft/embedded.html#__svg__SVGImageElement__x |
66 | | JS::NonnullGCPtr<SVG::SVGAnimatedLength> SVGImageElement::x() |
67 | 0 | { |
68 | 0 | if (!m_x) { |
69 | 0 | auto& realm = this->realm(); |
70 | 0 | m_x = SVGAnimatedLength::create(realm, SVGLength::create(realm, 0, 0), SVGLength::create(realm, 0, 0)); |
71 | 0 | } |
72 | |
|
73 | 0 | return *m_x; |
74 | 0 | } |
75 | | |
76 | | // https://svgwg.org/svg2-draft/embedded.html#__svg__SVGImageElement__y |
77 | | JS::NonnullGCPtr<SVG::SVGAnimatedLength> SVGImageElement::y() |
78 | 0 | { |
79 | 0 | if (!m_y) { |
80 | 0 | auto& realm = this->realm(); |
81 | 0 | m_y = SVGAnimatedLength::create(realm, SVGLength::create(realm, 0, 0), SVGLength::create(realm, 0, 0)); |
82 | 0 | } |
83 | |
|
84 | 0 | return *m_y; |
85 | 0 | } |
86 | | |
87 | | // https://svgwg.org/svg2-draft/embedded.html#__svg__SVGImageElement__width |
88 | | JS::NonnullGCPtr<SVG::SVGAnimatedLength> SVGImageElement::width() |
89 | 0 | { |
90 | 0 | if (!m_width) { |
91 | 0 | auto& realm = this->realm(); |
92 | 0 | m_width = SVGAnimatedLength::create(realm, SVGLength::create(realm, 0, intrinsic_width().value_or(0).to_double()), SVGLength::create(realm, 0, 0)); |
93 | 0 | } |
94 | |
|
95 | 0 | return *m_width; |
96 | 0 | } |
97 | | |
98 | | // https://svgwg.org/svg2-draft/embedded.html#__svg__SVGImageElement__height |
99 | | JS::NonnullGCPtr<SVG::SVGAnimatedLength> SVGImageElement::height() |
100 | 0 | { |
101 | 0 | if (!m_height) { |
102 | 0 | auto& realm = this->realm(); |
103 | 0 | m_height = SVGAnimatedLength::create(realm, SVGLength::create(realm, 0, intrinsic_height().value_or(0).to_double()), SVGLength::create(realm, 0, 0)); |
104 | 0 | } |
105 | |
|
106 | 0 | return *m_height; |
107 | 0 | } |
108 | | |
109 | | Gfx::Rect<CSSPixels> SVGImageElement::bounding_box() const |
110 | 0 | { |
111 | 0 | Optional<CSSPixels> width; |
112 | 0 | if (attribute(HTML::AttributeNames::width).has_value()) |
113 | 0 | width = CSSPixels { m_width->base_val()->value() }; |
114 | |
|
115 | 0 | Optional<CSSPixels> height; |
116 | 0 | if (attribute(HTML::AttributeNames::height).has_value()) |
117 | 0 | height = CSSPixels { m_height->base_val()->value() }; |
118 | |
|
119 | 0 | if (!height.has_value() && width.has_value() && intrinsic_aspect_ratio().has_value()) |
120 | 0 | height = width.value() / intrinsic_aspect_ratio().value(); |
121 | |
|
122 | 0 | if (!width.has_value() && height.has_value() && intrinsic_aspect_ratio().has_value()) |
123 | 0 | width = height.value() * intrinsic_aspect_ratio().value(); |
124 | |
|
125 | 0 | if (!width.has_value() && intrinsic_width().has_value()) |
126 | 0 | width = intrinsic_width(); |
127 | |
|
128 | 0 | if (!height.has_value() && intrinsic_height().has_value()) |
129 | 0 | height = intrinsic_height(); |
130 | |
|
131 | 0 | return { |
132 | 0 | CSSPixels { m_x ? m_x->base_val()->value() : 0 }, |
133 | 0 | CSSPixels { m_y ? m_y->base_val()->value() : 0 }, |
134 | 0 | width.value_or(0), |
135 | 0 | height.value_or(0), |
136 | 0 | }; |
137 | 0 | } |
138 | | |
139 | | // https://www.w3.org/TR/SVG2/linking.html#processingURL |
140 | | void SVGImageElement::process_the_url(Optional<String> const& href) |
141 | 0 | { |
142 | 0 | m_href = document().url().complete_url(href.value_or(String {})); |
143 | 0 | if (!m_href.is_valid()) |
144 | 0 | return; |
145 | | |
146 | 0 | fetch_the_document(m_href); |
147 | 0 | } |
148 | | |
149 | | // https://svgwg.org/svg2-draft/linking.html#processingURL-fetch |
150 | | void SVGImageElement::fetch_the_document(URL::URL const& url) |
151 | 0 | { |
152 | 0 | m_load_event_delayer.emplace(document()); |
153 | 0 | m_resource_request = HTML::SharedResourceRequest::get_or_create(realm(), document().page(), url); |
154 | 0 | m_resource_request->add_callbacks( |
155 | 0 | [this] { |
156 | 0 | m_load_event_delayer.clear(); |
157 | 0 | auto image_data = m_resource_request->image_data(); |
158 | 0 | if (image_data->is_animated() && image_data->frame_count() > 1) { |
159 | 0 | m_current_frame_index = 0; |
160 | 0 | m_animation_timer->set_interval(image_data->frame_duration(0)); |
161 | 0 | m_animation_timer->start(); |
162 | 0 | } |
163 | 0 | set_needs_style_update(true); |
164 | 0 | document().set_needs_layout(); |
165 | |
|
166 | 0 | dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load)); |
167 | 0 | }, |
168 | 0 | [this] { |
169 | 0 | m_load_event_delayer.clear(); |
170 | |
|
171 | 0 | dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error)); |
172 | 0 | }); |
173 | |
|
174 | 0 | if (m_resource_request->needs_fetching()) { |
175 | 0 | auto request = HTML::create_potential_CORS_request(vm(), url, Fetch::Infrastructure::Request::Destination::Image, HTML::CORSSettingAttribute::NoCORS); |
176 | 0 | request->set_client(&document().relevant_settings_object()); |
177 | 0 | m_resource_request->fetch_resource(realm(), request); |
178 | 0 | } |
179 | 0 | } |
180 | | |
181 | | JS::GCPtr<Layout::Node> SVGImageElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style) |
182 | 0 | { |
183 | 0 | return heap().allocate_without_realm<Layout::SVGImageBox>(document(), *this, move(style)); |
184 | 0 | } |
185 | | |
186 | | bool SVGImageElement::is_image_available() const |
187 | 0 | { |
188 | 0 | return m_resource_request && m_resource_request->image_data(); |
189 | 0 | } |
190 | | |
191 | | Optional<CSSPixels> SVGImageElement::intrinsic_width() const |
192 | 0 | { |
193 | 0 | if (!m_resource_request) |
194 | 0 | return {}; |
195 | 0 | if (auto image_data = m_resource_request->image_data()) |
196 | 0 | return image_data->intrinsic_width(); |
197 | 0 | return {}; |
198 | 0 | } |
199 | | |
200 | | Optional<CSSPixels> SVGImageElement::intrinsic_height() const |
201 | 0 | { |
202 | 0 | if (!m_resource_request) |
203 | 0 | return {}; |
204 | 0 | if (auto image_data = m_resource_request->image_data()) |
205 | 0 | return image_data->intrinsic_height(); |
206 | 0 | return {}; |
207 | 0 | } |
208 | | |
209 | | Optional<CSSPixelFraction> SVGImageElement::intrinsic_aspect_ratio() const |
210 | 0 | { |
211 | 0 | if (!m_resource_request) |
212 | 0 | return {}; |
213 | 0 | if (auto image_data = m_resource_request->image_data()) |
214 | 0 | return image_data->intrinsic_aspect_ratio(); |
215 | 0 | return {}; |
216 | 0 | } |
217 | | |
218 | | RefPtr<Gfx::ImmutableBitmap> SVGImageElement::current_image_bitmap(Gfx::IntSize size) const |
219 | 0 | { |
220 | 0 | if (!m_resource_request) |
221 | 0 | return {}; |
222 | 0 | if (auto data = m_resource_request->image_data()) |
223 | 0 | return data->bitmap(m_current_frame_index, size); |
224 | 0 | return {}; |
225 | 0 | } |
226 | | |
227 | | void SVGImageElement::animate() |
228 | 0 | { |
229 | 0 | auto image_data = m_resource_request->image_data(); |
230 | 0 | if (!image_data) { |
231 | 0 | return; |
232 | 0 | } |
233 | | |
234 | 0 | m_current_frame_index = (m_current_frame_index + 1) % image_data->frame_count(); |
235 | 0 | auto current_frame_duration = image_data->frame_duration(m_current_frame_index); |
236 | |
|
237 | 0 | if (current_frame_duration != m_animation_timer->interval()) { |
238 | 0 | m_animation_timer->restart(current_frame_duration); |
239 | 0 | } |
240 | |
|
241 | 0 | if (m_current_frame_index == image_data->frame_count() - 1) { |
242 | 0 | ++m_loops_completed; |
243 | 0 | if (m_loops_completed > 0 && m_loops_completed == image_data->loop_count()) { |
244 | 0 | m_animation_timer->stop(); |
245 | 0 | } |
246 | 0 | } |
247 | |
|
248 | 0 | if (paintable()) |
249 | 0 | paintable()->set_needs_display(); |
250 | 0 | } |
251 | | |
252 | | } |