cairo_svgpath/
lib.rs

1#![feature(proc_macro_hygiene)]
2extern crate proc_macro;
3use proc_macro::TokenStream;
4use quote::quote;
5
6#[proc_macro]
7pub fn svgpath(input: TokenStream) -> TokenStream {
8    let (ident, path) = get_params(input).expect("syntax error");
9    let mut path: Path = path.parse().expect("parsing the path failed");
10    simplify(&mut path)
11        .map(|segment| match segment {
12            PathSegment::MoveTo { x, y, .. } => quote! { #ident.move_to(#x, #y); },
13            PathSegment::LineTo { x, y, .. } => quote! { #ident.line_to(#x, #y); },
14            PathSegment::CurveTo {
15                x1,
16                y1,
17                x2,
18                y2,
19                x,
20                y,
21                ..
22            } => quote! { #ident.curve_to(#x1, #y1, #x2, #y2, #x, #y); },
23            PathSegment::ClosePath { .. } => quote! { #ident.close_path(); },
24            _ => unreachable!(),
25        })
26        .map(Into::<proc_macro::TokenStream>::into)
27        .collect()
28}
29
30fn get_params(input: TokenStream) -> Option<(proc_macro2::Ident, String)> {
31    let input: proc_macro2::TokenStream = input.into();
32    let mut input = input.into_iter();
33    let ident = input.next()?;
34    match input.next()? {
35        proc_macro2::TokenTree::Punct(punct) => {
36            if punct.as_char() != ',' {
37                return None;
38            }
39        }
40        _ => return None,
41    }
42
43    let ident = match ident {
44        proc_macro2::TokenTree::Ident(ident) => ident,
45        _ => return None,
46    };
47
48    let path = input.next()?;
49    // Handle trailing input
50    if let Some(_) = input.next() {
51        return None;
52    };
53
54    let path = match path {
55        proc_macro2::TokenTree::Literal(literal) => syn::Lit::new(literal),
56        _ => return None,
57    };
58
59    let path = match path {
60        syn::Lit::Str(string) => string.value(),
61        _ => return None,
62    };
63
64    Some((ident, path))
65}
66
67use svgtypes::{Path, PathSegment};
68
69// Adapted from resvg/usvg because the apes don't feel like marking their conversion routines as
70// pub. As a bonus, this uses half the amount of curves for arcs, yielding better runtime
71// performance. Did I mention I'm still amazed one can simply run a tesselator as a macro for this?
72fn simplify<'a>(path: &'a mut Path) -> impl Iterator<Item = PathSegment> + 'a {
73    path.conv_to_absolute();
74    // Previous MoveTo coordinates. Used for ClosePath.
75    let mut pmx = 0.0;
76    let mut pmy = 0.0;
77
78    // Previous coordinates.
79    let mut px = 0.0;
80    let mut py = 0.0;
81
82    // Previous SmoothQuadratic coordinates.
83    let mut ptx = 0.0;
84    let mut pty = 0.0;
85
86    let mut previous = None;
87
88    path.iter().flat_map(move |&seg| {
89        let segs = match seg {
90            PathSegment::MoveTo { .. } => vec![seg],
91            PathSegment::LineTo { .. } => vec![seg],
92            PathSegment::HorizontalLineTo { x, .. } => vec![PathSegment::LineTo {
93                x,
94                y: py,
95                abs: true,
96            }],
97            PathSegment::VerticalLineTo { y, .. } => vec![PathSegment::LineTo {
98                x: px,
99                y,
100                abs: true,
101            }],
102            PathSegment::CurveTo { .. } => vec![seg],
103            PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
104                let (x1, y1) = if let Some(prev_seg) = previous {
105                    match prev_seg {
106                        PathSegment::CurveTo { x2, y2, x, y, .. }
107                        | PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
108                            (x * 2.0 - x2, y * 2.0 - y2)
109                        }
110                        _ => (px, py),
111                    }
112                } else {
113                    (px, py)
114                };
115
116                vec![PathSegment::CurveTo {
117                    x1,
118                    y1,
119                    x2,
120                    y2,
121                    x,
122                    y,
123                    abs: true,
124                }]
125            }
126            PathSegment::Quadratic { x1, y1, x, y, .. } => vec![handle_quadratic(px, py, x1, y1, x, y)],
127            PathSegment::SmoothQuadratic { x, y, .. } => {
128                let (x1, y1) = if let Some(prev_seg) = previous {
129                    match prev_seg {
130                        PathSegment::Quadratic { x1, y1, x, y, .. } => (x * 2.0 - x1, y * 2.0 - y1),
131                        PathSegment::SmoothQuadratic { x, y, .. } => (x * 2.0 - ptx, y * 2.0 - pty),
132                        _ => (px, py),
133                    }
134                } else {
135                    (px, py)
136                };
137
138                ptx = x1;
139                pty = y1;
140
141                vec![handle_quadratic(px, py, x1, y1, x, y)]
142            }
143            PathSegment::EllipticalArc { rx, ry, x_axis_rotation, large_arc, sweep, x, y, .. } => {
144                let arc = lyon_geom::SvgArc {
145                    from: [px, py].into(),
146                    to: [x, y].into(),
147                    radii: [rx, ry].into(),
148                    x_rotation: euclid::Angle::degrees(x_axis_rotation),
149                    flags: lyon_geom::ArcFlags { large_arc, sweep },
150                };
151
152                let mut curves = vec![];
153                arc.for_each_cubic_bezier(&mut |curve| {
154                    curves.push(PathSegment::CurveTo {
155                        x1: curve.ctrl1.x,
156                        y1: curve.ctrl1.y,
157                        x2: curve.ctrl2.x,
158                        y2: curve.ctrl2.y,
159                        x: curve.to.x,
160                        y: curve.to.y,
161                        abs: true,
162                    });
163                });
164                curves
165            }
166            PathSegment::ClosePath { .. } => {
167                if let Some(PathSegment::ClosePath { .. }) = previous {
168                    // skip consecutive closes
169                    vec![]
170                } else {
171                    vec![seg]
172                }
173            }
174        };
175
176        if let Some(&seg) = segs.last() {
177            match seg {
178                PathSegment::MoveTo { x, y, .. } => {
179                    px = x;
180                    py = y;
181                    pmx = x;
182                    pmy = y;
183                }
184                PathSegment::LineTo { x, y, .. }
185                | PathSegment::CurveTo { x, y, .. } => {
186                    px = x;
187                    py = y;
188                }
189                PathSegment::ClosePath { .. } => {
190                    px = pmx;
191                    py = pmy;
192                }
193                _ => unreachable!(),
194            }
195        }
196
197        previous = Some(seg);
198        segs.into_iter()
199    })
200}
201
202fn handle_quadratic(px: f64, py: f64, x1: f64, y1: f64, x: f64, y: f64) -> PathSegment {
203    PathSegment::CurveTo {
204        x,
205        y,
206        x1: 2.0 / 3.0 * x1 + 1.0 / 3.0 * px,
207        y1: 2.0 / 3.0 * y1 + 1.0 / 3.0 * py,
208        x2: 2.0 / 3.0 * x1 + 1.0 / 3.0 * x,
209        y2: 2.0 / 3.0 * y1 + 1.0 / 3.0 * y,
210        abs: true,
211    }
212}