skrifa/outline/
unscaled.rs

1//! Compact representation of an unscaled, unhinted outline.
2
3#![allow(dead_code)]
4
5use super::DrawError;
6use crate::collections::SmallVec;
7use core::ops::Range;
8use raw::{
9    tables::glyf::PointFlags,
10    types::{F26Dot6, Point},
11};
12
13#[derive(Copy, Clone, Default, Debug)]
14pub(super) struct UnscaledPoint {
15    pub x: i16,
16    pub y: i16,
17    pub flags: PointFlags,
18    pub is_contour_start: bool,
19}
20
21impl UnscaledPoint {
22    pub fn from_glyf_point(
23        point: Point<F26Dot6>,
24        flags: PointFlags,
25        is_contour_start: bool,
26    ) -> Self {
27        let point = point.map(|x| (x.to_bits() >> 6) as i16);
28        Self {
29            x: point.x,
30            y: point.y,
31            flags: flags.without_markers(),
32            is_contour_start,
33        }
34    }
35
36    pub fn is_on_curve(self) -> bool {
37        self.flags.is_on_curve()
38    }
39}
40
41pub(super) trait UnscaledOutlineSink {
42    fn try_reserve(&mut self, additional: usize) -> Result<(), DrawError>;
43    fn push(&mut self, point: UnscaledPoint) -> Result<(), DrawError>;
44    fn extend(&mut self, points: impl IntoIterator<Item = UnscaledPoint>) -> Result<(), DrawError> {
45        for point in points.into_iter() {
46            self.push(point)?;
47        }
48        Ok(())
49    }
50}
51
52// please can I have smallvec?
53pub(super) struct UnscaledOutlineBuf<const INLINE_CAP: usize>(SmallVec<UnscaledPoint, INLINE_CAP>);
54
55impl<const INLINE_CAP: usize> UnscaledOutlineBuf<INLINE_CAP> {
56    pub fn new() -> Self {
57        Self(SmallVec::new())
58    }
59
60    pub fn clear(&mut self) {
61        self.0.clear();
62    }
63
64    pub fn as_ref(&self) -> UnscaledOutlineRef<'_> {
65        UnscaledOutlineRef {
66            points: self.0.as_slice(),
67        }
68    }
69}
70
71impl<const INLINE_CAP: usize> UnscaledOutlineSink for UnscaledOutlineBuf<INLINE_CAP> {
72    fn try_reserve(&mut self, additional: usize) -> Result<(), DrawError> {
73        if !self.0.try_reserve(additional) {
74            Err(DrawError::InsufficientMemory)
75        } else {
76            Ok(())
77        }
78    }
79
80    fn push(&mut self, point: UnscaledPoint) -> Result<(), DrawError> {
81        self.0.push(point);
82        Ok(())
83    }
84}
85
86#[derive(Copy, Clone, Debug)]
87pub(super) struct UnscaledOutlineRef<'a> {
88    pub points: &'a [UnscaledPoint],
89}
90
91impl UnscaledOutlineRef<'_> {
92    /// Returns the range of contour points and the index of the point within
93    /// that contour for the last point where `f` returns true.
94    ///
95    /// This is common code used for finding extrema when materializing blue
96    /// zones.
97    ///
98    /// For example: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L509>
99    pub fn find_last_contour(
100        &self,
101        mut f: impl FnMut(&UnscaledPoint) -> bool,
102    ) -> Option<(Range<usize>, usize)> {
103        if self.points.is_empty() {
104            return None;
105        }
106        let mut best_contour = 0..0;
107        // Index of the best point relative to the start of the best contour
108        let mut best_point = 0;
109        let mut cur_contour = 0..0;
110        let mut found_best_in_cur_contour = false;
111        for (point_ix, point) in self.points.iter().enumerate() {
112            if point.is_contour_start {
113                if found_best_in_cur_contour {
114                    best_contour = cur_contour;
115                }
116                cur_contour = point_ix..point_ix;
117                found_best_in_cur_contour = false;
118                // Ignore single point contours
119                match self.points.get(point_ix + 1) {
120                    Some(next_point) if next_point.is_contour_start => continue,
121                    None => continue,
122                    _ => {}
123                }
124            }
125            cur_contour.end += 1;
126            if f(point) {
127                best_point = point_ix - cur_contour.start;
128                found_best_in_cur_contour = true;
129            }
130        }
131        if found_best_in_cur_contour {
132            best_contour = cur_contour;
133        }
134        if !best_contour.is_empty() {
135            Some((best_contour, best_point))
136        } else {
137            None
138        }
139    }
140}
141
142#[derive(Copy, Clone)]
143enum PendingElement {
144    Line([f32; 2]),
145    Cubic([f32; 6]),
146}
147
148/// Adapts an UnscaledOutlineSink to be fed from a pen while tracking
149/// memory allocation errors.
150pub(super) struct UnscaledPenAdapter<'a, T> {
151    sink: &'a mut T,
152    failed: bool,
153    last_start: Option<(f32, f32)>,
154    pending: Option<PendingElement>,
155}
156
157impl<'a, T> UnscaledPenAdapter<'a, T> {
158    pub fn new(sink: &'a mut T) -> Self {
159        Self {
160            sink,
161            failed: false,
162            last_start: None,
163            pending: None,
164        }
165    }
166}
167
168impl<T> UnscaledPenAdapter<'_, T>
169where
170    T: UnscaledOutlineSink,
171{
172    fn push(&mut self, x: f32, y: f32, flags: PointFlags, is_contour_start: bool) {
173        if self
174            .sink
175            .push(UnscaledPoint {
176                x: x as i16,
177                y: y as i16,
178                flags,
179                is_contour_start,
180            })
181            .is_err()
182        {
183            self.failed = true;
184        }
185    }
186
187    fn flush_pending(&mut self, for_close: bool) {
188        if let Some(element) = self.pending.take() {
189            let [x, y] = match element {
190                PendingElement::Line([x, y]) => [x, y],
191                PendingElement::Cubic([x0, y0, x1, y1, x, y]) => {
192                    self.push(x0, y0, PointFlags::off_curve_cubic(), false);
193                    self.push(x1, y1, PointFlags::off_curve_cubic(), false);
194                    [x, y]
195                }
196            };
197            if !for_close || self.last_start != Some((x, y)) {
198                self.push(x, y, PointFlags::on_curve(), false);
199            }
200        }
201    }
202
203    pub fn finish(mut self) -> Result<(), DrawError> {
204        self.flush_pending(true);
205        if self.failed {
206            Err(DrawError::InsufficientMemory)
207        } else {
208            Ok(())
209        }
210    }
211}
212
213impl<T: UnscaledOutlineSink> super::OutlinePen for UnscaledPenAdapter<'_, T> {
214    fn move_to(&mut self, x: f32, y: f32) {
215        self.push(x, y, PointFlags::on_curve(), true);
216        self.last_start = Some((x, y));
217    }
218
219    fn line_to(&mut self, x: f32, y: f32) {
220        self.flush_pending(false);
221        self.pending = Some(PendingElement::Line([x, y]));
222    }
223
224    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
225        self.flush_pending(false);
226        self.push(cx0, cy0, PointFlags::off_curve_quad(), false);
227        self.push(x, y, PointFlags::on_curve(), false);
228    }
229
230    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
231        self.flush_pending(false);
232        self.pending = Some(PendingElement::Cubic([cx0, cy0, cx1, cy1, x, y]));
233    }
234
235    fn close(&mut self) {
236        self.flush_pending(true);
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use crate::{outline::OutlinePen, prelude::LocationRef, MetadataProvider};
244    use raw::{types::GlyphId, FontRef};
245
246    #[test]
247    fn read_glyf_outline() {
248        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
249        let glyph = font.outline_glyphs().get(GlyphId::new(5)).unwrap();
250        let mut outline = UnscaledOutlineBuf::<32>::new();
251        glyph
252            .draw_unscaled(LocationRef::default(), None, &mut outline)
253            .unwrap();
254        let outline = outline.as_ref();
255        let expected = [
256            // contour 0
257            (400, 80, 1),
258            (400, 360, 1),
259            (320, 360, 1),
260            (320, 600, 1),
261            (320, 633, 0),
262            (367, 680, 0),
263            (400, 680, 1),
264            (560, 680, 1),
265            (593, 680, 0),
266            (640, 633, 0),
267            (640, 600, 1),
268            (640, 360, 1),
269            (560, 360, 1),
270            (560, 80, 1),
271            // contour 1
272            (480, 720, 1),
273            (447, 720, 0),
274            (400, 767, 0),
275            (400, 800, 1),
276            (400, 833, 0),
277            (447, 880, 0),
278            (480, 880, 1),
279            (513, 880, 0),
280            (560, 833, 0),
281            (560, 800, 1),
282            (560, 767, 0),
283            (513, 720, 0),
284        ];
285        let points = outline
286            .points
287            .iter()
288            .map(|point| (point.x, point.y, point.flags.to_bits()))
289            .collect::<Vec<_>>();
290        assert_eq!(points, expected);
291    }
292
293    #[test]
294    #[cfg(feature = "spec_next")]
295    fn read_cubic_glyf_outline() {
296        let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
297        let glyph = font.outline_glyphs().get(GlyphId::new(2)).unwrap();
298        let mut outline = UnscaledOutlineBuf::<32>::new();
299        glyph
300            .draw_unscaled(LocationRef::default(), None, &mut outline)
301            .unwrap();
302        let outline = outline.as_ref();
303        let expected = [
304            // contour 0
305            (278, 710, 1),
306            (278, 470, 1),
307            (300, 500, 128),
308            (800, 500, 128),
309            (998, 470, 1),
310            (998, 710, 1),
311        ];
312        let points = outline
313            .points
314            .iter()
315            .map(|point| (point.x, point.y, point.flags.to_bits()))
316            .collect::<Vec<_>>();
317        assert_eq!(points, expected);
318    }
319
320    #[test]
321    fn read_cff_outline() {
322        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
323        let glyph = font.outline_glyphs().get(GlyphId::new(2)).unwrap();
324        let mut outline = UnscaledOutlineBuf::<32>::new();
325        glyph
326            .draw_unscaled(LocationRef::default(), None, &mut outline)
327            .unwrap();
328        let outline = outline.as_ref();
329        let expected = [
330            // contour 0
331            (83, 0, 1),
332            (163, 0, 1),
333            (163, 482, 1),
334            (83, 482, 1),
335            // contour 1
336            (124, 595, 1),
337            (160, 595, 128),
338            (181, 616, 128),
339            (181, 652, 1),
340            (181, 688, 128),
341            (160, 709, 128),
342            (124, 709, 1),
343            (88, 709, 128),
344            (67, 688, 128),
345            (67, 652, 1),
346            (67, 616, 128),
347            (88, 595, 128),
348        ];
349        let points = outline
350            .points
351            .iter()
352            .map(|point| (point.x, point.y, point.flags.to_bits()))
353            .collect::<Vec<_>>();
354        assert_eq!(points, expected);
355    }
356
357    #[test]
358    fn find_vertical_extrema() {
359        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
360        let glyph = font.outline_glyphs().get(GlyphId::new(5)).unwrap();
361        let mut outline = UnscaledOutlineBuf::<32>::new();
362        glyph
363            .draw_unscaled(LocationRef::default(), None, &mut outline)
364            .unwrap();
365        let outline = outline.as_ref();
366        // Find the maximum Y value and its containing contour
367        let mut top_y = None;
368        let (top_contour, top_point_ix) = outline
369            .find_last_contour(|point| {
370                if top_y.is_none() || Some(point.y) > top_y {
371                    top_y = Some(point.y);
372                    true
373                } else {
374                    false
375                }
376            })
377            .unwrap();
378        assert_eq!(top_contour, 14..26);
379        assert_eq!(top_point_ix, 5);
380        assert_eq!(top_y, Some(880));
381        // Find the minimum Y value and its containing contour
382        let mut bottom_y = None;
383        let (bottom_contour, bottom_point_ix) = outline
384            .find_last_contour(|point| {
385                if bottom_y.is_none() || Some(point.y) < bottom_y {
386                    bottom_y = Some(point.y);
387                    true
388                } else {
389                    false
390                }
391            })
392            .unwrap();
393        assert_eq!(bottom_contour, 0..14);
394        assert_eq!(bottom_point_ix, 0);
395        assert_eq!(bottom_y, Some(80));
396    }
397
398    /// When a contour ends with a line or cubic whose end matches the start
399    /// point, omit the last on curve. This matches FreeType behavior when
400    /// constructing a TrueType style outline from a CFF font.
401    #[test]
402    fn omit_unnecessary_trailing_oncurves() {
403        let mut outline = UnscaledOutlineBuf::<64>::new();
404        let mut pen = UnscaledPenAdapter::new(&mut outline);
405        pen.move_to(0.5, 1.5);
406        pen.line_to(1.0, 2.0);
407        // matches start, omit last on curve
408        pen.line_to(0.5, 1.5);
409        pen.close();
410        pen.move_to(5.0, 6.0);
411        pen.curve_to(1.0, 1.0, 2.0, 2.0, 2.0, 3.0);
412        // matches start, omit last on curve
413        pen.curve_to(1.0, 1.0, 2.0, 2.0, 5.0, 6.0);
414        pen.close();
415        pen.move_to(5.0, 6.0);
416        // doesn't match start, keep on curve
417        pen.curve_to(1.0, 1.0, 2.0, 2.0, 2.0, 3.0);
418        pen.close();
419        pen.finish().unwrap();
420        // Collect a vec of bools where true means on curve
421        let on_curves = outline
422            .0
423            .iter()
424            .map(|point| point.flags.is_on_curve())
425            .collect::<Vec<_>>();
426        #[rustfmt::skip]
427        let expected_on_curves = [
428            true,  // move
429            true,  // line
430                   // trailing line omitted
431            true,  // move
432            false, // cubic 1 off curve 1
433            false, // cubic 1 off curve 2
434            true,  // cubic 1 on curve
435            false, // cubic 2 off curve 1
436            false, // cubic 2 off curve 2
437                   // trailing on curve omitted
438            true,  // move
439            false, // cubic 1 off curve 1
440            false, // cubic 1 off curve 2
441            true,  // trailing on curve retained
442        ];
443        assert_eq!(on_curves, expected_on_curves);
444    }
445}