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