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