Skip to main content

typst_kit/
fonts.rs

1//! Font loading and management.
2//!
3//! This provides implementations to discover fonts [in directories](scan) and
4//! [from system](system) and can also serve standard [embedded] fonts.
5
6use std::any::Any;
7use std::fs;
8use std::path::PathBuf;
9use std::sync::OnceLock;
10
11use typst_library::foundations::Bytes;
12use typst_library::text::{Font, FontBook, FontInfo};
13use typst_utils::LazyHash;
14
15/// Holds loaded fonts.
16///
17/// Fonts can be added with [`push`](Self::push) and [`extend`](Self::extend).
18/// The three top-level font provider functions in this module can directly be
19/// used with [`FontStore::extend`].
20///
21/// Font are added in-order. The indices in the font book and those that should
22/// be passed to [`source`](Self::source) and [`source`](Self::font) match this
23/// order.
24pub struct FontStore {
25    book: LazyHash<FontBook>,
26    slots: Vec<FontSlot>,
27}
28
29impl FontStore {
30    /// Creates a new empty font store.
31    pub fn new() -> Self {
32        Self {
33            book: LazyHash::new(FontBook::new()),
34            slots: Vec::new(),
35        }
36    }
37
38    /// Adds a new entry to the store.
39    pub fn push(&mut self, entry: (impl FontSource, FontInfo)) {
40        self.book.push(entry.1);
41        self.slots
42            .push(FontSlot { source: Box::new(entry.0), font: OnceLock::new() });
43    }
44
45    /// Adds multiple new entries to the store.
46    pub fn extend<T>(&mut self, entries: impl IntoIterator<Item = (T, FontInfo)>)
47    where
48        T: FontSource,
49    {
50        for entry in entries {
51            self.push(entry);
52        }
53    }
54
55    /// Provides metadata for the added fonts.
56    ///
57    /// Can directly be used to implement
58    /// [`World::book`](typst_library::World::book).
59    pub fn book(&self) -> &LazyHash<FontBook> {
60        &self.book
61    }
62
63    /// Retrieves the font at the given index.
64    ///
65    /// Loads the font if it's not already loaded.
66    ///
67    /// Can directly be used to implement
68    /// [`World::font`](typst_library::World::font).
69    pub fn font(&self, index: usize) -> Option<Font> {
70        self.slots.get(index)?.get()
71    }
72
73    /// Retrieves the underlying font source for the font with this index.
74    pub fn source(&self, index: usize) -> Option<&dyn FontSource> {
75        Some(&*self.slots.get(index)?.source)
76    }
77}
78
79impl Default for FontStore {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85/// Holds a font source and the lazily loaded font itself.
86struct FontSlot {
87    source: Box<dyn FontSource>,
88    font: OnceLock<Option<Font>>,
89}
90
91impl FontSlot {
92    /// Get the font for this slot. This loads the font into memory on first
93    /// access.
94    fn get(&self) -> Option<Font> {
95        self.font.get_or_init(|| self.source.load()).clone()
96    }
97}
98
99/// Serves a font on-demand.
100pub trait FontSource: Send + Sync + Any {
101    /// Try to load the font.
102    fn load(&self) -> Option<Font>;
103}
104
105impl FontSource for Font {
106    fn load(&self) -> Option<Font> {
107        Some(self.clone())
108    }
109}
110
111impl FontSource for FontPath {
112    fn load(&self) -> Option<Font> {
113        let _scope = typst_timing::TimingScope::new("load font");
114        let data = fs::read(&self.path).ok()?;
115        Font::new(Bytes::new(data), self.index)
116    }
117}
118
119/// Locates a font on the file system.
120#[derive(Debug)]
121pub struct FontPath {
122    /// The path at which the font or font collection resides.
123    pub path: PathBuf,
124    /// The index in the font collection, or zero if the path points to a single
125    /// font rather than a collection.
126    pub index: u32,
127}
128
129/// Yields the embedded fonts.
130///
131/// - For Text: _Libertinus Serif_, _New Computer Modern_
132/// - For Math: _New Computer Modern Math_
133/// - For Code: _Deja Vu Sans Mono_
134#[cfg(feature = "embedded-fonts")]
135pub fn embedded() -> impl Iterator<Item = (Font, FontInfo)> {
136    typst_assets::fonts().flat_map(|data| {
137        Font::iter(Bytes::new(data)).map(|font| {
138            let info = font.info().clone();
139            (font, info)
140        })
141    })
142}
143
144/// Discovers system fonts.
145///
146/// This searches in operating-system dependent standard font locations.
147#[cfg(feature = "scan-fonts")]
148pub fn system() -> impl Iterator<Item = (FontPath, FontInfo)> {
149    let _scope = typst_timing::TimingScope::new("scan system fonts");
150    with_db(|db| {
151        db.load_system_fonts();
152
153        // Add Adobe Fonts on Windows and macOS.
154        #[cfg(any(target_os = "windows", target_os = "macos"))]
155        load_adobe_fonts(db);
156    })
157}
158
159/// Scans for fonts in a directory.
160///
161/// The directory is searched recursively.
162#[cfg(feature = "scan-fonts")]
163pub fn scan(path: &std::path::Path) -> impl Iterator<Item = (FontPath, FontInfo)> {
164    let _scope = typst_timing::TimingScope::new("scan system fonts");
165    with_db(move |db| db.load_fonts_dir(path))
166}
167
168/// Discovers fonts via `fontdb`.
169#[cfg(feature = "scan-fonts")]
170fn with_db(
171    f: impl FnOnce(&mut fontdb::Database),
172) -> impl Iterator<Item = (FontPath, FontInfo)> {
173    let mut db = fontdb::Database::new();
174    f(&mut db);
175    db.faces()
176        .filter_map(|face| {
177            let path = match &face.source {
178                fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => path,
179                // We never add binary sources to the database, so there
180                // shouldn't be any.
181                fontdb::Source::Binary(_) => return None,
182            };
183
184            let info = db
185                .with_face_data(face.id, FontInfo::new)
186                .expect("database must contain this font")?;
187
188            let path = FontPath { path: path.clone(), index: face.index };
189
190            Some((path, info))
191        })
192        .collect::<Vec<_>>()
193        .into_iter()
194}
195
196/// Loads Adobe fonts available on the system. Only supported on Windows and
197/// macOS.
198///
199/// This is permissible as per Clause 3.1 (A) of the
200/// [Adobe Fonts Service Product Specific Terms][terms].
201///
202/// [terms]: https://wwwimages2.adobe.com/content/dam/cc/en/legal/servicetou/Adobe-Fonts-Product-Specific-Terms-en_US-20241007.pdf
203#[cfg(all(feature = "scan-fonts", any(target_os = "windows", target_os = "macos")))]
204fn load_adobe_fonts(db: &mut fontdb::Database) {
205    let Some(data) = dirs::data_dir() else { return };
206    let base = data.join("Adobe");
207
208    let prefix = if cfg!(target_os = "macos") { "." } else { "" };
209    let subdirs = [
210        format!("CoreSync/plugins/livetype/{prefix}r"),
211        format!("{prefix}User Owned Fonts"),
212    ];
213
214    for subdir in subdirs {
215        let Ok(entries) = fs::read_dir(base.join(subdir)) else { return };
216        for entry in entries.flatten() {
217            // Adobe fonts are stored as files (directories are skipped).
218            let Ok(metadata) = entry.metadata() else { continue };
219            if metadata.is_file() {
220                db.load_font_file(entry.path()).ok();
221            }
222        }
223    }
224}