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 }
2043}
2044
2045#[cfg(feature = "system-fonts")]
2046fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
2047 let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
2048 shared_file_font_bytes_from_cache(cache, path)
2049}
2050
2051#[cfg(feature = "system-fonts")]
2052fn shared_file_font_bytes_from_cache(
2053 cache_lock: &Mutex<FileFontCache>,
2054 path: &Path,
2055) -> Option<Arc<[u8]>> {
2056 let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
2057 let mut cache = match cache_lock.lock() {
2058 Ok(cache) => cache,
2059 Err(poisoned) => {
2060 let mut cache = poisoned.into_inner();
2061 cache.clear();
2062 cache_lock.clear_poison();
2063 cache
2064 }
2065 };
2066 if let Some(index) = cache
2067 .entries
2068 .iter()
2069 .position(|(candidate, _, _)| candidate == &identity)
2070 {
2071 let entry = cache.entries.remove(index).expect("cache index exists");
2072 let bytes = Arc::clone(&entry.1);
2073 cache.entries.push_back(entry);
2074 return Some(bytes);
2075 }
2076
2077 let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
2078 cache_file_font_bytes(&mut cache, identity, bytes)
2079}
2080
2081#[cfg(feature = "system-fonts")]
2082fn cache_file_font_bytes(
2083 cache: &mut FileFontCache,
2084 identity: PathBuf,
2085 bytes: Arc<[u8]>,
2086) -> Option<Arc<[u8]>> {
2087 let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
2088 .saturating_add(identity.as_os_str().len())
2089 .saturating_add(bytes.len());
2090 if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
2091 while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
2092 || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
2093 {
2094 let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
2095 break;
2096 };
2097 cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
2098 }
2099 cache.bytes += entry_bytes;
2100 cache
2101 .entries
2102 .push_back((identity, Arc::clone(&bytes), entry_bytes));
2103 }
2104 Some(bytes)
2105}
2106
2107fn map_font_name(name: &str) -> &[&str] {
2114 match name {
2115 "Calibri" => &["Calibri", "Carlito"],
2116 "Calibri Light" => &["Calibri Light", "Carlito"],
2117 "Cambria" => &["Cambria", "Caladea"],
2118 "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
2119 "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
2120 "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
2121 "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
2122 "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
2123 "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
2124 "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
2125 "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
2126 "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
2127 "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
2128 "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
2129 "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
2130 "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
2131 "Impact" => &["Impact", "Liberation Sans", "Arial"],
2132 "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
2133 "Symbol" => &["Symbol", "DejaVu Sans"],
2134 "Wingdings" => &["Wingdings", "Symbol"],
2135 _ => &[],
2136 }
2137}
2138
2139#[cfg(test)]
2140mod tests {
2141 use super::*;
2142 use crate::Color;
2143 use crate::bundled_fonts::bundled_font_data;
2144
2145 fn multilingual_test_segment(
2146 manager: &mut FontManager,
2147 text: &str,
2148 size_pt: f64,
2149 source: Option<SourceSpan>,
2150 ) -> TextSegment {
2151 let font_id = manager
2152 .resolve_font_for_text(None, false, false, text)
2153 .expect("test font resolves");
2154 let metrics = manager
2155 .metrics(font_id, size_pt)
2156 .expect("test font metrics");
2157 let shaped = manager
2158 .shape_text(font_id, text, size_pt)
2159 .expect("test seed shapes");
2160 TextSegment {
2161 text: text.to_owned(),
2162 direction: TextDirection::Auto,
2163 source,
2164 font_id,
2165 font_size: size_pt,
2166 glyph_ids: shaped.glyph_ids,
2167 advances: shaped.advances,
2168 width: shaped.width,
2169 ascent: metrics.ascent,
2170 descent: metrics.descent,
2171 line_gap: metrics.line_gap,
2172 color: Color::BLACK,
2173 bold: false,
2174 italic: false,
2175 underline: None,
2176 strike: false,
2177 dstrike: false,
2178 highlight: None,
2179 baseline_offset: 0.0,
2180 hyperlink_url: None,
2181 field_kind: None,
2182 field_source: None,
2183 note: None,
2184 }
2185 }
2186
2187 #[test]
2193 fn word_line_metrics_are_the_windows_extent_and_its_external_leading() {
2194 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2195 for (family, units_per_em, ascent, descent, leading) in [
2196 ("Carlito", 2048.0, 1950.0, 550.0, 0.0),
2197 ("Liberation Sans", 2048.0, 1854.0, 434.0, 67.0),
2198 ("Liberation Serif", 2048.0, 1825.0, 443.0, 87.0),
2199 ("Caladea", 1000.0, 950.0, 222.0, 0.0),
2200 ("Liberation Mono", 2048.0, 1705.0, 615.0, 0.0),
2201 ("Noto Sans Devanagari", 1000.0, 896.0, 408.0, 0.0),
2204 ] {
2205 let font_id = manager
2206 .resolve_font(Some(family), false, false)
2207 .expect("bundled face resolves");
2208 let metrics = manager
2209 .word_line_metrics(font_id, 12.0)
2210 .expect("loaded face has metrics");
2211 let scale = 12.0 / units_per_em;
2212 assert_eq!(metrics.units_per_em as f64, units_per_em, "{family}");
2213 assert!((metrics.ascent - ascent * scale).abs() < 1e-9, "{family}");
2214 assert!((metrics.descent - descent * scale).abs() < 1e-9, "{family}");
2215 assert!(
2216 (metrics.line_gap - leading * scale).abs() < 1e-9,
2217 "{family}"
2218 );
2219 }
2220 }
2221
2222 #[test]
2223 fn caller_font_labels_resolve_after_exact_embedded_families() {
2224 let caladea = bundled_font_data()
2225 .into_iter()
2226 .find(|(family, _)| *family == "Caladea")
2227 .expect("Caladea is bundled")
2228 .1;
2229 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2230 manager.load_additional_fonts(&[
2231 FontFile {
2232 family: "Document Serif".to_owned(),
2233 data: caladea.to_vec(),
2234 },
2235 FontFile {
2236 family: "Carlito".to_owned(),
2237 data: caladea.to_vec(),
2238 },
2239 ]);
2240
2241 let aliased = manager
2242 .resolve_font(Some("Document Serif"), false, false)
2243 .expect("caller label resolves");
2244 assert_eq!(
2245 manager.fonts[manager.index_of(aliased).unwrap()].family,
2246 "Caladea"
2247 );
2248
2249 let exact = manager
2250 .resolve_font(Some("Carlito"), false, false)
2251 .expect("embedded family resolves exactly");
2252 assert_eq!(
2253 manager.fonts[manager.index_of(exact).unwrap()].family,
2254 "Carlito"
2255 );
2256
2257 manager.set_caller_aliases(&[("Document Serif".to_owned(), "Carlito".to_owned())]);
2258 let explicit = manager
2259 .resolve_font(Some("Document Serif"), false, false)
2260 .expect("explicit alias precedes label-derived alias");
2261 assert_eq!(
2262 manager.fonts[manager.index_of(explicit).unwrap()].family,
2263 "Carlito"
2264 );
2265
2266 manager.load_additional_fonts(&[FontFile {
2267 family: "Caladea".to_owned(),
2268 data: caladea.to_vec(),
2269 }]);
2270 assert!(manager.caller_aliases.is_empty());
2271
2272 manager.set_caller_aliases(&[("Arial".to_owned(), "Caladea".to_owned())]);
2273 let caller_alias = manager
2274 .resolve_font(Some("Arial"), false, false)
2275 .expect("explicit alias resolves before mapped fallback");
2276 assert_eq!(
2277 manager.fonts[manager.index_of(caller_alias).unwrap()].family,
2278 "Caladea"
2279 );
2280 let generic = manager
2281 .resolve_font(Some("Unmapped Document Family"), false, false)
2282 .expect("generic fallback remains available");
2283 assert_eq!(
2284 manager.fonts[manager.index_of(generic).unwrap()].family,
2285 "Carlito"
2286 );
2287 }
2288
2289 #[test]
2290 fn caller_alias_updates_preserve_bytes_and_invalidate_resolution_state() {
2291 let caladea = bundled_font_data()
2292 .into_iter()
2293 .find(|(family, _)| *family == "Caladea")
2294 .expect("Caladea is bundled")
2295 .1;
2296 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2297 manager.load_additional_fonts(&[FontFile {
2298 family: "Caladea".to_owned(),
2299 data: caladea.to_vec(),
2300 }]);
2301 let aliases = vec![
2302 ("Document Serif A".to_owned(), "Caladea".to_owned()),
2303 ("Document Serif B".to_owned(), "Caladea".to_owned()),
2304 ];
2305 assert!(manager.set_caller_aliases(&aliases));
2306
2307 let first = manager
2308 .resolve_font(Some("Document Serif A"), false, false)
2309 .expect("first alias resolves");
2310 let second = manager
2311 .resolve_font(Some("Document Serif B"), false, false)
2312 .expect("second alias resolves");
2313 let first_data = &manager.fonts[manager.index_of(first).unwrap()].data;
2314 let second_data = &manager.fonts[manager.index_of(second).unwrap()].data;
2315 assert!(Arc::ptr_eq(first_data, second_data));
2316 assert!(!manager.set_caller_aliases(&aliases));
2317
2318 assert!(
2319 manager.set_caller_aliases(&[("Document Serif A".to_owned(), "Carlito".to_owned(),)])
2320 );
2321 let changed = manager
2322 .resolve_font(Some("Document Serif A"), false, false)
2323 .expect("changed alias resolves");
2324 assert_eq!(
2325 manager.fonts[manager.index_of(changed).unwrap()].family,
2326 "Carlito"
2327 );
2328 assert!(
2329 manager.index_of(first).is_some(),
2330 "loaded faces are retained"
2331 );
2332 }
2333
2334 #[test]
2335 fn explicit_alias_state_respects_entry_and_retained_byte_ceilings() {
2336 let aliases = (0..CALLER_ALIAS_MAX_ENTRIES + 32)
2337 .map(|index| (format!("Document Serif {index}"), "Caladea".to_owned()))
2338 .collect::<Vec<_>>();
2339 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2340 manager.set_caller_aliases(&aliases);
2341 assert_eq!(
2342 manager.explicit_aliases.as_slice(),
2343 &aliases[..CALLER_ALIAS_MAX_ENTRIES]
2344 );
2345 assert!(manager.explicit_aliases.len() <= CALLER_ALIAS_MAX_ENTRIES);
2346 assert!(manager.explicit_alias_map.len() <= CALLER_ALIAS_MAX_ENTRIES);
2347
2348 let retained_large = ("x".repeat(32_760), String::new());
2349 let byte_limited = vec![
2350 retained_large.clone(),
2351 ("discarded bytes".to_owned(), "Caladea".to_owned()),
2352 ];
2353 manager.set_caller_aliases(&byte_limited);
2354 assert_eq!(manager.explicit_aliases, vec![retained_large]);
2355
2356 let oversized = vec![("x".repeat(40_000), "Caladea".to_owned())];
2357 manager.set_caller_aliases(&oversized);
2358 let retained_bytes = manager
2359 .explicit_aliases
2360 .iter()
2361 .map(|(requested, target)| requested.len() + target.len())
2362 .sum::<usize>()
2363 + manager
2364 .explicit_alias_map
2365 .iter()
2366 .map(|(requested, target)| requested.len() + target.len())
2367 .sum::<usize>();
2368 assert!(retained_bytes <= CALLER_ALIAS_MAX_RETAINED_BYTES);
2369 assert!(manager.explicit_aliases.is_empty());
2370 assert!(manager.explicit_alias_map.is_empty());
2371 }
2372
2373 #[test]
2374 fn label_alias_prefers_caller_bytes_over_bundled_same_family() {
2375 let bundled = bundled_font_data()
2376 .into_iter()
2377 .find(|(family, _)| *family == "Caladea")
2378 .expect("Caladea is bundled")
2379 .1;
2380 let mut caller = bundled.to_vec();
2381 caller.push(0);
2382 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2383 manager.load_additional_fonts(&[FontFile {
2384 family: "Document Serif".to_owned(),
2385 data: caller.clone(),
2386 }]);
2387
2388 let resolved = manager
2389 .resolve_font(Some("Document Serif"), false, false)
2390 .expect("caller label resolves");
2391 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2392 assert_eq!(loaded.family, "Caladea");
2393 assert_eq!(loaded.data.as_ref(), caller.as_slice());
2394 assert_ne!(loaded.data.as_ref(), bundled);
2395 }
2396
2397 #[test]
2398 fn case_only_caller_labels_resolve_to_the_supplied_face() {
2399 let bundled = bundled_font_data()
2400 .into_iter()
2401 .find(|(family, _)| *family == "Caladea")
2402 .expect("Caladea is bundled")
2403 .1;
2404 let mut caller = bundled.to_vec();
2405 caller.push(0);
2406 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2407 manager.load_additional_fonts(&[FontFile {
2408 family: "caladea".to_owned(),
2409 data: caller.clone(),
2410 }]);
2411
2412 let resolved = manager
2413 .resolve_font(Some("caladea"), false, false)
2414 .expect("case-only caller label resolves");
2415 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2416 assert_eq!(loaded.family, "Caladea");
2417 assert_eq!(loaded.data.as_ref(), caller.as_slice());
2418 }
2419
2420 #[test]
2421 fn constructor_label_alias_survives_additional_font_replacement() {
2422 let caladea = bundled_font_data()
2423 .into_iter()
2424 .find(|(family, _)| *family == "Caladea")
2425 .expect("Caladea is bundled")
2426 .1;
2427 let carlito = bundled_font_data()
2428 .into_iter()
2429 .find(|(family, _)| *family == "Carlito")
2430 .expect("Carlito is bundled")
2431 .1;
2432 let mut constructor = caladea.to_vec();
2433 constructor.push(0);
2434 let mut manager =
2435 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2436
2437 manager.load_additional_fonts(&[FontFile {
2438 family: "Additional Sans".to_owned(),
2439 data: carlito.to_vec(),
2440 }]);
2441
2442 let resolved = manager
2443 .resolve_font(Some("Document Serif"), false, false)
2444 .expect("constructor label still resolves");
2445 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2446 assert_eq!(loaded.family, "Caladea");
2447 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2448 }
2449
2450 #[test]
2451 fn constructor_family_priority_survives_additional_font_replacement() {
2452 let caladea = bundled_font_data()
2453 .into_iter()
2454 .find(|(family, _)| *family == "Caladea")
2455 .expect("Caladea is bundled")
2456 .1;
2457 let mut constructor = caladea.to_vec();
2458 constructor.push(0);
2459 let mut replacement = caladea.to_vec();
2460 replacement.extend_from_slice(&[0, 0]);
2461 let mut manager =
2462 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2463
2464 manager.load_additional_fonts(&[FontFile {
2465 family: "Caladea".to_owned(),
2466 data: replacement,
2467 }]);
2468
2469 let resolved = manager
2470 .resolve_font(Some("Caladea"), false, false)
2471 .expect("constructor family still resolves");
2472 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2473 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2474 }
2475
2476 #[test]
2477 fn caller_face_selection_matches_fontdb_css_rules() {
2478 let caladea = bundled_font_data()
2479 .into_iter()
2480 .find(|(family, _)| *family == "Caladea")
2481 .expect("Caladea is bundled")
2482 .1;
2483 let mut source = fontdb::Database::new();
2484 source.load_font_data(caladea.to_vec());
2485 let template = source.faces().next().expect("Caladea has a face").clone();
2486 let mut db = fontdb::Database::new();
2487 let mut add_face = |weight: u16, stretch: fontdb::Stretch, style: fontdb::Style| {
2488 let mut face = template.clone();
2489 face.weight = fontdb::Weight(weight);
2490 face.stretch = stretch;
2491 face.style = style;
2492 db.push_face_info(face)
2493 };
2494
2495 let weight_300 = add_face(300, fontdb::Stretch::Normal, fontdb::Style::Normal);
2496 let weight_500 = add_face(500, fontdb::Stretch::Normal, fontdb::Style::Normal);
2497 let weight_600 = add_face(600, fontdb::Stretch::Normal, fontdb::Style::Normal);
2498 let weight_800 = add_face(800, fontdb::Stretch::Normal, fontdb::Style::Normal);
2499 let expanded = add_face(400, fontdb::Stretch::SemiExpanded, fontdb::Style::Normal);
2500 let condensed = add_face(400, fontdb::Stretch::SemiCondensed, fontdb::Style::Normal);
2501 let normal = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Normal);
2502 let italic = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Italic);
2503
2504 assert_eq!(
2505 best_caller_face(
2506 &db,
2507 &[weight_300, weight_500],
2508 fontdb::Weight::NORMAL,
2509 fontdb::Style::Normal,
2510 ),
2511 Some(weight_500)
2512 );
2513 assert_eq!(
2514 best_caller_face(
2515 &db,
2516 &[weight_600, weight_800],
2517 fontdb::Weight::BOLD,
2518 fontdb::Style::Normal,
2519 ),
2520 Some(weight_800)
2521 );
2522 assert_eq!(
2523 best_caller_face(
2524 &db,
2525 &[expanded, condensed],
2526 fontdb::Weight::NORMAL,
2527 fontdb::Style::Normal,
2528 ),
2529 Some(condensed)
2530 );
2531 assert_eq!(
2532 best_caller_face(
2533 &db,
2534 &[normal, italic],
2535 fontdb::Weight::NORMAL,
2536 fontdb::Style::Oblique,
2537 ),
2538 Some(italic)
2539 );
2540 }
2541
2542 fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
2543 assert_eq!(family.len(), 7);
2544 let mut font = source.to_vec();
2545 let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
2546 let name_offset = (0..table_count)
2547 .find_map(|table| {
2548 let record = 12 + table * 16;
2549 (&font[record..record + 4] == b"name").then(|| {
2550 u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
2551 })
2552 })
2553 .expect("font has name table");
2554 let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
2555 let strings = name_offset
2556 + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
2557 for index in 0..count {
2558 let record = name_offset + 6 + index * 12;
2559 let platform = u16::from_be_bytes([font[record], font[record + 1]]);
2560 let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
2561 let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
2562 let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
2563 if !matches!(name_id, 1 | 16) {
2564 continue;
2565 }
2566 let destination = &mut font[strings + offset..strings + offset + length];
2567 match (platform, length) {
2568 (0 | 3, 14) => {
2569 for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
2570 bytes.copy_from_slice(&(ch as u16).to_be_bytes());
2571 }
2572 }
2573 (1, 7) => destination.copy_from_slice(family.as_bytes()),
2574 _ => {}
2575 }
2576 }
2577 font
2578 }
2579
2580 #[cfg(feature = "system-fonts")]
2581 fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
2582 let header_len = 12 + fonts.len() * 4;
2583 let mut collection = vec![0u8; header_len];
2584 collection[0..4].copy_from_slice(b"ttcf");
2585 collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
2586 collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
2587
2588 for (font_number, font) in fonts.iter().enumerate() {
2589 while !collection.len().is_multiple_of(4) {
2590 collection.push(0);
2591 }
2592 let collection_offset = collection.len();
2593 collection[12 + font_number * 4..16 + font_number * 4]
2594 .copy_from_slice(&(collection_offset as u32).to_be_bytes());
2595
2596 let mut adjusted = font.to_vec();
2597 let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
2598 for table in 0..table_count {
2599 let offset_position = 12 + table * 16 + 8;
2600 let offset = u32::from_be_bytes(
2601 adjusted[offset_position..offset_position + 4]
2602 .try_into()
2603 .expect("table offset"),
2604 );
2605 adjusted[offset_position..offset_position + 4]
2606 .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
2607 }
2608 collection.extend_from_slice(&adjusted);
2609 }
2610 collection
2611 }
2612
2613 #[test]
2614 fn deterministic_font_manager_uses_only_bundled_fonts() {
2615 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2616
2617 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2618 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
2619 }
2620
2621 #[cfg(feature = "system-fonts")]
2622 #[test]
2623 fn normal_font_discovery_initializes_once_per_process() {
2624 let _first = FontManager::new();
2625 let _second = FontManager::new();
2626 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2627
2628 let _deterministic =
2629 FontManager::new_deterministic().expect("bundled font manager should load");
2630 let _caller = FontManager::new_with_fonts(Vec::new());
2631 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2632 }
2633
2634 #[cfg(feature = "system-fonts")]
2635 #[test]
2636 fn file_backed_collection_faces_share_one_byte_buffer() {
2637 let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
2638 let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
2639 let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
2640 let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
2641 std::fs::write(&first_path, &collection).expect("write first temporary collection");
2642 std::fs::write(&second_path, &collection).expect("write second temporary collection");
2643
2644 let mut db = fontdb::Database::new();
2645 db.load_font_file(&first_path).expect("load first TTC");
2646 db.load_font_file(&second_path).expect("load second TTC");
2647 let canonical_first = std::fs::canonicalize(&first_path).unwrap();
2648 let canonical_second = std::fs::canonicalize(&second_path).unwrap();
2649 let first_ids = db
2650 .faces()
2651 .filter_map(|face| match &face.source {
2652 fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
2653 Some(face.id)
2654 }
2655 _ => None,
2656 })
2657 .collect::<Vec<_>>();
2658 let second_id = db
2659 .faces()
2660 .find_map(|face| match &face.source {
2661 fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
2662 Some(face.id)
2663 }
2664 _ => None,
2665 })
2666 .expect("second TTC face");
2667 assert_eq!(first_ids.len(), 2);
2668
2669 let mut memory = HashMap::new();
2670 let (first_face, first_index) =
2671 font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
2672 let (second_face, second_index) =
2673 font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
2674 let (other_file, _) =
2675 font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
2676 assert_ne!(first_index, second_index);
2677 assert!(Arc::ptr_eq(&first_face, &second_face));
2678 assert!(!Arc::ptr_eq(&first_face, &other_file));
2679
2680 std::fs::remove_file(first_path).expect("remove first temporary font");
2681 std::fs::remove_file(second_path).expect("remove second temporary font");
2682 }
2683
2684 #[test]
2685 fn shaping_memo_uses_complete_text_size_and_font_identity() {
2686 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2687 let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2688 let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
2689
2690 let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
2691 let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
2692 assert_eq!(first.glyph_ids, repeat.glyph_ids);
2693 assert_eq!(fm.shaping_memo_counts().0, 1);
2694
2695 fm.shape_text(regular, "different text", 11.0).unwrap();
2696 fm.shape_text(regular, "exact text", 12.0).unwrap();
2697 fm.shape_text(bold, "exact text", 11.0).unwrap();
2698 assert_eq!(fm.shaping_memo_counts().1, 4);
2699
2700 let replacement = FontFile {
2701 family: "Carlito".to_owned(),
2702 data: bundled_font_data()[1].1.to_vec(),
2703 };
2704 fm.load_additional_fonts(&[replacement]);
2705 assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
2706 let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2707 fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
2708 assert_eq!(fm.shaping_memo_counts().1, 1);
2709 }
2710
2711 #[test]
2712 fn arabic_joining_survives_script_and_line_break_boundaries() {
2713 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2714 let segment = multilingual_test_segment(&mut fm, "العربية", 18.0, None);
2715 let shaped = fm
2716 .shape_multilingual_text(segment, Some("ar"), TextDirection::RightToLeft, false)
2717 .unwrap();
2718 assert_eq!(shaped.len(), 1);
2719 assert_eq!(
2720 shaped[0].glyph_ids(),
2721 &[288, 85, 319, 18, 317, 19, 31, 48, 72, 8]
2722 );
2723 assert_eq!(
2724 shaped[0]
2725 .clusters()
2726 .iter()
2727 .map(|cluster| (cluster.glyph_start..cluster.glyph_end, cluster.char_range()))
2728 .collect::<Vec<_>>(),
2729 vec![
2730 (0..2, 6..7),
2731 (2..4, 5..6),
2732 (4..6, 4..5),
2733 (6..7, 3..4),
2734 (7..8, 2..3),
2735 (8..9, 1..2),
2736 (9..10, 0..1),
2737 ]
2738 );
2739 }
2740
2741 #[test]
2742 fn indic_clusters_are_never_split_or_mapped_as_independent_scalars() {
2743 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2744 let segment = multilingual_test_segment(&mut fm, "कि", 18.0, None);
2745 let shaped = fm
2746 .shape_multilingual_text(segment, Some("hi"), TextDirection::LeftToRight, false)
2747 .unwrap();
2748
2749 assert_eq!(shaped[0].clusters()[0].char_range(), 0..2);
2750 assert_eq!(shaped[0].x_offsets().len(), shaped[0].glyph_ids().len());
2751 assert_eq!(shaped[0].y_offsets().len(), shaped[0].glyph_ids().len());
2752 }
2753
2754 #[test]
2755 fn same_script_coverage_changes_split_only_between_graphemes() {
2756 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2757 let font_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2758 let metrics = fm.metrics(font_id, 18.0).unwrap();
2759 let seed = fm.shape_text(font_id, "A☀∙", 18.0).unwrap();
2760 let shaped = fm
2761 .shape_multilingual_text(
2762 TextSegment {
2763 text: "A☀∙".to_owned(),
2764 direction: TextDirection::Auto,
2765 source: None,
2766 font_id,
2767 font_size: 18.0,
2768 glyph_ids: seed.glyph_ids,
2769 advances: seed.advances,
2770 width: seed.width,
2771 ascent: metrics.ascent,
2772 descent: metrics.descent,
2773 line_gap: metrics.line_gap,
2774 color: Color::BLACK,
2775 bold: false,
2776 italic: false,
2777 underline: None,
2778 strike: false,
2779 dstrike: false,
2780 highlight: None,
2781 baseline_offset: 0.0,
2782 hyperlink_url: None,
2783 field_kind: None,
2784 field_source: None,
2785 note: None,
2786 },
2787 None,
2788 TextDirection::LeftToRight,
2789 false,
2790 )
2791 .unwrap();
2792
2793 assert_eq!(
2794 shaped.iter().map(|span| span.text()).collect::<Vec<_>>(),
2795 ["A", "☀", "∙"]
2796 );
2797 assert!(shaped.iter().all(|span| span.script() == TextScript::Latin));
2798 assert_ne!(shaped[1].font_id(), shaped[2].font_id());
2799 assert!(shaped.iter().all(|span| {
2800 let index = fm.index_of(span.font_id()).unwrap();
2801 fm.uncovered(index, span.text()).is_empty()
2802 }));
2803 }
2804
2805 #[test]
2806 fn multilingual_constructor_rejects_invalid_levels_and_cluster_maps() {
2807 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2808 let segment = multilingual_test_segment(&mut fm, "ab", 18.0, None);
2809 let valid = fm
2810 .shape_multilingual_text(segment, None, TextDirection::LeftToRight, true)
2811 .unwrap()
2812 .remove(0);
2813 let rebuild = |bidi_level, clusters| {
2814 MultilingualTextSegment::new(
2815 valid.base().clone(),
2816 valid.logical_index(),
2817 valid.language().map(str::to_owned),
2818 valid.script(),
2819 valid.direction(),
2820 bidi_level,
2821 valid.x_advances().to_vec(),
2822 valid.y_advances().to_vec(),
2823 valid.x_offsets().to_vec(),
2824 valid.y_offsets().to_vec(),
2825 clusters,
2826 valid.break_after(),
2827 )
2828 };
2829
2830 assert!(rebuild(255, valid.clusters().to_vec()).is_err());
2831 let mut out_of_bounds = valid.clusters().to_vec();
2832 out_of_bounds.last_mut().unwrap().char_end = 3;
2833 assert!(rebuild(valid.bidi_level(), out_of_bounds).is_err());
2834 let mut glyph_gap = valid.clusters().to_vec();
2835 glyph_gap[0].glyph_start = 1;
2836 assert!(rebuild(valid.bidi_level(), glyph_gap).is_err());
2837 }
2838
2839 #[test]
2840 fn thai_words_offer_approved_breaks_without_losing_source_text() {
2841 let text = "ภาษาไทยยินดีต้อนรับ";
2842 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2843 let source = SourceSpan {
2844 node: crate::SourceNodeId::new(3).unwrap(),
2845 char_start: 10,
2846 char_end: 29,
2847 };
2848 let segment = multilingual_test_segment(&mut fm, text, 18.0, Some(source));
2849 let shaped = fm
2850 .shape_multilingual_text(segment, Some("th"), TextDirection::LeftToRight, false)
2851 .unwrap();
2852 assert_eq!(
2853 shaped
2854 .iter()
2855 .map(|span| {
2856 let source = span.base().source.unwrap();
2857 (
2858 span.text(),
2859 span.break_after(),
2860 source.char_start..source.char_end,
2861 )
2862 })
2863 .collect::<Vec<_>>(),
2864 vec![
2865 ("ภาษา", true, 10..14),
2866 ("ไทยยิน", true, 14..20),
2867 ("ดี", true, 20..22),
2868 ("ต้อน", true, 22..26),
2869 ("รับ", true, 26..29),
2870 ]
2871 );
2872 assert_eq!(
2873 shaped.iter().map(|span| span.text()).collect::<String>(),
2874 text
2875 );
2876 }
2877
2878 #[test]
2879 fn latin_spans_end_at_line_break_opportunities_with_their_spaces_apart() {
2880 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2881 let segment = multilingual_test_segment(&mut fm, "in service, or re-coated", 18.0, None);
2882 let shaped = fm
2883 .shape_multilingual_text(segment, None, TextDirection::LeftToRight, false)
2884 .unwrap();
2885
2886 assert_eq!(
2889 shaped
2890 .iter()
2891 .map(|span| (span.text(), span.break_after()))
2892 .collect::<Vec<_>>(),
2893 vec![
2894 ("in", false),
2895 (" ", true),
2896 ("service,", false),
2897 (" ", true),
2898 ("or", false),
2899 (" ", true),
2900 ("re-", true),
2901 ("coated", true),
2902 ]
2903 );
2904 }
2905
2906 #[test]
2907 fn a_word_split_across_styled_segments_has_no_break_where_they_meet() {
2908 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2909 let first = multilingual_test_segment(&mut fm, "Well-kno", 18.0, None);
2910 let second = multilingual_test_segment(&mut fm, "wn state", 18.0, None);
2911 let shaped = fm
2912 .shape_multilingual_paragraph(
2913 vec![(first, None), (second, None)],
2914 TextDirection::LeftToRight,
2915 false,
2916 )
2917 .unwrap();
2918
2919 assert_eq!(
2920 shaped
2921 .iter()
2922 .map(|span| (span.text(), span.break_after()))
2923 .collect::<Vec<_>>(),
2924 vec![
2925 ("Well-", true),
2926 ("kno", false),
2927 ("wn", false),
2928 (" ", true),
2929 ("state", true),
2930 ]
2931 );
2932 }
2933
2934 #[test]
2935 fn shaping_memo_hits_preserve_fifo_order() {
2936 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2937 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2938 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2939 fm.shape_text(font, "newest exact shape", 11.0).unwrap();
2940 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2941
2942 let memo = fm.shaping_memo.lock().unwrap();
2943 assert_eq!(memo.hits, 1);
2944 assert_eq!(memo.entries.front().unwrap().0.text, "oldest exact shape");
2945 assert_eq!(memo.entries.back().unwrap().0.text, "newest exact shape");
2946 }
2947
2948 #[test]
2949 fn shaping_memo_fingerprint_collision_requires_exact_key_equality() {
2950 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2951 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2952 fm.shape_text(font, "first collision candidate", 11.0)
2953 .unwrap();
2954 let forced = shaping_fingerprint(font, "second collision candidate", 11.0f64.to_bits());
2955 fm.shaping_memo.lock().unwrap().entries[0].3 = forced;
2956
2957 fm.shape_text(font, "second collision candidate", 11.0)
2958 .unwrap();
2959
2960 let (hits, misses, entries, _) = fm.shaping_memo_counts();
2961 assert_eq!((hits, misses, entries), (0, 2, 2));
2962 }
2963
2964 #[test]
2965 fn shaping_memo_is_bounded_and_recovers_from_poison() {
2966 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2967 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2968 for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
2969 fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
2970 .unwrap();
2971 }
2972 let (_, _, entries, bytes) = fm.shaping_memo_counts();
2973 assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
2974 assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
2975
2976 let fm = Arc::new(fm);
2977 let poison = Arc::clone(&fm);
2978 assert!(
2979 std::thread::spawn(move || {
2980 let _guard = poison.shaping_memo.lock().unwrap();
2981 panic!("poison shaping cache for recovery coverage");
2982 })
2983 .join()
2984 .is_err()
2985 );
2986 let first = fm.shape_text(font, "after poison", 11.0).unwrap();
2987 let second = fm.shape_text(font, "after poison", 11.0).unwrap();
2988 assert_eq!(first.glyph_ids, second.glyph_ids);
2989 let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
2990 assert_eq!((hits, misses, entries), (1, 1, 1));
2991 assert!(bytes > 0);
2992 }
2993
2994 #[test]
2995 fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
2996 let mut memo = ShapingMemo::new();
2997 for suffix in ['a', 'b'] {
2998 memo.insert(
2999 ShapingKey {
3000 font_id: FontId(0),
3001 text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
3002 size_bits: 11.0f64.to_bits(),
3003 },
3004 ShapedText {
3005 glyph_ids: Vec::new(),
3006 advances: Vec::new(),
3007 width: 0.0,
3008 },
3009 );
3010 }
3011 assert_eq!(memo.entries.len(), 1);
3012 assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
3013 }
3014
3015 #[test]
3016 fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
3017 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
3018 for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
3019 fm.remember_coverage_fallback(false, false, 0);
3020 }
3021 assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
3022
3023 let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
3024 .filter_map(|value| char::from_u32(0x10_000 + value as u32))
3025 .collect::<Vec<_>>();
3026 fm.remember_coverage_misses(&misses);
3027 assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
3028
3029 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
3030 fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
3031 .expect("bounded fallback resolves");
3032 }
3033 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
3034 assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
3035 }
3036
3037 #[test]
3038 fn active_document_may_resolve_more_than_256_distinct_faces() {
3039 let source = bundled_font_data()[4].1;
3040 let mut db = fontdb::Database::new();
3041 for index in 0..257 {
3042 db.load_font_data(font_with_family(source, &format!("F{index:06}")));
3043 }
3044 let mut fm = FontManager::from_base_database(db);
3045 fm.begin_layout();
3046 let mut ids = HashSet::new();
3047 for index in 0..257 {
3048 let family = format!("F{index:06}");
3049 let id = fm
3050 .resolve_font(Some(&family), false, false)
3051 .expect("distinct active face resolves");
3052 assert_eq!(fm.font_data(id).unwrap().family, family);
3053 ids.insert(id);
3054 }
3055 assert_eq!(ids.len(), 257);
3056 fm.retain_current_fonts();
3057 assert_eq!(fm.fonts.len(), 257);
3058 }
3059
3060 #[test]
3061 fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
3062 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
3063 fm.begin_layout();
3064 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
3065 fm.resolve_font(Some("Carlito"), false, false).unwrap();
3066 }
3067 assert!(fm.paragraph_font_trace.is_none());
3068
3069 fm.begin_paragraph_font_trace();
3070 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
3071 fm.resolve_font(Some("Carlito"), false, false).unwrap();
3072 }
3073 assert!(fm.finish_paragraph_font_trace().is_none());
3074
3075 fm.begin_layout();
3076 assert_eq!(fm.layout_fonts.capacity(), 0);
3077 fm.begin_paragraph_font_trace();
3078 fm.resolve_font(Some("Carlito"), false, false).unwrap();
3079 let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
3080 assert_eq!(trace.len(), 1);
3081 assert_eq!(trace.capacity(), trace.len());
3082 }
3083
3084 #[cfg(feature = "system-fonts")]
3085 #[test]
3086 fn file_byte_cache_is_bounded_and_recovers_from_poison() {
3087 let cache = Arc::new(Mutex::new(FileFontCache::new()));
3088 {
3089 let mut cache = cache
3090 .lock()
3091 .unwrap_or_else(std::sync::PoisonError::into_inner);
3092 cache.clear();
3093 let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
3094 let returned = cache_file_font_bytes(
3095 &mut cache,
3096 PathBuf::from("oversized-font.ttc"),
3097 Arc::clone(&oversized),
3098 )
3099 .expect("oversized bytes are returned uncached");
3100 assert!(Arc::ptr_eq(&oversized, &returned));
3101 assert!(cache.entries.is_empty());
3102 assert_eq!(cache.bytes, 0);
3103 }
3104
3105 let poison = Arc::clone(&cache);
3106 assert!(
3107 std::thread::spawn(move || {
3108 let cache = poison;
3109 let _guard = cache.lock().unwrap();
3110 panic!("poison file byte cache for recovery coverage");
3111 })
3112 .join()
3113 .is_err()
3114 );
3115
3116 let path = std::env::temp_dir().join(format!(
3117 "rdocx-font-cache-poison-{}-{:?}.ttf",
3118 std::process::id(),
3119 std::thread::current().id()
3120 ));
3121 std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
3122 let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
3123 let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
3124 assert!(Arc::ptr_eq(&first, &second));
3125 std::fs::remove_file(path).expect("remove recovery font");
3126 }
3127
3128 #[cfg(not(feature = "system-fonts"))]
3129 #[test]
3130 fn no_default_features_omits_system_font_discovery() {
3131 let fm = FontManager::new();
3132 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
3133 }
3134
3135 #[test]
3136 fn font_manager_with_no_fonts_returns_an_error() {
3137 let mut fm = FontManager::new_with_fonts(Vec::new());
3138 assert!(matches!(
3139 fm.resolve_font(None, false, false),
3140 Err(LayoutError::FontNotFound(_))
3141 ));
3142 }
3143
3144 #[test]
3145 fn load_system_font() {
3146 let mut fm = FontManager::new();
3147 let result = fm.resolve_font(None, false, false);
3149 if let Ok(id) = result {
3151 assert_eq!(id.0, 0);
3152 }
3153 }
3154
3155 #[test]
3156 fn font_metrics_positive() {
3157 let mut fm = FontManager::new();
3158 if let Ok(id) = fm.resolve_font(None, false, false) {
3159 let metrics = fm.metrics(id, 12.0).unwrap();
3160 assert!(metrics.ascent > 0.0);
3161 assert!(metrics.descent > 0.0);
3162 assert!(metrics.units_per_em > 0);
3163 }
3164 }
3165
3166 #[test]
3167 fn shape_hello_world() {
3168 let mut fm = FontManager::new();
3169 if let Ok(id) = fm.resolve_font(None, false, false) {
3170 let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
3171 assert!(!shaped.glyph_ids.is_empty());
3172 assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
3173 assert!(shaped.width > 0.0);
3174 }
3175 }
3176
3177 #[test]
3178 fn font_caching() {
3179 let mut fm = FontManager::new();
3180 if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
3181 let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
3182 assert_eq!(id1, id2);
3183 }
3184 }
3185
3186 #[test]
3187 fn font_resolution_alias_cache_is_bounded() {
3188 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
3189 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
3190 fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
3191 .expect("fallback font resolves");
3192 }
3193 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
3194 assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
3195 }
3196
3197 #[test]
3198 fn bold_italic_variants() {
3199 let mut fm = FontManager::new();
3200 let regular = fm.resolve_font(None, false, false);
3201 let bold = fm.resolve_font(None, true, false);
3202 if let (Ok(r), Ok(b)) = (regular, bold) {
3203 assert_ne!(r, b);
3205 }
3206 }
3207
3208 #[test]
3211 fn latin_text_resolves_the_same_as_by_name() {
3212 let mut fm = FontManager::new();
3213 let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
3214 return;
3215 };
3216 let for_text = fm
3217 .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
3218 .unwrap();
3219 assert_eq!(by_name, for_text);
3220 }
3221
3222 #[test]
3228 fn text_no_font_can_draw_keeps_the_requested_font() {
3229 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
3230 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
3231 let resolved = fm
3232 .resolve_font_for_text(Some("Carlito"), false, false, "🀄")
3233 .unwrap();
3234 assert_eq!(
3235 primary, resolved,
3236 "with no covering font available the original must be kept"
3237 );
3238 }
3239
3240 #[test]
3242 fn whitespace_does_not_trigger_a_fallback() {
3243 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
3244 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
3245 let idx = fm.index_of(by_name).unwrap();
3246 assert!(
3248 fm.uncovered(idx, "a\u{00a0}b\tc")
3249 .iter()
3250 .all(|c| *c != '\t'),
3251 "control and whitespace characters must be ignored"
3252 );
3253 }
3254
3255 #[test]
3261 fn cjk_text_moves_off_a_latin_font_when_possible() {
3262 let mut fm = FontManager::new();
3263 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
3264 return;
3265 };
3266 let Some(idx) = fm.index_of(latin) else {
3267 return;
3268 };
3269 if fm.uncovered(idx, "这是中文").is_empty() {
3270 return; }
3272 let resolved = fm
3273 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
3274 .unwrap();
3275 if resolved == latin {
3276 return; }
3278 let new_idx = fm.index_of(resolved).unwrap();
3279 assert!(
3280 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
3281 "the replacement must cover more of the text than the original"
3282 );
3283 }
3284
3285 #[test]
3286 fn hangul_and_kana_receive_their_own_script_and_shaper_tag() {
3287 for character in [
3288 '\u{1100}', '\u{1112}', '\u{3131}', '\u{318e}', '\u{a960}', '\u{a97c}', '한', '힣',
3289 '\u{d7b0}', '\u{d7ff}', '\u{ffa0}', '\u{ffdc}',
3290 ] {
3291 assert_eq!(
3292 script_for_char(character),
3293 TextScript::Hangul,
3294 "{character:?} is Hangul"
3295 );
3296 }
3297 for character in [
3298 '\u{3041}', 'こ', 'ン', '\u{30ff}', '\u{31f0}', '\u{31ff}', '\u{ff66}', '\u{ff9f}',
3299 ] {
3300 assert_eq!(
3301 script_for_char(character),
3302 TextScript::Kana,
3303 "{character:?} is Kana"
3304 );
3305 }
3306 assert_eq!(
3307 harfrust_script(TextScript::Hangul),
3308 harfrust::script::HANGUL
3309 );
3310 assert_eq!(
3311 harfrust_script(TextScript::Kana),
3312 harfrust::script::HIRAGANA
3313 );
3314
3315 let previously_mapped = |codepoint: u32| match codepoint {
3319 0x0041..=0x024f | 0x1e00..=0x1eff => Some(TextScript::Latin),
3320 0x0590..=0x05ff => Some(TextScript::Hebrew),
3321 0x0600..=0x06ff | 0x0750..=0x077f | 0x08a0..=0x08ff => Some(TextScript::Arabic),
3322 0x0900..=0x097f | 0xa8e0..=0xa8ff => Some(TextScript::Devanagari),
3323 0x0e00..=0x0e7f => Some(TextScript::Thai),
3324 0x3400..=0x4dbf | 0x4e00..=0x9fff | 0xf900..=0xfaff => Some(TextScript::Han),
3325 _ => None,
3326 };
3327 for codepoint in 0..=0xffff_u32 {
3328 let Some(character) = char::from_u32(codepoint) else {
3329 continue;
3330 };
3331 let Some(expected) = previously_mapped(codepoint) else {
3332 continue;
3333 };
3334 assert_eq!(
3335 script_for_char(character),
3336 expected,
3337 "U+{codepoint:04X} changed script"
3338 );
3339 }
3340 }
3341
3342 #[test]
3343 fn a_mixed_script_string_shapes_every_script_with_a_covering_bundled_face() {
3344 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
3345 for text in ["안녕하세요", "こんにちは", "カタカナ", "שלום"] {
3346 let font_id = fm
3347 .resolve_font_for_text(Some("Carlito"), false, false, text)
3348 .expect("a bundled face covers the fixture repertoire");
3349 let index = fm.index_of(font_id).expect("resolved font is indexed");
3350 assert!(
3351 fm.uncovered(index, text).is_empty(),
3352 "no bundled face covers {text}"
3353 );
3354 }
3355 }
3356}