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)>,
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)>()
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 self.entries.push_back((key, shaped, entry_bytes));
116 }
117}
118
119const SHAPING_CACHE_MAX_ENTRIES: usize = 2_048;
120const SHAPING_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
121
122#[cfg(feature = "system-fonts")]
123struct FileFontCache {
124 entries: VecDeque<(PathBuf, Arc<[u8]>, usize)>,
125 bytes: usize,
126}
127
128#[cfg(feature = "system-fonts")]
129impl FileFontCache {
130 fn new() -> Self {
131 Self {
132 entries: VecDeque::new(),
133 bytes: 0,
134 }
135 }
136
137 fn clear(&mut self) {
138 self.entries.clear();
139 self.bytes = 0;
140 }
141}
142
143#[cfg(feature = "system-fonts")]
144const FILE_FONT_CACHE_MAX_ENTRIES: usize = 256;
145#[cfg(feature = "system-fonts")]
146const FILE_FONT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
147
148#[cfg(feature = "system-fonts")]
149static NORMAL_FONT_DATABASE: OnceLock<fontdb::Database> = OnceLock::new();
150#[cfg(feature = "system-fonts")]
151static FILE_FONT_CACHE: OnceLock<Mutex<FileFontCache>> = OnceLock::new();
152#[cfg(all(test, feature = "system-fonts"))]
153static SYSTEM_FONT_DISCOVERY_RUNS: AtomicUsize = AtomicUsize::new(0);
154
155struct LoadedFont {
157 db_id: fontdb::ID,
158 id: FontId,
159 family: String,
160 bold: bool,
161 italic: bool,
162 data: Arc<[u8]>,
163 face_index: u32,
164 units_per_em: u16,
165 ascender: i16,
167 descender: i16,
168 line_gap: i16,
169 shaper_data: harfrust::ShaperData,
172}
173
174struct ParagraphFontTrace {
175 ids: Vec<FontId>,
176 overflowed: bool,
177}
178
179pub struct FontManager {
181 db: fontdb::Database,
182 base_db: fontdb::Database,
184 cache: HashMap<FontKey, usize>,
186 memory_face_data: HashMap<fontdb::ID, Arc<[u8]>>,
188 fonts: Vec<LoadedFont>,
190 next_id: u32,
192 coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
200 coverage_misses: HashSet<char>,
203 additional_fonts: Vec<FontFile>,
205 shaping_memo: Mutex<ShapingMemo>,
207 paragraph_font_trace: Option<ParagraphFontTrace>,
209 layout_fonts: Vec<FontId>,
211}
212
213const BROAD_COVERAGE_FAMILIES: &[&str] = &[
218 "Noto Sans CJK SC",
220 "Noto Sans CJK JP",
221 "Noto Sans CJK KR",
222 "Noto Sans CJK TC",
223 "Noto Serif CJK SC",
224 "Source Han Sans SC",
225 "WenQuanYi Zen Hei",
226 "WenQuanYi Micro Hei",
227 "PingFang SC",
229 "PingFang TC",
230 "Hiragino Sans",
231 "Hiragino Kaku Gothic ProN",
232 "Apple SD Gothic Neo",
233 "Songti SC",
234 "STHeiti",
235 "Microsoft YaHei",
237 "Microsoft JhengHei",
238 "SimSun",
239 "SimHei",
240 "NSimSun",
241 "Yu Gothic",
242 "MS Gothic",
243 "Meiryo",
244 "Malgun Gothic",
245 "Arial Unicode MS",
247 "DejaVu Sans",
248];
249
250const RESOLUTION_CACHE_MAX_ENTRIES: usize = 256;
251const COVERAGE_FALLBACK_MAX_ENTRIES: usize = 256;
252const COVERAGE_MISS_MAX_ENTRIES: usize = 4_096;
253const PARAGRAPH_FONT_TRACE_MAX_ENTRIES: usize = 4_096;
254
255impl Default for FontManager {
256 fn default() -> Self {
257 Self::new()
258 }
259}
260
261impl FontManager {
262 fn from_base_database(db: fontdb::Database) -> Self {
263 Self {
264 base_db: db.clone(),
265 db,
266 cache: HashMap::new(),
267 memory_face_data: HashMap::new(),
268 fonts: Vec::new(),
269 next_id: 0,
270 coverage_fallbacks: HashMap::new(),
271 coverage_misses: HashSet::new(),
272 additional_fonts: Vec::new(),
273 shaping_memo: Mutex::new(ShapingMemo::new()),
274 paragraph_font_trace: None,
275 layout_fonts: Vec::new(),
276 }
277 }
278
279 pub fn new() -> Self {
284 #[cfg(feature = "system-fonts")]
285 {
286 let db = NORMAL_FONT_DATABASE.get_or_init(|| {
287 let mut db = bundled_font_database();
288 db.load_system_fonts();
289 #[cfg(test)]
290 SYSTEM_FONT_DISCOVERY_RUNS.fetch_add(1, Ordering::Relaxed);
291 db
292 });
293 Self::from_base_database(db.clone())
294 }
295
296 #[cfg(not(feature = "system-fonts"))]
297 Self::from_base_database(bundled_font_database())
298 }
299
300 pub fn new_deterministic() -> Result<Self> {
305 Ok(Self::from_base_database(bundled_font_database()))
306 }
307
308 pub fn load_additional_fonts(&mut self, font_files: &[FontFile]) -> bool {
315 if self.additional_fonts == font_files {
316 return false;
317 }
318
319 self.db = self.base_db.clone();
320 for font_file in font_files {
321 self.db.load_font_data(font_file.data.clone());
322 }
323 self.cache.clear();
324 self.memory_face_data.clear();
325 self.fonts.clear();
326 self.next_id = 0;
327 self.coverage_fallbacks.clear();
328 self.coverage_misses.clear();
329 self.additional_fonts = font_files.to_vec();
330 if self.shaping_memo.is_poisoned() {
331 self.shaping_memo.clear_poison();
332 }
333 self.shaping_memo
334 .get_mut()
335 .expect("shaping cache poison was cleared")
336 .clear();
337 true
338 }
339
340 pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
345 let mut db = fontdb::Database::new();
346 for (_name, data) in &fonts {
347 db.load_font_data(data.clone());
348 }
349 Self::from_base_database(db)
350 }
351
352 #[doc(hidden)]
354 pub fn begin_layout(&mut self) {
355 self.paragraph_font_trace = None;
356 self.layout_fonts = Vec::new();
357 }
358
359 #[doc(hidden)]
361 pub fn begin_paragraph_font_trace(&mut self) {
362 self.paragraph_font_trace = Some(ParagraphFontTrace {
363 ids: Vec::new(),
364 overflowed: false,
365 });
366 }
367
368 #[doc(hidden)]
370 pub fn finish_paragraph_font_trace(&mut self) -> Option<Vec<FontId>> {
371 let mut trace = self.paragraph_font_trace.take()?;
372 if trace.overflowed {
373 return None;
374 }
375 trace.ids.shrink_to_fit();
376 Some(trace.ids)
377 }
378
379 #[doc(hidden)]
381 pub fn replay_layout_font_trace(&mut self, trace: &[FontId]) {
382 for &font_id in trace {
383 self.record_layout_font(font_id);
384 }
385 }
386
387 #[doc(hidden)]
389 pub fn current_layout_fonts(&self) -> &[FontId] {
390 &self.layout_fonts
391 }
392
393 #[doc(hidden)]
395 pub fn every_loaded_font_is_current(&self) -> bool {
396 self.fonts
397 .iter()
398 .map(|font| font.id)
399 .eq(self.layout_fonts.iter().copied())
400 && self
401 .layout_fonts
402 .iter()
403 .enumerate()
404 .all(|(index, font_id)| *font_id == FontId(index as u32))
405 }
406
407 #[doc(hidden)]
411 pub fn retain_current_fonts(&mut self) {
412 let current = self.layout_fonts.iter().copied().collect::<HashSet<_>>();
413 let old_index_ids = self
414 .fonts
415 .iter()
416 .enumerate()
417 .map(|(index, font)| (index, font.id))
418 .collect::<HashMap<_, _>>();
419 self.fonts.retain(|font| current.contains(&font.id));
420 let current_order = self
421 .layout_fonts
422 .iter()
423 .enumerate()
424 .map(|(index, font_id)| (*font_id, index))
425 .collect::<HashMap<_, _>>();
426 self.fonts
427 .sort_by_key(|font| current_order.get(&font.id).copied().unwrap_or(usize::MAX));
428
429 let indices = self
430 .fonts
431 .iter()
432 .enumerate()
433 .map(|(index, font)| (font.id, index))
434 .collect::<HashMap<_, _>>();
435 let old_cache = std::mem::take(&mut self.cache);
436 self.cache = old_cache
437 .into_iter()
438 .filter_map(|(key, old_index)| {
439 let font_id = old_index_ids.get(&old_index)?;
440 Some((key, *indices.get(font_id)?))
441 })
442 .collect();
443 self.coverage_fallbacks.clear();
444 self.coverage_misses.clear();
445 let active_db_ids = self
446 .fonts
447 .iter()
448 .map(|font| font.db_id)
449 .collect::<HashSet<_>>();
450 self.memory_face_data
451 .retain(|db_id, _| active_db_ids.contains(db_id));
452
453 let memo = self
454 .shaping_memo
455 .get_mut()
456 .unwrap_or_else(std::sync::PoisonError::into_inner);
457 memo.entries
458 .retain(|(key, _, _)| current.contains(&key.font_id));
459 memo.bytes = memo.entries.iter().map(|(_, _, bytes)| bytes).sum();
460 }
461
462 pub fn resolve_font_for_text(
479 &mut self,
480 family: Option<&str>,
481 bold: bool,
482 italic: bool,
483 text: &str,
484 ) -> Result<FontId> {
485 let primary = self.resolve_font(family, bold, italic)?;
486
487 let Some(idx) = self.index_of(primary) else {
488 return Ok(primary);
489 };
490 let missing = self.uncovered(idx, text);
491 if missing.is_empty() {
492 return Ok(primary);
493 }
494
495 match self.font_covering(&missing, bold, italic) {
496 None => Ok(primary),
499 Some(id) => Ok(id),
500 }
501 }
502
503 fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
508 let font = &self.fonts[idx];
509 let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
510 return Vec::new();
511 };
512 let mut seen = HashSet::new();
513 text.chars()
514 .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
515 .filter(|&ch| face.glyph_index(ch).is_none())
516 .filter(|&ch| seen.insert(ch))
517 .collect()
518 }
519
520 fn covers(&self, idx: usize, ch: char) -> bool {
522 let font = &self.fonts[idx];
523 ttf_parser::Face::parse(&font.data, font.face_index)
524 .map(|face| face.glyph_index(ch).is_some())
525 .unwrap_or(false)
526 }
527
528 fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
537 if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
538 return None;
539 }
540
541 let mut best: Option<(usize, usize)> = None; let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
543 let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
544 if covered == 0 {
545 return false;
546 }
547 if best.map(|(n, _)| covered > n).unwrap_or(true) {
548 *best = Some((covered, idx));
549 }
550 covered == missing.len()
551 };
552
553 if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
556 for idx in known {
557 if consider(self, idx, &mut best) {
558 let id = self.fonts[idx].id;
559 self.record_layout_font(id);
560 return Some(id);
561 }
562 }
563 }
564
565 let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
569 .iter()
570 .map(|s| s.to_string())
571 .chain(
572 self.db
573 .faces()
574 .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
575 )
576 .collect();
577
578 for name in candidates {
579 let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
580 continue;
581 };
582 let Some(idx) = self.index_of(id) else {
583 continue;
584 };
585 let complete = consider(self, idx, &mut best);
586 if complete {
587 self.remember_coverage_fallback(bold, italic, idx);
588 return Some(id);
589 }
590 }
591
592 match best {
593 Some((_, idx)) => {
594 self.remember_coverage_fallback(bold, italic, idx);
595 let id = self.fonts[idx].id;
596 self.record_layout_font(id);
597 Some(id)
598 }
599 None => {
600 self.remember_coverage_misses(missing);
601 None
602 }
603 }
604 }
605
606 fn index_of(&self, id: FontId) -> Option<usize> {
608 self.fonts.iter().position(|f| f.id == id)
609 }
610
611 pub fn resolve_font(
614 &mut self,
615 family: Option<&str>,
616 bold: bool,
617 italic: bool,
618 ) -> Result<FontId> {
619 let family_name = family.unwrap_or("Arial");
620
621 let key = FontKey {
622 family: family_name.to_string(),
623 bold,
624 italic,
625 };
626
627 if let Some(idx) = self.cache.get(&key).copied() {
628 let id = self.fonts[idx].id;
629 self.record_layout_font(id);
630 return Ok(id);
631 }
632
633 let mapped = map_font_name(family_name);
635
636 let mut fallbacks: Vec<&str> = Vec::with_capacity(10);
638 fallbacks.push(family_name);
639 for alt in mapped {
640 if *alt != family_name {
641 fallbacks.push(alt);
642 }
643 }
644 for generic in &[
645 "Carlito",
646 "Arial",
647 "Liberation Sans",
648 "Helvetica",
649 "DejaVu Sans",
650 "Noto Sans",
651 ] {
652 if !fallbacks.contains(generic) {
653 fallbacks.push(generic);
654 }
655 }
656
657 let style = if italic {
658 fontdb::Style::Italic
659 } else {
660 fontdb::Style::Normal
661 };
662 let weight = if bold {
663 fontdb::Weight::BOLD
664 } else {
665 fontdb::Weight::NORMAL
666 };
667
668 let mut found_id = None;
669 for fallback in &fallbacks {
670 let query = fontdb::Query {
671 families: &[fontdb::Family::Name(fallback)],
672 weight,
673 style,
674 stretch: fontdb::Stretch::Normal,
675 };
676
677 if let Some(id) = self.db.query(&query) {
678 found_id = Some(id);
679 break;
680 }
681 }
682
683 if found_id.is_none() {
685 for generic_family in &[
686 fontdb::Family::SansSerif,
687 fontdb::Family::Serif,
688 fontdb::Family::Monospace,
689 ] {
690 let query = fontdb::Query {
691 families: &[*generic_family],
692 weight,
693 style,
694 stretch: fontdb::Stretch::Normal,
695 };
696 if let Some(id) = self.db.query(&query) {
697 found_id = Some(id);
698 break;
699 }
700 }
701 }
702
703 let db_id = found_id.ok_or_else(|| {
704 LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
705 })?;
706
707 if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
711 && let Some(idx) = self
712 .fonts
713 .iter()
714 .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
715 {
716 let id = self.fonts[idx].id;
717 self.record_layout_font(id);
718 return Ok(id);
719 }
720
721 let font_id = FontId(self.next_id);
722 self.next_id += 1;
723
724 let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
727 .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
728
729 let (units_per_em, ascender, descender, line_gap) = {
730 let face = ttf_parser::Face::parse(&data, face_index)
731 .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
732 (
733 face.units_per_em(),
734 face.ascender(),
735 face.descender(),
736 face.line_gap(),
737 )
738 };
739
740 if units_per_em == 0 {
743 return Err(LayoutError::FontParse(format!(
744 "font '{family_name}' declares zero units per em"
745 )));
746 }
747
748 let shaper_data = {
749 let face = harfrust::FontRef::from_index(&data, face_index)
750 .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
751 harfrust::ShaperData::new(&face)
752 };
753
754 let actual_family = self
755 .db
756 .face(db_id)
757 .map(|f| {
758 f.families
759 .first()
760 .map(|(name, _)| name.clone())
761 .unwrap_or_else(|| family_name.to_string())
762 })
763 .unwrap_or_else(|| family_name.to_string());
764
765 let idx = self.fonts.len();
766 self.fonts.push(LoadedFont {
767 db_id,
768 id: font_id,
769 family: actual_family,
770 bold,
771 italic,
772 data,
773 face_index,
774 units_per_em,
775 ascender,
776 descender,
777 line_gap,
778 shaper_data,
779 });
780 self.remember_font_key(key, idx);
781 self.record_layout_font(font_id);
782
783 Ok(font_id)
784 }
785
786 pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
788 let font = self.get_font(font_id)?;
789 let scale = size_pt / font.units_per_em as f64;
790
791 Ok(FontMetrics {
792 ascent: font.ascender as f64 * scale,
793 descent: -(font.descender as f64) * scale, line_gap: font.line_gap as f64 * scale,
795 units_per_em: font.units_per_em,
796 })
797 }
798
799 pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
801 if text.is_empty() {
804 return Ok(ShapedText {
805 glyph_ids: Vec::new(),
806 advances: Vec::new(),
807 width: 0.0,
808 });
809 }
810
811 let key = ShapingKey {
812 font_id,
813 text: text.to_owned(),
814 size_bits: size_pt.to_bits(),
815 };
816 let mut memo = match self.shaping_memo.lock() {
817 Ok(memo) => memo,
818 Err(poisoned) => {
819 let mut memo = poisoned.into_inner();
820 memo.clear();
821 self.shaping_memo.clear_poison();
822 memo
823 }
824 };
825 if let Some(index) = memo
826 .entries
827 .iter()
828 .position(|(candidate, _, _)| candidate == &key)
829 {
830 let entry = memo.entries.remove(index).expect("cache index exists");
831 let shaped = entry.1.clone();
832 memo.entries.push_back(entry);
833 #[cfg(test)]
834 {
835 memo.hits += 1;
836 }
837 return Ok(shaped);
838 }
839 #[cfg(test)]
840 {
841 memo.misses += 1;
842 }
843
844 let font = self.get_font(font_id)?;
845
846 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
847 .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
848
849 let shaper = font.shaper_data.shaper(&face).build();
850
851 let mut buffer = harfrust::UnicodeBuffer::new();
852 buffer.push_str(text);
853 buffer.guess_segment_properties();
856
857 let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
858 let infos = output.glyph_infos();
859 let positions = output.glyph_positions();
860
861 let upem = font.units_per_em as f64;
862 let scale = size_pt / upem;
863
864 let mut glyph_ids = Vec::with_capacity(infos.len());
865 let mut advances = Vec::with_capacity(positions.len());
866 let mut total_width = 0.0;
867
868 for (info, pos) in infos.iter().zip(positions.iter()) {
869 glyph_ids.push(info.glyph_id as u16);
870 let advance = pos.x_advance as f64 * scale;
871 advances.push(advance);
872 total_width += advance;
873 }
874
875 let shaped = ShapedText {
876 glyph_ids,
877 advances,
878 width: total_width,
879 };
880 memo.insert(key, shaped.clone());
881 Ok(shaped)
882 }
883
884 pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
886 let font = self.get_font(font_id)?;
887 Ok(crate::output::FontData {
888 id: font.id,
889 family: font.family.clone(),
890 data: font.data.to_vec(),
891 face_index: font.face_index,
892 bold: font.bold,
893 italic: font.italic,
894 })
895 }
896
897 pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
899 self.fonts
900 .iter()
901 .map(|f| crate::output::FontData {
902 id: f.id,
903 family: f.family.clone(),
904 data: f.data.to_vec(),
905 face_index: f.face_index,
906 bold: f.bold,
907 italic: f.italic,
908 })
909 .collect()
910 }
911
912 fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
913 self.fonts
914 .iter()
915 .find(|f| f.id == font_id)
916 .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
917 }
918
919 fn remember_font_key(&mut self, key: FontKey, index: usize) {
920 if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
921 self.cache.insert(key, index);
922 }
923 }
924
925 fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
926 let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
927 if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
928 known.push(index);
929 }
930 }
931
932 fn remember_coverage_misses(&mut self, missing: &[char]) {
933 for &ch in missing {
934 if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
935 break;
936 }
937 self.coverage_misses.insert(ch);
938 }
939 }
940
941 fn record_layout_font(&mut self, font_id: FontId) {
942 if let Some(trace) = self.paragraph_font_trace.as_mut() {
943 if trace.ids.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
944 trace.ids.push(font_id);
945 } else {
946 trace.overflowed = true;
947 }
948 }
949 if !self.layout_fonts.contains(&font_id) {
950 self.layout_fonts.push(font_id);
951 }
952 }
953
954 #[cfg(test)]
955 fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
956 let memo = self
957 .shaping_memo
958 .lock()
959 .unwrap_or_else(std::sync::PoisonError::into_inner);
960 (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
961 }
962}
963
964fn bundled_font_database() -> fontdb::Database {
965 let mut db = fontdb::Database::new();
966 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
967 db.load_font_data(data.to_vec());
968 }
969 db
970}
971
972fn font_data_for_face(
973 db: &fontdb::Database,
974 id: fontdb::ID,
975 memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
976) -> Option<(Arc<[u8]>, u32)> {
977 let face = db.face(id)?;
978 let face_index = face.index;
979 match &face.source {
980 fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
981 Some(data) => Some((Arc::clone(data), face_index)),
982 None => {
983 let data: Arc<[u8]> = Arc::from(data.as_ref().as_ref().to_vec());
984 memory_face_data.insert(id, Arc::clone(&data));
985 Some((data, face_index))
986 }
987 },
988 #[cfg(feature = "system-fonts")]
989 fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
990 }
991}
992
993#[cfg(feature = "system-fonts")]
994fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
995 let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
996 shared_file_font_bytes_from_cache(cache, path)
997}
998
999#[cfg(feature = "system-fonts")]
1000fn shared_file_font_bytes_from_cache(
1001 cache_lock: &Mutex<FileFontCache>,
1002 path: &Path,
1003) -> Option<Arc<[u8]>> {
1004 let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1005 let mut cache = match cache_lock.lock() {
1006 Ok(cache) => cache,
1007 Err(poisoned) => {
1008 let mut cache = poisoned.into_inner();
1009 cache.clear();
1010 cache_lock.clear_poison();
1011 cache
1012 }
1013 };
1014 if let Some(index) = cache
1015 .entries
1016 .iter()
1017 .position(|(candidate, _, _)| candidate == &identity)
1018 {
1019 let entry = cache.entries.remove(index).expect("cache index exists");
1020 let bytes = Arc::clone(&entry.1);
1021 cache.entries.push_back(entry);
1022 return Some(bytes);
1023 }
1024
1025 let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
1026 cache_file_font_bytes(&mut cache, identity, bytes)
1027}
1028
1029#[cfg(feature = "system-fonts")]
1030fn cache_file_font_bytes(
1031 cache: &mut FileFontCache,
1032 identity: PathBuf,
1033 bytes: Arc<[u8]>,
1034) -> Option<Arc<[u8]>> {
1035 let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
1036 .saturating_add(identity.as_os_str().len())
1037 .saturating_add(bytes.len());
1038 if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
1039 while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
1040 || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
1041 {
1042 let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
1043 break;
1044 };
1045 cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
1046 }
1047 cache.bytes += entry_bytes;
1048 cache
1049 .entries
1050 .push_back((identity, Arc::clone(&bytes), entry_bytes));
1051 }
1052 Some(bytes)
1053}
1054
1055fn map_font_name(name: &str) -> &[&str] {
1062 match name {
1063 "Calibri" => &["Calibri", "Carlito"],
1064 "Calibri Light" => &["Calibri Light", "Carlito"],
1065 "Cambria" => &["Cambria", "Caladea"],
1066 "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
1067 "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
1068 "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
1069 "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
1070 "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
1071 "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
1072 "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
1073 "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
1074 "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
1075 "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
1076 "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
1077 "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
1078 "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
1079 "Impact" => &["Impact", "Liberation Sans", "Arial"],
1080 "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
1081 "Symbol" => &["Symbol", "DejaVu Sans"],
1082 "Wingdings" => &["Wingdings", "Symbol"],
1083 _ => &[],
1084 }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089 use super::*;
1090 use crate::bundled_fonts::bundled_font_data;
1091
1092 fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
1093 assert_eq!(family.len(), 7);
1094 let mut font = source.to_vec();
1095 let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
1096 let name_offset = (0..table_count)
1097 .find_map(|table| {
1098 let record = 12 + table * 16;
1099 (&font[record..record + 4] == b"name").then(|| {
1100 u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
1101 })
1102 })
1103 .expect("font has name table");
1104 let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
1105 let strings = name_offset
1106 + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
1107 for index in 0..count {
1108 let record = name_offset + 6 + index * 12;
1109 let platform = u16::from_be_bytes([font[record], font[record + 1]]);
1110 let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
1111 let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
1112 let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
1113 if !matches!(name_id, 1 | 16) {
1114 continue;
1115 }
1116 let destination = &mut font[strings + offset..strings + offset + length];
1117 match (platform, length) {
1118 (0 | 3, 14) => {
1119 for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
1120 bytes.copy_from_slice(&(ch as u16).to_be_bytes());
1121 }
1122 }
1123 (1, 7) => destination.copy_from_slice(family.as_bytes()),
1124 _ => {}
1125 }
1126 }
1127 font
1128 }
1129
1130 #[cfg(feature = "system-fonts")]
1131 fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
1132 let header_len = 12 + fonts.len() * 4;
1133 let mut collection = vec![0u8; header_len];
1134 collection[0..4].copy_from_slice(b"ttcf");
1135 collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
1136 collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
1137
1138 for (font_number, font) in fonts.iter().enumerate() {
1139 while !collection.len().is_multiple_of(4) {
1140 collection.push(0);
1141 }
1142 let collection_offset = collection.len();
1143 collection[12 + font_number * 4..16 + font_number * 4]
1144 .copy_from_slice(&(collection_offset as u32).to_be_bytes());
1145
1146 let mut adjusted = font.to_vec();
1147 let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
1148 for table in 0..table_count {
1149 let offset_position = 12 + table * 16 + 8;
1150 let offset = u32::from_be_bytes(
1151 adjusted[offset_position..offset_position + 4]
1152 .try_into()
1153 .expect("table offset"),
1154 );
1155 adjusted[offset_position..offset_position + 4]
1156 .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
1157 }
1158 collection.extend_from_slice(&adjusted);
1159 }
1160 collection
1161 }
1162
1163 #[test]
1164 fn deterministic_font_manager_uses_only_bundled_fonts() {
1165 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1166
1167 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1168 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
1169 }
1170
1171 #[cfg(feature = "system-fonts")]
1172 #[test]
1173 fn normal_font_discovery_initializes_once_per_process() {
1174 let _first = FontManager::new();
1175 let _second = FontManager::new();
1176 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1177
1178 let _deterministic =
1179 FontManager::new_deterministic().expect("bundled font manager should load");
1180 let _caller = FontManager::new_with_fonts(Vec::new());
1181 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1182 }
1183
1184 #[cfg(feature = "system-fonts")]
1185 #[test]
1186 fn file_backed_collection_faces_share_one_byte_buffer() {
1187 let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
1188 let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
1189 let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
1190 let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
1191 std::fs::write(&first_path, &collection).expect("write first temporary collection");
1192 std::fs::write(&second_path, &collection).expect("write second temporary collection");
1193
1194 let mut db = fontdb::Database::new();
1195 db.load_font_file(&first_path).expect("load first TTC");
1196 db.load_font_file(&second_path).expect("load second TTC");
1197 let canonical_first = std::fs::canonicalize(&first_path).unwrap();
1198 let canonical_second = std::fs::canonicalize(&second_path).unwrap();
1199 let first_ids = db
1200 .faces()
1201 .filter_map(|face| match &face.source {
1202 fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
1203 Some(face.id)
1204 }
1205 _ => None,
1206 })
1207 .collect::<Vec<_>>();
1208 let second_id = db
1209 .faces()
1210 .find_map(|face| match &face.source {
1211 fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
1212 Some(face.id)
1213 }
1214 _ => None,
1215 })
1216 .expect("second TTC face");
1217 assert_eq!(first_ids.len(), 2);
1218
1219 let mut memory = HashMap::new();
1220 let (first_face, first_index) =
1221 font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
1222 let (second_face, second_index) =
1223 font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
1224 let (other_file, _) =
1225 font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
1226 assert_ne!(first_index, second_index);
1227 assert!(Arc::ptr_eq(&first_face, &second_face));
1228 assert!(!Arc::ptr_eq(&first_face, &other_file));
1229
1230 std::fs::remove_file(first_path).expect("remove first temporary font");
1231 std::fs::remove_file(second_path).expect("remove second temporary font");
1232 }
1233
1234 #[test]
1235 fn shaping_memo_uses_complete_text_size_and_font_identity() {
1236 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1237 let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1238 let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
1239
1240 let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
1241 let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
1242 assert_eq!(first.glyph_ids, repeat.glyph_ids);
1243 assert_eq!(fm.shaping_memo_counts().0, 1);
1244
1245 fm.shape_text(regular, "different text", 11.0).unwrap();
1246 fm.shape_text(regular, "exact text", 12.0).unwrap();
1247 fm.shape_text(bold, "exact text", 11.0).unwrap();
1248 assert_eq!(fm.shaping_memo_counts().1, 4);
1249
1250 let replacement = FontFile {
1251 family: "Carlito".to_owned(),
1252 data: bundled_font_data()[1].1.to_vec(),
1253 };
1254 fm.load_additional_fonts(&[replacement]);
1255 assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
1256 let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1257 fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
1258 assert_eq!(fm.shaping_memo_counts().1, 1);
1259 }
1260
1261 #[test]
1262 fn shaping_memo_is_bounded_and_recovers_from_poison() {
1263 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1264 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1265 for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
1266 fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
1267 .unwrap();
1268 }
1269 let (_, _, entries, bytes) = fm.shaping_memo_counts();
1270 assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
1271 assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
1272
1273 let fm = Arc::new(fm);
1274 let poison = Arc::clone(&fm);
1275 assert!(
1276 std::thread::spawn(move || {
1277 let _guard = poison.shaping_memo.lock().unwrap();
1278 panic!("poison shaping cache for recovery coverage");
1279 })
1280 .join()
1281 .is_err()
1282 );
1283 let first = fm.shape_text(font, "after poison", 11.0).unwrap();
1284 let second = fm.shape_text(font, "after poison", 11.0).unwrap();
1285 assert_eq!(first.glyph_ids, second.glyph_ids);
1286 let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
1287 assert_eq!((hits, misses, entries), (1, 1, 1));
1288 assert!(bytes > 0);
1289 }
1290
1291 #[test]
1292 fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
1293 let mut memo = ShapingMemo::new();
1294 for suffix in ['a', 'b'] {
1295 memo.insert(
1296 ShapingKey {
1297 font_id: FontId(0),
1298 text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
1299 size_bits: 11.0f64.to_bits(),
1300 },
1301 ShapedText {
1302 glyph_ids: Vec::new(),
1303 advances: Vec::new(),
1304 width: 0.0,
1305 },
1306 );
1307 }
1308 assert_eq!(memo.entries.len(), 1);
1309 assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
1310 }
1311
1312 #[test]
1313 fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
1314 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1315 for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
1316 fm.remember_coverage_fallback(false, false, 0);
1317 }
1318 assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
1319
1320 let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
1321 .filter_map(|value| char::from_u32(0x10_000 + value as u32))
1322 .collect::<Vec<_>>();
1323 fm.remember_coverage_misses(&misses);
1324 assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
1325
1326 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1327 fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
1328 .expect("bounded fallback resolves");
1329 }
1330 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1331 assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
1332 }
1333
1334 #[test]
1335 fn active_document_may_resolve_more_than_256_distinct_faces() {
1336 let source = bundled_font_data()[4].1;
1337 let mut db = fontdb::Database::new();
1338 for index in 0..257 {
1339 db.load_font_data(font_with_family(source, &format!("F{index:06}")));
1340 }
1341 let mut fm = FontManager::from_base_database(db);
1342 fm.begin_layout();
1343 let mut ids = HashSet::new();
1344 for index in 0..257 {
1345 let family = format!("F{index:06}");
1346 let id = fm
1347 .resolve_font(Some(&family), false, false)
1348 .expect("distinct active face resolves");
1349 assert_eq!(fm.font_data(id).unwrap().family, family);
1350 ids.insert(id);
1351 }
1352 assert_eq!(ids.len(), 257);
1353 fm.retain_current_fonts();
1354 assert_eq!(fm.fonts.len(), 257);
1355 }
1356
1357 #[test]
1358 fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
1359 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1360 fm.begin_layout();
1361 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1362 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1363 }
1364 assert!(fm.paragraph_font_trace.is_none());
1365
1366 fm.begin_paragraph_font_trace();
1367 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1368 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1369 }
1370 assert!(fm.finish_paragraph_font_trace().is_none());
1371
1372 fm.begin_layout();
1373 assert_eq!(fm.layout_fonts.capacity(), 0);
1374 fm.begin_paragraph_font_trace();
1375 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1376 let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
1377 assert_eq!(trace.len(), 1);
1378 assert_eq!(trace.capacity(), trace.len());
1379 }
1380
1381 #[cfg(feature = "system-fonts")]
1382 #[test]
1383 fn file_byte_cache_is_bounded_and_recovers_from_poison() {
1384 let cache = Arc::new(Mutex::new(FileFontCache::new()));
1385 {
1386 let mut cache = cache
1387 .lock()
1388 .unwrap_or_else(std::sync::PoisonError::into_inner);
1389 cache.clear();
1390 let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
1391 let returned = cache_file_font_bytes(
1392 &mut cache,
1393 PathBuf::from("oversized-font.ttc"),
1394 Arc::clone(&oversized),
1395 )
1396 .expect("oversized bytes are returned uncached");
1397 assert!(Arc::ptr_eq(&oversized, &returned));
1398 assert!(cache.entries.is_empty());
1399 assert_eq!(cache.bytes, 0);
1400 }
1401
1402 let poison = Arc::clone(&cache);
1403 assert!(
1404 std::thread::spawn(move || {
1405 let cache = poison;
1406 let _guard = cache.lock().unwrap();
1407 panic!("poison file byte cache for recovery coverage");
1408 })
1409 .join()
1410 .is_err()
1411 );
1412
1413 let path = std::env::temp_dir().join(format!(
1414 "rdocx-font-cache-poison-{}-{:?}.ttf",
1415 std::process::id(),
1416 std::thread::current().id()
1417 ));
1418 std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
1419 let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
1420 let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
1421 assert!(Arc::ptr_eq(&first, &second));
1422 std::fs::remove_file(path).expect("remove recovery font");
1423 }
1424
1425 #[cfg(not(feature = "system-fonts"))]
1426 #[test]
1427 fn no_default_features_omits_system_font_discovery() {
1428 let fm = FontManager::new();
1429 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1430 }
1431
1432 #[test]
1433 fn font_manager_with_no_fonts_returns_an_error() {
1434 let mut fm = FontManager::new_with_fonts(Vec::new());
1435 assert!(matches!(
1436 fm.resolve_font(None, false, false),
1437 Err(LayoutError::FontNotFound(_))
1438 ));
1439 }
1440
1441 #[test]
1442 fn load_system_font() {
1443 let mut fm = FontManager::new();
1444 let result = fm.resolve_font(None, false, false);
1446 if let Ok(id) = result {
1448 assert_eq!(id.0, 0);
1449 }
1450 }
1451
1452 #[test]
1453 fn font_metrics_positive() {
1454 let mut fm = FontManager::new();
1455 if let Ok(id) = fm.resolve_font(None, false, false) {
1456 let metrics = fm.metrics(id, 12.0).unwrap();
1457 assert!(metrics.ascent > 0.0);
1458 assert!(metrics.descent > 0.0);
1459 assert!(metrics.units_per_em > 0);
1460 }
1461 }
1462
1463 #[test]
1464 fn shape_hello_world() {
1465 let mut fm = FontManager::new();
1466 if let Ok(id) = fm.resolve_font(None, false, false) {
1467 let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
1468 assert!(!shaped.glyph_ids.is_empty());
1469 assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
1470 assert!(shaped.width > 0.0);
1471 }
1472 }
1473
1474 #[test]
1475 fn font_caching() {
1476 let mut fm = FontManager::new();
1477 if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
1478 let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
1479 assert_eq!(id1, id2);
1480 }
1481 }
1482
1483 #[test]
1484 fn font_resolution_alias_cache_is_bounded() {
1485 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1486 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1487 fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
1488 .expect("fallback font resolves");
1489 }
1490 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1491 assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1492 }
1493
1494 #[test]
1495 fn bold_italic_variants() {
1496 let mut fm = FontManager::new();
1497 let regular = fm.resolve_font(None, false, false);
1498 let bold = fm.resolve_font(None, true, false);
1499 if let (Ok(r), Ok(b)) = (regular, bold) {
1500 assert_ne!(r, b);
1502 }
1503 }
1504
1505 #[test]
1508 fn latin_text_resolves_the_same_as_by_name() {
1509 let mut fm = FontManager::new();
1510 let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
1511 return;
1512 };
1513 let for_text = fm
1514 .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
1515 .unwrap();
1516 assert_eq!(by_name, for_text);
1517 }
1518
1519 #[test]
1525 fn text_no_font_can_draw_keeps_the_requested_font() {
1526 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1527 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1528 let resolved = fm
1529 .resolve_font_for_text(Some("Carlito"), false, false, "这是中文")
1530 .unwrap();
1531 assert_eq!(
1532 primary, resolved,
1533 "with no covering font available the original must be kept"
1534 );
1535 }
1536
1537 #[test]
1539 fn whitespace_does_not_trigger_a_fallback() {
1540 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1541 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1542 let idx = fm.index_of(by_name).unwrap();
1543 assert!(
1545 fm.uncovered(idx, "a\u{00a0}b\tc")
1546 .iter()
1547 .all(|c| *c != '\t'),
1548 "control and whitespace characters must be ignored"
1549 );
1550 }
1551
1552 #[test]
1558 fn cjk_text_moves_off_a_latin_font_when_possible() {
1559 let mut fm = FontManager::new();
1560 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
1561 return;
1562 };
1563 let Some(idx) = fm.index_of(latin) else {
1564 return;
1565 };
1566 if fm.uncovered(idx, "这是中文").is_empty() {
1567 return; }
1569 let resolved = fm
1570 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
1571 .unwrap();
1572 if resolved == latin {
1573 return; }
1575 let new_idx = fm.index_of(resolved).unwrap();
1576 assert!(
1577 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
1578 "the replacement must cover more of the text than the original"
1579 );
1580 }
1581}