svg2gcode/converter/
visit.rs

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
36/// Used to skip over SVG elements that are explicitly marked as do not render
37fn 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        // - Defs are not rendered
43        // - Markers are not directly rendered
44        // - Symbols are not directly rendered
45        && !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        // TODO: https://www.w3.org/TR/css-transforms-1/#transform-origin-property
72        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            // https://stackoverflow.com/questions/18582935/the-applying-order-of-svg-transforms
79            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        // https://www.w3.org/TR/SVG/coords.html#EstablishingANewSVGViewport
88        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            // https://www.w3.org/TR/SVG/coords.html#SizingSVGInCSS
120            // aka _natural_ aspect ratio
121            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            // https://www.w3.org/TR/css-images-3/#default-sizing
128            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                    // Fallback: if there is no width or height, assume the coordinate system is just pixels on the viewport
134                    [view_box.w, view_box.h]
135                }
136                ([Some(d), None] | [None, Some(d)], None, None) => [d, d],
137                ([None, None], _, None) => {
138                    // We have no info at all, nothing can be done
139                    [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            // Part 2 of converting from SVG to GCode coordinates
164            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                            // Path must be closed if this is a polygon
200                            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            // No-op tags
367            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}