use std::fmt::Write;
use std::sync::OnceLock;
use ttf_parser::math::{Constants, MathValue};
pub use ttf_parser::GlyphId;
use ttf_parser::{Face, OutlineBuilder};
static DATA: &[u8] = include_bytes!("../fonts/STIXTwoMath-Regular.otf");
pub const NAME: &str = "STIX Two Math";
pub struct Font {
face: Face<'static>,
}
pub fn font() -> &'static Font {
static FONT: OnceLock<Font> = OnceLock::new();
FONT.get_or_init(|| Font { face: Face::parse(DATA, 0).expect("valid embedded font") })
}
#[derive(Debug, Clone, Copy)]
pub struct Part {
pub id: GlyphId,
pub start: f32,
pub end: f32,
pub advance: f32,
pub extender: bool,
}
impl Font {
pub fn units_per_em(&self) -> f32 {
f32::from(self.face.units_per_em())
}
pub fn glyph(&self, c: char) -> Option<GlyphId> {
self.face.glyph_index(c)
}
pub fn advance(&self, id: GlyphId) -> f32 {
f32::from(self.face.glyph_hor_advance(id).unwrap_or(0))
}
pub fn limits(&self, id: GlyphId) -> (f32, f32, f32, f32) {
match self.face.glyph_bounding_box(id) {
Some(r) => (f32::from(r.x_min), f32::from(r.y_min), f32::from(r.x_max), f32::from(r.y_max)),
None => (0.0, 0.0, 0.0, 0.0),
}
}
pub fn outline(&self, id: GlyphId) -> String {
let mut path = PathWriter(String::new());
self.face.outline_glyph(id, &mut path);
path.0.trim_end().to_string()
}
fn constants(&self) -> Constants<'static> {
self.face.tables().math.and_then(|m| m.constants).expect("the embedded font has a MATH table")
}
pub fn c(&self, f: impl Fn(&Constants<'static>) -> MathValue<'static>) -> f32 {
f32::from(f(&self.constants()).value)
}
pub fn script_scale(&self) -> f32 {
f32::from(self.constants().script_percent_scale_down()) / 100.0
}
pub fn script_script_scale(&self) -> f32 {
f32::from(self.constants().script_script_percent_scale_down()) / 100.0
}
pub fn display_operator_min_height(&self) -> f32 {
f32::from(self.constants().display_operator_min_height())
}
pub fn radical_degree_raise(&self) -> f32 {
f32::from(self.constants().radical_degree_bottom_raise_percent()) / 100.0
}
pub fn italic_correction(&self, id: GlyphId) -> f32 {
self.face.tables().math.and_then(|m| m.glyph_info).and_then(|g| g.italic_corrections).and_then(|c| c.get(id)).map_or(0.0, |v| f32::from(v.value))
}
pub fn accent_point(&self, id: GlyphId) -> Option<f32> {
self.face.tables().math.and_then(|m| m.glyph_info).and_then(|g| g.top_accent_attachments).and_then(|c| c.get(id)).map(|v| f32::from(v.value))
}
pub fn min_overlap(&self) -> f32 {
self.face.tables().math.and_then(|m| m.variants).map_or(0.0, |v| f32::from(v.min_connector_overlap))
}
pub fn variants(&self, id: GlyphId, vertical: bool) -> Vec<(GlyphId, f32)> {
self.face
.tables()
.math
.and_then(|m| m.variants)
.and_then(|v| if vertical { v.vertical_constructions } else { v.horizontal_constructions }.get(id))
.map(|c| c.variants.into_iter().map(|g| (g.variant_glyph, f32::from(g.advance_measurement))).collect())
.unwrap_or_default()
}
pub fn assembly(&self, id: GlyphId, vertical: bool) -> Option<Vec<Part>> {
let v = self.face.tables().math.and_then(|m| m.variants)?;
let constructions = if vertical { v.vertical_constructions } else { v.horizontal_constructions };
let assembly = constructions.get(id)?.assembly?;
Some(
assembly
.parts
.into_iter()
.map(|p| Part {
id: p.glyph_id,
start: f32::from(p.start_connector_length),
end: f32::from(p.end_connector_length),
advance: f32::from(p.full_advance),
extender: p.part_flags.extender(),
})
.collect(),
)
}
}
struct PathWriter(String);
fn num(v: f32) -> String {
let rounded = (v * 10.0).round() / 10.0;
if rounded.fract() == 0.0 {
format!("{}", rounded as i64)
} else {
format!("{rounded}")
}
}
impl OutlineBuilder for PathWriter {
fn move_to(&mut self, x: f32, y: f32) {
let _ = write!(self.0, "M{} {} ", num(x), num(y));
}
fn line_to(&mut self, x: f32, y: f32) {
let _ = write!(self.0, "L{} {} ", num(x), num(y));
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
let _ = write!(self.0, "Q{} {} {} {} ", num(x1), num(y1), num(x), num(y));
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
let _ = write!(self.0, "C{} {} {} {} {} {} ", num(x1), num(y1), num(x2), num(y2), num(x), num(y));
}
fn close(&mut self) {
self.0.push_str("Z ");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glyph_without_outline_has_zero_bounds() {
let f = font();
assert_eq!(f.limits(f.glyph(' ').unwrap()), (0.0, 0.0, 0.0, 0.0));
}
#[test]
fn quadratic_curve_becomes_q() {
let mut c = PathWriter(String::new());
c.quad_to(1.0, 2.25, 3.0, 4.0);
assert_eq!(c.0, "Q1 2.3 3 4 ");
}
}