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