Skip to main content

read_fonts/model/metrics/
global.rs

1//! Font-wide metrics.
2
3use super::metric;
4use crate::model::variation::NormalizedCoord;
5
6use crate::{
7    ps::{cff::CffFontRef, type1::Type1Font},
8    tables::{mvar::tags, os2::SelectionFlags},
9    TableProvider,
10};
11use types::{BoundingBox, F48Dot16};
12
13/// The two ends of a line, in whatever unit a caller uses.
14///
15/// Separate from [`LineBox`] because the gap after a line is not part of
16/// where it begins and ends, and the metrics built on a line use only the
17/// two ends.
18#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
19pub struct LineExtents<T> {
20    /// Height of the line above the baseline. Never negative.
21    pub ascender: T,
22    /// Depth of the line below the baseline, as a negative value. Never
23    /// positive.
24    pub descender: T,
25}
26
27/// Ascender, descender and line gap from one table.
28///
29/// In font units unless a caller names another unit. The ascender is above
30/// the baseline and the descender below it, whatever sign a font stores
31/// them with.
32#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
33pub struct LineBox<T = F48Dot16> {
34    /// Height of the line above the baseline. Never negative.
35    pub ascender: T,
36    /// Depth of the line below the baseline, as a negative value. Never
37    /// positive.
38    pub descender: T,
39    /// Extra space between the bottom of one line and the top of the next.
40    pub line_gap: T,
41}
42
43impl<T: Copy> LineBox<T> {
44    /// Returns where this line begins and ends, without the gap after it.
45    ///
46    /// Text stacks by the two ends; the gap separates one run of stacked
47    /// text from the next.
48    pub fn extents(&self) -> LineExtents<T> {
49        LineExtents {
50            ascender: self.ascender,
51            descender: self.descender,
52        }
53    }
54}
55
56impl LineBox {
57    /// Returns `true` if either end of the line is away from the baseline.
58    ///
59    /// Some fonts leave both ends at zero, and the line is then not used.
60    pub(crate) fn has_nonzero_extent(&self) -> bool {
61        self.ascender != F48Dot16::ZERO || self.descender != F48Dot16::ZERO
62    }
63}
64
65/// Font-wide metrics.
66///
67/// Read from `head`, `maxp`, `hhea`, `vhea` and `OS/2`, which together are a
68/// few hundred bytes.
69///
70/// A font has three sets of line metrics, and they often differ.
71/// [`hhea_line`](Self::hhea_line) is what macOS reads,
72/// [`win_line`](Self::win_line) is what Windows reads, and [`typo_line`](Self::typo_line) is what the
73/// font asks for when [`use_typo_metrics`](Self::use_typo_metrics) is set.
74/// Choosing between them is left to the caller.
75///
76/// A field missing from the font is `None`, which is distinct from a zero
77/// value.
78///
79/// `MVAR` varies most of these, so a set belongs to the location it was read
80/// at. The units per em, the counts, the bounding box, the widest and tallest
81/// advance and the average width cannot vary.
82///
83/// # Units
84///
85/// Measurements are in font units. They are [`F48Dot16`] because a
86/// location can move them off a whole unit, and rounding here would decide
87/// for every caller how that fraction is spent.
88///
89/// The two axes do not scale alike: [`vhea_line`](Self::vhea_line) is
90/// measured across the vertical baseline rather than along it, so it scales
91/// on x where the horizontal sets scale on y.
92#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
93pub struct Metrics {
94    /// Design units per em, which every measurement of the font uses.
95    ///
96    /// Always between 16 and 16384. A font with a value outside that range,
97    /// or with no `head`, uses 1000.
98    pub units_per_em: u16,
99    /// Design units per em as written in `head`, or zero if the font has no
100    /// `head`.
101    ///
102    /// Differs from [`units_per_em`](Self::units_per_em) only for a font with
103    /// an unusable value, which a caller may choose to reject.
104    pub raw_units_per_em: u16,
105    /// Number of glyphs in the font.
106    pub num_glyphs: u32,
107    /// Box enclosing every glyph in the font.
108    pub bounds: BoundingBox<F48Dot16>,
109    /// Line metrics from `hhea`.
110    pub hhea_line: Option<LineBox>,
111    /// Widest advance of any glyph.
112    pub max_h_advance: Option<F48Dot16>,
113    /// Line metrics from `vhea`, measured across the vertical baseline.
114    pub vhea_line: Option<LineBox>,
115    /// Tallest advance of any glyph.
116    pub max_v_advance: Option<F48Dot16>,
117    /// Typographic line metrics, from `OS/2`.
118    pub typo_line: Option<LineBox>,
119    /// Whether the font asks for [`typo_line`](Self::typo_line) to be used
120    /// in preference to [`hhea_line`](Self::hhea_line).
121    pub use_typo_metrics: bool,
122    /// The line outside which the font asks not to be clipped.
123    ///
124    /// `OS/2` gives the descent as a positive number below the baseline.
125    /// This negates it, so the pair describes a line like every other pair
126    /// here.
127    pub win_line: Option<LineExtents<F48Dot16>>,
128    /// Height of a lowercase x.
129    pub x_height: Option<F48Dot16>,
130    /// Height of a capital letter.
131    pub cap_height: Option<F48Dot16>,
132    /// Average glyph width, as the font reports it.
133    pub average_char_width: Option<F48Dot16>,
134}
135
136impl Metrics {
137    /// Reads the metrics from `tables` at `normalized_coords`.
138    ///
139    /// A missing or unreadable table leaves the metrics that come from it
140    /// unset rather than failing.
141    ///
142    /// Pass an empty location for the font's defaults. At any other location
143    /// `MVAR` is read and its deltas applied.
144    pub fn from_sfnt<'a>(
145        tables: &impl TableProvider<'a>,
146        normalized_coords: &[NormalizedCoord],
147    ) -> Self {
148        let coords = normalized_coords;
149        // Only at a location, so that the common case does not reach for a
150        // table it has nothing to ask.
151        let mvar = (!coords.is_empty()).then(|| tables.mvar().ok()).flatten();
152        let deltas = mvar.as_ref().and_then(|mvar| mvar.at(coords));
153        let deltas = deltas.as_ref();
154        let mut metrics = Self::default();
155        let head = tables.head();
156        metrics.raw_units_per_em = head.as_ref().map_or(0, |head| head.units_per_em());
157        metrics.units_per_em = units_per_em(metrics.raw_units_per_em);
158        if let Ok(head) = head {
159            metrics.bounds = BoundingBox {
160                x_min: F48Dot16::from_i32(head.x_min() as i32),
161                y_min: F48Dot16::from_i32(head.y_min() as i32),
162                x_max: F48Dot16::from_i32(head.x_max() as i32),
163                y_max: F48Dot16::from_i32(head.y_max() as i32),
164            };
165        }
166        if let Ok(maxp) = tables.selected_maxp() {
167            metrics.num_glyphs = maxp.num_glyphs();
168        }
169        if let Ok(hhea) = tables.selected_hhea() {
170            // `MVAR` names one horizontal ascender, descender and line gap
171            // between them, so the same delta applies to whichever of these
172            // two sets a caller goes on to believe.
173            metrics.hhea_line = Some(line_box(
174                metric(hhea.ascender().to_i16() as i32, deltas, tags::HASC),
175                metric(hhea.descender().to_i16() as i32, deltas, tags::HDSC),
176                metric(hhea.line_gap().to_i16() as i32, deltas, tags::HLGP),
177            ));
178            metrics.max_h_advance =
179                Some(F48Dot16::from_i32(hhea.advance_width_max().to_u16() as i32));
180        }
181        if let Ok(vhea) = tables.selected_vhea() {
182            metrics.vhea_line = Some(line_box(
183                metric(vhea.ascender().to_i16() as i32, deltas, tags::VASC),
184                metric(vhea.descender().to_i16() as i32, deltas, tags::VDSC),
185                metric(vhea.line_gap().to_i16() as i32, deltas, tags::VLGP),
186            ));
187            metrics.max_v_advance =
188                Some(F48Dot16::from_i32(vhea.advance_height_max().to_u16() as i32));
189        }
190        if let Ok(os2) = tables.os2() {
191            metrics.typo_line = Some(line_box(
192                metric(os2.s_typo_ascender() as i32, deltas, tags::HASC),
193                metric(os2.s_typo_descender() as i32, deltas, tags::HDSC),
194                metric(os2.s_typo_line_gap() as i32, deltas, tags::HLGP),
195            ));
196            metrics.win_line = Some(LineExtents {
197                ascender: metric(os2.us_win_ascent() as i32, deltas, tags::HCLA),
198                descender: -metric(os2.us_win_descent() as i32, deltas, tags::HCLD),
199            });
200            metrics.use_typo_metrics = os2
201                .fs_selection()
202                .contains(SelectionFlags::USE_TYPO_METRICS);
203            metrics.x_height = os2
204                .sx_height()
205                .map(|value| metric(value as i32, deltas, tags::XHGT));
206            metrics.cap_height = os2
207                .s_cap_height()
208                .map(|value| metric(value as i32, deltas, tags::CPHT));
209            metrics.average_char_width = Some(F48Dot16::from_i32(os2.x_avg_char_width() as i32));
210        }
211        metrics
212    }
213
214    /// Returns the ascender, descender and gap for horizontal text.
215    ///
216    /// The line is the first of these that applies:
217    ///
218    /// 1. The typographic line, if `OS/2` sets `USE_TYPO_METRICS`.
219    /// 2. The `hhea` line, if either end is nonzero.
220    /// 3. The typographic line, if either end is nonzero.
221    /// 4. The `OS/2` clipping line, with no gap.
222    ///
223    /// Returns `None` if the font has no line metrics.
224    pub fn h_line(&self) -> Option<LineBox> {
225        match (self.hhea_line, self.typo_line) {
226            (_, Some(typo)) if self.use_typo_metrics => Some(typo),
227            (Some(hhea), _) if hhea.has_nonzero_extent() => Some(hhea),
228            (_, Some(typo)) if typo.has_nonzero_extent() => Some(typo),
229            // The clipping line has no gap between one line and the next, so
230            // there is none to report.
231            (hhea, _) => self
232                .win_line
233                .map(|win| LineBox {
234                    ascender: win.ascender,
235                    descender: win.descender,
236                    line_gap: F48Dot16::ZERO,
237                })
238                .or(hhea),
239        }
240    }
241
242    /// Reads the metrics from a Type 1 font.
243    ///
244    /// Such a font has only a bounding box, a glyph count and a matrix. The
245    /// ascender and descender are the top and bottom of that box. Everything
246    /// else is unset.
247    pub fn from_type1(font: &Type1Font) -> Self {
248        let bbox = font.bbox();
249        let bounds = BoundingBox {
250            x_min: bbox.x_min.to_f48dot16(),
251            y_min: bbox.y_min.to_f48dot16(),
252            x_max: bbox.x_max.to_f48dot16(),
253            y_max: bbox.y_max.to_f48dot16(),
254        };
255        let raw_units_per_em = font.upem().clamp(0, u16::MAX as i32) as u16;
256        Self {
257            units_per_em: units_per_em(raw_units_per_em),
258            raw_units_per_em,
259            num_glyphs: font.num_glyphs(),
260            bounds,
261            hhea_line: Some(LineBox {
262                ascender: bounds.y_max,
263                descender: bounds.y_min,
264                line_gap: F48Dot16::ZERO,
265            }),
266            ..Default::default()
267        }
268    }
269
270    /// Reads the metrics from a standalone CFF font.
271    pub fn from_cff(font: &CffFontRef<'_>) -> Self {
272        let raw_units_per_em = font.upem().clamp(0, u16::MAX as i32) as u16;
273        let mut metrics = Self {
274            units_per_em: units_per_em(raw_units_per_em),
275            raw_units_per_em,
276            num_glyphs: font.num_glyphs(),
277            ..Default::default()
278        };
279        if let Some(metadata) = font.metadata() {
280            let bbox = metadata.bbox();
281            metrics.bounds = BoundingBox {
282                x_min: bbox.x_min.to_f48dot16(),
283                y_min: bbox.y_min.to_f48dot16(),
284                x_max: bbox.x_max.to_f48dot16(),
285                y_max: bbox.y_max.to_f48dot16(),
286            };
287            metrics.hhea_line = Some(LineBox {
288                ascender: metrics.bounds.y_max,
289                descender: metrics.bounds.y_min,
290                line_gap: F48Dot16::ZERO,
291            });
292        }
293        metrics
294    }
295}
296
297/// Returns the design units per em to use for a font whose `head` holds
298/// `stated`.
299///
300/// This is `stated` if it is between 16 and 16384, and 1000 otherwise, which
301/// matches HarfBuzz.
302/// Returns a line with its ascender above the baseline and its descender
303/// below it.
304///
305/// The `hhea`, `vhea` and `OS/2` descenders are negative by the
306/// specification, but some fonts store them positive, which would put the
307/// bottom of the line above the baseline. Either end is read by its size
308/// alone. The gap is kept as stored.
309fn line_box(ascender: F48Dot16, descender: F48Dot16, line_gap: F48Dot16) -> LineBox {
310    LineBox {
311        ascender: ascender.abs(),
312        descender: -descender.abs(),
313        line_gap,
314    }
315}
316
317pub(crate) fn units_per_em(stated: u16) -> u16 {
318    if (16..=16384).contains(&stated) {
319        stated
320    } else {
321        1000
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::FontRef;
329
330    /// Returns Tinos with the sign of its `hhea` descender flipped, as some
331    /// fonts store it.
332    fn positive_descender() -> Vec<u8> {
333        let mut data = font_test_data::TINOS_SUBSET.to_vec();
334        let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
335        let record = font
336            .table_directory()
337            .table_records()
338            .iter()
339            .find(|record| record.tag() == types::Tag::new(b"hhea"))
340            .unwrap();
341        // The descender follows the version and the ascender.
342        let at = record.offset() as usize + 6;
343        let descender = i16::from_be_bytes([data[at], data[at + 1]]);
344        assert!(descender < 0);
345        data[at..at + 2].copy_from_slice(&(-descender).to_be_bytes());
346        data
347    }
348
349    // A descender stored positive is read below the baseline, as is the
350    // vertical advance of a font without `vmtx`, which spans the line.
351    #[test]
352    fn descenders_stored_positive_are_below_the_baseline() {
353        use crate::model::Font;
354        use alloc::vec::Vec;
355        use types::GlyphId;
356        let flipped = positive_descender();
357        let stored = FontRef::new(&flipped).unwrap().hhea().unwrap().descender();
358        assert!(stored.to_i16() > 0);
359        let original =
360            Metrics::from_sfnt(&FontRef::new(font_test_data::TINOS_SUBSET).unwrap(), &[]);
361        let metrics = Metrics::from_sfnt(&FontRef::new(&flipped).unwrap(), &[]);
362        assert_eq!(metrics.hhea_line, original.hhea_line);
363        assert_eq!(metrics.h_line(), original.h_line());
364        assert!(metrics.h_line().unwrap().descender < F48Dot16::ZERO);
365        let [original, flipped] = [font_test_data::TINOS_SUBSET.to_vec(), flipped]
366            .map(|data| Font::new(data, 0).unwrap());
367        assert!(flipped.tables().vmtx().is_err());
368        let advances = |font: &Font| -> Vec<_> {
369            (0..font.num_glyphs())
370                .map(|gid| font.glyph_metrics().v_advance_exact(GlyphId::new(gid)))
371                .collect()
372        };
373        assert_eq!(advances(&flipped), advances(&original));
374    }
375
376    fn line(ascender: i32, descender: i32) -> LineBox {
377        LineBox {
378            ascender: F48Dot16::from_i32(ascender),
379            descender: F48Dot16::from_i32(descender),
380            line_gap: F48Dot16::ZERO,
381        }
382    }
383
384    #[test]
385    fn the_typographic_line_is_read_when_the_font_asks() {
386        let metrics = Metrics {
387            hhea_line: Some(line(800, -200)),
388            typo_line: Some(line(750, -250)),
389            use_typo_metrics: true,
390            ..Default::default()
391        };
392        assert_eq!(metrics.h_line(), Some(line(750, -250)));
393    }
394
395    #[test]
396    fn hhea_is_read_when_it_does_not() {
397        let metrics = Metrics {
398            hhea_line: Some(line(800, -200)),
399            typo_line: Some(line(750, -250)),
400            use_typo_metrics: false,
401            ..Default::default()
402        };
403        assert_eq!(metrics.h_line(), Some(line(800, -200)));
404    }
405
406    #[test]
407    fn a_silent_hhea_gives_way_to_the_typographic_line() {
408        let metrics = Metrics {
409            hhea_line: Some(line(0, 0)),
410            typo_line: Some(line(750, -250)),
411            use_typo_metrics: false,
412            ..Default::default()
413        };
414        assert_eq!(metrics.h_line(), Some(line(750, -250)));
415    }
416
417    #[test]
418    fn a_font_silent_twice_over_falls_back_to_the_clipping_line() {
419        // Arial Narrow Bold is the case: zeroed typographic metrics beside
420        // usable clipping ones, where its siblings state all three.
421        let metrics = Metrics {
422            hhea_line: Some(line(0, 0)),
423            typo_line: Some(line(0, 0)),
424            win_line: Some(LineExtents {
425                ascender: F48Dot16::from_i32(905),
426                descender: F48Dot16::from_i32(-212),
427            }),
428            use_typo_metrics: false,
429            ..Default::default()
430        };
431        // The clipping metrics state a descent below the baseline as a
432        // positive number, so it is negated to describe the same line.
433        assert_eq!(metrics.h_line(), Some(line(905, -212)));
434    }
435
436    #[test]
437    fn a_font_stating_no_line_reports_none() {
438        assert_eq!(Metrics::default().h_line(), None);
439    }
440
441    /// A horizontal font with `OS/2` and `post`, but no `vhea`.
442    fn horizontal() -> FontRef<'static> {
443        FontRef::new(font_test_data::TINOS_SUBSET).unwrap()
444    }
445
446    /// A font set vertically as well, so it has `vhea`.
447    fn vertical() -> FontRef<'static> {
448        FontRef::new(font_test_data::VORG).unwrap()
449    }
450
451    /// A variable font whose `MVAR` varies its line metrics, among others.
452    fn variable() -> FontRef<'static> {
453        FontRef::new(font_test_data::AMSTELVAR_AVAR2_A).unwrap()
454    }
455
456    /// The far end of every one of that font's twelve axes.
457    fn far() -> [NormalizedCoord; 12] {
458        [NormalizedCoord::from_f32(1.0); 12]
459    }
460
461    #[test]
462    fn the_three_sets_of_line_metrics_are_kept_apart() {
463        let metrics = Metrics::from_sfnt(&horizontal(), &[]);
464        assert_eq!(metrics.units_per_em, 2048);
465        let hhea = metrics.hhea_line.unwrap();
466        assert!(hhea.ascender > F48Dot16::ZERO);
467        assert!(hhea.descender < F48Dot16::ZERO);
468        // The typographic set descends the same way `hhea` does.
469        let typo = metrics.typo_line.unwrap();
470        assert!(typo.ascender > F48Dot16::ZERO);
471        assert!(typo.descender < F48Dot16::ZERO);
472        // The Windows pair does not: both are stated as positive extents.
473        let win = metrics.win_line.unwrap();
474        assert!(win.ascender > F48Dot16::ZERO);
475        assert!(win.descender < F48Dot16::ZERO);
476    }
477
478    #[test]
479    fn a_font_without_a_table_leaves_its_measurements_unset() {
480        // A font with no `vhea`, which most horizontal fonts are.
481        let metrics = Metrics::from_sfnt(&horizontal(), &[]);
482        assert!(metrics.vhea_line.is_none());
483        assert!(metrics.max_v_advance.is_none());
484        // And one that has it.
485        let metrics = Metrics::from_sfnt(&vertical(), &[]);
486        assert!(metrics.vhea_line.is_some());
487        assert!(metrics.max_v_advance.unwrap() > F48Dot16::ZERO);
488    }
489
490    #[test]
491    fn a_font_with_nothing_to_read_gives_up_rather_than_failing() {
492        let metrics = Metrics::from_sfnt(&FontRef::new(font_test_data::NAMES_ONLY).unwrap(), &[]);
493        // The font has no `head`, so only the em is set.
494        assert_eq!(
495            metrics,
496            Metrics {
497                units_per_em: 1000,
498                ..Default::default()
499            }
500        );
501    }
502
503    #[test]
504    fn a_font_stating_an_unusable_em_is_measured_on_a_thousand_units() {
505        let data = font_test_data::TINOS_SUBSET;
506        let reference = FontRef::new(data).unwrap();
507        let head = reference
508            .table_directory()
509            .table_records()
510            .iter()
511            .find(|record| record.tag() == types::Tag::new(b"head"))
512            .unwrap()
513            .offset() as usize;
514        assert_eq!(Metrics::from_sfnt(&reference, &[]).units_per_em, 2048);
515        for stated in [0u16, 15, 16385, u16::MAX] {
516            let mut data = data.to_vec();
517            // The em follows the version, revision, checksum, magic and flags.
518            data[head + 18..head + 20].copy_from_slice(&stated.to_be_bytes());
519            let font = FontRef::new(&data).unwrap();
520            let metrics = Metrics::from_sfnt(&font, &[]);
521            assert_eq!(metrics.units_per_em, 1000, "stated {stated}");
522            assert_eq!(metrics.raw_units_per_em, stated, "stated {stated}");
523        }
524        for stated in [16u16, 16384] {
525            assert_eq!(units_per_em(stated), stated);
526        }
527    }
528
529    #[test]
530    fn a_default_location_leaves_every_measurement_on_a_whole_unit() {
531        // Nothing has moved them off one, so each is exactly what its table
532        // states.
533        let metrics = Metrics::from_sfnt(&horizontal(), &[]);
534        let ascender = metrics.hhea_line.unwrap().ascender;
535        assert_eq!(ascender, F48Dot16::from_i32(ascender.to_i32()));
536    }
537
538    #[test]
539    fn a_location_moves_what_mvar_varies() {
540        let font = variable();
541        let default = Metrics::from_sfnt(&font, &[]);
542        let far = Metrics::from_sfnt(&font, &far());
543        assert_ne!(default, far);
544        // This font varies its clipping extents and both of its heights.
545        assert_ne!(default.win_line, far.win_line);
546        assert_ne!(default.x_height, far.x_height);
547        assert_ne!(default.cap_height, far.cap_height);
548    }
549
550    #[test]
551    fn the_two_horizontal_line_sets_move_together() {
552        // `MVAR` names one horizontal ascender between `hhea` and the
553        // typographic set, so a location moves both by the same amount.
554        let font = variable();
555        let default = Metrics::from_sfnt(&font, &[]);
556        let far = Metrics::from_sfnt(&font, &far());
557        let shift = far.hhea_line.unwrap().ascender - default.hhea_line.unwrap().ascender;
558        assert_ne!(shift, F48Dot16::ZERO);
559        assert_eq!(
560            far.typo_line.unwrap().ascender - default.typo_line.unwrap().ascender,
561            shift
562        );
563    }
564
565    #[test]
566    fn a_delta_keeps_the_fraction_the_variation_store_computed() {
567        // The whole reason these are not integers: some of this font's
568        // metrics land off a whole design unit, and rounding them here would
569        // decide for every caller how that fraction is spent.
570        // Part way along the axes, where the region scalars are fractions;
571        // at the far end of every one they are exactly one and the deltas
572        // land on whole units by luck rather than by design.
573        let far = Metrics::from_sfnt(&variable(), &[NormalizedCoord::from_f32(0.4); 12]);
574        let moved = [
575            far.hhea_line.unwrap().ascender,
576            far.hhea_line.unwrap().descender,
577            far.win_line.unwrap().ascender,
578            far.win_line.unwrap().descender,
579            far.x_height.unwrap(),
580            far.cap_height.unwrap(),
581        ];
582        assert!(moved
583            .iter()
584            .any(|value| *value != F48Dot16::from_i32(value.to_i32())));
585    }
586
587    #[test]
588    fn what_mvar_cannot_say_does_not_move() {
589        let font = variable();
590        let default = Metrics::from_sfnt(&font, &[]);
591        let far = Metrics::from_sfnt(&font, &far());
592        assert_eq!(default.units_per_em, far.units_per_em);
593        assert_eq!(default.num_glyphs, far.num_glyphs);
594        assert_eq!(default.bounds, far.bounds);
595        assert_eq!(default.max_h_advance, far.max_h_advance);
596        assert_eq!(default.average_char_width, far.average_char_width);
597    }
598
599    #[test]
600    fn a_type1_font_states_what_little_it_can() {
601        let font =
602            crate::ps::type1::Type1Font::new(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFA)
603                .unwrap();
604        let metrics = Metrics::from_type1(&font);
605        assert_eq!(metrics.units_per_em, 1000);
606        assert!(metrics.num_glyphs > 0);
607        assert!(metrics.bounds.y_max > F48Dot16::ZERO);
608        // No table states these, so the bounding box stands in for them.
609        assert_eq!(metrics.hhea_line.unwrap().ascender, metrics.bounds.y_max);
610        assert_eq!(metrics.hhea_line.unwrap().descender, metrics.bounds.y_min);
611        // And what such a font cannot express stays unset.
612        assert!(metrics.vhea_line.is_none());
613        assert!(metrics.typo_line.is_none());
614        assert!(metrics.win_line.is_none());
615        assert!(metrics.cap_height.is_none());
616    }
617
618    #[test]
619    fn a_static_font_reads_the_same_at_every_location() {
620        // Coordinates a font has no way to use change nothing about it.
621        let font = horizontal();
622        assert_eq!(
623            Metrics::from_sfnt(&font, &[]),
624            Metrics::from_sfnt(&font, &far())
625        );
626    }
627}