Skip to main content

read_fonts/tables/glyf/outline/
context.rs

1//! Where outline loading gets its data.
2
3use crate::{
4    tables::hmtx::GlyphMetricRecords,
5    tables::{
6        glyf::{Glyf, PHANTOM_POINT_COUNT},
7        gvar::{GlyphVariationData, SelectedGvar},
8        hvar::Hvar,
9        loca::{Loca, LocaGlyph},
10        os2::Os2,
11    },
12    types::{F2Dot14, Fixed, GlyphId, Point},
13    ReadError, TableProvider,
14};
15
16/// Everything [`OutlinePlan::load`](super::OutlinePlan::load) needs from a
17/// font: glyphs, variation data, and the metrics behind the phantom points.
18///
19/// A caller that already holds these tables implements this trait and hands
20/// them over. [`OutlineTables`] reads them from a font instead.
21///
22/// `'a` is the lifetime of the font data rather than of the context, so
23/// glyphs outlive any borrow of the implementor.
24pub trait OutlineContext<'a> {
25    /// Returns the glyph with the given identifier, or `None` if it is empty.
26    fn glyph(&self, glyph: GlyphId) -> Option<LocaGlyph<'a>>;
27
28    /// Variation data for a glyph, or `None` if the font does not vary or the
29    /// glyph has none.
30    fn glyph_variation_data(&self, glyph: GlyphId) -> Option<GlyphVariationData<'a>>;
31
32    /// True if the font has a `gvar` table, whatever any one glyph has in it.
33    ///
34    /// Answers for the font rather than per glyph, since it decides whether
35    /// the delta buffers are needed at all.
36    fn has_gvar(&self) -> bool;
37
38    /// Variation deltas for a glyph's four phantom points.
39    ///
40    /// [`SelectedGvar::phantom_point_deltas`] computes these from glyf/GLYF and loca/LOCA,
41    /// including which glyph of a composite tree supplies the metrics.
42    fn phantom_point_deltas(&self, glyph: GlyphId) -> Option<[Point<Fixed>; PHANTOM_POINT_COUNT]>;
43
44    /// Normalized variation coordinates, empty for the default instance.
45    ///
46    /// The context states the location so an implementor that holds one is
47    /// never handed a different one.
48    fn coords(&self) -> &[F2Dot14] {
49        &[]
50    }
51
52    /// Scalars for `gvar`'s shared tuples at [`Self::coords`], as
53    /// `SelectedGvar::compute_scalars` fills them.
54    ///
55    /// A scalar depends on the location and the tuple but never on the glyph,
56    /// so an implementor reading many glyphs at one location computes these
57    /// once. An empty or short slice is always correct; whatever is missing is
58    /// computed per glyph.
59    ///
60    /// These must be the scalars for [`Self::coords`]. Scalars from another
61    /// location produce wrong deltas and cannot be detected.
62    fn gvar_scalars(&self) -> &[Fixed] {
63        &[]
64    }
65
66    /// Design units per em.
67    fn units_per_em(&self) -> u16;
68
69    /// Left side bearing and advance width, in font units.
70    ///
71    /// These must be unvaried, straight out of `hmtx` and never adjusted by
72    /// `HVAR`. The loader applies `gvar` phantom point deltas to whatever this
73    /// returns, so varied metrics would be varied twice.
74    ///
75    /// The phantom deltas arrive in the tuple accumulation the outline needs
76    /// anyway, which makes them cheaper than an `HVAR` lookup per glyph.
77    fn h_metrics(&self, glyph: GlyphId) -> (i32, i32);
78
79    /// Top side bearing and vertical advance for a glyph, in font units.
80    ///
81    /// `None`, the default, means the font states no per glyph vertical
82    /// metrics. They are then derived from [`Self::h_line_metrics`] and the
83    /// glyph's bounding box, as FreeType does for a font without `vmtx`.
84    ///
85    /// Like [`Self::h_metrics`], these must be unvaried. The vertical phantom
86    /// points take their variation from `gvar` deltas.
87    fn v_metrics(&self, glyph: GlyphId) -> Option<(i32, i32)> {
88        let _ = glyph;
89        None
90    }
91
92    /// The ascender and descender of the line horizontal text sits on, in
93    /// font units.
94    ///
95    /// Only read when [`Self::v_metrics`] returns `None`, to derive that
96    /// glyph's vertical metrics.
97    fn h_line_metrics(&self) -> (i32, i32);
98
99    /// True if the font has an `HVAR` table.
100    ///
101    /// Phantom point deltas apply either way, per [`Self::h_metrics`]. This
102    /// only tells [`Scale26Dot6`](super::Scale26Dot6) to round them as
103    /// FreeType rounds `HVAR` metric deltas, so the output matches a FreeType
104    /// build that reads `HVAR`.
105    fn has_hvar(&self) -> bool {
106        false
107    }
108}
109
110/// An [`OutlineContext`] that reads the tables from a font.
111///
112/// [`new`][Self::new] reads at the default instance and [`at`][Self::at]
113/// selects a location. Nothing is cached between glyphs; a caller reading many
114/// glyphs at one location is better served by a context that caches.
115#[derive(Clone)]
116pub struct OutlineTables<'a> {
117    /// Glyph outlines.
118    pub glyf: Glyf<'a>,
119    /// The index into [`Self::glyf`].
120    pub loca: Loca<'a>,
121    /// Glyph variations, absent if the font does not vary.
122    pub gvar: Option<SelectedGvar<'a>>,
123    /// Source of the metrics for the first two phantom points.
124    pub hmtx: Option<GlyphMetricRecords<'a>>,
125    /// Supplies the ascender and descender for the last two.
126    pub os2: Option<Os2<'a>>,
127    /// Only its presence is read: it changes how deltas apply to phantom
128    /// points.
129    pub hvar: Option<Hvar<'a>>,
130    /// Design units per em, from `head`.
131    pub units_per_em: u16,
132    /// Not public, with `gvar_scalars`: the two are only correct together,
133    /// and [`at`][Self::at] is what keeps them so.
134    pub(super) coords: &'a [F2Dot14],
135    pub(super) gvar_scalars: &'a [Fixed],
136}
137
138impl<'a> OutlineTables<'a> {
139    /// Reads the tables from a font.
140    ///
141    /// Fails if `glyf`, `loca` or `head` is missing or malformed. The rest are
142    /// optional: without `gvar` the font is treated as non-variable, and
143    /// without `hmtx` or `OS/2` the corresponding phantom point metrics are
144    /// zero.
145    pub fn new(font: &impl TableProvider<'a>) -> Result<Self, ReadError> {
146        let (glyf, loca) = font.selected_glyf_loca(None)?;
147        Ok(Self {
148            glyf,
149            loca,
150            gvar: font.selected_gvar().ok(),
151            hmtx: font.h_glyph_metric_records().ok(),
152            os2: font.os2().ok(),
153            hvar: font.hvar().ok(),
154            units_per_em: font.head()?.units_per_em(),
155            coords: &[],
156            gvar_scalars: &[],
157        })
158    }
159
160    /// Reads at `coords` instead of the default instance.
161    ///
162    /// `gvar_scalars` are the shared tuple scalars for `coords`, from
163    /// `SelectedGvar::compute_scalars`; pass `&[]` to compute them per glyph
164    /// instead. They are taken together because scalars for another location
165    /// produce wrong deltas and nothing can detect it.
166    pub fn at(self, coords: &'a [F2Dot14], gvar_scalars: &'a [Fixed]) -> Self {
167        Self {
168            coords,
169            gvar_scalars,
170            ..self
171        }
172    }
173}
174
175impl<'a> OutlineContext<'a> for OutlineTables<'a> {
176    fn glyph(&self, glyph: GlyphId) -> Option<LocaGlyph<'a>> {
177        self.loca.get(glyph, &self.glyf)
178    }
179
180    fn glyph_variation_data(&self, glyph: GlyphId) -> Option<GlyphVariationData<'a>> {
181        // Missing or malformed variation data for a glyph is not an error; it
182        // simply does not vary.
183        self.gvar.as_ref()?.glyph_variation_data(glyph).ok()?
184    }
185
186    fn has_gvar(&self) -> bool {
187        self.gvar.is_some()
188    }
189
190    fn phantom_point_deltas(&self, glyph: GlyphId) -> Option<[Point<Fixed>; PHANTOM_POINT_COUNT]> {
191        self.gvar.as_ref()?.phantom_point_deltas_with_scalars(
192            &self.glyf,
193            &self.loca,
194            self.coords,
195            self.gvar_scalars,
196            glyph,
197        )
198    }
199
200    fn coords(&self) -> &[F2Dot14] {
201        self.coords
202    }
203
204    fn gvar_scalars(&self) -> &[Fixed] {
205        self.gvar_scalars
206    }
207
208    fn units_per_em(&self) -> u16 {
209        self.units_per_em
210    }
211
212    fn h_metrics(&self, glyph: GlyphId) -> (i32, i32) {
213        match &self.hmtx {
214            Some(hmtx) => (
215                hmtx.side_bearing(glyph).unwrap_or_default() as i32,
216                hmtx.advance(glyph).unwrap_or_default() as i32,
217            ),
218            None => (0, 0),
219        }
220    }
221
222    // `v_metrics` is left at its default on purpose, so vertical metrics are
223    // derived from the ascender and descender below. Reading `vmtx` and
224    // `VVAR` means two more tables for a type that caches nothing, and the
225    // fallback chain a font missing them still needs is involved enough to
226    // be worth writing once. A context that caches, and pays for both, is
227    // where that belongs.
228
229    fn h_line_metrics(&self) -> (i32, i32) {
230        match &self.os2 {
231            Some(os2) => (os2.s_typo_ascender() as i32, os2.s_typo_descender() as i32),
232            None => (0, 0),
233        }
234    }
235
236    fn has_hvar(&self) -> bool {
237        self.hvar.is_some()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::tables::glyf::outline::{Outline, Scale26Dot6, Unscaled};
245    use crate::{types::F26Dot6, FontRef};
246
247    #[test]
248    fn extended_outline_tables_load_wide_cubic_components() {
249        use super::super::PathContourStart;
250        use crate::model::glyph::outline::SvgSink;
251        for extended_metrics in [false, true] {
252            let data = font_test_data::extended::outlines_font(true, true, extended_metrics, false);
253            let font = FontRef::new(&data).unwrap();
254            let tables = OutlineTables::new(&font).unwrap();
255            let mut outline = Outline::<Unscaled>::new();
256            for gid in [1, 65536] {
257                outline.load(&tables, GlyphId::new(gid)).unwrap();
258                assert_eq!(outline.adjusted_advance_width(), 1000);
259                let mut pen = SvgSink::with_precision(1);
260                assert!(outline.to_path(PathContourStart::ScanBackward, &mut pen));
261                assert_eq!(
262                    pen.as_ref(),
263                    "M0.0,40.0 C0.0,0.0 80.0,0.0 80.0,40.0 C80.0,80.0 0.0,80.0 0.0,40.0 Z"
264                );
265            }
266        }
267    }
268
269    /// A context serving tables it already holds, and metrics from somewhere
270    /// other than `hmtx`, the reason this is a trait.
271    struct BorrowedContext<'a> {
272        glyf: Glyf<'a>,
273        loca: Loca<'a>,
274        advance: i32,
275        vertical: Option<(i32, i32)>,
276    }
277
278    impl<'a> OutlineContext<'a> for BorrowedContext<'a> {
279        fn glyph(&self, glyph: GlyphId) -> Option<LocaGlyph<'a>> {
280            self.loca.get(glyph, &self.glyf)
281        }
282        fn glyph_variation_data(&self, _glyph: GlyphId) -> Option<GlyphVariationData<'a>> {
283            None
284        }
285        fn has_gvar(&self) -> bool {
286            false
287        }
288        fn phantom_point_deltas(
289            &self,
290            _glyph: GlyphId,
291        ) -> Option<[Point<Fixed>; PHANTOM_POINT_COUNT]> {
292            None
293        }
294        fn units_per_em(&self) -> u16 {
295            1000
296        }
297        fn h_metrics(&self, _glyph: GlyphId) -> (i32, i32) {
298            (0, self.advance)
299        }
300        fn v_metrics(&self, _glyph: GlyphId) -> Option<(i32, i32)> {
301            self.vertical
302        }
303        fn h_line_metrics(&self) -> (i32, i32) {
304            (800, -200)
305        }
306    }
307
308    /// An outside implementation drives loading, and its metrics reach the
309    /// phantom points.
310    #[test]
311    fn a_custom_context_can_supply_everything() {
312        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
313        let context = BorrowedContext {
314            glyf: font.glyf().unwrap(),
315            loca: font.loca(None).unwrap(),
316            advance: 1234,
317            vertical: None,
318        };
319        let mut owned = Outline::<Scale26Dot6>::new();
320        let outline = owned.load(&context, GlyphId::new(1), None).unwrap();
321        assert!(!outline.points().is_empty());
322        assert_eq!(
323            outline.adjusted_advance_width(),
324            F26Dot6::from_bits(1234 << 6)
325        );
326    }
327
328    /// The default `glyph` reads through `loca`, so an implementation that
329    /// leaves it alone agrees with one that owns the tables.
330    #[test]
331    fn the_default_glyph_lookup_matches() {
332        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
333        let borrowed = BorrowedContext {
334            glyf: font.glyf().unwrap(),
335            loca: font.loca(None).unwrap(),
336            advance: 0,
337            vertical: None,
338        };
339        let owned = OutlineTables::new(&font).unwrap();
340        for gid in 0..font.maxp().unwrap().num_glyphs() {
341            let gid = GlyphId::from(gid);
342            assert_eq!(
343                borrowed.glyph(gid).unwrap().into_glyph().is_some(),
344                owned.glyph(gid).unwrap().into_glyph().is_some(),
345                "gid {gid}"
346            );
347        }
348    }
349
350    /// Vertical phantom points come from `v_metrics` when it has an answer.
351    #[test]
352    fn per_glyph_vertical_metrics_win() {
353        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
354        let glyph = GlyphId::new(1);
355        let y_max = font
356            .loca(None)
357            .unwrap()
358            .get_glyf(glyph, &font.glyf().unwrap())
359            .unwrap()
360            .unwrap()
361            .y_max() as i32;
362        let vertical = (70, 900);
363        let context = BorrowedContext {
364            glyf: font.glyf().unwrap(),
365            loca: font.loca(None).unwrap(),
366            advance: 0,
367            vertical: Some(vertical),
368        };
369        let mut owned = Outline::<Unscaled>::new();
370        let outline = owned.load(&context, glyph).unwrap();
371        let (tsb, vadvance) = vertical;
372        assert_eq!(outline.phantom_points()[2].y, y_max + tsb);
373        assert_eq!(outline.phantom_points()[3].y, y_max + tsb - vadvance);
374    }
375
376    /// Without them, they are derived from the ascender and descender, which
377    /// is what FreeType does for a font with no `vmtx`.
378    #[test]
379    fn vertical_metrics_fall_back_to_ascent_and_descent() {
380        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
381        let glyph = GlyphId::new(1);
382        let context = BorrowedContext {
383            glyf: font.glyf().unwrap(),
384            loca: font.loca(None).unwrap(),
385            advance: 0,
386            vertical: None,
387        };
388        let (ascent, descent) = context.h_line_metrics();
389        let mut owned = Outline::<Unscaled>::new();
390        let outline = owned.load(&context, glyph).unwrap();
391        // tsb = ascent - y_max, so y_max + tsb is just the ascent.
392        assert_eq!(outline.phantom_points()[2].y, ascent);
393        assert_eq!(outline.phantom_points()[3].y, descent);
394    }
395}