Skip to main content

read_fonts/tables/glyf/outline/
owned.rs

1//! An outline that owns the buffers it is loaded into.
2
3use super::{
4    outline_to_path, Buffers, Hinter, OutlineContext, OutlinePlan, OutlineRef, PathContourStart,
5    Scale, Scale26Dot6, ScaleF32, Unscaled,
6};
7use crate::model::Error;
8use crate::{
9    model::glyph::outline::PathSink,
10    tables::glyf::{PointFlags, PHANTOM_POINT_COUNT},
11    types::{F26Dot6, GlyphId, Point},
12};
13use alloc::vec::Vec;
14
15/// An outline and the buffers that hold it.
16///
17/// [`OutlinePlan::load`] requires the caller to size and supply seven
18/// buffers. This owns them, grows them as needed and reuses them across
19/// glyphs, so loading stops allocating after the first few.
20///
21/// Loading returns an [`OutlineRef`] borrowing those buffers. The same data is
22/// reachable through [`points`](Self::points) and its neighbours once that
23/// borrow ends.
24///
25/// ```
26/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
27/// use read_fonts::{
28///     tables::glyf::outline::{Outline, OutlineTables, Scale26Dot6},
29///     types::GlyphId,
30///     FontRef,
31/// };
32///
33/// let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE)?;
34/// let tables = OutlineTables::new(&font)?;
35/// let mut outline = Outline::<Scale26Dot6>::new();
36///
37/// // Borrows `outline`'s buffers for as long as it is held.
38/// let loaded = outline.load(&tables, GlyphId::new(1), Some(16.0))?;
39/// for (point, flags) in loaded.points().iter().zip(loaded.flags()) {
40///     let _ = (point, flags.is_off_curve_quad());
41/// }
42///
43/// // Once it is dropped, the outline can be read through its accessors.
44/// assert_eq!(outline.points().len(), outline.flags().len());
45/// # Ok(())
46/// # }
47/// ```
48pub struct Outline<S: Scale> {
49    points: Vec<Point<S::Coord>>,
50    flags: Vec<PointFlags>,
51    contours: Vec<u16>,
52    phantom: [Point<S::Coord>; PHANTOM_POINT_COUNT],
53    unscaled: Vec<Point<i32>>,
54    deltas: Vec<Point<S::Delta>>,
55    iup: Vec<Point<S::Delta>>,
56    composite_deltas: Vec<Point<S::Delta>>,
57    point_count: u32,
58    contour_count: u32,
59}
60
61impl<S: Scale> Outline<S> {
62    /// Creates an empty outline. The buffers are allocated on first use.
63    pub fn new() -> Self {
64        Self {
65            points: Vec::new(),
66            flags: Vec::new(),
67            contours: Vec::new(),
68            phantom: Default::default(),
69            unscaled: Vec::new(),
70            deltas: Vec::new(),
71            iup: Vec::new(),
72            composite_deltas: Vec::new(),
73            point_count: 0,
74            contour_count: 0,
75        }
76    }
77
78    /// The loaded points, excluding phantom points.
79    pub fn points(&self) -> &[Point<S::Coord>] {
80        &self.points[..self.point_count as usize]
81    }
82
83    /// The flag for each point in [`Self::points`].
84    pub fn flags(&self) -> &[PointFlags] {
85        &self.flags[..self.point_count as usize]
86    }
87
88    /// The index of the last point of each contour.
89    pub fn contours(&self) -> &[u16] {
90        &self.contours[..self.contour_count as usize]
91    }
92
93    /// The four phantom points, after scaling, variation and hinting.
94    pub fn phantom_points(&self) -> &[Point<S::Coord>; PHANTOM_POINT_COUNT] {
95        &self.phantom
96    }
97
98    /// The left side bearing after scaling, variation and hinting.
99    pub fn adjusted_lsb(&self) -> S::Coord {
100        self.phantom[0].x
101    }
102
103    /// The advance width after scaling, variation and hinting.
104    pub fn adjusted_advance_width(&self) -> S::Coord {
105        self.phantom[1].x - self.phantom[0].x
106    }
107
108    /// Converts the outline to path commands, calling `sink` for each.
109    ///
110    /// See [`OutlineRef::to_path`] for what `start` decides and when this
111    /// returns `false`.
112    #[must_use]
113    pub fn to_path(&self, start: PathContourStart, sink: &mut impl PathSink) -> bool {
114        outline_to_path(self.points(), self.flags(), self.contours(), start, sink)
115    }
116
117    /// Loads `glyph` at `scale`, replacing any previous contents.
118    ///
119    /// The per-scale `load` methods build the scale. Use this to supply a
120    /// hinter, or to drive a scale generically.
121    pub fn load_with<'a>(
122        &mut self,
123        context: &'a dyn OutlineContext<'a>,
124        glyph: GlyphId,
125        scale: &S,
126        hinter: Option<&mut dyn Hinter<S::Coord>>,
127    ) -> Result<OutlineRef<'_, S::Coord>, Error> {
128        let plan = OutlinePlan::new(context, glyph)?;
129        let lengths = plan.buffer_lengths::<S>();
130        grow(&mut self.points, lengths.points);
131        grow(&mut self.flags, lengths.points);
132        grow(&mut self.contours, lengths.contours);
133        grow(&mut self.unscaled, lengths.unscaled);
134        grow(&mut self.deltas, lengths.deltas);
135        grow(&mut self.iup, lengths.iup);
136        grow(&mut self.composite_deltas, lengths.composite_deltas);
137        let buffers = Buffers {
138            points: &mut self.points,
139            flags: &mut self.flags,
140            contours: &mut self.contours,
141            unscaled: &mut self.unscaled,
142            deltas: &mut self.deltas,
143            iup: &mut self.iup,
144            composite_deltas: &mut self.composite_deltas,
145        };
146        let outline = plan.load(context, scale, buffers, hinter)?;
147        // Kept so the accessors work once the returned borrow is released.
148        self.point_count = outline.points().len() as u32;
149        self.contour_count = outline.contours().len() as u32;
150        self.phantom = *outline.phantom_points();
151        Ok(outline)
152    }
153}
154
155impl<S: Scale> Default for Outline<S> {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161impl Outline<Scale26Dot6> {
162    /// Loads `glyph` at `ppem`, in 26.6 pixel coordinates.
163    ///
164    /// A `ppem` of `None` leaves the result in font units, shifted left by
165    /// six. The location comes from `context`.
166    pub fn load<'a>(
167        &mut self,
168        context: &'a dyn OutlineContext<'a>,
169        glyph: GlyphId,
170        ppem: Option<f32>,
171    ) -> Result<OutlineRef<'_, F26Dot6>, Error> {
172        let scale = Scale26Dot6::new(ppem, context.units_per_em());
173        self.load_with(context, glyph, &scale, None)
174    }
175
176    /// Loads `glyph` at `ppem`, running the glyph's TrueType
177    /// instructions through `hinter`.
178    pub fn load_hinted<'a>(
179        &mut self,
180        context: &'a dyn OutlineContext<'a>,
181        glyph: GlyphId,
182        ppem: Option<f32>,
183        hinter: &mut dyn Hinter<F26Dot6>,
184    ) -> Result<OutlineRef<'_, F26Dot6>, Error> {
185        let scale = Scale26Dot6::new(ppem, context.units_per_em());
186        self.load_with(context, glyph, &scale, Some(hinter))
187    }
188}
189
190impl Outline<ScaleF32> {
191    /// Loads `glyph` at `ppem`, in floating point pixel coordinates.
192    ///
193    /// A `ppem` of `None` leaves the result in font units. The location comes
194    /// from `context`.
195    pub fn load<'a>(
196        &mut self,
197        context: &'a dyn OutlineContext<'a>,
198        glyph: GlyphId,
199        ppem: Option<f32>,
200    ) -> Result<OutlineRef<'_, f32>, Error> {
201        let scale = ScaleF32::new(ppem, context.units_per_em());
202        self.load_with(context, glyph, &scale, None)
203    }
204}
205
206impl Outline<Unscaled> {
207    /// Loads `glyph` in font units.
208    ///
209    /// There is no size to pass: variations apply and components are placed,
210    /// but nothing is scaled. The location comes from `context`.
211    pub fn load<'a>(
212        &mut self,
213        context: &'a dyn OutlineContext<'a>,
214        glyph: GlyphId,
215    ) -> Result<OutlineRef<'_, i32>, Error> {
216        self.load_with(context, glyph, &Unscaled, None)
217    }
218}
219
220/// Grows `buf` to at least `len`, without ever shrinking it.
221fn grow<T: Copy + Default>(buf: &mut Vec<T>, len: u32) {
222    let len = len as usize;
223    if buf.len() < len {
224        buf.resize(len, T::default());
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::super::OutlineTables;
231    use super::*;
232    use crate::{types::F2Dot14, FontRef, TableProvider};
233    use alloc::{vec, vec::Vec};
234
235    /// The owning API agrees with driving [`OutlinePlan::load`] by hand.
236    #[test]
237    fn matches_the_low_level_api() {
238        let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
239        let context = OutlineTables::new(&font).unwrap();
240        let scale = Scale26Dot6::new(Some(16.0), context.units_per_em);
241        let mut owned = Outline::<Scale26Dot6>::new();
242        for gid in 0..font.maxp().unwrap().num_glyphs() {
243            let gid = GlyphId::from(gid);
244            let plan = OutlinePlan::new(&context, gid).unwrap();
245            let lengths = plan.buffer_lengths::<Scale26Dot6>();
246            let mut points = vec![Point::default(); lengths.points as usize];
247            let mut flags = vec![PointFlags::default(); lengths.points as usize];
248            let mut contours = vec![0u16; lengths.contours as usize];
249            let mut unscaled = vec![Point::default(); lengths.unscaled as usize];
250            let buffers = Buffers {
251                points: &mut points,
252                flags: &mut flags,
253                contours: &mut contours,
254                unscaled: &mut unscaled,
255                deltas: &mut [],
256                iup: &mut [],
257                composite_deltas: &mut [],
258            };
259            let expected = plan.load(&context, &scale, buffers, None).unwrap();
260            let expected = (
261                expected.points().to_vec(),
262                expected.contours().to_vec(),
263                expected.phantom_points(),
264            );
265            let actual = owned.load(&context, gid, Some(16.0)).unwrap();
266            assert_eq!(actual.points(), expected.0, "gid {gid}");
267            assert_eq!(actual.contours(), expected.1, "gid {gid}");
268            assert_eq!(actual.phantom_points(), expected.2, "gid {gid}");
269        }
270    }
271
272    /// The accessors see the same outline as the returned borrow.
273    #[test]
274    fn accessors_match_the_returned_outline() {
275        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
276        let context = OutlineTables::new(&font).unwrap();
277        let mut owned = Outline::<Scale26Dot6>::new();
278        let (points, contours, phantom, lsb, advance) = {
279            let out = owned.load(&context, GlyphId::new(5), None).unwrap();
280            (
281                out.points().to_vec(),
282                out.contours().to_vec(),
283                *out.phantom_points(),
284                out.adjusted_lsb(),
285                out.adjusted_advance_width(),
286            )
287        };
288        assert!(!points.is_empty());
289        assert_eq!(owned.points(), points);
290        assert_eq!(owned.contours(), contours);
291        assert_eq!(owned.phantom_points(), &phantom);
292        assert_eq!(owned.adjusted_lsb(), lsb);
293        assert_eq!(owned.adjusted_advance_width(), advance);
294    }
295
296    /// Buffers grow to fit the largest glyph and are then reused; a smaller
297    /// glyph after a larger one still reports only its own points.
298    #[test]
299    fn buffers_grow_and_are_reused() {
300        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
301        let context = OutlineTables::new(&font).unwrap();
302        let mut owned = Outline::<Scale26Dot6>::new();
303        // Glyph 5 is a composite of two copies of glyph 1, so it is larger.
304        owned.load(&context, GlyphId::new(5), None).unwrap();
305        let big = owned.points().len();
306        let capacity = owned.points.len();
307        owned.load(&context, GlyphId::new(1), None).unwrap();
308        assert!(owned.points().len() < big);
309        assert_eq!(owned.points.len(), capacity, "buffers must not shrink");
310        assert_eq!(owned.points().len(), owned.flags().len());
311    }
312
313    /// Nothing carries over from one glyph to the next.
314    #[test]
315    fn reuse_does_not_leak_between_glyphs() {
316        let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
317        let context = OutlineTables::new(&font).unwrap();
318        let mut owned = Outline::<Scale26Dot6>::new();
319        let first = owned
320            .load(&context, GlyphId::new(1), Some(24.0))
321            .unwrap()
322            .points
323            .to_vec();
324        // Run a different glyph through the same buffers, then come back.
325        owned.load(&context, GlyphId::new(3), Some(24.0)).unwrap();
326        let again = owned.load(&context, GlyphId::new(1), Some(24.0)).unwrap();
327        assert_eq!(again.points(), first);
328    }
329
330    /// Font units are what 26.6 produces at no size, shifted right by six.
331    #[test]
332    fn unscaled_agrees_with_26dot6_at_no_size() {
333        let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
334        let context = OutlineTables::new(&font).unwrap();
335        let mut fixed = Outline::<Scale26Dot6>::new();
336        let mut raw = Outline::<Unscaled>::new();
337        for gid in 0..font.maxp().unwrap().num_glyphs() {
338            let gid = GlyphId::from(gid);
339            let expected: Vec<_> = fixed
340                .load(&context, gid, None)
341                .unwrap()
342                .points
343                .iter()
344                .map(|point| point.map(|coord| coord.to_bits() >> 6))
345                .collect();
346            let actual = raw.load(&context, gid).unwrap();
347            assert_eq!(actual.points(), expected, "gid {gid}");
348        }
349    }
350
351    /// The f32 scale tracks the 26.6 scale to within a 26.6 unit.
352    #[test]
353    fn f32_tracks_26dot6() {
354        let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
355        let context = OutlineTables::new(&font).unwrap();
356        let mut fixed = Outline::<Scale26Dot6>::new();
357        let mut float = Outline::<ScaleF32>::new();
358        for gid in 0..font.maxp().unwrap().num_glyphs() {
359            let gid = GlyphId::from(gid);
360            let expected = fixed
361                .load(&context, gid, Some(32.0))
362                .unwrap()
363                .points
364                .to_vec();
365            let actual = float.load(&context, gid, Some(32.0)).unwrap();
366            for (expected, actual) in expected.iter().zip(actual.points()) {
367                assert!(
368                    (expected.x.to_f32() - actual.x).abs() < 1.0 / 64.0,
369                    "gid {gid}"
370                );
371                assert!(
372                    (expected.y.to_f32() - actual.y).abs() < 1.0 / 64.0,
373                    "gid {gid}"
374                );
375            }
376        }
377    }
378
379    /// Variation coordinates reach the loader and move the outline.
380    #[test]
381    fn coords_vary_the_outline() {
382        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
383        let context = OutlineTables::new(&font).unwrap();
384        assert!(context.gvar.is_some());
385        let mut owned = Outline::<Scale26Dot6>::new();
386        let default = owned
387            .load(&context, GlyphId::new(2), Some(16.0))
388            .unwrap()
389            .points
390            .to_vec();
391        let coords = [F2Dot14::from_f32(1.0)];
392        let context = context.at(&coords, &[]);
393        let varied = owned.load(&context, GlyphId::new(2), Some(16.0)).unwrap();
394        assert_eq!(varied.points().len(), default.len());
395        assert_ne!(varied.points(), default);
396    }
397
398    /// `OutlineTables` supplies the metrics, so phantom points carry the
399    /// glyph's real advance.
400    #[test]
401    fn tables_supply_phantom_metrics() {
402        let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
403        let context = OutlineTables::new(&font).unwrap();
404        let hmtx = font.hmtx().unwrap();
405        let mut owned = Outline::<Unscaled>::new();
406        for gid in 0..font.maxp().unwrap().num_glyphs() {
407            let gid = GlyphId::from(gid);
408            let out = owned.load(&context, gid).unwrap();
409            assert_eq!(
410                out.adjusted_advance_width(),
411                hmtx.advance(gid).unwrap() as i32,
412                "gid {gid}"
413            );
414        }
415    }
416}