use read_fonts::tables::gsub::{AlternateSubstFormat1, SubstitutionSubtables};
use read_fonts::types::{GlyphId16, Tag};
use read_fonts::{FontRef, TableProvider};
use skrifa::charmap::Charmap;
use skrifa::instance::{LocationRef, Size};
use skrifa::metrics::{BoundingBox, GlyphMetrics};
use skrifa::outline::{DrawSettings, OutlineGlyphCollection, OutlinePen};
use skrifa::MetadataProvider;
use crate::math::{self, Math, MathKernCorner};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct GlyphId(pub u16);
impl GlyphId {
pub(crate) fn raw(self) -> GlyphId16 {
GlyphId16::new(self.0)
}
}
impl From<GlyphId16> for GlyphId {
fn from(g: GlyphId16) -> Self {
GlyphId(g.to_u16())
}
}
#[derive(Debug)]
pub enum FontError {
Face(String),
NoMathTable,
}
impl core::fmt::Display for FontError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FontError::Face(e) => write!(f, "cannot parse font: {e}"),
FontError::NoMathTable => write!(f, "font has no OpenType MATH table"),
}
}
}
impl std::error::Error for FontError {}
pub(crate) type KernCorner = MathKernCorner;
pub(crate) enum Stretched {
Glyph(GlyphId),
Assembly {
parts: Vec<(GlyphId, f32)>,
extent: f32,
},
}
const MATH: Tag = Tag::new(b"MATH");
const SSTY: Tag = Tag::new(b"ssty");
pub struct MathFont<'a> {
#[cfg(feature = "shaping")]
font: FontRef<'a>,
units_per_em: f32,
ascent: f32,
descent: f32,
x_height: Option<f32>,
charmap: Charmap<'a>,
metrics: GlyphMetrics<'a>,
outlines: OutlineGlyphCollection<'a>,
math: Math<'a>,
ink_boxes: std::sync::OnceLock<Vec<std::sync::OnceLock<Option<BoundingBox>>>>,
ssty: Vec<AlternateSubstFormat1<'a>>,
#[cfg(feature = "shaping")]
shaper: harfrust::ShaperData,
#[cfg(feature = "shaping")]
plans: std::panic::AssertUnwindSafe<[std::sync::OnceLock<harfrust::ShapePlan>; 3]>,
}
const _: () = {
const fn assert_auto_traits<
T: Send + Sync + std::panic::RefUnwindSafe + std::panic::UnwindSafe,
>() {
}
assert_auto_traits::<MathFont<'static>>();
};
impl<'a> MathFont<'a> {
pub fn probe(data: &[u8], index: u32) -> bool {
FontRef::from_index(data, index)
.ok()
.and_then(|font| font.table_data(MATH))
.and_then(Math::read)
.is_some()
}
pub fn new(data: &'a [u8], index: u32) -> Result<Self, FontError> {
let font = FontRef::from_index(data, index).map_err(|e| FontError::Face(e.to_string()))?;
let math = font
.table_data(MATH)
.and_then(Math::read)
.ok_or(FontError::NoMathTable)?;
let head = font.head().map_err(|e| FontError::Face(e.to_string()))?;
let units_per_em = f32::from(head.units_per_em());
let metrics = font.metrics(Size::unscaled(), LocationRef::default());
let ssty = ssty_subtables(&font);
Ok(MathFont {
units_per_em,
ascent: metrics.ascent,
descent: -metrics.descent,
x_height: metrics.x_height,
charmap: font.charmap(),
metrics: font.glyph_metrics(Size::unscaled(), LocationRef::default()),
outlines: font.outline_glyphs(),
math,
ink_boxes: std::sync::OnceLock::new(),
ssty,
#[cfg(feature = "shaping")]
shaper: harfrust::ShaperData::new(&font),
#[cfg(feature = "shaping")]
plans: std::panic::AssertUnwindSafe([const { std::sync::OnceLock::new() }; 3]),
#[cfg(feature = "shaping")]
font,
})
}
#[cfg(feature = "shaping")]
pub(crate) fn shaper(&self) -> harfrust::Shaper<'_> {
self.shaper.shaper(&self.font).build()
}
#[cfg(feature = "shaping")]
pub(crate) fn shape_plan(&self, script_level: u8) -> &harfrust::ShapePlan {
let level = usize::from(script_level.min(2));
self.plans[level].get_or_init(|| {
let mut features = Vec::new();
if level > 0 {
features.push(harfrust::Feature::new(SSTY, level as u32, ..));
}
harfrust::ShapePlan::new(
&self.shaper(),
harfrust::Direction::LeftToRight,
Some(harfrust::script::COMMON),
None,
&features,
)
})
}
pub fn units_per_em(&self) -> f32 {
self.units_per_em
}
pub(crate) fn constants(&self) -> math::MathConstants<'a> {
self.math.constants()
}
pub(crate) fn glyph_index(&self, c: char) -> Option<GlyphId> {
let id = self.charmap.map(c)?;
u16::try_from(id.to_u32()).ok().map(GlyphId)
}
pub(crate) fn advance(&self, glyph: GlyphId) -> f32 {
self.metrics
.advance_width(glyph.raw().into())
.unwrap_or(0.0)
}
pub(crate) fn ink_box(&self, glyph: GlyphId) -> Option<BoundingBox> {
let table = self.ink_boxes.get_or_init(|| {
std::iter::repeat_with(std::sync::OnceLock::new)
.take(self.metrics.glyph_count() as usize)
.collect()
});
match table.get(usize::from(glyph.0)) {
Some(slot) => *slot.get_or_init(|| self.bounds(glyph)),
None => self.bounds(glyph),
}
}
fn bounds(&self, glyph: GlyphId) -> Option<BoundingBox> {
self.metrics
.bounds(glyph.raw().into())
.filter(|b| b.x_min < b.x_max || b.y_min < b.y_max)
}
pub(crate) fn outline(&self, glyph: GlyphId, pen: &mut impl OutlinePen) -> bool {
let Some(outline) = self.outlines.get(glyph.raw().into()) else {
return false;
};
outline
.draw(
DrawSettings::unhinted(Size::unscaled(), LocationRef::default()),
pen,
)
.is_ok()
}
pub(crate) fn stretch_vertical(&self, glyph: GlyphId, target: f32) -> Stretched {
self.stretch(glyph, target, false)
}
pub(crate) fn stretch_horizontal(&self, glyph: GlyphId, target: f32) -> Stretched {
self.stretch(glyph, target, true)
}
fn stretch(&self, glyph: GlyphId, target: f32, horizontal: bool) -> Stretched {
let Some(variants) = self.math.variants() else {
return Stretched::Glyph(glyph);
};
let Some(construction) = variants.construction(glyph.raw(), horizontal) else {
return Stretched::Glyph(glyph);
};
let mut best = glyph;
for v in construction.variants() {
best = v.variant_glyph.into();
if f32::from(v.advance_measurement) >= target {
return Stretched::Glyph(best);
}
}
construction
.glyph_assembly()
.and_then(|asm| {
self.assemble(&asm, f32::from(variants.min_connector_overlap()), target)
})
.unwrap_or(Stretched::Glyph(best))
}
fn assemble(
&self,
asm: &math::GlyphAssembly,
min_overlap: f32,
target: f32,
) -> Option<Stretched> {
let asm_parts: Vec<math::GlyphPart> = asm.parts().collect();
for repeats in 1..=32u32 {
let mut seq: Vec<math::GlyphPart> = Vec::new();
for part in &asm_parts {
let n = if part.extender { repeats } else { 1 };
for _ in 0..n {
seq.push(*part);
}
}
if seq.len() < 2 {
return None;
}
let sum: f32 = seq.iter().map(|p| f32::from(p.full_advance)).sum();
let joints = (seq.len() - 1) as f32;
if sum - min_overlap * joints < target {
continue; }
let mut overlap = (sum - target) / joints;
let max_overlap = seq
.windows(2)
.map(|w| f32::from(w[0].end_connector_length.min(w[1].start_connector_length)))
.fold(f32::INFINITY, f32::min);
overlap = overlap.clamp(min_overlap, max_overlap.max(min_overlap));
let mut parts = Vec::with_capacity(seq.len());
let mut bottom = 0.0;
for p in &seq {
parts.push((p.glyph_id.into(), bottom));
bottom += f32::from(p.full_advance) - overlap;
}
let extent = bottom + overlap;
return Some(Stretched::Assembly { parts, extent });
}
None
}
pub(crate) fn script_alternate(&self, glyph: GlyphId, level: u16) -> Option<GlyphId> {
debug_assert!(level >= 1);
for alt in &self.ssty {
let Some(idx) = alt.coverage().ok().and_then(|c| c.get(glyph.raw())) else {
continue;
};
let Ok(set) = alt.alternate_sets().get(usize::from(idx)) else {
continue;
};
let alternates = set.alternate_glyph_ids();
let Some(last) = alternates.len().checked_sub(1) else {
continue;
};
return alternates
.get(usize::from(level.saturating_sub(1)).min(last))
.map(|g| g.get().into());
}
None
}
pub(crate) fn italic_correction(&self, glyph: GlyphId) -> f32 {
self.math
.glyph_info()
.and_then(|gi| gi.italics_correction()?.get(glyph.raw()))
.map_or(0.0, |v| f32::from(v.value))
}
pub(crate) fn top_accent_attachment(&self, glyph: GlyphId) -> Option<f32> {
self.math
.glyph_info()
.and_then(|gi| gi.top_accent_attachment()?.get(glyph.raw()))
.map(|v| f32::from(v.value))
}
pub(crate) fn math_kern(&self, glyph: GlyphId, corner: KernCorner, height: f32) -> f32 {
let Some(kern) = self
.math
.glyph_info()
.and_then(|gi| gi.kern_info()?.get(glyph.raw(), corner))
else {
return 0.0;
};
let count = kern.count();
let mut index = count;
for i in 0..count {
let step = kern.height(i).map_or(f32::MAX, |v| f32::from(v.value));
if height <= step {
index = i;
break;
}
}
kern.kern(index).map_or(0.0, |v| f32::from(v.value))
}
pub(crate) fn x_height(&self) -> f32 {
self.x_height.unwrap_or(self.units_per_em * 0.5)
}
pub(crate) fn line_metrics(&self) -> (f32, f32) {
(self.ascent, self.descent)
}
}
fn ssty_subtables<'a>(font: &FontRef<'a>) -> Vec<AlternateSubstFormat1<'a>> {
let mut out = Vec::new();
let Ok(gsub) = font.gsub() else {
return out;
};
let (Ok(features), Ok(lookups)) = (gsub.feature_list(), gsub.lookup_list()) else {
return out;
};
let Some(feature) = features
.feature_records()
.iter()
.find(|r| r.feature_tag() == SSTY)
.and_then(|r| r.feature(features.offset_data()).ok())
else {
return out;
};
for li in feature.lookup_list_indices() {
let Ok(lookup) = lookups.lookups().get(usize::from(li.get())) else {
continue;
};
if let Ok(SubstitutionSubtables::Alternate(subs)) = lookup.subtables() {
out.extend(subs.iter().flatten());
}
}
out
}