Skip to main content

read_fonts/tables/glyf/outline/
plan.rs

1//! The survey that sizes an outline before it is built.
2
3use super::{load, BufferLengths, Buffers, Hinter, OutlineContext, OutlineRef, Scale};
4use crate::model::Error;
5use crate::{
6    limits::{MAX_COMPOSITE_EDGES, MAX_OUTLINE_POINTS, MAX_RECURSION_DEPTH},
7    tables::glyf::{CompositeGlyphFlags, Glyph, PHANTOM_POINT_COUNT},
8    types::GlyphId,
9};
10
11/// What an outline is made of, and how much scratch space building it needs.
12///
13/// Produced by walking the composite tree without reading point data, which
14/// is cheap enough to do per draw. [`Self::buffer_lengths`] sizes the buffers
15/// and [`Self::load`] fills them.
16#[derive(Clone, Default)]
17pub struct OutlinePlan<'a> {
18    /// The glyph this describes.
19    pub glyph: GlyphId,
20    /// The root glyph's `glyf` data, or `None` for an empty glyph such as a
21    /// space.
22    ///
23    /// A missing or unreadable glyph is an error from [`OutlinePlan::new`],
24    /// not `None` here.
25    pub glyph_data: Option<Glyph<'a>>,
26    /// Total points in the outline, summed over every simple glyph in the
27    /// tree, plus the phantom points.
28    pub num_points: u32,
29    /// Total contours, summed over every simple glyph in the tree.
30    pub num_contours: u32,
31    /// Largest point count, including phantom points, of any one simple glyph
32    /// in the tree.
33    pub max_simple_points: u32,
34    /// Largest point count, including phantom points, contributed by any one
35    /// composite glyph in the tree that carries instructions.
36    ///
37    /// Zero when no composite is instructed. Only a hinted composite keeps
38    /// its loaded points in font units, so this is tracked separately from
39    /// [`Self::max_simple_points`].
40    pub max_hinted_composite_points: u32,
41    /// Deepest the per-component delta stack gets while loading nested
42    /// composites.
43    pub max_component_delta_stack: u32,
44    /// True if any glyph in the tree carries TrueType instructions.
45    pub has_hinting: bool,
46    /// True if any glyph in the tree is flagged as having overlapping contours
47    /// or components.
48    pub has_overlaps: bool,
49    /// True when `gvar` deltas will be applied: the font has a `gvar` table
50    /// and the context names a location in it.
51    ///
52    /// A variable font read at its default instance needs no delta buffers.
53    /// This is the one field taken from the context rather than the walk.
54    pub applies_variations: bool,
55}
56
57impl core::fmt::Debug for OutlinePlan<'_> {
58    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59        // `Glyph` has no `Debug`, and printing a whole glyph here would not be
60        // useful anyway; note whether one is present instead.
61        f.debug_struct("OutlinePlan")
62            .field("glyph", &self.glyph)
63            .field("is_empty", &self.glyph_data.is_none())
64            .field("num_points", &self.num_points)
65            .field("num_contours", &self.num_contours)
66            .field("max_simple_points", &self.max_simple_points)
67            .field(
68                "max_hinted_composite_points",
69                &self.max_hinted_composite_points,
70            )
71            .field("max_component_delta_stack", &self.max_component_delta_stack)
72            .field("has_hinting", &self.has_hinting)
73            .field("has_overlaps", &self.has_overlaps)
74            .field("applies_variations", &self.applies_variations)
75            .finish()
76    }
77}
78
79impl<'a> OutlinePlan<'a> {
80    /// Walks the glyph, and the tree below a composite, collecting what
81    /// building its outline requires.
82    pub fn new(context: &dyn OutlineContext<'a>, glyph: GlyphId) -> Result<Self, Error> {
83        let mut plan = Self {
84            glyph,
85            applies_variations: context.has_gvar() && !context.coords().is_empty(),
86            ..Default::default()
87        };
88        let glyph_data = context
89            .glyph(glyph)
90            .ok_or(Error::MissingResource)?
91            .into_glyph();
92        if let Some(data) = glyph_data.as_ref() {
93            let mut components = 0;
94            plan.walk(context, data, 0, 0, &mut components)?;
95        }
96        // The phantom points belong to the outline as a whole, not to any one
97        // glyph in the tree.
98        plan.num_points += PHANTOM_POINT_COUNT as u32;
99        plan.glyph_data = glyph_data;
100        Ok(plan)
101    }
102
103    /// Returns how long each scratch buffer has to be to load at `S`.
104    pub fn buffer_lengths<S: Scale>(&self) -> BufferLengths {
105        let applies_variations = self.applies_variations;
106        BufferLengths {
107            points: self.num_points,
108            contours: self.num_contours,
109            unscaled: if S::NEEDS_UNSCALED {
110                self.max_simple_points.max(self.max_hinted_composite_points)
111            } else {
112                0
113            },
114            deltas: if applies_variations {
115                self.max_simple_points
116            } else {
117                0
118            },
119            iup: if applies_variations {
120                self.max_simple_points
121            } else {
122                0
123            },
124            composite_deltas: if applies_variations {
125                self.max_component_delta_stack
126            } else {
127                0
128            },
129        }
130    }
131
132    /// Builds the outline this plan describes.
133    ///
134    /// The buffers must be at least as long as [`buffer_lengths`] says. The
135    /// location comes from `context`. The result borrows `buffers` and
136    /// nothing else.
137    ///
138    /// [`buffer_lengths`]: OutlinePlan::buffer_lengths
139    ///
140    /// ```
141    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
142    /// use read_fonts::{
143    ///     tables::glyf::outline::{Buffers, OutlinePlan, OutlineTables, Scale26Dot6},
144    ///     types::{GlyphId, Point},
145    ///     FontRef,
146    /// };
147    ///
148    /// let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE)?;
149    /// let tables = OutlineTables::new(&font)?;
150    /// let plan = OutlinePlan::new(&tables, GlyphId::new(1))?;
151    ///
152    /// // A static draw, and 26.6 keeps a font unit copy of the points.
153    /// let lengths = plan.buffer_lengths::<Scale26Dot6>();
154    /// let mut points = vec![Point::default(); lengths.points as usize];
155    /// let mut flags = vec![Default::default(); lengths.points as usize];
156    /// let mut contours = vec![0; lengths.contours as usize];
157    /// let mut unscaled = vec![Point::default(); lengths.unscaled as usize];
158    /// let buffers = Buffers {
159    ///     points: &mut points,
160    ///     flags: &mut flags,
161    ///     contours: &mut contours,
162    ///     unscaled: &mut unscaled,
163    ///     // Only needed when varying.
164    ///     deltas: &mut [],
165    ///     iup: &mut [],
166    ///     composite_deltas: &mut [],
167    /// };
168    ///
169    /// let scale = Scale26Dot6::new(Some(16.0), tables.units_per_em);
170    /// let outline = plan.load(&tables, &scale, buffers, None)?;
171    /// assert_eq!(outline.points().len(), outline.flags().len());
172    /// # Ok(())
173    /// # }
174    /// ```
175    pub fn load<'s, 'buf, S: Scale>(
176        &'s self,
177        context: &'a dyn OutlineContext<'a>,
178        scale: &'s S,
179        buffers: Buffers<'buf, S>,
180        hinter: Option<&mut dyn Hinter<S::Coord>>,
181    ) -> Result<OutlineRef<'buf, S::Coord>, Error> {
182        load::load(self, context, scale, buffers, hinter)
183    }
184
185    fn walk(
186        &mut self,
187        context: &dyn OutlineContext<'a>,
188        glyph: &Glyph<'a>,
189        component_depth: u32,
190        recurse_depth: usize,
191        components: &mut usize,
192    ) -> Result<(), Error> {
193        if recurse_depth > MAX_RECURSION_DEPTH {
194            return Err(Error::LimitExceeded);
195        }
196        match glyph {
197            Glyph::Simple(simple) => {
198                let point_count = simple.num_points() as u32;
199                let with_phantom = point_count + PHANTOM_POINT_COUNT as u32;
200                self.max_simple_points = self.max_simple_points.max(with_phantom);
201                self.num_points += point_count;
202                if self.num_points as usize > MAX_OUTLINE_POINTS {
203                    return Err(Error::LimitExceeded);
204                }
205                self.num_contours += simple.end_pts_of_contours().len() as u32;
206                self.has_hinting |= simple.instruction_length() != 0;
207                self.has_overlaps |= simple.has_overlapping_contours();
208            }
209            Glyph::Composite(composite) => {
210                let (component_count, instructions) = composite.count_and_instructions();
211                // Each level of the tree contributes its own components plus a
212                // set of phantom points to the delta stack.
213                let level_size = component_count as u32 + PHANTOM_POINT_COUNT as u32;
214                let point_base = self.num_points;
215                for (component, flags) in composite.component_glyphs_and_flags() {
216                    self.has_overlaps |= flags.contains(CompositeGlyphFlags::OVERLAP_COMPOUND);
217                    let Some(component_glyph) = context
218                        .glyph(component)
219                        .ok_or(Error::MissingResource)?
220                        .into_glyph()
221                    else {
222                        continue;
223                    };
224                    *components += 1;
225                    if *components > MAX_COMPOSITE_EDGES {
226                        return Err(Error::LimitExceeded);
227                    }
228                    self.walk(
229                        context,
230                        &component_glyph,
231                        component_depth + level_size,
232                        recurse_depth + 1,
233                        components,
234                    )?;
235                }
236                if !instructions.unwrap_or_default().is_empty() {
237                    // A hinted composite needs its whole loaded point set
238                    // kept in font units.
239                    let loaded = self.num_points - point_base + PHANTOM_POINT_COUNT as u32;
240                    self.max_hinted_composite_points = self.max_hinted_composite_points.max(loaded);
241                    self.has_hinting = true;
242                }
243                self.max_component_delta_stack = self
244                    .max_component_delta_stack
245                    .max(component_depth + level_size);
246            }
247        }
248        Ok(())
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::super::{OutlineTables, Scale26Dot6, ScaleF32};
255    use super::*;
256    use crate::{
257        limits::{MAX_COMPOSITE_EDGES, MAX_RECURSION_DEPTH},
258        tables::{glyf::Glyf, loca::Loca},
259        FontData, FontRead,
260    };
261    use crate::{FontRef, TableProvider};
262    use alloc::{vec, vec::Vec};
263
264    /// `glyf_components.ttf` has, in order: a two contour simple glyph, a one
265    /// contour simple glyph, four composites referencing the latter, one
266    /// composite referencing it twice, and two composites referencing another
267    /// composite, which is the only nesting in the file.
268    fn components_font() -> FontRef<'static> {
269        FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap()
270    }
271
272    fn info_for(font: &FontRef<'static>, gid: u32) -> OutlinePlan<'static> {
273        let context = OutlineTables::new(font).unwrap();
274        OutlinePlan::new(&context, GlyphId::new(gid)).unwrap()
275    }
276
277    #[test]
278    fn empty_glyph_has_only_phantom_points() {
279        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
280        let context = OutlineTables::new(&font).unwrap();
281        // Glyph 0 in this font has no outline.
282        let plan = OutlinePlan::new(&context, GlyphId::new(0)).unwrap();
283        assert!(plan.glyph_data.is_none());
284        assert_eq!(plan.num_points, PHANTOM_POINT_COUNT as u32);
285        assert_eq!(plan.num_contours, 0);
286        assert_eq!(plan.max_simple_points, 0);
287        assert_eq!(plan.max_component_delta_stack, 0);
288        assert!(!plan.has_hinting);
289    }
290
291    #[test]
292    fn simple_glyph() {
293        let font = components_font();
294        // Glyph 1: one contour, four points.
295        let plan = info_for(&font, 1);
296        assert_eq!(plan.num_points, 4 + PHANTOM_POINT_COUNT as u32);
297        assert_eq!(plan.num_contours, 1);
298        assert_eq!(plan.max_simple_points, 4 + PHANTOM_POINT_COUNT as u32);
299        assert_eq!(plan.max_hinted_composite_points, 0);
300        assert_eq!(plan.max_component_delta_stack, 0);
301        assert!(!plan.has_hinting);
302    }
303
304    #[test]
305    fn simple_glyph_with_two_contours() {
306        let font = components_font();
307        // Glyph 0: two contours, eight points.
308        let plan = info_for(&font, 0);
309        assert_eq!(plan.num_points, 8 + PHANTOM_POINT_COUNT as u32);
310        assert_eq!(plan.num_contours, 2);
311        assert_eq!(plan.max_simple_points, 8 + PHANTOM_POINT_COUNT as u32);
312    }
313
314    #[test]
315    fn composite_with_one_component() {
316        let font = components_font();
317        // Glyph 2 references glyph 1, so it inherits its points and contours.
318        let plan = info_for(&font, 2);
319        assert_eq!(plan.num_points, 4 + PHANTOM_POINT_COUNT as u32);
320        assert_eq!(plan.num_contours, 1);
321        assert_eq!(plan.max_simple_points, 4 + PHANTOM_POINT_COUNT as u32);
322        // One component plus the phantom points.
323        assert_eq!(
324            plan.max_component_delta_stack,
325            1 + PHANTOM_POINT_COUNT as u32
326        );
327        // Nothing in this font is instructed.
328        assert_eq!(plan.max_hinted_composite_points, 0);
329        assert!(!plan.has_hinting);
330    }
331
332    #[test]
333    fn composite_with_two_components() {
334        let font = components_font();
335        // Glyph 5 references glyph 1 twice.
336        let plan = info_for(&font, 5);
337        assert_eq!(plan.num_points, 8 + PHANTOM_POINT_COUNT as u32);
338        assert_eq!(plan.num_contours, 2);
339        // The largest single simple glyph is still glyph 1.
340        assert_eq!(plan.max_simple_points, 4 + PHANTOM_POINT_COUNT as u32);
341        assert_eq!(
342            plan.max_component_delta_stack,
343            2 + PHANTOM_POINT_COUNT as u32
344        );
345    }
346
347    /// Nesting accumulates on the delta stack: each level contributes its own
348    /// component count plus a set of phantom points.
349    #[test]
350    fn nested_composite_deepens_the_delta_stack() {
351        let font = components_font();
352        // Glyph 7 references glyph 4, which references glyph 1.
353        let plan = info_for(&font, 7);
354        assert_eq!(plan.num_points, 4 + PHANTOM_POINT_COUNT as u32);
355        assert_eq!(plan.num_contours, 1);
356        // Two levels, each one component plus phantom points.
357        assert_eq!(
358            plan.max_component_delta_stack,
359            2 * (1 + PHANTOM_POINT_COUNT as u32)
360        );
361    }
362
363    #[test]
364    fn buffer_lengths_follow_the_walk() {
365        let font = components_font();
366        let plan = info_for(&font, 5);
367
368        // Static, at a scale that does not keep font units.
369        let plain = plan.buffer_lengths::<ScaleF32>();
370        assert_eq!(plain.points, plan.num_points);
371        assert_eq!(plain.contours, plan.num_contours);
372        assert_eq!(plain.unscaled, 0);
373        assert_eq!(plain.deltas, 0);
374        assert_eq!(plain.iup, 0);
375        assert_eq!(plain.composite_deltas, 0);
376
377        // A scale that keeps them sizes one more buffer, and nothing else.
378        let unscaled = plan.buffer_lengths::<Scale26Dot6>();
379        assert_eq!(unscaled.unscaled, plan.max_simple_points);
380        assert_eq!(unscaled.deltas, 0);
381
382        // Variations size the three delta buffers. The plan settled that
383        // against its context, so this is the same walk at a location.
384        let mut plan = plan;
385        plan.applies_variations = true;
386        let varied = plan.buffer_lengths::<ScaleF32>();
387        assert_eq!(varied.unscaled, 0);
388        assert_eq!(varied.deltas, plan.max_simple_points);
389        assert_eq!(varied.iup, plan.max_simple_points);
390        assert_eq!(varied.composite_deltas, plan.max_component_delta_stack);
391    }
392
393    /// The unscaled buffer has to hold whichever is larger: the biggest simple
394    /// glyph, or the loaded points of an instructed composite.
395    #[test]
396    fn buffer_lengths_unscaled_takes_the_larger() {
397        let mut plan = OutlinePlan {
398            max_simple_points: 10,
399            max_hinted_composite_points: 40,
400            ..Default::default()
401        };
402        assert_eq!(plan.buffer_lengths::<Scale26Dot6>().unscaled, 40);
403        plan.max_hinted_composite_points = 4;
404        assert_eq!(plan.buffer_lengths::<Scale26Dot6>().unscaled, 10);
405    }
406
407    #[test]
408    fn detects_hinting() {
409        let font = FontRef::new(font_test_data::COUSINE_HINT_SUBSET).unwrap();
410        let context = OutlineTables::new(&font).unwrap();
411        let n = font.maxp().unwrap().num_glyphs();
412        let hinted = (0..n)
413            .filter(|gid| {
414                OutlinePlan::new(&context, GlyphId::from(*gid))
415                    .map(|plan| plan.has_hinting)
416                    .unwrap_or(false)
417            })
418            .count();
419        assert!(hinted > 0, "expected some instructed glyphs");
420    }
421
422    /// Builds a `glyf`/`loca` pair from raw glyph records.
423    fn tables_from_glyphs(glyphs: &[Vec<u8>]) -> (Vec<u8>, Vec<u8>) {
424        let mut glyf = Vec::new();
425        let mut loca: Vec<u8> = Vec::new();
426        for glyph in glyphs {
427            loca.extend_from_slice(&(glyf.len() as u32).to_be_bytes());
428            glyf.extend_from_slice(glyph);
429        }
430        loca.extend_from_slice(&(glyf.len() as u32).to_be_bytes());
431        (glyf, loca)
432    }
433
434    /// A composite glyph record referencing each of `components` in turn.
435    fn composite(components: &[u16]) -> Vec<u8> {
436        const ARGS_ARE_WORDS: u16 = 0x0001;
437        const MORE_COMPONENTS: u16 = 0x0020;
438        let mut out = Vec::new();
439        out.extend_from_slice(&(-1i16).to_be_bytes()); // numberOfContours
440        out.extend_from_slice(&[0u8; 8]); // bounding box
441        for (i, &gid) in components.iter().enumerate() {
442            let mut flags = ARGS_ARE_WORDS;
443            if i + 1 < components.len() {
444                flags |= MORE_COMPONENTS;
445            }
446            out.extend_from_slice(&flags.to_be_bytes());
447            out.extend_from_slice(&gid.to_be_bytes());
448            out.extend_from_slice(&0i16.to_be_bytes()); // arg1
449            out.extend_from_slice(&0i16.to_be_bytes()); // arg2
450        }
451        out
452    }
453
454    /// An empty simple glyph record: zero contours, so it contributes nothing.
455    fn empty_simple() -> Vec<u8> {
456        let mut out = Vec::new();
457        out.extend_from_slice(&0i16.to_be_bytes()); // numberOfContours
458        out.extend_from_slice(&[0u8; 8]); // bounding box
459        out.extend_from_slice(&0u16.to_be_bytes()); // instructionLength
460        out
461    }
462
463    fn info_from_raw(glyf_bytes: &[u8], loca_bytes: &[u8], gid: u32) -> Result<(), Error> {
464        let glyf = Glyf::read(FontData::new(glyf_bytes)).unwrap();
465        let loca = Loca::read(FontData::new(loca_bytes), true).unwrap();
466        let context = OutlineTables {
467            glyf,
468            loca,
469            gvar: None,
470            hmtx: None,
471            os2: None,
472            hvar: None,
473            units_per_em: 1000,
474            coords: &[],
475            gvar_scalars: &[],
476        };
477        OutlinePlan::new(&context, GlyphId::new(gid)).map(|_| ())
478    }
479
480    /// A composite that references itself recurses forever without the depth
481    /// limit.
482    #[test]
483    fn self_referencing_composite_hits_the_recursion_limit() {
484        let (glyf, loca) = tables_from_glyphs(&[composite(&[0])]);
485        assert_eq!(info_from_raw(&glyf, &loca, 0), Err(Error::LimitExceeded));
486    }
487
488    /// A chain shorter than the limit is fine, and one past it is not.
489    #[test]
490    fn recursion_limit_boundary() {
491        // A chain of composites, each referencing the next, ending in a simple
492        // glyph. Depth is the number of composites.
493        let chain = |depth: usize| -> (Vec<u8>, Vec<u8>) {
494            let mut glyphs: Vec<Vec<u8>> =
495                (0..depth).map(|i| composite(&[(i + 1) as u16])).collect();
496            glyphs.push(empty_simple());
497            tables_from_glyphs(&glyphs)
498        };
499        let (glyf, loca) = chain(MAX_RECURSION_DEPTH);
500        assert!(info_from_raw(&glyf, &loca, 0).is_ok());
501        let (glyf, loca) = chain(MAX_RECURSION_DEPTH + 2);
502        assert_eq!(info_from_raw(&glyf, &loca, 0), Err(Error::LimitExceeded));
503    }
504
505    /// Fanning out wide stays shallow, so only the component limit catches it.
506    #[test]
507    fn wide_composite_hits_the_component_limit() {
508        let within: Vec<u16> = vec![1; MAX_COMPOSITE_EDGES];
509        let (glyf, loca) = tables_from_glyphs(&[composite(&within), empty_simple()]);
510        assert!(info_from_raw(&glyf, &loca, 0).is_ok());
511
512        let beyond: Vec<u16> = vec![1; MAX_COMPOSITE_EDGES + 1];
513        let (glyf, loca) = tables_from_glyphs(&[composite(&beyond), empty_simple()]);
514        assert_eq!(info_from_raw(&glyf, &loca, 0), Err(Error::LimitExceeded));
515    }
516
517    /// A component pointing at an empty glyph is skipped rather than counted
518    /// or treated as an error.
519    #[test]
520    fn empty_components_are_skipped() {
521        // Glyph 1 has a zero length loca entry, so it is empty.
522        let mut glyf = Vec::new();
523        let mut loca: Vec<u8> = Vec::new();
524        let root = composite(&[1]);
525        loca.extend_from_slice(&0u32.to_be_bytes());
526        glyf.extend_from_slice(&root);
527        // Both remaining offsets point at the end, giving glyph 1 no data.
528        loca.extend_from_slice(&(glyf.len() as u32).to_be_bytes());
529        loca.extend_from_slice(&(glyf.len() as u32).to_be_bytes());
530        assert!(info_from_raw(&glyf, &loca, 0).is_ok());
531    }
532}