1use std::io::Read;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24use cranpose_ui::text::{FontFamily, FontFile, FontStyle, FontWeight};
25
26use crate::software_text_raster::{
27 default_software_text_font, SoftwareTextFont, SoftwareTextFontError, SoftwareTextFontSet,
28};
29
30pub const ANDROID_SYSTEM_FONT_DIR: &str = "/system/fonts";
32
33pub const DEFAULT_SYSTEM_FAMILY_WEIGHTS: &[FontWeight] =
36 &[FontWeight::NORMAL, FontWeight::MEDIUM, FontWeight::BOLD];
37
38#[derive(Debug, thiserror::Error)]
44pub enum FontLoadError {
45 #[error("font family declares no faces")]
46 EmptyFamily,
47 #[error("font family is not backed by files, so it has nothing to load")]
48 NotFileBacked,
49 #[error("no system font file for this family under {directory}")]
50 NoSystemFontFile { directory: PathBuf },
51 #[error("failed to read font file {path}: {source}")]
52 Read {
53 path: PathBuf,
54 #[source]
55 source: std::io::Error,
56 },
57 #[error("failed to parse font file {path}: {source}")]
58 Parse {
59 path: PathBuf,
60 #[source]
61 source: SoftwareTextFontError,
62 },
63 #[error("failed to parse font bytes: {source}")]
64 ParseBytes {
65 #[source]
66 source: SoftwareTextFontError,
67 },
68}
69
70#[derive(Clone, Default)]
76pub struct SoftwareTextFontRegistry {
77 faces: Vec<SoftwareTextFont>,
78}
79
80impl SoftwareTextFontRegistry {
81 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn register_family(&mut self, family: &FontFamily) -> Result<(), FontLoadError> {
95 let files = font_files_for(family)?;
96 if files.is_empty() {
97 return Err(FontLoadError::EmptyFamily);
98 }
99
100 let mut reads = FontFileReads::default();
101 let mut first_error = None;
102 let mut loaded = 0usize;
103 for file in &files {
104 match self.register_read_face(
105 &mut reads,
106 family,
107 file.weight,
108 file.style,
109 Path::new(&file.path),
110 ) {
111 Ok(()) => loaded += 1,
112 Err(error) => first_error = first_error.or(Some(error)),
113 }
114 }
115
116 match first_error {
117 Some(error) if loaded == 0 => Err(error),
118 _ => Ok(()),
119 }
120 }
121
122 pub fn register_face_path(
124 &mut self,
125 family: &FontFamily,
126 weight: FontWeight,
127 style: FontStyle,
128 path: impl AsRef<Path>,
129 ) -> Result<(), FontLoadError> {
130 self.register_read_face(
131 &mut FontFileReads::default(),
132 family,
133 weight,
134 style,
135 path.as_ref(),
136 )
137 }
138
139 fn register_read_face(
140 &mut self,
141 reads: &mut FontFileReads,
142 family: &FontFamily,
143 weight: FontWeight,
144 style: FontStyle,
145 path: &Path,
146 ) -> Result<(), FontLoadError> {
147 let bytes = reads.read(path)?;
148 let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes.to_vec())
149 .map_err(|source| FontLoadError::Parse {
150 path: path.to_path_buf(),
151 source,
152 })?;
153 self.faces.push(face);
154 Ok(())
155 }
156
157 pub fn register_face_reader(
164 &mut self,
165 family: &FontFamily,
166 weight: FontWeight,
167 style: FontStyle,
168 reader: &mut impl Read,
169 ) -> Result<(), FontLoadError> {
170 let mut bytes = Vec::new();
171 reader
172 .read_to_end(&mut bytes)
173 .map_err(|source| FontLoadError::Read {
174 path: PathBuf::new(),
175 source,
176 })?;
177 self.register_face_bytes(family, weight, style, bytes)
178 }
179
180 pub fn register_face_bytes(
182 &mut self,
183 family: &FontFamily,
184 weight: FontWeight,
185 style: FontStyle,
186 bytes: impl Into<Vec<u8>>,
187 ) -> Result<(), FontLoadError> {
188 let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes)
189 .map_err(|source| FontLoadError::ParseBytes { source })?;
190 self.faces.push(face);
191 Ok(())
192 }
193
194 pub fn register_fallback_bytes(
199 &mut self,
200 bytes: impl Into<Vec<u8>>,
201 ) -> Result<(), FontLoadError> {
202 let face = SoftwareTextFont::from_bytes(bytes)
203 .map_err(|source| FontLoadError::ParseBytes { source })?;
204 self.faces.push(face);
205 Ok(())
206 }
207
208 pub fn register_system_family(
221 &mut self,
222 directory: impl AsRef<Path>,
223 family: &FontFamily,
224 weights: &[FontWeight],
225 ) -> Result<(), FontLoadError> {
226 let directory = directory.as_ref();
227 let mut reads = FontFileReads::default();
228 let mut first_error = None;
229 let mut loaded = 0usize;
230 for weight in weights {
231 match self.register_read_system_face(
232 &mut reads,
233 directory,
234 family,
235 *weight,
236 FontStyle::Normal,
237 ) {
238 Ok(()) => loaded += 1,
239 Err(error) => first_error = first_error.or(Some(error)),
240 }
241 }
242
243 match first_error {
244 Some(error) if loaded == 0 => Err(error),
245 _ => Ok(()),
246 }
247 }
248
249 pub fn register_system_face(
252 &mut self,
253 directory: impl AsRef<Path>,
254 family: &FontFamily,
255 weight: FontWeight,
256 style: FontStyle,
257 ) -> Result<(), FontLoadError> {
258 self.register_read_system_face(
259 &mut FontFileReads::default(),
260 directory.as_ref(),
261 family,
262 weight,
263 style,
264 )
265 }
266
267 fn register_read_system_face(
268 &mut self,
269 reads: &mut FontFileReads,
270 directory: &Path,
271 family: &FontFamily,
272 weight: FontWeight,
273 style: FontStyle,
274 ) -> Result<(), FontLoadError> {
275 let path = system_font_file(directory, family, weight).ok_or_else(|| {
276 FontLoadError::NoSystemFontFile {
277 directory: directory.to_path_buf(),
278 }
279 })?;
280 self.register_read_face(reads, family, weight, style, &path)
281 }
282
283 pub fn faces(&self) -> &[SoftwareTextFont] {
285 &self.faces
286 }
287
288 pub fn is_empty(&self) -> bool {
289 self.faces.is_empty()
290 }
291
292 pub fn into_font_set_or_default(mut self, fonts: &[&[u8]]) -> SoftwareTextFontSet {
300 for bytes in fonts {
301 let _ = self.register_fallback_bytes((*bytes).to_vec());
303 }
304 if self.faces.is_empty() {
305 if let Some(default_font) = default_software_text_font() {
306 self.faces.push(default_font);
307 }
308 }
309 SoftwareTextFontSet::from_faces(self.faces)
310 }
311}
312
313#[derive(Default)]
320struct FontFileReads {
321 entries: Vec<(PathBuf, Arc<[u8]>)>,
322}
323
324impl FontFileReads {
325 fn read(&mut self, path: &Path) -> Result<Arc<[u8]>, FontLoadError> {
326 if let Some((_, bytes)) = self.entries.iter().find(|(read, _)| read == path) {
327 return Ok(Arc::clone(bytes));
328 }
329 let bytes: Arc<[u8]> = std::fs::read(path)
330 .map_err(|source| FontLoadError::Read {
331 path: path.to_path_buf(),
332 source,
333 })?
334 .into();
335 self.entries.push((path.to_path_buf(), Arc::clone(&bytes)));
336 Ok(bytes)
337 }
338}
339
340pub fn system_font_file(
346 directory: &Path,
347 family: &FontFamily,
348 weight: FontWeight,
349) -> Option<PathBuf> {
350 let files = system_family_files(family)?;
351 files
352 .weighted
353 .iter()
354 .filter(|(candidate_weight, _)| *candidate_weight == weight.value())
355 .map(|(_, name)| directory.join(name))
356 .chain(files.regular.iter().map(|name| directory.join(name)))
357 .find(|path| path.is_file())
358}
359
360struct SystemFamilyFiles {
362 regular: &'static [&'static str],
363 weighted: &'static [(u16, &'static str)],
364}
365
366fn system_family_files(family: &FontFamily) -> Option<SystemFamilyFiles> {
367 match family {
371 FontFamily::Default | FontFamily::SansSerif => Some(SystemFamilyFiles {
372 regular: &[
373 "Roboto-Regular.ttf",
374 "RobotoStatic-Regular.ttf",
375 "NotoSans-Regular.ttf",
376 "DroidSans.ttf",
377 ],
378 weighted: &[
379 (300, "Roboto-Light.ttf"),
380 (500, "Roboto-Medium.ttf"),
381 (700, "Roboto-Bold.ttf"),
382 (900, "Roboto-Black.ttf"),
383 ],
384 }),
385 FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
387 regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
388 weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
389 }),
390 FontFamily::Monospace => Some(SystemFamilyFiles {
391 regular: &[
392 "DroidSansMono.ttf",
393 "RobotoMono-Regular.ttf",
394 "CutiveMono-Regular.ttf",
395 ],
396 weighted: &[(700, "RobotoMono-Bold.ttf")],
397 }),
398 FontFamily::Cursive => Some(SystemFamilyFiles {
399 regular: &["DancingScript-Regular.ttf"],
400 weighted: &[(700, "DancingScript-Bold.ttf")],
401 }),
402 FontFamily::Named(_) | FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_) => None,
403 }
404}
405
406fn font_files_for(family: &FontFamily) -> Result<Vec<FontFile>, FontLoadError> {
407 match family {
408 FontFamily::FileBacked(file_backed) => Ok(file_backed.fonts.clone()),
409 FontFamily::LoadedTypeface(typeface) => Ok(vec![FontFile::new(typeface.path.clone())]),
410 _ => Err(FontLoadError::NotFileBacked),
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use cranpose_ui::text::{SpanStyle, TextStyle};
418 use std::io::Cursor;
419
420 const REGULAR: &[u8] = include_bytes!("../assets/NotoSansMerged.ttf");
421 const BOLD: &[u8] = include_bytes!("../assets/NotoSansBold.ttf");
422
423 fn style_for(family: &FontFamily, weight: FontWeight) -> TextStyle {
424 TextStyle {
425 span_style: SpanStyle {
426 font_family: Some(family.clone()),
427 font_weight: Some(weight),
428 ..Default::default()
429 },
430 ..Default::default()
431 }
432 }
433
434 struct ScratchDir(PathBuf);
438
439 impl ScratchDir {
440 fn new(name: &str) -> Self {
441 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
442 .join("../../../target/test-output/cranpose-font-source")
443 .join(name);
444 let _ = std::fs::remove_dir_all(&path);
445 std::fs::create_dir_all(&path).expect("scratch directory");
446 Self(path)
447 }
448
449 fn write(&self, name: &str, bytes: &[u8]) -> PathBuf {
450 let path = self.0.join(name);
451 std::fs::write(&path, bytes).expect("scratch font file");
452 path
453 }
454
455 fn path(&self) -> &Path {
456 &self.0
457 }
458 }
459
460 impl Drop for ScratchDir {
461 fn drop(&mut self) {
462 let _ = std::fs::remove_dir_all(&self.0);
463 }
464 }
465
466 #[test]
467 fn register_family_picks_the_face_matching_the_requested_weight() {
468 let dir = ScratchDir::new("weights");
469 let regular = dir.write("Test-Regular.ttf", REGULAR);
470 let bold = dir.write("Test-Bold.ttf", BOLD);
471 let family = FontFamily::file_backed(vec![
472 FontFile::new(regular.to_string_lossy().into_owned()),
473 FontFile::new(bold.to_string_lossy().into_owned()).with_weight(FontWeight::BOLD),
474 ])
475 .expect("file-backed family");
476
477 let mut registry = SoftwareTextFontRegistry::new();
478 registry.register_family(&family).expect("family loads");
479 let fonts = registry.into_font_set_or_default(&[]);
480
481 let resolved_regular = fonts
482 .resolve(&style_for(&family, FontWeight::NORMAL))
483 .expect("regular face");
484 let resolved_bold = fonts
485 .resolve(&style_for(&family, FontWeight::BOLD))
486 .expect("bold face");
487
488 assert_eq!(resolved_regular.weight(), FontWeight::NORMAL);
489 assert_eq!(resolved_bold.weight(), FontWeight::BOLD);
490 assert_ne!(
491 resolved_regular.content_hash(),
492 resolved_bold.content_hash(),
493 "distinct faces must key the glyph atlas distinctly"
494 );
495 }
496
497 #[test]
498 fn register_family_honours_a_declared_weight_over_the_face_header() {
499 let dir = ScratchDir::new("declared");
500 let path = dir.write("Test-Regular.ttf", REGULAR);
501 let family =
502 FontFamily::file_backed(vec![
503 FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight::MEDIUM)
504 ])
505 .expect("file-backed family");
506
507 let mut registry = SoftwareTextFontRegistry::new();
508 registry.register_family(&family).expect("family loads");
509 let fonts = registry.into_font_set_or_default(&[]);
510
511 let resolved = fonts
512 .resolve(&style_for(&family, FontWeight::MEDIUM))
513 .expect("declared face");
514 assert_eq!(resolved.weight(), FontWeight::MEDIUM);
515 }
516
517 #[test]
518 fn register_family_reports_a_missing_file_without_panicking() {
519 let dir = ScratchDir::new("missing");
520 let family = FontFamily::file_backed(vec![FontFile::new(
521 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
522 )])
523 .expect("file-backed family");
524
525 let mut registry = SoftwareTextFontRegistry::new();
526 let error = registry
527 .register_family(&family)
528 .expect_err("a missing file cannot register");
529 assert!(matches!(error, FontLoadError::Read { .. }), "{error}");
530 assert!(registry.is_empty());
531 }
532
533 #[test]
534 fn register_family_reports_a_corrupt_file_without_panicking() {
535 let dir = ScratchDir::new("corrupt");
536 let path = dir.write("Corrupt.ttf", b"this is not a font");
537 let family =
538 FontFamily::file_backed(vec![FontFile::new(path.to_string_lossy().into_owned())])
539 .expect("file-backed family");
540
541 let mut registry = SoftwareTextFontRegistry::new();
542 let error = registry
543 .register_family(&family)
544 .expect_err("a corrupt file cannot register");
545 assert!(matches!(error, FontLoadError::Parse { .. }), "{error}");
546 assert!(registry.is_empty());
547 }
548
549 #[test]
550 #[cfg(feature = "embedded-default-font")]
551 fn a_family_that_failed_to_load_falls_back_to_the_default_face() {
552 let dir = ScratchDir::new("fallback");
553 let family = FontFamily::file_backed(vec![FontFile::new(
554 dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
555 )])
556 .expect("file-backed family");
557
558 let mut registry = SoftwareTextFontRegistry::new();
559 let _ = registry.register_family(&family);
560 let fonts = registry.into_font_set_or_default(&[]);
561
562 let resolved = fonts
563 .resolve(&style_for(&family, FontWeight::NORMAL))
564 .expect("fallback face");
565 assert_eq!(
566 resolved.content_hash(),
567 fonts.default_font().expect("default face").content_hash()
568 );
569 }
570
571 #[test]
572 fn a_partly_loadable_family_keeps_the_faces_that_did_load() {
573 let dir = ScratchDir::new("partial");
574 let regular = dir.write("Test-Regular.ttf", REGULAR);
575 let family = FontFamily::file_backed(vec![
576 FontFile::new(regular.to_string_lossy().into_owned()),
577 FontFile::new(dir.path().join("Absent.ttf").to_string_lossy().into_owned())
578 .with_weight(FontWeight::BOLD),
579 ])
580 .expect("file-backed family");
581
582 let mut registry = SoftwareTextFontRegistry::new();
583 registry
584 .register_family(&family)
585 .expect("one readable face is enough");
586 assert_eq!(registry.faces().len(), 1);
587 }
588
589 #[test]
590 fn register_face_reader_accepts_a_font_that_is_not_a_file() {
591 let family = FontFamily::named("Bundled");
592 let mut registry = SoftwareTextFontRegistry::new();
593 registry
594 .register_face_reader(
595 &family,
596 FontWeight::NORMAL,
597 FontStyle::Normal,
598 &mut Cursor::new(REGULAR.to_vec()),
599 )
600 .expect("streamed face loads");
601
602 let fonts = registry.into_font_set_or_default(&[]);
603 let resolved = fonts
604 .resolve(&style_for(&family, FontWeight::NORMAL))
605 .expect("streamed face");
606 assert_eq!(resolved.registered_family(), {
607 let mut expected = SoftwareTextFontRegistry::new();
608 expected
609 .register_face_bytes(
610 &family,
611 FontWeight::NORMAL,
612 FontStyle::Normal,
613 REGULAR.to_vec(),
614 )
615 .expect("face loads");
616 expected.faces()[0].registered_family()
617 });
618 }
619
620 #[test]
621 #[cfg(feature = "embedded-default-font")]
622 fn an_empty_registry_falls_back_to_the_embedded_default_face() {
623 let fonts = SoftwareTextFontRegistry::new().into_font_set_or_default(&[]);
624 assert!(
625 fonts.default_font().is_some(),
626 "the embedded default font must still serve apps that supply nothing"
627 );
628 assert!(fonts
629 .resolve(&TextStyle::default())
630 .is_some_and(|font| font.registered_family().is_none()));
631 }
632
633 #[test]
634 fn system_font_file_prefers_a_weight_specific_static_face() {
635 let dir = ScratchDir::new("system-static");
636 dir.write("Roboto-Regular.ttf", REGULAR);
637 let medium = dir.write("Roboto-Medium.ttf", BOLD);
638
639 assert_eq!(
640 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
641 Some(medium)
642 );
643 }
644
645 #[test]
646 fn system_font_file_falls_back_to_the_regular_face_for_other_weights() {
647 let dir = ScratchDir::new("system-regular");
648 let regular = dir.write("Roboto-Regular.ttf", REGULAR);
649
650 assert_eq!(
651 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
652 Some(regular)
653 );
654 }
655
656 #[test]
657 fn system_font_file_reports_nothing_when_the_directory_is_empty() {
658 let dir = ScratchDir::new("system-empty");
659 assert_eq!(
660 system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::NORMAL),
661 None
662 );
663 assert_eq!(
664 system_font_file(dir.path(), &FontFamily::named("Roboto"), FontWeight::NORMAL),
665 None
666 );
667 }
668
669 #[test]
670 fn register_system_family_binds_the_generic_alias_to_the_platform_face() {
671 let dir = ScratchDir::new("system-family");
672 dir.write("Roboto-Regular.ttf", REGULAR);
673 dir.write("Roboto-Bold.ttf", BOLD);
674
675 let mut registry = SoftwareTextFontRegistry::new();
676 registry
677 .register_system_family(
678 dir.path(),
679 &FontFamily::SansSerif,
680 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
681 )
682 .expect("system family loads");
683 let fonts = registry.into_font_set_or_default(&[]);
684
685 assert!(fonts.has_registered_family(&FontFamily::SansSerif));
686 let bold = fonts
687 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::BOLD))
688 .expect("bold system face");
689 assert_eq!(bold.weight(), FontWeight::BOLD);
690 assert_eq!(bold.registered_family(), {
691 let key = fonts
692 .resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
693 .expect("regular system face")
694 .registered_family();
695 key
696 });
697 }
698
699 #[test]
700 fn register_system_family_reports_an_absent_font_directory() {
701 let mut registry = SoftwareTextFontRegistry::new();
702 let error = registry
703 .register_system_family(
704 Path::new("/definitely/not/a/font/directory"),
705 &FontFamily::SansSerif,
706 DEFAULT_SYSTEM_FAMILY_WEIGHTS,
707 )
708 .expect_err("an absent directory cannot register");
709 assert!(
710 matches!(error, FontLoadError::NoSystemFontFile { .. }),
711 "{error}"
712 );
713 assert!(registry.is_empty());
714 }
715
716 #[test]
717 fn register_family_rejects_a_family_that_names_no_files() {
718 let mut registry = SoftwareTextFontRegistry::new();
719 let error = registry
720 .register_family(&FontFamily::named("Roboto"))
721 .expect_err("a named family has nothing to read");
722 assert!(matches!(error, FontLoadError::NotFileBacked), "{error}");
723 }
724
725 #[test]
726 fn loaded_typeface_families_register_their_single_file() {
727 let dir = ScratchDir::new("typeface");
728 let path = dir.write("Test-Regular.ttf", REGULAR);
729 let family = FontFamily::loaded_typeface_path(path.to_string_lossy().into_owned());
730
731 let mut registry = SoftwareTextFontRegistry::new();
732 registry.register_family(&family).expect("typeface loads");
733 let fonts = registry.into_font_set_or_default(&[]);
734
735 assert!(fonts.has_registered_family(&family));
736 assert!(fonts
737 .resolve(&style_for(&family, FontWeight::NORMAL))
738 .is_some_and(|font| font.registered_family().is_some()));
739 }
740}