Skip to main content

typf_fontdb/
lib.rs

1//! Font loading and face management for Typf.
2//!
3//! This crate turns raw font files into face objects that the rest of the
4//! pipeline can query. It keeps the original bytes in memory and creates parser
5//! views on demand, which is important for two reasons:
6//!
7//! - it avoids leaking long-lived parser objects,
8//! - it supports collection files such as TTCs, where one file contains several
9//!   faces and each face needs its own index.
10
11use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16use read_fonts::{FontRef as ReadFontRef, TableProvider};
17
18use typf_core::{
19    error::{FontLoadError, Result},
20    traits::FontRef as TypfFontRef,
21    types::{FontMetrics, VariationAxis},
22};
23
24/// Source descriptor for one loaded font face.
25#[derive(Clone, Debug)]
26pub struct TypfFontSource {
27    path: Option<PathBuf>,
28    face_index: u32,
29}
30
31impl TypfFontSource {
32    pub fn new(path: Option<PathBuf>, face_index: u32) -> Self {
33        Self { path, face_index }
34    }
35
36    pub fn path(&self) -> Option<&Path> {
37        self.path.as_deref()
38    }
39
40    pub fn face_index(&self) -> u32 {
41        self.face_index
42    }
43}
44
45/// In-memory font face ready for shaping and rendering.
46///
47/// The face keeps the original font bytes and recreates parser views on demand.
48/// For collection files such as TTCs, `face_index` selects the face inside the
49/// shared file.
50pub struct TypfFontFace {
51    data: Arc<Vec<u8>>,
52    source: TypfFontSource,
53    units_per_em: u16,
54    metrics: FontMetrics,
55}
56
57impl TypfFontFace {
58    /// Load the first face from a font file on disk.
59    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
60        Self::from_file_index(path, 0)
61    }
62
63    /// Load a specific face from a font file or collection.
64    pub fn from_file_index(path: impl AsRef<Path>, face_index: u32) -> Result<Self> {
65        let data = fs::read(path.as_ref())
66            .map_err(|_| FontLoadError::FileNotFound(path.as_ref().display().to_string()))?;
67
68        Self::from_data_index_with_path(data, face_index, Some(path.as_ref().to_path_buf()))
69    }
70
71    /// Load the first face from raw font bytes.
72    pub fn from_data(data: Vec<u8>) -> Result<Self> {
73        Self::from_data_index(data, 0)
74    }
75
76    /// Load a specific face from raw font bytes.
77    pub fn from_data_index(data: Vec<u8>, face_index: u32) -> Result<Self> {
78        Self::from_data_index_with_path(data, face_index, None)
79    }
80
81    fn from_data_index_with_path(
82        data: Vec<u8>,
83        face_index: u32,
84        path: Option<PathBuf>,
85    ) -> Result<Self> {
86        let font_ref = ReadFontRef::from_index(data.as_slice(), face_index)
87            .map_err(|_| FontLoadError::InvalidData)?;
88
89        let units_per_em = font_ref
90            .head()
91            .map(|head| head.units_per_em())
92            .unwrap_or(1000);
93
94        let (ascent, descent, line_gap) = font_ref
95            .os2()
96            .ok()
97            .map(|os2| {
98                (
99                    os2.s_typo_ascender(),
100                    os2.s_typo_descender(),
101                    os2.s_typo_line_gap(),
102                )
103            })
104            .or_else(|| {
105                font_ref.hhea().ok().map(|hhea| {
106                    (
107                        hhea.ascender().to_i16(),
108                        hhea.descender().to_i16(),
109                        hhea.line_gap().to_i16(),
110                    )
111                })
112            })
113            .unwrap_or((0, 0, 0));
114
115        Ok(TypfFontFace {
116            data: Arc::new(data),
117            source: TypfFontSource::new(path, face_index),
118            units_per_em,
119            metrics: FontMetrics {
120                units_per_em,
121                ascent,
122                descent,
123                line_gap,
124            },
125        })
126    }
127
128    pub fn source(&self) -> &TypfFontSource {
129        &self.source
130    }
131
132    pub fn face_index(&self) -> u32 {
133        self.source.face_index
134    }
135
136    pub fn path(&self) -> Option<&Path> {
137        self.source.path()
138    }
139
140    fn font_ref(&self) -> Option<ReadFontRef<'_>> {
141        ReadFontRef::from_index(self.data.as_slice(), self.source.face_index).ok()
142    }
143
144    pub fn glyph_id(&self, ch: char) -> Option<u32> {
145        self.font_ref()
146            .and_then(|font| font.cmap().ok()?.map_codepoint(ch).map(|gid| gid.to_u32()))
147    }
148
149    /// Return the advance width normalized to a 1000-unit em.
150    ///
151    /// Source fonts can use different units-per-em values. This method returns
152    /// a stable scale for callers that do not want to repeat that conversion.
153    pub fn advance_width(&self, glyph_id: u32) -> f32 {
154        self.font_ref()
155            .and_then(|font| {
156                let hmtx = font.hmtx().ok()?;
157
158                use read_fonts::types::GlyphId;
159                let glyph = GlyphId::new(glyph_id);
160                let advance = hmtx.advance(glyph)?;
161
162                let upem = self.units_per_em as f32;
163                Some(advance as f32 / upem * 1000.0)
164            })
165            .unwrap_or(500.0)
166    }
167
168    pub fn glyph_count(&self) -> Option<u32> {
169        self.font_ref()
170            .and_then(|font| font.maxp().ok().map(|maxp| maxp.num_glyphs() as u32))
171    }
172
173    /// Returns variable font axes from the fvar table.
174    pub fn variation_axes(&self) -> Option<Vec<VariationAxis>> {
175        let font = self.font_ref()?;
176        let fvar = font.fvar().ok()?;
177        let axes_slice = fvar.axes().ok()?;
178        let name_table = font.name().ok();
179
180        let axes: Vec<VariationAxis> = axes_slice
181            .iter()
182            .map(|axis| {
183                let tag_bytes = axis.axis_tag().into_bytes();
184                let tag = String::from_utf8_lossy(&tag_bytes).to_string();
185
186                let name = name_table.as_ref().and_then(|nt| {
187                    let name_id = axis.axis_name_id();
188                    nt.name_record()
189                        .iter()
190                        .find(|record| record.name_id() == name_id)
191                        .and_then(|record| record.string(nt.string_data()).ok())
192                        .map(|s| s.to_string())
193                });
194
195                let hidden = axis.flags() & 0x0001 != 0;
196
197                VariationAxis {
198                    tag,
199                    name,
200                    min_value: axis.min_value().to_f32(),
201                    default_value: axis.default_value().to_f32(),
202                    max_value: axis.max_value().to_f32(),
203                    hidden,
204                }
205            })
206            .collect();
207
208        Some(axes)
209    }
210}
211
212impl TypfFontRef for TypfFontFace {
213    fn data(&self) -> &[u8] {
214        self.data.as_slice()
215    }
216
217    fn data_shared(&self) -> Option<Arc<dyn AsRef<[u8]> + Send + Sync>> {
218        Some(self.data.clone())
219    }
220
221    fn units_per_em(&self) -> u16 {
222        self.units_per_em
223    }
224
225    fn metrics(&self) -> Option<FontMetrics> {
226        Some(self.metrics)
227    }
228
229    fn glyph_id(&self, ch: char) -> Option<u32> {
230        self.glyph_id(ch)
231    }
232
233    fn advance_width(&self, glyph_id: u32) -> f32 {
234        self.advance_width(glyph_id)
235    }
236
237    fn glyph_count(&self) -> Option<u32> {
238        self.glyph_count()
239    }
240
241    fn variation_axes(&self) -> Option<Vec<VariationAxis>> {
242        self.variation_axes()
243    }
244}
245
246/// Collection of loaded font faces and their source metadata.
247pub struct FontDatabase {
248    fonts: Vec<Arc<TypfFontFace>>,
249    sources: Vec<TypfFontSource>,
250    path_cache: HashMap<(PathBuf, u32), Arc<TypfFontFace>>,
251    default_font: Option<Arc<TypfFontFace>>,
252}
253
254impl FontDatabase {
255    /// Create an empty font database.
256    pub fn new() -> Self {
257        Self {
258            fonts: Vec::new(),
259            sources: Vec::new(),
260            path_cache: HashMap::new(),
261            default_font: None,
262        }
263    }
264
265    /// Load the first face from a file and reuse a cached copy when possible.
266    pub fn load_font(&mut self, path: impl AsRef<Path>) -> Result<Arc<TypfFontFace>> {
267        let path = path.as_ref();
268
269        let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
270        let face_index = 0;
271        let cache_key = (canonical.clone(), face_index);
272
273        if let Some(font) = self.path_cache.get(&cache_key) {
274            return Ok(font.clone());
275        }
276
277        let font = Arc::new(TypfFontFace::from_file(path)?);
278        self.path_cache.insert(cache_key, font.clone());
279        self.fonts.push(font.clone());
280        self.sources
281            .push(TypfFontSource::new(Some(canonical), face_index));
282
283        if self.default_font.is_none() {
284            self.default_font = Some(font.clone());
285        }
286
287        Ok(font)
288    }
289
290    pub fn load_font_data(&mut self, data: Vec<u8>) -> Result<Arc<TypfFontFace>> {
291        let font = Arc::new(TypfFontFace::from_data(data)?);
292        self.fonts.push(font.clone());
293        self.sources
294            .push(TypfFontSource::new(None, font.face_index()));
295
296        if self.default_font.is_none() {
297            self.default_font = Some(font.clone());
298        }
299
300        Ok(font)
301    }
302
303    pub fn default_font(&self) -> Option<Arc<TypfFontFace>> {
304        self.default_font.clone()
305    }
306
307    pub fn fonts(&self) -> &[Arc<TypfFontFace>] {
308        &self.fonts
309    }
310
311    pub fn sources(&self) -> &[TypfFontSource] {
312        &self.sources
313    }
314
315    /// Temporary lookup stub.
316    ///
317    /// This currently returns the default font instead of performing a real
318    /// family-name search.
319    pub fn find_font(&self, _name: &str) -> Option<Arc<TypfFontFace>> {
320        self.default_font.clone()
321    }
322
323    pub fn clear(&mut self) {
324        self.fonts.clear();
325        self.sources.clear();
326        self.path_cache.clear();
327        self.default_font = None;
328    }
329
330    pub fn font_count(&self) -> usize {
331        self.fonts.len()
332    }
333}
334
335impl Default for FontDatabase {
336    fn default() -> Self {
337        Self::new()
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use typf_core::traits::FontRef;
345
346    #[test]
347    fn test_empty_database() {
348        let db = FontDatabase::new();
349        assert!(db.default_font().is_none());
350        assert_eq!(db.fonts().len(), 0);
351        assert_eq!(db.sources().len(), 0);
352        assert_eq!(db.font_count(), 0);
353    }
354
355    #[test]
356    fn test_font_from_data() {
357        // Create a minimal font data (empty for test)
358        let data = vec![0; 100];
359        let result = TypfFontFace::from_data(data);
360        // This will fail with invalid data, which is expected
361        assert!(result.is_err());
362    }
363
364    #[test]
365    fn test_font_from_data_index_invalid() {
366        // Invalid face index should fail
367        let data = vec![0; 100];
368        let result = TypfFontFace::from_data_index(data, 5);
369        assert!(result.is_err());
370    }
371
372    #[test]
373    fn test_clear_database() {
374        let mut db = FontDatabase::new();
375        // After clear, database should be empty
376        db.clear();
377        assert!(db.default_font().is_none());
378        assert_eq!(db.sources().len(), 0);
379        assert_eq!(db.font_count(), 0);
380    }
381
382    #[test]
383    fn test_face_index_default() {
384        // When from_data fails, we can't test face_index, but we can verify
385        // the API exists and returns 0 for default construction path
386        let data = vec![0; 100];
387        let result = TypfFontFace::from_data_index(data, 0);
388        // Invalid data, but we're testing the API structure
389        assert!(result.is_err());
390    }
391
392    #[test]
393    fn test_typf_font_face_data_shared_when_loaded_then_some() {
394        let font_path = concat!(
395            env!("CARGO_MANIFEST_DIR"),
396            "/../../test-fonts/NotoSans-Regular.ttf"
397        );
398        let Ok(font) = TypfFontFace::from_file(font_path) else {
399            return;
400        };
401
402        let shared = font.data_shared();
403        assert!(
404            shared.is_some(),
405            "TypfFontFace should provide shared font bytes to avoid copies"
406        );
407
408        if let Some(shared) = shared {
409            assert_eq!(
410                shared.as_ref().as_ref(),
411                font.data(),
412                "shared bytes must match FontRef::data()"
413            );
414        }
415    }
416}