Skip to main content

read_fonts/model/glyph/outline/
unscaled.rs

1//! Outlines in font units, as points rather than path commands.
2//!
3//! An autohinter needs outline points in the form the `glyf` table stores
4//! them: whole font units, marked on-curve or off-curve, and grouped by
5//! contour. [`UnscaledOutlineSink`] receives these points. A `glyf` outline
6//! provides its points directly. A charstring or a `VARC` composite is drawn
7//! as a path and converted to points.
8
9use super::PathSink;
10use crate::tables::glyf::PointFlags;
11
12/// Receives the points of an outline in font units, one contour at a time.
13pub trait UnscaledOutlineSink {
14    /// Begins a contour.
15    fn start_contour(&mut self);
16
17    /// Adds a point to the open contour.
18    ///
19    /// `flags` indicate whether the point is on the curve or is an off-curve
20    /// control point of a quadratic or cubic curve. The interpreter marker
21    /// flags are always clear.
22    fn point(&mut self, x: i32, y: i32, flags: PointFlags);
23
24    /// Ends the open contour.
25    fn end_contour(&mut self);
26}
27
28impl<S: UnscaledOutlineSink + ?Sized> UnscaledOutlineSink for &mut S {
29    fn start_contour(&mut self) {
30        (**self).start_contour()
31    }
32
33    fn point(&mut self, x: i32, y: i32, flags: PointFlags) {
34        (**self).point(x, y, flags)
35    }
36
37    fn end_contour(&mut self) {
38        (**self).end_contour()
39    }
40}
41
42#[derive(Copy, Clone)]
43enum PendingElement {
44    Line([f32; 2]),
45    Quad([f32; 4]),
46    Cubic([f32; 6]),
47}
48
49/// A [`PathSink`] that converts a path into points and sends them to an
50/// [`UnscaledOutlineSink`].
51///
52/// This is used for sources that can only draw a path. The path must be in
53/// font units. Each segment's end point is held until the next command. When
54/// a contour closes at its start point, that point is not repeated. This
55/// matches how FreeType builds a TrueType outline from a charstring.
56pub(crate) struct PointsFromPath<'a, S: ?Sized> {
57    sink: &'a mut S,
58    /// True while a contour is open.
59    open: bool,
60    last_start: Option<(f32, f32)>,
61    pending: Option<PendingElement>,
62}
63
64impl<'a, S: ?Sized> PointsFromPath<'a, S> {
65    pub(crate) fn new(sink: &'a mut S) -> Self {
66        Self {
67            sink,
68            open: false,
69            last_start: None,
70            pending: None,
71        }
72    }
73}
74
75impl<S: UnscaledOutlineSink + ?Sized> PointsFromPath<'_, S> {
76    fn push(&mut self, x: f32, y: f32, flags: PointFlags) {
77        if !self.open {
78            self.sink.start_contour();
79            self.open = true;
80        }
81        // Truncation toward zero, as a charstring coordinate has always been
82        // taken into TrueType points here.
83        self.sink.point(x as i32, y as i32, flags);
84    }
85
86    fn flush_pending(&mut self, for_close: bool) {
87        if let Some(element) = self.pending.take() {
88            let [x, y] = match element {
89                PendingElement::Line([x, y]) => [x, y],
90                PendingElement::Quad([x0, y0, x, y]) => {
91                    self.push(x0, y0, PointFlags::off_curve_quad());
92                    [x, y]
93                }
94                PendingElement::Cubic([x0, y0, x1, y1, x, y]) => {
95                    self.push(x0, y0, PointFlags::off_curve_cubic());
96                    self.push(x1, y1, PointFlags::off_curve_cubic());
97                    [x, y]
98                }
99            };
100            if !for_close || self.last_start != Some((x, y)) {
101                self.push(x, y, PointFlags::on_curve());
102            }
103        }
104    }
105
106    fn close_contour(&mut self) {
107        self.flush_pending(true);
108        if self.open {
109            self.sink.end_contour();
110            self.open = false;
111        }
112    }
113
114    /// Ends the open contour, if any.
115    pub(crate) fn finish(mut self) {
116        self.close_contour();
117    }
118}
119
120impl<S: UnscaledOutlineSink + ?Sized> PathSink for PointsFromPath<'_, S> {
121    fn move_to(&mut self, x: f32, y: f32) {
122        self.close_contour();
123        self.push(x, y, PointFlags::on_curve());
124        self.last_start = Some((x, y));
125    }
126
127    fn line_to(&mut self, x: f32, y: f32) {
128        self.flush_pending(false);
129        self.pending = Some(PendingElement::Line([x, y]));
130    }
131
132    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
133        self.flush_pending(false);
134        self.pending = Some(PendingElement::Quad([cx0, cy0, x, y]));
135    }
136
137    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
138        self.flush_pending(false);
139        self.pending = Some(PendingElement::Cubic([cx0, cy0, cx1, cy1, x, y]));
140    }
141
142    fn close(&mut self) {
143        self.close_contour();
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::model::glyph::outline::DrawMemory;
151    use crate::{model::Font, types::GlyphId};
152    use alloc::vec::Vec;
153
154    /// Records the calls made to a sink, for testing.
155    #[derive(Default)]
156    struct Recorded {
157        points: Vec<(i32, i32, u8)>,
158        starts: Vec<usize>,
159        open: bool,
160        ended: usize,
161    }
162
163    impl UnscaledOutlineSink for Recorded {
164        fn start_contour(&mut self) {
165            assert!(!self.open, "a contour was started inside another");
166            self.open = true;
167            self.starts.push(self.points.len());
168        }
169
170        fn point(&mut self, x: i32, y: i32, flags: PointFlags) {
171            assert!(self.open, "a point arrived outside a contour");
172            self.points.push((x, y, flags.to_bits()));
173        }
174
175        fn end_contour(&mut self) {
176            assert!(self.open, "a contour ended that never started");
177            self.open = false;
178            self.ended += 1;
179        }
180    }
181
182    fn record(data: &[u8], glyph_id: u32) -> Recorded {
183        let font = Font::new(data.to_vec(), 0).unwrap();
184        let glyph = font.outline_glyphs().get(GlyphId::new(glyph_id)).unwrap();
185        let mut recorded = Recorded::default();
186        glyph
187            .draw_unscaled(DrawMemory::Scratch, &mut recorded)
188            .unwrap();
189        assert!(!recorded.open);
190        assert_eq!(recorded.ended, recorded.starts.len());
191        recorded
192    }
193
194    #[test]
195    fn read_glyf_outline() {
196        let recorded = record(font_test_data::MATERIAL_SYMBOLS_SUBSET, 5);
197        let expected = [
198            // contour 0
199            (400, 80, 1),
200            (400, 360, 1),
201            (320, 360, 1),
202            (320, 600, 1),
203            (320, 633, 0),
204            (367, 680, 0),
205            (400, 680, 1),
206            (560, 680, 1),
207            (593, 680, 0),
208            (640, 633, 0),
209            (640, 600, 1),
210            (640, 360, 1),
211            (560, 360, 1),
212            (560, 80, 1),
213            // contour 1
214            (480, 720, 1),
215            (447, 720, 0),
216            (400, 767, 0),
217            (400, 800, 1),
218            (400, 833, 0),
219            (447, 880, 0),
220            (480, 880, 1),
221            (513, 880, 0),
222            (560, 833, 0),
223            (560, 800, 1),
224            (560, 767, 0),
225            (513, 720, 0),
226        ];
227        assert_eq!(recorded.points, expected);
228        assert_eq!(recorded.starts, [0, 14]);
229    }
230
231    #[test]
232    fn the_advance_comes_with_the_points() {
233        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET.to_vec(), 0).unwrap();
234        let glyph = font.outline_glyphs().get(GlyphId::new(5)).unwrap();
235        let metrics = glyph
236            .draw_unscaled(DrawMemory::Scratch, &mut Recorded::default())
237            .unwrap();
238        assert_eq!(
239            metrics.h_advance,
240            Some(font.glyph_metrics().h_advance(GlyphId::new(5)))
241        );
242    }
243
244    #[test]
245    fn read_cubic_glyf_outline() {
246        let recorded = record(font_test_data::CUBIC_GLYF, 2);
247        let expected = [
248            (278, 710, 1),
249            (278, 470, 1),
250            (300, 500, 128),
251            (800, 500, 128),
252            (998, 470, 1),
253            (998, 710, 1),
254        ];
255        assert_eq!(recorded.points, expected);
256        assert_eq!(recorded.starts, [0]);
257    }
258
259    #[test]
260    fn read_cff_outline() {
261        let recorded = record(font_test_data::CANTARELL_VF_TRIMMED, 2);
262        let expected = [
263            // contour 0
264            (83, 0, 1),
265            (163, 0, 1),
266            (163, 482, 1),
267            (83, 482, 1),
268            // contour 1
269            (124, 595, 1),
270            (160, 595, 128),
271            (181, 616, 128),
272            (181, 652, 1),
273            (181, 688, 128),
274            (160, 709, 128),
275            (124, 709, 1),
276            (88, 709, 128),
277            (67, 688, 128),
278            (67, 652, 1),
279            (67, 616, 128),
280            (88, 595, 128),
281        ];
282        assert_eq!(recorded.points, expected);
283        assert_eq!(recorded.starts, [0, 4]);
284    }
285
286    #[test]
287    fn a_varc_composite_rebuilds_from_its_path() {
288        let font = Font::new(font_test_data::varc::CJK_6868.to_vec(), 0).unwrap();
289        let outlines = font.outline_glyphs();
290        let gid = (0..font.num_glyphs())
291            .map(GlyphId::new)
292            .find(|gid| {
293                matches!(
294                    outlines.get(*gid),
295                    Some(crate::model::glyph::outline::OutlineGlyph::Varc(_))
296                )
297            })
298            .unwrap();
299        let mut recorded = Recorded::default();
300        outlines
301            .get(gid)
302            .unwrap()
303            .draw_unscaled(DrawMemory::Scratch, &mut recorded)
304            .unwrap();
305        assert!(recorded.points.len() > 2);
306        assert_eq!(recorded.starts.first(), Some(&0));
307        assert!(!recorded.open);
308    }
309
310    // A contour that closes with a quad does not repeat its start point.
311    #[test]
312    fn a_closing_quad_does_not_repeat_the_start() {
313        let mut recorded = Recorded::default();
314        let mut pen = PointsFromPath::new(&mut recorded);
315        pen.move_to(0.0, 0.0);
316        pen.line_to(10.0, 0.0);
317        pen.quad_to(10.0, 10.0, 0.0, 0.0);
318        pen.close();
319        pen.finish();
320        let on = PointFlags::on_curve().to_bits();
321        let off = PointFlags::off_curve_quad().to_bits();
322        assert_eq!(recorded.points, [(0, 0, on), (10, 0, on), (10, 10, off)]);
323    }
324
325    // No contour of a VARC composite, whose glyf components draw as quads,
326    // repeats its start point.
327    #[test]
328    fn varc_contours_do_not_repeat_their_start() {
329        let font = Font::new(font_test_data::varc::CJK_6868.to_vec(), 0).unwrap();
330        let outlines = font.outline_glyphs();
331        let mut contours = 0;
332        for gid in (0..font.num_glyphs()).map(GlyphId::new) {
333            let Some(glyph @ crate::model::glyph::outline::OutlineGlyph::Varc(_)) =
334                outlines.get(gid)
335            else {
336                continue;
337            };
338            let mut recorded = Recorded::default();
339            glyph
340                .draw_unscaled(DrawMemory::Scratch, &mut recorded)
341                .unwrap();
342            let ends = recorded.starts[1..]
343                .iter()
344                .copied()
345                .chain([recorded.points.len()]);
346            for (start, end) in recorded.starts.iter().copied().zip(ends) {
347                let (first, last) = (recorded.points[start], recorded.points[end - 1]);
348                assert!(
349                    end - start < 2 || (first.0, first.1) != (last.0, last.1),
350                    "glyph {gid} contour at {start}"
351                );
352                contours += 1;
353            }
354        }
355        assert!(contours > 0);
356    }
357
358    /// When a contour ends with a line or cubic whose end matches the start
359    /// point, omit the last on curve. This matches FreeType behavior when
360    /// constructing a TrueType style outline from a CFF font.
361    #[test]
362    fn omit_unnecessary_trailing_oncurves() {
363        let mut recorded = Recorded::default();
364        let mut pen = PointsFromPath::new(&mut recorded);
365        pen.move_to(0.5, 1.5);
366        pen.line_to(1.0, 2.0);
367        // matches start, omit last on curve
368        pen.line_to(0.5, 1.5);
369        pen.close();
370        pen.move_to(5.0, 6.0);
371        pen.curve_to(1.0, 1.0, 2.0, 2.0, 2.0, 3.0);
372        // matches start, omit last on curve
373        pen.curve_to(1.0, 1.0, 2.0, 2.0, 5.0, 6.0);
374        pen.close();
375        pen.move_to(5.0, 6.0);
376        // doesn't match start, keep on curve
377        pen.curve_to(1.0, 1.0, 2.0, 2.0, 2.0, 3.0);
378        // and an unclosed contour is still ended
379        pen.finish();
380        let on_curves: Vec<_> = recorded
381            .points
382            .iter()
383            .map(|(_, _, flags)| PointFlags::from_bits(*flags).is_on_curve())
384            .collect();
385        #[rustfmt::skip]
386        let expected_on_curves = [
387            true,  // move
388            true,  // line
389                   // trailing line omitted
390            true,  // move
391            false, // cubic 1 off curve 1
392            false, // cubic 1 off curve 2
393            true,  // cubic 1 on curve
394            false, // cubic 2 off curve 1
395            false, // cubic 2 off curve 2
396                   // trailing on curve omitted
397            true,  // move
398            false, // cubic 1 off curve 1
399            false, // cubic 1 off curve 2
400            true,  // trailing on curve retained
401        ];
402        assert_eq!(on_curves, expected_on_curves);
403        assert_eq!(recorded.starts, [0, 2, 8]);
404        assert_eq!(recorded.ended, 3);
405    }
406}