1#[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
13pub struct Fonts {
19 book: LazyHash<FontBook>,
20 slots: Vec<FontSlot>,
21}
22
23struct 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 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 pub fn book(&self) -> &LazyHash<FontBook> {
74 &self.book
75 }
76
77 pub fn font(&self, index: usize) -> Option<Font> {
80 self.slots.get(index)?.get()
81 }
82
83 #[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
97pub struct FontsBuilder {
99 book: FontBook,
100 slots: Vec<FontSlot>,
101 #[cfg(not(target_arch = "wasm32"))]
104 seen: HashSet<PathBuf>,
105}
106
107impl FontsBuilder {
108 #[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 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 #[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 #[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 #[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}