1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use std::sync::Arc;
6
7use svgtypes::{Length, LengthUnit};
8
9use super::converter;
10use super::svgtree::{AId, EId, SvgNode};
11use crate::{Group, IsValidLength, Node, NonZeroRect, Path, Size, Transform, ViewBox};
12
13pub(crate) fn convert(
14 node: SvgNode,
15 state: &converter::State,
16 cache: &mut converter::Cache,
17 parent: &mut Group,
18) {
19 let child = match node.first_child() {
20 Some(v) => v,
21 None => return,
22 };
23
24 if state.parent_clip_path.is_some() && child.tag_name() == Some(EId::Symbol) {
25 // Ignore `symbol` referenced by `use` inside a `clipPath`.
26 // It will be ignored later anyway, but this will prevent
27 // a redundant `clipPath` creation (which is required for `symbol`).
28 return;
29 }
30
31 // We require an original transformation to setup 'clipPath'.
32 let mut orig_ts = node.resolve_transform(AId::Transform, state);
33 let mut new_ts = Transform::default();
34
35 {
36 let x = node.convert_user_length(AId::X, state, Length::zero());
37 let y = node.convert_user_length(AId::Y, state, Length::zero());
38 new_ts = new_ts.pre_translate(x, y);
39 }
40
41 let linked_to_symbol = child.tag_name() == Some(EId::Symbol);
42
43 if linked_to_symbol {
44 if let Some(ts) = viewbox_transform(node, child, state) {
45 new_ts = new_ts.pre_concat(ts);
46 }
47
48 if let Some(clip_rect) = get_clip_rect(node, child, state) {
49 let mut g = clip_element(node, clip_rect, orig_ts, state, cache);
50
51 // Make group for `use`.
52 if let Some(mut g2) =
53 converter::convert_group(node, state, true, cache, &mut g, &|cache, g2| {
54 convert_children(child, new_ts, state, cache, g2);
55 })
56 {
57 // We must reset transform, because it was already set
58 // to the group with clip-path.
59 g2.id = String::new(); // Prevent ID duplication.
60 g2.transform = Transform::default();
61 g.children.push(Node::Group(Box::new(g2)));
62 }
63
64 if g.children.is_empty() {
65 return;
66 }
67
68 g.calculate_bounding_boxes();
69 parent.children.push(Node::Group(Box::new(g)));
70 return;
71 }
72 }
73
74 orig_ts = orig_ts.pre_concat(new_ts);
75
76 if linked_to_symbol {
77 // Make group for `use`.
78 if let Some(mut g) =
79 converter::convert_group(node, state, false, cache, parent, &|cache, g| {
80 convert_children(child, orig_ts, state, cache, g);
81 })
82 {
83 g.transform = Transform::default();
84 parent.children.push(Node::Group(Box::new(g)));
85 }
86 } else {
87 let linked_to_svg = child.tag_name() == Some(EId::Svg);
88 if linked_to_svg {
89 // When a `use` element references a `svg` element,
90 // we have to remember `use` element size and use it
91 // instead of `svg` element size.
92
93 let def = Length::new(100.0, LengthUnit::Percent);
94
95 let mut state = state.clone();
96 // As per usual, the SVG spec doesn't clarify this edge case,
97 // but it seems like `use` size has to be reset by each `use`.
98 // Meaning if we have two nested `use` elements, where one had set `width` and
99 // other set `height`, we have to ignore the first `width`.
100 //
101 // Example:
102 // <use id="use1" xlink:href="#use2" width="100"/>
103 // <use id="use2" xlink:href="#svg2" height="100"/>
104 // <svg id="svg2" x="40" y="40" width="80" height="80" xmlns="http://www.w3.org/2000/svg"/>
105 //
106 // In this case `svg2` size is 80x100 and not 100x100.
107 state.use_size = (None, None);
108
109 // Width and height can be set independently.
110 if node.has_attribute(AId::Width) {
111 state.use_size.0 = Some(node.convert_user_length(AId::Width, &state, def));
112 }
113 if node.has_attribute(AId::Height) {
114 state.use_size.1 = Some(node.convert_user_length(AId::Height, &state, def));
115 }
116
117 convert_children(node, orig_ts, &state, cache, parent);
118 } else {
119 convert_children(node, orig_ts, state, cache, parent);
120 }
121 }
122}
123
124pub(crate) fn convert_svg(
125 node: SvgNode,
126 state: &converter::State,
127 cache: &mut converter::Cache,
128 parent: &mut Group,
129) {
130 // We require original transformation to setup 'clipPath'.
131 let mut orig_ts = node.resolve_transform(AId::Transform, state);
132 let mut new_ts = Transform::default();
133
134 {
135 let x = node.convert_user_length(AId::X, state, Length::zero());
136 let y = node.convert_user_length(AId::Y, state, Length::zero());
137 new_ts = new_ts.pre_translate(x, y);
138 }
139
140 if let Some(ts) = viewbox_transform(node, node, state) {
141 new_ts = new_ts.pre_concat(ts);
142 }
143
144 // We have to create a new state which would have its viewBox set to the current SVG element.
145 // Note that we're not updating State::size - it's a completely different property.
146 let mut new_state = state.clone();
147 new_state.view_box = {
148 if let Some(vb) = node.parse_viewbox() {
149 vb
150 } else {
151 // No `viewBox` attribute? Then use `x`, `y`, `width` and `height` instead.
152 let x = node.convert_user_length(AId::X, &new_state, Length::zero());
153 let y = node.convert_user_length(AId::Y, &new_state, Length::zero());
154 let (mut w, mut h) = use_node_size(node, &new_state);
155
156 // If attributes `width` and/or `height` are provided on the `use` element,
157 // then these values will override the corresponding attributes
158 // on the `svg` in the generated tree.
159 w = new_state.use_size.0.unwrap_or(w);
160 h = new_state.use_size.1.unwrap_or(h);
161
162 NonZeroRect::from_xywh(x, y, w, h).unwrap_or(new_state.view_box)
163 }
164 };
165
166 if let Some(clip_rect) = get_clip_rect(node, node, state) {
167 let mut g = clip_element(node, clip_rect, orig_ts, state, cache);
168 convert_children(node, new_ts, &new_state, cache, &mut g);
169 g.calculate_bounding_boxes();
170 parent.children.push(Node::Group(Box::new(g)));
171 } else {
172 orig_ts = orig_ts.pre_concat(new_ts);
173 convert_children(node, orig_ts, &new_state, cache, parent);
174 }
175}
176
177fn clip_element(
178 node: SvgNode,
179 clip_rect: NonZeroRect,
180 transform: Transform,
181 state: &converter::State,
182 cache: &mut converter::Cache,
183) -> Group {
184 // We can't set `clip-path` on the element itself,
185 // because it will be affected by a possible transform.
186 // So we have to create an additional group.
187
188 // Emulate a new viewport via clipPath.
189 //
190 // From:
191 // <defs/>
192 // <elem/>
193 //
194 // To:
195 // <defs>
196 // <clipPath id="clipPath1">
197 // <rect/>
198 // </clipPath>
199 // </defs>
200 // <g clip-path="ulr(#clipPath1)">
201 // <elem/>
202 // </g>
203
204 let mut clip_path = crate::ClipPath::empty(cache.gen_clip_path_id());
205
206 let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect(
207 clip_rect.to_rect(),
208 )))
209 .unwrap();
210 path.fill = Some(crate::Fill::default());
211 clip_path.root.children.push(Node::Path(Box::new(path)));
212
213 // Nodes generated by markers must not have an ID. Otherwise we would have duplicates.
214 let id = if state.parent_markers.is_empty() {
215 node.element_id().to_string()
216 } else {
217 String::new()
218 };
219
220 Group {
221 id,
222 transform,
223 clip_path: Some(Arc::new(clip_path)),
224 ..Group::empty()
225 }
226}
227
228fn convert_children(
229 node: SvgNode,
230 transform: Transform,
231 state: &converter::State,
232 cache: &mut converter::Cache,
233 parent: &mut Group,
234) {
235 let required: bool = !transform.is_identity();
236 if let Some(mut g: Group) =
237 converter::convert_group(node, state, force:required, cache, parent, &|cache: &mut Cache, g: &mut Group| {
238 if state.parent_clip_path.is_some() {
239 converter::convert_clip_path_elements(clip_node:node, state, cache, parent:g);
240 } else {
241 converter::convert_children(parent_node:node, state, cache, parent:g);
242 }
243 })
244 {
245 g.transform = transform;
246 parent.children.push(Node::Group(Box::new(g)));
247 }
248}
249
250fn get_clip_rect(
251 use_node: SvgNode,
252 symbol_node: SvgNode,
253 state: &converter::State,
254) -> Option<NonZeroRect> {
255 // No need to clip elements with overflow:visible.
256 if matches!(
257 symbol_node.attribute(AId::Overflow),
258 Some("visible") | Some("auto")
259 ) {
260 return None;
261 }
262
263 // A nested `svg` with only the `viewBox` attribute and no "rectangle" (x, y, width, height)
264 // should not be clipped.
265 if use_node.tag_name() == Some(EId::Svg) {
266 // Nested `svg` referenced by `use` still should be clipped, but by `use` bounds.
267 if state.use_size.0.is_none() && state.use_size.1.is_none() {
268 if !(use_node.has_attribute(AId::Width) && use_node.has_attribute(AId::Height)) {
269 return None;
270 }
271 }
272 }
273
274 let (x, y, mut w, mut h) = {
275 let x = use_node.convert_user_length(AId::X, state, Length::zero());
276 let y = use_node.convert_user_length(AId::Y, state, Length::zero());
277 let (w, h) = use_node_size(use_node, state);
278 (x, y, w, h)
279 };
280
281 if use_node.tag_name() == Some(EId::Svg) {
282 // If attributes `width` and/or `height` are provided on the `use` element,
283 // then these values will override the corresponding attributes
284 // on the `svg` in the generated tree.
285 w = state.use_size.0.unwrap_or(w);
286 h = state.use_size.1.unwrap_or(h);
287 }
288
289 if !w.is_valid_length() || !h.is_valid_length() {
290 return None;
291 }
292
293 NonZeroRect::from_xywh(x, y, w, h)
294}
295
296fn use_node_size(node: SvgNode, state: &converter::State) -> (f32, f32) {
297 let def: Length = Length::new(number:100.0, unit:LengthUnit::Percent);
298 let w: f32 = node.convert_user_length(AId::Width, state, def);
299 let h: f32 = node.convert_user_length(AId::Height, state, def);
300 (w, h)
301}
302
303fn viewbox_transform(
304 node: SvgNode,
305 linked: SvgNode,
306 state: &converter::State,
307) -> Option<Transform> {
308 let (mut w: f32, mut h: f32) = use_node_size(node, state);
309
310 if node.tag_name() == Some(EId::Svg) {
311 // If attributes `width` and/or `height` are provided on the `use` element,
312 // then these values will override the corresponding attributes
313 // on the `svg` in the generated tree.
314 w = state.use_size.0.unwrap_or(default:w);
315 h = state.use_size.1.unwrap_or(default:h);
316 }
317
318 let size: Size = Size::from_wh(width:w, height:h)?;
319 let rect: NonZeroRect = linked.parse_viewbox()?;
320 let aspect: AspectRatio = linkedOption
321 .attribute(AId::PreserveAspectRatio)
322 .unwrap_or_default();
323 let view_box: ViewBox = ViewBox { rect, aspect };
324
325 Some(view_box.to_transform(img_size:size))
326}
327