use std::sync::OnceLock;
use crate::foundations::Cast;
use crate::layout::{Em, Frame};
use crate::text::{DEFAULT_SUBSCRIPT_METRICS, DEFAULT_SUPERSCRIPT_METRICS, FontInstance};
#[derive(Debug, Clone)]
pub struct FontMetrics {
pub units_per_em: f64,
pub ascender: Em,
pub cap_height: Em,
pub x_height: Em,
pub descender: Em,
pub strikethrough: LineMetrics,
pub underline: LineMetrics,
pub overline: LineMetrics,
pub subscript: Option<ScriptMetrics>,
pub superscript: Option<ScriptMetrics>,
pub math: OnceLock<Box<MathConstants>>,
}
impl FontMetrics {
pub fn from_ttf(ttf: &ttf_parser::Face) -> Self {
let units_per_em = f64::from(ttf.units_per_em());
let to_em = |units| Em::from_units(units, units_per_em);
let ascender = to_em(ttf.typographic_ascender().unwrap_or(ttf.ascender()));
let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em);
let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em);
let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender()));
let strikeout = ttf.strikeout_metrics();
let underline = ttf.underline_metrics();
let strikethrough = LineMetrics {
position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)),
thickness: strikeout
.or(underline)
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
};
let underline = LineMetrics {
position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)),
thickness: underline
.or(strikeout)
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
};
let overline = LineMetrics {
position: cap_height + Em::new(0.1),
thickness: underline.thickness,
};
let subscript = ttf.subscript_metrics().map(|metrics| ScriptMetrics {
width: to_em(metrics.x_size),
height: to_em(metrics.y_size),
horizontal_offset: to_em(metrics.x_offset),
vertical_offset: -to_em(metrics.y_offset),
});
let superscript = ttf.superscript_metrics().map(|metrics| ScriptMetrics {
width: to_em(metrics.x_size),
height: to_em(metrics.y_size),
horizontal_offset: to_em(metrics.x_offset),
vertical_offset: to_em(metrics.y_offset),
});
Self {
units_per_em,
ascender,
cap_height,
x_height,
descender,
strikethrough,
underline,
overline,
superscript,
subscript,
math: OnceLock::new(),
}
}
pub fn vertical(&self, metric: VerticalFontMetric) -> Em {
match metric {
VerticalFontMetric::Ascender => self.ascender,
VerticalFontMetric::CapHeight => self.cap_height,
VerticalFontMetric::XHeight => self.x_height,
VerticalFontMetric::Baseline => Em::zero(),
VerticalFontMetric::Descender => self.descender,
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct LineMetrics {
pub position: Em,
pub thickness: Em,
}
#[derive(Debug, Copy, Clone)]
pub struct ScriptMetrics {
pub width: Em,
pub height: Em,
pub horizontal_offset: Em,
pub vertical_offset: Em,
}
#[derive(Debug, Copy, Clone)]
pub struct MathConstants {
pub space_width: Em,
pub script_percent_scale_down: i16,
pub script_script_percent_scale_down: i16,
pub display_operator_min_height: Em,
pub axis_height: Em,
pub accent_base_height: Em,
pub flattened_accent_base_height: Em,
pub subscript_shift_down: Em,
pub subscript_top_max: Em,
pub subscript_baseline_drop_min: Em,
pub superscript_shift_up: Em,
pub superscript_shift_up_cramped: Em,
pub superscript_bottom_min: Em,
pub superscript_baseline_drop_max: Em,
pub sub_superscript_gap_min: Em,
pub superscript_bottom_max_with_subscript: Em,
pub space_after_script: Em,
pub upper_limit_gap_min: Em,
pub upper_limit_baseline_rise_min: Em,
pub lower_limit_gap_min: Em,
pub lower_limit_baseline_drop_min: Em,
pub stack_top_shift_up: Em,
pub stack_top_display_style_shift_up: Em,
pub stack_bottom_shift_down: Em,
pub stack_bottom_display_style_shift_down: Em,
pub stack_gap_min: Em,
pub stack_display_style_gap_min: Em,
pub fraction_numerator_shift_up: Em,
pub fraction_numerator_display_style_shift_up: Em,
pub fraction_denominator_shift_down: Em,
pub fraction_denominator_display_style_shift_down: Em,
pub fraction_numerator_gap_min: Em,
pub fraction_num_display_style_gap_min: Em,
pub fraction_rule_thickness: Em,
pub fraction_denominator_gap_min: Em,
pub fraction_denom_display_style_gap_min: Em,
pub skewed_fraction_vertical_gap: Em,
pub skewed_fraction_horizontal_gap: Em,
pub overbar_vertical_gap: Em,
pub overbar_rule_thickness: Em,
pub overbar_extra_ascender: Em,
pub underbar_vertical_gap: Em,
pub underbar_rule_thickness: Em,
pub underbar_extra_descender: Em,
pub radical_vertical_gap: Em,
pub radical_display_style_vertical_gap: Em,
pub radical_rule_thickness: Em,
pub radical_extra_ascender: Em,
pub radical_kern_before_degree: Em,
pub radical_kern_after_degree: Em,
pub radical_degree_bottom_raise_percent: f64,
}
impl MathConstants {
pub(super) fn new(font: &FontInstance) -> Box<Self> {
let ttf = font.ttf();
let space_width = ttf
.glyph_index(' ')
.and_then(|id| ttf.glyph_hor_advance(id).map(|units| font.to_em(units)))
.unwrap_or(typst_library::math::THICK);
ttf.tables()
.math
.and_then(|math| math.constants)
.map(|constants| Self::from_constants(font, &constants, space_width))
.unwrap_or_else(|| Self::fallback(font, space_width))
}
fn from_constants(
font: &FontInstance,
constants: &ttf_parser::math::Constants,
space_width: Em,
) -> Box<Self> {
let is_cambria =
|| font.post_script_name().is_some_and(|name| name == "CambriaMath");
Box::new(Self {
space_width,
script_percent_scale_down: constants.script_percent_scale_down(),
script_script_percent_scale_down: constants
.script_script_percent_scale_down(),
display_operator_min_height: font.to_em(if is_cambria() {
constants.delimited_sub_formula_min_height()
} else {
constants.display_operator_min_height()
}),
axis_height: font.to_em(constants.axis_height().value),
accent_base_height: font.to_em(constants.accent_base_height().value),
flattened_accent_base_height: font
.to_em(constants.flattened_accent_base_height().value),
subscript_shift_down: font.to_em(constants.subscript_shift_down().value),
subscript_top_max: font.to_em(constants.subscript_top_max().value),
subscript_baseline_drop_min: font
.to_em(constants.subscript_baseline_drop_min().value),
superscript_shift_up: font.to_em(constants.superscript_shift_up().value),
superscript_shift_up_cramped: font
.to_em(constants.superscript_shift_up_cramped().value),
superscript_bottom_min: font.to_em(constants.superscript_bottom_min().value),
superscript_baseline_drop_max: font
.to_em(constants.superscript_baseline_drop_max().value),
sub_superscript_gap_min: font
.to_em(constants.sub_superscript_gap_min().value),
superscript_bottom_max_with_subscript: font
.to_em(constants.superscript_bottom_max_with_subscript().value),
space_after_script: font.to_em(constants.space_after_script().value),
upper_limit_gap_min: font.to_em(constants.upper_limit_gap_min().value),
upper_limit_baseline_rise_min: font
.to_em(constants.upper_limit_baseline_rise_min().value),
lower_limit_gap_min: font.to_em(constants.lower_limit_gap_min().value),
lower_limit_baseline_drop_min: font
.to_em(constants.lower_limit_baseline_drop_min().value),
stack_top_shift_up: font.to_em(constants.stack_top_shift_up().value),
stack_top_display_style_shift_up: font
.to_em(constants.stack_top_display_style_shift_up().value),
stack_bottom_shift_down: font
.to_em(constants.stack_bottom_shift_down().value),
stack_bottom_display_style_shift_down: font
.to_em(constants.stack_bottom_display_style_shift_down().value),
stack_gap_min: font.to_em(constants.stack_gap_min().value),
stack_display_style_gap_min: font
.to_em(constants.stack_display_style_gap_min().value),
fraction_numerator_shift_up: font
.to_em(constants.fraction_numerator_shift_up().value),
fraction_numerator_display_style_shift_up: font
.to_em(constants.fraction_numerator_display_style_shift_up().value),
fraction_denominator_shift_down: font
.to_em(constants.fraction_denominator_shift_down().value),
fraction_denominator_display_style_shift_down: font
.to_em(constants.fraction_denominator_display_style_shift_down().value),
fraction_numerator_gap_min: font
.to_em(constants.fraction_numerator_gap_min().value),
fraction_num_display_style_gap_min: font
.to_em(constants.fraction_num_display_style_gap_min().value),
fraction_rule_thickness: font
.to_em(constants.fraction_rule_thickness().value),
fraction_denominator_gap_min: font
.to_em(constants.fraction_denominator_gap_min().value),
fraction_denom_display_style_gap_min: font
.to_em(constants.fraction_denom_display_style_gap_min().value),
skewed_fraction_vertical_gap: font
.to_em(constants.skewed_fraction_vertical_gap().value),
skewed_fraction_horizontal_gap: font
.to_em(constants.skewed_fraction_horizontal_gap().value),
overbar_vertical_gap: font.to_em(constants.overbar_vertical_gap().value),
overbar_rule_thickness: font.to_em(constants.overbar_rule_thickness().value),
overbar_extra_ascender: font.to_em(constants.overbar_extra_ascender().value),
underbar_vertical_gap: font.to_em(constants.underbar_vertical_gap().value),
underbar_rule_thickness: font
.to_em(constants.underbar_rule_thickness().value),
underbar_extra_descender: font
.to_em(constants.underbar_extra_descender().value),
radical_vertical_gap: font.to_em(constants.radical_vertical_gap().value),
radical_display_style_vertical_gap: font
.to_em(constants.radical_display_style_vertical_gap().value),
radical_rule_thickness: font.to_em(constants.radical_rule_thickness().value),
radical_extra_ascender: font.to_em(constants.radical_extra_ascender().value),
radical_kern_before_degree: font
.to_em(constants.radical_kern_before_degree().value),
radical_kern_after_degree: font
.to_em(constants.radical_kern_after_degree().value),
radical_degree_bottom_raise_percent: constants
.radical_degree_bottom_raise_percent()
as f64
/ 100.0,
})
}
fn fallback(font: &FontInstance, space_width: Em) -> Box<Self> {
let metrics = font.metrics();
Box::new(MathConstants {
space_width,
script_percent_scale_down: 70,
script_script_percent_scale_down: 50,
display_operator_min_height: Em::zero(),
axis_height: metrics.x_height / 2.0,
accent_base_height: metrics.x_height,
flattened_accent_base_height: metrics.cap_height,
subscript_shift_down: metrics
.subscript
.map(|metrics| metrics.vertical_offset)
.unwrap_or(DEFAULT_SUBSCRIPT_METRICS.vertical_offset),
subscript_top_max: 0.8 * metrics.x_height,
subscript_baseline_drop_min: Em::zero(),
superscript_shift_up: metrics
.superscript
.map(|metrics| metrics.vertical_offset)
.unwrap_or(DEFAULT_SUPERSCRIPT_METRICS.vertical_offset),
superscript_shift_up_cramped: Em::zero(),
superscript_bottom_min: 0.25 * metrics.x_height,
superscript_baseline_drop_max: Em::zero(),
sub_superscript_gap_min: 4.0 * metrics.underline.thickness,
superscript_bottom_max_with_subscript: 0.8 * metrics.x_height,
space_after_script: Em::new(1.0 / 24.0),
upper_limit_gap_min: Em::zero(),
upper_limit_baseline_rise_min: Em::zero(),
lower_limit_gap_min: Em::zero(),
lower_limit_baseline_drop_min: Em::zero(),
stack_top_shift_up: Em::zero(),
stack_top_display_style_shift_up: Em::zero(),
stack_bottom_shift_down: Em::zero(),
stack_bottom_display_style_shift_down: Em::zero(),
stack_gap_min: 3.0 * metrics.underline.thickness,
stack_display_style_gap_min: 7.0 * metrics.underline.thickness,
fraction_numerator_shift_up: Em::zero(),
fraction_numerator_display_style_shift_up: Em::zero(),
fraction_denominator_shift_down: Em::zero(),
fraction_denominator_display_style_shift_down: Em::zero(),
fraction_numerator_gap_min: metrics.underline.thickness,
fraction_num_display_style_gap_min: 3.0 * metrics.underline.thickness,
fraction_rule_thickness: metrics.underline.thickness,
fraction_denominator_gap_min: metrics.underline.thickness,
fraction_denom_display_style_gap_min: 3.0 * metrics.underline.thickness,
skewed_fraction_vertical_gap: Em::zero(),
skewed_fraction_horizontal_gap: Em::new(0.5),
overbar_vertical_gap: 3.0 * metrics.underline.thickness,
overbar_rule_thickness: metrics.underline.thickness,
overbar_extra_ascender: metrics.underline.thickness,
underbar_vertical_gap: 3.0 * metrics.underline.thickness,
underbar_rule_thickness: metrics.underline.thickness,
underbar_extra_descender: metrics.underline.thickness,
radical_vertical_gap: 1.25 * metrics.underline.thickness,
radical_display_style_vertical_gap: metrics.underline.thickness
+ 0.25 * metrics.x_height,
radical_rule_thickness: metrics.underline.thickness,
radical_extra_ascender: metrics.underline.thickness,
radical_kern_before_degree: Em::new(5.0 / 18.0),
radical_kern_after_degree: Em::new(-10.0 / 18.0),
radical_degree_bottom_raise_percent: 0.6,
})
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum VerticalFontMetric {
Ascender,
CapHeight,
XHeight,
Baseline,
Descender,
}
#[derive(Debug, Copy, Clone)]
pub enum TextEdgeBounds<'a> {
Zero,
Glyph(u16),
Frame(&'a Frame),
}