typst_kit/
fonts.rs

1//! Default implementation for searching local and system installed fonts as
2//! well as loading embedded default fonts.
3//!
4//! # Embedded fonts
5//! The following fonts are available as embedded fonts via the `embed-fonts`
6//! feature flag:
7//! - For text: Libertinus Serif, New Computer Modern
8//! - For math: New Computer Modern Math
9//! - For code: Deja Vu Sans Mono
10
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::sync::OnceLock;
14
15use fontdb::{Database, Source};
16use typst_library::foundations::Bytes;
17use typst_library::text::{Font, FontBook, FontInfo};
18use typst_timing::TimingScope;
19
20/// Holds details about the location of a font and lazily the font itself.
21#[derive(Debug)]
22pub struct FontSlot {
23    /// The path at which the font can be found on the system.
24    path: Option<PathBuf>,
25    /// The index of the font in its collection. Zero if the path does not point
26    /// to a collection.
27    index: u32,
28    /// The lazily loaded font.
29    font: OnceLock<Option<Font>>,
30}
31
32impl FontSlot {
33    /// Returns the path at which the font can be found on the system, or `None`
34    /// if the font was embedded.
35    pub fn path(&self) -> Option<&Path> {
36        self.path.as_deref()
37    }
38
39    /// Returns the index of the font in its collection. Zero if the path does
40    /// not point to a collection.
41    pub fn index(&self) -> u32 {
42        self.index
43    }
44
45    /// Get the font for this slot. This loads the font into memory on first
46    /// access.
47    pub fn get(&self) -> Option<Font> {
48        self.font
49            .get_or_init(|| {
50                let _scope = TimingScope::new("load font");
51                let data = fs::read(
52                    self.path
53                        .as_ref()
54                        .expect("`path` is not `None` if `font` is uninitialized"),
55                )
56                .ok()?;
57                Font::new(Bytes::new(data), self.index)
58            })
59            .clone()
60    }
61}
62
63/// The result of a font search, created by calling [`FontSearcher::search`].
64#[derive(Debug)]
65pub struct Fonts {
66    /// Metadata about all discovered fonts.
67    pub book: FontBook,
68    /// Slots that the fonts are loaded into.
69    pub fonts: Vec<FontSlot>,
70}
71
72impl Fonts {
73    /// Creates a new font searcer with the default settings.
74    pub fn searcher() -> FontSearcher {
75        FontSearcher::new()
76    }
77}
78
79/// Searches for fonts.
80///
81/// Fonts are added in the following order (descending priority):
82/// 1. Font directories
83/// 2. System fonts (if included & enabled)
84/// 3. Embedded fonts (if enabled)
85#[derive(Debug)]
86pub struct FontSearcher {
87    db: Database,
88    include_system_fonts: bool,
89    #[cfg(feature = "embed-fonts")]
90    include_embedded_fonts: bool,
91    book: FontBook,
92    fonts: Vec<FontSlot>,
93}
94
95impl FontSearcher {
96    /// Create a new, empty system searcher. The searcher is created with the
97    /// default configuration, it will include embedded fonts and system fonts.
98    pub fn new() -> Self {
99        Self {
100            db: Database::new(),
101            include_system_fonts: true,
102            #[cfg(feature = "embed-fonts")]
103            include_embedded_fonts: true,
104            book: FontBook::new(),
105            fonts: vec![],
106        }
107    }
108
109    /// Whether to search for and load system fonts, defaults to `true`.
110    pub fn include_system_fonts(&mut self, value: bool) -> &mut Self {
111        self.include_system_fonts = value;
112        self
113    }
114
115    /// Whether to load embedded fonts, defaults to `true`.
116    #[cfg(feature = "embed-fonts")]
117    pub fn include_embedded_fonts(&mut self, value: bool) -> &mut Self {
118        self.include_embedded_fonts = value;
119        self
120    }
121
122    /// Start searching for and loading fonts. To additionally load fonts
123    /// from specific directories, use [`search_with`][Self::search_with].
124    ///
125    /// # Examples
126    /// ```no_run
127    /// # use typst_kit::fonts::FontSearcher;
128    /// let fonts = FontSearcher::new()
129    ///     .include_system_fonts(true)
130    ///     .search();
131    /// ```
132    pub fn search(&mut self) -> Fonts {
133        self.search_with::<_, &str>([])
134    }
135
136    /// Start searching for and loading fonts, with additional directories.
137    ///
138    /// # Examples
139    /// ```no_run
140    /// # use typst_kit::fonts::FontSearcher;
141    /// let fonts = FontSearcher::new()
142    ///     .include_system_fonts(true)
143    ///     .search_with(["./assets/fonts/"]);
144    /// ```
145    pub fn search_with<I, P>(&mut self, font_dirs: I) -> Fonts
146    where
147        I: IntoIterator<Item = P>,
148        P: AsRef<Path>,
149    {
150        // Font paths have highest priority.
151        for path in font_dirs {
152            self.db.load_fonts_dir(path);
153        }
154
155        if self.include_system_fonts {
156            // System fonts have second priority.
157            self.db.load_system_fonts();
158        }
159
160        for face in self.db.faces() {
161            let path = match &face.source {
162                Source::File(path) | Source::SharedFile(path, _) => path,
163                // We never add binary sources to the database, so there
164                // shouldn't be any.
165                Source::Binary(_) => continue,
166            };
167
168            let info = self
169                .db
170                .with_face_data(face.id, FontInfo::new)
171                .expect("database must contain this font");
172
173            if let Some(info) = info {
174                self.book.push(info);
175                self.fonts.push(FontSlot {
176                    path: Some(path.clone()),
177                    index: face.index,
178                    font: OnceLock::new(),
179                });
180            }
181        }
182
183        // Embedded fonts have lowest priority.
184        #[cfg(feature = "embed-fonts")]
185        if self.include_embedded_fonts {
186            self.add_embedded();
187        }
188
189        Fonts {
190            book: std::mem::take(&mut self.book),
191            fonts: std::mem::take(&mut self.fonts),
192        }
193    }
194
195    /// Add fonts that are embedded in the binary.
196    #[cfg(feature = "embed-fonts")]
197    fn add_embedded(&mut self) {
198        for data in typst_assets::fonts() {
199            let buffer = Bytes::new(data);
200            for (i, font) in Font::iter(buffer).enumerate() {
201                self.book.push(font.info().clone());
202                self.fonts.push(FontSlot {
203                    path: None,
204                    index: i as u32,
205                    font: OnceLock::from(Some(font)),
206                });
207            }
208        }
209    }
210}
211
212impl Default for FontSearcher {
213    fn default() -> Self {
214        Self::new()
215    }
216}