use crate::mbox::FaceId;
use crate::node::MathFont;
pub struct FaceSet {
fonts: Vec<fmd_font::Font>,
}
pub const FACE_REGULAR: FaceId = FaceId(0);
pub const FACE_ITALIC: FaceId = FaceId(1);
pub const FACE_BOLD: FaceId = FaceId(2);
pub const FACE_BOLD_ITALIC: FaceId = FaceId(3);
pub const FACE_TYPEWRITER: FaceId = FaceId(4);
pub const FACE_SANS: FaceId = FaceId(5);
pub const FACE_SYMBOLS: FaceId = FaceId(6);
impl FaceSet {
#[must_use]
pub fn from_fonts(fonts: Vec<fmd_font::Font>) -> Self {
Self { fonts }
}
#[cfg(feature = "bundled-faces")]
pub fn bundled() -> Result<Self, fmd_font::FontError> {
let fonts = vec![
fmd_font::Font::parse(fmd_font::bundled::CM_REGULAR.to_vec())?,
fmd_font::Font::parse(fmd_font::bundled::CM_ITALIC.to_vec())?,
fmd_font::Font::parse(fmd_font::bundled::CM_BOLD.to_vec())?,
fmd_font::Font::parse(fmd_font::bundled::CM_BOLD_ITALIC.to_vec())?,
fmd_font::Font::parse(fmd_font::bundled::CM_TYPEWRITER.to_vec())?,
fmd_font::Font::parse(fmd_font::bundled::PLEX_REGULAR.to_vec())?,
fmd_font::Font::parse(fmd_font::bundled::NOTO_SANS_MATH_SYMBOLS.to_vec())?,
];
Ok(Self::from_fonts(fonts))
}
#[must_use]
pub fn font(&self, id: FaceId) -> Option<&fmd_font::Font> {
self.fonts.get(id.0)
}
#[must_use]
pub fn len(&self) -> usize {
self.fonts.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.fonts.is_empty()
}
#[must_use]
pub fn resolve(&self, ch: char, prefer: &[FaceId]) -> Option<(FaceId, u16)> {
if let Some(hit) = self.resolve_exact(ch, prefer) {
return Some(hit);
}
char_alternate(ch).and_then(|alt| self.resolve_exact(alt, prefer))
}
fn resolve_exact(&self, ch: char, prefer: &[FaceId]) -> Option<(FaceId, u16)> {
for &id in prefer {
if let Some(font) = self.font(id) {
let gid = font.glyph_index(ch);
if gid != 0 {
return Some((id, gid));
}
}
}
for (i, font) in self.fonts.iter().enumerate() {
let gid = font.glyph_index(ch);
if gid != 0 {
return Some((FaceId(i), gid));
}
}
None
}
}
#[must_use]
pub fn char_alternate(ch: char) -> Option<char> {
Some(match ch {
'ϵ' => 'ε', 'ϑ' => 'θ',
'ϱ' => 'ρ',
'ϖ' => 'π',
'⌀' => '∅',
_ => return None,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct GlyphMetrics {
pub advance: f64,
pub height: f64,
pub depth: f64,
pub italic: f64,
}
#[must_use]
pub fn glyph_metrics(font: &fmd_font::Font, gid: u16) -> GlyphMetrics {
let upm = f64::from(font.units_per_em.max(1));
let advance = f64::from(font.advance_width(gid)) / upm;
let (height, depth, italic) = font.glyph_bbox(gid).map_or((0.0, 0.0, 0.0), |bbox| {
let xmax = f64::from(bbox[2]) / upm;
(
f64::from(bbox[3]).max(0.0) / upm,
(-f64::from(bbox[1])).max(0.0) / upm,
(xmax - advance).max(0.0),
)
});
GlyphMetrics {
advance,
height,
depth,
italic,
}
}
#[must_use]
pub fn kern_em(font: &fmd_font::Font, left: u16, right: u16) -> f64 {
let upm = f64::from(font.units_per_em.max(1));
f64::from(font.kerning_between_glyphs(left, right)) / upm
}
#[must_use]
pub fn default_math_chain(ch: char) -> &'static [FaceId] {
let italicized = ch.is_ascii_alphabetic()
|| ('α'..='ω').contains(&ch)
|| ch == 'ϵ'
|| ch == 'ϑ'
|| ch == 'ϖ'
|| ch == 'ϱ'
|| ch == 'ς'
|| ch == 'φ'
|| ch == 'ϕ';
if italicized {
&[FACE_ITALIC, FACE_REGULAR, FACE_SYMBOLS]
} else {
&[FACE_REGULAR, FACE_SYMBOLS, FACE_ITALIC]
}
}
#[must_use]
pub fn alphabet_map(font: MathFont, ch: char) -> (char, &'static [FaceId]) {
match font {
MathFont::Roman => (ch, &[FACE_REGULAR, FACE_SYMBOLS]),
MathFont::Bold => (ch, &[FACE_BOLD, FACE_REGULAR, FACE_SYMBOLS]),
MathFont::Italic => (ch, &[FACE_ITALIC, FACE_REGULAR, FACE_SYMBOLS]),
MathFont::BoldItalic => (
ch,
&[FACE_BOLD_ITALIC, FACE_BOLD, FACE_ITALIC, FACE_REGULAR],
),
MathFont::Typewriter => (ch, &[FACE_TYPEWRITER, FACE_REGULAR]),
MathFont::SansSerif => (ch, &[FACE_SANS, FACE_REGULAR]),
MathFont::Blackboard => (
blackboard_char(ch),
&[FACE_SYMBOLS, FACE_REGULAR, FACE_ITALIC],
),
MathFont::Calligraphic => (
calligraphic_char(ch),
&[FACE_SYMBOLS, FACE_ITALIC, FACE_REGULAR],
),
}
}
#[must_use]
pub fn blackboard_char(ch: char) -> char {
match ch {
'C' => 'ℂ',
'H' => 'ℍ',
'N' => 'ℕ',
'P' => 'ℙ',
'Q' => 'ℚ',
'R' => 'ℝ',
'Z' => 'ℤ',
'A'..='Z' => offset_char(0x1D538, ch, 'A'),
'a'..='z' => offset_char(0x1D552, ch, 'a'),
'0'..='9' => offset_char(0x1D7D8, ch, '0'),
other => other,
}
}
#[must_use]
pub fn calligraphic_char(ch: char) -> char {
match ch {
'B' => 'ℬ',
'E' => 'ℰ',
'F' => 'ℱ',
'H' => 'ℋ',
'I' => 'ℐ',
'L' => 'ℒ',
'M' => 'ℳ',
'R' => 'ℛ',
'e' => 'ℯ',
'g' => 'ℊ',
'o' => 'ℴ',
'A'..='Z' => offset_char(0x1D49C, ch, 'A'),
'a'..='z' => offset_char(0x1D4B6, ch, 'a'),
other => other,
}
}
fn offset_char(base: u32, ch: char, zero: char) -> char {
char::from_u32(base + (ch as u32) - (zero as u32)).unwrap_or(ch)
}
#[must_use]
pub fn accent_spacing_fallback(combining: char) -> Option<char> {
Some(match combining {
'\u{0302}' => '\u{02C6}', '\u{0303}' => '\u{02DC}', '\u{0304}' => '\u{00AF}', '\u{0306}' => '\u{02D8}', '\u{0307}' => '\u{02D9}', '\u{0308}' => '\u{00A8}', '\u{030A}' => '\u{02DA}', '\u{030C}' => '\u{02C7}', '\u{0301}' => '\u{00B4}', '\u{0300}' => '`', '\u{20D7}' => '\u{2192}', _ => return None,
})
}
#[cfg(all(test, feature = "bundled-faces"))]
#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn bundled_roster_parses_and_resolves_the_basics() {
let faces = match FaceSet::bundled() {
Ok(f) => f,
Err(e) => panic!("bundled faces: {e}"),
};
assert_eq!(faces.len(), 7);
let Some((face, gid)) = faces.resolve('x', default_math_chain('x')) else {
panic!("x must resolve");
};
assert_eq!(face, FACE_ITALIC);
assert_ne!(gid, 0);
let Some((face, _)) = faces.resolve('7', default_math_chain('7')) else {
panic!("7 must resolve");
};
assert_eq!(face, FACE_REGULAR);
}
#[test]
fn metrics_are_sane_for_x() {
let faces = match FaceSet::bundled() {
Ok(f) => f,
Err(e) => panic!("bundled faces: {e}"),
};
let Some((face, gid)) = faces.resolve('x', &[FACE_REGULAR]) else {
panic!("x in regular");
};
let Some(font) = faces.font(face) else {
panic!("face")
};
let m = glyph_metrics(font, gid);
assert!(m.advance > 0.3 && m.advance < 0.7, "{m:?}");
assert!(
(m.height - crate::metrics::CM.x_height).abs() < 0.002,
"measured x-height {} vs σ5 {}",
m.height,
crate::metrics::CM.x_height
);
}
}