xref: /aosp_15_r20/external/cronet/third_party/rust/chromium_crates_io/vendor/skrifa-0.15.5/src/metrics.rs (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1 //! Global font and glyph specific metrics.
2 //!
3 //! Metrics are various measurements that define positioning and layout
4 //! characteristics for a font. They come in two flavors:
5 //!
6 //! * Global metrics: these are applicable to all glyphs in a font and generally
7 //! define values that are used for the layout of a collection of glyphs. For example,
8 //! the ascent, descent and leading values determine the position of the baseline where
9 //! a glyph should be rendered as well as the suggested spacing above and below it.
10 //!
11 //! * Glyph metrics: these apply to single glyphs. For example, the advance
12 //! width value describes the distance between two consecutive glyphs on a line.
13 //!
14 //! ### Selecting an "instance"
15 //! Both global and glyph specific metrics accept two additional pieces of information
16 //! to select the desired instance of a font:
17 //! * Size: represented by the [Size] type, this determines the scaling factor that is
18 //! applied to all metrics.
19 //! * Normalized variation coordinates: respresented by the [LocationRef] type,
20 //! these define the position in design space for a variable font. For a non-variable
21 //! font, these coordinates are ignored and you can pass [LocationRef::default()]
22 //! as an argument for this parameter.
23 //!
24 
25 use read_fonts::{
26     tables::{
27         glyf::{CompositeGlyphFlags, Glyf, Glyph},
28         gvar::Gvar,
29         hmtx::LongMetric,
30         hvar::Hvar,
31         loca::Loca,
32         os2::SelectionFlags,
33     },
34     types::{BigEndian, Fixed, GlyphId},
35     TableProvider,
36 };
37 
38 use super::instance::{LocationRef, NormalizedCoord, Size};
39 
40 /// Type for a bounding box with single precision floating point coordinates.
41 pub type BoundingBox = read_fonts::types::BoundingBox<f32>;
42 
43 /// Metrics for a text decoration.
44 ///
45 /// This represents the suggested offset and thickness of an underline
46 /// or strikeout text decoration.
47 #[derive(Copy, Clone, PartialEq, Default, Debug)]
48 pub struct Decoration {
49     /// Offset to the top of the decoration from the baseline.
50     pub offset: f32,
51     /// Thickness of the decoration.
52     pub thickness: f32,
53 }
54 
55 /// Metrics that apply to all glyphs in a font.
56 ///
57 /// These are retrieved for a specific position in the design space.
58 ///
59 /// This metrics here are derived from the following tables:
60 /// * [head](https://learn.microsoft.com/en-us/typography/opentype/spec/head): `units_per_em`, `bounds`
61 /// * [maxp](https://learn.microsoft.com/en-us/typography/opentype/spec/maxp): `glyph_count`
62 /// * [post](https://learn.microsoft.com/en-us/typography/opentype/spec/post): `is_monospace`, `italic_angle`, `underline`
63 /// * [OS/2](https://learn.microsoft.com/en-us/typography/opentype/spec/os2): `average_width`, `cap_height`,
64 /// `x_height`, `strikeout`, as well as the line metrics: `ascent`, `descent`, `leading` if the `USE_TYPOGRAPHIC_METRICS`
65 /// flag is set or the `hhea` line metrics are zero (the Windows metrics are used as a last resort).
66 /// * [hhea](https://learn.microsoft.com/en-us/typography/opentype/spec/hhea): `max_width`, as well as the line metrics:
67 /// `ascent`, `descent`, `leading` if they are non-zero and the `USE_TYPOGRAHIC_METRICS` flag is not set in the OS/2 table
68 ///
69 /// For variable fonts, deltas are computed using the  [MVAR](https://learn.microsoft.com/en-us/typography/opentype/spec/MVAR)
70 /// table.
71 #[derive(Copy, Clone, PartialEq, Default, Debug)]
72 pub struct Metrics {
73     /// Number of font design units per em unit.
74     pub units_per_em: u16,
75     /// Number of glyphs in the font.
76     pub glyph_count: u16,
77     /// True if the font is not proportionally spaced.
78     pub is_monospace: bool,
79     /// Italic angle in counter-clockwise degrees from the vertical. Zero for upright text,
80     /// negative for text that leans to the right.
81     pub italic_angle: f32,
82     /// Distance from the baseline to the top of the alignment box.
83     pub ascent: f32,
84     /// Distance from the baseline to the bottom of the alignment box.
85     pub descent: f32,
86     /// Recommended additional spacing between lines.
87     pub leading: f32,
88     /// Distance from the baseline to the top of a typical English capital.
89     pub cap_height: Option<f32>,
90     /// Distance from the baseline to the top of the lowercase "x" or
91     /// similar character.
92     pub x_height: Option<f32>,
93     /// Average width of all non-zero width characters in the font.
94     pub average_width: Option<f32>,
95     /// Maximum advance width of all characters in the font.
96     pub max_width: Option<f32>,
97     /// Metrics for an underline decoration.
98     pub underline: Option<Decoration>,
99     /// Metrics for a strikeout decoration.
100     pub strikeout: Option<Decoration>,
101     /// Union of minimum and maximum extents for all glyphs in the font.
102     pub bounds: Option<BoundingBox>,
103 }
104 
105 impl Metrics {
106     /// Creates new metrics for the given font, size, and location in
107     /// normalized variation space.
new<'a>( font: &impl TableProvider<'a>, size: Size, location: impl Into<LocationRef<'a>>, ) -> Self108     pub fn new<'a>(
109         font: &impl TableProvider<'a>,
110         size: Size,
111         location: impl Into<LocationRef<'a>>,
112     ) -> Self {
113         let head = font.head();
114         let mut metrics = Metrics {
115             units_per_em: head.map(|head| head.units_per_em()).unwrap_or_default(),
116             ..Default::default()
117         };
118         let coords = location.into().coords();
119         let scale = size.linear_scale(metrics.units_per_em);
120         if let Ok(head) = font.head() {
121             metrics.bounds = Some(BoundingBox {
122                 x_min: head.x_min() as f32 * scale,
123                 y_min: head.y_min() as f32 * scale,
124                 x_max: head.x_max() as f32 * scale,
125                 y_max: head.y_max() as f32 * scale,
126             });
127         }
128         if let Ok(maxp) = font.maxp() {
129             metrics.glyph_count = maxp.num_glyphs();
130         }
131         if let Ok(post) = font.post() {
132             metrics.is_monospace = post.is_fixed_pitch() != 0;
133             metrics.italic_angle = post.italic_angle().to_f64() as f32;
134             metrics.underline = Some(Decoration {
135                 offset: post.underline_position().to_i16() as f32 * scale,
136                 thickness: post.underline_thickness().to_i16() as f32 * scale,
137             });
138         }
139         let hhea = font.hhea();
140         if let Ok(hhea) = &hhea {
141             metrics.max_width = Some(hhea.advance_width_max().to_u16() as f32 * scale);
142         }
143         // Choosing proper line metrics is a challenge due to the changing
144         // spec, backward compatibility and broken fonts.
145         //
146         // We use the same strategy as FreeType:
147         // 1. Use the OS/2 metrics if the table exists and the USE_TYPO_METRICS
148         //    flag is set.
149         // 2. Otherwise, use the hhea metrics.
150         // 3. If hhea metrics are zero and the OS/2 table exists:
151         //    3a. Use the typo metrics if they are non-zero
152         //    3b. Otherwise, use the win metrics
153         //
154         // See: https://github.com/freetype/freetype/blob/5c37b6406258ec0d7ab64b8619c5ea2c19e3c69a/src/sfnt/sfobjs.c#L1311
155         let os2 = font.os2().ok();
156         let mut used_typo_metrics = false;
157         if let Some(os2) = &os2 {
158             if os2
159                 .fs_selection()
160                 .contains(SelectionFlags::USE_TYPO_METRICS)
161             {
162                 metrics.ascent = os2.s_typo_ascender() as f32 * scale;
163                 metrics.descent = os2.s_typo_descender() as f32 * scale;
164                 metrics.leading = os2.s_typo_line_gap() as f32 * scale;
165                 used_typo_metrics = true;
166             }
167             metrics.average_width = Some(os2.x_avg_char_width() as f32 * scale);
168             metrics.cap_height = os2.s_cap_height().map(|v| v as f32 * scale);
169             metrics.x_height = os2.sx_height().map(|v| v as f32 * scale);
170             metrics.strikeout = Some(Decoration {
171                 offset: os2.y_strikeout_position() as f32 * scale,
172                 thickness: os2.y_strikeout_size() as f32 * scale,
173             });
174         }
175         if !used_typo_metrics {
176             if let Ok(hhea) = font.hhea() {
177                 metrics.ascent = hhea.ascender().to_i16() as f32 * scale;
178                 metrics.descent = hhea.descender().to_i16() as f32 * scale;
179                 metrics.leading = hhea.line_gap().to_i16() as f32 * scale;
180             }
181             if metrics.ascent == 0.0 && metrics.descent == 0.0 {
182                 if let Some(os2) = &os2 {
183                     if os2.s_typo_ascender() != 0 || os2.s_typo_descender() != 0 {
184                         metrics.ascent = os2.s_typo_ascender() as f32 * scale;
185                         metrics.descent = os2.s_typo_descender() as f32 * scale;
186                         metrics.leading = os2.s_typo_line_gap() as f32 * scale;
187                     } else {
188                         metrics.ascent = os2.us_win_ascent() as f32 * scale;
189                         // Win descent is always positive while other descent values are negative. Negate it
190                         // to ensure we return consistent metrics.
191                         metrics.descent = -(os2.us_win_descent() as f32 * scale);
192                     }
193                 }
194             }
195         }
196         if let (Ok(mvar), true) = (font.mvar(), !coords.is_empty()) {
197             use read_fonts::tables::mvar::tags::*;
198             let metric_delta =
199                 |tag| mvar.metric_delta(tag, coords).unwrap_or_default().to_f64() as f32 * scale;
200             metrics.ascent += metric_delta(HASC);
201             metrics.descent += metric_delta(HDSC);
202             metrics.leading += metric_delta(HLGP);
203             if let Some(cap_height) = &mut metrics.cap_height {
204                 *cap_height += metric_delta(CPHT);
205             }
206             if let Some(x_height) = &mut metrics.x_height {
207                 *x_height += metric_delta(XHGT);
208             }
209             if let Some(underline) = &mut metrics.underline {
210                 underline.offset += metric_delta(UNDO);
211                 underline.thickness += metric_delta(UNDS);
212             }
213             if let Some(strikeout) = &mut metrics.strikeout {
214                 strikeout.offset += metric_delta(STRO);
215                 strikeout.thickness += metric_delta(STRS);
216             }
217         }
218         metrics
219     }
220 }
221 
222 /// Glyph specific metrics.
223 #[derive(Clone)]
224 pub struct GlyphMetrics<'a> {
225     glyph_count: u16,
226     fixed_scale: FixedScaleFactor,
227     h_metrics: &'a [LongMetric],
228     default_advance_width: u16,
229     lsbs: &'a [BigEndian<i16>],
230     hvar: Option<Hvar<'a>>,
231     gvar: Option<Gvar<'a>>,
232     loca_glyf: Option<(Loca<'a>, Glyf<'a>)>,
233     coords: &'a [NormalizedCoord],
234 }
235 
236 impl<'a> GlyphMetrics<'a> {
237     /// Creates new glyph metrics from the given font, size, and location in
238     /// normalized variation space.
new( font: &impl TableProvider<'a>, size: Size, location: impl Into<LocationRef<'a>>, ) -> Self239     pub fn new(
240         font: &impl TableProvider<'a>,
241         size: Size,
242         location: impl Into<LocationRef<'a>>,
243     ) -> Self {
244         let glyph_count = font
245             .maxp()
246             .map(|maxp| maxp.num_glyphs())
247             .unwrap_or_default();
248         let upem = font
249             .head()
250             .map(|head| head.units_per_em())
251             .unwrap_or_default();
252         let fixed_scale = FixedScaleFactor(size.fixed_linear_scale(upem));
253         let coords = location.into().coords();
254         let (h_metrics, default_advance_width, lsbs) = font
255             .hmtx()
256             .map(|hmtx| {
257                 let h_metrics = hmtx.h_metrics();
258                 let default_advance_width = h_metrics.last().map(|m| m.advance.get()).unwrap_or(0);
259                 let lsbs = hmtx.left_side_bearings();
260                 (h_metrics, default_advance_width, lsbs)
261             })
262             .unwrap_or_default();
263         let hvar = font.hvar().ok();
264         let gvar = font.gvar().ok();
265         let loca_glyf = if let (Ok(loca), Ok(glyf)) = (font.loca(None), font.glyf()) {
266             Some((loca, glyf))
267         } else {
268             None
269         };
270         Self {
271             glyph_count,
272             fixed_scale,
273             h_metrics,
274             default_advance_width,
275             lsbs,
276             hvar,
277             gvar,
278             loca_glyf,
279             coords,
280         }
281     }
282 
283     /// Returns the number of available glyphs in the font.
glyph_count(&self) -> u16284     pub fn glyph_count(&self) -> u16 {
285         self.glyph_count
286     }
287 
288     /// Returns the advance width for the specified glyph.
289     ///
290     /// If normalized coordinates were provided when constructing glyph metrics and
291     /// an `HVAR` table is present, applies the appropriate delta.
advance_width(&self, glyph_id: GlyphId) -> Option<f32>292     pub fn advance_width(&self, glyph_id: GlyphId) -> Option<f32> {
293         if glyph_id.to_u16() >= self.glyph_count {
294             return None;
295         }
296         let mut advance = self
297             .h_metrics
298             .get(glyph_id.to_u16() as usize)
299             .map(|metric| metric.advance())
300             .unwrap_or(self.default_advance_width) as i32;
301         if let Some(hvar) = &self.hvar {
302             advance += hvar
303                 .advance_width_delta(glyph_id, self.coords)
304                 // FreeType truncates metric deltas...
305                 // https://github.com/freetype/freetype/blob/7838c78f53f206ac5b8e9cefde548aa81cb00cf4/src/truetype/ttgxvar.c#L1027
306                 .map(|delta| delta.to_f64() as i32)
307                 .unwrap_or(0);
308         } else if self.gvar.is_some() {
309             advance += self.metric_deltas_from_gvar(glyph_id)[1];
310         }
311         Some(self.fixed_scale.apply(advance))
312     }
313 
314     /// Returns the left side bearing for the specified glyph.
315     ///
316     /// If normalized coordinates were provided when constructing glyph metrics and
317     /// an `HVAR` table is present, applies the appropriate delta.
left_side_bearing(&self, glyph_id: GlyphId) -> Option<f32>318     pub fn left_side_bearing(&self, glyph_id: GlyphId) -> Option<f32> {
319         if glyph_id.to_u16() >= self.glyph_count {
320             return None;
321         }
322         let gid_index = glyph_id.to_u16() as usize;
323         let mut lsb = self
324             .h_metrics
325             .get(gid_index)
326             .map(|metric| metric.side_bearing())
327             .unwrap_or_else(|| {
328                 self.lsbs
329                     .get(gid_index.saturating_sub(self.h_metrics.len()))
330                     .map(|lsb| lsb.get())
331                     .unwrap_or_default()
332             }) as i32;
333         if let Some(hvar) = &self.hvar {
334             lsb += hvar
335                 .lsb_delta(glyph_id, self.coords)
336                 // FreeType truncates metric deltas...
337                 // https://github.com/freetype/freetype/blob/7838c78f53f206ac5b8e9cefde548aa81cb00cf4/src/truetype/ttgxvar.c#L1027
338                 .map(|delta| delta.to_f64() as i32)
339                 .unwrap_or(0);
340         } else if self.gvar.is_some() {
341             lsb += self.metric_deltas_from_gvar(glyph_id)[0];
342         }
343         Some(self.fixed_scale.apply(lsb))
344     }
345 
346     /// Returns the bounding box for the specified glyph.
347     ///
348     /// Note that variations are not reflected in the bounding box returned by
349     /// this method.
bounds(&self, glyph_id: GlyphId) -> Option<BoundingBox>350     pub fn bounds(&self, glyph_id: GlyphId) -> Option<BoundingBox> {
351         let (loca, glyf) = self.loca_glyf.as_ref()?;
352         Some(match loca.get_glyf(glyph_id, glyf).ok()? {
353             Some(glyph) => BoundingBox {
354                 x_min: self.fixed_scale.apply(glyph.x_min() as i32),
355                 y_min: self.fixed_scale.apply(glyph.y_min() as i32),
356                 x_max: self.fixed_scale.apply(glyph.x_max() as i32),
357                 y_max: self.fixed_scale.apply(glyph.y_max() as i32),
358             },
359             // Empty glyphs have an empty bounding box
360             None => BoundingBox::default(),
361         })
362     }
363 }
364 
365 impl<'a> GlyphMetrics<'a> {
metric_deltas_from_gvar(&self, glyph_id: GlyphId) -> [i32; 2]366     fn metric_deltas_from_gvar(&self, glyph_id: GlyphId) -> [i32; 2] {
367         GvarMetricDeltas::new(self)
368             .and_then(|metric_deltas| metric_deltas.compute_deltas(glyph_id))
369             .unwrap_or_default()
370     }
371 }
372 
373 #[derive(Copy, Clone)]
374 struct FixedScaleFactor(Fixed);
375 
376 impl FixedScaleFactor {
377     #[inline(always)]
apply(self, value: i32) -> f32378     fn apply(self, value: i32) -> f32 {
379         // Match FreeType metric scaling
380         // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/base/ftadvanc.c#L50>
381         self.0
382             .mul_div(Fixed::from_bits(value), Fixed::from_bits(64))
383             .to_f32()
384     }
385 }
386 
387 struct GvarMetricDeltas<'a> {
388     loca: Loca<'a>,
389     glyf: Glyf<'a>,
390     gvar: Gvar<'a>,
391     coords: &'a [NormalizedCoord],
392 }
393 
394 impl<'a> GvarMetricDeltas<'a> {
new(metrics: &GlyphMetrics<'a>) -> Option<Self>395     fn new(metrics: &GlyphMetrics<'a>) -> Option<Self> {
396         let (loca, glyf) = metrics.loca_glyf.clone()?;
397         let gvar = metrics.gvar.clone()?;
398         Some(Self {
399             loca,
400             glyf,
401             gvar,
402             coords: metrics.coords,
403         })
404     }
405 
406     /// Returns [lsb_delta, advance_delta]
compute_deltas(&self, glyph_id: GlyphId) -> Option<[i32; 2]>407     fn compute_deltas(&self, glyph_id: GlyphId) -> Option<[i32; 2]> {
408         // For any given glyph, there's only one outline that contributes to
409         // metrics deltas (via "phantom points"). For simple glyphs, that is
410         // the glyph itself. For composite glyphs, it is the first component
411         // in the tree that has the USE_MY_METRICS flag set or, if there are
412         // none, the composite glyph itself.
413         //
414         // This searches for the glyph that meets that criteria and also
415         // returns the point count (for composites, this is the component
416         // count), so that we know where the deltas for phantom points start
417         // in the variation data.
418         let (glyph_id, point_count) = self.find_glyph_and_point_count(glyph_id, 0)?;
419         // [left_extent_delta, right_extent_delta]
420         let mut metric_deltas = [Fixed::ZERO; 2];
421         let phantom_range = point_count..point_count + 2;
422         let var_data = self.gvar.glyph_variation_data(glyph_id).ok()?;
423         // Note that phantom points can never belong to a contour so we don't have
424         // to handle the IUP case here.
425         for (tuple, scalar) in var_data.active_tuples_at(self.coords) {
426             for tuple_delta in tuple.deltas() {
427                 let ix = tuple_delta.position as usize;
428                 if phantom_range.contains(&ix) {
429                     metric_deltas[ix - phantom_range.start] += tuple_delta.apply_scalar(scalar).x;
430                 }
431             }
432         }
433         metric_deltas[1] -= metric_deltas[0];
434         Some(metric_deltas.map(|x| x.to_i32()))
435     }
436 
437     /// Returns the glyph id and associated point count that determines the
438     /// metrics for the requested glyph.
find_glyph_and_point_count( &self, glyph_id: GlyphId, recurse_depth: usize, ) -> Option<(GlyphId, usize)>439     fn find_glyph_and_point_count(
440         &self,
441         glyph_id: GlyphId,
442         recurse_depth: usize,
443     ) -> Option<(GlyphId, usize)> {
444         if recurse_depth > crate::GLYF_COMPOSITE_RECURSION_LIMIT {
445             return None;
446         }
447         let glyph = self.loca.get_glyf(glyph_id, &self.glyf).ok()??;
448         match glyph {
449             Glyph::Simple(simple) => {
450                 // Simple glyphs always use their own metrics
451                 Some((glyph_id, simple.num_points()))
452             }
453             Glyph::Composite(composite) => {
454                 // For composite glyphs, if one of the components has the
455                 // USE_MY_METRICS flag set, recurse into the glyph referenced
456                 // by that component. Otherwise, return the composite glyph
457                 // itself and the number of components as the point count.
458                 let mut count = 0;
459                 for component in composite.components() {
460                     count += 1;
461                     if component
462                         .flags
463                         .contains(CompositeGlyphFlags::USE_MY_METRICS)
464                     {
465                         return self.find_glyph_and_point_count(component.glyph, recurse_depth + 1);
466                     }
467                 }
468                 Some((glyph_id, count))
469             }
470         }
471     }
472 }
473 
474 #[cfg(test)]
475 mod tests {
476     use super::*;
477     use crate::MetadataProvider as _;
478     use font_test_data::{SIMPLE_GLYF, VAZIRMATN_VAR};
479     use read_fonts::FontRef;
480 
481     #[test]
metrics()482     fn metrics() {
483         let font = FontRef::new(SIMPLE_GLYF).unwrap();
484         let metrics = font.metrics(Size::unscaled(), LocationRef::default());
485         let expected = Metrics {
486             units_per_em: 1024,
487             glyph_count: 3,
488             bounds: Some(BoundingBox {
489                 x_min: 51.0,
490                 y_min: -250.0,
491                 x_max: 998.0,
492                 y_max: 950.0,
493             }),
494             average_width: Some(1275.0),
495             max_width: None,
496             x_height: Some(512.0),
497             cap_height: Some(717.0),
498             is_monospace: false,
499             italic_angle: 0.0,
500             ascent: 950.0,
501             descent: -250.0,
502             leading: 0.0,
503             underline: None,
504             strikeout: Some(Decoration {
505                 offset: 307.0,
506                 thickness: 51.0,
507             }),
508         };
509         assert_eq!(metrics, expected);
510     }
511 
512     #[test]
metrics_missing_os2()513     fn metrics_missing_os2() {
514         let font = FontRef::new(VAZIRMATN_VAR).unwrap();
515         let metrics = font.metrics(Size::unscaled(), LocationRef::default());
516         let expected = Metrics {
517             units_per_em: 2048,
518             glyph_count: 4,
519             bounds: Some(BoundingBox {
520                 x_min: 29.0,
521                 y_min: 0.0,
522                 x_max: 1310.0,
523                 y_max: 1847.0,
524             }),
525             average_width: None,
526             max_width: Some(1336.0),
527             x_height: None,
528             cap_height: None,
529             is_monospace: false,
530             italic_angle: 0.0,
531             ascent: 2100.0,
532             descent: -1100.0,
533             leading: 0.0,
534             underline: None,
535             strikeout: None,
536         };
537         assert_eq!(metrics, expected);
538     }
539 
540     #[test]
glyph_metrics()541     fn glyph_metrics() {
542         let font = FontRef::new(VAZIRMATN_VAR).unwrap();
543         let glyph_metrics = font.glyph_metrics(Size::unscaled(), LocationRef::default());
544         // (advance_width, lsb) in glyph order
545         let expected = &[
546             (908.0, 100.0),
547             (1336.0, 29.0),
548             (1336.0, 29.0),
549             (633.0, 57.0),
550         ];
551         let result = (0..4)
552             .map(|i| {
553                 let gid = GlyphId::new(i as u16);
554                 let advance_width = glyph_metrics.advance_width(gid).unwrap();
555                 let lsb = glyph_metrics.left_side_bearing(gid).unwrap();
556                 (advance_width, lsb)
557             })
558             .collect::<Vec<_>>();
559         assert_eq!(expected, &result[..]);
560     }
561 
562     /// Asserts that the results generated with Size::unscaled() and
563     /// Size::new(upem) are equal.
564     ///
565     /// See <https://github.com/googlefonts/fontations/issues/590#issuecomment-1711595882>
566     #[test]
glyph_metrics_unscaled_matches_upem_scale()567     fn glyph_metrics_unscaled_matches_upem_scale() {
568         let font = FontRef::new(VAZIRMATN_VAR).unwrap();
569         let upem = font.head().unwrap().units_per_em() as f32;
570         let unscaled_metrics = font.glyph_metrics(Size::unscaled(), LocationRef::default());
571         let upem_metrics = font.glyph_metrics(Size::new(upem), LocationRef::default());
572         for i in 0..unscaled_metrics.glyph_count() {
573             let gid = GlyphId::new(i);
574             assert_eq!(
575                 unscaled_metrics.advance_width(gid),
576                 upem_metrics.advance_width(gid)
577             );
578             assert_eq!(
579                 unscaled_metrics.left_side_bearing(gid),
580                 upem_metrics.left_side_bearing(gid)
581             );
582         }
583     }
584 
585     #[test]
glyph_metrics_var()586     fn glyph_metrics_var() {
587         let font = FontRef::new(VAZIRMATN_VAR).unwrap();
588         let coords = &[NormalizedCoord::from_f32(-0.8)];
589         let glyph_metrics = font.glyph_metrics(Size::unscaled(), LocationRef::new(coords));
590         // (advance_width, lsb) in glyph order
591         let expected = &[
592             (908.0, 100.0),
593             (1246.0, 29.0),
594             (1246.0, 29.0),
595             (556.0, 57.0),
596         ];
597         let result = (0..4)
598             .map(|i| {
599                 let gid = GlyphId::new(i as u16);
600                 let advance_width = glyph_metrics.advance_width(gid).unwrap();
601                 let lsb = glyph_metrics.left_side_bearing(gid).unwrap();
602                 (advance_width, lsb)
603             })
604             .collect::<Vec<_>>();
605         assert_eq!(expected, &result[..]);
606     }
607 
608     #[test]
glyph_metrics_missing_hvar()609     fn glyph_metrics_missing_hvar() {
610         let font = FontRef::new(VAZIRMATN_VAR).unwrap();
611         let glyph_count = font.maxp().unwrap().num_glyphs();
612         // Test a few different locations in variation space
613         for coord in [-1.0, -0.8, 0.0, 0.75, 1.0] {
614             let coords = &[NormalizedCoord::from_f32(coord)];
615             let location = LocationRef::new(coords);
616             let glyph_metrics = font.glyph_metrics(Size::unscaled(), location);
617             let mut glyph_metrics_no_hvar = glyph_metrics.clone();
618             // Setting hvar to None forces use of gvar for metric deltas
619             glyph_metrics_no_hvar.hvar = None;
620             for gid in 0..glyph_count {
621                 let gid = GlyphId::new(gid);
622                 assert_eq!(
623                     glyph_metrics.advance_width(gid),
624                     glyph_metrics_no_hvar.advance_width(gid)
625                 );
626                 assert_eq!(
627                     glyph_metrics.left_side_bearing(gid),
628                     glyph_metrics_no_hvar.left_side_bearing(gid)
629                 );
630             }
631         }
632     }
633 
634     /// Ensure our fixed point scaling code matches FreeType for advances.
635     ///
636     /// <https://github.com/googlefonts/fontations/issues/590>
637     #[test]
match_freetype_glyph_metric_scaling()638     fn match_freetype_glyph_metric_scaling() {
639         // fontations:
640         // gid: 36 advance: 15.33600044250488281250 gid: 68 advance: 13.46399974822998046875 gid: 47 advance: 12.57600021362304687500 gid: 79 advance: 6.19199991226196289062
641         // ft:
642         // gid: 36 advance: 15.33595275878906250000 gid: 68 advance: 13.46395874023437500000 gid: 47 advance: 12.57595825195312500000 gid: 79 advance: 6.19198608398437500000
643         // with font.setSize(24);
644         //
645         // Raw advances for gids 36, 68, 47, and 79 in NotoSans-Regular
646         let font_unit_advances = [639, 561, 524, 258];
647         #[allow(clippy::excessive_precision)]
648         let scaled_advances = [
649             15.33595275878906250000,
650             13.46395874023437500000,
651             12.57595825195312500000,
652             6.19198608398437500000,
653         ];
654         let fixed_scale = FixedScaleFactor(Size::new(24.0).fixed_linear_scale(1000));
655         for (font_unit_advance, expected_scaled_advance) in
656             font_unit_advances.iter().zip(scaled_advances)
657         {
658             let scaled_advance = fixed_scale.apply(*font_unit_advance);
659             assert_eq!(scaled_advance, expected_scaled_advance);
660         }
661     }
662 }
663