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