keyset_font/
lib.rs

1//! This crate contains the font loading and parsing logic used internally by [keyset].
2//!
3//! [keyset]: https://crates.io/crates/keyset
4
5#![warn(
6    missing_docs,
7    clippy::all,
8    clippy::correctness,
9    clippy::suspicious,
10    clippy::style,
11    clippy::complexity,
12    clippy::perf,
13    clippy::pedantic,
14    clippy::cargo,
15    clippy::nursery
16)]
17
18mod default;
19mod error;
20mod face;
21
22use std::fmt::Debug;
23use std::sync::OnceLock;
24
25use dashmap::DashMap;
26use geom::{BezPath, Rect, Shape};
27use log::warn;
28use ttf_parser::GlyphId;
29
30pub use self::error::{Error, Result};
31use face::Face;
32
33/// A glyph loaded from a [`Font`]
34#[derive(Debug, Clone)]
35#[non_exhaustive]
36pub struct Glyph {
37    /// The outline of the glyph
38    pub path: BezPath,
39    /// The bounds of the glyph's outline. The value of `glyph.bounds` is equivalent to the result
40    /// of `glyph.path.bounding_box()`
41    pub bounds: Rect,
42    /// The glyphs horizontal advance
43    pub advance: f64,
44}
45
46impl Glyph {
47    fn parse_from(face: &Face, gid: GlyphId) -> Option<Self> {
48        struct BezPathBuilder(BezPath);
49
50        // GRCOV_EXCL_START // TODO these are pretty trivial but we could cover them in tests
51        impl ttf_parser::OutlineBuilder for BezPathBuilder {
52            fn move_to(&mut self, x: f32, y: f32) {
53                // Y axis is flipped in fonts compared to SVGs
54                self.0.move_to((x.into(), (-y).into()));
55            }
56
57            fn line_to(&mut self, x: f32, y: f32) {
58                // Y axis is flipped in fonts compared to SVGs
59                self.0.line_to((x.into(), (-y).into()));
60            }
61
62            fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
63                // Y axis is flipped in fonts compared to SVGs
64                self.0
65                    .quad_to((x1.into(), (-y1).into()), (x.into(), (-y).into()));
66            }
67
68            fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
69                // Y axis is flipped in fonts compared to SVGs
70                self.0.curve_to(
71                    (x1.into(), (-y1).into()),
72                    (x2.into(), (-y2).into()),
73                    (x.into(), (-y).into()),
74                );
75            }
76
77            fn close(&mut self) {
78                self.0.close_path();
79            }
80        }
81        // GRCOV_EXCL_STOP
82
83        let mut builder = BezPathBuilder(BezPath::new());
84
85        let bounds = face.outline_glyph(gid, &mut builder)?;
86        let path = builder.0;
87
88        let bounds = Rect::new(
89            f64::from(bounds.x_min),
90            f64::from(bounds.y_min),
91            f64::from(bounds.x_max),
92            f64::from(bounds.y_max),
93        );
94
95        let advance = f64::from(face.glyph_hor_advance(gid)?);
96
97        Some(Self {
98            path,
99            bounds,
100            advance,
101        })
102    }
103}
104
105/// A parsed font
106#[derive(Clone)]
107pub struct Font {
108    face: Face,
109    family: OnceLock<String>,
110    name: OnceLock<String>,
111    cap_height: OnceLock<f64>,
112    x_height: OnceLock<f64>,
113    notdef: OnceLock<Glyph>,
114    glyphs: DashMap<char, Option<Glyph>>,
115    kerning: DashMap<(char, char), f64>,
116}
117
118impl Debug for Font {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_tuple("Font").field(self.name()).finish()
121    }
122}
123
124impl Default for Font {
125    fn default() -> Self {
126        Self::default_ref().clone()
127    }
128}
129
130impl Font {
131    /// Returns a static reference to the default font
132    ///
133    /// This is equivalent to calling [`Default::default`] but returns a reference and avoids
134    /// cloning any internal data
135    #[must_use]
136    pub fn default_ref() -> &'static Self {
137        default::font()
138    }
139
140    /// Parse a font from TrueType or OpenType format font data
141    ///
142    /// # Errors
143    ///
144    /// If there is an error parsing the font data
145    pub fn from_ttf(data: Vec<u8>) -> Result<Self> {
146        Ok(Self {
147            face: Face::from_ttf(data)?,
148            family: OnceLock::new(),
149            name: OnceLock::new(),
150            cap_height: OnceLock::new(),
151            x_height: OnceLock::new(),
152            notdef: OnceLock::new(),
153            glyphs: DashMap::new(),
154            kerning: DashMap::new(),
155        })
156    }
157
158    /// The font family name
159    ///
160    /// Returns `"unknown"` if the font does not specify a family name
161    pub fn family(&self) -> &String {
162        self.family.get_or_init(|| {
163            self.face
164                .names()
165                .into_iter()
166                .filter(|n| n.name_id == ttf_parser::name_id::FAMILY)
167                .find_map(|n| n.to_string())
168                .unwrap_or_else(|| {
169                    warn!("cannot read font family name");
170                    "unknown".to_owned()
171                })
172        })
173    }
174
175    /// The font's full name
176    ///
177    /// Returns `"unknown"` if the font does not specify a full name
178    pub fn name(&self) -> &String {
179        self.name.get_or_init(|| {
180            self.face
181                .names()
182                .into_iter()
183                .filter(|n| n.name_id == ttf_parser::name_id::FULL_NAME)
184                .find_map(|n| n.to_string())
185                .unwrap_or_else(|| {
186                    warn!("cannot read font full name");
187                    "unknown".to_owned()
188                })
189        })
190    }
191
192    /// The number font units per EM
193    pub fn em_size(&self) -> f64 {
194        f64::from(self.face.units_per_em())
195    }
196
197    /// The capital height in font units
198    ///
199    /// Measures the height of the uppercase `'M'` if it is not set. In case the font does not contain
200    /// an uppercase `'M'`, a default value is returned
201    pub fn cap_height(&self) -> f64 {
202        *self.cap_height.get_or_init(|| {
203            self.face
204                .capital_height()
205                .map(f64::from)
206                .or_else(|| self.glyph('M').map(|g| g.path.bounding_box().height()))
207                .unwrap_or_else(|| {
208                    default::cap_height() / default::line_height() * self.line_height()
209                })
210        })
211    }
212
213    /// The x-height in font units
214    ///
215    /// Measures the height of the lowercase `'x'` if it is not set. In case the font does not contain
216    /// a lowercase `'x'`, a default value is returned
217    pub fn x_height(&self) -> f64 {
218        *self.x_height.get_or_init(|| {
219            self.face
220                .x_height()
221                .map(f64::from)
222                .or_else(|| self.glyph('x').map(|g| g.path.bounding_box().height()))
223                .unwrap_or_else(|| {
224                    default::x_height() / default::line_height() * self.line_height()
225                })
226        })
227    }
228
229    /// The font's ascender in font units
230    pub fn ascender(&self) -> f64 {
231        f64::from(self.face.ascender())
232    }
233
234    /// The font's descender in font units
235    pub fn descender(&self) -> f64 {
236        -f64::from(self.face.descender())
237    }
238
239    /// The font's line gap in font units
240    pub fn line_gap(&self) -> f64 {
241        f64::from(self.face.line_gap())
242    }
243
244    /// The font's line height in font units
245    ///
246    /// This is equal to `self.ascender() + self.descender() + self.line_gap()`
247    pub fn line_height(&self) -> f64 {
248        self.ascender() + self.descender() + self.line_gap()
249    }
250
251    /// The font's slope angle in clockwise degrees if specified
252    pub fn slope(&self) -> Option<f64> {
253        self.face
254            .italic_angle()
255            .map(f64::from)
256            .map(std::ops::Neg::neg) // Negate so forward = positive
257    }
258
259    /// The number of glyph outlines in the font
260    pub fn num_glyphs(&self) -> usize {
261        usize::from(self.face.number_of_glyphs())
262    }
263
264    /// Returns the flyph for a given character if present in the font
265    #[allow(clippy::missing_panics_doc)] // Only unwrapping a mutex
266    pub fn glyph(&self, char: char) -> Option<Glyph> {
267        self.glyphs
268            .entry(char)
269            .or_insert_with(|| {
270                self.face
271                    .glyph_index(char)
272                    .and_then(|gid| Glyph::parse_from(&self.face, gid))
273            })
274            .clone()
275    }
276
277    /// Returns the glyph for a given character, or the default replacement character if not present
278    pub fn glyph_or_default(&self, char: char) -> Glyph {
279        self.glyph(char).unwrap_or_else(|| self.notdef())
280    }
281
282    /// Returns the font's default replacement glyph, `.notdef`, or a builtin default if not present
283    pub fn notdef(&self) -> Glyph {
284        self.notdef
285            .get_or_init(|| {
286                Glyph::parse_from(&self.face, GlyphId(0)).unwrap_or_else(|| {
287                    warn!("no valid outline for glyph .notdef in font");
288                    default::notdef()
289                })
290            })
291            .clone()
292    }
293
294    /// Returns the kerning between two characters' glyphs, or 0 if no kerning is specified in the
295    /// font
296    #[allow(clippy::missing_panics_doc)] // Only unwrapping a mutex
297    pub fn kerning(&self, left: char, right: char) -> f64 {
298        *self.kerning.entry((left, right)).or_insert_with(|| {
299            if let (Some(lhs), Some(rhs)) =
300                (self.face.glyph_index(left), self.face.glyph_index(right))
301            {
302                self.face.glyphs_kerning(lhs, rhs).map_or(0.0, f64::from)
303            } else {
304                0.0
305            }
306        })
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use assert_approx_eq::assert_approx_eq;
313    use assert_matches::assert_matches;
314
315    use super::*;
316
317    #[test]
318    fn glyph_parse_from() {
319        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
320        let face = Face::from_ttf(data).unwrap();
321
322        let a = Glyph::parse_from(&face, GlyphId(1)).unwrap();
323        assert_approx_eq!(a.advance, 540.0);
324        assert_approx_eq!(a.bounds.width(), 535.0);
325        assert_approx_eq!(a.bounds.height(), 656.0);
326
327        let v = Glyph::parse_from(&face, GlyphId(2)).unwrap();
328        assert_approx_eq!(v.advance, 540.0);
329        assert_approx_eq!(v.bounds.width(), 535.0);
330        assert_approx_eq!(v.bounds.height(), 656.0);
331    }
332
333    #[test]
334    fn font_debug() {
335        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
336        let font = Font::from_ttf(data).unwrap();
337
338        assert_eq!(format!("{:?}", font), r#"Font("demo regular")"#);
339    }
340
341    #[test]
342    fn font_clone() {
343        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
344        let font = Font::from_ttf(data).unwrap();
345
346        let _ = font.clone(); // Shouldn't panic
347    }
348
349    #[test]
350    fn font_default() {
351        let _ = Font::default(); // Shouldn't panic
352    }
353
354    #[test]
355    fn font_from_ttf() {
356        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
357        let font = Font::from_ttf(data).unwrap();
358
359        assert_matches!(font.face, Face { .. });
360        assert!(font.family.get().is_none());
361        assert!(font.name.get().is_none());
362        assert!(font.cap_height.get().is_none());
363        assert!(font.x_height.get().is_none());
364        assert!(font.notdef.get().is_none());
365        assert_eq!(font.glyphs.len(), 0);
366        assert_eq!(font.kerning.len(), 0);
367    }
368
369    #[test]
370    fn font_properties() {
371        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
372        let font = Font::from_ttf(data).unwrap();
373
374        assert_eq!(font.family(), "demo");
375        assert_eq!(font.name(), "demo regular");
376        assert_approx_eq!(font.em_size(), 1000.0);
377        assert_approx_eq!(font.cap_height(), 650.0);
378        assert_approx_eq!(font.x_height(), 450.0);
379        assert_approx_eq!(font.ascender(), 1024.0);
380        assert_approx_eq!(font.descender(), 400.0);
381        assert_approx_eq!(font.line_gap(), 0.0);
382        assert_approx_eq!(font.line_height(), 1424.0);
383        assert_eq!(font.slope(), None);
384        assert_eq!(font.num_glyphs(), 3);
385
386        let data = std::fs::read(env!("NULL_TTF")).unwrap();
387        let font = Font::from_ttf(data).unwrap();
388
389        let line_scaling = font.line_height() / default::line_height();
390        assert_eq!(font.family(), "unknown");
391        assert_eq!(font.name(), "unknown");
392        assert_approx_eq!(font.em_size(), 1000.0);
393        assert_approx_eq!(font.cap_height(), default::cap_height() * line_scaling);
394        assert_approx_eq!(font.x_height(), default::x_height() * line_scaling);
395        assert_approx_eq!(font.ascender(), 600.0);
396        assert_approx_eq!(font.descender(), 400.0);
397        assert_approx_eq!(font.line_gap(), 200.0);
398        assert_approx_eq!(font.line_height(), 1200.0);
399        assert_eq!(font.slope(), None);
400        assert_eq!(font.num_glyphs(), 1); // Just .notdef
401    }
402
403    #[test]
404    fn font_glyph() {
405        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
406        let font = Font::from_ttf(data).unwrap();
407
408        assert!(font.glyph('A').is_some());
409        assert!(font.glyph('B').is_none());
410    }
411
412    #[test]
413    fn font_glyph_or_default() {
414        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
415        let font = Font::from_ttf(data).unwrap();
416
417        assert_ne!(font.glyph_or_default('A').path, font.notdef().path);
418        assert_eq!(font.glyph_or_default('B').path, font.notdef().path);
419    }
420
421    #[test]
422    fn font_notdef() {
423        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
424        let font = Font::from_ttf(data).unwrap();
425
426        assert_eq!(font.notdef().path.elements().len(), 12);
427
428        let data = std::fs::read(env!("NULL_TTF")).unwrap();
429        let font = Font::from_ttf(data).unwrap();
430
431        assert_eq!(font.notdef().path.elements().len(), 26);
432    }
433
434    #[test]
435    fn font_kerning() {
436        let data = std::fs::read(env!("DEMO_TTF")).unwrap();
437        let font = Font::from_ttf(data).unwrap();
438
439        assert_approx_eq!(font.kerning('A', 'V'), -70.0);
440        assert_approx_eq!(font.kerning('A', 'B'), 0.0);
441    }
442}