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}