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