use std::fmt::Write;
use std::sync::OnceLock;
use ttf_parser::Face;
pub use ttf_parser::GlyphId;
use crate::FONT_BYTES;
fn face() -> &'static Face<'static> {
static FACE: OnceLock<Face<'static>> = OnceLock::new();
FACE.get_or_init(|| Face::parse(FONT_BYTES, 0).expect("bundled math font must parse"))
}
pub fn units_per_em() -> f32 {
face().units_per_em() as f32
}
#[allow(dead_code)]
pub fn has_math_table() -> bool {
face().tables().math.is_some()
}
pub fn glyph_id(ch: char) -> Option<GlyphId> {
face().glyph_index(ch)
}
pub fn map_variant(font: pulldown_latex::event::Font, c: char) -> char {
use pulldown_latex::event::Font;
char::from_u32(match (font, c) {
(Font::BoldScript, 'A'..='Z') => c as u32 + 0x1D48F,
(Font::BoldScript, 'a'..='z') => c as u32 + 0x1D489,
(Font::BoldItalic, 'A'..='Z') => c as u32 + 0x1D427,
(Font::BoldItalic, 'a'..='z') => c as u32 + 0x1D421,
(Font::BoldItalic, '\u{0391}'..='\u{03A1}' | '\u{03A3}'..='\u{03A9}') => c as u32 + 0x1D38B,
(Font::BoldItalic, '\u{03B1}'..='\u{03C9}') => c as u32 + 0x1D385,
(Font::Bold, 'A'..='Z') => c as u32 + 0x1D3BF,
(Font::Bold, 'a'..='z') => c as u32 + 0x1D3B9,
(Font::Bold, '\u{0391}'..='\u{03A1}' | '\u{03A3}'..='\u{03A9}') => c as u32 + 0x1D317,
(Font::Bold, '\u{03B1}'..='\u{03C9}') => c as u32 + 0x1D311,
(Font::Bold, '0'..='9') => c as u32 + 0x1D79E,
(Font::Fraktur, 'A' | 'B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y') => c as u32 + 0x1D4C3,
(Font::Fraktur, 'C') => c as u32 + 0x20EA,
(Font::Fraktur, 'H' | 'I') => c as u32 + 0x20C4,
(Font::Fraktur, 'R') => c as u32 + 0x20CA,
(Font::Fraktur, 'Z') => c as u32 + 0x20CE,
(Font::Fraktur, 'a'..='z') => c as u32 + 0x1D4BD,
(Font::Script, 'A' | 'C' | 'D' | 'G' | 'J' | 'K' | 'N'..='Q' | 'S'..='Z') => c as u32 + 0x1D45B,
(Font::Script, 'B') => c as u32 + 0x20EA,
(Font::Script, 'E' | 'F') => c as u32 + 0x20EB,
(Font::Script, 'H') => c as u32 + 0x20C3,
(Font::Script, 'I') => c as u32 + 0x20C7,
(Font::Script, 'L') => c as u32 + 0x20C6,
(Font::Script, 'M') => c as u32 + 0x20E6,
(Font::Script, 'R') => c as u32 + 0x20C9,
(Font::Script, 'a'..='d' | 'f' | 'h'..='n' | 'p'..='z') => c as u32 + 0x1D455,
(Font::Script, 'e') => c as u32 + 0x20CA,
(Font::Script, 'g') => c as u32 + 0x20A3,
(Font::Script, 'o') => c as u32 + 0x20C5,
(Font::Monospace, 'A'..='Z') => c as u32 + 0x1D62F,
(Font::Monospace, 'a'..='z') => c as u32 + 0x1D629,
(Font::Monospace, '0'..='9') => c as u32 + 0x1D7C6,
(Font::SansSerif, 'A'..='Z') => c as u32 + 0x1D55F,
(Font::SansSerif, 'a'..='z') => c as u32 + 0x1D559,
(Font::SansSerif, '0'..='9') => c as u32 + 0x1D7B2,
(Font::DoubleStruck, 'A' | 'B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y') => c as u32 + 0x1D4F7,
(Font::DoubleStruck, 'C') => c as u32 + 0x20BF,
(Font::DoubleStruck, 'H') => c as u32 + 0x20C5,
(Font::DoubleStruck, 'N') => c as u32 + 0x20C7,
(Font::DoubleStruck, 'P' | 'Q') => c as u32 + 0x20C9,
(Font::DoubleStruck, 'R') => c as u32 + 0x20CB,
(Font::DoubleStruck, 'Z') => c as u32 + 0x20CA,
(Font::DoubleStruck, 'a'..='z') => c as u32 + 0x1D4F1,
(Font::DoubleStruck, '0'..='9') => c as u32 + 0x1D7A8,
(Font::Italic, 'A'..='Z') => c as u32 + 0x1D3F3,
(Font::Italic, 'a'..='g' | 'i'..='z') => c as u32 + 0x1D3ED,
(Font::Italic, 'h') => c as u32 + 0x20A6,
(Font::Italic, '\u{0391}'..='\u{03A1}' | '\u{03A3}'..='\u{03A9}') => c as u32 + 0x1D351,
(Font::Italic, '\u{03B1}'..='\u{03C9}') => c as u32 + 0x1D34B,
(Font::BoldFraktur, 'A'..='Z') => c as u32 + 0x1D52B,
(Font::BoldFraktur, 'a'..='z') => c as u32 + 0x1D525,
(Font::SansSerifItalic, 'A'..='Z') => c as u32 + 0x1D5D7,
(Font::SansSerifItalic, 'a'..='z') => c as u32 + 0x1D5C1,
(Font::BoldSansSerif, 'A'..='Z') => c as u32 + 0x1D593,
(Font::BoldSansSerif, 'a'..='z') => c as u32 + 0x1D58D,
(Font::BoldSansSerif, '0'..='9') => c as u32 + 0x1D7BC,
(Font::SansSerifBoldItalic, 'A'..='Z') => c as u32 + 0x1D5FB,
(Font::SansSerifBoldItalic, 'a'..='z') => c as u32 + 0x1D5F5,
(_, _) => c as u32,
})
.unwrap_or(c)
}
pub fn accent_glyph(ch: char) -> Option<GlyphId> {
let combining = match ch {
'^' | '\u{0302}' | 'ˆ' => '\u{0302}', '~' | '\u{0303}' | '˜' => '\u{0303}', '‾' | '¯' | '\u{0304}' => '\u{0304}', '→' | '\u{20D7}' => '\u{20D7}', '˙' | '\u{0307}' => '\u{0307}', '¨' | '\u{0308}' => '\u{0308}', 'ˇ' | '\u{030C}' => '\u{030C}', '˘' | '\u{0306}' => '\u{0306}', '´' | '\u{0301}' => '\u{0301}', '`' | '\u{0300}' => '\u{0300}', _ => return None,
};
face().glyph_index(combining)
}
#[derive(Debug, Clone, Copy)]
pub struct GlyphMetrics {
pub advance: f32,
pub height: f32,
pub depth: f32,
}
pub fn glyph_x_bounds(id: GlyphId, font_size: f32) -> (f32, f32) {
let face = face();
let scale = font_size / face.units_per_em() as f32;
match face.glyph_bounding_box(id) {
Some(b) => (b.x_min as f32 * scale, b.x_max as f32 * scale),
None => (0.0, 0.0),
}
}
pub fn glyph_metrics(id: GlyphId, font_size: f32) -> GlyphMetrics {
let face = face();
let upem = face.units_per_em() as f32;
let scale = font_size / upem;
let advance = face.glyph_hor_advance(id).unwrap_or(0) as f32 * scale;
let bbox = face.glyph_bounding_box(id);
let (height, depth) = match bbox {
Some(b) => (b.y_max as f32 * scale, (-(b.y_min as f32)) * scale),
None => (0.0, 0.0),
};
GlyphMetrics {
advance,
height,
depth,
}
}
struct PathBuilder(String);
impl ttf_parser::OutlineBuilder for PathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
let _ = write!(self.0, "M{} {} ", x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
let _ = write!(self.0, "L{} {} ", x, y);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
let _ = write!(self.0, "Q{} {} {} {} ", x1, y1, x, y);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
let _ = write!(self.0, "C{} {} {} {} {} {} ", x1, y1, x2, y2, x, y);
}
fn close(&mut self) {
self.0.push_str("Z ");
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub enum MathConstant {
AxisHeight,
FractionNumeratorShiftUp,
FractionDenominatorShiftDown,
FractionRuleThickness,
FractionNumDisplayStyleShiftUp,
FractionDenomDisplayStyleShiftDown,
FractionNumeratorGapMin,
FractionNumDisplayStyleGapMin,
FractionDenominatorGapMin,
FractionDenomDisplayStyleGapMin,
SubscriptShiftDown,
SubscriptTopMax,
SubscriptBaselineDropMin,
SuperscriptShiftUp,
SuperscriptShiftUpCramped,
SuperscriptBottomMin,
SuperscriptBaselineDropMax,
SubSuperscriptGapMin,
SuperscriptBottomMaxWithSubscript,
RadicalRuleThickness,
RadicalVerticalGap,
RadicalDisplayStyleVerticalGap,
RadicalKernBeforeDegree,
RadicalKernAfterDegree,
RadicalDegreeBottomRaisePercent,
UpperLimitGapMin,
UpperLimitBaselineRiseMin,
LowerLimitGapMin,
LowerLimitBaselineDropMin,
AccentBaseHeight,
}
pub fn math_constant(c: MathConstant, font_size: f32) -> f32 {
let face = face();
let scale = font_size / face.units_per_em() as f32;
let Some(math) = face.tables().math else {
return 0.0;
};
let Some(consts) = math.constants else {
return 0.0;
};
use MathConstant::*;
let value: i16 = match c {
AxisHeight => consts.axis_height().value,
FractionNumeratorShiftUp => consts.fraction_numerator_shift_up().value,
FractionDenominatorShiftDown => consts.fraction_denominator_shift_down().value,
FractionRuleThickness => consts.fraction_rule_thickness().value,
FractionNumDisplayStyleShiftUp => consts.fraction_numerator_display_style_shift_up().value,
FractionDenomDisplayStyleShiftDown => {
consts.fraction_denominator_display_style_shift_down().value
}
FractionNumeratorGapMin => consts.fraction_numerator_gap_min().value,
FractionNumDisplayStyleGapMin => consts.fraction_num_display_style_gap_min().value,
FractionDenominatorGapMin => consts.fraction_denominator_gap_min().value,
FractionDenomDisplayStyleGapMin => consts.fraction_denom_display_style_gap_min().value,
SubscriptShiftDown => consts.subscript_shift_down().value,
SubscriptTopMax => consts.subscript_top_max().value,
SubscriptBaselineDropMin => consts.subscript_baseline_drop_min().value,
SuperscriptShiftUp => consts.superscript_shift_up().value,
SuperscriptShiftUpCramped => consts.superscript_shift_up_cramped().value,
SuperscriptBottomMin => consts.superscript_bottom_min().value,
SuperscriptBaselineDropMax => consts.superscript_baseline_drop_max().value,
SubSuperscriptGapMin => consts.sub_superscript_gap_min().value,
SuperscriptBottomMaxWithSubscript => consts.superscript_bottom_max_with_subscript().value,
RadicalRuleThickness => consts.radical_rule_thickness().value,
RadicalVerticalGap => consts.radical_vertical_gap().value,
RadicalDisplayStyleVerticalGap => consts.radical_display_style_vertical_gap().value,
RadicalKernBeforeDegree => consts.radical_kern_before_degree().value,
RadicalKernAfterDegree => consts.radical_kern_after_degree().value,
RadicalDegreeBottomRaisePercent => {
return consts.radical_degree_bottom_raise_percent() as f32 / 100.0;
}
UpperLimitGapMin => consts.upper_limit_gap_min().value,
UpperLimitBaselineRiseMin => consts.upper_limit_baseline_rise_min().value,
LowerLimitGapMin => consts.lower_limit_gap_min().value,
LowerLimitBaselineDropMin => consts.lower_limit_baseline_drop_min().value,
AccentBaseHeight => consts.accent_base_height().value,
};
value as f32 * scale
}
pub fn math_variant_vertical(base: GlyphId, target_design_units: f32) -> Option<(GlyphId, f32)> {
let math = face().tables().math?;
let variants = math.variants?;
let construction = variants.vertical_constructions.get(base)?;
let mut largest: Option<(GlyphId, f32)> = None;
for v in construction.variants {
let adv = v.advance_measurement as f32;
if adv >= target_design_units {
return Some((v.variant_glyph, adv));
}
match largest {
Some((_, h)) if h >= adv => {}
_ => largest = Some((v.variant_glyph, adv)),
}
}
largest
}
pub fn outline_path(id: GlyphId) -> String {
let mut b = PathBuilder(String::new());
let _ = face().outline_glyph(id, &mut b);
b.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn face_loads_and_has_math_table() {
let upem = units_per_em();
assert!(upem > 0.0, "units_per_em should be > 0, got {}", upem);
assert!(has_math_table(), "bundled font must have MATH table");
}
#[test]
fn maps_ascii_letter_to_glyph_id() {
let id = glyph_id('E').expect("E must exist in bundled math font");
assert!(id.0 > 0);
}
#[test]
fn maps_greek_alpha() {
let id = glyph_id('α').expect("α must exist");
assert!(id.0 > 0);
}
#[test]
fn returns_none_for_unmapped_codepoint() {
assert!(glyph_id('\u{E000}').is_none());
}
#[test]
fn metrics_for_capital_e() {
let id = glyph_id('E').unwrap();
let m = glyph_metrics(id, 16.0);
assert!(m.advance > 0.0);
assert!(m.height > 0.0);
assert!(m.depth >= 0.0);
assert!(m.height > m.depth);
}
#[test]
fn metrics_scale_linearly_with_size() {
let id = glyph_id('E').unwrap();
let m1 = glyph_metrics(id, 10.0);
let m2 = glyph_metrics(id, 20.0);
let ratio = m2.advance / m1.advance;
assert!(
(ratio - 2.0).abs() < 1e-3,
"expected 2.0 ratio, got {}",
ratio
);
}
#[test]
fn reads_axis_height() {
let h = math_constant(MathConstant::AxisHeight, 16.0);
assert!(
h > 0.0 && h < 16.0,
"AxisHeight should be small positive px, got {}",
h
);
}
#[test]
fn reads_fraction_rule_thickness() {
let t = math_constant(MathConstant::FractionRuleThickness, 16.0);
assert!(
t > 0.0 && t < 2.0,
"FractionRuleThickness should be ~1px, got {}",
t
);
}
#[test]
fn integral_has_bigger_variant() {
let id = glyph_id('∫').unwrap();
let (variant, advance) = math_variant_vertical(id, 1500.0)
.expect("integral must have a bigger vertical variant");
assert!(
variant != id,
"should return a different glyph for bigger size"
);
assert!(advance >= 1500.0);
}
#[test]
fn returns_none_for_atom_without_variants() {
let id = glyph_id('E').unwrap();
assert!(math_variant_vertical(id, 50.0).is_none());
}
#[test]
fn returns_largest_when_target_exceeds_all_variants() {
let id = glyph_id('∫').unwrap();
let (variant, h) = math_variant_vertical(id, 1e9)
.expect("should fall back to largest variant, not None");
assert!(
variant != id,
"should return a non-base variant; got base glyph"
);
assert!(
h > 1500.0,
"largest variant should be substantially bigger than base; got {}",
h,
);
}
#[test]
fn outlines_capital_e_to_path_string() {
let id = glyph_id('E').unwrap();
let path = outline_path(id);
assert!(
path.contains('M'),
"path should contain at least one Move: {}",
path
);
assert!(
path.contains('L') || path.contains('C') || path.contains('Q'),
"path should contain at least one line/curve segment: {}",
path
);
assert!(
path.ends_with('Z') || path.contains("Z "),
"path should be closed: {}",
path
);
}
#[test]
fn outline_uses_design_units_no_scaling() {
let id = glyph_id('E').unwrap();
let path = outline_path(id);
let any_large = path
.split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
.filter_map(|s| s.parse::<f32>().ok())
.any(|n| n.abs() > 50.0);
assert!(any_large, "expected design-unit magnitudes, got: {}", path);
}
}