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