Skip to main content

usvg/parser/
use_node.rs

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.pre_concat(orig_ts);
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    // Cache the parent absolute transform.
273    let parent_abs_transform = parent.abs_transform;
274
275    let required = !transform.is_identity();
276
277    let Some(g) = converter::convert_group(node, state, required, cache, parent, &|cache, g| {
278        // Make sure they are correct before converting children.
279        g.transform = transform;
280        g.abs_transform = parent_abs_transform.pre_concat(transform);
281        g.is_context_element = is_context_element;
282
283        if state.parent_clip_path.is_some() {
284            converter::convert_clip_path_elements(node, state, cache, g);
285        } else {
286            converter::convert_children(node, state, cache, g);
287        }
288    }) else {
289        return;
290    };
291
292    parent.children.push(Node::Group(Box::new(g)));
293}
294
295fn get_clip_rect(
296    use_node: SvgNode,
297    symbol_node: SvgNode,
298    state: &converter::State,
299) -> Option<NonZeroRect> {
300    // No need to clip elements with overflow:visible.
301    if matches!(
302        symbol_node.attribute(AId::Overflow),
303        Some("visible") | Some("auto")
304    ) {
305        return None;
306    }
307
308    // A nested `svg` with only the `viewBox` attribute and no "rectangle" (x, y, width, height)
309    // should not be clipped.
310    if use_node.tag_name() == Some(EId::Svg) {
311        // Nested `svg` referenced by `use` still should be clipped, but by `use` bounds.
312        if state.use_size.0.is_none() && state.use_size.1.is_none() {
313            if !(use_node.has_attribute(AId::Width) && use_node.has_attribute(AId::Height)) {
314                return None;
315            }
316        }
317    }
318
319    let (x, y, mut w, mut h) = {
320        let x = use_node.convert_user_length(AId::X, state, Length::zero());
321        let y = use_node.convert_user_length(AId::Y, state, Length::zero());
322        let (w, h) = use_node_size(use_node, state);
323        (x, y, w, h)
324    };
325
326    if use_node.tag_name() == Some(EId::Svg) {
327        // If attributes `width` and/or `height` are provided on the `use` element,
328        // then these values will override the corresponding attributes
329        // on the `svg` in the generated tree.
330        w = state.use_size.0.unwrap_or(w);
331        h = state.use_size.1.unwrap_or(h);
332    }
333
334    if !w.is_valid_length() || !h.is_valid_length() {
335        return None;
336    }
337
338    NonZeroRect::from_xywh(x, y, w, h)
339}
340
341fn use_node_size(node: SvgNode, state: &converter::State) -> (f32, f32) {
342    let def = Length::new(100.0, LengthUnit::Percent);
343    let w = node.convert_user_length(AId::Width, state, def);
344    let h = node.convert_user_length(AId::Height, state, def);
345    (w, h)
346}
347
348fn viewbox_transform(
349    node: SvgNode,
350    linked: SvgNode,
351    state: &converter::State,
352) -> Option<Transform> {
353    let (mut w, mut h) = use_node_size(node, state);
354
355    if node.tag_name() == Some(EId::Svg) {
356        // If attributes `width` and/or `height` are provided on the `use` element,
357        // then these values will override the corresponding attributes
358        // on the `svg` in the generated tree.
359        w = state.use_size.0.unwrap_or(w);
360        h = state.use_size.1.unwrap_or(h);
361    }
362
363    let size = Size::from_wh(w, h)?;
364    let rect = linked.parse_viewbox()?;
365    let aspect = linked
366        .attribute(AId::PreserveAspectRatio)
367        .unwrap_or_default();
368    let view_box = ViewBox { rect, aspect };
369
370    Some(view_box.to_transform(size))
371}