1use motion_core::sync::MutexExt;
4use std::sync::{
5 Arc, Mutex, MutexGuard, OnceLock,
6 atomic::{AtomicU64, Ordering},
7};
8
9#[cfg(test)]
10#[path = "font/tests.rs"]
11mod explicit_face_tests;
12
13pub const MAX_FONT_FACE_BYTES: usize = 32 * 1024 * 1024;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct FontIdentifier(pub(crate) fontdb::ID);
19
20#[derive(Clone)]
23pub struct FontLibrary {
24 pub(crate) database: Arc<fontdb::Database>,
25 bytes: usize,
26 snapshot_id: u64,
27 font_system: Arc<OnceLock<Mutex<cosmic_text::FontSystem>>>,
28 pub(crate) glyph_cache: Arc<motion_core::SceneCache>,
29 pub(crate) layout_cache: crate::text::SharedLayouts,
30 used_fonts: Arc<
31 Mutex<
32 std::collections::BTreeMap<cosmic_text::fontdb::ID, (motion_core::FontIdentity, usize)>,
33 >,
34 >,
35}
36
37impl FontLibrary {
38 pub fn new() -> Self {
40 Self {
41 database: Arc::new(fontdb::Database::new()),
42 bytes: 0,
43 snapshot_id: identity(),
44 font_system: Arc::new(OnceLock::new()),
45 glyph_cache: Arc::new(motion_core::SceneCache::with_limits(
46 motion_core::CacheLimits {
47 max_entries: 2048,
48 max_bytes: 16 * 1024 * 1024,
49 },
50 )),
51 layout_cache: Default::default(),
52 used_fonts: Arc::default(),
53 }
54 }
55
56 pub fn load_file(
58 &mut self,
59 path: impl AsRef<std::path::Path>,
60 ) -> motion_core::Result<Vec<FontIdentifier>> {
61 use std::io::Read;
62 let path = path.as_ref();
63 let metadata = std::fs::metadata(path)?;
64 if !metadata.is_file() || metadata.len() > 256 * 1024 * 1024 {
65 return Err(invalid("Font file must be regular and at most 256 MiB"));
66 }
67 let mut bytes = Vec::new();
68 std::fs::File::open(path)?
69 .take(256 * 1024 * 1024 + 1)
70 .read_to_end(&mut bytes)?;
71 self.load_data(bytes.into())
72 .map_err(|error| motion_core::Error::asset("font", path.display().to_string(), error))
73 }
74
75 pub fn load_data(&mut self, data: Arc<[u8]>) -> motion_core::Result<Vec<FontIdentifier>> {
78 let bytes = self
79 .bytes
80 .checked_add(data.len())
81 .filter(|bytes| *bytes <= 256 * 1024 * 1024)
82 .ok_or_else(|| invalid("Font-library source payload exceeds 256 MiB"))?;
83 let mut database = self.database.as_ref().clone();
84 let source: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(data);
85 let ids = database.load_font_source(fontdb::Source::Binary(source));
86 if ids.is_empty() {
87 return Err(invalid("No valid font faces found"));
88 }
89 if database.faces().count() > 4096 {
90 return Err(invalid("Font library exceeds 4096 faces"));
91 }
92 if self.is_empty() {
93 let family = database
94 .faces()
95 .next()
96 .and_then(|face| face.families.first())
97 .map(|family| family.0.clone());
98 if let Some(family) = family {
99 database.set_sans_serif_family(family.clone());
100 database.set_serif_family(family.clone());
101 database.set_monospace_family(family);
102 }
103 }
104 self.database = Arc::new(database);
105 self.bytes = bytes;
106 self.snapshot_id = identity();
107 self.font_system = Arc::new(OnceLock::new());
108 self.glyph_cache = Arc::new(motion_core::SceneCache::with_limits(
109 motion_core::CacheLimits {
110 max_entries: 2048,
111 max_bytes: 16 * 1024 * 1024,
112 },
113 ));
114 self.layout_cache = Default::default();
115 self.used_fonts = Arc::default();
116 Ok(ids.into_iter().map(FontIdentifier).collect())
117 }
118
119 pub fn load_dir(
122 &mut self,
123 directory: impl AsRef<std::path::Path>,
124 ) -> motion_core::Result<usize> {
125 let mut pending = vec![directory.as_ref().to_path_buf()];
126 let mut files = Vec::new();
127 let mut visited = 0;
128 while let Some(directory) = pending.pop() {
129 let mut entries = std::fs::read_dir(directory)?
130 .take(4097 - visited)
131 .collect::<Result<Vec<_>, _>>()?;
132 entries.sort_by_key(|entry| entry.path());
133 for entry in entries {
134 visited += 1;
135 if visited > 4096 {
136 return Err(invalid("Font directory exceeds 4096 visited entries"));
137 }
138 let kind = entry.file_type()?;
139 if kind.is_dir() {
140 pending.push(entry.path());
141 } else if kind.is_file()
142 && entry
143 .path()
144 .extension()
145 .and_then(|value| value.to_str())
146 .is_some_and(|value| {
147 matches!(
148 value.to_ascii_lowercase().as_str(),
149 "ttf" | "otf" | "ttc" | "otc"
150 )
151 })
152 {
153 files.push(entry.path());
154 }
155 }
156 }
157 files.sort();
158 let mut updated = self.clone();
159 let before = updated.len();
160 for file in files {
161 updated.load_file(file)?;
162 }
163 let added = updated.len() - before;
164 *self = updated;
165 Ok(added)
166 }
167
168 pub fn load_system_fonts(&mut self) -> motion_core::Result<usize> {
171 let mut discovered = fontdb::Database::new();
172 discovered.load_system_fonts();
173 self.index_faces(discovered.faces().cloned())
174 }
175
176 pub fn load_system_family(&mut self, family: &str) -> motion_core::Result<usize> {
178 if family.is_empty() || family.len() > 1024 {
179 return Err(invalid("System font family must have 1..=1024 UTF-8 bytes"));
180 }
181 let mut discovered = fontdb::Database::new();
182 discovered.load_system_fonts();
183 let faces = discovered
184 .faces()
185 .filter(|face| {
186 face.families
187 .iter()
188 .any(|(name, _)| name.eq_ignore_ascii_case(family))
189 })
190 .cloned()
191 .collect::<Vec<_>>();
192 if faces.is_empty() {
193 return Err(invalid("Requested installed font family was not found"));
194 }
195 self.index_faces(faces)
196 }
197
198 pub fn default_family(mut self, family: impl Into<String>) -> Self {
200 let family = family.into();
201 let mut database = self.database.as_ref().clone();
202 database.set_sans_serif_family(family.clone());
203 database.set_serif_family(family.clone());
204 database.set_monospace_family(family);
205 self.database = Arc::new(database);
206 self.snapshot_id = identity();
207 self.font_system = Arc::new(OnceLock::new());
208 self.glyph_cache = Arc::new(motion_core::SceneCache::with_limits(
209 motion_core::CacheLimits {
210 max_entries: 2048,
211 max_bytes: 16 * 1024 * 1024,
212 },
213 ));
214 self.layout_cache = Default::default();
215 self.used_fonts = Arc::default();
216 self
217 }
218
219 pub fn database(&self) -> &fontdb::Database {
222 self.database.as_ref()
223 }
224
225 pub fn snapshot_id(&self) -> u64 {
227 self.snapshot_id
228 }
229
230 pub fn len(&self) -> usize {
232 self.database.faces().count()
233 }
234
235 pub fn is_empty(&self) -> bool {
237 self.len() == 0
238 }
239
240 fn index_faces(
241 &mut self,
242 faces: impl IntoIterator<Item = fontdb::FaceInfo>,
243 ) -> motion_core::Result<usize> {
244 let mut database = self.database.as_ref().clone();
245 let before = self.len();
246 for mut face in faces {
247 if let fontdb::Source::SharedFile(path, _) = face.source {
248 face.source = fontdb::Source::File(path);
249 }
250 if database.faces().count() >= 65_536 {
251 return Err(invalid("Font metadata exceeds 65536 faces"));
252 }
253 database.push_face_info(face);
254 }
255 let added = database.faces().count() - before;
256 self.database = Arc::new(database);
257 self.snapshot_id = identity();
258 self.font_system = Arc::new(OnceLock::new());
259 self.layout_cache = Default::default();
260 self.used_fonts = Arc::default();
261 Ok(added)
262 }
263
264 pub(crate) fn with_render_cache(&self, cache: &motion_core::RenderCache) -> Self {
265 let mut fonts = self.clone();
266 fonts.glyph_cache = cache.partition(motion_core::CachePartition::Glyph).clone();
267 fonts
268 }
269
270 pub(crate) fn lock_font_system(
272 &self,
273 ) -> Result<MutexGuard<'_, cosmic_text::FontSystem>, motion_core::ValidationError> {
274 self.font_system_cell().lock().map_err(|_| {
275 motion_core::ValidationError::invalid("fonts", "library", "Font engine is poisoned")
276 })
277 }
278
279 fn font_system_cell(&self) -> &Mutex<cosmic_text::FontSystem> {
280 self.font_system.get_or_init(|| {
281 let mut database = cosmic_text::fontdb::Database::new();
282 for face in self.database.faces() {
283 let source = match &face.source {
284 fontdb::Source::Binary(data) => {
285 cosmic_text::fontdb::Source::Binary(data.clone())
286 }
287 fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => {
288 cosmic_text::fontdb::Source::File(path.clone())
289 }
290 };
291 database.push_face_info(cosmic_text::fontdb::FaceInfo {
292 id: cosmic_text::fontdb::ID::dummy(),
293 source,
294 index: face.index,
295 families: face
296 .families
297 .iter()
298 .map(|(name, language)| {
299 let language = if language.primary_language() == "English"
300 && language.region() == "United States"
301 {
302 cosmic_text::fontdb::Language::English_UnitedStates
303 } else {
304 cosmic_text::fontdb::Language::Unknown
305 };
306 (name.clone(), language)
307 })
308 .collect(),
309 post_script_name: face.post_script_name.clone(),
310 style: match face.style {
311 fontdb::Style::Normal => cosmic_text::fontdb::Style::Normal,
312 fontdb::Style::Italic => cosmic_text::fontdb::Style::Italic,
313 fontdb::Style::Oblique => cosmic_text::fontdb::Style::Oblique,
314 },
315 weight: cosmic_text::fontdb::Weight(face.weight.0),
316 stretch: match face.stretch.to_number() {
317 1 => cosmic_text::fontdb::Stretch::UltraCondensed,
318 2 => cosmic_text::fontdb::Stretch::ExtraCondensed,
319 3 => cosmic_text::fontdb::Stretch::Condensed,
320 4 => cosmic_text::fontdb::Stretch::SemiCondensed,
321 6 => cosmic_text::fontdb::Stretch::SemiExpanded,
322 7 => cosmic_text::fontdb::Stretch::Expanded,
323 8 => cosmic_text::fontdb::Stretch::ExtraExpanded,
324 9 => cosmic_text::fontdb::Stretch::UltraExpanded,
325 _ => cosmic_text::fontdb::Stretch::Normal,
326 },
327 monospaced: face.monospaced,
328 });
329 }
330 let families = [
331 fontdb::Family::SansSerif,
332 fontdb::Family::Serif,
333 fontdb::Family::Monospace,
334 ]
335 .map(|family| {
336 self.database
337 .query(&fontdb::Query {
338 families: &[family],
339 ..fontdb::Query::default()
340 })
341 .and_then(|id| self.database.face(id))
342 .and_then(|face| face.families.first())
343 .map(|family| family.0.clone())
344 });
345 if let Some(family) = &families[0] {
346 database.set_sans_serif_family(family.clone());
347 }
348 if let Some(family) = &families[1] {
349 database.set_serif_family(family.clone());
350 }
351 if let Some(family) = &families[2] {
352 database.set_monospace_family(family.clone());
353 }
354 Mutex::new(cosmic_text::FontSystem::new_with_locale_and_db(
355 "en-US".to_owned(),
356 database,
357 ))
358 })
359 }
360
361 pub fn retained_bytes(&self) -> usize {
363 let used = self.used_fonts.lock_unpoisoned();
364 let sources = used
365 .values()
366 .map(|(font, bytes)| (font.file_hash, *bytes))
367 .collect::<std::collections::BTreeMap<_, _>>();
368 self.bytes + sources.values().sum::<usize>()
369 }
370
371 pub(crate) fn record_font(
372 &self,
373 system: &cosmic_text::FontSystem,
374 id: cosmic_text::fontdb::ID,
375 bytes: &[u8],
376 ) -> motion_core::FontIdentity {
377 use std::hash::{Hash, Hasher};
378
379 let mut used = self.used_fonts.lock_unpoisoned();
380 used.entry(id)
381 .or_insert_with(|| {
382 let mut hash = std::collections::hash_map::DefaultHasher::new();
383 bytes.hash(&mut hash);
384 let face = system.db().face(id);
385 let retained = face
386 .filter(|face| !matches!(face.source, cosmic_text::fontdb::Source::Binary(_)))
387 .map_or(0, |_| bytes.len());
388 (
389 motion_core::FontIdentity {
390 name: face.map(|face| face.post_script_name.clone()),
391 file_hash: hash.finish(),
392 },
393 retained,
394 )
395 })
396 .0
397 .clone()
398 }
399
400 pub fn used_fonts(&self) -> Vec<motion_core::FontIdentity> {
402 let used = self.used_fonts.lock_unpoisoned();
403 let mut fonts = used
404 .values()
405 .map(|(font, _)| font.clone())
406 .collect::<Vec<_>>();
407 fonts.sort_by(|left, right| {
408 (&left.name, left.file_hash).cmp(&(&right.name, right.file_hash))
409 });
410 fonts
411 }
412}
413
414impl Default for FontLibrary {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420impl std::panic::RefUnwindSafe for FontLibrary {}
421
422impl From<FontLibrary> for motion_core::asset::environment::FontEnvironment {
423 fn from(fonts: FontLibrary) -> Self {
424 let identity = fonts.snapshot_id();
425 let usage = fonts.clone();
426 Self::new(fonts, identity).with_usage(move || usage.used_fonts())
427 }
428}
429
430impl From<&FontLibrary> for motion_core::asset::environment::FontEnvironment {
431 fn from(fonts: &FontLibrary) -> Self {
432 fonts.clone().into()
433 }
434}
435
436impl std::panic::UnwindSafe for FontLibrary {}
437
438impl std::fmt::Debug for FontLibrary {
439 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440 formatter
441 .debug_struct("FontLibrary")
442 .field("faces", &self.len())
443 .field("bytes", &self.bytes)
444 .field("snapshot", &self.snapshot_id)
445 .finish()
446 }
447}
448
449#[derive(Clone, Debug)]
454pub struct FontFace {
455 bytes: Arc<[u8]>,
456 index: u32,
457}
458
459impl FontFace {
460 pub fn from_bytes(
463 bytes: Vec<u8>,
464 face_index: u32,
465 ) -> Result<Self, motion_core::ValidationError> {
466 if bytes.len() > MAX_FONT_FACE_BYTES {
467 return Err(motion_core::ValidationError::limit(
468 "font",
469 "font_bytes",
470 "font exceeds 32 MiB",
471 ));
472 }
473 skrifa::FontRef::from_index(&bytes, face_index).map_err(|_| {
474 crate::text::invalid("font", "Invalid font data or collection face index")
475 })?;
476 Ok(Self {
477 bytes: bytes.into(),
478 index: face_index,
479 })
480 }
481
482 pub fn byte_len(&self) -> usize {
484 self.bytes.len()
485 }
486
487 pub fn index(&self) -> u32 {
489 self.index
490 }
491
492 pub fn library(&self) -> Result<FontLibrary, motion_core::ValidationError> {
494 let mut fonts = FontLibrary::new();
495 fonts
496 .load_data(self.bytes.clone())
497 .map_err(|error| crate::text::invalid("font", &error.to_string()))?;
498 let family = fonts
499 .database()
500 .faces()
501 .find(|face| face.index == self.index)
502 .and_then(|face| face.families.first().map(|family| family.0.clone()))
503 .ok_or_else(|| crate::text::invalid("font", "Explicit font face has no family"))?;
504 Ok(fonts.default_family(family))
505 }
506}
507
508fn identity() -> u64 {
509 static NEXT: AtomicU64 = AtomicU64::new(1);
510 NEXT.fetch_add(1, Ordering::Relaxed)
511}
512
513fn invalid(message: &str) -> motion_core::Error {
514 motion_core::Error::asset(
515 "font",
516 "<library>",
517 std::io::Error::new(std::io::ErrorKind::InvalidData, message),
518 )
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn indexed_file_sources_remain_lazy_until_layout() -> motion_core::Result<()> {
527 let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
528 .join("../../tests/fixtures/fonts/NotoSans-Regular.ttf");
529 let mut discovered = fontdb::Database::new();
530 discovered.load_font_file(fixture)?;
531 let mut fonts = FontLibrary::new();
532 let empty = fonts.clone();
533 assert!(fonts.index_faces(discovered.faces().cloned())? > 0);
534 assert!(empty.is_empty());
535 assert_eq!(fonts.retained_bytes(), 0);
536 assert!(
537 fonts
538 .lock_font_system()?
539 .db()
540 .faces()
541 .all(|face| matches!(face.source, cosmic_text::fontdb::Source::File(_)))
542 );
543 let paragraph = crate::Text::new("Lazy font")
544 .family("Noto Sans")
545 .layout(&fonts)?;
546 assert!(paragraph.bounds().width() > 0.0);
547 assert!(!paragraph.glyphs().is_empty());
548 assert!(fonts.retained_bytes() > 0);
549 assert_eq!(fonts.used_fonts().len(), 1);
550 assert_eq!(
551 motion_core::FontEnvironment::from(&fonts).used_fonts(),
552 fonts.used_fonts()
553 );
554 Ok(())
555 }
556
557 #[test]
558 fn empty_font_libraries_are_explicit_and_invalid_registration_is_atomic() {
559 let mut fonts = FontLibrary::new();
560 assert!(fonts.is_empty());
561 assert_eq!(fonts.retained_bytes(), 0);
562 let snapshot = fonts.clone();
563 assert!(Arc::ptr_eq(&fonts.database, &snapshot.database));
564 assert!(fonts.load_data(Arc::from(&b"invalid font"[..])).is_err());
565 assert!(Arc::ptr_eq(&fonts.database, &snapshot.database));
566 assert_eq!(fonts.snapshot_id(), snapshot.snapshot_id());
567 assert_eq!(fonts.len(), 0);
568 assert!(fonts.load_system_family("").is_err());
569 assert_eq!(fonts.snapshot_id(), snapshot.snapshot_id());
570 }
571
572 #[test]
573 #[ignore = "explicitly discovers installed fonts; requires MOTION_TEST_SYSTEM_FAMILY"]
574 fn explicitly_requested_installed_family_is_frozen_and_usable() {
575 let mut fonts = FontLibrary::new();
576 let empty = fonts.clone();
577 let family = std::env::var("MOTION_TEST_SYSTEM_FAMILY").expect("explicit installed family");
578 let added = fonts.load_system_family(&family).unwrap();
579 assert!(added > 0);
580 assert_eq!(fonts.retained_bytes(), 0);
581 assert!(empty.is_empty());
582 let paragraph = crate::Text::new("Installed family")
583 .family(family)
584 .layout(&fonts)
585 .unwrap();
586 assert!(paragraph.bounds().width() > 0.0);
587 println!(
588 "explicit installed family: faces={added} immutable_bytes={} shape=pass",
589 fonts.retained_bytes()
590 );
591 }
592}