Skip to main content

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