1use std::str::FromStr;
2
3use euclid::default::Transform2D;
4use log::{debug, warn};
5use roxmltree::{Document, Node};
6use svgtypes::{AspectRatio, PathParser, PathSegment, PointsParser, TransformListParser, ViewBox};
7
8use super::{
9 path::apply_path,
10 transform::{get_viewport_transform, svg_transform_into_euclid_transform},
11 units::DimensionHint,
12 ConversionVisitor,
13};
14use crate::{converter::node_name, Turtle};
15
16const SVG_TAG_NAME: &str = "svg";
17const CLIP_PATH_TAG_NAME: &str = "clipPath";
18const PATH_TAG_NAME: &str = "path";
19const POLYLINE_TAG_NAME: &str = "polyline";
20const POLYGON_TAG_NAME: &str = "polygon";
21const RECT_TAG_NAME: &str = "rect";
22const CIRCLE_TAG_NAME: &str = "circle";
23const ELLIPSE_TAG_NAME: &str = "ellipse";
24const LINE_TAG_NAME: &str = "line";
25const GROUP_TAG_NAME: &str = "g";
26const DEFS_TAG_NAME: &str = "defs";
27const USE_TAG_NAME: &str = "use";
28const MARKER_TAG_NAME: &str = "marker";
29const SYMBOL_TAG_NAME: &str = "symbol";
30
31pub trait XmlVisitor {
32 fn visit_enter(&mut self, node: Node);
33 fn visit_exit(&mut self, node: Node);
34}
35
36fn should_render_node(node: Node) -> bool {
38 node.is_element()
39 && !node
40 .attribute("style")
41 .map_or(false, |style| style.contains("display:none"))
42 && !matches!(node.tag_name().name(), DEFS_TAG_NAME | MARKER_TAG_NAME | SYMBOL_TAG_NAME)
46}
47
48pub fn depth_first_visit(doc: &Document, visitor: &mut impl XmlVisitor) {
49 fn visit_node(node: Node, visitor: &mut impl XmlVisitor) {
50 if !should_render_node(node) {
51 return;
52 }
53 visitor.visit_enter(node);
54 node.children().for_each(|child| visit_node(child, visitor));
55 visitor.visit_exit(node);
56 }
57
58 doc.root()
59 .children()
60 .for_each(|child| visit_node(child, visitor));
61}
62
63impl<'a, T: Turtle> XmlVisitor for ConversionVisitor<'a, T> {
64 fn visit_enter(&mut self, node: Node) {
65 use PathSegment::*;
66
67 if node.tag_name().name() == CLIP_PATH_TAG_NAME {
68 warn!("Clip paths are not supported: {:?}", node);
69 }
70
71 if let Some(mut origin) = node.attribute("transform-origin").map(PointsParser::from) {
73 let _origin = origin.next();
74 warn!("transform-origin not supported yet");
75 }
76
77 let mut flattened_transform = if let Some(transform) = node.attribute("transform") {
78 TransformListParser::from(transform)
80 .map(|token| token.expect("could not parse a transform in a list of transforms"))
81 .map(svg_transform_into_euclid_transform)
82 .fold(Transform2D::identity(), |acc, t| t.then(&acc))
83 } else {
84 Transform2D::identity()
85 };
86
87 if node.has_tag_name(SVG_TAG_NAME) {
89 let view_box = node
90 .attribute("viewBox")
91 .map(ViewBox::from_str)
92 .transpose()
93 .expect("could not parse viewBox")
94 .filter(|view_box| {
95 if view_box.w <= 0. || view_box.h <= 0. {
96 warn!("Invalid viewBox: {view_box:?}");
97 false
98 } else {
99 true
100 }
101 });
102 let preserve_aspect_ratio = node.attribute("preserveAspectRatio").map(|attr| {
103 AspectRatio::from_str(attr).expect("could not parse preserveAspectRatio")
104 });
105 let mut viewport_size =
106 ["width", "height"].map(|attr| self.length_attr_to_user_units(&node, attr));
107
108 let dimensions_override: [_; 2] = self
109 .options
110 .dimensions
111 .map(|l| l.map(|l| self.length_to_user_units(l, DimensionHint::Horizontal)));
112 for (original_dim, override_dim) in viewport_size
113 .iter_mut()
114 .zip(dimensions_override.into_iter())
115 {
116 *original_dim = override_dim.or(*original_dim);
117 }
118
119 let intrinsic_aspect_ratio = match (view_box, viewport_size) {
122 (None, [Some(ref width), Some(ref height)]) => Some(*width / *height),
123 (Some(ref view_box), _) => Some(view_box.w / view_box.h),
124 (None, [None, None] | [None, Some(_)] | [Some(_), None]) => None,
125 };
126
127 let viewport_size = match (viewport_size, intrinsic_aspect_ratio, view_box) {
129 ([Some(w), Some(h)], _, _) => [w, h],
130 ([Some(w), None], Some(ratio), _) => [w, w / ratio],
131 ([None, Some(h)], Some(ratio), _) => [h * ratio, h],
132 ([None, None], _, Some(view_box)) => {
133 [view_box.w, view_box.h]
135 }
136 ([Some(d), None] | [None, Some(d)], None, None) => [d, d],
137 ([None, None], _, None) => {
138 [1., 1.]
140 }
141 ([None, Some(_)] | [Some(_), None], None, Some(_)) => {
142 unreachable!("intrinsic ratio necessarily exists")
143 }
144 };
145
146 let viewport_pos = ["x", "y"].map(|attr| self.length_attr_to_user_units(&node, attr));
147
148 self.viewport_dim_stack
149 .push(match (view_box.as_ref(), &viewport_size) {
150 (Some(ViewBox { w, h, .. }), _) => [*w, *h],
151 (None, [w, h]) => [*w, *h],
152 });
153
154 if let Some(view_box) = view_box {
155 let viewport_transform = get_viewport_transform(
156 view_box,
157 preserve_aspect_ratio,
158 viewport_size,
159 viewport_pos,
160 );
161 flattened_transform = flattened_transform.then(&viewport_transform);
162 }
163 flattened_transform = flattened_transform.then(&Transform2D::translation(
165 0.,
166 -(viewport_size[1] + viewport_pos[1].unwrap_or(0.)),
167 ));
168 } else if node.has_attribute("viewBox") {
169 warn!("View box is not supported on a {}", node.tag_name().name());
170 }
171
172 self.terrarium.push_transform(flattened_transform);
173
174 match node.tag_name().name() {
175 PATH_TAG_NAME => {
176 if let Some(d) = node.attribute("d") {
177 self.comment(&node);
178 apply_path(
179 &mut self.terrarium,
180 PathParser::from(d)
181 .map(|segment| segment.expect("could not parse path segment")),
182 );
183 } else {
184 warn!("There is a path node containing no actual path: {node:?}");
185 }
186 }
187 name @ (POLYLINE_TAG_NAME | POLYGON_TAG_NAME) => {
188 if let Some(points) = node.attribute("points") {
189 self.comment(&node);
190
191 let mut pp = PointsParser::from(points).peekable();
192 let path = pp
193 .peek()
194 .copied()
195 .map(|(x, y)| MoveTo { abs: true, x, y })
196 .into_iter()
197 .chain(pp.map(|(x, y)| LineTo { abs: true, x, y }))
198 .chain(
199 if name == POLYGON_TAG_NAME {
201 Some(ClosePath { abs: true })
202 } else {
203 None
204 },
205 );
206
207 apply_path(&mut self.terrarium, path);
208 } else {
209 warn!("There is a {name} node containing no actual path: {node:?}");
210 }
211 }
212 RECT_TAG_NAME => {
213 let x = self.length_attr_to_user_units(&node, "x").unwrap_or(0.);
214 let y = self.length_attr_to_user_units(&node, "y").unwrap_or(0.);
215 let width = self.length_attr_to_user_units(&node, "width");
216 let height = self.length_attr_to_user_units(&node, "height");
217 let rx = self.length_attr_to_user_units(&node, "rx").unwrap_or(0.);
218 let ry = self.length_attr_to_user_units(&node, "ry").unwrap_or(0.);
219 let has_radius = rx > 0. && ry > 0.;
220
221 match (width, height) {
222 (Some(width), Some(height)) => {
223 self.comment(&node);
224 apply_path(
225 &mut self.terrarium,
226 [
227 MoveTo {
228 abs: true,
229 x: x + rx,
230 y,
231 },
232 HorizontalLineTo {
233 abs: true,
234 x: x + width - rx,
235 },
236 EllipticalArc {
237 abs: true,
238 rx,
239 ry,
240 x_axis_rotation: 0.,
241 large_arc: false,
242 sweep: true,
243 x: x + width,
244 y: y + ry,
245 },
246 VerticalLineTo {
247 abs: true,
248 y: y + height - ry,
249 },
250 EllipticalArc {
251 abs: true,
252 rx,
253 ry,
254 x_axis_rotation: 0.,
255 large_arc: false,
256 sweep: true,
257 x: x + width - rx,
258 y: y + height,
259 },
260 HorizontalLineTo {
261 abs: true,
262 x: x + rx,
263 },
264 EllipticalArc {
265 abs: true,
266 rx,
267 ry,
268 x_axis_rotation: 0.,
269 large_arc: false,
270 sweep: true,
271 x,
272 y: y + height - ry,
273 },
274 VerticalLineTo {
275 abs: true,
276 y: y + ry,
277 },
278 EllipticalArc {
279 abs: true,
280 rx,
281 ry,
282 x_axis_rotation: 0.,
283 large_arc: false,
284 sweep: true,
285 x: x + rx,
286 y,
287 },
288 ClosePath { abs: true },
289 ]
290 .into_iter()
291 .filter(|p| has_radius || !matches!(p, EllipticalArc { .. })),
292 )
293 }
294 _other => {
295 warn!("Invalid rectangle node: {node:?}");
296 }
297 }
298 }
299 CIRCLE_TAG_NAME | ELLIPSE_TAG_NAME => {
300 let cx = self.length_attr_to_user_units(&node, "cx").unwrap_or(0.);
301 let cy = self.length_attr_to_user_units(&node, "cy").unwrap_or(0.);
302 let r = self.length_attr_to_user_units(&node, "r").unwrap_or(0.);
303 let rx = self.length_attr_to_user_units(&node, "rx").unwrap_or(r);
304 let ry = self.length_attr_to_user_units(&node, "ry").unwrap_or(r);
305 if rx > 0. && ry > 0. {
306 self.comment(&node);
307 apply_path(
308 &mut self.terrarium,
309 std::iter::once(MoveTo {
310 abs: true,
311 x: cx + rx,
312 y: cy,
313 })
314 .chain(
315 [(cx, cy + ry), (cx - rx, cy), (cx, cy - ry), (cx + rx, cy)].map(
316 |(x, y)| EllipticalArc {
317 abs: true,
318 rx,
319 ry,
320 x_axis_rotation: 0.,
321 large_arc: false,
322 sweep: true,
323 x,
324 y,
325 },
326 ),
327 )
328 .chain(std::iter::once(ClosePath { abs: true })),
329 );
330 } else {
331 warn!("Invalid {} node: {node:?}", node.tag_name().name());
332 }
333 }
334 LINE_TAG_NAME => {
335 let x1 = self.length_attr_to_user_units(&node, "x1");
336 let y1 = self.length_attr_to_user_units(&node, "y1");
337 let x2 = self.length_attr_to_user_units(&node, "x2");
338 let y2 = self.length_attr_to_user_units(&node, "y2");
339 match (x1, y1, x2, y2) {
340 (Some(x1), Some(y1), Some(x2), Some(y2)) => {
341 self.comment(&node);
342 apply_path(
343 &mut self.terrarium,
344 [
345 MoveTo {
346 abs: true,
347 x: x1,
348 y: y1,
349 },
350 LineTo {
351 abs: true,
352 x: x2,
353 y: y2,
354 },
355 ],
356 );
357 }
358 _other => {
359 warn!("Invalid line node: {node:?}");
360 }
361 }
362 }
363 USE_TAG_NAME => {
364 warn!("Unsupported node: {node:?}");
365 }
366 SVG_TAG_NAME | GROUP_TAG_NAME => {}
368 _ => {
369 debug!("Unknown node: {}", node.tag_name().name());
370 }
371 }
372
373 self.name_stack.push(node_name(&node));
374 }
375
376 fn visit_exit(&mut self, node: Node) {
377 self.terrarium.pop_transform();
378 self.name_stack.pop();
379 if node.tag_name().name() == SVG_TAG_NAME {
380 self.viewport_dim_stack.pop();
381 }
382 }
383}