use super::metric;
use crate::model::variation::NormalizedCoord;
use crate::{
ps::{cff::CffFontRef, type1::Type1Font},
tables::{mvar::tags, os2::SelectionFlags},
TableProvider,
};
use types::{BoundingBox, F48Dot16};
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub struct LineExtents<T> {
pub ascender: T,
pub descender: T,
}
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub struct LineBox<T = F48Dot16> {
pub ascender: T,
pub descender: T,
pub line_gap: T,
}
impl<T: Copy> LineBox<T> {
pub fn extents(&self) -> LineExtents<T> {
LineExtents {
ascender: self.ascender,
descender: self.descender,
}
}
}
impl LineBox {
pub(crate) fn has_nonzero_extent(&self) -> bool {
self.ascender != F48Dot16::ZERO || self.descender != F48Dot16::ZERO
}
}
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub struct Metrics {
pub units_per_em: u16,
pub raw_units_per_em: u16,
pub num_glyphs: u32,
pub bounds: BoundingBox<F48Dot16>,
pub hhea_line: Option<LineBox>,
pub max_h_advance: Option<F48Dot16>,
pub vhea_line: Option<LineBox>,
pub max_v_advance: Option<F48Dot16>,
pub typo_line: Option<LineBox>,
pub use_typo_metrics: bool,
pub win_line: Option<LineExtents<F48Dot16>>,
pub x_height: Option<F48Dot16>,
pub cap_height: Option<F48Dot16>,
pub average_char_width: Option<F48Dot16>,
}
impl Metrics {
pub fn from_sfnt<'a>(
tables: &impl TableProvider<'a>,
normalized_coords: &[NormalizedCoord],
) -> Self {
let coords = normalized_coords;
let mvar = (!coords.is_empty()).then(|| tables.mvar().ok()).flatten();
let deltas = mvar.as_ref().and_then(|mvar| mvar.at(coords));
let deltas = deltas.as_ref();
let mut metrics = Self::default();
let head = tables.head();
metrics.raw_units_per_em = head.as_ref().map_or(0, |head| head.units_per_em());
metrics.units_per_em = units_per_em(metrics.raw_units_per_em);
if let Ok(head) = head {
metrics.bounds = BoundingBox {
x_min: F48Dot16::from_i32(head.x_min() as i32),
y_min: F48Dot16::from_i32(head.y_min() as i32),
x_max: F48Dot16::from_i32(head.x_max() as i32),
y_max: F48Dot16::from_i32(head.y_max() as i32),
};
}
if let Ok(maxp) = tables.selected_maxp() {
metrics.num_glyphs = maxp.num_glyphs();
}
if let Ok(hhea) = tables.selected_hhea() {
metrics.hhea_line = Some(line_box(
metric(hhea.ascender().to_i16() as i32, deltas, tags::HASC),
metric(hhea.descender().to_i16() as i32, deltas, tags::HDSC),
metric(hhea.line_gap().to_i16() as i32, deltas, tags::HLGP),
));
metrics.max_h_advance =
Some(F48Dot16::from_i32(hhea.advance_width_max().to_u16() as i32));
}
if let Ok(vhea) = tables.selected_vhea() {
metrics.vhea_line = Some(line_box(
metric(vhea.ascender().to_i16() as i32, deltas, tags::VASC),
metric(vhea.descender().to_i16() as i32, deltas, tags::VDSC),
metric(vhea.line_gap().to_i16() as i32, deltas, tags::VLGP),
));
metrics.max_v_advance =
Some(F48Dot16::from_i32(vhea.advance_height_max().to_u16() as i32));
}
if let Ok(os2) = tables.os2() {
metrics.typo_line = Some(line_box(
metric(os2.s_typo_ascender() as i32, deltas, tags::HASC),
metric(os2.s_typo_descender() as i32, deltas, tags::HDSC),
metric(os2.s_typo_line_gap() as i32, deltas, tags::HLGP),
));
metrics.win_line = Some(LineExtents {
ascender: metric(os2.us_win_ascent() as i32, deltas, tags::HCLA),
descender: -metric(os2.us_win_descent() as i32, deltas, tags::HCLD),
});
metrics.use_typo_metrics = os2
.fs_selection()
.contains(SelectionFlags::USE_TYPO_METRICS);
metrics.x_height = os2
.sx_height()
.map(|value| metric(value as i32, deltas, tags::XHGT));
metrics.cap_height = os2
.s_cap_height()
.map(|value| metric(value as i32, deltas, tags::CPHT));
metrics.average_char_width = Some(F48Dot16::from_i32(os2.x_avg_char_width() as i32));
}
metrics
}
pub fn h_line(&self) -> Option<LineBox> {
match (self.hhea_line, self.typo_line) {
(_, Some(typo)) if self.use_typo_metrics => Some(typo),
(Some(hhea), _) if hhea.has_nonzero_extent() => Some(hhea),
(_, Some(typo)) if typo.has_nonzero_extent() => Some(typo),
(hhea, _) => self
.win_line
.map(|win| LineBox {
ascender: win.ascender,
descender: win.descender,
line_gap: F48Dot16::ZERO,
})
.or(hhea),
}
}
pub fn from_type1(font: &Type1Font) -> Self {
let bbox = font.bbox();
let bounds = BoundingBox {
x_min: bbox.x_min.to_f48dot16(),
y_min: bbox.y_min.to_f48dot16(),
x_max: bbox.x_max.to_f48dot16(),
y_max: bbox.y_max.to_f48dot16(),
};
let raw_units_per_em = font.upem().clamp(0, u16::MAX as i32) as u16;
Self {
units_per_em: units_per_em(raw_units_per_em),
raw_units_per_em,
num_glyphs: font.num_glyphs(),
bounds,
hhea_line: Some(LineBox {
ascender: bounds.y_max,
descender: bounds.y_min,
line_gap: F48Dot16::ZERO,
}),
..Default::default()
}
}
pub fn from_cff(font: &CffFontRef<'_>) -> Self {
let raw_units_per_em = font.upem().clamp(0, u16::MAX as i32) as u16;
let mut metrics = Self {
units_per_em: units_per_em(raw_units_per_em),
raw_units_per_em,
num_glyphs: font.num_glyphs(),
..Default::default()
};
if let Some(metadata) = font.metadata() {
let bbox = metadata.bbox();
metrics.bounds = BoundingBox {
x_min: bbox.x_min.to_f48dot16(),
y_min: bbox.y_min.to_f48dot16(),
x_max: bbox.x_max.to_f48dot16(),
y_max: bbox.y_max.to_f48dot16(),
};
metrics.hhea_line = Some(LineBox {
ascender: metrics.bounds.y_max,
descender: metrics.bounds.y_min,
line_gap: F48Dot16::ZERO,
});
}
metrics
}
}
fn line_box(ascender: F48Dot16, descender: F48Dot16, line_gap: F48Dot16) -> LineBox {
LineBox {
ascender: ascender.abs(),
descender: -descender.abs(),
line_gap,
}
}
pub(crate) fn units_per_em(stated: u16) -> u16 {
if (16..=16384).contains(&stated) {
stated
} else {
1000
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FontRef;
fn positive_descender() -> Vec<u8> {
let mut data = font_test_data::TINOS_SUBSET.to_vec();
let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
let record = font
.table_directory()
.table_records()
.iter()
.find(|record| record.tag() == types::Tag::new(b"hhea"))
.unwrap();
let at = record.offset() as usize + 6;
let descender = i16::from_be_bytes([data[at], data[at + 1]]);
assert!(descender < 0);
data[at..at + 2].copy_from_slice(&(-descender).to_be_bytes());
data
}
#[test]
fn descenders_stored_positive_are_below_the_baseline() {
use crate::model::Font;
use alloc::vec::Vec;
use types::GlyphId;
let flipped = positive_descender();
let stored = FontRef::new(&flipped).unwrap().hhea().unwrap().descender();
assert!(stored.to_i16() > 0);
let original =
Metrics::from_sfnt(&FontRef::new(font_test_data::TINOS_SUBSET).unwrap(), &[]);
let metrics = Metrics::from_sfnt(&FontRef::new(&flipped).unwrap(), &[]);
assert_eq!(metrics.hhea_line, original.hhea_line);
assert_eq!(metrics.h_line(), original.h_line());
assert!(metrics.h_line().unwrap().descender < F48Dot16::ZERO);
let [original, flipped] = [font_test_data::TINOS_SUBSET.to_vec(), flipped]
.map(|data| Font::new(data, 0).unwrap());
assert!(flipped.tables().vmtx().is_err());
let advances = |font: &Font| -> Vec<_> {
(0..font.num_glyphs())
.map(|gid| font.glyph_metrics().v_advance_exact(GlyphId::new(gid)))
.collect()
};
assert_eq!(advances(&flipped), advances(&original));
}
fn line(ascender: i32, descender: i32) -> LineBox {
LineBox {
ascender: F48Dot16::from_i32(ascender),
descender: F48Dot16::from_i32(descender),
line_gap: F48Dot16::ZERO,
}
}
#[test]
fn the_typographic_line_is_read_when_the_font_asks() {
let metrics = Metrics {
hhea_line: Some(line(800, -200)),
typo_line: Some(line(750, -250)),
use_typo_metrics: true,
..Default::default()
};
assert_eq!(metrics.h_line(), Some(line(750, -250)));
}
#[test]
fn hhea_is_read_when_it_does_not() {
let metrics = Metrics {
hhea_line: Some(line(800, -200)),
typo_line: Some(line(750, -250)),
use_typo_metrics: false,
..Default::default()
};
assert_eq!(metrics.h_line(), Some(line(800, -200)));
}
#[test]
fn a_silent_hhea_gives_way_to_the_typographic_line() {
let metrics = Metrics {
hhea_line: Some(line(0, 0)),
typo_line: Some(line(750, -250)),
use_typo_metrics: false,
..Default::default()
};
assert_eq!(metrics.h_line(), Some(line(750, -250)));
}
#[test]
fn a_font_silent_twice_over_falls_back_to_the_clipping_line() {
let metrics = Metrics {
hhea_line: Some(line(0, 0)),
typo_line: Some(line(0, 0)),
win_line: Some(LineExtents {
ascender: F48Dot16::from_i32(905),
descender: F48Dot16::from_i32(-212),
}),
use_typo_metrics: false,
..Default::default()
};
assert_eq!(metrics.h_line(), Some(line(905, -212)));
}
#[test]
fn a_font_stating_no_line_reports_none() {
assert_eq!(Metrics::default().h_line(), None);
}
fn horizontal() -> FontRef<'static> {
FontRef::new(font_test_data::TINOS_SUBSET).unwrap()
}
fn vertical() -> FontRef<'static> {
FontRef::new(font_test_data::VORG).unwrap()
}
fn variable() -> FontRef<'static> {
FontRef::new(font_test_data::AMSTELVAR_AVAR2_A).unwrap()
}
fn far() -> [NormalizedCoord; 12] {
[NormalizedCoord::from_f32(1.0); 12]
}
#[test]
fn the_three_sets_of_line_metrics_are_kept_apart() {
let metrics = Metrics::from_sfnt(&horizontal(), &[]);
assert_eq!(metrics.units_per_em, 2048);
let hhea = metrics.hhea_line.unwrap();
assert!(hhea.ascender > F48Dot16::ZERO);
assert!(hhea.descender < F48Dot16::ZERO);
let typo = metrics.typo_line.unwrap();
assert!(typo.ascender > F48Dot16::ZERO);
assert!(typo.descender < F48Dot16::ZERO);
let win = metrics.win_line.unwrap();
assert!(win.ascender > F48Dot16::ZERO);
assert!(win.descender < F48Dot16::ZERO);
}
#[test]
fn a_font_without_a_table_leaves_its_measurements_unset() {
let metrics = Metrics::from_sfnt(&horizontal(), &[]);
assert!(metrics.vhea_line.is_none());
assert!(metrics.max_v_advance.is_none());
let metrics = Metrics::from_sfnt(&vertical(), &[]);
assert!(metrics.vhea_line.is_some());
assert!(metrics.max_v_advance.unwrap() > F48Dot16::ZERO);
}
#[test]
fn a_font_with_nothing_to_read_gives_up_rather_than_failing() {
let metrics = Metrics::from_sfnt(&FontRef::new(font_test_data::NAMES_ONLY).unwrap(), &[]);
assert_eq!(
metrics,
Metrics {
units_per_em: 1000,
..Default::default()
}
);
}
#[test]
fn a_font_stating_an_unusable_em_is_measured_on_a_thousand_units() {
let data = font_test_data::TINOS_SUBSET;
let reference = FontRef::new(data).unwrap();
let head = reference
.table_directory()
.table_records()
.iter()
.find(|record| record.tag() == types::Tag::new(b"head"))
.unwrap()
.offset() as usize;
assert_eq!(Metrics::from_sfnt(&reference, &[]).units_per_em, 2048);
for stated in [0u16, 15, 16385, u16::MAX] {
let mut data = data.to_vec();
data[head + 18..head + 20].copy_from_slice(&stated.to_be_bytes());
let font = FontRef::new(&data).unwrap();
let metrics = Metrics::from_sfnt(&font, &[]);
assert_eq!(metrics.units_per_em, 1000, "stated {stated}");
assert_eq!(metrics.raw_units_per_em, stated, "stated {stated}");
}
for stated in [16u16, 16384] {
assert_eq!(units_per_em(stated), stated);
}
}
#[test]
fn a_default_location_leaves_every_measurement_on_a_whole_unit() {
let metrics = Metrics::from_sfnt(&horizontal(), &[]);
let ascender = metrics.hhea_line.unwrap().ascender;
assert_eq!(ascender, F48Dot16::from_i32(ascender.to_i32()));
}
#[test]
fn a_location_moves_what_mvar_varies() {
let font = variable();
let default = Metrics::from_sfnt(&font, &[]);
let far = Metrics::from_sfnt(&font, &far());
assert_ne!(default, far);
assert_ne!(default.win_line, far.win_line);
assert_ne!(default.x_height, far.x_height);
assert_ne!(default.cap_height, far.cap_height);
}
#[test]
fn the_two_horizontal_line_sets_move_together() {
let font = variable();
let default = Metrics::from_sfnt(&font, &[]);
let far = Metrics::from_sfnt(&font, &far());
let shift = far.hhea_line.unwrap().ascender - default.hhea_line.unwrap().ascender;
assert_ne!(shift, F48Dot16::ZERO);
assert_eq!(
far.typo_line.unwrap().ascender - default.typo_line.unwrap().ascender,
shift
);
}
#[test]
fn a_delta_keeps_the_fraction_the_variation_store_computed() {
let far = Metrics::from_sfnt(&variable(), &[NormalizedCoord::from_f32(0.4); 12]);
let moved = [
far.hhea_line.unwrap().ascender,
far.hhea_line.unwrap().descender,
far.win_line.unwrap().ascender,
far.win_line.unwrap().descender,
far.x_height.unwrap(),
far.cap_height.unwrap(),
];
assert!(moved
.iter()
.any(|value| *value != F48Dot16::from_i32(value.to_i32())));
}
#[test]
fn what_mvar_cannot_say_does_not_move() {
let font = variable();
let default = Metrics::from_sfnt(&font, &[]);
let far = Metrics::from_sfnt(&font, &far());
assert_eq!(default.units_per_em, far.units_per_em);
assert_eq!(default.num_glyphs, far.num_glyphs);
assert_eq!(default.bounds, far.bounds);
assert_eq!(default.max_h_advance, far.max_h_advance);
assert_eq!(default.average_char_width, far.average_char_width);
}
#[test]
fn a_type1_font_states_what_little_it_can() {
let font =
crate::ps::type1::Type1Font::new(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFA)
.unwrap();
let metrics = Metrics::from_type1(&font);
assert_eq!(metrics.units_per_em, 1000);
assert!(metrics.num_glyphs > 0);
assert!(metrics.bounds.y_max > F48Dot16::ZERO);
assert_eq!(metrics.hhea_line.unwrap().ascender, metrics.bounds.y_max);
assert_eq!(metrics.hhea_line.unwrap().descender, metrics.bounds.y_min);
assert!(metrics.vhea_line.is_none());
assert!(metrics.typo_line.is_none());
assert!(metrics.win_line.is_none());
assert!(metrics.cap_height.is_none());
}
#[test]
fn a_static_font_reads_the_same_at_every_location() {
let font = horizontal();
assert_eq!(
Metrics::from_sfnt(&font, &[]),
Metrics::from_sfnt(&font, &far())
);
}
}