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 note: None,
2037 }
2038 }
2039
2040 #[test]
2041 fn caller_font_labels_resolve_after_exact_embedded_families() {
2042 let caladea = bundled_font_data()
2043 .into_iter()
2044 .find(|(family, _)| *family == "Caladea")
2045 .expect("Caladea is bundled")
2046 .1;
2047 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2048 manager.load_additional_fonts(&[
2049 FontFile {
2050 family: "Document Serif".to_owned(),
2051 data: caladea.to_vec(),
2052 },
2053 FontFile {
2054 family: "Carlito".to_owned(),
2055 data: caladea.to_vec(),
2056 },
2057 ]);
2058
2059 let aliased = manager
2060 .resolve_font(Some("Document Serif"), false, false)
2061 .expect("caller label resolves");
2062 assert_eq!(
2063 manager.fonts[manager.index_of(aliased).unwrap()].family,
2064 "Caladea"
2065 );
2066
2067 let exact = manager
2068 .resolve_font(Some("Carlito"), false, false)
2069 .expect("embedded family resolves exactly");
2070 assert_eq!(
2071 manager.fonts[manager.index_of(exact).unwrap()].family,
2072 "Carlito"
2073 );
2074
2075 manager.set_caller_aliases(&[("Document Serif".to_owned(), "Carlito".to_owned())]);
2076 let explicit = manager
2077 .resolve_font(Some("Document Serif"), false, false)
2078 .expect("explicit alias precedes label-derived alias");
2079 assert_eq!(
2080 manager.fonts[manager.index_of(explicit).unwrap()].family,
2081 "Carlito"
2082 );
2083
2084 manager.load_additional_fonts(&[FontFile {
2085 family: "Caladea".to_owned(),
2086 data: caladea.to_vec(),
2087 }]);
2088 assert!(manager.caller_aliases.is_empty());
2089
2090 manager.set_caller_aliases(&[("Arial".to_owned(), "Caladea".to_owned())]);
2091 let caller_alias = manager
2092 .resolve_font(Some("Arial"), false, false)
2093 .expect("explicit alias resolves before mapped fallback");
2094 assert_eq!(
2095 manager.fonts[manager.index_of(caller_alias).unwrap()].family,
2096 "Caladea"
2097 );
2098 let generic = manager
2099 .resolve_font(Some("Unmapped Document Family"), false, false)
2100 .expect("generic fallback remains available");
2101 assert_eq!(
2102 manager.fonts[manager.index_of(generic).unwrap()].family,
2103 "Carlito"
2104 );
2105 }
2106
2107 #[test]
2108 fn caller_alias_updates_preserve_bytes_and_invalidate_resolution_state() {
2109 let caladea = bundled_font_data()
2110 .into_iter()
2111 .find(|(family, _)| *family == "Caladea")
2112 .expect("Caladea is bundled")
2113 .1;
2114 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2115 manager.load_additional_fonts(&[FontFile {
2116 family: "Caladea".to_owned(),
2117 data: caladea.to_vec(),
2118 }]);
2119 let aliases = vec![
2120 ("Document Serif A".to_owned(), "Caladea".to_owned()),
2121 ("Document Serif B".to_owned(), "Caladea".to_owned()),
2122 ];
2123 assert!(manager.set_caller_aliases(&aliases));
2124
2125 let first = manager
2126 .resolve_font(Some("Document Serif A"), false, false)
2127 .expect("first alias resolves");
2128 let second = manager
2129 .resolve_font(Some("Document Serif B"), false, false)
2130 .expect("second alias resolves");
2131 let first_data = &manager.fonts[manager.index_of(first).unwrap()].data;
2132 let second_data = &manager.fonts[manager.index_of(second).unwrap()].data;
2133 assert!(Arc::ptr_eq(first_data, second_data));
2134 assert!(!manager.set_caller_aliases(&aliases));
2135
2136 assert!(
2137 manager.set_caller_aliases(&[("Document Serif A".to_owned(), "Carlito".to_owned(),)])
2138 );
2139 let changed = manager
2140 .resolve_font(Some("Document Serif A"), false, false)
2141 .expect("changed alias resolves");
2142 assert_eq!(
2143 manager.fonts[manager.index_of(changed).unwrap()].family,
2144 "Carlito"
2145 );
2146 assert!(
2147 manager.index_of(first).is_some(),
2148 "loaded faces are retained"
2149 );
2150 }
2151
2152 #[test]
2153 fn explicit_alias_state_respects_entry_and_retained_byte_ceilings() {
2154 let aliases = (0..CALLER_ALIAS_MAX_ENTRIES + 32)
2155 .map(|index| (format!("Document Serif {index}"), "Caladea".to_owned()))
2156 .collect::<Vec<_>>();
2157 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2158 manager.set_caller_aliases(&aliases);
2159 assert_eq!(
2160 manager.explicit_aliases.as_slice(),
2161 &aliases[..CALLER_ALIAS_MAX_ENTRIES]
2162 );
2163 assert!(manager.explicit_aliases.len() <= CALLER_ALIAS_MAX_ENTRIES);
2164 assert!(manager.explicit_alias_map.len() <= CALLER_ALIAS_MAX_ENTRIES);
2165
2166 let retained_large = ("x".repeat(32_760), String::new());
2167 let byte_limited = vec![
2168 retained_large.clone(),
2169 ("discarded bytes".to_owned(), "Caladea".to_owned()),
2170 ];
2171 manager.set_caller_aliases(&byte_limited);
2172 assert_eq!(manager.explicit_aliases, vec![retained_large]);
2173
2174 let oversized = vec![("x".repeat(40_000), "Caladea".to_owned())];
2175 manager.set_caller_aliases(&oversized);
2176 let retained_bytes = manager
2177 .explicit_aliases
2178 .iter()
2179 .map(|(requested, target)| requested.len() + target.len())
2180 .sum::<usize>()
2181 + manager
2182 .explicit_alias_map
2183 .iter()
2184 .map(|(requested, target)| requested.len() + target.len())
2185 .sum::<usize>();
2186 assert!(retained_bytes <= CALLER_ALIAS_MAX_RETAINED_BYTES);
2187 assert!(manager.explicit_aliases.is_empty());
2188 assert!(manager.explicit_alias_map.is_empty());
2189 }
2190
2191 #[test]
2192 fn label_alias_prefers_caller_bytes_over_bundled_same_family() {
2193 let bundled = bundled_font_data()
2194 .into_iter()
2195 .find(|(family, _)| *family == "Caladea")
2196 .expect("Caladea is bundled")
2197 .1;
2198 let mut caller = bundled.to_vec();
2199 caller.push(0);
2200 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2201 manager.load_additional_fonts(&[FontFile {
2202 family: "Document Serif".to_owned(),
2203 data: caller.clone(),
2204 }]);
2205
2206 let resolved = manager
2207 .resolve_font(Some("Document Serif"), false, false)
2208 .expect("caller label resolves");
2209 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2210 assert_eq!(loaded.family, "Caladea");
2211 assert_eq!(loaded.data.as_ref(), caller.as_slice());
2212 assert_ne!(loaded.data.as_ref(), bundled);
2213 }
2214
2215 #[test]
2216 fn case_only_caller_labels_resolve_to_the_supplied_face() {
2217 let bundled = bundled_font_data()
2218 .into_iter()
2219 .find(|(family, _)| *family == "Caladea")
2220 .expect("Caladea is bundled")
2221 .1;
2222 let mut caller = bundled.to_vec();
2223 caller.push(0);
2224 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2225 manager.load_additional_fonts(&[FontFile {
2226 family: "caladea".to_owned(),
2227 data: caller.clone(),
2228 }]);
2229
2230 let resolved = manager
2231 .resolve_font(Some("caladea"), false, false)
2232 .expect("case-only caller label resolves");
2233 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2234 assert_eq!(loaded.family, "Caladea");
2235 assert_eq!(loaded.data.as_ref(), caller.as_slice());
2236 }
2237
2238 #[test]
2239 fn constructor_label_alias_survives_additional_font_replacement() {
2240 let caladea = bundled_font_data()
2241 .into_iter()
2242 .find(|(family, _)| *family == "Caladea")
2243 .expect("Caladea is bundled")
2244 .1;
2245 let carlito = bundled_font_data()
2246 .into_iter()
2247 .find(|(family, _)| *family == "Carlito")
2248 .expect("Carlito is bundled")
2249 .1;
2250 let mut constructor = caladea.to_vec();
2251 constructor.push(0);
2252 let mut manager =
2253 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2254
2255 manager.load_additional_fonts(&[FontFile {
2256 family: "Additional Sans".to_owned(),
2257 data: carlito.to_vec(),
2258 }]);
2259
2260 let resolved = manager
2261 .resolve_font(Some("Document Serif"), false, false)
2262 .expect("constructor label still resolves");
2263 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2264 assert_eq!(loaded.family, "Caladea");
2265 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2266 }
2267
2268 #[test]
2269 fn constructor_family_priority_survives_additional_font_replacement() {
2270 let caladea = bundled_font_data()
2271 .into_iter()
2272 .find(|(family, _)| *family == "Caladea")
2273 .expect("Caladea is bundled")
2274 .1;
2275 let mut constructor = caladea.to_vec();
2276 constructor.push(0);
2277 let mut replacement = caladea.to_vec();
2278 replacement.extend_from_slice(&[0, 0]);
2279 let mut manager =
2280 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2281
2282 manager.load_additional_fonts(&[FontFile {
2283 family: "Caladea".to_owned(),
2284 data: replacement,
2285 }]);
2286
2287 let resolved = manager
2288 .resolve_font(Some("Caladea"), false, false)
2289 .expect("constructor family still resolves");
2290 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2291 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2292 }
2293
2294 #[test]
2295 fn caller_face_selection_matches_fontdb_css_rules() {
2296 let caladea = bundled_font_data()
2297 .into_iter()
2298 .find(|(family, _)| *family == "Caladea")
2299 .expect("Caladea is bundled")
2300 .1;
2301 let mut source = fontdb::Database::new();
2302 source.load_font_data(caladea.to_vec());
2303 let template = source.faces().next().expect("Caladea has a face").clone();
2304 let mut db = fontdb::Database::new();
2305 let mut add_face = |weight: u16, stretch: fontdb::Stretch, style: fontdb::Style| {
2306 let mut face = template.clone();
2307 face.weight = fontdb::Weight(weight);
2308 face.stretch = stretch;
2309 face.style = style;
2310 db.push_face_info(face)
2311 };
2312
2313 let weight_300 = add_face(300, fontdb::Stretch::Normal, fontdb::Style::Normal);
2314 let weight_500 = add_face(500, fontdb::Stretch::Normal, fontdb::Style::Normal);
2315 let weight_600 = add_face(600, fontdb::Stretch::Normal, fontdb::Style::Normal);
2316 let weight_800 = add_face(800, fontdb::Stretch::Normal, fontdb::Style::Normal);
2317 let expanded = add_face(400, fontdb::Stretch::SemiExpanded, fontdb::Style::Normal);
2318 let condensed = add_face(400, fontdb::Stretch::SemiCondensed, fontdb::Style::Normal);
2319 let normal = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Normal);
2320 let italic = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Italic);
2321
2322 assert_eq!(
2323 best_caller_face(
2324 &db,
2325 &[weight_300, weight_500],
2326 fontdb::Weight::NORMAL,
2327 fontdb::Style::Normal,
2328 ),
2329 Some(weight_500)
2330 );
2331 assert_eq!(
2332 best_caller_face(
2333 &db,
2334 &[weight_600, weight_800],
2335 fontdb::Weight::BOLD,
2336 fontdb::Style::Normal,
2337 ),
2338 Some(weight_800)
2339 );
2340 assert_eq!(
2341 best_caller_face(
2342 &db,
2343 &[expanded, condensed],
2344 fontdb::Weight::NORMAL,
2345 fontdb::Style::Normal,
2346 ),
2347 Some(condensed)
2348 );
2349 assert_eq!(
2350 best_caller_face(
2351 &db,
2352 &[normal, italic],
2353 fontdb::Weight::NORMAL,
2354 fontdb::Style::Oblique,
2355 ),
2356 Some(italic)
2357 );
2358 }
2359
2360 fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
2361 assert_eq!(family.len(), 7);
2362 let mut font = source.to_vec();
2363 let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
2364 let name_offset = (0..table_count)
2365 .find_map(|table| {
2366 let record = 12 + table * 16;
2367 (&font[record..record + 4] == b"name").then(|| {
2368 u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
2369 })
2370 })
2371 .expect("font has name table");
2372 let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
2373 let strings = name_offset
2374 + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
2375 for index in 0..count {
2376 let record = name_offset + 6 + index * 12;
2377 let platform = u16::from_be_bytes([font[record], font[record + 1]]);
2378 let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
2379 let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
2380 let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
2381 if !matches!(name_id, 1 | 16) {
2382 continue;
2383 }
2384 let destination = &mut font[strings + offset..strings + offset + length];
2385 match (platform, length) {
2386 (0 | 3, 14) => {
2387 for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
2388 bytes.copy_from_slice(&(ch as u16).to_be_bytes());
2389 }
2390 }
2391 (1, 7) => destination.copy_from_slice(family.as_bytes()),
2392 _ => {}
2393 }
2394 }
2395 font
2396 }
2397
2398 #[cfg(feature = "system-fonts")]
2399 fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
2400 let header_len = 12 + fonts.len() * 4;
2401 let mut collection = vec![0u8; header_len];
2402 collection[0..4].copy_from_slice(b"ttcf");
2403 collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
2404 collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
2405
2406 for (font_number, font) in fonts.iter().enumerate() {
2407 while !collection.len().is_multiple_of(4) {
2408 collection.push(0);
2409 }
2410 let collection_offset = collection.len();
2411 collection[12 + font_number * 4..16 + font_number * 4]
2412 .copy_from_slice(&(collection_offset as u32).to_be_bytes());
2413
2414 let mut adjusted = font.to_vec();
2415 let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
2416 for table in 0..table_count {
2417 let offset_position = 12 + table * 16 + 8;
2418 let offset = u32::from_be_bytes(
2419 adjusted[offset_position..offset_position + 4]
2420 .try_into()
2421 .expect("table offset"),
2422 );
2423 adjusted[offset_position..offset_position + 4]
2424 .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
2425 }
2426 collection.extend_from_slice(&adjusted);
2427 }
2428 collection
2429 }
2430
2431 #[test]
2432 fn deterministic_font_manager_uses_only_bundled_fonts() {
2433 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2434
2435 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2436 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
2437 }
2438
2439 #[cfg(feature = "system-fonts")]
2440 #[test]
2441 fn normal_font_discovery_initializes_once_per_process() {
2442 let _first = FontManager::new();
2443 let _second = FontManager::new();
2444 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2445
2446 let _deterministic =
2447 FontManager::new_deterministic().expect("bundled font manager should load");
2448 let _caller = FontManager::new_with_fonts(Vec::new());
2449 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2450 }
2451
2452 #[cfg(feature = "system-fonts")]
2453 #[test]
2454 fn file_backed_collection_faces_share_one_byte_buffer() {
2455 let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
2456 let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
2457 let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
2458 let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
2459 std::fs::write(&first_path, &collection).expect("write first temporary collection");
2460 std::fs::write(&second_path, &collection).expect("write second temporary collection");
2461
2462 let mut db = fontdb::Database::new();
2463 db.load_font_file(&first_path).expect("load first TTC");
2464 db.load_font_file(&second_path).expect("load second TTC");
2465 let canonical_first = std::fs::canonicalize(&first_path).unwrap();
2466 let canonical_second = std::fs::canonicalize(&second_path).unwrap();
2467 let first_ids = db
2468 .faces()
2469 .filter_map(|face| match &face.source {
2470 fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
2471 Some(face.id)
2472 }
2473 _ => None,
2474 })
2475 .collect::<Vec<_>>();
2476 let second_id = db
2477 .faces()
2478 .find_map(|face| match &face.source {
2479 fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
2480 Some(face.id)
2481 }
2482 _ => None,
2483 })
2484 .expect("second TTC face");
2485 assert_eq!(first_ids.len(), 2);
2486
2487 let mut memory = HashMap::new();
2488 let (first_face, first_index) =
2489 font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
2490 let (second_face, second_index) =
2491 font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
2492 let (other_file, _) =
2493 font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
2494 assert_ne!(first_index, second_index);
2495 assert!(Arc::ptr_eq(&first_face, &second_face));
2496 assert!(!Arc::ptr_eq(&first_face, &other_file));
2497
2498 std::fs::remove_file(first_path).expect("remove first temporary font");
2499 std::fs::remove_file(second_path).expect("remove second temporary font");
2500 }
2501
2502 #[test]
2503 fn shaping_memo_uses_complete_text_size_and_font_identity() {
2504 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2505 let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2506 let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
2507
2508 let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
2509 let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
2510 assert_eq!(first.glyph_ids, repeat.glyph_ids);
2511 assert_eq!(fm.shaping_memo_counts().0, 1);
2512
2513 fm.shape_text(regular, "different text", 11.0).unwrap();
2514 fm.shape_text(regular, "exact text", 12.0).unwrap();
2515 fm.shape_text(bold, "exact text", 11.0).unwrap();
2516 assert_eq!(fm.shaping_memo_counts().1, 4);
2517
2518 let replacement = FontFile {
2519 family: "Carlito".to_owned(),
2520 data: bundled_font_data()[1].1.to_vec(),
2521 };
2522 fm.load_additional_fonts(&[replacement]);
2523 assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
2524 let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2525 fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
2526 assert_eq!(fm.shaping_memo_counts().1, 1);
2527 }
2528
2529 #[test]
2530 fn arabic_joining_survives_script_and_line_break_boundaries() {
2531 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2532 let segment = multilingual_test_segment(&mut fm, "العربية", 18.0, None);
2533 let shaped = fm
2534 .shape_multilingual_text(segment, Some("ar"), TextDirection::RightToLeft, false)
2535 .unwrap();
2536 assert_eq!(shaped.len(), 1);
2537 assert_eq!(
2538 shaped[0].glyph_ids(),
2539 &[288, 85, 319, 18, 317, 19, 31, 48, 72, 8]
2540 );
2541 assert_eq!(
2542 shaped[0]
2543 .clusters()
2544 .iter()
2545 .map(|cluster| (cluster.glyph_start..cluster.glyph_end, cluster.char_range()))
2546 .collect::<Vec<_>>(),
2547 vec![
2548 (0..2, 6..7),
2549 (2..4, 5..6),
2550 (4..6, 4..5),
2551 (6..7, 3..4),
2552 (7..8, 2..3),
2553 (8..9, 1..2),
2554 (9..10, 0..1),
2555 ]
2556 );
2557 }
2558
2559 #[test]
2560 fn indic_clusters_are_never_split_or_mapped_as_independent_scalars() {
2561 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2562 let segment = multilingual_test_segment(&mut fm, "कि", 18.0, None);
2563 let shaped = fm
2564 .shape_multilingual_text(segment, Some("hi"), TextDirection::LeftToRight, false)
2565 .unwrap();
2566
2567 assert_eq!(shaped[0].clusters()[0].char_range(), 0..2);
2568 assert_eq!(shaped[0].x_offsets().len(), shaped[0].glyph_ids().len());
2569 assert_eq!(shaped[0].y_offsets().len(), shaped[0].glyph_ids().len());
2570 }
2571
2572 #[test]
2573 fn same_script_coverage_changes_split_only_between_graphemes() {
2574 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2575 let font_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2576 let metrics = fm.metrics(font_id, 18.0).unwrap();
2577 let seed = fm.shape_text(font_id, "A☀∙", 18.0).unwrap();
2578 let shaped = fm
2579 .shape_multilingual_text(
2580 TextSegment {
2581 text: "A☀∙".to_owned(),
2582 direction: TextDirection::Auto,
2583 source: None,
2584 font_id,
2585 font_size: 18.0,
2586 glyph_ids: seed.glyph_ids,
2587 advances: seed.advances,
2588 width: seed.width,
2589 ascent: metrics.ascent,
2590 descent: metrics.descent,
2591 line_gap: metrics.line_gap,
2592 color: Color::BLACK,
2593 bold: false,
2594 italic: false,
2595 underline: None,
2596 strike: false,
2597 dstrike: false,
2598 highlight: None,
2599 baseline_offset: 0.0,
2600 hyperlink_url: None,
2601 field_kind: None,
2602 note: None,
2603 },
2604 None,
2605 TextDirection::LeftToRight,
2606 false,
2607 )
2608 .unwrap();
2609
2610 assert_eq!(
2611 shaped.iter().map(|span| span.text()).collect::<Vec<_>>(),
2612 ["A", "☀", "∙"]
2613 );
2614 assert!(shaped.iter().all(|span| span.script() == TextScript::Latin));
2615 assert_ne!(shaped[1].font_id(), shaped[2].font_id());
2616 assert!(shaped.iter().all(|span| {
2617 let index = fm.index_of(span.font_id()).unwrap();
2618 fm.uncovered(index, span.text()).is_empty()
2619 }));
2620 }
2621
2622 #[test]
2623 fn multilingual_constructor_rejects_invalid_levels_and_cluster_maps() {
2624 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2625 let segment = multilingual_test_segment(&mut fm, "ab", 18.0, None);
2626 let valid = fm
2627 .shape_multilingual_text(segment, None, TextDirection::LeftToRight, true)
2628 .unwrap()
2629 .remove(0);
2630 let rebuild = |bidi_level, clusters| {
2631 MultilingualTextSegment::new(
2632 valid.base().clone(),
2633 valid.logical_index(),
2634 valid.language().map(str::to_owned),
2635 valid.script(),
2636 valid.direction(),
2637 bidi_level,
2638 valid.x_advances().to_vec(),
2639 valid.y_advances().to_vec(),
2640 valid.x_offsets().to_vec(),
2641 valid.y_offsets().to_vec(),
2642 clusters,
2643 valid.break_after(),
2644 )
2645 };
2646
2647 assert!(rebuild(255, valid.clusters().to_vec()).is_err());
2648 let mut out_of_bounds = valid.clusters().to_vec();
2649 out_of_bounds.last_mut().unwrap().char_end = 3;
2650 assert!(rebuild(valid.bidi_level(), out_of_bounds).is_err());
2651 let mut glyph_gap = valid.clusters().to_vec();
2652 glyph_gap[0].glyph_start = 1;
2653 assert!(rebuild(valid.bidi_level(), glyph_gap).is_err());
2654 }
2655
2656 #[test]
2657 fn thai_words_offer_approved_breaks_without_losing_source_text() {
2658 let text = "ภาษาไทยยินดีต้อนรับ";
2659 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2660 let source = SourceSpan {
2661 node: crate::SourceNodeId::new(3).unwrap(),
2662 char_start: 10,
2663 char_end: 29,
2664 };
2665 let segment = multilingual_test_segment(&mut fm, text, 18.0, Some(source));
2666 let shaped = fm
2667 .shape_multilingual_text(segment, Some("th"), TextDirection::LeftToRight, false)
2668 .unwrap();
2669 assert_eq!(
2670 shaped
2671 .iter()
2672 .map(|span| {
2673 let source = span.base().source.unwrap();
2674 (
2675 span.text(),
2676 span.break_after(),
2677 source.char_start..source.char_end,
2678 )
2679 })
2680 .collect::<Vec<_>>(),
2681 vec![
2682 ("ภาษา", true, 10..14),
2683 ("ไทยยิน", true, 14..20),
2684 ("ดี", true, 20..22),
2685 ("ต้อน", true, 22..26),
2686 ("รับ", true, 26..29),
2687 ]
2688 );
2689 assert_eq!(
2690 shaped.iter().map(|span| span.text()).collect::<String>(),
2691 text
2692 );
2693 }
2694
2695 #[test]
2696 fn shaping_memo_hits_preserve_fifo_order() {
2697 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2698 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2699 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2700 fm.shape_text(font, "newest exact shape", 11.0).unwrap();
2701 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2702
2703 let memo = fm.shaping_memo.lock().unwrap();
2704 assert_eq!(memo.hits, 1);
2705 assert_eq!(memo.entries.front().unwrap().0.text, "oldest exact shape");
2706 assert_eq!(memo.entries.back().unwrap().0.text, "newest exact shape");
2707 }
2708
2709 #[test]
2710 fn shaping_memo_fingerprint_collision_requires_exact_key_equality() {
2711 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2712 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2713 fm.shape_text(font, "first collision candidate", 11.0)
2714 .unwrap();
2715 let forced = shaping_fingerprint(font, "second collision candidate", 11.0f64.to_bits());
2716 fm.shaping_memo.lock().unwrap().entries[0].3 = forced;
2717
2718 fm.shape_text(font, "second collision candidate", 11.0)
2719 .unwrap();
2720
2721 let (hits, misses, entries, _) = fm.shaping_memo_counts();
2722 assert_eq!((hits, misses, entries), (0, 2, 2));
2723 }
2724
2725 #[test]
2726 fn shaping_memo_is_bounded_and_recovers_from_poison() {
2727 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2728 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2729 for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
2730 fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
2731 .unwrap();
2732 }
2733 let (_, _, entries, bytes) = fm.shaping_memo_counts();
2734 assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
2735 assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
2736
2737 let fm = Arc::new(fm);
2738 let poison = Arc::clone(&fm);
2739 assert!(
2740 std::thread::spawn(move || {
2741 let _guard = poison.shaping_memo.lock().unwrap();
2742 panic!("poison shaping cache for recovery coverage");
2743 })
2744 .join()
2745 .is_err()
2746 );
2747 let first = fm.shape_text(font, "after poison", 11.0).unwrap();
2748 let second = fm.shape_text(font, "after poison", 11.0).unwrap();
2749 assert_eq!(first.glyph_ids, second.glyph_ids);
2750 let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
2751 assert_eq!((hits, misses, entries), (1, 1, 1));
2752 assert!(bytes > 0);
2753 }
2754
2755 #[test]
2756 fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
2757 let mut memo = ShapingMemo::new();
2758 for suffix in ['a', 'b'] {
2759 memo.insert(
2760 ShapingKey {
2761 font_id: FontId(0),
2762 text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
2763 size_bits: 11.0f64.to_bits(),
2764 },
2765 ShapedText {
2766 glyph_ids: Vec::new(),
2767 advances: Vec::new(),
2768 width: 0.0,
2769 },
2770 );
2771 }
2772 assert_eq!(memo.entries.len(), 1);
2773 assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
2774 }
2775
2776 #[test]
2777 fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
2778 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
2779 for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
2780 fm.remember_coverage_fallback(false, false, 0);
2781 }
2782 assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
2783
2784 let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
2785 .filter_map(|value| char::from_u32(0x10_000 + value as u32))
2786 .collect::<Vec<_>>();
2787 fm.remember_coverage_misses(&misses);
2788 assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
2789
2790 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2791 fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
2792 .expect("bounded fallback resolves");
2793 }
2794 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2795 assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
2796 }
2797
2798 #[test]
2799 fn active_document_may_resolve_more_than_256_distinct_faces() {
2800 let source = bundled_font_data()[4].1;
2801 let mut db = fontdb::Database::new();
2802 for index in 0..257 {
2803 db.load_font_data(font_with_family(source, &format!("F{index:06}")));
2804 }
2805 let mut fm = FontManager::from_base_database(db);
2806 fm.begin_layout();
2807 let mut ids = HashSet::new();
2808 for index in 0..257 {
2809 let family = format!("F{index:06}");
2810 let id = fm
2811 .resolve_font(Some(&family), false, false)
2812 .expect("distinct active face resolves");
2813 assert_eq!(fm.font_data(id).unwrap().family, family);
2814 ids.insert(id);
2815 }
2816 assert_eq!(ids.len(), 257);
2817 fm.retain_current_fonts();
2818 assert_eq!(fm.fonts.len(), 257);
2819 }
2820
2821 #[test]
2822 fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
2823 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
2824 fm.begin_layout();
2825 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
2826 fm.resolve_font(Some("Carlito"), false, false).unwrap();
2827 }
2828 assert!(fm.paragraph_font_trace.is_none());
2829
2830 fm.begin_paragraph_font_trace();
2831 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
2832 fm.resolve_font(Some("Carlito"), false, false).unwrap();
2833 }
2834 assert!(fm.finish_paragraph_font_trace().is_none());
2835
2836 fm.begin_layout();
2837 assert_eq!(fm.layout_fonts.capacity(), 0);
2838 fm.begin_paragraph_font_trace();
2839 fm.resolve_font(Some("Carlito"), false, false).unwrap();
2840 let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
2841 assert_eq!(trace.len(), 1);
2842 assert_eq!(trace.capacity(), trace.len());
2843 }
2844
2845 #[cfg(feature = "system-fonts")]
2846 #[test]
2847 fn file_byte_cache_is_bounded_and_recovers_from_poison() {
2848 let cache = Arc::new(Mutex::new(FileFontCache::new()));
2849 {
2850 let mut cache = cache
2851 .lock()
2852 .unwrap_or_else(std::sync::PoisonError::into_inner);
2853 cache.clear();
2854 let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
2855 let returned = cache_file_font_bytes(
2856 &mut cache,
2857 PathBuf::from("oversized-font.ttc"),
2858 Arc::clone(&oversized),
2859 )
2860 .expect("oversized bytes are returned uncached");
2861 assert!(Arc::ptr_eq(&oversized, &returned));
2862 assert!(cache.entries.is_empty());
2863 assert_eq!(cache.bytes, 0);
2864 }
2865
2866 let poison = Arc::clone(&cache);
2867 assert!(
2868 std::thread::spawn(move || {
2869 let cache = poison;
2870 let _guard = cache.lock().unwrap();
2871 panic!("poison file byte cache for recovery coverage");
2872 })
2873 .join()
2874 .is_err()
2875 );
2876
2877 let path = std::env::temp_dir().join(format!(
2878 "rdocx-font-cache-poison-{}-{:?}.ttf",
2879 std::process::id(),
2880 std::thread::current().id()
2881 ));
2882 std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
2883 let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
2884 let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
2885 assert!(Arc::ptr_eq(&first, &second));
2886 std::fs::remove_file(path).expect("remove recovery font");
2887 }
2888
2889 #[cfg(not(feature = "system-fonts"))]
2890 #[test]
2891 fn no_default_features_omits_system_font_discovery() {
2892 let fm = FontManager::new();
2893 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2894 }
2895
2896 #[test]
2897 fn font_manager_with_no_fonts_returns_an_error() {
2898 let mut fm = FontManager::new_with_fonts(Vec::new());
2899 assert!(matches!(
2900 fm.resolve_font(None, false, false),
2901 Err(LayoutError::FontNotFound(_))
2902 ));
2903 }
2904
2905 #[test]
2906 fn load_system_font() {
2907 let mut fm = FontManager::new();
2908 let result = fm.resolve_font(None, false, false);
2910 if let Ok(id) = result {
2912 assert_eq!(id.0, 0);
2913 }
2914 }
2915
2916 #[test]
2917 fn font_metrics_positive() {
2918 let mut fm = FontManager::new();
2919 if let Ok(id) = fm.resolve_font(None, false, false) {
2920 let metrics = fm.metrics(id, 12.0).unwrap();
2921 assert!(metrics.ascent > 0.0);
2922 assert!(metrics.descent > 0.0);
2923 assert!(metrics.units_per_em > 0);
2924 }
2925 }
2926
2927 #[test]
2928 fn shape_hello_world() {
2929 let mut fm = FontManager::new();
2930 if let Ok(id) = fm.resolve_font(None, false, false) {
2931 let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
2932 assert!(!shaped.glyph_ids.is_empty());
2933 assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
2934 assert!(shaped.width > 0.0);
2935 }
2936 }
2937
2938 #[test]
2939 fn font_caching() {
2940 let mut fm = FontManager::new();
2941 if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
2942 let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
2943 assert_eq!(id1, id2);
2944 }
2945 }
2946
2947 #[test]
2948 fn font_resolution_alias_cache_is_bounded() {
2949 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2950 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2951 fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
2952 .expect("fallback font resolves");
2953 }
2954 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2955 assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2956 }
2957
2958 #[test]
2959 fn bold_italic_variants() {
2960 let mut fm = FontManager::new();
2961 let regular = fm.resolve_font(None, false, false);
2962 let bold = fm.resolve_font(None, true, false);
2963 if let (Ok(r), Ok(b)) = (regular, bold) {
2964 assert_ne!(r, b);
2966 }
2967 }
2968
2969 #[test]
2972 fn latin_text_resolves_the_same_as_by_name() {
2973 let mut fm = FontManager::new();
2974 let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
2975 return;
2976 };
2977 let for_text = fm
2978 .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
2979 .unwrap();
2980 assert_eq!(by_name, for_text);
2981 }
2982
2983 #[test]
2989 fn text_no_font_can_draw_keeps_the_requested_font() {
2990 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2991 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2992 let resolved = fm
2993 .resolve_font_for_text(Some("Carlito"), false, false, "🀄")
2994 .unwrap();
2995 assert_eq!(
2996 primary, resolved,
2997 "with no covering font available the original must be kept"
2998 );
2999 }
3000
3001 #[test]
3003 fn whitespace_does_not_trigger_a_fallback() {
3004 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
3005 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
3006 let idx = fm.index_of(by_name).unwrap();
3007 assert!(
3009 fm.uncovered(idx, "a\u{00a0}b\tc")
3010 .iter()
3011 .all(|c| *c != '\t'),
3012 "control and whitespace characters must be ignored"
3013 );
3014 }
3015
3016 #[test]
3022 fn cjk_text_moves_off_a_latin_font_when_possible() {
3023 let mut fm = FontManager::new();
3024 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
3025 return;
3026 };
3027 let Some(idx) = fm.index_of(latin) else {
3028 return;
3029 };
3030 if fm.uncovered(idx, "这是中文").is_empty() {
3031 return; }
3033 let resolved = fm
3034 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
3035 .unwrap();
3036 if resolved == latin {
3037 return; }
3039 let new_idx = fm.index_of(resolved).unwrap();
3040 assert!(
3041 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
3042 "the replacement must cover more of the text than the original"
3043 );
3044 }
3045}