Skip to main content

embedded_typst/
fonts.rs

1//! Fonts for [`EmbeddedWorld`](crate::EmbeddedWorld).
2
3#[cfg(not(target_arch = "wasm32"))]
4use std::collections::HashSet;
5#[cfg(not(target_arch = "wasm32"))]
6use std::path::{Path, PathBuf};
7use std::sync::OnceLock;
8
9use typst::foundations::Bytes;
10use typst::text::{Font, FontBook};
11use typst::utils::LazyHash;
12
13/// Font metadata for Typst plus the fonts themselves. Fonts given as data are
14/// loaded up front; fonts found on disk are only read into memory when Typst
15/// actually uses them.
16///
17/// Build once and share it between compilations with an `Arc`.
18pub struct Fonts {
19    book: LazyHash<FontBook>,
20    slots: Vec<FontSlot>,
21}
22
23/// A single font face, either already loaded or located in a file on disk.
24struct FontSlot {
25    #[cfg(not(target_arch = "wasm32"))]
26    path: Option<PathBuf>,
27    #[cfg(not(target_arch = "wasm32"))]
28    index: u32,
29    font: OnceLock<Option<Font>>,
30}
31
32impl FontSlot {
33    fn loaded(font: Font) -> Self {
34        Self {
35            #[cfg(not(target_arch = "wasm32"))]
36            path: None,
37            #[cfg(not(target_arch = "wasm32"))]
38            index: 0,
39            font: OnceLock::from(Some(font)),
40        }
41    }
42
43    fn get(&self) -> Option<Font> {
44        self.font
45            .get_or_init(|| {
46                #[cfg(not(target_arch = "wasm32"))]
47                {
48                    let data = std::fs::read(self.path.as_ref()?).ok()?;
49                    Font::new(Bytes::new(data), self.index)
50                }
51                #[cfg(target_arch = "wasm32")]
52                {
53                    None
54                }
55            })
56            .clone()
57    }
58}
59
60impl Fonts {
61    /// Start with no fonts. Fonts are registered in the order the builder
62    /// methods are called.
63    pub fn builder() -> FontsBuilder {
64        FontsBuilder {
65            book: FontBook::new(),
66            slots: Vec::new(),
67            #[cfg(not(target_arch = "wasm32"))]
68            seen: HashSet::new(),
69        }
70    }
71
72    /// The metadata of all fonts, for [`World::book`](typst::World::book).
73    pub fn book(&self) -> &LazyHash<FontBook> {
74        &self.book
75    }
76
77    /// The font at `index` in the [book](Self::book), read from disk on first
78    /// use.
79    pub fn font(&self, index: usize) -> Option<Font> {
80        self.slots.get(index)?.get()
81    }
82
83    /// Font files from disk that have been loaded so far, with their data.
84    #[cfg(not(target_arch = "wasm32"))]
85    pub fn loaded_files(&self) -> Vec<(PathBuf, Bytes)> {
86        self.slots
87            .iter()
88            .filter_map(|slot| {
89                let path = slot.path.clone()?;
90                let font = slot.font.get()?.as_ref()?;
91                Some((path, font.data().clone()))
92            })
93            .collect()
94    }
95}
96
97/// Collects fonts for [`Fonts`].
98pub struct FontsBuilder {
99    book: FontBook,
100    slots: Vec<FontSlot>,
101    /// Font files already registered, so a file found twice (as a system
102    /// font and in a font path, say) is only listed once.
103    #[cfg(not(target_arch = "wasm32"))]
104    seen: HashSet<PathBuf>,
105}
106
107impl FontsBuilder {
108    /// Add Typst's bundled fonts, so every target, especially Wasm, can render
109    /// a document without host-installed fonts.
110    #[cfg(feature = "typst-fonts")]
111    pub fn typst_fonts(mut self) -> Self {
112        for data in typst_assets::fonts() {
113            self.push_data(Bytes::new(data));
114        }
115        self
116    }
117
118    /// Add every face of a font file or collection held in memory, such as
119    /// an `include_bytes!` font.
120    pub fn data(mut self, data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self {
121        self.push_data(Bytes::new(data));
122        self
123    }
124
125    /// Add the fonts installed on the system.
126    #[cfg(not(target_arch = "wasm32"))]
127    pub fn system_fonts(mut self) -> Self {
128        let mut database = fontdb::Database::new();
129        database.load_system_fonts();
130        self.push_database(&database);
131        self
132    }
133
134    /// Add a font file or every font file in a directory (recursively).
135    /// Paths that do not exist or hold no fonts add nothing.
136    #[cfg(not(target_arch = "wasm32"))]
137    pub fn path(mut self, path: impl AsRef<Path>) -> Self {
138        let path = path.as_ref();
139        let mut database = fontdb::Database::new();
140        if path.is_dir() {
141            database.load_fonts_dir(path);
142        } else if path.is_file() {
143            let _ = database.load_font_file(path);
144        }
145        self.push_database(&database);
146        self
147    }
148
149    pub fn build(self) -> Fonts {
150        Fonts {
151            book: LazyHash::new(self.book),
152            slots: self.slots,
153        }
154    }
155
156    fn push_data(&mut self, data: Bytes) {
157        for font in Font::iter(data) {
158            self.book.push(font.info().clone());
159            self.slots.push(FontSlot::loaded(font));
160        }
161    }
162
163    /// Register the faces of every file in `database` without keeping the
164    /// data: fontdb lists every face of a collection (.ttc/.otc) separately,
165    /// so each file is read once from fontdb's (memory-mapped) data.
166    #[cfg(not(target_arch = "wasm32"))]
167    fn push_database(&mut self, database: &fontdb::Database) {
168        let files: Vec<_> = database
169            .faces()
170            .filter_map(|face| match &face.source {
171                fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => self
172                    .seen
173                    .insert(path.clone())
174                    .then(|| (face.id, path.clone())),
175                fontdb::Source::Binary(_) => None,
176            })
177            .collect();
178
179        for (id, path) in files {
180            database.with_face_data(id, |data, _| {
181                for (index, info) in typst::text::FontInfo::iter(data).enumerate() {
182                    self.book.push(info);
183                    self.slots.push(FontSlot {
184                        path: Some(path.clone()),
185                        index: index as u32,
186                        font: OnceLock::new(),
187                    });
188                }
189            });
190        }
191    }
192}