/src/serenity/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020, Matthew Olsson <mattco@serenityos.org> |
3 | | * Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org> |
4 | | * Copyright (c) 2023, MacDue <macdue@dueutil.tech> |
5 | | * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> |
6 | | * |
7 | | * SPDX-License-Identifier: BSD-2-Clause |
8 | | */ |
9 | | |
10 | | #include <LibWeb/Bindings/Intrinsics.h> |
11 | | #include <LibWeb/Bindings/SVGGraphicsElementPrototype.h> |
12 | | #include <LibWeb/CSS/Parser/Parser.h> |
13 | | #include <LibWeb/DOM/Document.h> |
14 | | #include <LibWeb/Layout/Node.h> |
15 | | #include <LibWeb/Painting/PaintStyle.h> |
16 | | #include <LibWeb/Painting/PaintableBox.h> |
17 | | #include <LibWeb/Painting/SVGGraphicsPaintable.h> |
18 | | #include <LibWeb/SVG/AttributeNames.h> |
19 | | #include <LibWeb/SVG/AttributeParser.h> |
20 | | #include <LibWeb/SVG/SVGClipPathElement.h> |
21 | | #include <LibWeb/SVG/SVGGradientElement.h> |
22 | | #include <LibWeb/SVG/SVGGraphicsElement.h> |
23 | | #include <LibWeb/SVG/SVGMaskElement.h> |
24 | | #include <LibWeb/SVG/SVGSVGElement.h> |
25 | | #include <LibWeb/SVG/SVGSymbolElement.h> |
26 | | |
27 | | namespace Web::SVG { |
28 | | |
29 | | SVGGraphicsElement::SVGGraphicsElement(DOM::Document& document, DOM::QualifiedName qualified_name) |
30 | 0 | : SVGElement(document, move(qualified_name)) |
31 | 0 | { |
32 | 0 | } |
33 | | |
34 | | void SVGGraphicsElement::initialize(JS::Realm& realm) |
35 | 0 | { |
36 | 0 | Base::initialize(realm); |
37 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGGraphicsElement); |
38 | 0 | } |
39 | | |
40 | | void SVGGraphicsElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value) |
41 | 0 | { |
42 | 0 | SVGElement::attribute_changed(name, old_value, value); |
43 | 0 | if (name == "transform"sv) { |
44 | 0 | auto transform_list = AttributeParser::parse_transform(value.value_or(String {})); |
45 | 0 | if (transform_list.has_value()) |
46 | 0 | m_transform = transform_from_transform_list(*transform_list); |
47 | | // FIXME: This should only invalidate the contents of the SVG. |
48 | 0 | document().invalidate_layout_tree(); |
49 | 0 | } |
50 | 0 | } |
51 | | |
52 | | Optional<Painting::PaintStyle> SVGGraphicsElement::svg_paint_computed_value_to_gfx_paint_style(SVGPaintContext const& paint_context, Optional<CSS::SVGPaint> const& paint_value) const |
53 | 0 | { |
54 | | // FIXME: This entire function is an ad-hoc hack: |
55 | 0 | if (!paint_value.has_value() || !paint_value->is_url()) |
56 | 0 | return {}; |
57 | 0 | if (auto gradient = try_resolve_url_to<SVG::SVGGradientElement const>(paint_value->as_url())) |
58 | 0 | return gradient->to_gfx_paint_style(paint_context); |
59 | 0 | return {}; |
60 | 0 | } |
61 | | |
62 | | Optional<Painting::PaintStyle> SVGGraphicsElement::fill_paint_style(SVGPaintContext const& paint_context) const |
63 | 0 | { |
64 | 0 | if (!layout_node()) |
65 | 0 | return {}; |
66 | 0 | return svg_paint_computed_value_to_gfx_paint_style(paint_context, layout_node()->computed_values().fill()); |
67 | 0 | } |
68 | | |
69 | | Optional<Painting::PaintStyle> SVGGraphicsElement::stroke_paint_style(SVGPaintContext const& paint_context) const |
70 | 0 | { |
71 | 0 | if (!layout_node()) |
72 | 0 | return {}; |
73 | 0 | return svg_paint_computed_value_to_gfx_paint_style(paint_context, layout_node()->computed_values().stroke()); |
74 | 0 | } |
75 | | |
76 | | JS::GCPtr<SVG::SVGMaskElement const> SVGGraphicsElement::mask() const |
77 | 0 | { |
78 | 0 | auto const& mask_reference = layout_node()->computed_values().mask(); |
79 | 0 | if (!mask_reference.has_value()) |
80 | 0 | return {}; |
81 | 0 | return try_resolve_url_to<SVG::SVGMaskElement const>(mask_reference->url()); |
82 | 0 | } |
83 | | |
84 | | JS::GCPtr<SVG::SVGClipPathElement const> SVGGraphicsElement::clip_path() const |
85 | 0 | { |
86 | 0 | auto const& clip_path_reference = layout_node()->computed_values().clip_path(); |
87 | 0 | if (!clip_path_reference.has_value() || !clip_path_reference->is_url()) |
88 | 0 | return {}; |
89 | 0 | return try_resolve_url_to<SVG::SVGClipPathElement const>(clip_path_reference->url()); |
90 | 0 | } |
91 | | |
92 | | Gfx::AffineTransform transform_from_transform_list(ReadonlySpan<Transform> transform_list) |
93 | 0 | { |
94 | 0 | Gfx::AffineTransform affine_transform; |
95 | 0 | for (auto& transform : transform_list) { |
96 | 0 | transform.operation.visit( |
97 | 0 | [&](Transform::Translate const& translate) { |
98 | 0 | affine_transform.multiply(Gfx::AffineTransform {}.translate({ translate.x, translate.y })); |
99 | 0 | }, |
100 | 0 | [&](Transform::Scale const& scale) { |
101 | 0 | affine_transform.multiply(Gfx::AffineTransform {}.scale({ scale.x, scale.y })); |
102 | 0 | }, |
103 | 0 | [&](Transform::Rotate const& rotate) { |
104 | 0 | Gfx::AffineTransform translate_transform; |
105 | 0 | affine_transform.multiply( |
106 | 0 | Gfx::AffineTransform {} |
107 | 0 | .translate({ rotate.x, rotate.y }) |
108 | 0 | .rotate_radians(AK::to_radians(rotate.a)) |
109 | 0 | .translate({ -rotate.x, -rotate.y })); |
110 | 0 | }, |
111 | 0 | [&](Transform::SkewX const& skew_x) { |
112 | 0 | affine_transform.multiply(Gfx::AffineTransform {}.skew_radians(AK::to_radians(skew_x.a), 0)); |
113 | 0 | }, |
114 | 0 | [&](Transform::SkewY const& skew_y) { |
115 | 0 | affine_transform.multiply(Gfx::AffineTransform {}.skew_radians(0, AK::to_radians(skew_y.a))); |
116 | 0 | }, |
117 | 0 | [&](Transform::Matrix const& matrix) { |
118 | 0 | affine_transform.multiply(Gfx::AffineTransform { |
119 | 0 | matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f }); |
120 | 0 | }); |
121 | 0 | } |
122 | 0 | return affine_transform; |
123 | 0 | } |
124 | | |
125 | | Gfx::AffineTransform SVGGraphicsElement::get_transform() const |
126 | 0 | { |
127 | 0 | Gfx::AffineTransform transform = m_transform; |
128 | 0 | for (auto* svg_ancestor = shadow_including_first_ancestor_of_type<SVGGraphicsElement>(); svg_ancestor; svg_ancestor = svg_ancestor->shadow_including_first_ancestor_of_type<SVGGraphicsElement>()) { |
129 | 0 | transform = Gfx::AffineTransform { svg_ancestor->element_transform() }.multiply(transform); |
130 | 0 | } |
131 | 0 | return transform; |
132 | 0 | } |
133 | | |
134 | | struct NamedPropertyID { |
135 | | NamedPropertyID(CSS::PropertyID property_id) |
136 | 0 | : id(property_id) |
137 | 0 | , name(CSS::string_from_property_id(property_id)) |
138 | 0 | { |
139 | 0 | } |
140 | | |
141 | | CSS::PropertyID id; |
142 | | StringView name; |
143 | | }; |
144 | | |
145 | | void SVGGraphicsElement::apply_presentational_hints(CSS::StyleProperties& style) const |
146 | 0 | { |
147 | 0 | static Array const attribute_style_properties { |
148 | | // FIXME: The `fill` attribute and CSS `fill` property are not the same! But our support is limited enough that they are equivalent for now. |
149 | 0 | NamedPropertyID(CSS::PropertyID::Fill), |
150 | | // FIXME: The `stroke` attribute and CSS `stroke` property are not the same! But our support is limited enough that they are equivalent for now. |
151 | 0 | NamedPropertyID(CSS::PropertyID::Stroke), |
152 | 0 | NamedPropertyID(CSS::PropertyID::StrokeLinecap), |
153 | 0 | NamedPropertyID(CSS::PropertyID::StrokeLinejoin), |
154 | 0 | NamedPropertyID(CSS::PropertyID::StrokeMiterlimit), |
155 | 0 | NamedPropertyID(CSS::PropertyID::StrokeWidth), |
156 | 0 | NamedPropertyID(CSS::PropertyID::FillRule), |
157 | 0 | NamedPropertyID(CSS::PropertyID::FillOpacity), |
158 | 0 | NamedPropertyID(CSS::PropertyID::StrokeOpacity), |
159 | 0 | NamedPropertyID(CSS::PropertyID::Opacity), |
160 | 0 | NamedPropertyID(CSS::PropertyID::TextAnchor), |
161 | 0 | NamedPropertyID(CSS::PropertyID::FontSize), |
162 | 0 | NamedPropertyID(CSS::PropertyID::Mask), |
163 | 0 | NamedPropertyID(CSS::PropertyID::MaskType), |
164 | 0 | NamedPropertyID(CSS::PropertyID::ClipPath), |
165 | 0 | NamedPropertyID(CSS::PropertyID::ClipRule), |
166 | 0 | NamedPropertyID(CSS::PropertyID::Display), |
167 | 0 | }; |
168 | |
|
169 | 0 | CSS::Parser::ParsingContext parsing_context { document(), CSS::Parser::ParsingContext::Mode::SVGPresentationAttribute }; |
170 | 0 | for_each_attribute([&](auto& name, auto& value) { |
171 | 0 | for (auto property : attribute_style_properties) { |
172 | 0 | if (!name.equals_ignoring_ascii_case(property.name)) |
173 | 0 | continue; |
174 | 0 | if (auto style_value = parse_css_value(parsing_context, value, property.id)) |
175 | 0 | style.set_property(property.id, style_value.release_nonnull()); |
176 | 0 | break; |
177 | 0 | } |
178 | 0 | }); |
179 | 0 | } |
180 | | |
181 | | static FillRule to_svg_fill_rule(CSS::FillRule fill_rule) |
182 | 0 | { |
183 | 0 | switch (fill_rule) { |
184 | 0 | case CSS::FillRule::Nonzero: |
185 | 0 | return FillRule::Nonzero; |
186 | 0 | case CSS::FillRule::Evenodd: |
187 | 0 | return FillRule::Evenodd; |
188 | 0 | default: |
189 | 0 | VERIFY_NOT_REACHED(); |
190 | 0 | } |
191 | 0 | } |
192 | | |
193 | | Optional<FillRule> SVGGraphicsElement::fill_rule() const |
194 | 0 | { |
195 | 0 | if (!layout_node()) |
196 | 0 | return {}; |
197 | 0 | return to_svg_fill_rule(layout_node()->computed_values().fill_rule()); |
198 | 0 | } |
199 | | |
200 | | Optional<ClipRule> SVGGraphicsElement::clip_rule() const |
201 | 0 | { |
202 | 0 | if (!layout_node()) |
203 | 0 | return {}; |
204 | 0 | return to_svg_fill_rule(layout_node()->computed_values().clip_rule()); |
205 | 0 | } |
206 | | |
207 | | Optional<Gfx::Color> SVGGraphicsElement::fill_color() const |
208 | 0 | { |
209 | 0 | if (!layout_node()) |
210 | 0 | return {}; |
211 | | // FIXME: In the working-draft spec, `fill` is intended to be a shorthand, with `fill-color` |
212 | | // being what we actually want to use. But that's not final or widely supported yet. |
213 | 0 | return layout_node()->computed_values().fill().map([&](auto& paint) -> Gfx::Color { |
214 | 0 | if (!paint.is_color()) |
215 | 0 | return Color::Black; |
216 | 0 | return paint.as_color(); |
217 | 0 | }); |
218 | 0 | } |
219 | | |
220 | | Optional<Gfx::Color> SVGGraphicsElement::stroke_color() const |
221 | 0 | { |
222 | 0 | if (!layout_node()) |
223 | 0 | return {}; |
224 | | // FIXME: In the working-draft spec, `stroke` is intended to be a shorthand, with `stroke-color` |
225 | | // being what we actually want to use. But that's not final or widely supported yet. |
226 | 0 | return layout_node()->computed_values().stroke().map([](auto& paint) -> Gfx::Color { |
227 | 0 | if (!paint.is_color()) |
228 | 0 | return Color::Black; |
229 | 0 | return paint.as_color(); |
230 | 0 | }); |
231 | 0 | } |
232 | | |
233 | | Optional<float> SVGGraphicsElement::fill_opacity() const |
234 | 0 | { |
235 | 0 | if (!layout_node()) |
236 | 0 | return {}; |
237 | 0 | return layout_node()->computed_values().fill_opacity(); |
238 | 0 | } |
239 | | |
240 | | Optional<CSS::StrokeLinecap> SVGGraphicsElement::stroke_linecap() const |
241 | 0 | { |
242 | 0 | if (!layout_node()) |
243 | 0 | return {}; |
244 | 0 | return layout_node()->computed_values().stroke_linecap(); |
245 | 0 | } |
246 | | |
247 | | Optional<CSS::StrokeLinejoin> SVGGraphicsElement::stroke_linejoin() const |
248 | 0 | { |
249 | 0 | if (!layout_node()) |
250 | 0 | return {}; |
251 | 0 | return layout_node()->computed_values().stroke_linejoin(); |
252 | 0 | } |
253 | | |
254 | | Optional<CSS::NumberOrCalculated> SVGGraphicsElement::stroke_miterlimit() const |
255 | 0 | { |
256 | 0 | if (!layout_node()) |
257 | 0 | return {}; |
258 | 0 | return layout_node()->computed_values().stroke_miterlimit(); |
259 | 0 | } |
260 | | |
261 | | Optional<float> SVGGraphicsElement::stroke_opacity() const |
262 | 0 | { |
263 | 0 | if (!layout_node()) |
264 | 0 | return {}; |
265 | 0 | return layout_node()->computed_values().stroke_opacity(); |
266 | 0 | } |
267 | | |
268 | | Optional<float> SVGGraphicsElement::stroke_width() const |
269 | 0 | { |
270 | 0 | if (!layout_node()) |
271 | 0 | return {}; |
272 | | // FIXME: Converting to pixels isn't really correct - values should be in "user units" |
273 | | // https://svgwg.org/svg2-draft/coords.html#TermUserUnits |
274 | 0 | auto width = layout_node()->computed_values().stroke_width(); |
275 | | // Resolved relative to the "Scaled viewport size": https://www.w3.org/TR/2017/WD-fill-stroke-3-20170413/#scaled-viewport-size |
276 | | // FIXME: This isn't right, but it's something. |
277 | 0 | CSSPixels viewport_width = 0; |
278 | 0 | CSSPixels viewport_height = 0; |
279 | 0 | if (auto* svg_svg_element = shadow_including_first_ancestor_of_type<SVGSVGElement>()) { |
280 | 0 | if (auto svg_svg_layout_node = svg_svg_element->layout_node()) { |
281 | 0 | viewport_width = svg_svg_layout_node->computed_values().width().to_px(*svg_svg_layout_node, 0); |
282 | 0 | viewport_height = svg_svg_layout_node->computed_values().height().to_px(*svg_svg_layout_node, 0); |
283 | 0 | } |
284 | 0 | } |
285 | 0 | auto scaled_viewport_size = (viewport_width + viewport_height) * CSSPixels(0.5); |
286 | 0 | return width.to_px(*layout_node(), scaled_viewport_size).to_double(); |
287 | 0 | } |
288 | | |
289 | | // https://svgwg.org/svg2-draft/types.html#__svg__SVGGraphicsElement__getBBox |
290 | | JS::NonnullGCPtr<Geometry::DOMRect> SVGGraphicsElement::get_b_box(Optional<SVGBoundingBoxOptions>) |
291 | 0 | { |
292 | | // FIXME: It should be possible to compute this without layout updates. The bounding box is within the |
293 | | // SVG coordinate space (before any viewbox or other transformations), so it should be possible to |
294 | | // calculate this from SVG geometry without a full layout tree (at least for simple cases). |
295 | | // See: https://svgwg.org/svg2-draft/coords.html#BoundingBoxes |
296 | 0 | const_cast<DOM::Document&>(document()).update_layout(); |
297 | 0 | if (!layout_node()) |
298 | 0 | return Geometry::DOMRect::create(realm()); |
299 | | // Invert the SVG -> screen space transform. |
300 | 0 | auto owner_svg_element = this->owner_svg_element(); |
301 | 0 | if (!owner_svg_element) |
302 | 0 | return Geometry::DOMRect::create(realm()); |
303 | 0 | auto svg_element_rect = owner_svg_element->paintable_box()->absolute_rect(); |
304 | 0 | auto inverse_transform = static_cast<Painting::SVGGraphicsPaintable&>(*paintable_box()).computed_transforms().svg_to_css_pixels_transform().inverse(); |
305 | 0 | auto translated_rect = paintable_box()->absolute_rect().to_type<float>().translated(-svg_element_rect.location().to_type<float>()); |
306 | 0 | if (inverse_transform.has_value()) |
307 | 0 | translated_rect = inverse_transform->map(translated_rect); |
308 | 0 | return Geometry::DOMRect::create(realm(), translated_rect); |
309 | 0 | } |
310 | | |
311 | | JS::NonnullGCPtr<SVGAnimatedTransformList> SVGGraphicsElement::transform() const |
312 | 0 | { |
313 | 0 | dbgln("(STUBBED) SVGGraphicsElement::transform(). Called on: {}", debug_description()); |
314 | 0 | auto base_val = SVGTransformList::create(realm()); |
315 | 0 | auto anim_val = SVGTransformList::create(realm()); |
316 | 0 | return SVGAnimatedTransformList::create(realm(), base_val, anim_val); |
317 | 0 | } |
318 | | |
319 | | JS::GCPtr<Geometry::DOMMatrix> SVGGraphicsElement::get_screen_ctm() |
320 | 0 | { |
321 | 0 | dbgln("(STUBBED) SVGGraphicsElement::get_screen_ctm(). Called on: {}", debug_description()); |
322 | 0 | return Geometry::DOMMatrix::create(realm()); |
323 | 0 | } |
324 | | |
325 | | } |