text_typeset/font/registry.rs
1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::sync::OnceLock;
5
6use fontdb::{Database, Family, Query, Source, Style, Weight};
7use harfrust::{FontRef, ShaperData};
8
9use crate::font::writing_system_index::{
10 FaceBytes, FaceRef, WritingSystemIndexBuilder, build_from_faces,
11};
12use crate::types::FontFaceId;
13
14/// Summary of one installed font family, for building a font picker.
15///
16/// `name` is the primary (English, where available) family name; faces of
17/// different weights/styles are collapsed into one entry. `monospaced` is
18/// true when any face of the family is monospaced — read straight from
19/// fontdb metadata, so producing a whole [`Vec<FontFamilyInfo>`] loads no
20/// font bytes.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct FontFamilyInfo {
23 /// Primary family name (e.g. `"DejaVu Sans"`).
24 pub name: String,
25 /// True if any face of the family is monospaced.
26 pub monospaced: bool,
27}
28
29/// Byte container owned by a [`FontEntry`]. Erased behind a trait
30/// object so the registry can hold any of `Vec<u8>`, `memmap2::Mmap`,
31/// or `&'static [u8]` (via a wrapper) without copying. fontdb's
32/// `Source::Binary` uses the same shape, so the same `Arc` can be
33/// shared with the database.
34pub type SharedFontData = Arc<dyn AsRef<[u8]> + Sync + Send>;
35
36/// Backing bytes for a [`FontEntry`].
37///
38/// Explicitly registered fonts are [`Resident`](FontData::Resident) — the
39/// caller handed us the bytes. System fonts discovered by
40/// [`load_system_fonts`](FontRegistry::new) are [`Lazy`](FontData::Lazy):
41/// we keep only the file path until the face is actually shaped or queried
42/// for coverage, then read and cache it. This keeps `new()` cheap even
43/// when the OS has hundreds of installed faces.
44enum FontData {
45 Resident(SharedFontData),
46 Lazy {
47 path: PathBuf,
48 cell: OnceLock<SharedFontData>,
49 },
50}
51
52pub struct FontEntry {
53 pub fontdb_id: fontdb::ID,
54 pub face_index: u32,
55 data: FontData,
56 pub swash_cache_key: swash::CacheKey,
57 /// True for faces discovered via the OS font enumeration. Glyph
58 /// fallback prefers explicitly-registered faces and only reaches for
59 /// system faces as a last resort (see
60 /// [`find_fallback_font`](crate::font::resolve::find_fallback_font)).
61 pub is_system: bool,
62 /// Lazily-built HarfBuzz shaping tables for this face. `ShaperData`
63 /// preprocesses the font's GSUB/GPOS/cmap tables and is independent
64 /// of any `FontRef` lifetime, so it can be built once and reused
65 /// across every shape call. Built on first shape via
66 /// [`shaper_data`](Self::shaper_data). `OnceLock` keeps `FontEntry`
67 /// `Sync` (its internal caches are atomic).
68 shaper_data: OnceLock<ShaperData>,
69}
70
71impl FontEntry {
72 /// Borrow the underlying font bytes, loading them from disk on first
73 /// use for lazily-tracked system faces. Returns an empty slice if a
74 /// system font file can no longer be read (uninstalled mid-session);
75 /// shaping then degrades gracefully to `None`/`.notdef`.
76 pub fn bytes(&self) -> &[u8] {
77 match &self.data {
78 // Two `as_ref()` hops: Arc<dyn AsRef<[u8]>> → dyn → &[u8].
79 FontData::Resident(data) => (**data).as_ref(),
80 FontData::Lazy { path, cell } => {
81 let arc = cell.get_or_init(|| {
82 let bytes = std::fs::read(path).unwrap_or_default();
83 Arc::new(bytes) as SharedFontData
84 });
85 (**arc).as_ref()
86 }
87 }
88 }
89
90 /// Borrow this face's shaping tables, building them on first use.
91 ///
92 /// `ShaperData::new` reprocesses the font tables; doing it once per
93 /// face (instead of once per shape call) avoids redundant work on
94 /// every relayout/keystroke. The `font` must refer to this same
95 /// face — callers already hold a `FontRef` opened from `bytes()`.
96 pub fn shaper_data(&self, font: &FontRef) -> &ShaperData {
97 self.shaper_data.get_or_init(|| ShaperData::new(font))
98 }
99}
100
101pub struct FontRegistry {
102 fontdb: Database,
103 fonts: Vec<Option<FontEntry>>,
104 /// fontdb face ID → our `FontFaceId`, so `query_font` doesn't scan
105 /// `fonts` linearly (it can hold hundreds of system faces).
106 fontdb_index: HashMap<fontdb::ID, FontFaceId>,
107 generic_families: HashMap<String, String>,
108 default_font: Option<FontFaceId>,
109 default_size_px: f32,
110}
111
112impl Default for FontRegistry {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118impl FontRegistry {
119 /// Create a registry pre-populated with the operating system's fonts.
120 ///
121 /// OS faces participate in family lookup and glyph fallback so that
122 /// arbitrary documents (CJK, emoji, scripts the host didn't bundle)
123 /// still render. Their bytes load lazily on first use, but the
124 /// enumeration itself scans the OS font directories — a one-time
125 /// startup cost. Use [`new_without_system_fonts`](Self::new_without_system_fonts)
126 /// when the host ships a controlled font set and wants neither the
127 /// scan nor implicit OS fonts.
128 pub fn new() -> Self {
129 let mut registry = Self::new_without_system_fonts();
130 registry.load_system_fonts();
131 registry
132 }
133
134 /// Create an empty registry with no system fonts. Fonts enter only
135 /// via the `register_font*` methods.
136 pub fn new_without_system_fonts() -> Self {
137 Self {
138 fontdb: Database::new(),
139 fonts: Vec::new(),
140 fontdb_index: HashMap::new(),
141 generic_families: HashMap::new(),
142 default_font: None,
143 default_size_px: 16.0,
144 }
145 }
146
147 /// Enumerate OS fonts into `fontdb` and register a lazy [`FontEntry`]
148 /// for each so they join family lookup and the fallback chain. Bytes
149 /// load on first access; only the file path is kept until then.
150 fn load_system_fonts(&mut self) {
151 self.fontdb.load_system_fonts();
152
153 // Collect first: `faces()` borrows `self.fontdb` immutably while
154 // the loop needs `&mut self` to push entries.
155 let discovered: Vec<(fontdb::ID, u32, PathBuf)> = self
156 .fontdb
157 .faces()
158 .filter_map(|f| match &f.source {
159 Source::File(path) => Some((f.id, f.index, path.clone())),
160 Source::SharedFile(path, _) => Some((f.id, f.index, path.clone())),
161 Source::Binary(_) => None,
162 })
163 .collect();
164
165 for (fontdb_id, face_index, path) in discovered {
166 if self.fontdb_index.contains_key(&fontdb_id) {
167 continue;
168 }
169 let entry = FontEntry {
170 fontdb_id,
171 face_index,
172 data: FontData::Lazy {
173 path,
174 cell: OnceLock::new(),
175 },
176 swash_cache_key: swash::CacheKey::new(),
177 is_system: true,
178 shaper_data: OnceLock::new(),
179 };
180 let face_id = FontFaceId(self.fonts.len() as u32);
181 self.fonts.push(Some(entry));
182 self.fontdb_index.insert(fontdb_id, face_id);
183 }
184 }
185
186 /// Register a font from raw bytes. Returns IDs for all faces found
187 /// (font collections may contain multiple faces).
188 ///
189 /// This copies the bytes into an owned `Vec<u8>`. For large fonts
190 /// where the caller already holds the data in a shareable
191 /// container (e.g. an `Arc<Mmap>` for a system emoji font), use
192 /// [`register_font_shared`](Self::register_font_shared) instead to
193 /// avoid the copy.
194 pub fn register_font(&mut self, data: &[u8]) -> Vec<FontFaceId> {
195 let arc_data: SharedFontData = Arc::new(data.to_vec());
196 self.register_font_shared(arc_data)
197 }
198
199 /// Register a font from a pre-built shared byte container,
200 /// avoiding the copy that [`register_font`](Self::register_font)
201 /// would perform.
202 ///
203 /// The shared container is held by every resulting [`FontEntry`]
204 /// and by fontdb's `Source::Binary`. Drop it after the registry
205 /// drops the entries.
206 pub fn register_font_shared(&mut self, data: SharedFontData) -> Vec<FontFaceId> {
207 let source = Source::Binary(data.clone());
208 let fontdb_ids = self.fontdb.load_font_source(source);
209
210 let mut face_ids = Vec::new();
211 for fontdb_id in fontdb_ids {
212 let face_index = self.fontdb.face(fontdb_id).map(|f| f.index).unwrap_or(0);
213
214 let swash_cache_key = swash::CacheKey::new();
215 let entry = FontEntry {
216 fontdb_id,
217 face_index,
218 data: FontData::Resident(data.clone()),
219 swash_cache_key,
220 is_system: false,
221 shaper_data: OnceLock::new(),
222 };
223
224 let face_id = FontFaceId(self.fonts.len() as u32);
225 self.fonts.push(Some(entry));
226 self.fontdb_index.insert(fontdb_id, face_id);
227 face_ids.push(face_id);
228 }
229 face_ids
230 }
231
232 /// Register a font with explicit metadata, overriding the font's name table.
233 pub fn register_font_as(
234 &mut self,
235 data: &[u8],
236 family: &str,
237 weight: u16,
238 italic: bool,
239 ) -> Vec<FontFaceId> {
240 let arc_data: SharedFontData = Arc::new(data.to_vec());
241 self.register_font_shared_as(arc_data, family, weight, italic)
242 }
243
244 /// Like [`register_font_as`](Self::register_font_as) but takes a
245 /// pre-built shared byte container, avoiding the copy.
246 pub fn register_font_shared_as(
247 &mut self,
248 data: SharedFontData,
249 family: &str,
250 weight: u16,
251 italic: bool,
252 ) -> Vec<FontFaceId> {
253 let source = Source::Binary(data.clone());
254 let fontdb_ids = self.fontdb.load_font_source(source);
255
256 let mut face_ids = Vec::new();
257 for fontdb_id in fontdb_ids {
258 // Override metadata in fontdb
259 if let Some(face_info) = self.fontdb.face(fontdb_id) {
260 let mut info = face_info.clone();
261 info.families = vec![(family.to_string(), fontdb::Language::English_UnitedStates)];
262 info.weight = Weight(weight);
263 info.style = if italic { Style::Italic } else { Style::Normal };
264 // Remove old and re-add with new metadata
265 let face_index = info.index;
266 self.fontdb.remove_face(fontdb_id);
267 let new_id = self.fontdb.push_face_info(info);
268
269 let swash_cache_key = swash::CacheKey::new();
270 let entry = FontEntry {
271 fontdb_id: new_id,
272 face_index,
273 data: FontData::Resident(data.clone()),
274 swash_cache_key,
275 is_system: false,
276 shaper_data: OnceLock::new(),
277 };
278
279 let face_id = FontFaceId(self.fonts.len() as u32);
280 self.fonts.push(Some(entry));
281 self.fontdb_index.insert(new_id, face_id);
282 face_ids.push(face_id);
283 }
284 }
285 face_ids
286 }
287
288 pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
289 self.default_font = Some(face);
290 self.default_size_px = size_px;
291 }
292
293 pub fn default_font(&self) -> Option<FontFaceId> {
294 self.default_font
295 }
296
297 pub fn default_size_px(&self) -> f32 {
298 self.default_size_px
299 }
300
301 pub fn set_generic_family(&mut self, generic: &str, family: &str) {
302 self.generic_families
303 .insert(generic.to_string(), family.to_string());
304 }
305
306 /// Resolve a family name, mapping generic names (serif, monospace, etc.)
307 /// to their configured concrete family names.
308 pub fn resolve_family_name<'a>(&'a self, family: &'a str) -> &'a str {
309 self.generic_families
310 .get(family)
311 .map(|s| s.as_str())
312 .unwrap_or(family)
313 }
314
315 /// Query fontdb for a font matching the given criteria.
316 pub fn query_font(&self, family: &str, weight: u16, italic: bool) -> Option<FontFaceId> {
317 let resolved = self.resolve_family_name(family);
318 let style = if italic { Style::Italic } else { Style::Normal };
319
320 let query = Query {
321 families: &[Family::Name(resolved)],
322 weight: Weight(weight),
323 style,
324 ..Query::default()
325 };
326
327 let fontdb_id = self.fontdb.query(&query)?;
328 self.fontdb_id_to_face_id(fontdb_id)
329 }
330
331 /// Look up the family name of a registered font.
332 pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
333 let entry = self.get(face_id)?;
334 let face_info = self.fontdb.face(entry.fontdb_id)?;
335 face_info.families.first().map(|(name, _)| name.clone())
336 }
337
338 /// Look up a FontEntry by FontFaceId.
339 pub fn get(&self, face_id: FontFaceId) -> Option<&FontEntry> {
340 self.fonts
341 .get(face_id.0 as usize)
342 .and_then(|opt| opt.as_ref())
343 }
344
345 /// Query for a variant (different weight/style) of an existing registered font.
346 /// Looks up the family name of `base_face` and queries fontdb for a match.
347 pub fn query_variant(
348 &self,
349 base_face: FontFaceId,
350 weight: u16,
351 italic: bool,
352 ) -> Option<FontFaceId> {
353 let entry = self.get(base_face)?;
354 let face_info = self.fontdb.face(entry.fontdb_id)?;
355 let family_name = face_info.families.first().map(|(name, _)| name.as_str())?;
356 self.query_font(family_name, weight, italic)
357 }
358
359 /// Find our FontFaceId for a fontdb ID.
360 fn fontdb_id_to_face_id(&self, fontdb_id: fontdb::ID) -> Option<FontFaceId> {
361 self.fontdb_index.get(&fontdb_id).copied()
362 }
363
364 /// Enumerate every installed font family, deduplicated (case-insensitive)
365 /// and sorted (case-insensitive Unicode).
366 ///
367 /// Cheap: it reads only fontdb metadata (family name + monospaced flag),
368 /// so no font file is opened. This is the item source for a font picker.
369 /// Multiple faces of one family (Regular/Bold/Italic) collapse into a
370 /// single entry, whose `monospaced` is true if any face is monospaced.
371 pub fn families(&self) -> Vec<FontFamilyInfo> {
372 // key = lowercased name (dedupe); value = (display name, monospaced).
373 let mut map: HashMap<String, (String, bool)> = HashMap::new();
374 for face in self.fontdb.faces() {
375 // `families[0]` is the English (US) name where present.
376 let Some((name, _)) = face.families.first() else {
377 continue;
378 };
379 let entry = map
380 .entry(name.to_lowercase())
381 .or_insert_with(|| (name.clone(), false));
382 entry.1 |= face.monospaced;
383 }
384 let mut out: Vec<FontFamilyInfo> = map
385 .into_values()
386 .map(|(name, monospaced)| FontFamilyInfo { name, monospaced })
387 .collect();
388 out.sort_by(|a, b| {
389 a.name
390 .to_lowercase()
391 .cmp(&b.name.to_lowercase())
392 .then_with(|| a.name.cmp(&b.name))
393 });
394 out
395 }
396
397 /// Enumerate installed font family names, deduplicated and sorted — the
398 /// simple projection of [`families`](Self::families).
399 pub fn family_names(&self) -> Vec<String> {
400 self.families().into_iter().map(|f| f.name).collect()
401 }
402
403 /// True if any face of the named family is monospaced. Reads only fontdb
404 /// metadata (no bytes loaded). Case-insensitive on the family name.
405 pub fn family_is_monospaced(&self, family: &str) -> bool {
406 let target = family.to_lowercase();
407 self.fontdb.faces().any(|f| {
408 f.monospaced
409 && f.families
410 .first()
411 .is_some_and(|(n, _)| n.to_lowercase() == target)
412 })
413 }
414
415 /// Build a `Send` snapshot of every family's face byte-sources, ready to
416 /// be moved to a background thread and turned into a writing-system
417 /// coverage map (see [`WritingSystemIndexBuilder`]).
418 ///
419 /// Cheap on the calling thread: it clones file paths / shares `Arc`s but
420 /// reads no font bytes. The expensive OS/2 parsing happens in
421 /// [`WritingSystemIndexBuilder::build`], which the caller runs
422 /// off-thread.
423 /// The coverage map is keyed by the **lowercased** family name so it
424 /// aligns with [`families`](Self::families)' case-insensitive dedup —
425 /// callers look up by `name.to_lowercase()`. (Faces of one family can
426 /// disagree on name casing; a case-sensitive key would split or miss.)
427 pub fn writing_system_index_builder(&self) -> WritingSystemIndexBuilder {
428 build_from_faces(self.fontdb.faces().filter_map(|face| {
429 let (family, _) = face.families.first()?;
430 let bytes = match &face.source {
431 Source::File(path) => FaceBytes::Path(path.clone()),
432 Source::SharedFile(_, data) => FaceBytes::Shared(data.clone()),
433 Source::Binary(data) => FaceBytes::Shared(data.clone()),
434 };
435 Some((
436 family.to_lowercase(),
437 FaceRef {
438 bytes,
439 index: face.index,
440 },
441 ))
442 }))
443 }
444
445 /// Iterate all registered font entries for glyph fallback.
446 pub fn all_entries(&self) -> impl Iterator<Item = (FontFaceId, &FontEntry)> {
447 self.fonts
448 .iter()
449 .enumerate()
450 .filter_map(|(i, opt)| opt.as_ref().map(|entry| (FontFaceId(i as u32), entry)))
451 }
452}