1use std::collections::{HashMap, HashSet, VecDeque};
7use std::ops::Range;
8#[cfg(feature = "system-fonts")]
9use std::path::{Path, PathBuf};
10#[cfg(feature = "system-fonts")]
11use std::sync::OnceLock;
12use std::sync::{Arc, Mutex};
13
14#[cfg(all(test, feature = "system-fonts"))]
15use std::sync::atomic::{AtomicUsize, Ordering};
16
17use crate::error::{LayoutError, Result};
18use crate::line::TextSegment;
19use crate::output::{FontId, SourceSpan};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct FontFile {
24 pub family: String,
26 pub data: Vec<u8>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32struct FontKey {
33 family: String,
34 bold: bool,
35 italic: bool,
36}
37
38#[derive(Debug, Clone, Copy)]
40pub struct FontMetrics {
41 pub ascent: f64,
43 pub descent: f64,
45 pub line_gap: f64,
47 pub units_per_em: u16,
49}
50
51#[derive(Debug, Clone)]
53pub struct ShapedText {
54 pub glyph_ids: Vec<u16>,
56 pub advances: Vec<f64>,
58 pub width: f64,
60}
61
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
64pub enum TextDirection {
65 #[default]
66 Auto,
67 LeftToRight,
68 RightToLeft,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum TextScript {
74 Latin,
75 Arabic,
76 Hebrew,
77 Devanagari,
78 Thai,
79 Han,
80 Common,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct GlyphCluster {
86 pub glyph_start: u32,
87 pub glyph_end: u32,
88 pub char_start: u32,
89 pub char_end: u32,
90}
91
92impl GlyphCluster {
93 pub fn is_valid(&self) -> bool {
94 self.glyph_start < self.glyph_end && self.char_start < self.char_end
95 }
96
97 pub fn char_range(&self) -> Range<u32> {
98 self.char_start..self.char_end
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct MultilingualTextSegment {
105 base: TextSegment,
106 logical_index: usize,
107 language: Option<String>,
108 script: TextScript,
109 direction: TextDirection,
110 bidi_level: u8,
111 x_advances: Vec<f64>,
112 y_advances: Vec<f64>,
113 x_offsets: Vec<f64>,
114 y_offsets: Vec<f64>,
115 clusters: Vec<GlyphCluster>,
116 break_after: bool,
117}
118
119impl MultilingualTextSegment {
120 #[allow(clippy::too_many_arguments)]
121 pub fn new(
122 base: TextSegment,
123 logical_index: usize,
124 language: Option<String>,
125 script: TextScript,
126 direction: TextDirection,
127 bidi_level: u8,
128 x_advances: Vec<f64>,
129 y_advances: Vec<f64>,
130 x_offsets: Vec<f64>,
131 y_offsets: Vec<f64>,
132 clusters: Vec<GlyphCluster>,
133 break_after: bool,
134 ) -> Result<Self> {
135 let glyph_count = base.glyph_ids.len();
136 let char_count = base.text.chars().count();
137 let valid_level = unicode_bidi::Level::new(bidi_level).is_ok();
138 if x_advances.len() != glyph_count
139 || y_advances.len() != glyph_count
140 || x_offsets.len() != glyph_count
141 || y_offsets.len() != glyph_count
142 || base.advances.len() != glyph_count
143 || !valid_level
144 || !position_values_are_finite(
145 &x_advances,
146 &y_advances,
147 &x_offsets,
148 &y_offsets,
149 base.width,
150 )
151 || !cluster_ranges_are_valid(&clusters, glyph_count, char_count, bidi_level % 2 == 1)
152 {
153 return Err(LayoutError::Layout(
154 "invalid multilingual glyph positioning or cluster range".to_owned(),
155 ));
156 }
157 Ok(Self {
158 base,
159 logical_index,
160 language,
161 script,
162 direction,
163 bidi_level,
164 x_advances,
165 y_advances,
166 x_offsets,
167 y_offsets,
168 clusters,
169 break_after,
170 })
171 }
172
173 pub fn text(&self) -> &str {
174 &self.base.text
175 }
176
177 pub fn base(&self) -> &TextSegment {
178 &self.base
179 }
180
181 pub fn font_id(&self) -> FontId {
182 self.base.font_id
183 }
184
185 pub fn language(&self) -> Option<&str> {
186 self.language.as_deref()
187 }
188
189 pub fn script(&self) -> TextScript {
190 self.script
191 }
192
193 pub fn direction(&self) -> TextDirection {
194 self.direction
195 }
196
197 pub fn bidi_level(&self) -> u8 {
198 self.bidi_level
199 }
200
201 pub fn logical_index(&self) -> usize {
202 self.logical_index
203 }
204
205 pub fn glyph_ids(&self) -> &[u16] {
206 &self.base.glyph_ids
207 }
208
209 pub fn x_advances(&self) -> &[f64] {
210 &self.x_advances
211 }
212
213 pub fn y_advances(&self) -> &[f64] {
214 &self.y_advances
215 }
216
217 pub fn x_offsets(&self) -> &[f64] {
218 &self.x_offsets
219 }
220
221 pub fn y_offsets(&self) -> &[f64] {
222 &self.y_offsets
223 }
224
225 pub fn clusters(&self) -> &[GlyphCluster] {
226 &self.clusters
227 }
228
229 pub fn width(&self) -> f64 {
230 self.base.width
231 }
232
233 pub fn break_after(&self) -> bool {
234 self.break_after
235 }
236}
237
238pub(crate) fn position_values_are_finite(
239 x_advances: &[f64],
240 y_advances: &[f64],
241 x_offsets: &[f64],
242 y_offsets: &[f64],
243 width: f64,
244) -> bool {
245 width.is_finite()
246 && x_advances
247 .iter()
248 .chain(y_advances)
249 .chain(x_offsets)
250 .chain(y_offsets)
251 .all(|value| value.is_finite())
252}
253
254pub(crate) fn cluster_ranges_are_valid(
255 clusters: &[GlyphCluster],
256 glyph_count: usize,
257 char_count: usize,
258 right_to_left: bool,
259) -> bool {
260 if glyph_count == 0 || char_count == 0 {
261 return glyph_count == 0 && char_count == 0 && clusters.is_empty();
262 }
263 let mut previous_glyph_end = 0u32;
264 let mut previous_char_range = None::<Range<u32>>;
265 let mut covered_chars = 0usize;
266 for cluster in clusters {
267 if !cluster.is_valid()
268 || cluster.glyph_start != previous_glyph_end
269 || cluster.glyph_end as usize > glyph_count
270 || cluster.char_end as usize > char_count
271 {
272 return false;
273 }
274 if let Some(previous) = previous_char_range {
275 let contiguous = if right_to_left {
276 cluster.char_end == previous.start
277 } else {
278 cluster.char_start == previous.end
279 };
280 if !contiguous {
281 return false;
282 }
283 }
284 previous_glyph_end = cluster.glyph_end;
285 previous_char_range = Some(cluster.char_range());
286 covered_chars += (cluster.char_end - cluster.char_start) as usize;
287 }
288 previous_glyph_end as usize == glyph_count
289 && covered_chars == char_count
290 && previous_char_range.is_some_and(|last| {
291 if right_to_left {
292 clusters
293 .first()
294 .is_some_and(|first| first.char_end as usize == char_count)
295 && last.start == 0
296 } else {
297 clusters.first().is_some_and(|first| first.char_start == 0)
298 && last.end as usize == char_count
299 }
300 })
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
304struct ShapingKey {
305 font_id: FontId,
306 text: String,
307 size_bits: u64,
308}
309
310struct ShapingMemo {
311 entries: VecDeque<(ShapingKey, ShapedText, usize, u64)>,
312 bytes: usize,
313 #[cfg(test)]
314 hits: usize,
315 #[cfg(test)]
316 misses: usize,
317}
318
319impl ShapingMemo {
320 fn new() -> Self {
321 Self {
322 entries: VecDeque::new(),
323 bytes: 0,
324 #[cfg(test)]
325 hits: 0,
326 #[cfg(test)]
327 misses: 0,
328 }
329 }
330
331 fn clear(&mut self) {
332 self.entries.clear();
333 self.bytes = 0;
334 #[cfg(test)]
335 {
336 self.hits = 0;
337 self.misses = 0;
338 }
339 }
340
341 fn insert(&mut self, key: ShapingKey, shaped: ShapedText) {
342 let entry_bytes = std::mem::size_of::<(ShapingKey, ShapedText, usize, u64)>()
343 + key.text.len()
344 + shaped.glyph_ids.len() * std::mem::size_of::<u16>()
345 + shaped.advances.len() * std::mem::size_of::<f64>();
346 if entry_bytes > SHAPING_CACHE_MAX_BYTES {
347 return;
348 }
349 while self.entries.len() >= SHAPING_CACHE_MAX_ENTRIES
350 || self.bytes.saturating_add(entry_bytes) > SHAPING_CACHE_MAX_BYTES
351 {
352 let Some((_, _, evicted_bytes, _)) = self.entries.pop_front() else {
353 break;
354 };
355 self.bytes = self.bytes.saturating_sub(evicted_bytes);
356 }
357 self.bytes += entry_bytes;
358 let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
359 self.entries
360 .push_back((key, shaped, entry_bytes, fingerprint));
361 }
362}
363
364fn shaping_fingerprint(font_id: FontId, text: &str, size_bits: u64) -> u64 {
365 use std::hash::{Hash, Hasher};
366
367 let mut hasher = std::collections::hash_map::DefaultHasher::new();
368 font_id.hash(&mut hasher);
369 text.hash(&mut hasher);
370 size_bits.hash(&mut hasher);
371 hasher.finish()
372}
373
374const SHAPING_CACHE_MAX_ENTRIES: usize = 2_048;
375const SHAPING_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
376
377#[cfg(feature = "system-fonts")]
378struct FileFontCache {
379 entries: VecDeque<(PathBuf, Arc<[u8]>, usize)>,
380 bytes: usize,
381}
382
383#[cfg(feature = "system-fonts")]
384impl FileFontCache {
385 fn new() -> Self {
386 Self {
387 entries: VecDeque::new(),
388 bytes: 0,
389 }
390 }
391
392 fn clear(&mut self) {
393 self.entries.clear();
394 self.bytes = 0;
395 }
396}
397
398#[cfg(feature = "system-fonts")]
399const FILE_FONT_CACHE_MAX_ENTRIES: usize = 256;
400#[cfg(feature = "system-fonts")]
401const FILE_FONT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
402
403#[cfg(feature = "system-fonts")]
404static NORMAL_FONT_DATABASE: OnceLock<fontdb::Database> = OnceLock::new();
405#[cfg(feature = "system-fonts")]
406static FILE_FONT_CACHE: OnceLock<Mutex<FileFontCache>> = OnceLock::new();
407#[cfg(all(test, feature = "system-fonts"))]
408static SYSTEM_FONT_DISCOVERY_RUNS: AtomicUsize = AtomicUsize::new(0);
409
410struct LoadedFont {
412 db_id: fontdb::ID,
413 id: FontId,
414 family: String,
415 bold: bool,
416 italic: bool,
417 data: Arc<[u8]>,
418 face_index: u32,
419 units_per_em: u16,
420 ascender: i16,
422 descender: i16,
423 line_gap: i16,
424 shaper_data: harfrust::ShaperData,
427}
428
429struct ParagraphFontTrace {
430 ids: Vec<FontId>,
431 overflowed: bool,
432}
433
434pub struct FontManager {
436 db: fontdb::Database,
437 base_db: fontdb::Database,
439 base_caller_aliases: HashMap<String, Vec<fontdb::ID>>,
441 base_caller_families: HashMap<String, Vec<fontdb::ID>>,
443 cache: HashMap<FontKey, usize>,
445 memory_face_data: HashMap<fontdb::ID, Arc<[u8]>>,
447 fonts: Vec<LoadedFont>,
449 next_id: u32,
451 coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
459 coverage_misses: HashSet<char>,
462 additional_fonts: Vec<FontFile>,
464 caller_aliases: HashMap<String, Vec<fontdb::ID>>,
467 caller_families: HashMap<String, Vec<fontdb::ID>>,
470 explicit_aliases: Vec<(String, String)>,
472 explicit_alias_map: HashMap<String, String>,
474 shaping_memo: Mutex<ShapingMemo>,
476 paragraph_font_trace: Option<ParagraphFontTrace>,
478 layout_fonts: Vec<FontId>,
480}
481
482const BROAD_COVERAGE_FAMILIES: &[&str] = &[
487 "Noto Sans Arabic",
489 "Noto Sans Devanagari",
490 "Noto Sans Thai",
491 "Noto Sans SC",
492 "Noto Sans CJK SC",
494 "Noto Sans CJK JP",
495 "Noto Sans CJK KR",
496 "Noto Sans CJK TC",
497 "Noto Serif CJK SC",
498 "Source Han Sans SC",
499 "WenQuanYi Zen Hei",
500 "WenQuanYi Micro Hei",
501 "PingFang SC",
503 "PingFang TC",
504 "Hiragino Sans",
505 "Hiragino Kaku Gothic ProN",
506 "Apple SD Gothic Neo",
507 "Songti SC",
508 "STHeiti",
509 "Microsoft YaHei",
511 "Microsoft JhengHei",
512 "SimSun",
513 "SimHei",
514 "NSimSun",
515 "Yu Gothic",
516 "MS Gothic",
517 "Meiryo",
518 "Malgun Gothic",
519 "Arial Unicode MS",
521 "DejaVu Sans",
522];
523
524const RESOLUTION_CACHE_MAX_ENTRIES: usize = 256;
525const COVERAGE_FALLBACK_MAX_ENTRIES: usize = 256;
526const COVERAGE_MISS_MAX_ENTRIES: usize = 4_096;
527const PARAGRAPH_FONT_TRACE_MAX_ENTRIES: usize = 4_096;
528
529const CALLER_ALIAS_MAX_ENTRIES: usize = 256;
531
532const CALLER_ALIAS_MAX_RETAINED_BYTES: usize = 64 * 1024;
535
536fn bounded_caller_aliases(aliases: &[(String, String)]) -> Vec<(String, String)> {
543 let mut bounded = Vec::with_capacity(aliases.len().min(CALLER_ALIAS_MAX_ENTRIES));
544 let mut retained_bytes = 0usize;
545 for (requested, target) in aliases {
546 if bounded.len() == CALLER_ALIAS_MAX_ENTRIES {
547 break;
548 }
549 let normalized_requested = requested.to_lowercase();
550 let entry_bytes = requested
551 .len()
552 .saturating_add(target.len())
553 .saturating_add(normalized_requested.len())
554 .saturating_add(target.len());
555 let next_retained_bytes = retained_bytes.saturating_add(entry_bytes);
556 if next_retained_bytes > CALLER_ALIAS_MAX_RETAINED_BYTES {
557 break;
558 }
559 bounded.push((requested.to_owned(), target.to_owned()));
560 retained_bytes = next_retained_bytes;
561 }
562 bounded
563}
564
565impl Default for FontManager {
566 fn default() -> Self {
567 Self::new()
568 }
569}
570
571impl FontManager {
572 fn from_base_database(db: fontdb::Database) -> Self {
573 Self {
574 base_db: db.clone(),
575 base_caller_aliases: HashMap::new(),
576 base_caller_families: HashMap::new(),
577 db,
578 cache: HashMap::new(),
579 memory_face_data: HashMap::new(),
580 fonts: Vec::new(),
581 next_id: 0,
582 coverage_fallbacks: HashMap::new(),
583 coverage_misses: HashSet::new(),
584 additional_fonts: Vec::new(),
585 caller_aliases: HashMap::new(),
586 caller_families: HashMap::new(),
587 explicit_aliases: Vec::new(),
588 explicit_alias_map: HashMap::new(),
589 shaping_memo: Mutex::new(ShapingMemo::new()),
590 paragraph_font_trace: None,
591 layout_fonts: Vec::new(),
592 }
593 }
594
595 pub fn new() -> Self {
600 #[cfg(feature = "system-fonts")]
601 {
602 let db = NORMAL_FONT_DATABASE.get_or_init(|| {
603 let mut db = bundled_font_database();
604 db.load_system_fonts();
605 #[cfg(test)]
606 SYSTEM_FONT_DISCOVERY_RUNS.fetch_add(1, Ordering::Relaxed);
607 db
608 });
609 Self::from_base_database(db.clone())
610 }
611
612 #[cfg(not(feature = "system-fonts"))]
613 Self::from_base_database(bundled_font_database())
614 }
615
616 pub fn new_deterministic() -> Result<Self> {
621 Ok(Self::from_base_database(bundled_font_database()))
622 }
623
624 pub fn load_additional_fonts(&mut self, font_files: &[FontFile]) -> bool {
631 if self.additional_fonts == font_files {
632 return false;
633 }
634
635 self.db = self.base_db.clone();
636 self.caller_aliases.clone_from(&self.base_caller_aliases);
637 self.caller_families.clone_from(&self.base_caller_families);
638 for font_file in font_files {
639 Self::load_caller_font(
640 &mut self.db,
641 &mut self.caller_aliases,
642 &mut self.caller_families,
643 &font_file.family,
644 font_file.data.clone(),
645 );
646 }
647 self.cache.clear();
648 self.memory_face_data.clear();
649 self.fonts.clear();
650 self.next_id = 0;
651 self.coverage_fallbacks.clear();
652 self.coverage_misses.clear();
653 self.additional_fonts = font_files.to_vec();
654 if self.shaping_memo.is_poisoned() {
655 self.shaping_memo.clear_poison();
656 }
657 self.shaping_memo
658 .get_mut()
659 .expect("shaping cache poison was cleared")
660 .clear();
661 true
662 }
663
664 pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
669 let mut db = fontdb::Database::new();
670 let mut caller_aliases = HashMap::new();
671 let mut caller_families = HashMap::new();
672 for (family, data) in &fonts {
673 Self::load_caller_font(
674 &mut db,
675 &mut caller_aliases,
676 &mut caller_families,
677 family,
678 data.clone(),
679 );
680 }
681 let mut manager = Self::from_base_database(db);
682 manager.base_caller_aliases = caller_aliases.clone();
683 manager.base_caller_families = caller_families.clone();
684 manager.caller_aliases = caller_aliases;
685 manager.caller_families = caller_families;
686 manager
687 }
688
689 pub fn set_caller_aliases(&mut self, aliases: &[(String, String)]) -> bool {
695 let aliases = bounded_caller_aliases(aliases);
696 if self.explicit_aliases == aliases {
697 return false;
698 }
699
700 self.explicit_alias_map = aliases
701 .iter()
702 .map(|(requested, target)| (requested.to_lowercase(), target.clone()))
703 .collect();
704 self.explicit_aliases = aliases;
705 self.cache.clear();
706 self.coverage_fallbacks.clear();
707 self.coverage_misses.clear();
708 true
709 }
710
711 fn load_caller_font(
712 db: &mut fontdb::Database,
713 aliases: &mut HashMap<String, Vec<fontdb::ID>>,
714 families: &mut HashMap<String, Vec<fontdb::ID>>,
715 family: &str,
716 data: Vec<u8>,
717 ) {
718 let before = db.len();
719 db.load_font_data(data);
720 let loaded_faces = db
721 .faces()
722 .skip(before)
723 .map(|face| {
724 (
725 face.id,
726 face.families
727 .iter()
728 .map(|(name, _)| name.clone())
729 .collect::<Vec<_>>(),
730 )
731 })
732 .collect::<Vec<_>>();
733 for (id, loaded_families) in &loaded_faces {
734 for loaded_family in loaded_families {
735 families.entry(loaded_family.clone()).or_default().push(*id);
736 }
737 }
738 if !loaded_faces.is_empty()
739 && loaded_faces
740 .iter()
741 .all(|(_, families)| !families.iter().any(|loaded| loaded == family))
742 {
743 aliases
744 .entry(family.to_lowercase())
745 .or_default()
746 .extend(loaded_faces.into_iter().map(|(id, _)| id));
747 }
748 }
749
750 #[doc(hidden)]
752 pub fn begin_layout(&mut self) {
753 self.paragraph_font_trace = None;
754 self.layout_fonts = Vec::new();
755 }
756
757 #[doc(hidden)]
759 pub fn begin_paragraph_font_trace(&mut self) {
760 self.paragraph_font_trace = Some(ParagraphFontTrace {
761 ids: Vec::new(),
762 overflowed: false,
763 });
764 }
765
766 #[doc(hidden)]
768 pub fn finish_paragraph_font_trace(&mut self) -> Option<Vec<FontId>> {
769 let mut trace = self.paragraph_font_trace.take()?;
770 if trace.overflowed {
771 return None;
772 }
773 trace.ids.shrink_to_fit();
774 Some(trace.ids)
775 }
776
777 #[doc(hidden)]
779 pub fn replay_layout_font_trace(&mut self, trace: &[FontId]) {
780 for &font_id in trace {
781 self.record_layout_font(font_id);
782 }
783 }
784
785 #[doc(hidden)]
787 pub fn current_layout_fonts(&self) -> &[FontId] {
788 &self.layout_fonts
789 }
790
791 #[doc(hidden)]
793 pub fn every_loaded_font_is_current(&self) -> bool {
794 self.fonts
795 .iter()
796 .map(|font| font.id)
797 .eq(self.layout_fonts.iter().copied())
798 && self
799 .layout_fonts
800 .iter()
801 .enumerate()
802 .all(|(index, font_id)| *font_id == FontId(index as u32))
803 }
804
805 #[doc(hidden)]
809 pub fn retain_current_fonts(&mut self) {
810 let current = self.layout_fonts.iter().copied().collect::<HashSet<_>>();
811 let old_index_ids = self
812 .fonts
813 .iter()
814 .enumerate()
815 .map(|(index, font)| (index, font.id))
816 .collect::<HashMap<_, _>>();
817 self.fonts.retain(|font| current.contains(&font.id));
818 let current_order = self
819 .layout_fonts
820 .iter()
821 .enumerate()
822 .map(|(index, font_id)| (*font_id, index))
823 .collect::<HashMap<_, _>>();
824 self.fonts
825 .sort_by_key(|font| current_order.get(&font.id).copied().unwrap_or(usize::MAX));
826
827 let indices = self
828 .fonts
829 .iter()
830 .enumerate()
831 .map(|(index, font)| (font.id, index))
832 .collect::<HashMap<_, _>>();
833 let old_cache = std::mem::take(&mut self.cache);
834 self.cache = old_cache
835 .into_iter()
836 .filter_map(|(key, old_index)| {
837 let font_id = old_index_ids.get(&old_index)?;
838 Some((key, *indices.get(font_id)?))
839 })
840 .collect();
841 self.coverage_fallbacks.clear();
842 self.coverage_misses.clear();
843 let active_db_ids = self
844 .fonts
845 .iter()
846 .map(|font| font.db_id)
847 .collect::<HashSet<_>>();
848 self.memory_face_data
849 .retain(|db_id, _| active_db_ids.contains(db_id));
850
851 let memo = self
852 .shaping_memo
853 .get_mut()
854 .unwrap_or_else(std::sync::PoisonError::into_inner);
855 memo.entries
856 .retain(|(key, _, _, _)| current.contains(&key.font_id));
857 memo.bytes = memo.entries.iter().map(|(_, _, bytes, _)| bytes).sum();
858 }
859
860 pub fn resolve_font_for_text(
877 &mut self,
878 family: Option<&str>,
879 bold: bool,
880 italic: bool,
881 text: &str,
882 ) -> Result<FontId> {
883 let primary = self.resolve_font(family, bold, italic)?;
884
885 let Some(idx) = self.index_of(primary) else {
886 return Ok(primary);
887 };
888 let missing = self.uncovered(idx, text);
889 if missing.is_empty() {
890 return Ok(primary);
891 }
892
893 match self.font_covering(&missing, bold, italic) {
894 None => Ok(primary),
897 Some(id) => Ok(id),
898 }
899 }
900
901 fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
906 let font = &self.fonts[idx];
907 let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
908 return Vec::new();
909 };
910 let mut seen = HashSet::new();
911 text.chars()
912 .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
913 .filter(|&ch| face.glyph_index(ch).is_none())
914 .filter(|&ch| seen.insert(ch))
915 .collect()
916 }
917
918 fn covers(&self, idx: usize, ch: char) -> bool {
920 let font = &self.fonts[idx];
921 ttf_parser::Face::parse(&font.data, font.face_index)
922 .map(|face| face.glyph_index(ch).is_some())
923 .unwrap_or(false)
924 }
925
926 fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
935 if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
936 return None;
937 }
938
939 let mut best: Option<(usize, usize)> = None; let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
941 let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
942 if covered == 0 {
943 return false;
944 }
945 if best.map(|(n, _)| covered > n).unwrap_or(true) {
946 *best = Some((covered, idx));
947 }
948 covered == missing.len()
949 };
950
951 if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
954 for idx in known {
955 if consider(self, idx, &mut best) {
956 let id = self.fonts[idx].id;
957 self.record_layout_font(id);
958 return Some(id);
959 }
960 }
961 }
962
963 let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
967 .iter()
968 .map(|s| s.to_string())
969 .chain(
970 self.db
971 .faces()
972 .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
973 )
974 .collect();
975
976 for name in candidates {
977 let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
978 continue;
979 };
980 let Some(idx) = self.index_of(id) else {
981 continue;
982 };
983 let complete = consider(self, idx, &mut best);
984 if complete {
985 self.remember_coverage_fallback(bold, italic, idx);
986 return Some(id);
987 }
988 }
989
990 match best {
991 Some((_, idx)) => {
992 self.remember_coverage_fallback(bold, italic, idx);
993 let id = self.fonts[idx].id;
994 self.record_layout_font(id);
995 Some(id)
996 }
997 None => {
998 self.remember_coverage_misses(missing);
999 None
1000 }
1001 }
1002 }
1003
1004 fn index_of(&self, id: FontId) -> Option<usize> {
1006 self.fonts.iter().position(|f| f.id == id)
1007 }
1008
1009 pub fn resolve_font(
1012 &mut self,
1013 family: Option<&str>,
1014 bold: bool,
1015 italic: bool,
1016 ) -> Result<FontId> {
1017 self.resolve_font_inner(family, bold, italic, true)
1018 }
1019
1020 #[doc(hidden)]
1022 pub fn resolve_font_for_metrics(
1023 &mut self,
1024 family: Option<&str>,
1025 bold: bool,
1026 italic: bool,
1027 ) -> Result<FontId> {
1028 self.resolve_font_inner(family, bold, italic, false)
1029 }
1030
1031 fn resolve_font_inner(
1032 &mut self,
1033 family: Option<&str>,
1034 bold: bool,
1035 italic: bool,
1036 record_layout_use: bool,
1037 ) -> Result<FontId> {
1038 let family_name = family.unwrap_or("Arial");
1039
1040 let key = FontKey {
1041 family: family_name.to_string(),
1042 bold,
1043 italic,
1044 };
1045
1046 if let Some(idx) = self.cache.get(&key).copied() {
1047 let id = self.fonts[idx].id;
1048 if record_layout_use {
1049 self.record_layout_font(id);
1050 }
1051 return Ok(id);
1052 }
1053
1054 let requested_key = family_name.to_lowercase();
1055 let style = if italic {
1056 fontdb::Style::Italic
1057 } else {
1058 fontdb::Style::Normal
1059 };
1060 let weight = if bold {
1061 fontdb::Weight::BOLD
1062 } else {
1063 fontdb::Weight::NORMAL
1064 };
1065
1066 let query_family = |family: &str| {
1067 if let Some(ids) = self.caller_families.get(family)
1068 && let Some(id) = best_caller_face(&self.db, ids, weight, style)
1069 {
1070 return Some(id);
1071 }
1072 let query = fontdb::Query {
1073 families: &[fontdb::Family::Name(family)],
1074 weight,
1075 style,
1076 stretch: fontdb::Stretch::Normal,
1077 };
1078 self.db.query(&query)
1079 };
1080
1081 let mut found_id = query_family(family_name);
1082 if found_id.is_none()
1083 && let Some(alias) = self.explicit_alias_map.get(&requested_key)
1084 {
1085 found_id = query_family(alias);
1086 }
1087 if found_id.is_none()
1088 && let Some(ids) = self.caller_aliases.get(&requested_key)
1089 {
1090 found_id = best_caller_face(&self.db, ids, weight, style);
1091 }
1092
1093 let mut fallbacks: Vec<&str> = map_font_name(family_name).to_vec();
1096 for generic in &[
1097 "Carlito",
1098 "Arial",
1099 "Liberation Sans",
1100 "Helvetica",
1101 "DejaVu Sans",
1102 "Noto Sans",
1103 ] {
1104 if !fallbacks.contains(generic) {
1105 fallbacks.push(generic);
1106 }
1107 }
1108
1109 if found_id.is_none() {
1110 for fallback in &fallbacks {
1111 let Some(id) = query_family(fallback) else {
1112 continue;
1113 };
1114 found_id = Some(id);
1115 break;
1116 }
1117 }
1118
1119 if found_id.is_none() {
1121 for generic_family in &[
1122 fontdb::Family::SansSerif,
1123 fontdb::Family::Serif,
1124 fontdb::Family::Monospace,
1125 ] {
1126 let query = fontdb::Query {
1127 families: &[*generic_family],
1128 weight,
1129 style,
1130 stretch: fontdb::Stretch::Normal,
1131 };
1132 if let Some(id) = self.db.query(&query) {
1133 found_id = Some(id);
1134 break;
1135 }
1136 }
1137 }
1138
1139 let db_id = found_id.ok_or_else(|| {
1140 LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
1141 })?;
1142
1143 if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
1147 && let Some(idx) = self
1148 .fonts
1149 .iter()
1150 .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
1151 {
1152 let id = self.fonts[idx].id;
1153 if record_layout_use {
1154 self.record_layout_font(id);
1155 }
1156 return Ok(id);
1157 }
1158
1159 let font_id = FontId(self.next_id);
1160 self.next_id += 1;
1161
1162 let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
1165 .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
1166
1167 let (units_per_em, ascender, descender, line_gap) = {
1168 let face = ttf_parser::Face::parse(&data, face_index)
1169 .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
1170 (
1171 face.units_per_em(),
1172 face.ascender(),
1173 face.descender(),
1174 face.line_gap(),
1175 )
1176 };
1177
1178 if units_per_em == 0 {
1181 return Err(LayoutError::FontParse(format!(
1182 "font '{family_name}' declares zero units per em"
1183 )));
1184 }
1185
1186 let shaper_data = {
1187 let face = harfrust::FontRef::from_index(&data, face_index)
1188 .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
1189 harfrust::ShaperData::new(&face)
1190 };
1191
1192 let actual_family = self
1193 .db
1194 .face(db_id)
1195 .map(|f| {
1196 f.families
1197 .first()
1198 .map(|(name, _)| name.clone())
1199 .unwrap_or_else(|| family_name.to_string())
1200 })
1201 .unwrap_or_else(|| family_name.to_string());
1202
1203 let idx = self.fonts.len();
1204 self.fonts.push(LoadedFont {
1205 db_id,
1206 id: font_id,
1207 family: actual_family,
1208 bold,
1209 italic,
1210 data,
1211 face_index,
1212 units_per_em,
1213 ascender,
1214 descender,
1215 line_gap,
1216 shaper_data,
1217 });
1218 self.remember_font_key(key, idx);
1219 if record_layout_use {
1220 self.record_layout_font(font_id);
1221 }
1222
1223 Ok(font_id)
1224 }
1225
1226 pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
1228 let font = self.get_font(font_id)?;
1229 let scale = size_pt / font.units_per_em as f64;
1230
1231 Ok(FontMetrics {
1232 ascent: font.ascender as f64 * scale,
1233 descent: -(font.descender as f64) * scale, line_gap: font.line_gap as f64 * scale,
1235 units_per_em: font.units_per_em,
1236 })
1237 }
1238
1239 pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
1241 if text.is_empty() {
1244 return Ok(ShapedText {
1245 glyph_ids: Vec::new(),
1246 advances: Vec::new(),
1247 width: 0.0,
1248 });
1249 }
1250
1251 let key = ShapingKey {
1252 font_id,
1253 text: text.to_owned(),
1254 size_bits: size_pt.to_bits(),
1255 };
1256 let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
1257 let mut memo = match self.shaping_memo.lock() {
1258 Ok(memo) => memo,
1259 Err(poisoned) => {
1260 let mut memo = poisoned.into_inner();
1261 memo.clear();
1262 self.shaping_memo.clear_poison();
1263 memo
1264 }
1265 };
1266 if let Some(shaped) = memo
1267 .entries
1268 .iter()
1269 .rev()
1270 .find(|(candidate, _, _, candidate_fingerprint)| {
1271 *candidate_fingerprint == fingerprint && candidate == &key
1272 })
1273 .map(|(_, shaped, _, _)| shaped.clone())
1274 {
1275 #[cfg(test)]
1276 {
1277 memo.hits += 1;
1278 }
1279 return Ok(shaped);
1280 }
1281 #[cfg(test)]
1282 {
1283 memo.misses += 1;
1284 }
1285
1286 let font = self.get_font(font_id)?;
1287
1288 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
1289 .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
1290
1291 let shaper = font.shaper_data.shaper(&face).build();
1292
1293 let mut buffer = harfrust::UnicodeBuffer::new();
1294 buffer.push_str(text);
1295 buffer.guess_segment_properties();
1298
1299 let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
1300 let infos = output.glyph_infos();
1301 let positions = output.glyph_positions();
1302
1303 let upem = font.units_per_em as f64;
1304 let scale = size_pt / upem;
1305
1306 let mut glyph_ids = Vec::with_capacity(infos.len());
1307 let mut advances = Vec::with_capacity(positions.len());
1308 let mut total_width = 0.0;
1309
1310 for (info, pos) in infos.iter().zip(positions.iter()) {
1311 glyph_ids.push(info.glyph_id as u16);
1312 let advance = pos.x_advance as f64 * scale;
1313 advances.push(advance);
1314 total_width += advance;
1315 }
1316
1317 let shaped = ShapedText {
1318 glyph_ids,
1319 advances,
1320 width: total_width,
1321 };
1322 memo.insert(key, shaped.clone());
1323 Ok(shaped)
1324 }
1325
1326 pub fn shape_multilingual_text(
1331 &mut self,
1332 segment: TextSegment,
1333 language: Option<&str>,
1334 base_direction: TextDirection,
1335 no_wrap: bool,
1336 ) -> Result<Vec<MultilingualTextSegment>> {
1337 if segment.text.is_empty() {
1338 return Ok(Vec::new());
1339 }
1340
1341 let paragraph_level = match base_direction {
1342 TextDirection::Auto => None,
1343 TextDirection::LeftToRight => Some(unicode_bidi::Level::ltr()),
1344 TextDirection::RightToLeft => Some(unicode_bidi::Level::rtl()),
1345 };
1346 let bidi = unicode_bidi::BidiInfo::new(&segment.text, paragraph_level);
1347 let levels = bidi.levels.clone();
1348 self.shape_multilingual_with_levels(segment, language, no_wrap, &levels, 0)
1349 }
1350
1351 pub fn shape_multilingual_paragraph(
1353 &mut self,
1354 segments: Vec<(TextSegment, Option<String>)>,
1355 base_direction: TextDirection,
1356 no_wrap: bool,
1357 ) -> Result<Vec<MultilingualTextSegment>> {
1358 let paragraph_text = segments
1359 .iter()
1360 .map(|(segment, _)| segment.text.as_str())
1361 .collect::<String>();
1362 if paragraph_text.is_empty() {
1363 return Ok(Vec::new());
1364 }
1365 let paragraph_level = match base_direction {
1366 TextDirection::Auto => None,
1367 TextDirection::LeftToRight => Some(unicode_bidi::Level::ltr()),
1368 TextDirection::RightToLeft => Some(unicode_bidi::Level::rtl()),
1369 };
1370 let bidi = unicode_bidi::BidiInfo::new(¶graph_text, paragraph_level);
1371 let mut byte_offset = 0usize;
1372 let mut logical_index = 0usize;
1373 let mut shaped = Vec::new();
1374 for (segment, language) in segments {
1375 let byte_end = byte_offset + segment.text.len();
1376 if !segment.text.is_empty() {
1377 let spans = self.shape_multilingual_with_levels(
1378 segment,
1379 language.as_deref(),
1380 no_wrap,
1381 &bidi.levels[byte_offset..byte_end],
1382 logical_index,
1383 )?;
1384 logical_index += spans.len();
1385 shaped.extend(spans);
1386 }
1387 byte_offset = byte_end;
1388 }
1389 Ok(shaped)
1390 }
1391
1392 fn shape_multilingual_with_levels(
1393 &mut self,
1394 segment: TextSegment,
1395 language: Option<&str>,
1396 no_wrap: bool,
1397 levels: &[unicode_bidi::Level],
1398 logical_index_base: usize,
1399 ) -> Result<Vec<MultilingualTextSegment>> {
1400 let grapheme_boundaries = icu_segmenter::GraphemeClusterSegmenter::new()
1401 .segment_str(&segment.text)
1402 .collect::<Vec<_>>();
1403 let break_offsets = if no_wrap {
1404 HashSet::new()
1405 } else {
1406 multilingual_break_opportunities(&segment.text)
1407 };
1408
1409 let mut logical_ranges = Vec::<(usize, usize, TextScript, unicode_bidi::Level)>::new();
1410 let mut start = 0usize;
1411 let mut current_script = TextScript::Common;
1412 let mut current_level = levels[0];
1413 for window in grapheme_boundaries.windows(2) {
1414 let grapheme_start = window[0];
1415 let grapheme_end = window[1];
1416 let script = script_for_grapheme(&segment.text[grapheme_start..grapheme_end]);
1417 let script = if script == TextScript::Common {
1418 current_script
1419 } else {
1420 script
1421 };
1422 let level = levels[grapheme_start];
1423 if grapheme_start > start && (script != current_script || level != current_level) {
1424 logical_ranges.push((start, grapheme_start, current_script, current_level));
1425 start = grapheme_start;
1426 }
1427 current_script = script;
1428 current_level = level;
1429 if break_offsets.contains(&grapheme_end) && grapheme_end < segment.text.len() {
1430 logical_ranges.push((start, grapheme_end, current_script, current_level));
1431 start = grapheme_end;
1432 }
1433 }
1434 if start < segment.text.len() {
1435 logical_ranges.push((start, segment.text.len(), current_script, current_level));
1436 }
1437
1438 let mut font_ranges = Vec::new();
1439 for (start, end, script, level) in logical_ranges {
1440 let boundaries = icu_segmenter::GraphemeClusterSegmenter::new()
1441 .segment_str(&segment.text[start..end])
1442 .map(|offset| start + offset)
1443 .collect::<Vec<_>>();
1444 let mut range_start = start;
1445 let mut range_font = None;
1446 for window in boundaries.windows(2) {
1447 let grapheme_start = window[0];
1448 let grapheme_end = window[1];
1449 let font_id = self.font_for_multilingual_span(
1450 segment.font_id,
1451 &segment.text[grapheme_start..grapheme_end],
1452 segment.bold,
1453 segment.italic,
1454 );
1455 if let Some(current_font) = range_font
1456 && current_font != font_id
1457 {
1458 font_ranges.push((range_start, grapheme_start, script, level, current_font));
1459 range_start = grapheme_start;
1460 }
1461 range_font = Some(font_id);
1462 }
1463 if let Some(font_id) = range_font {
1464 font_ranges.push((range_start, end, script, level, font_id));
1465 }
1466 }
1467
1468 let mut logical = Vec::with_capacity(font_ranges.len());
1469 for (logical_index, (start, end, script, level, font_id)) in
1470 font_ranges.into_iter().enumerate()
1471 {
1472 let text = &segment.text[start..end];
1473 let metrics = self.metrics(font_id, segment.font_size)?;
1474 let direction = if level.is_rtl() {
1475 TextDirection::RightToLeft
1476 } else {
1477 TextDirection::LeftToRight
1478 };
1479 let positioned = self.shape_explicit(
1480 font_id,
1481 text,
1482 segment.font_size,
1483 script,
1484 language,
1485 direction,
1486 )?;
1487 let char_start = segment.text[..start].chars().count() as u32;
1488 let char_end = segment.text[..end].chars().count() as u32;
1489 let source = segment.source.map(|source| SourceSpan {
1490 node: source.node,
1491 char_start: source.char_start + char_start,
1492 char_end: source.char_start + char_end,
1493 });
1494 let mut base = segment.clone();
1495 base.text = text.to_owned();
1496 base.source = source;
1497 base.font_id = font_id;
1498 base.glyph_ids = positioned.glyph_ids;
1499 base.advances = positioned.x_advances.clone();
1500 base.width = positioned.x_advances.iter().sum();
1501 base.ascent = metrics.ascent;
1502 base.descent = metrics.descent;
1503 base.line_gap = metrics.line_gap;
1504 logical.push(MultilingualTextSegment::new(
1505 base,
1506 logical_index_base + logical_index,
1507 language.map(str::to_owned),
1508 script,
1509 direction,
1510 level.number(),
1511 positioned.x_advances,
1512 positioned.y_advances,
1513 positioned.x_offsets,
1514 positioned.y_offsets,
1515 positioned.clusters,
1516 break_offsets.contains(&end),
1517 )?);
1518 }
1519
1520 Ok(logical)
1521 }
1522
1523 fn font_for_multilingual_span(
1524 &mut self,
1525 preferred: FontId,
1526 text: &str,
1527 bold: bool,
1528 italic: bool,
1529 ) -> FontId {
1530 let Some(index) = self.index_of(preferred) else {
1531 return preferred;
1532 };
1533 if self.uncovered(index, text).is_empty() {
1534 return preferred;
1535 }
1536 let required = text
1537 .chars()
1538 .filter(|character| !character.is_whitespace() && !character.is_control())
1539 .collect::<Vec<_>>();
1540 self.font_covering(&required, bold, italic)
1541 .unwrap_or(preferred)
1542 }
1543
1544 fn shape_explicit(
1545 &self,
1546 font_id: FontId,
1547 text: &str,
1548 size_pt: f64,
1549 script: TextScript,
1550 language: Option<&str>,
1551 direction: TextDirection,
1552 ) -> Result<PositionedShape> {
1553 let font = self.get_font(font_id)?;
1554 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
1555 .map_err(|error| LayoutError::Shaping(format!("failed to read font face: {error}")))?;
1556 let shaper = font.shaper_data.shaper(&face).build();
1557 let mut buffer = harfrust::UnicodeBuffer::new();
1558 buffer.push_str(text);
1559 buffer.set_script(harfrust_script(script));
1560 buffer.set_direction(match direction {
1561 TextDirection::RightToLeft => harfrust::Direction::RightToLeft,
1562 TextDirection::Auto | TextDirection::LeftToRight => harfrust::Direction::LeftToRight,
1563 });
1564 if let Some(language) = language.and_then(harfrust::Language::new) {
1565 buffer.set_language(language);
1566 }
1567 let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
1568 let infos = output.glyph_infos();
1569 let positions = output.glyph_positions();
1570 let scale = size_pt / f64::from(font.units_per_em);
1571 let glyph_ids = infos
1572 .iter()
1573 .map(|info| info.glyph_id as u16)
1574 .collect::<Vec<_>>();
1575 let x_advances = positions
1576 .iter()
1577 .map(|pos| f64::from(pos.x_advance) * scale)
1578 .collect();
1579 let y_advances = positions
1580 .iter()
1581 .map(|pos| f64::from(pos.y_advance) * scale)
1582 .collect();
1583 let x_offsets = positions
1584 .iter()
1585 .map(|pos| f64::from(pos.x_offset) * scale)
1586 .collect();
1587 let y_offsets = positions
1588 .iter()
1589 .map(|pos| f64::from(pos.y_offset) * scale)
1590 .collect();
1591 let clusters = glyph_clusters(infos, text);
1592 Ok(PositionedShape {
1593 glyph_ids,
1594 x_advances,
1595 y_advances,
1596 x_offsets,
1597 y_offsets,
1598 clusters,
1599 })
1600 }
1601
1602 pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
1604 let font = self.get_font(font_id)?;
1605 Ok(crate::output::FontData {
1606 id: font.id,
1607 family: font.family.clone(),
1608 data: Arc::clone(&font.data),
1609 face_index: font.face_index,
1610 bold: font.bold,
1611 italic: font.italic,
1612 })
1613 }
1614
1615 pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
1617 self.fonts
1618 .iter()
1619 .map(|f| crate::output::FontData {
1620 id: f.id,
1621 family: f.family.clone(),
1622 data: Arc::clone(&f.data),
1623 face_index: f.face_index,
1624 bold: f.bold,
1625 italic: f.italic,
1626 })
1627 .collect()
1628 }
1629
1630 fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
1631 self.fonts
1632 .iter()
1633 .find(|f| f.id == font_id)
1634 .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
1635 }
1636
1637 fn remember_font_key(&mut self, key: FontKey, index: usize) {
1638 if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
1639 self.cache.insert(key, index);
1640 }
1641 }
1642
1643 fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
1644 let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
1645 if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
1646 known.push(index);
1647 }
1648 }
1649
1650 fn remember_coverage_misses(&mut self, missing: &[char]) {
1651 for &ch in missing {
1652 if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
1653 break;
1654 }
1655 self.coverage_misses.insert(ch);
1656 }
1657 }
1658
1659 fn record_layout_font(&mut self, font_id: FontId) {
1660 if let Some(trace) = self.paragraph_font_trace.as_mut() {
1661 if trace.ids.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
1662 trace.ids.push(font_id);
1663 } else {
1664 trace.overflowed = true;
1665 }
1666 }
1667 if !self.layout_fonts.contains(&font_id) {
1668 self.layout_fonts.push(font_id);
1669 }
1670 }
1671
1672 #[cfg(test)]
1673 fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
1674 let memo = self
1675 .shaping_memo
1676 .lock()
1677 .unwrap_or_else(std::sync::PoisonError::into_inner);
1678 (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
1679 }
1680}
1681
1682struct PositionedShape {
1683 glyph_ids: Vec<u16>,
1684 x_advances: Vec<f64>,
1685 y_advances: Vec<f64>,
1686 x_offsets: Vec<f64>,
1687 y_offsets: Vec<f64>,
1688 clusters: Vec<GlyphCluster>,
1689}
1690
1691fn script_for_grapheme(grapheme: &str) -> TextScript {
1692 grapheme
1693 .chars()
1694 .map(script_for_char)
1695 .find(|script| *script != TextScript::Common)
1696 .unwrap_or(TextScript::Common)
1697}
1698
1699fn script_for_char(character: char) -> TextScript {
1700 match character as u32 {
1701 0x0041..=0x024f | 0x1e00..=0x1eff => TextScript::Latin,
1702 0x0590..=0x05ff => TextScript::Hebrew,
1703 0x0600..=0x06ff | 0x0750..=0x077f | 0x08a0..=0x08ff => TextScript::Arabic,
1704 0x0900..=0x097f | 0xa8e0..=0xa8ff => TextScript::Devanagari,
1705 0x0e00..=0x0e7f => TextScript::Thai,
1706 0x3400..=0x4dbf | 0x4e00..=0x9fff | 0xf900..=0xfaff => TextScript::Han,
1707 _ => TextScript::Common,
1708 }
1709}
1710
1711fn harfrust_script(script: TextScript) -> harfrust::Script {
1712 match script {
1713 TextScript::Latin => harfrust::script::LATIN,
1714 TextScript::Arabic => harfrust::script::ARABIC,
1715 TextScript::Hebrew => harfrust::script::HEBREW,
1716 TextScript::Devanagari => harfrust::script::DEVANAGARI,
1717 TextScript::Thai => harfrust::script::THAI,
1718 TextScript::Han => harfrust::script::HAN,
1719 TextScript::Common => harfrust::script::COMMON,
1720 }
1721}
1722
1723fn multilingual_break_opportunities(text: &str) -> HashSet<usize> {
1724 let mut opportunities = icu_segmenter::WordSegmenter::new_auto(Default::default())
1725 .segment_str(text)
1726 .filter(|offset| *offset > 0 && *offset <= text.len())
1727 .collect::<HashSet<_>>();
1728 for (offset, _) in unicode_linebreak::linebreaks(text) {
1729 if offset > 0 {
1730 opportunities.insert(offset);
1731 }
1732 }
1733 opportunities.retain(|offset| {
1734 let before = text[..*offset].chars().next_back();
1735 let after = text[*offset..].chars().next();
1736 before
1737 .zip(after)
1738 .is_none_or(|(before, after)| crate::line::multilingual_break_allowed(before, after))
1739 });
1740 opportunities
1741}
1742
1743fn glyph_clusters(infos: &[harfrust::GlyphInfo], text: &str) -> Vec<GlyphCluster> {
1744 let mut byte_starts = infos
1745 .iter()
1746 .map(|info| info.cluster as usize)
1747 .collect::<Vec<_>>();
1748 byte_starts.push(text.len());
1749 byte_starts.sort_unstable();
1750 byte_starts.dedup();
1751
1752 let mut clusters = Vec::new();
1753 let mut glyph_start = 0usize;
1754 while glyph_start < infos.len() {
1755 let cluster_byte = infos[glyph_start].cluster as usize;
1756 let mut glyph_end = glyph_start + 1;
1757 while glyph_end < infos.len() && infos[glyph_end].cluster as usize == cluster_byte {
1758 glyph_end += 1;
1759 }
1760 let byte_end = byte_starts
1761 .iter()
1762 .copied()
1763 .find(|candidate| *candidate > cluster_byte)
1764 .unwrap_or(text.len());
1765 clusters.push(GlyphCluster {
1766 glyph_start: glyph_start as u32,
1767 glyph_end: glyph_end as u32,
1768 char_start: text[..cluster_byte].chars().count() as u32,
1769 char_end: text[..byte_end].chars().count() as u32,
1770 });
1771 glyph_start = glyph_end;
1772 }
1773 clusters
1774}
1775
1776fn best_caller_face(
1777 db: &fontdb::Database,
1778 ids: &[fontdb::ID],
1779 weight: fontdb::Weight,
1780 style: fontdb::Style,
1781) -> Option<fontdb::ID> {
1782 const CANDIDATE_FAMILY: &str = "__rdocx_caller_candidate__";
1783
1784 let mut candidates = fontdb::Database::new();
1785 let mut candidate_ids = Vec::with_capacity(ids.len());
1786 for id in ids {
1787 let mut face = db.face(*id)?.clone();
1788 for (family, _) in &mut face.families {
1789 CANDIDATE_FAMILY.clone_into(family);
1790 }
1791 let candidate_id = candidates.push_face_info(face);
1792 candidate_ids.push((candidate_id, *id));
1793 }
1794
1795 let selected = candidates.query(&fontdb::Query {
1796 families: &[fontdb::Family::Name(CANDIDATE_FAMILY)],
1797 weight,
1798 style,
1799 stretch: fontdb::Stretch::Normal,
1800 })?;
1801 candidate_ids
1802 .into_iter()
1803 .find_map(|(candidate, original)| (candidate == selected).then_some(original))
1804}
1805
1806fn bundled_font_database() -> fontdb::Database {
1807 let mut db = fontdb::Database::new();
1808 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
1809 db.load_font_data(data.to_vec());
1810 }
1811 db
1812}
1813
1814fn font_data_for_face(
1815 db: &fontdb::Database,
1816 id: fontdb::ID,
1817 memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
1818) -> Option<(Arc<[u8]>, u32)> {
1819 let face = db.face(id)?;
1820 let face_index = face.index;
1821 match &face.source {
1822 fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
1823 Some(data) => Some((Arc::clone(data), face_index)),
1824 None => {
1825 let data: Arc<[u8]> = Arc::from(data.as_ref().as_ref().to_vec());
1826 memory_face_data.insert(id, Arc::clone(&data));
1827 Some((data, face_index))
1828 }
1829 },
1830 #[cfg(feature = "system-fonts")]
1831 fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
1832 }
1833}
1834
1835#[cfg(feature = "system-fonts")]
1836fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
1837 let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
1838 shared_file_font_bytes_from_cache(cache, path)
1839}
1840
1841#[cfg(feature = "system-fonts")]
1842fn shared_file_font_bytes_from_cache(
1843 cache_lock: &Mutex<FileFontCache>,
1844 path: &Path,
1845) -> Option<Arc<[u8]>> {
1846 let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1847 let mut cache = match cache_lock.lock() {
1848 Ok(cache) => cache,
1849 Err(poisoned) => {
1850 let mut cache = poisoned.into_inner();
1851 cache.clear();
1852 cache_lock.clear_poison();
1853 cache
1854 }
1855 };
1856 if let Some(index) = cache
1857 .entries
1858 .iter()
1859 .position(|(candidate, _, _)| candidate == &identity)
1860 {
1861 let entry = cache.entries.remove(index).expect("cache index exists");
1862 let bytes = Arc::clone(&entry.1);
1863 cache.entries.push_back(entry);
1864 return Some(bytes);
1865 }
1866
1867 let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
1868 cache_file_font_bytes(&mut cache, identity, bytes)
1869}
1870
1871#[cfg(feature = "system-fonts")]
1872fn cache_file_font_bytes(
1873 cache: &mut FileFontCache,
1874 identity: PathBuf,
1875 bytes: Arc<[u8]>,
1876) -> Option<Arc<[u8]>> {
1877 let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
1878 .saturating_add(identity.as_os_str().len())
1879 .saturating_add(bytes.len());
1880 if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
1881 while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
1882 || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
1883 {
1884 let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
1885 break;
1886 };
1887 cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
1888 }
1889 cache.bytes += entry_bytes;
1890 cache
1891 .entries
1892 .push_back((identity, Arc::clone(&bytes), entry_bytes));
1893 }
1894 Some(bytes)
1895}
1896
1897fn map_font_name(name: &str) -> &[&str] {
1904 match name {
1905 "Calibri" => &["Calibri", "Carlito"],
1906 "Calibri Light" => &["Calibri Light", "Carlito"],
1907 "Cambria" => &["Cambria", "Caladea"],
1908 "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
1909 "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
1910 "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
1911 "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
1912 "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
1913 "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
1914 "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
1915 "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
1916 "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
1917 "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
1918 "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
1919 "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
1920 "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
1921 "Impact" => &["Impact", "Liberation Sans", "Arial"],
1922 "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
1923 "Symbol" => &["Symbol", "DejaVu Sans"],
1924 "Wingdings" => &["Wingdings", "Symbol"],
1925 _ => &[],
1926 }
1927}
1928
1929#[cfg(test)]
1930mod tests {
1931 use super::*;
1932 use crate::Color;
1933 use crate::bundled_fonts::bundled_font_data;
1934
1935 fn multilingual_test_segment(
1936 manager: &mut FontManager,
1937 text: &str,
1938 size_pt: f64,
1939 source: Option<SourceSpan>,
1940 ) -> TextSegment {
1941 let font_id = manager
1942 .resolve_font_for_text(None, false, false, text)
1943 .expect("test font resolves");
1944 let metrics = manager
1945 .metrics(font_id, size_pt)
1946 .expect("test font metrics");
1947 let shaped = manager
1948 .shape_text(font_id, text, size_pt)
1949 .expect("test seed shapes");
1950 TextSegment {
1951 text: text.to_owned(),
1952 source,
1953 font_id,
1954 font_size: size_pt,
1955 glyph_ids: shaped.glyph_ids,
1956 advances: shaped.advances,
1957 width: shaped.width,
1958 ascent: metrics.ascent,
1959 descent: metrics.descent,
1960 line_gap: metrics.line_gap,
1961 color: Color::BLACK,
1962 bold: false,
1963 italic: false,
1964 underline: None,
1965 strike: false,
1966 dstrike: false,
1967 highlight: None,
1968 baseline_offset: 0.0,
1969 hyperlink_url: None,
1970 field_kind: None,
1971 note: None,
1972 }
1973 }
1974
1975 #[test]
1976 fn caller_font_labels_resolve_after_exact_embedded_families() {
1977 let caladea = bundled_font_data()
1978 .into_iter()
1979 .find(|(family, _)| *family == "Caladea")
1980 .expect("Caladea is bundled")
1981 .1;
1982 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1983 manager.load_additional_fonts(&[
1984 FontFile {
1985 family: "Document Serif".to_owned(),
1986 data: caladea.to_vec(),
1987 },
1988 FontFile {
1989 family: "Carlito".to_owned(),
1990 data: caladea.to_vec(),
1991 },
1992 ]);
1993
1994 let aliased = manager
1995 .resolve_font(Some("Document Serif"), false, false)
1996 .expect("caller label resolves");
1997 assert_eq!(
1998 manager.fonts[manager.index_of(aliased).unwrap()].family,
1999 "Caladea"
2000 );
2001
2002 let exact = manager
2003 .resolve_font(Some("Carlito"), false, false)
2004 .expect("embedded family resolves exactly");
2005 assert_eq!(
2006 manager.fonts[manager.index_of(exact).unwrap()].family,
2007 "Carlito"
2008 );
2009
2010 manager.set_caller_aliases(&[("Document Serif".to_owned(), "Carlito".to_owned())]);
2011 let explicit = manager
2012 .resolve_font(Some("Document Serif"), false, false)
2013 .expect("explicit alias precedes label-derived alias");
2014 assert_eq!(
2015 manager.fonts[manager.index_of(explicit).unwrap()].family,
2016 "Carlito"
2017 );
2018
2019 manager.load_additional_fonts(&[FontFile {
2020 family: "Caladea".to_owned(),
2021 data: caladea.to_vec(),
2022 }]);
2023 assert!(manager.caller_aliases.is_empty());
2024
2025 manager.set_caller_aliases(&[("Arial".to_owned(), "Caladea".to_owned())]);
2026 let caller_alias = manager
2027 .resolve_font(Some("Arial"), false, false)
2028 .expect("explicit alias resolves before mapped fallback");
2029 assert_eq!(
2030 manager.fonts[manager.index_of(caller_alias).unwrap()].family,
2031 "Caladea"
2032 );
2033 let generic = manager
2034 .resolve_font(Some("Unmapped Document Family"), false, false)
2035 .expect("generic fallback remains available");
2036 assert_eq!(
2037 manager.fonts[manager.index_of(generic).unwrap()].family,
2038 "Carlito"
2039 );
2040 }
2041
2042 #[test]
2043 fn caller_alias_updates_preserve_bytes_and_invalidate_resolution_state() {
2044 let caladea = bundled_font_data()
2045 .into_iter()
2046 .find(|(family, _)| *family == "Caladea")
2047 .expect("Caladea is bundled")
2048 .1;
2049 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2050 manager.load_additional_fonts(&[FontFile {
2051 family: "Caladea".to_owned(),
2052 data: caladea.to_vec(),
2053 }]);
2054 let aliases = vec![
2055 ("Document Serif A".to_owned(), "Caladea".to_owned()),
2056 ("Document Serif B".to_owned(), "Caladea".to_owned()),
2057 ];
2058 assert!(manager.set_caller_aliases(&aliases));
2059
2060 let first = manager
2061 .resolve_font(Some("Document Serif A"), false, false)
2062 .expect("first alias resolves");
2063 let second = manager
2064 .resolve_font(Some("Document Serif B"), false, false)
2065 .expect("second alias resolves");
2066 let first_data = &manager.fonts[manager.index_of(first).unwrap()].data;
2067 let second_data = &manager.fonts[manager.index_of(second).unwrap()].data;
2068 assert!(Arc::ptr_eq(first_data, second_data));
2069 assert!(!manager.set_caller_aliases(&aliases));
2070
2071 assert!(
2072 manager.set_caller_aliases(&[("Document Serif A".to_owned(), "Carlito".to_owned(),)])
2073 );
2074 let changed = manager
2075 .resolve_font(Some("Document Serif A"), false, false)
2076 .expect("changed alias resolves");
2077 assert_eq!(
2078 manager.fonts[manager.index_of(changed).unwrap()].family,
2079 "Carlito"
2080 );
2081 assert!(
2082 manager.index_of(first).is_some(),
2083 "loaded faces are retained"
2084 );
2085 }
2086
2087 #[test]
2088 fn explicit_alias_state_respects_entry_and_retained_byte_ceilings() {
2089 let aliases = (0..CALLER_ALIAS_MAX_ENTRIES + 32)
2090 .map(|index| (format!("Document Serif {index}"), "Caladea".to_owned()))
2091 .collect::<Vec<_>>();
2092 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2093 manager.set_caller_aliases(&aliases);
2094 assert_eq!(
2095 manager.explicit_aliases.as_slice(),
2096 &aliases[..CALLER_ALIAS_MAX_ENTRIES]
2097 );
2098 assert!(manager.explicit_aliases.len() <= CALLER_ALIAS_MAX_ENTRIES);
2099 assert!(manager.explicit_alias_map.len() <= CALLER_ALIAS_MAX_ENTRIES);
2100
2101 let retained_large = ("x".repeat(32_760), String::new());
2102 let byte_limited = vec![
2103 retained_large.clone(),
2104 ("discarded bytes".to_owned(), "Caladea".to_owned()),
2105 ];
2106 manager.set_caller_aliases(&byte_limited);
2107 assert_eq!(manager.explicit_aliases, vec![retained_large]);
2108
2109 let oversized = vec![("x".repeat(40_000), "Caladea".to_owned())];
2110 manager.set_caller_aliases(&oversized);
2111 let retained_bytes = manager
2112 .explicit_aliases
2113 .iter()
2114 .map(|(requested, target)| requested.len() + target.len())
2115 .sum::<usize>()
2116 + manager
2117 .explicit_alias_map
2118 .iter()
2119 .map(|(requested, target)| requested.len() + target.len())
2120 .sum::<usize>();
2121 assert!(retained_bytes <= CALLER_ALIAS_MAX_RETAINED_BYTES);
2122 assert!(manager.explicit_aliases.is_empty());
2123 assert!(manager.explicit_alias_map.is_empty());
2124 }
2125
2126 #[test]
2127 fn label_alias_prefers_caller_bytes_over_bundled_same_family() {
2128 let bundled = bundled_font_data()
2129 .into_iter()
2130 .find(|(family, _)| *family == "Caladea")
2131 .expect("Caladea is bundled")
2132 .1;
2133 let mut caller = bundled.to_vec();
2134 caller.push(0);
2135 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2136 manager.load_additional_fonts(&[FontFile {
2137 family: "Document Serif".to_owned(),
2138 data: caller.clone(),
2139 }]);
2140
2141 let resolved = manager
2142 .resolve_font(Some("Document Serif"), false, false)
2143 .expect("caller label resolves");
2144 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2145 assert_eq!(loaded.family, "Caladea");
2146 assert_eq!(loaded.data.as_ref(), caller.as_slice());
2147 assert_ne!(loaded.data.as_ref(), bundled);
2148 }
2149
2150 #[test]
2151 fn case_only_caller_labels_resolve_to_the_supplied_face() {
2152 let bundled = bundled_font_data()
2153 .into_iter()
2154 .find(|(family, _)| *family == "Caladea")
2155 .expect("Caladea is bundled")
2156 .1;
2157 let mut caller = bundled.to_vec();
2158 caller.push(0);
2159 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2160 manager.load_additional_fonts(&[FontFile {
2161 family: "caladea".to_owned(),
2162 data: caller.clone(),
2163 }]);
2164
2165 let resolved = manager
2166 .resolve_font(Some("caladea"), false, false)
2167 .expect("case-only caller label resolves");
2168 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2169 assert_eq!(loaded.family, "Caladea");
2170 assert_eq!(loaded.data.as_ref(), caller.as_slice());
2171 }
2172
2173 #[test]
2174 fn constructor_label_alias_survives_additional_font_replacement() {
2175 let caladea = bundled_font_data()
2176 .into_iter()
2177 .find(|(family, _)| *family == "Caladea")
2178 .expect("Caladea is bundled")
2179 .1;
2180 let carlito = bundled_font_data()
2181 .into_iter()
2182 .find(|(family, _)| *family == "Carlito")
2183 .expect("Carlito is bundled")
2184 .1;
2185 let mut constructor = caladea.to_vec();
2186 constructor.push(0);
2187 let mut manager =
2188 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2189
2190 manager.load_additional_fonts(&[FontFile {
2191 family: "Additional Sans".to_owned(),
2192 data: carlito.to_vec(),
2193 }]);
2194
2195 let resolved = manager
2196 .resolve_font(Some("Document Serif"), false, false)
2197 .expect("constructor label still resolves");
2198 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2199 assert_eq!(loaded.family, "Caladea");
2200 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2201 }
2202
2203 #[test]
2204 fn constructor_family_priority_survives_additional_font_replacement() {
2205 let caladea = bundled_font_data()
2206 .into_iter()
2207 .find(|(family, _)| *family == "Caladea")
2208 .expect("Caladea is bundled")
2209 .1;
2210 let mut constructor = caladea.to_vec();
2211 constructor.push(0);
2212 let mut replacement = caladea.to_vec();
2213 replacement.extend_from_slice(&[0, 0]);
2214 let mut manager =
2215 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2216
2217 manager.load_additional_fonts(&[FontFile {
2218 family: "Caladea".to_owned(),
2219 data: replacement,
2220 }]);
2221
2222 let resolved = manager
2223 .resolve_font(Some("Caladea"), false, false)
2224 .expect("constructor family still resolves");
2225 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2226 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2227 }
2228
2229 #[test]
2230 fn caller_face_selection_matches_fontdb_css_rules() {
2231 let caladea = bundled_font_data()
2232 .into_iter()
2233 .find(|(family, _)| *family == "Caladea")
2234 .expect("Caladea is bundled")
2235 .1;
2236 let mut source = fontdb::Database::new();
2237 source.load_font_data(caladea.to_vec());
2238 let template = source.faces().next().expect("Caladea has a face").clone();
2239 let mut db = fontdb::Database::new();
2240 let mut add_face = |weight: u16, stretch: fontdb::Stretch, style: fontdb::Style| {
2241 let mut face = template.clone();
2242 face.weight = fontdb::Weight(weight);
2243 face.stretch = stretch;
2244 face.style = style;
2245 db.push_face_info(face)
2246 };
2247
2248 let weight_300 = add_face(300, fontdb::Stretch::Normal, fontdb::Style::Normal);
2249 let weight_500 = add_face(500, fontdb::Stretch::Normal, fontdb::Style::Normal);
2250 let weight_600 = add_face(600, fontdb::Stretch::Normal, fontdb::Style::Normal);
2251 let weight_800 = add_face(800, fontdb::Stretch::Normal, fontdb::Style::Normal);
2252 let expanded = add_face(400, fontdb::Stretch::SemiExpanded, fontdb::Style::Normal);
2253 let condensed = add_face(400, fontdb::Stretch::SemiCondensed, fontdb::Style::Normal);
2254 let normal = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Normal);
2255 let italic = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Italic);
2256
2257 assert_eq!(
2258 best_caller_face(
2259 &db,
2260 &[weight_300, weight_500],
2261 fontdb::Weight::NORMAL,
2262 fontdb::Style::Normal,
2263 ),
2264 Some(weight_500)
2265 );
2266 assert_eq!(
2267 best_caller_face(
2268 &db,
2269 &[weight_600, weight_800],
2270 fontdb::Weight::BOLD,
2271 fontdb::Style::Normal,
2272 ),
2273 Some(weight_800)
2274 );
2275 assert_eq!(
2276 best_caller_face(
2277 &db,
2278 &[expanded, condensed],
2279 fontdb::Weight::NORMAL,
2280 fontdb::Style::Normal,
2281 ),
2282 Some(condensed)
2283 );
2284 assert_eq!(
2285 best_caller_face(
2286 &db,
2287 &[normal, italic],
2288 fontdb::Weight::NORMAL,
2289 fontdb::Style::Oblique,
2290 ),
2291 Some(italic)
2292 );
2293 }
2294
2295 fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
2296 assert_eq!(family.len(), 7);
2297 let mut font = source.to_vec();
2298 let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
2299 let name_offset = (0..table_count)
2300 .find_map(|table| {
2301 let record = 12 + table * 16;
2302 (&font[record..record + 4] == b"name").then(|| {
2303 u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
2304 })
2305 })
2306 .expect("font has name table");
2307 let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
2308 let strings = name_offset
2309 + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
2310 for index in 0..count {
2311 let record = name_offset + 6 + index * 12;
2312 let platform = u16::from_be_bytes([font[record], font[record + 1]]);
2313 let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
2314 let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
2315 let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
2316 if !matches!(name_id, 1 | 16) {
2317 continue;
2318 }
2319 let destination = &mut font[strings + offset..strings + offset + length];
2320 match (platform, length) {
2321 (0 | 3, 14) => {
2322 for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
2323 bytes.copy_from_slice(&(ch as u16).to_be_bytes());
2324 }
2325 }
2326 (1, 7) => destination.copy_from_slice(family.as_bytes()),
2327 _ => {}
2328 }
2329 }
2330 font
2331 }
2332
2333 #[cfg(feature = "system-fonts")]
2334 fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
2335 let header_len = 12 + fonts.len() * 4;
2336 let mut collection = vec![0u8; header_len];
2337 collection[0..4].copy_from_slice(b"ttcf");
2338 collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
2339 collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
2340
2341 for (font_number, font) in fonts.iter().enumerate() {
2342 while !collection.len().is_multiple_of(4) {
2343 collection.push(0);
2344 }
2345 let collection_offset = collection.len();
2346 collection[12 + font_number * 4..16 + font_number * 4]
2347 .copy_from_slice(&(collection_offset as u32).to_be_bytes());
2348
2349 let mut adjusted = font.to_vec();
2350 let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
2351 for table in 0..table_count {
2352 let offset_position = 12 + table * 16 + 8;
2353 let offset = u32::from_be_bytes(
2354 adjusted[offset_position..offset_position + 4]
2355 .try_into()
2356 .expect("table offset"),
2357 );
2358 adjusted[offset_position..offset_position + 4]
2359 .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
2360 }
2361 collection.extend_from_slice(&adjusted);
2362 }
2363 collection
2364 }
2365
2366 #[test]
2367 fn deterministic_font_manager_uses_only_bundled_fonts() {
2368 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2369
2370 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2371 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
2372 }
2373
2374 #[cfg(feature = "system-fonts")]
2375 #[test]
2376 fn normal_font_discovery_initializes_once_per_process() {
2377 let _first = FontManager::new();
2378 let _second = FontManager::new();
2379 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2380
2381 let _deterministic =
2382 FontManager::new_deterministic().expect("bundled font manager should load");
2383 let _caller = FontManager::new_with_fonts(Vec::new());
2384 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2385 }
2386
2387 #[cfg(feature = "system-fonts")]
2388 #[test]
2389 fn file_backed_collection_faces_share_one_byte_buffer() {
2390 let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
2391 let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
2392 let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
2393 let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
2394 std::fs::write(&first_path, &collection).expect("write first temporary collection");
2395 std::fs::write(&second_path, &collection).expect("write second temporary collection");
2396
2397 let mut db = fontdb::Database::new();
2398 db.load_font_file(&first_path).expect("load first TTC");
2399 db.load_font_file(&second_path).expect("load second TTC");
2400 let canonical_first = std::fs::canonicalize(&first_path).unwrap();
2401 let canonical_second = std::fs::canonicalize(&second_path).unwrap();
2402 let first_ids = db
2403 .faces()
2404 .filter_map(|face| match &face.source {
2405 fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
2406 Some(face.id)
2407 }
2408 _ => None,
2409 })
2410 .collect::<Vec<_>>();
2411 let second_id = db
2412 .faces()
2413 .find_map(|face| match &face.source {
2414 fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
2415 Some(face.id)
2416 }
2417 _ => None,
2418 })
2419 .expect("second TTC face");
2420 assert_eq!(first_ids.len(), 2);
2421
2422 let mut memory = HashMap::new();
2423 let (first_face, first_index) =
2424 font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
2425 let (second_face, second_index) =
2426 font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
2427 let (other_file, _) =
2428 font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
2429 assert_ne!(first_index, second_index);
2430 assert!(Arc::ptr_eq(&first_face, &second_face));
2431 assert!(!Arc::ptr_eq(&first_face, &other_file));
2432
2433 std::fs::remove_file(first_path).expect("remove first temporary font");
2434 std::fs::remove_file(second_path).expect("remove second temporary font");
2435 }
2436
2437 #[test]
2438 fn shaping_memo_uses_complete_text_size_and_font_identity() {
2439 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2440 let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2441 let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
2442
2443 let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
2444 let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
2445 assert_eq!(first.glyph_ids, repeat.glyph_ids);
2446 assert_eq!(fm.shaping_memo_counts().0, 1);
2447
2448 fm.shape_text(regular, "different text", 11.0).unwrap();
2449 fm.shape_text(regular, "exact text", 12.0).unwrap();
2450 fm.shape_text(bold, "exact text", 11.0).unwrap();
2451 assert_eq!(fm.shaping_memo_counts().1, 4);
2452
2453 let replacement = FontFile {
2454 family: "Carlito".to_owned(),
2455 data: bundled_font_data()[1].1.to_vec(),
2456 };
2457 fm.load_additional_fonts(&[replacement]);
2458 assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
2459 let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2460 fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
2461 assert_eq!(fm.shaping_memo_counts().1, 1);
2462 }
2463
2464 #[test]
2465 fn arabic_joining_survives_script_and_line_break_boundaries() {
2466 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2467 let segment = multilingual_test_segment(&mut fm, "العربية", 18.0, None);
2468 let shaped = fm
2469 .shape_multilingual_text(segment, Some("ar"), TextDirection::RightToLeft, false)
2470 .unwrap();
2471 assert_eq!(shaped.len(), 1);
2472 assert_eq!(
2473 shaped[0].glyph_ids(),
2474 &[288, 85, 319, 18, 317, 19, 31, 48, 72, 8]
2475 );
2476 assert_eq!(
2477 shaped[0]
2478 .clusters()
2479 .iter()
2480 .map(|cluster| (cluster.glyph_start..cluster.glyph_end, cluster.char_range()))
2481 .collect::<Vec<_>>(),
2482 vec![
2483 (0..2, 6..7),
2484 (2..4, 5..6),
2485 (4..6, 4..5),
2486 (6..7, 3..4),
2487 (7..8, 2..3),
2488 (8..9, 1..2),
2489 (9..10, 0..1),
2490 ]
2491 );
2492 }
2493
2494 #[test]
2495 fn indic_clusters_are_never_split_or_mapped_as_independent_scalars() {
2496 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2497 let segment = multilingual_test_segment(&mut fm, "कि", 18.0, None);
2498 let shaped = fm
2499 .shape_multilingual_text(segment, Some("hi"), TextDirection::LeftToRight, false)
2500 .unwrap();
2501
2502 assert_eq!(shaped[0].clusters()[0].char_range(), 0..2);
2503 assert_eq!(shaped[0].x_offsets().len(), shaped[0].glyph_ids().len());
2504 assert_eq!(shaped[0].y_offsets().len(), shaped[0].glyph_ids().len());
2505 }
2506
2507 #[test]
2508 fn same_script_coverage_changes_split_only_between_graphemes() {
2509 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2510 let font_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2511 let metrics = fm.metrics(font_id, 18.0).unwrap();
2512 let seed = fm.shape_text(font_id, "A☀∙", 18.0).unwrap();
2513 let shaped = fm
2514 .shape_multilingual_text(
2515 TextSegment {
2516 text: "A☀∙".to_owned(),
2517 source: None,
2518 font_id,
2519 font_size: 18.0,
2520 glyph_ids: seed.glyph_ids,
2521 advances: seed.advances,
2522 width: seed.width,
2523 ascent: metrics.ascent,
2524 descent: metrics.descent,
2525 line_gap: metrics.line_gap,
2526 color: Color::BLACK,
2527 bold: false,
2528 italic: false,
2529 underline: None,
2530 strike: false,
2531 dstrike: false,
2532 highlight: None,
2533 baseline_offset: 0.0,
2534 hyperlink_url: None,
2535 field_kind: None,
2536 note: None,
2537 },
2538 None,
2539 TextDirection::LeftToRight,
2540 false,
2541 )
2542 .unwrap();
2543
2544 assert_eq!(
2545 shaped.iter().map(|span| span.text()).collect::<Vec<_>>(),
2546 ["A", "☀", "∙"]
2547 );
2548 assert!(shaped.iter().all(|span| span.script() == TextScript::Latin));
2549 assert_ne!(shaped[1].font_id(), shaped[2].font_id());
2550 assert!(shaped.iter().all(|span| {
2551 let index = fm.index_of(span.font_id()).unwrap();
2552 fm.uncovered(index, span.text()).is_empty()
2553 }));
2554 }
2555
2556 #[test]
2557 fn multilingual_constructor_rejects_invalid_levels_and_cluster_maps() {
2558 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2559 let segment = multilingual_test_segment(&mut fm, "ab", 18.0, None);
2560 let valid = fm
2561 .shape_multilingual_text(segment, None, TextDirection::LeftToRight, true)
2562 .unwrap()
2563 .remove(0);
2564 let rebuild = |bidi_level, clusters| {
2565 MultilingualTextSegment::new(
2566 valid.base().clone(),
2567 valid.logical_index(),
2568 valid.language().map(str::to_owned),
2569 valid.script(),
2570 valid.direction(),
2571 bidi_level,
2572 valid.x_advances().to_vec(),
2573 valid.y_advances().to_vec(),
2574 valid.x_offsets().to_vec(),
2575 valid.y_offsets().to_vec(),
2576 clusters,
2577 valid.break_after(),
2578 )
2579 };
2580
2581 assert!(rebuild(255, valid.clusters().to_vec()).is_err());
2582 let mut out_of_bounds = valid.clusters().to_vec();
2583 out_of_bounds.last_mut().unwrap().char_end = 3;
2584 assert!(rebuild(valid.bidi_level(), out_of_bounds).is_err());
2585 let mut glyph_gap = valid.clusters().to_vec();
2586 glyph_gap[0].glyph_start = 1;
2587 assert!(rebuild(valid.bidi_level(), glyph_gap).is_err());
2588 }
2589
2590 #[test]
2591 fn thai_words_offer_approved_breaks_without_losing_source_text() {
2592 let text = "ภาษาไทยยินดีต้อนรับ";
2593 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2594 let source = SourceSpan {
2595 node: crate::SourceNodeId::new(3).unwrap(),
2596 char_start: 10,
2597 char_end: 29,
2598 };
2599 let segment = multilingual_test_segment(&mut fm, text, 18.0, Some(source));
2600 let shaped = fm
2601 .shape_multilingual_text(segment, Some("th"), TextDirection::LeftToRight, false)
2602 .unwrap();
2603 assert_eq!(
2604 shaped
2605 .iter()
2606 .map(|span| {
2607 let source = span.base().source.unwrap();
2608 (
2609 span.text(),
2610 span.break_after(),
2611 source.char_start..source.char_end,
2612 )
2613 })
2614 .collect::<Vec<_>>(),
2615 vec![
2616 ("ภาษา", true, 10..14),
2617 ("ไทยยิน", true, 14..20),
2618 ("ดี", true, 20..22),
2619 ("ต้อน", true, 22..26),
2620 ("รับ", true, 26..29),
2621 ]
2622 );
2623 assert_eq!(
2624 shaped.iter().map(|span| span.text()).collect::<String>(),
2625 text
2626 );
2627 }
2628
2629 #[test]
2630 fn shaping_memo_hits_preserve_fifo_order() {
2631 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2632 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2633 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2634 fm.shape_text(font, "newest exact shape", 11.0).unwrap();
2635 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2636
2637 let memo = fm.shaping_memo.lock().unwrap();
2638 assert_eq!(memo.hits, 1);
2639 assert_eq!(memo.entries.front().unwrap().0.text, "oldest exact shape");
2640 assert_eq!(memo.entries.back().unwrap().0.text, "newest exact shape");
2641 }
2642
2643 #[test]
2644 fn shaping_memo_fingerprint_collision_requires_exact_key_equality() {
2645 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2646 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2647 fm.shape_text(font, "first collision candidate", 11.0)
2648 .unwrap();
2649 let forced = shaping_fingerprint(font, "second collision candidate", 11.0f64.to_bits());
2650 fm.shaping_memo.lock().unwrap().entries[0].3 = forced;
2651
2652 fm.shape_text(font, "second collision candidate", 11.0)
2653 .unwrap();
2654
2655 let (hits, misses, entries, _) = fm.shaping_memo_counts();
2656 assert_eq!((hits, misses, entries), (0, 2, 2));
2657 }
2658
2659 #[test]
2660 fn shaping_memo_is_bounded_and_recovers_from_poison() {
2661 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2662 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2663 for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
2664 fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
2665 .unwrap();
2666 }
2667 let (_, _, entries, bytes) = fm.shaping_memo_counts();
2668 assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
2669 assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
2670
2671 let fm = Arc::new(fm);
2672 let poison = Arc::clone(&fm);
2673 assert!(
2674 std::thread::spawn(move || {
2675 let _guard = poison.shaping_memo.lock().unwrap();
2676 panic!("poison shaping cache for recovery coverage");
2677 })
2678 .join()
2679 .is_err()
2680 );
2681 let first = fm.shape_text(font, "after poison", 11.0).unwrap();
2682 let second = fm.shape_text(font, "after poison", 11.0).unwrap();
2683 assert_eq!(first.glyph_ids, second.glyph_ids);
2684 let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
2685 assert_eq!((hits, misses, entries), (1, 1, 1));
2686 assert!(bytes > 0);
2687 }
2688
2689 #[test]
2690 fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
2691 let mut memo = ShapingMemo::new();
2692 for suffix in ['a', 'b'] {
2693 memo.insert(
2694 ShapingKey {
2695 font_id: FontId(0),
2696 text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
2697 size_bits: 11.0f64.to_bits(),
2698 },
2699 ShapedText {
2700 glyph_ids: Vec::new(),
2701 advances: Vec::new(),
2702 width: 0.0,
2703 },
2704 );
2705 }
2706 assert_eq!(memo.entries.len(), 1);
2707 assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
2708 }
2709
2710 #[test]
2711 fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
2712 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
2713 for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
2714 fm.remember_coverage_fallback(false, false, 0);
2715 }
2716 assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
2717
2718 let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
2719 .filter_map(|value| char::from_u32(0x10_000 + value as u32))
2720 .collect::<Vec<_>>();
2721 fm.remember_coverage_misses(&misses);
2722 assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
2723
2724 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2725 fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
2726 .expect("bounded fallback resolves");
2727 }
2728 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2729 assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
2730 }
2731
2732 #[test]
2733 fn active_document_may_resolve_more_than_256_distinct_faces() {
2734 let source = bundled_font_data()[4].1;
2735 let mut db = fontdb::Database::new();
2736 for index in 0..257 {
2737 db.load_font_data(font_with_family(source, &format!("F{index:06}")));
2738 }
2739 let mut fm = FontManager::from_base_database(db);
2740 fm.begin_layout();
2741 let mut ids = HashSet::new();
2742 for index in 0..257 {
2743 let family = format!("F{index:06}");
2744 let id = fm
2745 .resolve_font(Some(&family), false, false)
2746 .expect("distinct active face resolves");
2747 assert_eq!(fm.font_data(id).unwrap().family, family);
2748 ids.insert(id);
2749 }
2750 assert_eq!(ids.len(), 257);
2751 fm.retain_current_fonts();
2752 assert_eq!(fm.fonts.len(), 257);
2753 }
2754
2755 #[test]
2756 fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
2757 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
2758 fm.begin_layout();
2759 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
2760 fm.resolve_font(Some("Carlito"), false, false).unwrap();
2761 }
2762 assert!(fm.paragraph_font_trace.is_none());
2763
2764 fm.begin_paragraph_font_trace();
2765 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
2766 fm.resolve_font(Some("Carlito"), false, false).unwrap();
2767 }
2768 assert!(fm.finish_paragraph_font_trace().is_none());
2769
2770 fm.begin_layout();
2771 assert_eq!(fm.layout_fonts.capacity(), 0);
2772 fm.begin_paragraph_font_trace();
2773 fm.resolve_font(Some("Carlito"), false, false).unwrap();
2774 let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
2775 assert_eq!(trace.len(), 1);
2776 assert_eq!(trace.capacity(), trace.len());
2777 }
2778
2779 #[cfg(feature = "system-fonts")]
2780 #[test]
2781 fn file_byte_cache_is_bounded_and_recovers_from_poison() {
2782 let cache = Arc::new(Mutex::new(FileFontCache::new()));
2783 {
2784 let mut cache = cache
2785 .lock()
2786 .unwrap_or_else(std::sync::PoisonError::into_inner);
2787 cache.clear();
2788 let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
2789 let returned = cache_file_font_bytes(
2790 &mut cache,
2791 PathBuf::from("oversized-font.ttc"),
2792 Arc::clone(&oversized),
2793 )
2794 .expect("oversized bytes are returned uncached");
2795 assert!(Arc::ptr_eq(&oversized, &returned));
2796 assert!(cache.entries.is_empty());
2797 assert_eq!(cache.bytes, 0);
2798 }
2799
2800 let poison = Arc::clone(&cache);
2801 assert!(
2802 std::thread::spawn(move || {
2803 let cache = poison;
2804 let _guard = cache.lock().unwrap();
2805 panic!("poison file byte cache for recovery coverage");
2806 })
2807 .join()
2808 .is_err()
2809 );
2810
2811 let path = std::env::temp_dir().join(format!(
2812 "rdocx-font-cache-poison-{}-{:?}.ttf",
2813 std::process::id(),
2814 std::thread::current().id()
2815 ));
2816 std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
2817 let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
2818 let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
2819 assert!(Arc::ptr_eq(&first, &second));
2820 std::fs::remove_file(path).expect("remove recovery font");
2821 }
2822
2823 #[cfg(not(feature = "system-fonts"))]
2824 #[test]
2825 fn no_default_features_omits_system_font_discovery() {
2826 let fm = FontManager::new();
2827 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2828 }
2829
2830 #[test]
2831 fn font_manager_with_no_fonts_returns_an_error() {
2832 let mut fm = FontManager::new_with_fonts(Vec::new());
2833 assert!(matches!(
2834 fm.resolve_font(None, false, false),
2835 Err(LayoutError::FontNotFound(_))
2836 ));
2837 }
2838
2839 #[test]
2840 fn load_system_font() {
2841 let mut fm = FontManager::new();
2842 let result = fm.resolve_font(None, false, false);
2844 if let Ok(id) = result {
2846 assert_eq!(id.0, 0);
2847 }
2848 }
2849
2850 #[test]
2851 fn font_metrics_positive() {
2852 let mut fm = FontManager::new();
2853 if let Ok(id) = fm.resolve_font(None, false, false) {
2854 let metrics = fm.metrics(id, 12.0).unwrap();
2855 assert!(metrics.ascent > 0.0);
2856 assert!(metrics.descent > 0.0);
2857 assert!(metrics.units_per_em > 0);
2858 }
2859 }
2860
2861 #[test]
2862 fn shape_hello_world() {
2863 let mut fm = FontManager::new();
2864 if let Ok(id) = fm.resolve_font(None, false, false) {
2865 let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
2866 assert!(!shaped.glyph_ids.is_empty());
2867 assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
2868 assert!(shaped.width > 0.0);
2869 }
2870 }
2871
2872 #[test]
2873 fn font_caching() {
2874 let mut fm = FontManager::new();
2875 if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
2876 let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
2877 assert_eq!(id1, id2);
2878 }
2879 }
2880
2881 #[test]
2882 fn font_resolution_alias_cache_is_bounded() {
2883 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2884 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2885 fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
2886 .expect("fallback font resolves");
2887 }
2888 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2889 assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2890 }
2891
2892 #[test]
2893 fn bold_italic_variants() {
2894 let mut fm = FontManager::new();
2895 let regular = fm.resolve_font(None, false, false);
2896 let bold = fm.resolve_font(None, true, false);
2897 if let (Ok(r), Ok(b)) = (regular, bold) {
2898 assert_ne!(r, b);
2900 }
2901 }
2902
2903 #[test]
2906 fn latin_text_resolves_the_same_as_by_name() {
2907 let mut fm = FontManager::new();
2908 let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
2909 return;
2910 };
2911 let for_text = fm
2912 .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
2913 .unwrap();
2914 assert_eq!(by_name, for_text);
2915 }
2916
2917 #[test]
2923 fn text_no_font_can_draw_keeps_the_requested_font() {
2924 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2925 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2926 let resolved = fm
2927 .resolve_font_for_text(Some("Carlito"), false, false, "🀄")
2928 .unwrap();
2929 assert_eq!(
2930 primary, resolved,
2931 "with no covering font available the original must be kept"
2932 );
2933 }
2934
2935 #[test]
2937 fn whitespace_does_not_trigger_a_fallback() {
2938 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2939 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2940 let idx = fm.index_of(by_name).unwrap();
2941 assert!(
2943 fm.uncovered(idx, "a\u{00a0}b\tc")
2944 .iter()
2945 .all(|c| *c != '\t'),
2946 "control and whitespace characters must be ignored"
2947 );
2948 }
2949
2950 #[test]
2956 fn cjk_text_moves_off_a_latin_font_when_possible() {
2957 let mut fm = FontManager::new();
2958 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
2959 return;
2960 };
2961 let Some(idx) = fm.index_of(latin) else {
2962 return;
2963 };
2964 if fm.uncovered(idx, "这是中文").is_empty() {
2965 return; }
2967 let resolved = fm
2968 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
2969 .unwrap();
2970 if resolved == latin {
2971 return; }
2973 let new_idx = fm.index_of(resolved).unwrap();
2974 assert!(
2975 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
2976 "the replacement must cover more of the text than the original"
2977 );
2978 }
2979}