1 | // Copyright 2019 the Resvg Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use std::sync::Arc; |
5 | |
6 | use svgtypes::{Length, LengthUnit}; |
7 | |
8 | use super::svgtree::{AId, EId, SvgNode}; |
9 | use super::{converter, style}; |
10 | use crate::tree::ContextElement; |
11 | use crate::{Group, IsValidLength, Node, NonZeroRect, Path, Size, Transform, ViewBox}; |
12 | |
13 | pub(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 | |
163 | pub(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 | |
213 | fn 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 | |
264 | fn 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 | |
294 | fn 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 | |
340 | fn 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 | |
347 | fn 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 | |