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 self.resolve_font_inner(family, bold, italic, true)
620 }
621
622 #[doc(hidden)]
624 pub fn resolve_font_for_metrics(
625 &mut self,
626 family: Option<&str>,
627 bold: bool,
628 italic: bool,
629 ) -> Result<FontId> {
630 self.resolve_font_inner(family, bold, italic, false)
631 }
632
633 fn resolve_font_inner(
634 &mut self,
635 family: Option<&str>,
636 bold: bool,
637 italic: bool,
638 record_layout_use: bool,
639 ) -> Result<FontId> {
640 let family_name = family.unwrap_or("Arial");
641
642 let key = FontKey {
643 family: family_name.to_string(),
644 bold,
645 italic,
646 };
647
648 if let Some(idx) = self.cache.get(&key).copied() {
649 let id = self.fonts[idx].id;
650 if record_layout_use {
651 self.record_layout_font(id);
652 }
653 return Ok(id);
654 }
655
656 let mapped = map_font_name(family_name);
658
659 let mut fallbacks: Vec<&str> = Vec::with_capacity(10);
661 fallbacks.push(family_name);
662 for alt in mapped {
663 if *alt != family_name {
664 fallbacks.push(alt);
665 }
666 }
667 for generic in &[
668 "Carlito",
669 "Arial",
670 "Liberation Sans",
671 "Helvetica",
672 "DejaVu Sans",
673 "Noto Sans",
674 ] {
675 if !fallbacks.contains(generic) {
676 fallbacks.push(generic);
677 }
678 }
679
680 let style = if italic {
681 fontdb::Style::Italic
682 } else {
683 fontdb::Style::Normal
684 };
685 let weight = if bold {
686 fontdb::Weight::BOLD
687 } else {
688 fontdb::Weight::NORMAL
689 };
690
691 let mut found_id = None;
692 for fallback in &fallbacks {
693 let query = fontdb::Query {
694 families: &[fontdb::Family::Name(fallback)],
695 weight,
696 style,
697 stretch: fontdb::Stretch::Normal,
698 };
699
700 if let Some(id) = self.db.query(&query) {
701 found_id = Some(id);
702 break;
703 }
704 }
705
706 if found_id.is_none() {
708 for generic_family in &[
709 fontdb::Family::SansSerif,
710 fontdb::Family::Serif,
711 fontdb::Family::Monospace,
712 ] {
713 let query = fontdb::Query {
714 families: &[*generic_family],
715 weight,
716 style,
717 stretch: fontdb::Stretch::Normal,
718 };
719 if let Some(id) = self.db.query(&query) {
720 found_id = Some(id);
721 break;
722 }
723 }
724 }
725
726 let db_id = found_id.ok_or_else(|| {
727 LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
728 })?;
729
730 if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
734 && let Some(idx) = self
735 .fonts
736 .iter()
737 .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
738 {
739 let id = self.fonts[idx].id;
740 if record_layout_use {
741 self.record_layout_font(id);
742 }
743 return Ok(id);
744 }
745
746 let font_id = FontId(self.next_id);
747 self.next_id += 1;
748
749 let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
752 .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
753
754 let (units_per_em, ascender, descender, line_gap) = {
755 let face = ttf_parser::Face::parse(&data, face_index)
756 .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
757 (
758 face.units_per_em(),
759 face.ascender(),
760 face.descender(),
761 face.line_gap(),
762 )
763 };
764
765 if units_per_em == 0 {
768 return Err(LayoutError::FontParse(format!(
769 "font '{family_name}' declares zero units per em"
770 )));
771 }
772
773 let shaper_data = {
774 let face = harfrust::FontRef::from_index(&data, face_index)
775 .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
776 harfrust::ShaperData::new(&face)
777 };
778
779 let actual_family = self
780 .db
781 .face(db_id)
782 .map(|f| {
783 f.families
784 .first()
785 .map(|(name, _)| name.clone())
786 .unwrap_or_else(|| family_name.to_string())
787 })
788 .unwrap_or_else(|| family_name.to_string());
789
790 let idx = self.fonts.len();
791 self.fonts.push(LoadedFont {
792 db_id,
793 id: font_id,
794 family: actual_family,
795 bold,
796 italic,
797 data,
798 face_index,
799 units_per_em,
800 ascender,
801 descender,
802 line_gap,
803 shaper_data,
804 });
805 self.remember_font_key(key, idx);
806 if record_layout_use {
807 self.record_layout_font(font_id);
808 }
809
810 Ok(font_id)
811 }
812
813 pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
815 let font = self.get_font(font_id)?;
816 let scale = size_pt / font.units_per_em as f64;
817
818 Ok(FontMetrics {
819 ascent: font.ascender as f64 * scale,
820 descent: -(font.descender as f64) * scale, line_gap: font.line_gap as f64 * scale,
822 units_per_em: font.units_per_em,
823 })
824 }
825
826 pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
828 if text.is_empty() {
831 return Ok(ShapedText {
832 glyph_ids: Vec::new(),
833 advances: Vec::new(),
834 width: 0.0,
835 });
836 }
837
838 let key = ShapingKey {
839 font_id,
840 text: text.to_owned(),
841 size_bits: size_pt.to_bits(),
842 };
843 let mut memo = match self.shaping_memo.lock() {
844 Ok(memo) => memo,
845 Err(poisoned) => {
846 let mut memo = poisoned.into_inner();
847 memo.clear();
848 self.shaping_memo.clear_poison();
849 memo
850 }
851 };
852 if let Some(index) = memo
853 .entries
854 .iter()
855 .position(|(candidate, _, _)| candidate == &key)
856 {
857 let entry = memo.entries.remove(index).expect("cache index exists");
858 let shaped = entry.1.clone();
859 memo.entries.push_back(entry);
860 #[cfg(test)]
861 {
862 memo.hits += 1;
863 }
864 return Ok(shaped);
865 }
866 #[cfg(test)]
867 {
868 memo.misses += 1;
869 }
870
871 let font = self.get_font(font_id)?;
872
873 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
874 .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
875
876 let shaper = font.shaper_data.shaper(&face).build();
877
878 let mut buffer = harfrust::UnicodeBuffer::new();
879 buffer.push_str(text);
880 buffer.guess_segment_properties();
883
884 let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
885 let infos = output.glyph_infos();
886 let positions = output.glyph_positions();
887
888 let upem = font.units_per_em as f64;
889 let scale = size_pt / upem;
890
891 let mut glyph_ids = Vec::with_capacity(infos.len());
892 let mut advances = Vec::with_capacity(positions.len());
893 let mut total_width = 0.0;
894
895 for (info, pos) in infos.iter().zip(positions.iter()) {
896 glyph_ids.push(info.glyph_id as u16);
897 let advance = pos.x_advance as f64 * scale;
898 advances.push(advance);
899 total_width += advance;
900 }
901
902 let shaped = ShapedText {
903 glyph_ids,
904 advances,
905 width: total_width,
906 };
907 memo.insert(key, shaped.clone());
908 Ok(shaped)
909 }
910
911 pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
913 let font = self.get_font(font_id)?;
914 Ok(crate::output::FontData {
915 id: font.id,
916 family: font.family.clone(),
917 data: Arc::clone(&font.data),
918 face_index: font.face_index,
919 bold: font.bold,
920 italic: font.italic,
921 })
922 }
923
924 pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
926 self.fonts
927 .iter()
928 .map(|f| crate::output::FontData {
929 id: f.id,
930 family: f.family.clone(),
931 data: Arc::clone(&f.data),
932 face_index: f.face_index,
933 bold: f.bold,
934 italic: f.italic,
935 })
936 .collect()
937 }
938
939 fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
940 self.fonts
941 .iter()
942 .find(|f| f.id == font_id)
943 .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
944 }
945
946 fn remember_font_key(&mut self, key: FontKey, index: usize) {
947 if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
948 self.cache.insert(key, index);
949 }
950 }
951
952 fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
953 let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
954 if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
955 known.push(index);
956 }
957 }
958
959 fn remember_coverage_misses(&mut self, missing: &[char]) {
960 for &ch in missing {
961 if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
962 break;
963 }
964 self.coverage_misses.insert(ch);
965 }
966 }
967
968 fn record_layout_font(&mut self, font_id: FontId) {
969 if let Some(trace) = self.paragraph_font_trace.as_mut() {
970 if trace.ids.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
971 trace.ids.push(font_id);
972 } else {
973 trace.overflowed = true;
974 }
975 }
976 if !self.layout_fonts.contains(&font_id) {
977 self.layout_fonts.push(font_id);
978 }
979 }
980
981 #[cfg(test)]
982 fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
983 let memo = self
984 .shaping_memo
985 .lock()
986 .unwrap_or_else(std::sync::PoisonError::into_inner);
987 (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
988 }
989}
990
991fn bundled_font_database() -> fontdb::Database {
992 let mut db = fontdb::Database::new();
993 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
994 db.load_font_data(data.to_vec());
995 }
996 db
997}
998
999fn font_data_for_face(
1000 db: &fontdb::Database,
1001 id: fontdb::ID,
1002 memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
1003) -> Option<(Arc<[u8]>, u32)> {
1004 let face = db.face(id)?;
1005 let face_index = face.index;
1006 match &face.source {
1007 fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
1008 Some(data) => Some((Arc::clone(data), face_index)),
1009 None => {
1010 let data: Arc<[u8]> = Arc::from(data.as_ref().as_ref().to_vec());
1011 memory_face_data.insert(id, Arc::clone(&data));
1012 Some((data, face_index))
1013 }
1014 },
1015 #[cfg(feature = "system-fonts")]
1016 fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
1017 }
1018}
1019
1020#[cfg(feature = "system-fonts")]
1021fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
1022 let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
1023 shared_file_font_bytes_from_cache(cache, path)
1024}
1025
1026#[cfg(feature = "system-fonts")]
1027fn shared_file_font_bytes_from_cache(
1028 cache_lock: &Mutex<FileFontCache>,
1029 path: &Path,
1030) -> Option<Arc<[u8]>> {
1031 let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1032 let mut cache = match cache_lock.lock() {
1033 Ok(cache) => cache,
1034 Err(poisoned) => {
1035 let mut cache = poisoned.into_inner();
1036 cache.clear();
1037 cache_lock.clear_poison();
1038 cache
1039 }
1040 };
1041 if let Some(index) = cache
1042 .entries
1043 .iter()
1044 .position(|(candidate, _, _)| candidate == &identity)
1045 {
1046 let entry = cache.entries.remove(index).expect("cache index exists");
1047 let bytes = Arc::clone(&entry.1);
1048 cache.entries.push_back(entry);
1049 return Some(bytes);
1050 }
1051
1052 let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
1053 cache_file_font_bytes(&mut cache, identity, bytes)
1054}
1055
1056#[cfg(feature = "system-fonts")]
1057fn cache_file_font_bytes(
1058 cache: &mut FileFontCache,
1059 identity: PathBuf,
1060 bytes: Arc<[u8]>,
1061) -> Option<Arc<[u8]>> {
1062 let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
1063 .saturating_add(identity.as_os_str().len())
1064 .saturating_add(bytes.len());
1065 if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
1066 while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
1067 || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
1068 {
1069 let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
1070 break;
1071 };
1072 cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
1073 }
1074 cache.bytes += entry_bytes;
1075 cache
1076 .entries
1077 .push_back((identity, Arc::clone(&bytes), entry_bytes));
1078 }
1079 Some(bytes)
1080}
1081
1082fn map_font_name(name: &str) -> &[&str] {
1089 match name {
1090 "Calibri" => &["Calibri", "Carlito"],
1091 "Calibri Light" => &["Calibri Light", "Carlito"],
1092 "Cambria" => &["Cambria", "Caladea"],
1093 "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
1094 "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
1095 "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
1096 "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
1097 "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
1098 "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
1099 "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
1100 "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
1101 "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
1102 "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
1103 "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
1104 "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
1105 "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
1106 "Impact" => &["Impact", "Liberation Sans", "Arial"],
1107 "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
1108 "Symbol" => &["Symbol", "DejaVu Sans"],
1109 "Wingdings" => &["Wingdings", "Symbol"],
1110 _ => &[],
1111 }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::*;
1117 use crate::bundled_fonts::bundled_font_data;
1118
1119 fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
1120 assert_eq!(family.len(), 7);
1121 let mut font = source.to_vec();
1122 let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
1123 let name_offset = (0..table_count)
1124 .find_map(|table| {
1125 let record = 12 + table * 16;
1126 (&font[record..record + 4] == b"name").then(|| {
1127 u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
1128 })
1129 })
1130 .expect("font has name table");
1131 let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
1132 let strings = name_offset
1133 + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
1134 for index in 0..count {
1135 let record = name_offset + 6 + index * 12;
1136 let platform = u16::from_be_bytes([font[record], font[record + 1]]);
1137 let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
1138 let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
1139 let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
1140 if !matches!(name_id, 1 | 16) {
1141 continue;
1142 }
1143 let destination = &mut font[strings + offset..strings + offset + length];
1144 match (platform, length) {
1145 (0 | 3, 14) => {
1146 for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
1147 bytes.copy_from_slice(&(ch as u16).to_be_bytes());
1148 }
1149 }
1150 (1, 7) => destination.copy_from_slice(family.as_bytes()),
1151 _ => {}
1152 }
1153 }
1154 font
1155 }
1156
1157 #[cfg(feature = "system-fonts")]
1158 fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
1159 let header_len = 12 + fonts.len() * 4;
1160 let mut collection = vec![0u8; header_len];
1161 collection[0..4].copy_from_slice(b"ttcf");
1162 collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
1163 collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
1164
1165 for (font_number, font) in fonts.iter().enumerate() {
1166 while !collection.len().is_multiple_of(4) {
1167 collection.push(0);
1168 }
1169 let collection_offset = collection.len();
1170 collection[12 + font_number * 4..16 + font_number * 4]
1171 .copy_from_slice(&(collection_offset as u32).to_be_bytes());
1172
1173 let mut adjusted = font.to_vec();
1174 let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
1175 for table in 0..table_count {
1176 let offset_position = 12 + table * 16 + 8;
1177 let offset = u32::from_be_bytes(
1178 adjusted[offset_position..offset_position + 4]
1179 .try_into()
1180 .expect("table offset"),
1181 );
1182 adjusted[offset_position..offset_position + 4]
1183 .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
1184 }
1185 collection.extend_from_slice(&adjusted);
1186 }
1187 collection
1188 }
1189
1190 #[test]
1191 fn deterministic_font_manager_uses_only_bundled_fonts() {
1192 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1193
1194 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1195 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
1196 }
1197
1198 #[cfg(feature = "system-fonts")]
1199 #[test]
1200 fn normal_font_discovery_initializes_once_per_process() {
1201 let _first = FontManager::new();
1202 let _second = FontManager::new();
1203 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1204
1205 let _deterministic =
1206 FontManager::new_deterministic().expect("bundled font manager should load");
1207 let _caller = FontManager::new_with_fonts(Vec::new());
1208 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1209 }
1210
1211 #[cfg(feature = "system-fonts")]
1212 #[test]
1213 fn file_backed_collection_faces_share_one_byte_buffer() {
1214 let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
1215 let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
1216 let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
1217 let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
1218 std::fs::write(&first_path, &collection).expect("write first temporary collection");
1219 std::fs::write(&second_path, &collection).expect("write second temporary collection");
1220
1221 let mut db = fontdb::Database::new();
1222 db.load_font_file(&first_path).expect("load first TTC");
1223 db.load_font_file(&second_path).expect("load second TTC");
1224 let canonical_first = std::fs::canonicalize(&first_path).unwrap();
1225 let canonical_second = std::fs::canonicalize(&second_path).unwrap();
1226 let first_ids = db
1227 .faces()
1228 .filter_map(|face| match &face.source {
1229 fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
1230 Some(face.id)
1231 }
1232 _ => None,
1233 })
1234 .collect::<Vec<_>>();
1235 let second_id = db
1236 .faces()
1237 .find_map(|face| match &face.source {
1238 fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
1239 Some(face.id)
1240 }
1241 _ => None,
1242 })
1243 .expect("second TTC face");
1244 assert_eq!(first_ids.len(), 2);
1245
1246 let mut memory = HashMap::new();
1247 let (first_face, first_index) =
1248 font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
1249 let (second_face, second_index) =
1250 font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
1251 let (other_file, _) =
1252 font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
1253 assert_ne!(first_index, second_index);
1254 assert!(Arc::ptr_eq(&first_face, &second_face));
1255 assert!(!Arc::ptr_eq(&first_face, &other_file));
1256
1257 std::fs::remove_file(first_path).expect("remove first temporary font");
1258 std::fs::remove_file(second_path).expect("remove second temporary font");
1259 }
1260
1261 #[test]
1262 fn shaping_memo_uses_complete_text_size_and_font_identity() {
1263 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1264 let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1265 let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
1266
1267 let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
1268 let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
1269 assert_eq!(first.glyph_ids, repeat.glyph_ids);
1270 assert_eq!(fm.shaping_memo_counts().0, 1);
1271
1272 fm.shape_text(regular, "different text", 11.0).unwrap();
1273 fm.shape_text(regular, "exact text", 12.0).unwrap();
1274 fm.shape_text(bold, "exact text", 11.0).unwrap();
1275 assert_eq!(fm.shaping_memo_counts().1, 4);
1276
1277 let replacement = FontFile {
1278 family: "Carlito".to_owned(),
1279 data: bundled_font_data()[1].1.to_vec(),
1280 };
1281 fm.load_additional_fonts(&[replacement]);
1282 assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
1283 let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1284 fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
1285 assert_eq!(fm.shaping_memo_counts().1, 1);
1286 }
1287
1288 #[test]
1289 fn shaping_memo_is_bounded_and_recovers_from_poison() {
1290 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1291 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1292 for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
1293 fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
1294 .unwrap();
1295 }
1296 let (_, _, entries, bytes) = fm.shaping_memo_counts();
1297 assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
1298 assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
1299
1300 let fm = Arc::new(fm);
1301 let poison = Arc::clone(&fm);
1302 assert!(
1303 std::thread::spawn(move || {
1304 let _guard = poison.shaping_memo.lock().unwrap();
1305 panic!("poison shaping cache for recovery coverage");
1306 })
1307 .join()
1308 .is_err()
1309 );
1310 let first = fm.shape_text(font, "after poison", 11.0).unwrap();
1311 let second = fm.shape_text(font, "after poison", 11.0).unwrap();
1312 assert_eq!(first.glyph_ids, second.glyph_ids);
1313 let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
1314 assert_eq!((hits, misses, entries), (1, 1, 1));
1315 assert!(bytes > 0);
1316 }
1317
1318 #[test]
1319 fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
1320 let mut memo = ShapingMemo::new();
1321 for suffix in ['a', 'b'] {
1322 memo.insert(
1323 ShapingKey {
1324 font_id: FontId(0),
1325 text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
1326 size_bits: 11.0f64.to_bits(),
1327 },
1328 ShapedText {
1329 glyph_ids: Vec::new(),
1330 advances: Vec::new(),
1331 width: 0.0,
1332 },
1333 );
1334 }
1335 assert_eq!(memo.entries.len(), 1);
1336 assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
1337 }
1338
1339 #[test]
1340 fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
1341 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1342 for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
1343 fm.remember_coverage_fallback(false, false, 0);
1344 }
1345 assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
1346
1347 let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
1348 .filter_map(|value| char::from_u32(0x10_000 + value as u32))
1349 .collect::<Vec<_>>();
1350 fm.remember_coverage_misses(&misses);
1351 assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
1352
1353 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1354 fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
1355 .expect("bounded fallback resolves");
1356 }
1357 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1358 assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
1359 }
1360
1361 #[test]
1362 fn active_document_may_resolve_more_than_256_distinct_faces() {
1363 let source = bundled_font_data()[4].1;
1364 let mut db = fontdb::Database::new();
1365 for index in 0..257 {
1366 db.load_font_data(font_with_family(source, &format!("F{index:06}")));
1367 }
1368 let mut fm = FontManager::from_base_database(db);
1369 fm.begin_layout();
1370 let mut ids = HashSet::new();
1371 for index in 0..257 {
1372 let family = format!("F{index:06}");
1373 let id = fm
1374 .resolve_font(Some(&family), false, false)
1375 .expect("distinct active face resolves");
1376 assert_eq!(fm.font_data(id).unwrap().family, family);
1377 ids.insert(id);
1378 }
1379 assert_eq!(ids.len(), 257);
1380 fm.retain_current_fonts();
1381 assert_eq!(fm.fonts.len(), 257);
1382 }
1383
1384 #[test]
1385 fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
1386 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1387 fm.begin_layout();
1388 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1389 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1390 }
1391 assert!(fm.paragraph_font_trace.is_none());
1392
1393 fm.begin_paragraph_font_trace();
1394 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1395 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1396 }
1397 assert!(fm.finish_paragraph_font_trace().is_none());
1398
1399 fm.begin_layout();
1400 assert_eq!(fm.layout_fonts.capacity(), 0);
1401 fm.begin_paragraph_font_trace();
1402 fm.resolve_font(Some("Carlito"), false, false).unwrap();
1403 let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
1404 assert_eq!(trace.len(), 1);
1405 assert_eq!(trace.capacity(), trace.len());
1406 }
1407
1408 #[cfg(feature = "system-fonts")]
1409 #[test]
1410 fn file_byte_cache_is_bounded_and_recovers_from_poison() {
1411 let cache = Arc::new(Mutex::new(FileFontCache::new()));
1412 {
1413 let mut cache = cache
1414 .lock()
1415 .unwrap_or_else(std::sync::PoisonError::into_inner);
1416 cache.clear();
1417 let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
1418 let returned = cache_file_font_bytes(
1419 &mut cache,
1420 PathBuf::from("oversized-font.ttc"),
1421 Arc::clone(&oversized),
1422 )
1423 .expect("oversized bytes are returned uncached");
1424 assert!(Arc::ptr_eq(&oversized, &returned));
1425 assert!(cache.entries.is_empty());
1426 assert_eq!(cache.bytes, 0);
1427 }
1428
1429 let poison = Arc::clone(&cache);
1430 assert!(
1431 std::thread::spawn(move || {
1432 let cache = poison;
1433 let _guard = cache.lock().unwrap();
1434 panic!("poison file byte cache for recovery coverage");
1435 })
1436 .join()
1437 .is_err()
1438 );
1439
1440 let path = std::env::temp_dir().join(format!(
1441 "rdocx-font-cache-poison-{}-{:?}.ttf",
1442 std::process::id(),
1443 std::thread::current().id()
1444 ));
1445 std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
1446 let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
1447 let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
1448 assert!(Arc::ptr_eq(&first, &second));
1449 std::fs::remove_file(path).expect("remove recovery font");
1450 }
1451
1452 #[cfg(not(feature = "system-fonts"))]
1453 #[test]
1454 fn no_default_features_omits_system_font_discovery() {
1455 let fm = FontManager::new();
1456 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1457 }
1458
1459 #[test]
1460 fn font_manager_with_no_fonts_returns_an_error() {
1461 let mut fm = FontManager::new_with_fonts(Vec::new());
1462 assert!(matches!(
1463 fm.resolve_font(None, false, false),
1464 Err(LayoutError::FontNotFound(_))
1465 ));
1466 }
1467
1468 #[test]
1469 fn load_system_font() {
1470 let mut fm = FontManager::new();
1471 let result = fm.resolve_font(None, false, false);
1473 if let Ok(id) = result {
1475 assert_eq!(id.0, 0);
1476 }
1477 }
1478
1479 #[test]
1480 fn font_metrics_positive() {
1481 let mut fm = FontManager::new();
1482 if let Ok(id) = fm.resolve_font(None, false, false) {
1483 let metrics = fm.metrics(id, 12.0).unwrap();
1484 assert!(metrics.ascent > 0.0);
1485 assert!(metrics.descent > 0.0);
1486 assert!(metrics.units_per_em > 0);
1487 }
1488 }
1489
1490 #[test]
1491 fn shape_hello_world() {
1492 let mut fm = FontManager::new();
1493 if let Ok(id) = fm.resolve_font(None, false, false) {
1494 let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
1495 assert!(!shaped.glyph_ids.is_empty());
1496 assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
1497 assert!(shaped.width > 0.0);
1498 }
1499 }
1500
1501 #[test]
1502 fn font_caching() {
1503 let mut fm = FontManager::new();
1504 if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
1505 let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
1506 assert_eq!(id1, id2);
1507 }
1508 }
1509
1510 #[test]
1511 fn font_resolution_alias_cache_is_bounded() {
1512 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1513 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1514 fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
1515 .expect("fallback font resolves");
1516 }
1517 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1518 assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1519 }
1520
1521 #[test]
1522 fn bold_italic_variants() {
1523 let mut fm = FontManager::new();
1524 let regular = fm.resolve_font(None, false, false);
1525 let bold = fm.resolve_font(None, true, false);
1526 if let (Ok(r), Ok(b)) = (regular, bold) {
1527 assert_ne!(r, b);
1529 }
1530 }
1531
1532 #[test]
1535 fn latin_text_resolves_the_same_as_by_name() {
1536 let mut fm = FontManager::new();
1537 let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
1538 return;
1539 };
1540 let for_text = fm
1541 .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
1542 .unwrap();
1543 assert_eq!(by_name, for_text);
1544 }
1545
1546 #[test]
1552 fn text_no_font_can_draw_keeps_the_requested_font() {
1553 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1554 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1555 let resolved = fm
1556 .resolve_font_for_text(Some("Carlito"), false, false, "这是中文")
1557 .unwrap();
1558 assert_eq!(
1559 primary, resolved,
1560 "with no covering font available the original must be kept"
1561 );
1562 }
1563
1564 #[test]
1566 fn whitespace_does_not_trigger_a_fallback() {
1567 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1568 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1569 let idx = fm.index_of(by_name).unwrap();
1570 assert!(
1572 fm.uncovered(idx, "a\u{00a0}b\tc")
1573 .iter()
1574 .all(|c| *c != '\t'),
1575 "control and whitespace characters must be ignored"
1576 );
1577 }
1578
1579 #[test]
1585 fn cjk_text_moves_off_a_latin_font_when_possible() {
1586 let mut fm = FontManager::new();
1587 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
1588 return;
1589 };
1590 let Some(idx) = fm.index_of(latin) else {
1591 return;
1592 };
1593 if fm.uncovered(idx, "这是中文").is_empty() {
1594 return; }
1596 let resolved = fm
1597 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
1598 .unwrap();
1599 if resolved == latin {
1600 return; }
1602 let new_idx = fm.index_of(resolved).unwrap();
1603 assert!(
1604 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
1605 "the replacement must cover more of the text than the original"
1606 );
1607 }
1608}