use crate::{
ps::type1::Type1Font,
tables::{
mvar::{tags, MvarInstance},
os2::SelectionFlags,
},
TableProvider,
};
use types::{BoundingBox, F2Dot14, F48Dot16, Tag};
fn metric(value: i32, deltas: Option<&MvarInstance>, tag: Tag) -> F48Dot16 {
F48Dot16::from_i32(value)
+ deltas
.and_then(|deltas| deltas.get(tag))
.unwrap_or_default()
}
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub struct LineBox {
pub ascender: F48Dot16,
pub descender: F48Dot16,
pub line_gap: F48Dot16,
}
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub struct GlobalMetrics {
pub units_per_em: u16,
pub num_glyphs: u32,
pub bounds: BoundingBox<F48Dot16>,
pub hhea_line: Option<LineBox>,
pub max_advance_width: Option<F48Dot16>,
pub vhea_line: Option<LineBox>,
pub max_advance_height: Option<F48Dot16>,
pub typo_line: Option<LineBox>,
pub use_typo_metrics: bool,
pub win_ascent: Option<F48Dot16>,
pub win_descent: Option<F48Dot16>,
pub x_height: Option<F48Dot16>,
pub cap_height: Option<F48Dot16>,
pub average_char_width: Option<F48Dot16>,
}
impl GlobalMetrics {
pub fn from_sfnt<'a>(tables: &impl TableProvider<'a>, coords: &[F2Dot14]) -> Self {
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();
if let Ok(head) = tables.head() {
metrics.units_per_em = head.units_per_em();
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.maxp() {
metrics.num_glyphs = maxp.num_glyphs() as u32;
}
if let Ok(hhea) = tables.hhea() {
metrics.hhea_line = Some(LineBox {
ascender: metric(hhea.ascender().to_i16() as i32, deltas, tags::HASC),
descender: metric(hhea.descender().to_i16() as i32, deltas, tags::HDSC),
line_gap: metric(hhea.line_gap().to_i16() as i32, deltas, tags::HLGP),
});
metrics.max_advance_width =
Some(F48Dot16::from_i32(hhea.advance_width_max().to_u16() as i32));
}
if let Ok(vhea) = tables.vhea() {
metrics.vhea_line = Some(LineBox {
ascender: metric(vhea.ascender().to_i16() as i32, deltas, tags::VASC),
descender: metric(vhea.descender().to_i16() as i32, deltas, tags::VDSC),
line_gap: metric(vhea.line_gap().to_i16() as i32, deltas, tags::VLGP),
});
metrics.max_advance_height =
Some(F48Dot16::from_i32(vhea.advance_height_max().to_u16() as i32));
}
if let Ok(os2) = tables.os2() {
metrics.typo_line = Some(LineBox {
ascender: metric(os2.s_typo_ascender() as i32, deltas, tags::HASC),
descender: metric(os2.s_typo_descender() as i32, deltas, tags::HDSC),
line_gap: metric(os2.s_typo_line_gap() as i32, deltas, tags::HLGP),
});
metrics.win_ascent = Some(metric(os2.us_win_ascent() as i32, deltas, tags::HCLA));
metrics.win_descent = Some(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 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(),
};
Self {
units_per_em: font.upem().clamp(0, u16::MAX as i32) as u16,
num_glyphs: font.num_glyphs(),
bounds,
hhea_line: Some(LineBox {
ascender: bounds.y_max,
descender: bounds.y_min,
line_gap: F48Dot16::ZERO,
}),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FontRef;
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() -> [F2Dot14; 12] {
[F2Dot14::from_f32(1.0); 12]
}
#[test]
fn the_three_sets_of_line_metrics_are_kept_apart() {
let metrics = GlobalMetrics::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);
assert!(metrics.win_ascent.unwrap() > F48Dot16::ZERO);
assert!(metrics.win_descent.unwrap() > F48Dot16::ZERO);
}
#[test]
fn a_font_without_a_table_leaves_its_measurements_unset() {
let metrics = GlobalMetrics::from_sfnt(&horizontal(), &[]);
assert!(metrics.vhea_line.is_none());
assert!(metrics.max_advance_height.is_none());
let metrics = GlobalMetrics::from_sfnt(&vertical(), &[]);
assert!(metrics.vhea_line.is_some());
assert!(metrics.max_advance_height.unwrap() > F48Dot16::ZERO);
}
#[test]
fn a_font_with_nothing_to_read_gives_up_rather_than_failing() {
let metrics =
GlobalMetrics::from_sfnt(&FontRef::new(font_test_data::NAMES_ONLY).unwrap(), &[]);
assert_eq!(metrics, GlobalMetrics::default());
}
#[test]
fn a_default_location_leaves_every_measurement_on_a_whole_unit() {
let metrics = GlobalMetrics::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 = GlobalMetrics::from_sfnt(&font, &[]);
let far = GlobalMetrics::from_sfnt(&font, &far());
assert_ne!(default, far);
assert_ne!(default.win_ascent, far.win_ascent);
assert_ne!(default.win_descent, far.win_descent);
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 = GlobalMetrics::from_sfnt(&font, &[]);
let far = GlobalMetrics::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 = GlobalMetrics::from_sfnt(&variable(), &[F2Dot14::from_f32(0.4); 12]);
let moved = [
far.hhea_line.unwrap().ascender,
far.hhea_line.unwrap().descender,
far.win_ascent.unwrap(),
far.win_descent.unwrap(),
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 = GlobalMetrics::from_sfnt(&font, &[]);
let far = GlobalMetrics::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_advance_width, far.max_advance_width);
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 = GlobalMetrics::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_ascent.is_none());
assert!(metrics.cap_height.is_none());
}
#[test]
fn a_static_font_reads_the_same_at_every_location() {
let font = horizontal();
assert_eq!(
GlobalMetrics::from_sfnt(&font, &[]),
GlobalMetrics::from_sfnt(&font, &far())
);
}
}