1use 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
15pub struct FontStore {
25 book: LazyHash<FontBook>,
26 slots: Vec<FontSlot>,
27}
28
29impl FontStore {
30 pub fn new() -> Self {
32 Self {
33 book: LazyHash::new(FontBook::new()),
34 slots: Vec::new(),
35 }
36 }
37
38 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 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 pub fn book(&self) -> &LazyHash<FontBook> {
60 &self.book
61 }
62
63 pub fn font(&self, index: usize) -> Option<Font> {
70 self.slots.get(index)?.get()
71 }
72
73 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
85struct FontSlot {
87 source: Box<dyn FontSource>,
88 font: OnceLock<Option<Font>>,
89}
90
91impl FontSlot {
92 fn get(&self) -> Option<Font> {
95 self.font.get_or_init(|| self.source.load()).clone()
96 }
97}
98
99pub trait FontSource: Send + Sync + Any {
101 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#[derive(Debug)]
121pub struct FontPath {
122 pub path: PathBuf,
124 pub index: u32,
127}
128
129#[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#[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 #[cfg(any(target_os = "windows", target_os = "macos"))]
155 load_adobe_fonts(db);
156 })
157}
158
159#[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#[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 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#[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 let Ok(metadata) = entry.metadata() else { continue };
219 if metadata.is_file() {
220 db.load_font_file(entry.path()).ok();
221 }
222 }
223 }
224}