1use crate::mbox::FaceId;
20use crate::node::MathFont;
21
22pub struct FaceSet {
26 fonts: Vec<fmd_font::Font>,
27}
28
29pub const FACE_REGULAR: FaceId = FaceId(0);
31pub const FACE_ITALIC: FaceId = FaceId(1);
33pub const FACE_BOLD: FaceId = FaceId(2);
35pub const FACE_BOLD_ITALIC: FaceId = FaceId(3);
37pub const FACE_TYPEWRITER: FaceId = FaceId(4);
39pub const FACE_SANS: FaceId = FaceId(5);
41pub const FACE_SYMBOLS: FaceId = FaceId(6);
43
44impl FaceSet {
45 #[must_use]
48 pub fn from_fonts(fonts: Vec<fmd_font::Font>) -> Self {
49 Self { fonts }
50 }
51
52 #[cfg(feature = "bundled-faces")]
60 pub fn bundled() -> Result<Self, fmd_font::FontError> {
61 let fonts = vec![
62 fmd_font::Font::parse(fmd_font::bundled::CM_REGULAR.to_vec())?,
63 fmd_font::Font::parse(fmd_font::bundled::CM_ITALIC.to_vec())?,
64 fmd_font::Font::parse(fmd_font::bundled::CM_BOLD.to_vec())?,
65 fmd_font::Font::parse(fmd_font::bundled::CM_BOLD_ITALIC.to_vec())?,
66 fmd_font::Font::parse(fmd_font::bundled::CM_TYPEWRITER.to_vec())?,
67 fmd_font::Font::parse(fmd_font::bundled::PLEX_REGULAR.to_vec())?,
68 fmd_font::Font::parse(fmd_font::bundled::NOTO_SANS_MATH_SYMBOLS.to_vec())?,
69 ];
70 Ok(Self::from_fonts(fonts))
71 }
72
73 #[must_use]
75 pub fn font(&self, id: FaceId) -> Option<&fmd_font::Font> {
76 self.fonts.get(id.0)
77 }
78
79 #[must_use]
81 pub fn len(&self) -> usize {
82 self.fonts.len()
83 }
84
85 #[must_use]
87 pub fn is_empty(&self) -> bool {
88 self.fonts.is_empty()
89 }
90
91 #[must_use]
95 pub fn resolve(&self, ch: char, prefer: &[FaceId]) -> Option<(FaceId, u16)> {
96 if let Some(hit) = self.resolve_exact(ch, prefer) {
97 return Some(hit);
98 }
99 char_alternate(ch).and_then(|alt| self.resolve_exact(alt, prefer))
100 }
101
102 fn resolve_exact(&self, ch: char, prefer: &[FaceId]) -> Option<(FaceId, u16)> {
103 for &id in prefer {
104 if let Some(font) = self.font(id) {
105 let gid = font.glyph_index(ch);
106 if gid != 0 {
107 return Some((id, gid));
108 }
109 }
110 }
111 for (i, font) in self.fonts.iter().enumerate() {
112 let gid = font.glyph_index(ch);
113 if gid != 0 {
114 return Some((FaceId(i), gid));
115 }
116 }
117 None
118 }
119}
120
121#[must_use]
128pub fn char_alternate(ch: char) -> Option<char> {
129 Some(match ch {
130 'ϵ' => 'ε', 'ϑ' => 'θ',
132 'ϱ' => 'ρ',
133 'ϖ' => 'π',
134 '⌀' => '∅',
135 _ => return None,
136 })
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Default)]
141pub struct GlyphMetrics {
142 pub advance: f64,
144 pub height: f64,
146 pub depth: f64,
148 pub italic: f64,
150}
151
152#[must_use]
154pub fn glyph_metrics(font: &fmd_font::Font, gid: u16) -> GlyphMetrics {
155 let upm = f64::from(font.units_per_em.max(1));
156 let advance = f64::from(font.advance_width(gid)) / upm;
157 let (height, depth, italic) = font.glyph_bbox(gid).map_or((0.0, 0.0, 0.0), |bbox| {
158 let xmax = f64::from(bbox[2]) / upm;
159 (
160 f64::from(bbox[3]).max(0.0) / upm,
161 (-f64::from(bbox[1])).max(0.0) / upm,
162 (xmax - advance).max(0.0),
163 )
164 });
165 GlyphMetrics {
166 advance,
167 height,
168 depth,
169 italic,
170 }
171}
172
173#[must_use]
175pub fn kern_em(font: &fmd_font::Font, left: u16, right: u16) -> f64 {
176 let upm = f64::from(font.units_per_em.max(1));
177 f64::from(font.kerning_between_glyphs(left, right)) / upm
178}
179
180#[must_use]
185pub fn default_math_chain(ch: char) -> &'static [FaceId] {
186 let italicized = ch.is_ascii_alphabetic()
187 || ('α'..='ω').contains(&ch)
188 || ch == 'ϵ'
189 || ch == 'ϑ'
190 || ch == 'ϖ'
191 || ch == 'ϱ'
192 || ch == 'ς'
193 || ch == 'φ'
194 || ch == 'ϕ';
195 if italicized {
196 &[FACE_ITALIC, FACE_REGULAR, FACE_SYMBOLS]
197 } else {
198 &[FACE_REGULAR, FACE_SYMBOLS, FACE_ITALIC]
199 }
200}
201
202#[must_use]
208pub fn alphabet_map(font: MathFont, ch: char) -> (char, &'static [FaceId]) {
209 match font {
210 MathFont::Roman => (ch, &[FACE_REGULAR, FACE_SYMBOLS]),
211 MathFont::Bold => (ch, &[FACE_BOLD, FACE_REGULAR, FACE_SYMBOLS]),
212 MathFont::Italic => (ch, &[FACE_ITALIC, FACE_REGULAR, FACE_SYMBOLS]),
213 MathFont::BoldItalic => (
214 ch,
215 &[FACE_BOLD_ITALIC, FACE_BOLD, FACE_ITALIC, FACE_REGULAR],
216 ),
217 MathFont::Typewriter => (ch, &[FACE_TYPEWRITER, FACE_REGULAR]),
218 MathFont::SansSerif => (ch, &[FACE_SANS, FACE_REGULAR]),
219 MathFont::Blackboard => (
220 blackboard_char(ch),
221 &[FACE_SYMBOLS, FACE_REGULAR, FACE_ITALIC],
222 ),
223 MathFont::Calligraphic => (
224 calligraphic_char(ch),
225 &[FACE_SYMBOLS, FACE_ITALIC, FACE_REGULAR],
226 ),
227 }
228}
229
230#[must_use]
234pub fn blackboard_char(ch: char) -> char {
235 match ch {
236 'C' => 'ℂ',
237 'H' => 'ℍ',
238 'N' => 'ℕ',
239 'P' => 'ℙ',
240 'Q' => 'ℚ',
241 'R' => 'ℝ',
242 'Z' => 'ℤ',
243 'A'..='Z' => offset_char(0x1D538, ch, 'A'),
244 'a'..='z' => offset_char(0x1D552, ch, 'a'),
245 '0'..='9' => offset_char(0x1D7D8, ch, '0'),
246 other => other,
247 }
248}
249
250#[must_use]
253pub fn calligraphic_char(ch: char) -> char {
254 match ch {
255 'B' => 'ℬ',
256 'E' => 'ℰ',
257 'F' => 'ℱ',
258 'H' => 'ℋ',
259 'I' => 'ℐ',
260 'L' => 'ℒ',
261 'M' => 'ℳ',
262 'R' => 'ℛ',
263 'e' => 'ℯ',
264 'g' => 'ℊ',
265 'o' => 'ℴ',
266 'A'..='Z' => offset_char(0x1D49C, ch, 'A'),
267 'a'..='z' => offset_char(0x1D4B6, ch, 'a'),
268 other => other,
269 }
270}
271
272fn offset_char(base: u32, ch: char, zero: char) -> char {
273 char::from_u32(base + (ch as u32) - (zero as u32)).unwrap_or(ch)
274}
275
276#[must_use]
280pub fn accent_spacing_fallback(combining: char) -> Option<char> {
281 Some(match combining {
282 '\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,
294 })
295}
296
297#[cfg(all(test, feature = "bundled-faces"))]
298#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn bundled_roster_parses_and_resolves_the_basics() {
304 let faces = match FaceSet::bundled() {
305 Ok(f) => f,
306 Err(e) => panic!("bundled faces: {e}"),
307 };
308 assert_eq!(faces.len(), 7);
309 let Some((face, gid)) = faces.resolve('x', default_math_chain('x')) else {
311 panic!("x must resolve");
312 };
313 assert_eq!(face, FACE_ITALIC);
314 assert_ne!(gid, 0);
315 let Some((face, _)) = faces.resolve('7', default_math_chain('7')) else {
316 panic!("7 must resolve");
317 };
318 assert_eq!(face, FACE_REGULAR);
319 }
320
321 #[test]
322 fn metrics_are_sane_for_x() {
323 let faces = match FaceSet::bundled() {
324 Ok(f) => f,
325 Err(e) => panic!("bundled faces: {e}"),
326 };
327 let Some((face, gid)) = faces.resolve('x', &[FACE_REGULAR]) else {
328 panic!("x in regular");
329 };
330 let Some(font) = faces.font(face) else {
331 panic!("face")
332 };
333 let m = glyph_metrics(font, gid);
334 assert!(m.advance > 0.3 && m.advance < 0.7, "{m:?}");
335 assert!(
338 (m.height - crate::metrics::CM.x_height).abs() < 0.002,
339 "measured x-height {} vs σ5 {}",
340 m.height,
341 crate::metrics::CM.x_height
342 );
343 }
344}