1use std::collections::{HashMap, HashSet, VecDeque};
7#[cfg(feature = "system-fonts")]
8use std::path::{Path, PathBuf};
9#[cfg(feature = "system-fonts")]
10use std::sync::OnceLock;
11use std::sync::{Arc, Mutex};
12
13#[cfg(all(test, feature = "system-fonts"))]
14use std::sync::atomic::{AtomicUsize, Ordering};
15
16use crate::error::{LayoutError, Result};
17use crate::output::FontId;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct FontFile {
22 pub family: String,
24 pub data: Vec<u8>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30struct FontKey {
31 family: String,
32 bold: bool,
33 italic: bool,
34}
35
36#[derive(Debug, Clone, Copy)]
38pub struct FontMetrics {
39 pub ascent: f64,
41 pub descent: f64,
43 pub line_gap: f64,
45 pub units_per_em: u16,
47}
48
49#[derive(Debug, Clone)]
51pub struct ShapedText {
52 pub glyph_ids: Vec<u16>,
54 pub advances: Vec<f64>,
56 pub width: f64,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61struct ShapingKey {
62 font_id: FontId,
63 text: String,
64 size_bits: u64,
65}
66
67struct ShapingMemo {
68 entries: VecDeque<(ShapingKey, ShapedText, usize, u64)>,
69 bytes: usize,
70 #[cfg(test)]
71 hits: usize,
72 #[cfg(test)]
73 misses: usize,
74}
75
76impl ShapingMemo {
77 fn new() -> Self {
78 Self {
79 entries: VecDeque::new(),
80 bytes: 0,
81 #[cfg(test)]
82 hits: 0,
83 #[cfg(test)]
84 misses: 0,
85 }
86 }
87
88 fn clear(&mut self) {
89 self.entries.clear();
90 self.bytes = 0;
91 #[cfg(test)]
92 {
93 self.hits = 0;
94 self.misses = 0;
95 }
96 }
97
98 fn insert(&mut self, key: ShapingKey, shaped: ShapedText) {
99 let entry_bytes = std::mem::size_of::<(ShapingKey, ShapedText, usize, u64)>()
100 + key.text.len()
101 + shaped.glyph_ids.len() * std::mem::size_of::<u16>()
102 + shaped.advances.len() * std::mem::size_of::<f64>();
103 if entry_bytes > SHAPING_CACHE_MAX_BYTES {
104 return;
105 }
106 while self.entries.len() >= SHAPING_CACHE_MAX_ENTRIES
107 || self.bytes.saturating_add(entry_bytes) > SHAPING_CACHE_MAX_BYTES
108 {
109 let Some((_, _, evicted_bytes, _)) = self.entries.pop_front() else {
110 break;
111 };
112 self.bytes = self.bytes.saturating_sub(evicted_bytes);
113 }
114 self.bytes += entry_bytes;
115 let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
116 self.entries
117 .push_back((key, shaped, entry_bytes, fingerprint));
118 }
119}
120
121fn shaping_fingerprint(font_id: FontId, text: &str, size_bits: u64) -> u64 {
122 use std::hash::{Hash, Hasher};
123
124 let mut hasher = std::collections::hash_map::DefaultHasher::new();
125 font_id.hash(&mut hasher);
126 text.hash(&mut hasher);
127 size_bits.hash(&mut hasher);
128 hasher.finish()
129}
130
131const SHAPING_CACHE_MAX_ENTRIES: usize = 2_048;
132const SHAPING_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
133
134#[cfg(feature = "system-fonts")]
135struct FileFontCache {
136 entries: VecDeque<(PathBuf, Arc<[u8]>, usize)>,
137 bytes: usize,
138}
139
140#[cfg(feature = "system-fonts")]
141impl FileFontCache {
142 fn new() -> Self {
143 Self {
144 entries: VecDeque::new(),
145 bytes: 0,
146 }
147 }
148
149 fn clear(&mut self) {
150 self.entries.clear();
151 self.bytes = 0;
152 }
153}
154
155#[cfg(feature = "system-fonts")]
156const FILE_FONT_CACHE_MAX_ENTRIES: usize = 256;
157#[cfg(feature = "system-fonts")]
158const FILE_FONT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
159
160#[cfg(feature = "system-fonts")]
161static NORMAL_FONT_DATABASE: OnceLock<fontdb::Database> = OnceLock::new();
162#[cfg(feature = "system-fonts")]
163static FILE_FONT_CACHE: OnceLock<Mutex<FileFontCache>> = OnceLock::new();
164#[cfg(all(test, feature = "system-fonts"))]
165static SYSTEM_FONT_DISCOVERY_RUNS: AtomicUsize = AtomicUsize::new(0);
166
167struct LoadedFont {
169 db_id: fontdb::ID,
170 id: FontId,
171 family: String,
172 bold: bool,
173 italic: bool,
174 data: Arc<[u8]>,
175 face_index: u32,
176 units_per_em: u16,
177 ascender: i16,
179 descender: i16,
180 line_gap: i16,
181 shaper_data: harfrust::ShaperData,
184}
185
186struct ParagraphFontTrace {
187 ids: Vec<FontId>,
188 overflowed: bool,
189}
190
191pub struct FontManager {
193 db: fontdb::Database,
194 base_db: fontdb::Database,
196 base_caller_aliases: HashMap<String, Vec<fontdb::ID>>,
198 base_caller_families: HashMap<String, Vec<fontdb::ID>>,
200 cache: HashMap<FontKey, usize>,
202 memory_face_data: HashMap<fontdb::ID, Arc<[u8]>>,
204 fonts: Vec<LoadedFont>,
206 next_id: u32,
208 coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
216 coverage_misses: HashSet<char>,
219 additional_fonts: Vec<FontFile>,
221 caller_aliases: HashMap<String, Vec<fontdb::ID>>,
224 caller_families: HashMap<String, Vec<fontdb::ID>>,
227 explicit_aliases: Vec<(String, String)>,
229 explicit_alias_map: HashMap<String, String>,
231 shaping_memo: Mutex<ShapingMemo>,
233 paragraph_font_trace: Option<ParagraphFontTrace>,
235 layout_fonts: Vec<FontId>,
237}
238
239const BROAD_COVERAGE_FAMILIES: &[&str] = &[
244 "Noto Sans CJK SC",
246 "Noto Sans CJK JP",
247 "Noto Sans CJK KR",
248 "Noto Sans CJK TC",
249 "Noto Serif CJK SC",
250 "Source Han Sans SC",
251 "WenQuanYi Zen Hei",
252 "WenQuanYi Micro Hei",
253 "PingFang SC",
255 "PingFang TC",
256 "Hiragino Sans",
257 "Hiragino Kaku Gothic ProN",
258 "Apple SD Gothic Neo",
259 "Songti SC",
260 "STHeiti",
261 "Microsoft YaHei",
263 "Microsoft JhengHei",
264 "SimSun",
265 "SimHei",
266 "NSimSun",
267 "Yu Gothic",
268 "MS Gothic",
269 "Meiryo",
270 "Malgun Gothic",
271 "Arial Unicode MS",
273 "DejaVu Sans",
274];
275
276const RESOLUTION_CACHE_MAX_ENTRIES: usize = 256;
277const COVERAGE_FALLBACK_MAX_ENTRIES: usize = 256;
278const COVERAGE_MISS_MAX_ENTRIES: usize = 4_096;
279const PARAGRAPH_FONT_TRACE_MAX_ENTRIES: usize = 4_096;
280
281const CALLER_ALIAS_MAX_ENTRIES: usize = 256;
283
284const CALLER_ALIAS_MAX_RETAINED_BYTES: usize = 64 * 1024;
287
288fn bounded_caller_aliases(aliases: &[(String, String)]) -> Vec<(String, String)> {
295 let mut bounded = Vec::with_capacity(aliases.len().min(CALLER_ALIAS_MAX_ENTRIES));
296 let mut retained_bytes = 0usize;
297 for (requested, target) in aliases {
298 if bounded.len() == CALLER_ALIAS_MAX_ENTRIES {
299 break;
300 }
301 let normalized_requested = requested.to_lowercase();
302 let entry_bytes = requested
303 .len()
304 .saturating_add(target.len())
305 .saturating_add(normalized_requested.len())
306 .saturating_add(target.len());
307 let next_retained_bytes = retained_bytes.saturating_add(entry_bytes);
308 if next_retained_bytes > CALLER_ALIAS_MAX_RETAINED_BYTES {
309 break;
310 }
311 bounded.push((requested.to_owned(), target.to_owned()));
312 retained_bytes = next_retained_bytes;
313 }
314 bounded
315}
316
317impl Default for FontManager {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323impl FontManager {
324 fn from_base_database(db: fontdb::Database) -> Self {
325 Self {
326 base_db: db.clone(),
327 base_caller_aliases: HashMap::new(),
328 base_caller_families: HashMap::new(),
329 db,
330 cache: HashMap::new(),
331 memory_face_data: HashMap::new(),
332 fonts: Vec::new(),
333 next_id: 0,
334 coverage_fallbacks: HashMap::new(),
335 coverage_misses: HashSet::new(),
336 additional_fonts: Vec::new(),
337 caller_aliases: HashMap::new(),
338 caller_families: HashMap::new(),
339 explicit_aliases: Vec::new(),
340 explicit_alias_map: HashMap::new(),
341 shaping_memo: Mutex::new(ShapingMemo::new()),
342 paragraph_font_trace: None,
343 layout_fonts: Vec::new(),
344 }
345 }
346
347 pub fn new() -> Self {
352 #[cfg(feature = "system-fonts")]
353 {
354 let db = NORMAL_FONT_DATABASE.get_or_init(|| {
355 let mut db = bundled_font_database();
356 db.load_system_fonts();
357 #[cfg(test)]
358 SYSTEM_FONT_DISCOVERY_RUNS.fetch_add(1, Ordering::Relaxed);
359 db
360 });
361 Self::from_base_database(db.clone())
362 }
363
364 #[cfg(not(feature = "system-fonts"))]
365 Self::from_base_database(bundled_font_database())
366 }
367
368 pub fn new_deterministic() -> Result<Self> {
373 Ok(Self::from_base_database(bundled_font_database()))
374 }
375
376 pub fn load_additional_fonts(&mut self, font_files: &[FontFile]) -> bool {
383 if self.additional_fonts == font_files {
384 return false;
385 }
386
387 self.db = self.base_db.clone();
388 self.caller_aliases.clone_from(&self.base_caller_aliases);
389 self.caller_families.clone_from(&self.base_caller_families);
390 for font_file in font_files {
391 Self::load_caller_font(
392 &mut self.db,
393 &mut self.caller_aliases,
394 &mut self.caller_families,
395 &font_file.family,
396 font_file.data.clone(),
397 );
398 }
399 self.cache.clear();
400 self.memory_face_data.clear();
401 self.fonts.clear();
402 self.next_id = 0;
403 self.coverage_fallbacks.clear();
404 self.coverage_misses.clear();
405 self.additional_fonts = font_files.to_vec();
406 if self.shaping_memo.is_poisoned() {
407 self.shaping_memo.clear_poison();
408 }
409 self.shaping_memo
410 .get_mut()
411 .expect("shaping cache poison was cleared")
412 .clear();
413 true
414 }
415
416 pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
421 let mut db = fontdb::Database::new();
422 let mut caller_aliases = HashMap::new();
423 let mut caller_families = HashMap::new();
424 for (family, data) in &fonts {
425 Self::load_caller_font(
426 &mut db,
427 &mut caller_aliases,
428 &mut caller_families,
429 family,
430 data.clone(),
431 );
432 }
433 let mut manager = Self::from_base_database(db);
434 manager.base_caller_aliases = caller_aliases.clone();
435 manager.base_caller_families = caller_families.clone();
436 manager.caller_aliases = caller_aliases;
437 manager.caller_families = caller_families;
438 manager
439 }
440
441 pub fn set_caller_aliases(&mut self, aliases: &[(String, String)]) -> bool {
447 let aliases = bounded_caller_aliases(aliases);
448 if self.explicit_aliases == aliases {
449 return false;
450 }
451
452 self.explicit_alias_map = aliases
453 .iter()
454 .map(|(requested, target)| (requested.to_lowercase(), target.clone()))
455 .collect();
456 self.explicit_aliases = aliases;
457 self.cache.clear();
458 self.coverage_fallbacks.clear();
459 self.coverage_misses.clear();
460 true
461 }
462
463 fn load_caller_font(
464 db: &mut fontdb::Database,
465 aliases: &mut HashMap<String, Vec<fontdb::ID>>,
466 families: &mut HashMap<String, Vec<fontdb::ID>>,
467 family: &str,
468 data: Vec<u8>,
469 ) {
470 let before = db.len();
471 db.load_font_data(data);
472 let loaded_faces = db
473 .faces()
474 .skip(before)
475 .map(|face| {
476 (
477 face.id,
478 face.families
479 .iter()
480 .map(|(name, _)| name.clone())
481 .collect::<Vec<_>>(),
482 )
483 })
484 .collect::<Vec<_>>();
485 for (id, loaded_families) in &loaded_faces {
486 for loaded_family in loaded_families {
487 families.entry(loaded_family.clone()).or_default().push(*id);
488 }
489 }
490 if !loaded_faces.is_empty()
491 && loaded_faces
492 .iter()
493 .all(|(_, families)| !families.iter().any(|loaded| loaded == family))
494 {
495 aliases
496 .entry(family.to_lowercase())
497 .or_default()
498 .extend(loaded_faces.into_iter().map(|(id, _)| id));
499 }
500 }
501
502 #[doc(hidden)]
504 pub fn begin_layout(&mut self) {
505 self.paragraph_font_trace = None;
506 self.layout_fonts = Vec::new();
507 }
508
509 #[doc(hidden)]
511 pub fn begin_paragraph_font_trace(&mut self) {
512 self.paragraph_font_trace = Some(ParagraphFontTrace {
513 ids: Vec::new(),
514 overflowed: false,
515 });
516 }
517
518 #[doc(hidden)]
520 pub fn finish_paragraph_font_trace(&mut self) -> Option<Vec<FontId>> {
521 let mut trace = self.paragraph_font_trace.take()?;
522 if trace.overflowed {
523 return None;
524 }
525 trace.ids.shrink_to_fit();
526 Some(trace.ids)
527 }
528
529 #[doc(hidden)]
531 pub fn replay_layout_font_trace(&mut self, trace: &[FontId]) {
532 for &font_id in trace {
533 self.record_layout_font(font_id);
534 }
535 }
536
537 #[doc(hidden)]
539 pub fn current_layout_fonts(&self) -> &[FontId] {
540 &self.layout_fonts
541 }
542
543 #[doc(hidden)]
545 pub fn every_loaded_font_is_current(&self) -> bool {
546 self.fonts
547 .iter()
548 .map(|font| font.id)
549 .eq(self.layout_fonts.iter().copied())
550 && self
551 .layout_fonts
552 .iter()
553 .enumerate()
554 .all(|(index, font_id)| *font_id == FontId(index as u32))
555 }
556
557 #[doc(hidden)]
561 pub fn retain_current_fonts(&mut self) {
562 let current = self.layout_fonts.iter().copied().collect::<HashSet<_>>();
563 let old_index_ids = self
564 .fonts
565 .iter()
566 .enumerate()
567 .map(|(index, font)| (index, font.id))
568 .collect::<HashMap<_, _>>();
569 self.fonts.retain(|font| current.contains(&font.id));
570 let current_order = self
571 .layout_fonts
572 .iter()
573 .enumerate()
574 .map(|(index, font_id)| (*font_id, index))
575 .collect::<HashMap<_, _>>();
576 self.fonts
577 .sort_by_key(|font| current_order.get(&font.id).copied().unwrap_or(usize::MAX));
578
579 let indices = self
580 .fonts
581 .iter()
582 .enumerate()
583 .map(|(index, font)| (font.id, index))
584 .collect::<HashMap<_, _>>();
585 let old_cache = std::mem::take(&mut self.cache);
586 self.cache = old_cache
587 .into_iter()
588 .filter_map(|(key, old_index)| {
589 let font_id = old_index_ids.get(&old_index)?;
590 Some((key, *indices.get(font_id)?))
591 })
592 .collect();
593 self.coverage_fallbacks.clear();
594 self.coverage_misses.clear();
595 let active_db_ids = self
596 .fonts
597 .iter()
598 .map(|font| font.db_id)
599 .collect::<HashSet<_>>();
600 self.memory_face_data
601 .retain(|db_id, _| active_db_ids.contains(db_id));
602
603 let memo = self
604 .shaping_memo
605 .get_mut()
606 .unwrap_or_else(std::sync::PoisonError::into_inner);
607 memo.entries
608 .retain(|(key, _, _, _)| current.contains(&key.font_id));
609 memo.bytes = memo.entries.iter().map(|(_, _, bytes, _)| bytes).sum();
610 }
611
612 pub fn resolve_font_for_text(
629 &mut self,
630 family: Option<&str>,
631 bold: bool,
632 italic: bool,
633 text: &str,
634 ) -> Result<FontId> {
635 let primary = self.resolve_font(family, bold, italic)?;
636
637 let Some(idx) = self.index_of(primary) else {
638 return Ok(primary);
639 };
640 let missing = self.uncovered(idx, text);
641 if missing.is_empty() {
642 return Ok(primary);
643 }
644
645 match self.font_covering(&missing, bold, italic) {
646 None => Ok(primary),
649 Some(id) => Ok(id),
650 }
651 }
652
653 fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
658 let font = &self.fonts[idx];
659 let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
660 return Vec::new();
661 };
662 let mut seen = HashSet::new();
663 text.chars()
664 .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
665 .filter(|&ch| face.glyph_index(ch).is_none())
666 .filter(|&ch| seen.insert(ch))
667 .collect()
668 }
669
670 fn covers(&self, idx: usize, ch: char) -> bool {
672 let font = &self.fonts[idx];
673 ttf_parser::Face::parse(&font.data, font.face_index)
674 .map(|face| face.glyph_index(ch).is_some())
675 .unwrap_or(false)
676 }
677
678 fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
687 if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
688 return None;
689 }
690
691 let mut best: Option<(usize, usize)> = None; let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
693 let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
694 if covered == 0 {
695 return false;
696 }
697 if best.map(|(n, _)| covered > n).unwrap_or(true) {
698 *best = Some((covered, idx));
699 }
700 covered == missing.len()
701 };
702
703 if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
706 for idx in known {
707 if consider(self, idx, &mut best) {
708 let id = self.fonts[idx].id;
709 self.record_layout_font(id);
710 return Some(id);
711 }
712 }
713 }
714
715 let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
719 .iter()
720 .map(|s| s.to_string())
721 .chain(
722 self.db
723 .faces()
724 .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
725 )
726 .collect();
727
728 for name in candidates {
729 let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
730 continue;
731 };
732 let Some(idx) = self.index_of(id) else {
733 continue;
734 };
735 let complete = consider(self, idx, &mut best);
736 if complete {
737 self.remember_coverage_fallback(bold, italic, idx);
738 return Some(id);
739 }
740 }
741
742 match best {
743 Some((_, idx)) => {
744 self.remember_coverage_fallback(bold, italic, idx);
745 let id = self.fonts[idx].id;
746 self.record_layout_font(id);
747 Some(id)
748 }
749 None => {
750 self.remember_coverage_misses(missing);
751 None
752 }
753 }
754 }
755
756 fn index_of(&self, id: FontId) -> Option<usize> {
758 self.fonts.iter().position(|f| f.id == id)
759 }
760
761 pub fn resolve_font(
764 &mut self,
765 family: Option<&str>,
766 bold: bool,
767 italic: bool,
768 ) -> Result<FontId> {
769 self.resolve_font_inner(family, bold, italic, true)
770 }
771
772 #[doc(hidden)]
774 pub fn resolve_font_for_metrics(
775 &mut self,
776 family: Option<&str>,
777 bold: bool,
778 italic: bool,
779 ) -> Result<FontId> {
780 self.resolve_font_inner(family, bold, italic, false)
781 }
782
783 fn resolve_font_inner(
784 &mut self,
785 family: Option<&str>,
786 bold: bool,
787 italic: bool,
788 record_layout_use: bool,
789 ) -> Result<FontId> {
790 let family_name = family.unwrap_or("Arial");
791
792 let key = FontKey {
793 family: family_name.to_string(),
794 bold,
795 italic,
796 };
797
798 if let Some(idx) = self.cache.get(&key).copied() {
799 let id = self.fonts[idx].id;
800 if record_layout_use {
801 self.record_layout_font(id);
802 }
803 return Ok(id);
804 }
805
806 let requested_key = family_name.to_lowercase();
807 let style = if italic {
808 fontdb::Style::Italic
809 } else {
810 fontdb::Style::Normal
811 };
812 let weight = if bold {
813 fontdb::Weight::BOLD
814 } else {
815 fontdb::Weight::NORMAL
816 };
817
818 let query_family = |family: &str| {
819 if let Some(ids) = self.caller_families.get(family)
820 && let Some(id) = best_caller_face(&self.db, ids, weight, style)
821 {
822 return Some(id);
823 }
824 let query = fontdb::Query {
825 families: &[fontdb::Family::Name(family)],
826 weight,
827 style,
828 stretch: fontdb::Stretch::Normal,
829 };
830 self.db.query(&query)
831 };
832
833 let mut found_id = query_family(family_name);
834 if found_id.is_none()
835 && let Some(alias) = self.explicit_alias_map.get(&requested_key)
836 {
837 found_id = query_family(alias);
838 }
839 if found_id.is_none()
840 && let Some(ids) = self.caller_aliases.get(&requested_key)
841 {
842 found_id = best_caller_face(&self.db, ids, weight, style);
843 }
844
845 let mut fallbacks: Vec<&str> = map_font_name(family_name).to_vec();
848 for generic in &[
849 "Carlito",
850 "Arial",
851 "Liberation Sans",
852 "Helvetica",
853 "DejaVu Sans",
854 "Noto Sans",
855 ] {
856 if !fallbacks.contains(generic) {
857 fallbacks.push(generic);
858 }
859 }
860
861 if found_id.is_none() {
862 for fallback in &fallbacks {
863 let Some(id) = query_family(fallback) else {
864 continue;
865 };
866 found_id = Some(id);
867 break;
868 }
869 }
870
871 if found_id.is_none() {
873 for generic_family in &[
874 fontdb::Family::SansSerif,
875 fontdb::Family::Serif,
876 fontdb::Family::Monospace,
877 ] {
878 let query = fontdb::Query {
879 families: &[*generic_family],
880 weight,
881 style,
882 stretch: fontdb::Stretch::Normal,
883 };
884 if let Some(id) = self.db.query(&query) {
885 found_id = Some(id);
886 break;
887 }
888 }
889 }
890
891 let db_id = found_id.ok_or_else(|| {
892 LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
893 })?;
894
895 if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
899 && let Some(idx) = self
900 .fonts
901 .iter()
902 .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
903 {
904 let id = self.fonts[idx].id;
905 if record_layout_use {
906 self.record_layout_font(id);
907 }
908 return Ok(id);
909 }
910
911 let font_id = FontId(self.next_id);
912 self.next_id += 1;
913
914 let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
917 .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
918
919 let (units_per_em, ascender, descender, line_gap) = {
920 let face = ttf_parser::Face::parse(&data, face_index)
921 .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
922 (
923 face.units_per_em(),
924 face.ascender(),
925 face.descender(),
926 face.line_gap(),
927 )
928 };
929
930 if units_per_em == 0 {
933 return Err(LayoutError::FontParse(format!(
934 "font '{family_name}' declares zero units per em"
935 )));
936 }
937
938 let shaper_data = {
939 let face = harfrust::FontRef::from_index(&data, face_index)
940 .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
941 harfrust::ShaperData::new(&face)
942 };
943
944 let actual_family = self
945 .db
946 .face(db_id)
947 .map(|f| {
948 f.families
949 .first()
950 .map(|(name, _)| name.clone())
951 .unwrap_or_else(|| family_name.to_string())
952 })
953 .unwrap_or_else(|| family_name.to_string());
954
955 let idx = self.fonts.len();
956 self.fonts.push(LoadedFont {
957 db_id,
958 id: font_id,
959 family: actual_family,
960 bold,
961 italic,
962 data,
963 face_index,
964 units_per_em,
965 ascender,
966 descender,
967 line_gap,
968 shaper_data,
969 });
970 self.remember_font_key(key, idx);
971 if record_layout_use {
972 self.record_layout_font(font_id);
973 }
974
975 Ok(font_id)
976 }
977
978 pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
980 let font = self.get_font(font_id)?;
981 let scale = size_pt / font.units_per_em as f64;
982
983 Ok(FontMetrics {
984 ascent: font.ascender as f64 * scale,
985 descent: -(font.descender as f64) * scale, line_gap: font.line_gap as f64 * scale,
987 units_per_em: font.units_per_em,
988 })
989 }
990
991 pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
993 if text.is_empty() {
996 return Ok(ShapedText {
997 glyph_ids: Vec::new(),
998 advances: Vec::new(),
999 width: 0.0,
1000 });
1001 }
1002
1003 let key = ShapingKey {
1004 font_id,
1005 text: text.to_owned(),
1006 size_bits: size_pt.to_bits(),
1007 };
1008 let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
1009 let mut memo = match self.shaping_memo.lock() {
1010 Ok(memo) => memo,
1011 Err(poisoned) => {
1012 let mut memo = poisoned.into_inner();
1013 memo.clear();
1014 self.shaping_memo.clear_poison();
1015 memo
1016 }
1017 };
1018 if let Some(shaped) = memo
1019 .entries
1020 .iter()
1021 .rev()
1022 .find(|(candidate, _, _, candidate_fingerprint)| {
1023 *candidate_fingerprint == fingerprint && candidate == &key
1024 })
1025 .map(|(_, shaped, _, _)| shaped.clone())
1026 {
1027 #[cfg(test)]
1028 {
1029 memo.hits += 1;
1030 }
1031 return Ok(shaped);
1032 }
1033 #[cfg(test)]
1034 {
1035 memo.misses += 1;
1036 }
1037
1038 let font = self.get_font(font_id)?;
1039
1040 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
1041 .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
1042
1043 let shaper = font.shaper_data.shaper(&face).build();
1044
1045 let mut buffer = harfrust::UnicodeBuffer::new();
1046 buffer.push_str(text);
1047 buffer.guess_segment_properties();
1050
1051 let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
1052 let infos = output.glyph_infos();
1053 let positions = output.glyph_positions();
1054
1055 let upem = font.units_per_em as f64;
1056 let scale = size_pt / upem;
1057
1058 let mut glyph_ids = Vec::with_capacity(infos.len());
1059 let mut advances = Vec::with_capacity(positions.len());
1060 let mut total_width = 0.0;
1061
1062 for (info, pos) in infos.iter().zip(positions.iter()) {
1063 glyph_ids.push(info.glyph_id as u16);
1064 let advance = pos.x_advance as f64 * scale;
1065 advances.push(advance);
1066 total_width += advance;
1067 }
1068
1069 let shaped = ShapedText {
1070 glyph_ids,
1071 advances,
1072 width: total_width,
1073 };
1074 memo.insert(key, shaped.clone());
1075 Ok(shaped)
1076 }
1077
1078 pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
1080 let font = self.get_font(font_id)?;
1081 Ok(crate::output::FontData {
1082 id: font.id,
1083 family: font.family.clone(),
1084 data: Arc::clone(&font.data),
1085 face_index: font.face_index,
1086 bold: font.bold,
1087 italic: font.italic,
1088 })
1089 }
1090
1091 pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
1093 self.fonts
1094 .iter()
1095 .map(|f| crate::output::FontData {
1096 id: f.id,
1097 family: f.family.clone(),
1098 data: Arc::clone(&f.data),
1099 face_index: f.face_index,
1100 bold: f.bold,
1101 italic: f.italic,
1102 })
1103 .collect()
1104 }
1105
1106 fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
1107 self.fonts
1108 .iter()
1109 .find(|f| f.id == font_id)
1110 .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
1111 }
1112
1113 fn remember_font_key(&mut self, key: FontKey, index: usize) {
1114 if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
1115 self.cache.insert(key, index);
1116 }
1117 }
1118
1119 fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
1120 let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
1121 if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
1122 known.push(index);
1123 }
1124 }
1125
1126 fn remember_coverage_misses(&mut self, missing: &[char]) {
1127 for &ch in missing {
1128 if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
1129 break;
1130 }
1131 self.coverage_misses.insert(ch);
1132 }
1133 }
1134
1135 fn record_layout_font(&mut self, font_id: FontId) {
1136 if let Some(trace) = self.paragraph_font_trace.as_mut() {
1137 if trace.ids.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
1138 trace.ids.push(font_id);
1139 } else {
1140 trace.overflowed = true;
1141 }
1142 }
1143 if !self.layout_fonts.contains(&font_id) {
1144 self.layout_fonts.push(font_id);
1145 }
1146 }
1147
1148 #[cfg(test)]
1149 fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
1150 let memo = self
1151 .shaping_memo
1152 .lock()
1153 .unwrap_or_else(std::sync::PoisonError::into_inner);
1154 (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
1155 }
1156}
1157
1158fn best_caller_face(
1159 db: &fontdb::Database,
1160 ids: &[fontdb::ID],
1161 weight: fontdb::Weight,
1162 style: fontdb::Style,
1163) -> Option<fontdb::ID> {
1164 const CANDIDATE_FAMILY: &str = "__rdocx_caller_candidate__";
1165
1166 let mut candidates = fontdb::Database::new();
1167 let mut candidate_ids = Vec::with_capacity(ids.len());
1168 for id in ids {
1169 let mut face = db.face(*id)?.clone();
1170 for (family, _) in &mut face.families {
1171 CANDIDATE_FAMILY.clone_into(family);
1172 }
1173 let candidate_id = candidates.push_face_info(face);
1174 candidate_ids.push((candidate_id, *id));
1175 }
1176
1177 let selected = candidates.query(&fontdb::Query {
1178 families: &[fontdb::Family::Name(CANDIDATE_FAMILY)],
1179 weight,
1180 style,
1181 stretch: fontdb::Stretch::Normal,
1182 })?;
1183 candidate_ids
1184 .into_iter()
1185 .find_map(|(candidate, original)| (candidate == selected).then_some(original))
1186}
1187
1188fn bundled_font_database() -> fontdb::Database {
1189 let mut db = fontdb::Database::new();
1190 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
1191 db.load_font_data(data.to_vec());
1192 }
1193 db
1194}
1195
1196fn font_data_for_face(
1197 db: &fontdb::Database,
1198 id: fontdb::ID,
1199 memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
1200) -> Option<(Arc<[u8]>, u32)> {
1201 let face = db.face(id)?;
1202 let face_index = face.index;
1203 match &face.source {
1204 fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
1205 Some(data) => Some((Arc::clone(data), face_index)),
1206 None => {
1207 let data: Arc<[u8]> = Arc::from(data.as_ref().as_ref().to_vec());
1208 memory_face_data.insert(id, Arc::clone(&data));
1209 Some((data, face_index))
1210 }
1211 },
1212 #[cfg(feature = "system-fonts")]
1213 fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
1214 }
1215}
1216
1217#[cfg(feature = "system-fonts")]
1218fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
1219 let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
1220 shared_file_font_bytes_from_cache(cache, path)
1221}
1222
1223#[cfg(feature = "system-fonts")]
1224fn shared_file_font_bytes_from_cache(
1225 cache_lock: &Mutex<FileFontCache>,
1226 path: &Path,
1227) -> Option<Arc<[u8]>> {
1228 let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1229 let mut cache = match cache_lock.lock() {
1230 Ok(cache) => cache,
1231 Err(poisoned) => {
1232 let mut cache = poisoned.into_inner();
1233 cache.clear();
1234 cache_lock.clear_poison();
1235 cache
1236 }
1237 };
1238 if let Some(index) = cache
1239 .entries
1240 .iter()
1241 .position(|(candidate, _, _)| candidate == &identity)
1242 {
1243 let entry = cache.entries.remove(index).expect("cache index exists");
1244 let bytes = Arc::clone(&entry.1);
1245 cache.entries.push_back(entry);
1246 return Some(bytes);
1247 }
1248
1249 let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
1250 cache_file_font_bytes(&mut cache, identity, bytes)
1251}
1252
1253#[cfg(feature = "system-fonts")]
1254fn cache_file_font_bytes(
1255 cache: &mut FileFontCache,
1256 identity: PathBuf,
1257 bytes: Arc<[u8]>,
1258) -> Option<Arc<[u8]>> {
1259 let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
1260 .saturating_add(identity.as_os_str().len())
1261 .saturating_add(bytes.len());
1262 if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
1263 while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
1264 || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
1265 {
1266 let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
1267 break;
1268 };
1269 cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
1270 }
1271 cache.bytes += entry_bytes;
1272 cache
1273 .entries
1274 .push_back((identity, Arc::clone(&bytes), entry_bytes));
1275 }
1276 Some(bytes)
1277}
1278
1279fn map_font_name(name: &str) -> &[&str] {
1286 match name {
1287 "Calibri" => &["Calibri", "Carlito"],
1288 "Calibri Light" => &["Calibri Light", "Carlito"],
1289 "Cambria" => &["Cambria", "Caladea"],
1290 "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
1291 "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
1292 "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
1293 "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
1294 "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
1295 "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
1296 "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
1297 "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
1298 "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
1299 "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
1300 "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
1301 "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
1302 "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
1303 "Impact" => &["Impact", "Liberation Sans", "Arial"],
1304 "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
1305 "Symbol" => &["Symbol", "DejaVu Sans"],
1306 "Wingdings" => &["Wingdings", "Symbol"],
1307 _ => &[],
1308 }
1309}
1310
1311#[cfg(test)]
1312mod tests {
1313 use super::*;
1314 use crate::bundled_fonts::bundled_font_data;
1315
1316 #[test]
1317 fn caller_font_labels_resolve_after_exact_embedded_families() {
1318 let caladea = bundled_font_data()
1319 .into_iter()
1320 .find(|(family, _)| *family == "Caladea")
1321 .expect("Caladea is bundled")
1322 .1;
1323 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1324 manager.load_additional_fonts(&[
1325 FontFile {
1326 family: "Document Serif".to_owned(),
1327 data: caladea.to_vec(),
1328 },
1329 FontFile {
1330 family: "Carlito".to_owned(),
1331 data: caladea.to_vec(),
1332 },
1333 ]);
1334
1335 let aliased = manager
1336 .resolve_font(Some("Document Serif"), false, false)
1337 .expect("caller label resolves");
1338 assert_eq!(
1339 manager.fonts[manager.index_of(aliased).unwrap()].family,
1340 "Caladea"
1341 );
1342
1343 let exact = manager
1344 .resolve_font(Some("Carlito"), false, false)
1345 .expect("embedded family resolves exactly");
1346 assert_eq!(
1347 manager.fonts[manager.index_of(exact).unwrap()].family,
1348 "Carlito"
1349 );
1350
1351 manager.set_caller_aliases(&[("Document Serif".to_owned(), "Carlito".to_owned())]);
1352 let explicit = manager
1353 .resolve_font(Some("Document Serif"), false, false)
1354 .expect("explicit alias precedes label-derived alias");
1355 assert_eq!(
1356 manager.fonts[manager.index_of(explicit).unwrap()].family,
1357 "Carlito"
1358 );
1359
1360 manager.load_additional_fonts(&[FontFile {
1361 family: "Caladea".to_owned(),
1362 data: caladea.to_vec(),
1363 }]);
1364 assert!(manager.caller_aliases.is_empty());
1365
1366 manager.set_caller_aliases(&[("Arial".to_owned(), "Caladea".to_owned())]);
1367 let caller_alias = manager
1368 .resolve_font(Some("Arial"), false, false)
1369 .expect("explicit alias resolves before mapped fallback");
1370 assert_eq!(
1371 manager.fonts[manager.index_of(caller_alias).unwrap()].family,
1372 "Caladea"
1373 );
1374 let generic = manager
1375 .resolve_font(Some("Unmapped Document Family"), false, false)
1376 .expect("generic fallback remains available");
1377 assert_eq!(
1378 manager.fonts[manager.index_of(generic).unwrap()].family,
1379 "Carlito"
1380 );
1381 }
1382
1383 #[test]
1384 fn caller_alias_updates_preserve_bytes_and_invalidate_resolution_state() {
1385 let caladea = bundled_font_data()
1386 .into_iter()
1387 .find(|(family, _)| *family == "Caladea")
1388 .expect("Caladea is bundled")
1389 .1;
1390 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1391 manager.load_additional_fonts(&[FontFile {
1392 family: "Caladea".to_owned(),
1393 data: caladea.to_vec(),
1394 }]);
1395 let aliases = vec![
1396 ("Document Serif A".to_owned(), "Caladea".to_owned()),
1397 ("Document Serif B".to_owned(), "Caladea".to_owned()),
1398 ];
1399 assert!(manager.set_caller_aliases(&aliases));
1400
1401 let first = manager
1402 .resolve_font(Some("Document Serif A"), false, false)
1403 .expect("first alias resolves");
1404 let second = manager
1405 .resolve_font(Some("Document Serif B"), false, false)
1406 .expect("second alias resolves");
1407 let first_data = &manager.fonts[manager.index_of(first).unwrap()].data;
1408 let second_data = &manager.fonts[manager.index_of(second).unwrap()].data;
1409 assert!(Arc::ptr_eq(first_data, second_data));
1410 assert!(!manager.set_caller_aliases(&aliases));
1411
1412 assert!(
1413 manager.set_caller_aliases(&[("Document Serif A".to_owned(), "Carlito".to_owned(),)])
1414 );
1415 let changed = manager
1416 .resolve_font(Some("Document Serif A"), false, false)
1417 .expect("changed alias resolves");
1418 assert_eq!(
1419 manager.fonts[manager.index_of(changed).unwrap()].family,
1420 "Carlito"
1421 );
1422 assert!(
1423 manager.index_of(first).is_some(),
1424 "loaded faces are retained"
1425 );
1426 }
1427
1428 #[test]
1429 fn explicit_alias_state_respects_entry_and_retained_byte_ceilings() {
1430 let aliases = (0..CALLER_ALIAS_MAX_ENTRIES + 32)
1431 .map(|index| (format!("Document Serif {index}"), "Caladea".to_owned()))
1432 .collect::<Vec<_>>();
1433 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1434 manager.set_caller_aliases(&aliases);
1435 assert_eq!(
1436 manager.explicit_aliases.as_slice(),
1437 &aliases[..CALLER_ALIAS_MAX_ENTRIES]
1438 );
1439 assert!(manager.explicit_aliases.len() <= CALLER_ALIAS_MAX_ENTRIES);
1440 assert!(manager.explicit_alias_map.len() <= CALLER_ALIAS_MAX_ENTRIES);
1441
1442 let retained_large = ("x".repeat(32_760), String::new());
1443 let byte_limited = vec![
1444 retained_large.clone(),
1445 ("discarded bytes".to_owned(), "Caladea".to_owned()),
1446 ];
1447 manager.set_caller_aliases(&byte_limited);
1448 assert_eq!(manager.explicit_aliases, vec![retained_large]);
1449
1450 let oversized = vec![("x".repeat(40_000), "Caladea".to_owned())];
1451 manager.set_caller_aliases(&oversized);
1452 let retained_bytes = manager
1453 .explicit_aliases
1454 .iter()
1455 .map(|(requested, target)| requested.len() + target.len())
1456 .sum::<usize>()
1457 + manager
1458 .explicit_alias_map
1459 .iter()
1460 .map(|(requested, target)| requested.len() + target.len())
1461 .sum::<usize>();
1462 assert!(retained_bytes <= CALLER_ALIAS_MAX_RETAINED_BYTES);
1463 assert!(manager.explicit_aliases.is_empty());
1464 assert!(manager.explicit_alias_map.is_empty());
1465 }
1466
1467 #[test]
1468 fn label_alias_prefers_caller_bytes_over_bundled_same_family() {
1469 let bundled = bundled_font_data()
1470 .into_iter()
1471 .find(|(family, _)| *family == "Caladea")
1472 .expect("Caladea is bundled")
1473 .1;
1474 let mut caller = bundled.to_vec();
1475 caller.push(0);
1476 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1477 manager.load_additional_fonts(&[FontFile {
1478 family: "Document Serif".to_owned(),
1479 data: caller.clone(),
1480 }]);
1481
1482 let resolved = manager
1483 .resolve_font(Some("Document Serif"), false, false)
1484 .expect("caller label resolves");
1485 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
1486 assert_eq!(loaded.family, "Caladea");
1487 assert_eq!(loaded.data.as_ref(), caller.as_slice());
1488 assert_ne!(loaded.data.as_ref(), bundled);
1489 }
1490
1491 #[test]
1492 fn case_only_caller_labels_resolve_to_the_supplied_face() {
1493 let bundled = bundled_font_data()
1494 .into_iter()
1495 .find(|(family, _)| *family == "Caladea")
1496 .expect("Caladea is bundled")
1497 .1;
1498 let mut caller = bundled.to_vec();
1499 caller.push(0);
1500 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1501 manager.load_additional_fonts(&[FontFile {
1502 family: "caladea".to_owned(),
1503 data: caller.clone(),
1504 }]);
1505
1506 let resolved = manager
1507 .resolve_font(Some("caladea"), false, false)
1508 .expect("case-only caller label resolves");
1509 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
1510 assert_eq!(loaded.family, "Caladea");
1511 assert_eq!(loaded.data.as_ref(), caller.as_slice());
1512 }
1513
1514 #[test]
1515 fn constructor_label_alias_survives_additional_font_replacement() {
1516 let caladea = bundled_font_data()
1517 .into_iter()
1518 .find(|(family, _)| *family == "Caladea")
1519 .expect("Caladea is bundled")
1520 .1;
1521 let carlito = bundled_font_data()
1522 .into_iter()
1523 .find(|(family, _)| *family == "Carlito")
1524 .expect("Carlito is bundled")
1525 .1;
1526 let mut constructor = caladea.to_vec();
1527 constructor.push(0);
1528 let mut manager =
1529 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
1530
1531 manager.load_additional_fonts(&[FontFile {
1532 family: "Additional Sans".to_owned(),
1533 data: carlito.to_vec(),
1534 }]);
1535
1536 let resolved = manager
1537 .resolve_font(Some("Document Serif"), false, false)
1538 .expect("constructor label still resolves");
1539 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
1540 assert_eq!(loaded.family, "Caladea");
1541 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
1542 }
1543
1544 #[test]
1545 fn constructor_family_priority_survives_additional_font_replacement() {
1546 let caladea = bundled_font_data()
1547 .into_iter()
1548 .find(|(family, _)| *family == "Caladea")
1549 .expect("Caladea is bundled")
1550 .1;
1551 let mut constructor = caladea.to_vec();
1552 constructor.push(0);
1553 let mut replacement = caladea.to_vec();
1554 replacement.extend_from_slice(&[0, 0]);
1555 let mut manager =
1556 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
1557
1558 manager.load_additional_fonts(&[FontFile {
1559 family: "Caladea".to_owned(),
1560 data: replacement,
1561 }]);
1562
1563 let resolved = manager
1564 .resolve_font(Some("Caladea"), false, false)
1565 .expect("constructor family still resolves");
1566 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
1567 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
1568 }
1569
1570 #[test]
1571 fn caller_face_selection_matches_fontdb_css_rules() {
1572 let caladea = bundled_font_data()
1573 .into_iter()
1574 .find(|(family, _)| *family == "Caladea")
1575 .expect("Caladea is bundled")
1576 .1;
1577 let mut source = fontdb::Database::new();
1578 source.load_font_data(caladea.to_vec());
1579 let template = source.faces().next().expect("Caladea has a face").clone();
1580 let mut db = fontdb::Database::new();
1581 let mut add_face = |weight: u16, stretch: fontdb::Stretch, style: fontdb::Style| {
1582 let mut face = template.clone();
1583 face.weight = fontdb::Weight(weight);
1584 face.stretch = stretch;
1585 face.style = style;
1586 db.push_face_info(face)
1587 };
1588
1589 let weight_300 = add_face(300, fontdb::Stretch::Normal, fontdb::Style::Normal);
1590 let weight_500 = add_face(500, fontdb::Stretch::Normal, fontdb::Style::Normal);
1591 let weight_600 = add_face(600, fontdb::Stretch::Normal, fontdb::Style::Normal);
1592 let weight_800 = add_face(800, fontdb::Stretch::Normal, fontdb::Style::Normal);
1593 let expanded = add_face(400, fontdb::Stretch::SemiExpanded, fontdb::Style::Normal);
1594 let condensed = add_face(400, fontdb::Stretch::SemiCondensed, fontdb::Style::Normal);
1595 let normal = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Normal);
1596 let italic = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Italic);
1597
1598 assert_eq!(
1599 best_caller_face(
1600 &db,
1601 &[weight_300, weight_500],
1602 fontdb::Weight::NORMAL,
1603 fontdb::Style::Normal,
1604 ),
1605 Some(weight_500)
1606 );
1607 assert_eq!(
1608 best_caller_face(
1609 &db,
1610 &[weight_600, weight_800],
1611 fontdb::Weight::BOLD,
1612 fontdb::Style::Normal,
1613 ),
1614 Some(weight_800)
1615 );
1616 assert_eq!(
1617 best_caller_face(
1618 &db,
1619 &[expanded, condensed],
1620 fontdb::Weight::NORMAL,
1621 fontdb::Style::Normal,
1622 ),
1623 Some(condensed)
1624 );
1625 assert_eq!(
1626 best_caller_face(
1627 &db,
1628 &[normal, italic],
1629 fontdb::Weight::NORMAL,
1630 fontdb::Style::Oblique,
1631 ),
1632 Some(italic)
1633 );
1634 }
1635
1636 fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
1637 assert_eq!(family.len(), 7);
1638 let mut font = source.to_vec();
1639 let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
1640 let name_offset = (0..table_count)
1641 .find_map(|table| {
1642 let record = 12 + table * 16;
1643 (&font[record..record + 4] == b"name").then(|| {
1644 u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
1645 })
1646 })
1647 .expect("font has name table");
1648 let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
1649 let strings = name_offset
1650 + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
1651 for index in 0..count {
1652 let record = name_offset + 6 + index * 12;
1653 let platform = u16::from_be_bytes([font[record], font[record + 1]]);
1654 let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
1655 let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
1656 let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
1657 if !matches!(name_id, 1 | 16) {
1658 continue;
1659 }
1660 let destination = &mut font[strings + offset..strings + offset + length];
1661 match (platform, length) {
1662 (0 | 3, 14) => {
1663 for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
1664 bytes.copy_from_slice(&(ch as u16).to_be_bytes());
1665 }
1666 }
1667 (1, 7) => destination.copy_from_slice(family.as_bytes()),
1668 _ => {}
1669 }
1670 }
1671 font
1672 }
1673
1674 #[cfg(feature = "system-fonts")]
1675 fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
1676 let header_len = 12 + fonts.len() * 4;
1677 let mut collection = vec![0u8; header_len];
1678 collection[0..4].copy_from_slice(b"ttcf");
1679 collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
1680 collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
1681
1682 for (font_number, font) in fonts.iter().enumerate() {
1683 while !collection.len().is_multiple_of(4) {
1684 collection.push(0);
1685 }
1686 let collection_offset = collection.len();
1687 collection[12 + font_number * 4..16 + font_number * 4]
1688 .copy_from_slice(&(collection_offset as u32).to_be_bytes());
1689
1690 let mut adjusted = font.to_vec();
1691 let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
1692 for table in 0..table_count {
1693 let offset_position = 12 + table * 16 + 8;
1694 let offset = u32::from_be_bytes(
1695 adjusted[offset_position..offset_position + 4]
1696 .try_into()
1697 .expect("table offset"),
1698 );
1699 adjusted[offset_position..offset_position + 4]
1700 .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
1701 }
1702 collection.extend_from_slice(&adjusted);
1703 }
1704 collection
1705 }
1706
1707 #[test]
1708 fn deterministic_font_manager_uses_only_bundled_fonts() {
1709 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1710
1711 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1712 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
1713 }
1714
1715 #[cfg(feature = "system-fonts")]
1716 #[test]
1717 fn normal_font_discovery_initializes_once_per_process() {
1718 let _first = FontManager::new();
1719 let _second = FontManager::new();
1720 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1721
1722 let _deterministic =
1723 FontManager::new_deterministic().expect("bundled font manager should load");
1724 let _caller = FontManager::new_with_fonts(Vec::new());
1725 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1726 }
1727
1728 #[cfg(feature = "system-fonts")]
1729 #[test]
1730 fn file_backed_collection_faces_share_one_byte_buffer() {
1731 let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
1732 let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
1733 let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
1734 let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
1735 std::fs::write(&first_path, &collection).expect("write first temporary collection");
1736 std::fs::write(&second_path, &collection).expect("write second temporary collection");
1737
1738 let mut db = fontdb::Database::new();
1739 db.load_font_file(&first_path).expect("load first TTC");
1740 db.load_font_file(&second_path).expect("load second TTC");
1741 let canonical_first = std::fs::canonicalize(&first_path).unwrap();
1742 let canonical_second = std::fs::canonicalize(&second_path).unwrap();
1743 let first_ids = db
1744 .faces()
1745 .filter_map(|face| match &face.source {
1746 fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
1747 Some(face.id)
1748 }
1749 _ => None,
1750 })
1751 .collect::<Vec<_>>();
1752 let second_id = db
1753 .faces()
1754 .find_map(|face| match &face.source {
1755 fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
1756 Some(face.id)
1757 }
1758 _ => None,
1759 })
1760 .expect("second TTC face");
1761 assert_eq!(first_ids.len(), 2);
1762
1763 let mut memory = HashMap::new();
1764 let (first_face, first_index) =
1765 font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
1766 let (second_face, second_index) =
1767 font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
1768 let (other_file, _) =
1769 font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
1770 assert_ne!(first_index, second_index);
1771 assert!(Arc::ptr_eq(&first_face, &second_face));
1772 assert!(!Arc::ptr_eq(&first_face, &other_file));
1773
1774 std::fs::remove_file(first_path).expect("remove first temporary font");
1775 std::fs::remove_file(second_path).expect("remove second temporary font");
1776 }
1777
1778 #[test]
1779 fn shaping_memo_uses_complete_text_size_and_font_identity() {
1780 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1781 let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1782 let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
1783
1784 let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
1785 let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
1786 assert_eq!(first.glyph_ids, repeat.glyph_ids);
1787 assert_eq!(fm.shaping_memo_counts().0, 1);
1788
1789 fm.shape_text(regular, "different text", 11.0).unwrap();
1790 fm.shape_text(regular, "exact text", 12.0).unwrap();
1791 fm.shape_text(bold, "exact text", 11.0).unwrap();
1792 assert_eq!(fm.shaping_memo_counts().1, 4);
1793
1794 let replacement = FontFile {
1795 family: "Carlito".to_owned(),
1796 data: bundled_font_data()[1].1.to_vec(),
1797 };
1798 fm.load_additional_fonts(&[replacement]);
1799 assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
1800 let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1801 fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
1802 assert_eq!(fm.shaping_memo_counts().1, 1);
1803 }
1804
1805 #[test]
1806 fn shaping_memo_hits_preserve_fifo_order() {
1807 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1808 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1809 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
1810 fm.shape_text(font, "newest exact shape", 11.0).unwrap();
1811 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
1812
1813 let memo = fm.shaping_memo.lock().unwrap();
1814 assert_eq!(memo.hits, 1);
1815 assert_eq!(memo.entries.front().unwrap().0.text, "oldest exact shape");
1816 assert_eq!(memo.entries.back().unwrap().0.text, "newest exact shape");
1817 }
1818
1819 #[test]
1820 fn shaping_memo_fingerprint_collision_requires_exact_key_equality() {
1821 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1822 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1823 fm.shape_text(font, "first collision candidate", 11.0)
1824 .unwrap();
1825 let forced = shaping_fingerprint(font, "second collision candidate", 11.0f64.to_bits());
1826 fm.shaping_memo.lock().unwrap().entries[0].3 = forced;
1827
1828 fm.shape_text(font, "second collision candidate", 11.0)
1829 .unwrap();
1830
1831 let (hits, misses, entries, _) = fm.shaping_memo_counts();
1832 assert_eq!((hits, misses, entries), (0, 2, 2));
1833 }
1834
1835 #[test]
1836 fn shaping_memo_is_bounded_and_recovers_from_poison() {
1837 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1838 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1839 for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
1840 fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
1841 .unwrap();
1842 }
1843 let (_, _, entries, bytes) = fm.shaping_memo_counts();
1844 assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
1845 assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
1846
1847 let fm = Arc::new(fm);
1848 let poison = Arc::clone(&fm);
1849 assert!(
1850 std::thread::spawn(move || {
1851 let _guard = poison.shaping_memo.lock().unwrap();
1852 panic!("poison shaping cache for recovery coverage");
1853 })
1854 .join()
1855 .is_err()
1856 );
1857 let first = fm.shape_text(font, "after poison", 11.0).unwrap();
1858 let second = fm.shape_text(font, "after poison", 11.0).unwrap();
1859 assert_eq!(first.glyph_ids, second.glyph_ids);
1860 let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
1861 assert_eq!((hits, misses, entries), (1, 1, 1));
1862 assert!(bytes > 0);
1863 }
1864
1865 #[test]
1866 fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
1867 let mut memo = ShapingMemo::new();
1868 for suffix in ['a', 'b'] {
1869 memo.insert(
1870 ShapingKey {
1871 font_id: FontId(0),
1872 text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
1873 size_bits: 11.0f64.to_bits(),
1874 },
1875 ShapedText {
1876 glyph_ids: Vec::new(),
1877 advances: Vec::new(),
1878 width: 0.0,
1879 },
1880 );
1881 }
1882 assert_eq!(memo.entries.len(), 1);
1883 assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
1884 }
1885
1886 #[test]
1887 fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
1888 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1889 for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
1890 fm.remember_coverage_fallback(false, false, 0);
1891 }
1892 assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
1893
1894 let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
1895 .filter_map(|value| char::from_u32(0x10_000 + value as u32))
1896 .collect::<Vec<_>>();
1897 fm.remember_coverage_misses(&misses);
1898 assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
1899
1900 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1901 fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
1902 .expect("bounded fallback resolves");
1903 }
1904 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1905 assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
1906 }
1907
1908 #[test]
1909 fn active_document_may_resolve_more_than_256_distinct_faces() {
1910 let source = bundled_font_data()[4].1;
1911 let mut db = fontdb::Database::new();
1912 for index in 0..257 {
1913 db.load_font_data(font_with_family(source, &format!("F{index:06}")));
1914 }
1915 let mut fm = FontManager::from_base_database(db);
1916 fm.begin_layout();
1917 let mut ids = HashSet::new();
1918 for index in 0..257 {
1919 let family = format!("F{index:06}");
1920 let id = fm
1921 .resolve_font(Some(&family), false, false)
1922 .expect("distinct active face resolves");
1923 assert_eq!(fm.font_data(id).unwrap().family, family);
1924 ids.insert(id);
1925 }
1926 assert_eq!(ids.len(), 257);
1927 fm.retain_current_fonts();
1928 assert_eq!(fm.fonts.len(), 257);
1929 }
1930
1931 #[test]
1932 fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
1933 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1934 fm.begin_layout();
1935 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1936 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1937 }
1938 assert!(fm.paragraph_font_trace.is_none());
1939
1940 fm.begin_paragraph_font_trace();
1941 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1942 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1943 }
1944 assert!(fm.finish_paragraph_font_trace().is_none());
1945
1946 fm.begin_layout();
1947 assert_eq!(fm.layout_fonts.capacity(), 0);
1948 fm.begin_paragraph_font_trace();
1949 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1950 let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
1951 assert_eq!(trace.len(), 1);
1952 assert_eq!(trace.capacity(), trace.len());
1953 }
1954
1955 #[cfg(feature = "system-fonts")]
1956 #[test]
1957 fn file_byte_cache_is_bounded_and_recovers_from_poison() {
1958 let cache = Arc::new(Mutex::new(FileFontCache::new()));
1959 {
1960 let mut cache = cache
1961 .lock()
1962 .unwrap_or_else(std::sync::PoisonError::into_inner);
1963 cache.clear();
1964 let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
1965 let returned = cache_file_font_bytes(
1966 &mut cache,
1967 PathBuf::from("oversized-font.ttc"),
1968 Arc::clone(&oversized),
1969 )
1970 .expect("oversized bytes are returned uncached");
1971 assert!(Arc::ptr_eq(&oversized, &returned));
1972 assert!(cache.entries.is_empty());
1973 assert_eq!(cache.bytes, 0);
1974 }
1975
1976 let poison = Arc::clone(&cache);
1977 assert!(
1978 std::thread::spawn(move || {
1979 let cache = poison;
1980 let _guard = cache.lock().unwrap();
1981 panic!("poison file byte cache for recovery coverage");
1982 })
1983 .join()
1984 .is_err()
1985 );
1986
1987 let path = std::env::temp_dir().join(format!(
1988 "rdocx-font-cache-poison-{}-{:?}.ttf",
1989 std::process::id(),
1990 std::thread::current().id()
1991 ));
1992 std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
1993 let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
1994 let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
1995 assert!(Arc::ptr_eq(&first, &second));
1996 std::fs::remove_file(path).expect("remove recovery font");
1997 }
1998
1999 #[cfg(not(feature = "system-fonts"))]
2000 #[test]
2001 fn no_default_features_omits_system_font_discovery() {
2002 let fm = FontManager::new();
2003 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2004 }
2005
2006 #[test]
2007 fn font_manager_with_no_fonts_returns_an_error() {
2008 let mut fm = FontManager::new_with_fonts(Vec::new());
2009 assert!(matches!(
2010 fm.resolve_font(None, false, false),
2011 Err(LayoutError::FontNotFound(_))
2012 ));
2013 }
2014
2015 #[test]
2016 fn load_system_font() {
2017 let mut fm = FontManager::new();
2018 let result = fm.resolve_font(None, false, false);
2020 if let Ok(id) = result {
2022 assert_eq!(id.0, 0);
2023 }
2024 }
2025
2026 #[test]
2027 fn font_metrics_positive() {
2028 let mut fm = FontManager::new();
2029 if let Ok(id) = fm.resolve_font(None, false, false) {
2030 let metrics = fm.metrics(id, 12.0).unwrap();
2031 assert!(metrics.ascent > 0.0);
2032 assert!(metrics.descent > 0.0);
2033 assert!(metrics.units_per_em > 0);
2034 }
2035 }
2036
2037 #[test]
2038 fn shape_hello_world() {
2039 let mut fm = FontManager::new();
2040 if let Ok(id) = fm.resolve_font(None, false, false) {
2041 let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
2042 assert!(!shaped.glyph_ids.is_empty());
2043 assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
2044 assert!(shaped.width > 0.0);
2045 }
2046 }
2047
2048 #[test]
2049 fn font_caching() {
2050 let mut fm = FontManager::new();
2051 if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
2052 let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
2053 assert_eq!(id1, id2);
2054 }
2055 }
2056
2057 #[test]
2058 fn font_resolution_alias_cache_is_bounded() {
2059 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2060 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2061 fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
2062 .expect("fallback font resolves");
2063 }
2064 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2065 assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2066 }
2067
2068 #[test]
2069 fn bold_italic_variants() {
2070 let mut fm = FontManager::new();
2071 let regular = fm.resolve_font(None, false, false);
2072 let bold = fm.resolve_font(None, true, false);
2073 if let (Ok(r), Ok(b)) = (regular, bold) {
2074 assert_ne!(r, b);
2076 }
2077 }
2078
2079 #[test]
2082 fn latin_text_resolves_the_same_as_by_name() {
2083 let mut fm = FontManager::new();
2084 let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
2085 return;
2086 };
2087 let for_text = fm
2088 .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
2089 .unwrap();
2090 assert_eq!(by_name, for_text);
2091 }
2092
2093 #[test]
2099 fn text_no_font_can_draw_keeps_the_requested_font() {
2100 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2101 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2102 let resolved = fm
2103 .resolve_font_for_text(Some("Carlito"), false, false, "这是中文")
2104 .unwrap();
2105 assert_eq!(
2106 primary, resolved,
2107 "with no covering font available the original must be kept"
2108 );
2109 }
2110
2111 #[test]
2113 fn whitespace_does_not_trigger_a_fallback() {
2114 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2115 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2116 let idx = fm.index_of(by_name).unwrap();
2117 assert!(
2119 fm.uncovered(idx, "a\u{00a0}b\tc")
2120 .iter()
2121 .all(|c| *c != '\t'),
2122 "control and whitespace characters must be ignored"
2123 );
2124 }
2125
2126 #[test]
2132 fn cjk_text_moves_off_a_latin_font_when_possible() {
2133 let mut fm = FontManager::new();
2134 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
2135 return;
2136 };
2137 let Some(idx) = fm.index_of(latin) else {
2138 return;
2139 };
2140 if fm.uncovered(idx, "这是中文").is_empty() {
2141 return; }
2143 let resolved = fm
2144 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
2145 .unwrap();
2146 if resolved == latin {
2147 return; }
2149 let new_idx = fm.index_of(resolved).unwrap();
2150 assert!(
2151 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
2152 "the replacement must cover more of the text than the original"
2153 );
2154 }
2155}