skrifa/outline/
pen.rs

1//! Types for collecting the output when drawing a glyph outline.
2
3use alloc::{string::String, vec::Vec};
4use core::fmt::{self, Write};
5
6/// Interface for accepting a sequence of path commands.
7pub trait OutlinePen {
8    /// Emit a command to begin a new subpath at (x, y).
9    fn move_to(&mut self, x: f32, y: f32);
10
11    /// Emit a line segment from the current point to (x, y).
12    fn line_to(&mut self, x: f32, y: f32);
13
14    /// Emit a quadratic bezier segment from the current point with a control
15    /// point at (cx0, cy0) and ending at (x, y).
16    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32);
17
18    /// Emit a cubic bezier segment from the current point with control
19    /// points at (cx0, cy0) and (cx1, cy1) and ending at (x, y).
20    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32);
21
22    /// Emit a command to close the current subpath.
23    fn close(&mut self);
24}
25
26/// Single element of a path.
27#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
28pub enum PathElement {
29    /// Begin a new subpath at (x, y).
30    MoveTo { x: f32, y: f32 },
31    /// Draw a line from the current point to (x, y).
32    LineTo { x: f32, y: f32 },
33    /// Draw a quadratic bezier from the current point with a control point at
34    /// (cx0, cy0) and ending at (x, y).
35    QuadTo { cx0: f32, cy0: f32, x: f32, y: f32 },
36    /// Draw a cubic bezier from the current point with control points at
37    /// (cx0, cy0) and (cx1, cy1) and ending at (x, y).
38    CurveTo {
39        cx0: f32,
40        cy0: f32,
41        cx1: f32,
42        cy1: f32,
43        x: f32,
44        y: f32,
45    },
46    /// Close the current subpath.
47    Close,
48}
49
50/// Style for path conversion.
51///
52/// The order to process points in a glyf point stream is ambiguous when the
53/// first point is off-curve. Major implementations differ. Which one would
54/// you like to match?
55///
56/// **If you add a new one make sure to update the fuzzer.**
57#[derive(Debug, Default, Copy, Clone)]
58pub enum PathStyle {
59    /// If the first point is off-curve, check if the last is on-curve
60    /// If it is, start there. If it isn't, start at the implied midpoint
61    /// between first and last.
62    #[default]
63    FreeType,
64    /// If the first point is off-curve, check if the second is on-curve.
65    /// If it is, start there. If it isn't, start at the implied midpoint
66    /// between first and second.
67    ///
68    /// Matches hb-draw's interpretation of a point stream.
69    HarfBuzz,
70}
71
72impl OutlinePen for Vec<PathElement> {
73    fn move_to(&mut self, x: f32, y: f32) {
74        self.push(PathElement::MoveTo { x, y })
75    }
76
77    fn line_to(&mut self, x: f32, y: f32) {
78        self.push(PathElement::LineTo { x, y })
79    }
80
81    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
82        self.push(PathElement::QuadTo { cx0, cy0, x, y })
83    }
84
85    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
86        self.push(PathElement::CurveTo {
87            cx0,
88            cy0,
89            cx1,
90            cy1,
91            x,
92            y,
93        })
94    }
95
96    fn close(&mut self) {
97        self.push(PathElement::Close)
98    }
99}
100
101/// Pen that drops all drawing output into the ether.
102pub struct NullPen;
103
104impl OutlinePen for NullPen {
105    fn move_to(&mut self, _x: f32, _y: f32) {}
106    fn line_to(&mut self, _x: f32, _y: f32) {}
107    fn quad_to(&mut self, _cx0: f32, _cy0: f32, _x: f32, _y: f32) {}
108    fn curve_to(&mut self, _cx0: f32, _cy0: f32, _cx1: f32, _cy1: f32, _x: f32, _y: f32) {}
109    fn close(&mut self) {}
110}
111
112/// Pen that generates SVG style path data.
113#[derive(Clone, Default, Debug)]
114pub struct SvgPen(String, Option<usize>);
115
116impl SvgPen {
117    /// Creates a new SVG pen that formats floating point values with the
118    /// standard behavior.
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    /// Creates a new SVG pen with the given precision (the number of digits
124    /// that will be printed after the decimal).
125    pub fn with_precision(precision: usize) -> Self {
126        Self(String::default(), Some(precision))
127    }
128
129    /// Clears the content of the internal string.
130    pub fn clear(&mut self) {
131        self.0.clear();
132    }
133
134    fn maybe_push_space(&mut self) {
135        if !self.0.is_empty() {
136            self.0.push(' ');
137        }
138    }
139}
140
141impl core::ops::Deref for SvgPen {
142    type Target = str;
143
144    fn deref(&self) -> &Self::Target {
145        self.0.as_str()
146    }
147}
148
149impl OutlinePen for SvgPen {
150    fn move_to(&mut self, x: f32, y: f32) {
151        self.maybe_push_space();
152        let _ = if let Some(prec) = self.1 {
153            write!(self.0, "M{x:.0$},{y:.0$}", prec)
154        } else {
155            write!(self.0, "M{x},{y}")
156        };
157    }
158
159    fn line_to(&mut self, x: f32, y: f32) {
160        self.maybe_push_space();
161        let _ = if let Some(prec) = self.1 {
162            write!(self.0, "L{x:.0$},{y:.0$}", prec)
163        } else {
164            write!(self.0, "L{x},{y}")
165        };
166    }
167
168    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
169        self.maybe_push_space();
170        let _ = if let Some(prec) = self.1 {
171            write!(self.0, "Q{cx0:.0$},{cy0:.0$} {x:.0$},{y:.0$}", prec)
172        } else {
173            write!(self.0, "Q{cx0},{cy0} {x},{y}")
174        };
175    }
176
177    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
178        self.maybe_push_space();
179        let _ = if let Some(prec) = self.1 {
180            write!(
181                self.0,
182                "C{cx0:.0$},{cy0:.0$} {cx1:.0$},{cy1:.0$} {x:.0$},{y:.0$}",
183                prec
184            )
185        } else {
186            write!(self.0, "C{cx0},{cy0} {cx1},{cy1} {x},{y}")
187        };
188    }
189
190    fn close(&mut self) {
191        self.maybe_push_space();
192        self.0.push('Z');
193    }
194}
195
196impl AsRef<str> for SvgPen {
197    fn as_ref(&self) -> &str {
198        self.0.as_ref()
199    }
200}
201
202impl From<String> for SvgPen {
203    fn from(value: String) -> Self {
204        Self(value, None)
205    }
206}
207
208impl From<SvgPen> for String {
209    fn from(value: SvgPen) -> Self {
210        value.0
211    }
212}
213
214impl fmt::Display for SvgPen {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        write!(f, "{}", self.0)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn svg_pen_precision() {
226        let svg_data = [None, Some(1), Some(4)].map(|prec| {
227            let mut pen = match prec {
228                None => SvgPen::new(),
229                Some(prec) => SvgPen::with_precision(prec),
230            };
231            pen.move_to(1.0, 2.45556);
232            pen.line_to(1.2, 4.0);
233            pen.quad_to(2.0345, 3.56789, -0.157, -425.07);
234            pen.curve_to(-37.0010, 4.5, 2.0, 1.0, -0.5, -0.25);
235            pen.close();
236            pen.to_string()
237        });
238        let expected = [
239            "M1,2.45556 L1.2,4 Q2.0345,3.56789 -0.157,-425.07 C-37.001,4.5 2,1 -0.5,-0.25 Z", 
240            "M1.0,2.5 L1.2,4.0 Q2.0,3.6 -0.2,-425.1 C-37.0,4.5 2.0,1.0 -0.5,-0.2 Z", 
241            "M1.0000,2.4556 L1.2000,4.0000 Q2.0345,3.5679 -0.1570,-425.0700 C-37.0010,4.5000 2.0000,1.0000 -0.5000,-0.2500 Z"
242        ];
243        for (result, expected) in svg_data.iter().zip(&expected) {
244            assert_eq!(result, expected);
245        }
246    }
247}