1use 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#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
19pub struct LineExtents<T> {
20 pub ascender: T,
22 pub descender: T,
25}
26
27#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
33pub struct LineBox<T = F48Dot16> {
34 pub ascender: T,
36 pub descender: T,
39 pub line_gap: T,
41}
42
43impl<T: Copy> LineBox<T> {
44 pub fn extents(&self) -> LineExtents<T> {
49 LineExtents {
50 ascender: self.ascender,
51 descender: self.descender,
52 }
53 }
54}
55
56impl LineBox {
57 pub(crate) fn has_nonzero_extent(&self) -> bool {
61 self.ascender != F48Dot16::ZERO || self.descender != F48Dot16::ZERO
62 }
63}
64
65#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
93pub struct Metrics {
94 pub units_per_em: u16,
99 pub raw_units_per_em: u16,
105 pub num_glyphs: u32,
107 pub bounds: BoundingBox<F48Dot16>,
109 pub hhea_line: Option<LineBox>,
111 pub max_h_advance: Option<F48Dot16>,
113 pub vhea_line: Option<LineBox>,
115 pub max_v_advance: Option<F48Dot16>,
117 pub typo_line: Option<LineBox>,
119 pub use_typo_metrics: bool,
122 pub win_line: Option<LineExtents<F48Dot16>>,
128 pub x_height: Option<F48Dot16>,
130 pub cap_height: Option<F48Dot16>,
132 pub average_char_width: Option<F48Dot16>,
134}
135
136impl Metrics {
137 pub fn from_sfnt<'a>(
145 tables: &impl TableProvider<'a>,
146 normalized_coords: &[NormalizedCoord],
147 ) -> Self {
148 let coords = normalized_coords;
149 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 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 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 (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 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 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
297fn 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 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 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 #[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 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 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 fn horizontal() -> FontRef<'static> {
443 FontRef::new(font_test_data::TINOS_SUBSET).unwrap()
444 }
445
446 fn vertical() -> FontRef<'static> {
448 FontRef::new(font_test_data::VORG).unwrap()
449 }
450
451 fn variable() -> FontRef<'static> {
453 FontRef::new(font_test_data::AMSTELVAR_AVAR2_A).unwrap()
454 }
455
456 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 let typo = metrics.typo_line.unwrap();
470 assert!(typo.ascender > F48Dot16::ZERO);
471 assert!(typo.descender < F48Dot16::ZERO);
472 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 let metrics = Metrics::from_sfnt(&horizontal(), &[]);
482 assert!(metrics.vhea_line.is_none());
483 assert!(metrics.max_v_advance.is_none());
484 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 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 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 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 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 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 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 assert_eq!(metrics.hhea_line.unwrap().ascender, metrics.bounds.y_max);
610 assert_eq!(metrics.hhea_line.unwrap().descender, metrics.bounds.y_min);
611 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 let font = horizontal();
622 assert_eq!(
623 Metrics::from_sfnt(&font, &[]),
624 Metrics::from_sfnt(&font, &far())
625 );
626 }
627}