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
14use std::sync::atomic::{AtomicUsize, Ordering};
15
16use crate::error::{LayoutError, Result};
17use crate::line::TextSegment;
18use crate::output::{Diagnostic, FontId, SourceSpan};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct FontFile {
23 pub family: String,
25 pub data: Vec<u8>,
27}
28
29impl FontFile {
30 pub fn from_dir(path: &std::path::Path) -> Result<Vec<Self>> {
34 let failed = |error: std::io::Error| {
35 LayoutError::FontParse(format!("font directory {}: {error}", path.display()))
36 };
37 if !std::fs::metadata(path).map_err(failed)?.is_dir() {
38 return Err(LayoutError::FontParse(format!(
39 "font directory {} is not a directory",
40 path.display()
41 )));
42 }
43 let mut paths = std::fs::read_dir(path)
44 .map_err(failed)?
45 .map(|entry| entry.map(|entry| entry.path()).map_err(failed))
46 .collect::<Result<Vec<_>>>()?;
47 paths.sort();
48 let mut fonts = Vec::new();
49 for entry in paths {
50 let supported = entry
51 .extension()
52 .and_then(|ext| ext.to_str())
53 .is_some_and(|ext| {
54 ["ttf", "otf", "ttc"]
55 .iter()
56 .any(|kind| ext.eq_ignore_ascii_case(kind))
57 });
58 if !supported {
59 continue;
60 }
61 let data = std::fs::read(&entry).map_err(|error| {
62 LayoutError::FontParse(format!("font file {}: {error}", entry.display()))
63 })?;
64 ttf_parser::Face::parse(&data, 0).map_err(|error| {
65 LayoutError::FontParse(format!("font file {}: {error}", entry.display()))
66 })?;
67 let family = entry
68 .file_stem()
69 .and_then(|stem| stem.to_str())
70 .ok_or_else(|| {
71 LayoutError::FontParse(format!(
72 "font filename {} is not UTF-8",
73 entry.display()
74 ))
75 })?
76 .to_owned();
77 fonts.push(Self { family, data });
78 }
79 Ok(fonts)
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85struct FontKey {
86 family: String,
87 bold: bool,
88 italic: bool,
89}
90
91#[derive(Debug, Clone, Copy)]
93pub struct FontMetrics {
94 pub ascent: f64,
96 pub descent: f64,
98 pub line_gap: f64,
100 pub units_per_em: u16,
102}
103
104#[derive(Debug, Clone)]
106pub struct ShapedText {
107 pub glyph_ids: Vec<u16>,
109 pub advances: Vec<f64>,
111 pub width: f64,
113}
114
115#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
117pub enum TextDirection {
118 #[default]
119 Auto,
120 LeftToRight,
121 RightToLeft,
122}
123
124pub(crate) fn directional_level(
125 direction: TextDirection,
126 paragraph_level: unicode_bidi::Level,
127) -> unicode_bidi::Level {
128 let paragraph_number = paragraph_level.number();
129 let number = match direction {
130 TextDirection::Auto => paragraph_number,
131 TextDirection::LeftToRight if paragraph_level.is_rtl() => paragraph_number + 1,
132 TextDirection::LeftToRight => paragraph_number,
133 TextDirection::RightToLeft if paragraph_level.is_rtl() => paragraph_number,
134 TextDirection::RightToLeft => paragraph_number + 1,
135 };
136 unicode_bidi::Level::new(number).expect("one directional embedding fits the bidi level limit")
137}
138
139pub(crate) fn explicit_direction_levels(
140 text: &str,
141 direction: TextDirection,
142 paragraph_level: unicode_bidi::Level,
143) -> Result<Vec<unicode_bidi::Level>> {
144 let local_base = match direction {
145 TextDirection::Auto => paragraph_level,
146 TextDirection::LeftToRight => unicode_bidi::Level::ltr(),
147 TextDirection::RightToLeft => unicode_bidi::Level::rtl(),
148 };
149 let target_base = directional_level(direction, paragraph_level);
150 let offset = target_base.number() - local_base.number();
151 unicode_bidi::BidiInfo::new(text, Some(local_base))
152 .levels
153 .into_iter()
154 .map(|level| {
155 level
156 .number()
157 .checked_add(offset)
158 .and_then(|number| unicode_bidi::Level::new(number).ok())
159 .ok_or_else(|| {
160 LayoutError::Layout(
161 "run direction exceeded the Unicode bidi level limit".to_owned(),
162 )
163 })
164 })
165 .collect()
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174#[non_exhaustive]
175pub enum TextScript {
176 Latin,
177 Arabic,
178 Hebrew,
179 Devanagari,
180 Thai,
181 Han,
182 Hangul,
184 Kana,
186 Common,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub struct GlyphCluster {
192 pub glyph_start: u32,
193 pub glyph_end: u32,
194 pub char_start: u32,
195 pub char_end: u32,
196}
197
198impl GlyphCluster {
199 pub fn is_valid(&self) -> bool {
200 self.glyph_start < self.glyph_end && self.char_start < self.char_end
201 }
202
203 pub fn char_range(&self) -> Range<u32> {
204 self.char_start..self.char_end
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct MultilingualTextSegment {
211 base: TextSegment,
212 logical_index: usize,
213 language: Option<String>,
214 script: TextScript,
215 direction: TextDirection,
216 bidi_level: u8,
217 x_advances: Vec<f64>,
218 y_advances: Vec<f64>,
219 x_offsets: Vec<f64>,
220 y_offsets: Vec<f64>,
221 clusters: Vec<GlyphCluster>,
222 break_after: bool,
223}
224
225impl MultilingualTextSegment {
226 #[allow(clippy::too_many_arguments)]
227 pub fn new(
228 base: TextSegment,
229 logical_index: usize,
230 language: Option<String>,
231 script: TextScript,
232 direction: TextDirection,
233 bidi_level: u8,
234 x_advances: Vec<f64>,
235 y_advances: Vec<f64>,
236 x_offsets: Vec<f64>,
237 y_offsets: Vec<f64>,
238 clusters: Vec<GlyphCluster>,
239 break_after: bool,
240 ) -> Result<Self> {
241 let glyph_count = base.glyph_ids.len();
242 let char_count = base.text.chars().count();
243 let valid_level = unicode_bidi::Level::new(bidi_level).is_ok();
244 if x_advances.len() != glyph_count
245 || y_advances.len() != glyph_count
246 || x_offsets.len() != glyph_count
247 || y_offsets.len() != glyph_count
248 || base.advances.len() != glyph_count
249 || !valid_level
250 || !position_values_are_finite(
251 &x_advances,
252 &y_advances,
253 &x_offsets,
254 &y_offsets,
255 base.width,
256 )
257 || !cluster_ranges_are_valid(&clusters, glyph_count, char_count, bidi_level % 2 == 1)
258 {
259 return Err(LayoutError::Layout(
260 "invalid multilingual glyph positioning or cluster range".to_owned(),
261 ));
262 }
263 Ok(Self {
264 base,
265 logical_index,
266 language,
267 script,
268 direction,
269 bidi_level,
270 x_advances,
271 y_advances,
272 x_offsets,
273 y_offsets,
274 clusters,
275 break_after,
276 })
277 }
278
279 pub fn text(&self) -> &str {
280 &self.base.text
281 }
282
283 pub fn base(&self) -> &TextSegment {
284 &self.base
285 }
286
287 pub fn font_id(&self) -> FontId {
288 self.base.font_id
289 }
290
291 pub fn language(&self) -> Option<&str> {
292 self.language.as_deref()
293 }
294
295 pub fn script(&self) -> TextScript {
296 self.script
297 }
298
299 pub fn direction(&self) -> TextDirection {
300 self.direction
301 }
302
303 pub fn bidi_level(&self) -> u8 {
304 self.bidi_level
305 }
306
307 pub fn logical_index(&self) -> usize {
308 self.logical_index
309 }
310
311 pub fn glyph_ids(&self) -> &[u16] {
312 &self.base.glyph_ids
313 }
314
315 pub fn x_advances(&self) -> &[f64] {
316 &self.x_advances
317 }
318
319 pub fn y_advances(&self) -> &[f64] {
320 &self.y_advances
321 }
322
323 pub fn x_offsets(&self) -> &[f64] {
324 &self.x_offsets
325 }
326
327 pub fn y_offsets(&self) -> &[f64] {
328 &self.y_offsets
329 }
330
331 pub fn clusters(&self) -> &[GlyphCluster] {
332 &self.clusters
333 }
334
335 pub fn width(&self) -> f64 {
336 self.base.width
337 }
338
339 pub fn break_after(&self) -> bool {
340 self.break_after
341 }
342}
343
344pub(crate) fn position_values_are_finite(
345 x_advances: &[f64],
346 y_advances: &[f64],
347 x_offsets: &[f64],
348 y_offsets: &[f64],
349 width: f64,
350) -> bool {
351 width.is_finite()
352 && x_advances
353 .iter()
354 .chain(y_advances)
355 .chain(x_offsets)
356 .chain(y_offsets)
357 .all(|value| value.is_finite())
358}
359
360pub(crate) fn cluster_ranges_are_valid(
361 clusters: &[GlyphCluster],
362 glyph_count: usize,
363 char_count: usize,
364 right_to_left: bool,
365) -> bool {
366 if glyph_count == 0 || char_count == 0 {
367 return glyph_count == 0 && char_count == 0 && clusters.is_empty();
368 }
369 let mut previous_glyph_end = 0u32;
370 let mut previous_char_range = None::<Range<u32>>;
371 let mut covered_chars = 0usize;
372 for cluster in clusters {
373 if !cluster.is_valid()
374 || cluster.glyph_start != previous_glyph_end
375 || cluster.glyph_end as usize > glyph_count
376 || cluster.char_end as usize > char_count
377 {
378 return false;
379 }
380 if let Some(previous) = previous_char_range {
381 let contiguous = if right_to_left {
382 cluster.char_end == previous.start
383 } else {
384 cluster.char_start == previous.end
385 };
386 if !contiguous {
387 return false;
388 }
389 }
390 previous_glyph_end = cluster.glyph_end;
391 previous_char_range = Some(cluster.char_range());
392 covered_chars += (cluster.char_end - cluster.char_start) as usize;
393 }
394 previous_glyph_end as usize == glyph_count
395 && covered_chars == char_count
396 && previous_char_range.is_some_and(|last| {
397 if right_to_left {
398 clusters
399 .first()
400 .is_some_and(|first| first.char_end as usize == char_count)
401 && last.start == 0
402 } else {
403 clusters.first().is_some_and(|first| first.char_start == 0)
404 && last.end as usize == char_count
405 }
406 })
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
410struct ShapingKey {
411 font_id: FontId,
412 text: String,
413 size_bits: u64,
414}
415
416struct ShapingMemo {
417 entries: VecDeque<(ShapingKey, ShapedText, usize, u64)>,
418 bytes: usize,
419 #[cfg(test)]
420 hits: usize,
421 #[cfg(test)]
422 misses: usize,
423}
424
425impl ShapingMemo {
426 fn new() -> Self {
427 Self {
428 entries: VecDeque::new(),
429 bytes: 0,
430 #[cfg(test)]
431 hits: 0,
432 #[cfg(test)]
433 misses: 0,
434 }
435 }
436
437 fn clear(&mut self) {
438 self.entries.clear();
439 self.bytes = 0;
440 #[cfg(test)]
441 {
442 self.hits = 0;
443 self.misses = 0;
444 }
445 }
446
447 fn insert(&mut self, key: ShapingKey, shaped: ShapedText) {
448 let entry_bytes = std::mem::size_of::<(ShapingKey, ShapedText, usize, u64)>()
449 + key.text.len()
450 + shaped.glyph_ids.len() * std::mem::size_of::<u16>()
451 + shaped.advances.len() * std::mem::size_of::<f64>();
452 if entry_bytes > SHAPING_CACHE_MAX_BYTES {
453 return;
454 }
455 while self.entries.len() >= SHAPING_CACHE_MAX_ENTRIES
456 || self.bytes.saturating_add(entry_bytes) > SHAPING_CACHE_MAX_BYTES
457 {
458 let Some((_, _, evicted_bytes, _)) = self.entries.pop_front() else {
459 break;
460 };
461 self.bytes = self.bytes.saturating_sub(evicted_bytes);
462 }
463 self.bytes += entry_bytes;
464 let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
465 self.entries
466 .push_back((key, shaped, entry_bytes, fingerprint));
467 }
468}
469
470fn shaping_fingerprint(font_id: FontId, text: &str, size_bits: u64) -> u64 {
471 use std::hash::{Hash, Hasher};
472
473 let mut hasher = std::collections::hash_map::DefaultHasher::new();
474 font_id.hash(&mut hasher);
475 text.hash(&mut hasher);
476 size_bits.hash(&mut hasher);
477 hasher.finish()
478}
479
480const SHAPING_CACHE_MAX_ENTRIES: usize = 2_048;
481const SHAPING_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
482
483#[cfg(feature = "system-fonts")]
484struct FileFontCache {
485 entries: VecDeque<(PathBuf, Arc<[u8]>, usize)>,
486 bytes: usize,
487}
488
489#[cfg(feature = "system-fonts")]
490impl FileFontCache {
491 fn new() -> Self {
492 Self {
493 entries: VecDeque::new(),
494 bytes: 0,
495 }
496 }
497
498 fn clear(&mut self) {
499 self.entries.clear();
500 self.bytes = 0;
501 }
502}
503
504#[cfg(feature = "system-fonts")]
505const FILE_FONT_CACHE_MAX_ENTRIES: usize = 256;
506#[cfg(feature = "system-fonts")]
507const FILE_FONT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
508
509#[cfg(feature = "system-fonts")]
510static NORMAL_FONT_DATABASE: OnceLock<fontdb::Database> = OnceLock::new();
511#[cfg(feature = "system-fonts")]
512static FILE_FONT_CACHE: OnceLock<Mutex<FileFontCache>> = OnceLock::new();
513#[cfg(all(test, feature = "system-fonts"))]
514static SYSTEM_FONT_DISCOVERY_RUNS: AtomicUsize = AtomicUsize::new(0);
515
516struct LoadedFont {
518 db_id: fontdb::ID,
519 id: FontId,
520 family: String,
521 bold: bool,
522 italic: bool,
523 data: Arc<[u8]>,
524 face_index: u32,
525 units_per_em: u16,
526 ascender: i16,
528 descender: i16,
529 line_gap: i16,
530 word_line: Option<WordLineUnits>,
534 shaper_data: harfrust::ShaperData,
537 variations: Vec<([u8; 4], f32)>,
538 shaper_instance: Option<harfrust::ShaperInstance>,
539 synthetic_bold: bool,
540 synthetic_italic: bool,
541}
542
543fn face_style_adjustment(
546 face: &ttf_parser::Face<'_>,
547 bold: bool,
548 italic: bool,
549) -> (Vec<([u8; 4], f32)>, bool, bool) {
550 let axis = |tag: &[u8; 4]| {
551 face.variation_axes()
552 .into_iter()
553 .find(|axis| axis.tag == ttf_parser::Tag::from_bytes(tag))
554 };
555 let mut variations = Vec::new();
556 let mut select = |tag, axis: ttf_parser::VariationAxis, value: f32| {
557 let selected = value.clamp(axis.min_value, axis.max_value);
558 if selected != axis.def_value {
559 variations.push((tag, selected));
560 }
561 selected
562 };
563 let genuine_bold = if bold {
564 match axis(b"wght") {
565 Some(weight) => select(*b"wght", weight, 700.0) >= 700.0,
566 None => face.is_bold() || face.weight().to_number() >= 700,
567 }
568 } else {
569 false
570 };
571 let mut genuine_italic = face.style() != ttf_parser::Style::Normal;
572 if italic && !genuine_italic {
573 if let Some(axis) =
574 axis(b"ital").filter(|axis| axis.min_value <= 1.0 && axis.max_value >= 1.0)
575 {
576 select(*b"ital", axis, 1.0);
577 genuine_italic = true;
578 } else if let Some(axis) = axis(b"slnt").filter(|axis| axis.min_value < 0.0) {
579 genuine_italic = select(*b"slnt", axis, -12.0) < 0.0;
580 }
581 }
582 (variations, bold && !genuine_bold, italic && !genuine_italic)
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
587struct WordLineUnits {
588 ascent: i32,
590 descent: i32,
592 leading: i32,
594}
595
596impl WordLineUnits {
597 fn of(face: &ttf_parser::Face<'_>) -> Option<Self> {
600 let os2 = face.tables().os2?;
601 if os2.use_typographic_metrics() {
602 return Some(Self {
603 ascent: i32::from(os2.typographic_ascender()),
604 descent: -i32::from(os2.typographic_descender()),
605 leading: i32::from(os2.typographic_line_gap()).max(0),
606 });
607 }
608 let table = face
612 .raw_face()
613 .table(ttf_parser::Tag::from_bytes(b"OS/2"))?;
614 let unsigned = |offset: usize| {
615 table
616 .get(offset..offset + 2)
617 .map(|bytes| i32::from(u16::from_be_bytes([bytes[0], bytes[1]])))
618 };
619 let ascent = unsigned(74)?;
620 let descent = unsigned(76)?;
621 if ascent + descent == 0 {
622 return None;
623 }
624 let hhea = face.tables().hhea;
625 let hhea_extent = i32::from(hhea.ascender) - i32::from(hhea.descender);
626 Some(Self {
627 ascent,
628 descent,
629 leading: (i32::from(hhea.line_gap) - (ascent + descent - hhea_extent)).max(0),
630 })
631 }
632}
633
634#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct FontContribution {
637 pub font_id: FontId,
638 pub requested_family: Option<String>,
639}
640
641#[derive(Debug)]
643pub struct FontContributionScope {
644 generation: u64,
645 event_start: usize,
646 depth: usize,
647 owns_trace: bool,
648 diagnostic_start: usize,
649}
650
651#[derive(Debug, Clone, PartialEq, Eq)]
652pub enum FontTraceEvent {
653 Resolution(FontId),
654 Contribution(FontContribution),
655}
656
657struct ParagraphFontTrace {
658 events: Vec<FontTraceEvent>,
659 generation: u64,
660 scope_depth: usize,
661 owned_family_bytes: usize,
662 overflowed: bool,
663}
664
665pub struct FontManager {
667 db: fontdb::Database,
668 base_db: fontdb::Database,
670 base_caller_aliases: HashMap<String, Vec<fontdb::ID>>,
672 base_caller_families: HashMap<String, Vec<fontdb::ID>>,
674 cache: HashMap<FontKey, usize>,
676 memory_face_data: HashMap<fontdb::ID, Arc<[u8]>>,
678 fonts: Vec<LoadedFont>,
680 next_id: u32,
682 coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
690 coverage_misses: HashSet<char>,
693 additional_fonts: Vec<FontFile>,
695 embedded_fonts: Vec<(Vec<String>, Arc<[u8]>)>,
697 embedded_aliases: HashMap<String, Vec<fontdb::ID>>,
699 caller_aliases: HashMap<String, Vec<fontdb::ID>>,
702 caller_families: HashMap<String, Vec<fontdb::ID>>,
705 explicit_aliases: Vec<(String, String)>,
707 explicit_alias_map: HashMap<String, String>,
709 shaping_memo: Mutex<ShapingMemo>,
711 paragraph_font_trace: Option<ParagraphFontTrace>,
713 layout_fonts: Vec<FontId>,
715 layout_font_diagnostics: Vec<Diagnostic>,
718}
719
720const BROAD_COVERAGE_FAMILIES: &[&str] = &[
725 "Noto Sans Arabic",
727 "Noto Sans Devanagari",
728 "Noto Sans Thai",
729 "Noto Sans SC",
730 "Noto Sans CJK SC",
732 "Noto Sans CJK JP",
733 "Noto Sans CJK KR",
734 "Noto Sans CJK TC",
735 "Noto Serif CJK SC",
736 "Source Han Sans SC",
737 "WenQuanYi Zen Hei",
738 "WenQuanYi Micro Hei",
739 "PingFang SC",
741 "PingFang TC",
742 "Hiragino Sans",
743 "Hiragino Kaku Gothic ProN",
744 "Apple SD Gothic Neo",
745 "Songti SC",
746 "STHeiti",
747 "Microsoft YaHei",
749 "Microsoft JhengHei",
750 "SimSun",
751 "SimHei",
752 "NSimSun",
753 "Yu Gothic",
754 "MS Gothic",
755 "Meiryo",
756 "Malgun Gothic",
757 "Arial Unicode MS",
759 "DejaVu Sans",
760];
761
762const RESOLUTION_CACHE_MAX_ENTRIES: usize = 256;
763const COVERAGE_FALLBACK_MAX_ENTRIES: usize = 256;
764const COVERAGE_MISS_MAX_ENTRIES: usize = 4_096;
765const PARAGRAPH_FONT_TRACE_MAX_ENTRIES: usize = 4_096;
766const PARAGRAPH_FONT_TRACE_MAX_BYTES: usize = SHAPING_CACHE_MAX_BYTES;
767static NEXT_FONT_TRACE_GENERATION: AtomicUsize = AtomicUsize::new(1);
768
769const CALLER_ALIAS_MAX_ENTRIES: usize = 256;
771
772const CALLER_ALIAS_MAX_RETAINED_BYTES: usize = 64 * 1024;
775
776fn bounded_caller_aliases(aliases: &[(String, String)]) -> Vec<(String, String)> {
783 let mut bounded = Vec::with_capacity(aliases.len().min(CALLER_ALIAS_MAX_ENTRIES));
784 let mut retained_bytes = 0usize;
785 for (requested, target) in aliases {
786 if bounded.len() == CALLER_ALIAS_MAX_ENTRIES {
787 break;
788 }
789 let normalized_requested = requested.to_lowercase();
790 let entry_bytes = requested
791 .len()
792 .saturating_add(target.len())
793 .saturating_add(normalized_requested.len())
794 .saturating_add(target.len());
795 let next_retained_bytes = retained_bytes.saturating_add(entry_bytes);
796 if next_retained_bytes > CALLER_ALIAS_MAX_RETAINED_BYTES {
797 break;
798 }
799 bounded.push((requested.to_owned(), target.to_owned()));
800 retained_bytes = next_retained_bytes;
801 }
802 bounded
803}
804
805impl Default for FontManager {
806 fn default() -> Self {
807 Self::new()
808 }
809}
810
811impl FontManager {
812 fn from_base_database(db: fontdb::Database) -> Self {
813 Self {
814 base_db: db.clone(),
815 base_caller_aliases: HashMap::new(),
816 base_caller_families: HashMap::new(),
817 db,
818 cache: HashMap::new(),
819 memory_face_data: HashMap::new(),
820 fonts: Vec::new(),
821 next_id: 0,
822 coverage_fallbacks: HashMap::new(),
823 coverage_misses: HashSet::new(),
824 additional_fonts: Vec::new(),
825 embedded_fonts: Vec::new(),
826 embedded_aliases: HashMap::new(),
827 caller_aliases: HashMap::new(),
828 caller_families: HashMap::new(),
829 explicit_aliases: Vec::new(),
830 explicit_alias_map: HashMap::new(),
831 shaping_memo: Mutex::new(ShapingMemo::new()),
832 paragraph_font_trace: None,
833 layout_fonts: Vec::new(),
834 layout_font_diagnostics: Vec::new(),
835 }
836 }
837
838 pub fn new() -> Self {
843 #[cfg(feature = "system-fonts")]
844 {
845 let db = NORMAL_FONT_DATABASE.get_or_init(|| {
846 let mut db = bundled_font_database();
847 db.load_system_fonts();
848 #[cfg(test)]
849 SYSTEM_FONT_DISCOVERY_RUNS.fetch_add(1, Ordering::Relaxed);
850 db
851 });
852 Self::from_base_database(db.clone())
853 }
854
855 #[cfg(not(feature = "system-fonts"))]
856 Self::from_base_database(bundled_font_database())
857 }
858
859 pub fn new_deterministic() -> Result<Self> {
864 Ok(Self::from_base_database(bundled_font_database()))
865 }
866
867 pub fn load_additional_fonts(&mut self, font_files: &[FontFile]) -> bool {
874 if self.additional_fonts == font_files {
875 return false;
876 }
877 let embedded = self.embedded_fonts.clone();
878 self.rebuild_font_sources(&embedded, font_files).is_ok()
881 }
882
883 #[doc(hidden)]
886 pub fn load_embedded_fonts(&mut self, sources: &[(Vec<String>, Arc<[u8]>)]) -> Result<bool> {
887 if self.embedded_fonts == sources {
888 return Ok(false);
889 }
890 let callers = self.additional_fonts.clone();
891 self.rebuild_font_sources(sources, &callers)?;
892 Ok(true)
893 }
894
895 fn rebuild_font_sources(
896 &mut self,
897 embedded_fonts: &[(Vec<String>, Arc<[u8]>)],
898 font_files: &[FontFile],
899 ) -> Result<()> {
900 const FACE_LIMIT: usize = 64 * 1024 * 1024;
901 const TOTAL_LIMIT: usize = 256 * 1024 * 1024;
902 const ALIAS_LIMIT: usize = 16_384;
903 const ALIAS_BYTES_LIMIT: usize = 4 * 1024 * 1024;
904 let refused = || {
905 LayoutError::FontParse(
906 "embedded font sources exceed the data or alias limits".to_owned(),
907 )
908 };
909 if embedded_fonts.len() > 4096 {
910 return Err(refused());
911 }
912 let mut total = 0usize;
913 let mut aliases = 0usize;
914 let mut alias_bytes = 0usize;
915 for (names, data) in embedded_fonts {
916 total = total.checked_add(data.len()).ok_or_else(refused)?;
917 aliases = aliases.checked_add(names.len()).ok_or_else(refused)?;
918 if data.len() > FACE_LIMIT
919 || total > TOTAL_LIMIT
920 || aliases > ALIAS_LIMIT
921 || names.is_empty()
922 {
923 return Err(refused());
924 }
925 for name in names {
926 alias_bytes = alias_bytes
927 .checked_add(name.len().saturating_mul(4))
928 .ok_or_else(refused)?;
929 if name.is_empty() || name.len() > 4096 || alias_bytes > ALIAS_BYTES_LIMIT {
930 return Err(refused());
931 }
932 }
933 }
934 let mut face_count = 0usize;
935 let mut alias_references = 0usize;
936 let mut font_name_bytes = 0usize;
937 for (names, data) in embedded_fonts {
938 let count = ttf_parser::fonts_in_collection(data).unwrap_or(1);
939 if count == 0 || count > 256 {
940 return Err(refused());
941 }
942 face_count = face_count.checked_add(count as usize).ok_or_else(refused)?;
943 alias_references = alias_references
944 .checked_add(
945 names
946 .len()
947 .checked_mul(count as usize)
948 .ok_or_else(refused)?,
949 )
950 .ok_or_else(refused)?;
951 if face_count > 4096 || alias_references > 262_144 {
952 return Err(refused());
953 }
954 for index in 0..count {
955 let face = ttf_parser::Face::parse(data, index).map_err(|error| {
956 LayoutError::FontParse(format!("embedded font face: {error:?}"))
957 })?;
958 if face.names().len() > 1024 {
959 return Err(refused());
960 }
961 for name in face.names() {
962 font_name_bytes = font_name_bytes
963 .checked_add(name.name.len())
964 .ok_or_else(refused)?;
965 if font_name_bytes > ALIAS_BYTES_LIMIT {
966 return Err(refused());
967 }
968 }
969 }
970 }
971 let mut db = self.base_db.clone();
972 let mut embedded_aliases: HashMap<String, Vec<fontdb::ID>> = HashMap::new();
973 for (names, data) in embedded_fonts {
974 let before = db.len();
975 let mut body = Vec::new();
976 body.try_reserve_exact(data.len()).map_err(|error| {
977 LayoutError::FontParse(format!("embedded font allocation: {error}"))
978 })?;
979 body.extend_from_slice(data);
980 db.load_font_data(body);
981 let ids = db
982 .faces()
983 .skip(before)
984 .map(|face| face.id)
985 .collect::<Vec<_>>();
986 if ids.is_empty() {
987 return Err(LayoutError::FontParse(
988 "embedded font contains no supported faces".to_owned(),
989 ));
990 }
991 for name in names {
992 embedded_aliases
993 .entry(name.to_lowercase())
994 .or_default()
995 .extend(ids.iter().copied());
996 }
997 }
998 let mut caller_aliases = self.base_caller_aliases.clone();
999 let mut caller_families = self.base_caller_families.clone();
1000 for font_file in font_files {
1001 Self::load_caller_font(
1002 &mut db,
1003 &mut caller_aliases,
1004 &mut caller_families,
1005 &font_file.family,
1006 font_file.data.clone(),
1007 );
1008 }
1009 for (active, base) in [
1014 (&mut caller_aliases, &self.base_caller_aliases),
1015 (&mut caller_families, &self.base_caller_families),
1016 ] {
1017 for (family, base_ids) in base {
1018 if let Some(ids) = active.get_mut(family) {
1019 ids[..base_ids.len()].reverse();
1020 ids.rotate_left(base_ids.len());
1021 }
1022 }
1023 }
1024
1025 self.db = db;
1026 self.caller_aliases = caller_aliases;
1027 self.caller_families = caller_families;
1028 self.embedded_aliases = embedded_aliases;
1029 self.cache.clear();
1030 self.memory_face_data.clear();
1031 self.fonts.clear();
1032 self.next_id = 0;
1033 self.coverage_fallbacks.clear();
1034 self.coverage_misses.clear();
1035 self.additional_fonts = font_files.to_vec();
1036 self.embedded_fonts = embedded_fonts.to_vec();
1037 if self.shaping_memo.is_poisoned() {
1038 self.shaping_memo.clear_poison();
1039 }
1040 self.shaping_memo
1041 .get_mut()
1042 .expect("shaping cache poison was cleared")
1043 .clear();
1044 Ok(())
1045 }
1046
1047 pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
1052 let mut db = fontdb::Database::new();
1053 let mut caller_aliases = HashMap::new();
1054 let mut caller_families = HashMap::new();
1055 for (family, data) in &fonts {
1056 Self::load_caller_font(
1057 &mut db,
1058 &mut caller_aliases,
1059 &mut caller_families,
1060 family,
1061 data.clone(),
1062 );
1063 }
1064 let mut manager = Self::from_base_database(db);
1065 manager.base_caller_aliases = caller_aliases.clone();
1066 manager.base_caller_families = caller_families.clone();
1067 manager.caller_aliases = caller_aliases;
1068 manager.caller_families = caller_families;
1069 for active in [&mut manager.caller_aliases, &mut manager.caller_families] {
1071 for ids in active.values_mut() {
1072 ids.reverse();
1073 }
1074 }
1075 manager
1076 }
1077
1078 pub fn set_caller_aliases(&mut self, aliases: &[(String, String)]) -> bool {
1084 let aliases = bounded_caller_aliases(aliases);
1085 if self.explicit_aliases == aliases {
1086 return false;
1087 }
1088
1089 self.explicit_alias_map = aliases
1090 .iter()
1091 .map(|(requested, target)| (requested.to_lowercase(), target.clone()))
1092 .collect();
1093 self.explicit_aliases = aliases;
1094 self.cache.clear();
1095 self.coverage_fallbacks.clear();
1096 self.coverage_misses.clear();
1097 true
1098 }
1099
1100 fn load_caller_font(
1101 db: &mut fontdb::Database,
1102 aliases: &mut HashMap<String, Vec<fontdb::ID>>,
1103 families: &mut HashMap<String, Vec<fontdb::ID>>,
1104 family: &str,
1105 data: Vec<u8>,
1106 ) {
1107 let before = db.len();
1108 db.load_font_data(data);
1109 let loaded_faces = db
1110 .faces()
1111 .skip(before)
1112 .map(|face| {
1113 (
1114 face.id,
1115 face.families
1116 .iter()
1117 .map(|(name, _)| name.clone())
1118 .collect::<Vec<_>>(),
1119 )
1120 })
1121 .collect::<Vec<_>>();
1122 for (id, loaded_families) in &loaded_faces {
1123 for loaded_family in loaded_families {
1124 families.entry(loaded_family.clone()).or_default().push(*id);
1125 }
1126 }
1127 if !loaded_faces.is_empty()
1128 && loaded_faces
1129 .iter()
1130 .all(|(_, families)| !families.iter().any(|loaded| loaded == family))
1131 {
1132 aliases
1133 .entry(family.to_lowercase())
1134 .or_default()
1135 .extend(loaded_faces.into_iter().map(|(id, _)| id));
1136 }
1137 }
1138
1139 #[doc(hidden)]
1141 pub fn begin_layout(&mut self) {
1142 self.paragraph_font_trace = None;
1143 self.layout_fonts = Vec::new();
1144 self.layout_font_diagnostics = Vec::new();
1145 }
1146
1147 #[doc(hidden)]
1149 pub fn begin_paragraph_font_trace(&mut self) {
1150 let Ok(generation) = NEXT_FONT_TRACE_GENERATION.fetch_update(
1151 Ordering::Relaxed,
1152 Ordering::Relaxed,
1153 |generation| generation.checked_add(1),
1154 ) else {
1155 self.paragraph_font_trace = None;
1156 return;
1157 };
1158 let generation = generation as u64;
1159 self.paragraph_font_trace = Some(ParagraphFontTrace {
1160 events: Vec::new(),
1161 generation,
1162 scope_depth: 0,
1163 owned_family_bytes: 0,
1164 overflowed: false,
1165 });
1166 }
1167
1168 #[doc(hidden)]
1170 pub fn finish_paragraph_font_trace(&mut self) -> Option<Vec<FontId>> {
1171 if self.paragraph_font_trace.as_ref()?.scope_depth != 0 {
1172 return None;
1173 }
1174 let trace = self.paragraph_font_trace.take()?;
1175 if trace.overflowed {
1176 return None;
1177 }
1178 let mut ids = trace
1179 .events
1180 .into_iter()
1181 .filter_map(|event| match event {
1182 FontTraceEvent::Resolution(id) => Some(id),
1183 FontTraceEvent::Contribution(_) => None,
1184 })
1185 .collect::<Vec<_>>();
1186 ids.shrink_to_fit();
1187 Some(ids)
1188 }
1189
1190 #[doc(hidden)]
1192 pub fn finish_paragraph_font_contributions(&mut self) -> Option<Vec<FontContribution>> {
1193 if self.paragraph_font_trace.as_ref()?.scope_depth != 0 {
1194 return None;
1195 }
1196 let trace = self.paragraph_font_trace.take()?;
1197 if trace.overflowed {
1198 return None;
1199 }
1200 let mut contributions = trace
1201 .events
1202 .into_iter()
1203 .filter_map(|event| match event {
1204 FontTraceEvent::Contribution(contribution) => Some(contribution),
1205 FontTraceEvent::Resolution(_) => None,
1206 })
1207 .collect::<Vec<_>>();
1208 contributions.shrink_to_fit();
1209 Some(contributions)
1210 }
1211
1212 #[doc(hidden)]
1214 pub fn finish_paragraph_font_events(&mut self) -> Option<Vec<FontTraceEvent>> {
1215 if self.paragraph_font_trace.as_ref()?.scope_depth != 0 {
1216 return None;
1217 }
1218 let mut trace = self.paragraph_font_trace.take()?;
1219 if trace.overflowed {
1220 return None;
1221 }
1222 trace.events.shrink_to_fit();
1223 Some(trace.events)
1224 }
1225
1226 #[doc(hidden)]
1228 pub fn begin_font_contribution_scope(&mut self) -> Option<FontContributionScope> {
1229 let owns_trace = self.paragraph_font_trace.is_none();
1230 if owns_trace {
1231 self.begin_paragraph_font_trace();
1232 }
1233 let trace = self.paragraph_font_trace.as_mut()?;
1234 if trace.overflowed {
1235 return None;
1236 }
1237 let depth = trace.scope_depth.checked_add(1)?;
1238 trace.scope_depth = depth;
1239 Some(FontContributionScope {
1240 generation: trace.generation,
1241 event_start: trace.events.len(),
1242 depth,
1243 owns_trace,
1244 diagnostic_start: self.layout_font_diagnostics.len(),
1245 })
1246 }
1247
1248 #[doc(hidden)]
1250 pub fn finish_font_contribution_scope(
1251 &mut self,
1252 scope: FontContributionScope,
1253 ) -> Result<Option<Vec<FontTraceEvent>>> {
1254 let trace = self.paragraph_font_trace.as_ref().ok_or_else(|| {
1255 LayoutError::Layout("font contribution scope no longer active".into())
1256 })?;
1257 if trace.generation != scope.generation
1258 || trace.scope_depth != scope.depth
1259 || (!trace.overflowed && scope.event_start > trace.events.len())
1260 {
1261 return Err(LayoutError::Layout(
1262 "stale or nested font contribution scope".into(),
1263 ));
1264 }
1265 if trace.overflowed {
1266 self.layout_font_diagnostics
1267 .truncate(scope.diagnostic_start);
1268 if scope.owns_trace {
1269 self.paragraph_font_trace = None;
1270 } else {
1271 self.paragraph_font_trace.as_mut().unwrap().scope_depth -= 1;
1272 }
1273 return Ok(None);
1274 }
1275 let staged = &trace.events[scope.event_start..];
1276 let bytes = staged.iter().fold(
1277 staged
1278 .len()
1279 .checked_mul(std::mem::size_of::<FontTraceEvent>()),
1280 |total, event| {
1281 total.and_then(|total| {
1282 total.checked_add(match event {
1283 FontTraceEvent::Resolution(_) => 0,
1284 FontTraceEvent::Contribution(contribution) => contribution
1285 .requested_family
1286 .as_ref()
1287 .map_or(0, String::capacity),
1288 })
1289 })
1290 },
1291 );
1292 if bytes.is_none_or(|bytes| bytes > PARAGRAPH_FONT_TRACE_MAX_BYTES) {
1293 return Err(LayoutError::Layout(
1294 "font trace capture exceeds byte budget".into(),
1295 ));
1296 }
1297 let mut result = Vec::with_capacity(staged.len());
1298 if result
1299 .capacity()
1300 .saturating_mul(std::mem::size_of::<FontTraceEvent>())
1301 .saturating_add(bytes.unwrap().saturating_sub(std::mem::size_of_val(staged)))
1302 > PARAGRAPH_FONT_TRACE_MAX_BYTES
1303 {
1304 return Err(LayoutError::Layout(
1305 "font trace capture allocation exceeds budget".into(),
1306 ));
1307 }
1308 result.extend(staged.iter().cloned());
1309 self.layout_font_diagnostics
1310 .truncate(scope.diagnostic_start);
1311 if scope.owns_trace {
1312 self.paragraph_font_trace = None;
1313 } else {
1314 let trace = self.paragraph_font_trace.as_mut().unwrap();
1315 let mut index = 0usize;
1316 trace.events.retain(|event| {
1317 let keep =
1318 index < scope.event_start || matches!(event, FontTraceEvent::Resolution(_));
1319 index += 1;
1320 keep
1321 });
1322 trace.owned_family_bytes = trace
1323 .events
1324 .iter()
1325 .filter_map(|event| match event {
1326 FontTraceEvent::Contribution(contribution) => {
1327 contribution.requested_family.as_ref()
1328 }
1329 FontTraceEvent::Resolution(_) => None,
1330 })
1331 .map(String::capacity)
1332 .sum();
1333 trace.scope_depth -= 1;
1334 }
1335 Ok(Some(result))
1336 }
1337
1338 #[doc(hidden)]
1340 pub fn copy_font_request_context(
1341 &mut self,
1342 family: Option<&str>,
1343 owned_bytes: &mut usize,
1344 context_entries: &mut usize,
1345 ) -> Option<String> {
1346 let family = family?;
1347 let total = owned_bytes.checked_add(family.len());
1348 if total.is_none_or(|bytes| bytes > PARAGRAPH_FONT_TRACE_MAX_BYTES)
1349 || *context_entries >= PARAGRAPH_FONT_TRACE_MAX_ENTRIES
1350 {
1351 if let Some(trace) = self.paragraph_font_trace.as_mut() {
1352 trace.events = Vec::new();
1353 trace.owned_family_bytes = 0;
1354 trace.overflowed = true;
1355 }
1356 *owned_bytes = PARAGRAPH_FONT_TRACE_MAX_BYTES;
1357 return None;
1358 }
1359 let copied = family.to_owned();
1360 *context_entries += 1;
1361 *owned_bytes = owned_bytes.saturating_add(copied.capacity());
1362 Some(copied)
1363 }
1364
1365 #[doc(hidden)]
1367 pub fn record_font_contribution(
1368 &mut self,
1369 font_id: FontId,
1370 requested_family: Option<&str>,
1371 text: &str,
1372 ) -> Result<()> {
1373 if text.is_empty() {
1374 return Ok(());
1375 }
1376 self.get_font(font_id)?;
1377 let bytes = requested_family.map_or(0, str::len);
1378 if let Some(trace) = self.paragraph_font_trace.as_mut() {
1379 let total = trace
1380 .owned_family_bytes
1381 .checked_add(bytes)
1382 .and_then(|bytes| {
1383 bytes.checked_add(
1384 trace
1385 .events
1386 .capacity()
1387 .max(trace.events.len().saturating_add(1))
1388 .saturating_mul(std::mem::size_of::<FontTraceEvent>()),
1389 )
1390 });
1391 if total.is_none_or(|bytes| bytes > PARAGRAPH_FONT_TRACE_MAX_BYTES)
1392 || trace.events.len() >= PARAGRAPH_FONT_TRACE_MAX_ENTRIES
1393 {
1394 trace.events = Vec::new();
1395 trace.owned_family_bytes = 0;
1396 trace.overflowed = true;
1397 } else if !trace.overflowed {
1398 let requested_family = requested_family.map(str::to_owned);
1399 trace.owned_family_bytes += requested_family.as_ref().map_or(0, String::capacity);
1400 trace
1401 .events
1402 .push(FontTraceEvent::Contribution(FontContribution {
1403 font_id,
1404 requested_family,
1405 }));
1406 if trace
1407 .events
1408 .capacity()
1409 .saturating_mul(std::mem::size_of::<FontTraceEvent>())
1410 .saturating_add(trace.owned_family_bytes)
1411 > PARAGRAPH_FONT_TRACE_MAX_BYTES
1412 {
1413 trace.events = Vec::new();
1414 trace.owned_family_bytes = 0;
1415 trace.overflowed = true;
1416 }
1417 }
1418 }
1419 if !self.layout_fonts.contains(&font_id) {
1420 self.layout_fonts.push(font_id);
1421 }
1422 self.record_font_approximation(font_id, requested_family);
1423 Ok(())
1424 }
1425
1426 #[doc(hidden)]
1428 pub fn validate_font_trace_events(&self, contributions: &[FontTraceEvent]) -> Result<()> {
1429 let owned_bytes = contributions.iter().try_fold(0usize, |total, event| {
1430 let (id, bytes) = match event {
1431 FontTraceEvent::Resolution(id) => (*id, 0),
1432 FontTraceEvent::Contribution(contribution) => (
1433 contribution.font_id,
1434 contribution
1435 .requested_family
1436 .as_ref()
1437 .map_or(0, String::capacity),
1438 ),
1439 };
1440 self.get_font(id)?;
1441 total
1442 .checked_add(bytes)
1443 .ok_or_else(|| LayoutError::Layout("font trace byte count overflow".into()))
1444 })?;
1445 let (count, existing_bytes, capacity, overflowed) = self
1446 .paragraph_font_trace
1447 .as_ref()
1448 .map_or((0, 0, 0, false), |trace| {
1449 (
1450 trace.events.len(),
1451 trace.owned_family_bytes,
1452 trace.events.capacity(),
1453 trace.overflowed,
1454 )
1455 });
1456 let total_count = count.checked_add(contributions.len());
1457 let total_bytes = total_count
1458 .and_then(|count| {
1459 capacity
1460 .max(count)
1461 .checked_mul(std::mem::size_of::<FontTraceEvent>())
1462 })
1463 .and_then(|bytes| bytes.checked_add(existing_bytes))
1464 .and_then(|bytes| bytes.checked_add(owned_bytes));
1465 if overflowed
1466 || total_count.is_none_or(|count| count > PARAGRAPH_FONT_TRACE_MAX_ENTRIES)
1467 || total_bytes.is_none_or(|bytes| bytes > PARAGRAPH_FONT_TRACE_MAX_BYTES)
1468 {
1469 return Err(LayoutError::Layout(
1470 "font contribution replay exceeds trace budget".into(),
1471 ));
1472 }
1473 Ok(())
1474 }
1475
1476 #[doc(hidden)]
1478 pub fn replay_font_trace_events(&mut self, contributions: &[FontTraceEvent]) -> Result<()> {
1479 self.validate_font_trace_events(contributions)?;
1480 if let Some(trace) = self.paragraph_font_trace.as_mut() {
1481 let mut prepared = Vec::with_capacity(trace.events.len() + contributions.len());
1482 let owned_bytes = trace.owned_family_bytes
1483 + contributions
1484 .iter()
1485 .map(|entry| match entry {
1486 FontTraceEvent::Resolution(_) => 0,
1487 FontTraceEvent::Contribution(entry) => {
1488 entry.requested_family.as_ref().map_or(0, String::capacity)
1489 }
1490 })
1491 .sum::<usize>();
1492 if prepared
1493 .capacity()
1494 .saturating_mul(std::mem::size_of::<FontTraceEvent>())
1495 .saturating_add(owned_bytes)
1496 > PARAGRAPH_FONT_TRACE_MAX_BYTES
1497 {
1498 return Err(LayoutError::Layout(
1499 "font contribution replay allocation exceeds budget".into(),
1500 ));
1501 }
1502 prepared.extend(trace.events.iter().cloned());
1503 prepared.extend(contributions.iter().cloned());
1504 trace.owned_family_bytes = prepared
1505 .iter()
1506 .filter_map(|event| match event {
1507 FontTraceEvent::Contribution(contribution) => {
1508 contribution.requested_family.as_ref()
1509 }
1510 FontTraceEvent::Resolution(_) => None,
1511 })
1512 .map(String::capacity)
1513 .sum();
1514 trace.events = prepared;
1515 }
1516 for event in contributions {
1517 let id = match event {
1518 FontTraceEvent::Resolution(id) => *id,
1519 FontTraceEvent::Contribution(contribution) => contribution.font_id,
1520 };
1521 if !self.layout_fonts.contains(&id) {
1522 self.layout_fonts.push(id);
1523 }
1524 if let FontTraceEvent::Contribution(contribution) = event {
1525 self.record_font_approximation(id, contribution.requested_family.as_deref());
1526 }
1527 }
1528 Ok(())
1529 }
1530
1531 #[doc(hidden)]
1533 pub fn replay_layout_font_trace(&mut self, trace: &[FontId]) {
1534 for &font_id in trace {
1535 self.record_layout_font(font_id);
1536 }
1537 }
1538
1539 #[doc(hidden)]
1541 pub fn current_layout_fonts(&self) -> &[FontId] {
1542 &self.layout_fonts
1543 }
1544
1545 #[doc(hidden)]
1547 pub fn every_loaded_font_is_current(&self) -> bool {
1548 self.fonts
1549 .iter()
1550 .map(|font| font.id)
1551 .eq(self.layout_fonts.iter().copied())
1552 && self
1553 .layout_fonts
1554 .iter()
1555 .enumerate()
1556 .all(|(index, font_id)| *font_id == FontId(index as u32))
1557 }
1558
1559 #[doc(hidden)]
1563 pub fn retain_current_fonts(&mut self) {
1564 let current = self.layout_fonts.iter().copied().collect::<HashSet<_>>();
1565 let old_index_ids = self
1566 .fonts
1567 .iter()
1568 .enumerate()
1569 .map(|(index, font)| (index, font.id))
1570 .collect::<HashMap<_, _>>();
1571 self.fonts.retain(|font| current.contains(&font.id));
1572 let current_order = self
1573 .layout_fonts
1574 .iter()
1575 .enumerate()
1576 .map(|(index, font_id)| (*font_id, index))
1577 .collect::<HashMap<_, _>>();
1578 self.fonts
1579 .sort_by_key(|font| current_order.get(&font.id).copied().unwrap_or(usize::MAX));
1580
1581 let indices = self
1582 .fonts
1583 .iter()
1584 .enumerate()
1585 .map(|(index, font)| (font.id, index))
1586 .collect::<HashMap<_, _>>();
1587 let old_cache = std::mem::take(&mut self.cache);
1588 self.cache = old_cache
1589 .into_iter()
1590 .filter_map(|(key, old_index)| {
1591 let font_id = old_index_ids.get(&old_index)?;
1592 Some((key, *indices.get(font_id)?))
1593 })
1594 .collect();
1595 self.coverage_fallbacks.clear();
1596 self.coverage_misses.clear();
1597 let active_db_ids = self
1598 .fonts
1599 .iter()
1600 .map(|font| font.db_id)
1601 .collect::<HashSet<_>>();
1602 self.memory_face_data
1603 .retain(|db_id, _| active_db_ids.contains(db_id));
1604
1605 let memo = self
1606 .shaping_memo
1607 .get_mut()
1608 .unwrap_or_else(std::sync::PoisonError::into_inner);
1609 memo.entries
1610 .retain(|(key, _, _, _)| current.contains(&key.font_id));
1611 memo.bytes = memo.entries.iter().map(|(_, _, bytes, _)| bytes).sum();
1612 }
1613
1614 pub fn resolve_font_for_text(
1631 &mut self,
1632 family: Option<&str>,
1633 bold: bool,
1634 italic: bool,
1635 text: &str,
1636 ) -> Result<FontId> {
1637 let primary = self.resolve_font(family, bold, italic)?;
1638
1639 let Some(idx) = self.index_of(primary) else {
1640 return Ok(primary);
1641 };
1642 let missing = self.uncovered(idx, text);
1643 if missing.is_empty() {
1644 return Ok(primary);
1645 }
1646
1647 match self.font_covering(&missing, bold, italic) {
1648 None => Ok(primary),
1651 Some(id) => Ok(id),
1652 }
1653 }
1654
1655 #[doc(hidden)]
1657 pub fn current_font_diagnostics(&self) -> &[Diagnostic] {
1658 &self.layout_font_diagnostics
1659 }
1660
1661 fn physical_requested_family_matches(&self, index: usize, requested: &str) -> bool {
1664 let Some(font) = self.fonts.get(index) else {
1665 return false;
1666 };
1667 let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
1668 return false;
1669 };
1670 face.names().into_iter().any(|name| {
1671 matches!(
1672 name.name_id,
1673 ttf_parser::name_id::FAMILY
1674 | ttf_parser::name_id::TYPOGRAPHIC_FAMILY
1675 | ttf_parser::name_id::WWS_FAMILY
1676 ) && name
1677 .to_string()
1678 .is_some_and(|family| family.eq_ignore_ascii_case(requested))
1679 })
1680 }
1681
1682 fn record_font_approximation(&mut self, id: FontId, requested: Option<&str>) {
1683 let Some(requested) = requested else {
1684 return;
1685 };
1686 let requested = if requested.eq_ignore_ascii_case("Aptos") {
1687 "Aptos"
1688 } else if requested.eq_ignore_ascii_case("Aptos Display") {
1689 "Aptos Display"
1690 } else {
1691 return;
1692 };
1693 let Some(index) = self.index_of(id) else {
1694 return;
1695 };
1696 if self.physical_requested_family_matches(index, requested) {
1697 return;
1698 }
1699 let prefix = format!("Font '{requested}' is approximate: ");
1700 if self
1701 .layout_font_diagnostics
1702 .iter()
1703 .any(|diagnostic| diagnostic.message.starts_with(&prefix))
1704 {
1705 return;
1706 }
1707 self.layout_font_diagnostics.push(Diagnostic {
1708 message: format!(
1709 "{prefix}selected '{}' for contributing text",
1710 self.fonts[index].family
1711 ),
1712 });
1713 }
1714
1715 pub fn resolve_symbol_text(
1718 &mut self,
1719 family: Option<&str>,
1720 bold: bool,
1721 italic: bool,
1722 original: &str,
1723 ppt_bullet_compatibility: bool,
1724 ) -> Result<(String, FontId)> {
1725 let primary = self.resolve_font(family, bold, italic)?;
1726 if self.has_original_symbol_coverage(primary, family, original) {
1727 return Ok((original.to_owned(), primary));
1728 }
1729 let displayed: String = original
1730 .chars()
1731 .map(|character| {
1732 let code = character as u32;
1733 let code = if (0xf000..=0xf0ff).contains(&code) {
1734 code - 0xf000
1735 } else {
1736 code
1737 };
1738 let mapped = if family.is_some_and(|name| name.eq_ignore_ascii_case("Wingdings")) {
1739 match code {
1740 0xa7 => Some('\u{25aa}'),
1741 0xd8 => Some('\u{27a2}'),
1742 0xfc => Some('\u{2713}'),
1743 0x76 => Some('\u{2756}'),
1744 0x71 => Some('\u{2751}'),
1745 0x6e => Some('\u{25a0}'),
1746 0xfe => Some('\u{2611}'),
1747 0xb7 if ppt_bullet_compatibility && character == '\u{f0b7}' => {
1748 Some('\u{2022}')
1749 }
1750 _ => None,
1751 }
1752 } else if family.is_some_and(|name| name.eq_ignore_ascii_case("Symbol"))
1753 && code == 0xb7
1754 {
1755 Some('\u{2022}')
1756 } else {
1757 None
1758 };
1759 mapped.unwrap_or(character)
1760 })
1761 .collect();
1762 let selected = self.resolve_font_for_text(family, bold, italic, &displayed)?;
1763 Ok((displayed, selected))
1764 }
1765
1766 pub fn has_original_symbol_coverage(
1769 &self,
1770 selected: FontId,
1771 family: Option<&str>,
1772 original: &str,
1773 ) -> bool {
1774 self.index_of(selected).is_some_and(|index| {
1775 family.is_some_and(|name| self.physical_requested_family_matches(index, name))
1776 && self.uncovered(index, original).is_empty()
1777 })
1778 }
1779
1780 pub fn unknown_symbol_diagnostics(
1783 &self,
1784 selected: FontId,
1785 requested: Option<&str>,
1786 original: &str,
1787 displayed: &str,
1788 ) -> Vec<String> {
1789 if self.index_of(selected).is_none() {
1790 return Vec::new();
1791 }
1792 let family = match requested {
1793 Some(name) if name.eq_ignore_ascii_case("Wingdings") => "Wingdings",
1794 Some(name) if name.eq_ignore_ascii_case("Symbol") => "Symbol",
1795 _ => return Vec::new(),
1796 };
1797 let mut result = Vec::new();
1798 for (source, displayed) in original.chars().zip(displayed.chars()) {
1799 let code = source as u32;
1800 if source != displayed
1801 || source.is_whitespace()
1802 || source.is_control()
1803 || !(code <= 0xff || (0xf000..=0xf0ff).contains(&code))
1804 {
1805 continue;
1806 }
1807 let mut utf8 = [0; 4];
1808 if self.has_original_symbol_coverage(
1809 selected,
1810 Some(family),
1811 source.encode_utf8(&mut utf8),
1812 ) {
1813 continue;
1814 }
1815 let message = format!("Unknown {family} symbol U+{code:04X}: original code retained");
1816 if !result.contains(&message) {
1817 result.push(message);
1818 }
1819 }
1820 result
1822 }
1823
1824 fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
1829 let font = &self.fonts[idx];
1830 let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
1831 return Vec::new();
1832 };
1833 let mut seen = HashSet::new();
1834 text.chars()
1835 .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
1836 .filter(|&ch| face.glyph_index(ch).is_none())
1837 .filter(|&ch| seen.insert(ch))
1838 .collect()
1839 }
1840
1841 fn covers(&self, idx: usize, ch: char) -> bool {
1843 let font = &self.fonts[idx];
1844 ttf_parser::Face::parse(&font.data, font.face_index)
1845 .map(|face| face.glyph_index(ch).is_some())
1846 .unwrap_or(false)
1847 }
1848
1849 fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
1858 if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
1859 return None;
1860 }
1861
1862 let mut best: Option<(usize, usize)> = None; let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
1864 let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
1865 if covered == 0 {
1866 return false;
1867 }
1868 if best.map(|(n, _)| covered > n).unwrap_or(true) {
1869 *best = Some((covered, idx));
1870 }
1871 covered == missing.len()
1872 };
1873
1874 if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
1877 for idx in known {
1878 if consider(self, idx, &mut best) {
1879 let id = self.fonts[idx].id;
1880 self.record_layout_font(id);
1881 return Some(id);
1882 }
1883 }
1884 }
1885
1886 let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
1890 .iter()
1891 .map(|s| s.to_string())
1892 .chain(
1893 self.db
1894 .faces()
1895 .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
1896 )
1897 .collect();
1898
1899 for name in candidates {
1900 let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
1901 continue;
1902 };
1903 let Some(idx) = self.index_of(id) else {
1904 continue;
1905 };
1906 let complete = consider(self, idx, &mut best);
1907 if complete {
1908 self.remember_coverage_fallback(bold, italic, idx);
1909 return Some(id);
1910 }
1911 }
1912
1913 match best {
1914 Some((_, idx)) => {
1915 self.remember_coverage_fallback(bold, italic, idx);
1916 let id = self.fonts[idx].id;
1917 self.record_layout_font(id);
1918 Some(id)
1919 }
1920 None => {
1921 self.remember_coverage_misses(missing);
1922 None
1923 }
1924 }
1925 }
1926
1927 fn index_of(&self, id: FontId) -> Option<usize> {
1929 self.fonts.iter().position(|f| f.id == id)
1930 }
1931
1932 pub fn resolve_font(
1935 &mut self,
1936 family: Option<&str>,
1937 bold: bool,
1938 italic: bool,
1939 ) -> Result<FontId> {
1940 self.resolve_font_inner(family, bold, italic, true)
1941 }
1942
1943 #[doc(hidden)]
1945 pub fn resolve_font_for_metrics(
1946 &mut self,
1947 family: Option<&str>,
1948 bold: bool,
1949 italic: bool,
1950 ) -> Result<FontId> {
1951 self.resolve_font_inner(family, bold, italic, false)
1952 }
1953
1954 fn resolve_font_inner(
1955 &mut self,
1956 family: Option<&str>,
1957 bold: bool,
1958 italic: bool,
1959 record_layout_use: bool,
1960 ) -> Result<FontId> {
1961 let family_name = family.unwrap_or("Arial");
1962
1963 let key = FontKey {
1964 family: family_name.to_string(),
1965 bold,
1966 italic,
1967 };
1968
1969 if let Some(idx) = self.cache.get(&key).copied() {
1970 let id = self.fonts[idx].id;
1971 if record_layout_use {
1972 self.record_layout_font(id);
1973 }
1974 return Ok(id);
1975 }
1976
1977 let requested_key = family_name.to_lowercase();
1978 let style = if italic {
1979 fontdb::Style::Italic
1980 } else {
1981 fontdb::Style::Normal
1982 };
1983 let weight = if bold {
1984 fontdb::Weight::BOLD
1985 } else {
1986 fontdb::Weight::NORMAL
1987 };
1988
1989 let caller_candidate = || {
1990 let exact = self.caller_families.get(family_name);
1991 let aliases = self.caller_aliases.get(&requested_key);
1992 match (exact, aliases) {
1993 (Some(exact), Some(aliases)) => {
1994 let (mut constructor, mut additional): (Vec<_>, Vec<_>) = self
1997 .db
1998 .faces()
1999 .filter(|face| exact.contains(&face.id) || aliases.contains(&face.id))
2000 .map(|face| face.id)
2001 .partition(|id| self.base_db.face(*id).is_some());
2002 constructor.reverse();
2003 additional.extend(constructor);
2004 best_caller_face(&self.db, &additional, weight, style)
2005 }
2006 (Some(ids), None) | (None, Some(ids)) => {
2007 best_caller_face(&self.db, ids, weight, style)
2008 }
2009 (None, None) => None,
2010 }
2011 };
2012 let query_family = |family: &str| {
2013 if let Some(ids) = self.caller_families.get(family)
2014 && let Some(id) = best_caller_face(&self.db, ids, weight, style)
2015 {
2016 return Some(id);
2017 }
2018 if let Some(ids) = self.embedded_aliases.get(&family.to_lowercase())
2019 && let Some(id) = best_caller_face(&self.db, ids, weight, style)
2020 {
2021 return Some(id);
2022 }
2023 let query = fontdb::Query {
2024 families: &[fontdb::Family::Name(family)],
2025 weight,
2026 style,
2027 stretch: fontdb::Stretch::Normal,
2028 };
2029 self.db.query(&query)
2030 };
2031
2032 let mut found_id = if self.explicit_alias_map.contains_key(&requested_key) {
2033 query_family(family_name)
2034 } else {
2035 caller_candidate().or_else(|| query_family(family_name))
2036 };
2037 if found_id.is_none()
2038 && let Some(alias) = self.explicit_alias_map.get(&requested_key)
2039 {
2040 found_id = query_family(alias);
2041 }
2042 if found_id.is_none()
2043 && let Some(ids) = self.caller_aliases.get(&requested_key)
2044 {
2045 found_id = best_caller_face(&self.db, ids, weight, style);
2046 }
2047
2048 let mut fallbacks: Vec<&str> = map_font_name(family_name).to_vec();
2051 for generic in &[
2052 "Carlito",
2053 "Arial",
2054 "Liberation Sans",
2055 "Helvetica",
2056 "DejaVu Sans",
2057 "Noto Sans",
2058 ] {
2059 if !fallbacks.contains(generic) {
2060 fallbacks.push(generic);
2061 }
2062 }
2063
2064 if found_id.is_none() {
2065 for fallback in &fallbacks {
2066 let Some(id) = query_family(fallback) else {
2067 continue;
2068 };
2069 found_id = Some(id);
2070 break;
2071 }
2072 }
2073
2074 if found_id.is_none() {
2076 for generic_family in &[
2077 fontdb::Family::SansSerif,
2078 fontdb::Family::Serif,
2079 fontdb::Family::Monospace,
2080 ] {
2081 let query = fontdb::Query {
2082 families: &[*generic_family],
2083 weight,
2084 style,
2085 stretch: fontdb::Stretch::Normal,
2086 };
2087 if let Some(id) = self.db.query(&query) {
2088 found_id = Some(id);
2089 break;
2090 }
2091 }
2092 }
2093
2094 let db_id = found_id.ok_or_else(|| {
2095 LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
2096 })?;
2097
2098 if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
2102 && let Some(idx) = self
2103 .fonts
2104 .iter()
2105 .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
2106 {
2107 let id = self.fonts[idx].id;
2108 if record_layout_use {
2109 self.record_layout_font(id);
2110 }
2111 return Ok(id);
2112 }
2113
2114 let font_id = FontId(self.next_id);
2115 self.next_id += 1;
2116
2117 let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
2120 .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
2121
2122 let (
2123 units_per_em,
2124 ascender,
2125 descender,
2126 line_gap,
2127 word_line,
2128 variations,
2129 synthetic_bold,
2130 synthetic_italic,
2131 ) = {
2132 let mut face = ttf_parser::Face::parse(&data, face_index)
2133 .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
2134 let (variations, synthetic_bold, synthetic_italic) =
2135 face_style_adjustment(&face, bold, italic);
2136 for (tag, value) in &variations {
2137 face.set_variation(ttf_parser::Tag::from_bytes(tag), *value)
2138 .ok_or_else(|| {
2139 LayoutError::FontParse("selected font axis could not be applied".into())
2140 })?;
2141 }
2142 (
2143 face.units_per_em(),
2144 face.ascender(),
2145 face.descender(),
2146 face.line_gap(),
2147 WordLineUnits::of(&face),
2148 variations,
2149 synthetic_bold,
2150 synthetic_italic,
2151 )
2152 };
2153
2154 if units_per_em == 0 {
2157 return Err(LayoutError::FontParse(format!(
2158 "font '{family_name}' declares zero units per em"
2159 )));
2160 }
2161
2162 let (shaper_data, shaper_instance) = {
2163 let face = harfrust::FontRef::from_index(&data, face_index)
2164 .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
2165 let instance = (!variations.is_empty()).then(|| {
2166 harfrust::ShaperInstance::from_variations(
2167 &face,
2168 variations.iter().map(|(tag, value)| harfrust::Variation {
2169 tag: harfrust::Tag::new(tag),
2170 value: *value,
2171 }),
2172 )
2173 });
2174 (harfrust::ShaperData::new(&face), instance)
2175 };
2176
2177 let actual_family = self
2178 .db
2179 .face(db_id)
2180 .map(|f| {
2181 f.families
2182 .first()
2183 .map(|(name, _)| name.clone())
2184 .unwrap_or_else(|| family_name.to_string())
2185 })
2186 .unwrap_or_else(|| family_name.to_string());
2187
2188 let idx = self.fonts.len();
2189 self.fonts.push(LoadedFont {
2190 db_id,
2191 id: font_id,
2192 family: actual_family,
2193 bold,
2194 italic,
2195 data,
2196 face_index,
2197 units_per_em,
2198 ascender,
2199 descender,
2200 line_gap,
2201 word_line,
2202 shaper_data,
2203 variations,
2204 shaper_instance,
2205 synthetic_bold,
2206 synthetic_italic,
2207 });
2208 self.remember_font_key(key, idx);
2209 if record_layout_use {
2210 self.record_layout_font(font_id);
2211 }
2212
2213 Ok(font_id)
2214 }
2215
2216 pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
2218 let font = self.get_font(font_id)?;
2219 let scale = size_pt / font.units_per_em as f64;
2220
2221 Ok(FontMetrics {
2222 ascent: font.ascender as f64 * scale,
2223 descent: -(font.descender as f64) * scale, line_gap: font.line_gap as f64 * scale,
2225 units_per_em: font.units_per_em,
2226 })
2227 }
2228
2229 pub fn word_line_metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
2241 let font = self.get_font(font_id)?;
2242 let scale = size_pt / font.units_per_em as f64;
2243 let units = font.word_line.unwrap_or(WordLineUnits {
2244 ascent: i32::from(font.ascender),
2245 descent: -i32::from(font.descender),
2246 leading: i32::from(font.line_gap).max(0),
2247 });
2248
2249 Ok(FontMetrics {
2250 ascent: units.ascent as f64 * scale,
2251 descent: units.descent as f64 * scale,
2252 line_gap: units.leading as f64 * scale,
2253 units_per_em: font.units_per_em,
2254 })
2255 }
2256
2257 pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
2259 if text.is_empty() {
2262 return Ok(ShapedText {
2263 glyph_ids: Vec::new(),
2264 advances: Vec::new(),
2265 width: 0.0,
2266 });
2267 }
2268
2269 let key = ShapingKey {
2270 font_id,
2271 text: text.to_owned(),
2272 size_bits: size_pt.to_bits(),
2273 };
2274 let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
2275 let mut memo = match self.shaping_memo.lock() {
2276 Ok(memo) => memo,
2277 Err(poisoned) => {
2278 let mut memo = poisoned.into_inner();
2279 memo.clear();
2280 self.shaping_memo.clear_poison();
2281 memo
2282 }
2283 };
2284 if let Some(shaped) = memo
2285 .entries
2286 .iter()
2287 .rev()
2288 .find(|(candidate, _, _, candidate_fingerprint)| {
2289 *candidate_fingerprint == fingerprint && candidate == &key
2290 })
2291 .map(|(_, shaped, _, _)| shaped.clone())
2292 {
2293 #[cfg(test)]
2294 {
2295 memo.hits += 1;
2296 }
2297 return Ok(shaped);
2298 }
2299 #[cfg(test)]
2300 {
2301 memo.misses += 1;
2302 }
2303
2304 let font = self.get_font(font_id)?;
2305
2306 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
2307 .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
2308
2309 let shaper = font
2310 .shaper_data
2311 .shaper(&face)
2312 .instance(font.shaper_instance.as_ref())
2313 .build();
2314
2315 let mut buffer = harfrust::UnicodeBuffer::new();
2316 buffer.push_str(text);
2317 buffer.guess_segment_properties();
2320
2321 let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
2322 let infos = output.glyph_infos();
2323 let positions = output.glyph_positions();
2324
2325 let upem = font.units_per_em as f64;
2326 let scale = size_pt / upem;
2327
2328 let mut glyph_ids = Vec::with_capacity(infos.len());
2329 let mut advances = Vec::with_capacity(positions.len());
2330 let mut total_width = 0.0;
2331
2332 for (info, pos) in infos.iter().zip(positions.iter()) {
2333 glyph_ids.push(info.glyph_id as u16);
2334 let advance = pos.x_advance as f64 * scale;
2335 advances.push(advance);
2336 total_width += advance;
2337 }
2338
2339 let shaped = ShapedText {
2340 glyph_ids,
2341 advances,
2342 width: total_width,
2343 };
2344 memo.insert(key, shaped.clone());
2345 Ok(shaped)
2346 }
2347
2348 pub fn shape_multilingual_text(
2353 &mut self,
2354 segment: TextSegment,
2355 language: Option<&str>,
2356 base_direction: TextDirection,
2357 no_wrap: bool,
2358 ) -> Result<Vec<MultilingualTextSegment>> {
2359 if segment.text.is_empty() {
2360 return Ok(Vec::new());
2361 }
2362
2363 let paragraph_level = match base_direction {
2364 TextDirection::Auto => None,
2365 TextDirection::LeftToRight => Some(unicode_bidi::Level::ltr()),
2366 TextDirection::RightToLeft => Some(unicode_bidi::Level::rtl()),
2367 };
2368 let bidi = unicode_bidi::BidiInfo::new(&segment.text, paragraph_level);
2369 let levels = bidi.levels.clone();
2370 let break_offsets = if no_wrap {
2371 HashSet::new()
2372 } else {
2373 multilingual_break_opportunities(&segment.text)
2374 };
2375 self.shape_multilingual_with_levels(
2376 segment,
2377 language,
2378 &break_offsets,
2379 &levels,
2380 0,
2381 (false, false, &mut Vec::new()),
2382 )
2383 }
2384
2385 pub fn shape_multilingual_paragraph(
2387 &mut self,
2388 segments: Vec<(TextSegment, Option<String>)>,
2389 base_direction: TextDirection,
2390 no_wrap: bool,
2391 ) -> Result<Vec<MultilingualTextSegment>> {
2392 let requests = vec![(false, false); segments.len()];
2393 self.shape_multilingual_paragraph_with_caps(segments, base_direction, no_wrap, &requests)
2394 .map(|(spans, _)| spans)
2395 }
2396
2397 pub fn shape_multilingual_paragraph_with_caps(
2402 &mut self,
2403 segments: Vec<(TextSegment, Option<String>)>,
2404 base_direction: TextDirection,
2405 no_wrap: bool,
2406 requests: &[(bool, bool)],
2407 ) -> Result<(Vec<MultilingualTextSegment>, Vec<String>)> {
2408 if requests.iter().any(|(small, all)| *small && *all) {
2409 return Err(LayoutError::Shaping(
2410 "conflicting all-caps and small-caps request".to_owned(),
2411 ));
2412 }
2413 if requests.len() != segments.len() {
2414 return Err(LayoutError::Shaping(
2415 "small-caps request count differs from styled runs".to_owned(),
2416 ));
2417 }
2418 let mut diagnostics = Vec::new();
2419 let paragraph_text = segments
2420 .iter()
2421 .map(|(segment, _)| segment.text.as_str())
2422 .collect::<String>();
2423 let mut paragraph_offset = 0usize;
2424 let segment_starts = segments
2425 .iter()
2426 .map(|(segment, _)| {
2427 let start = paragraph_offset;
2428 paragraph_offset += segment.text.len();
2429 start
2430 })
2431 .collect::<Vec<_>>();
2432 if paragraph_text.is_empty() {
2433 return Ok((Vec::new(), diagnostics));
2434 }
2435 let paragraph_level = match base_direction {
2436 TextDirection::Auto => None,
2437 TextDirection::LeftToRight => Some(unicode_bidi::Level::ltr()),
2438 TextDirection::RightToLeft => Some(unicode_bidi::Level::rtl()),
2439 };
2440 let bidi = unicode_bidi::BidiInfo::new(¶graph_text, paragraph_level);
2441 let paragraph_level = bidi
2442 .paragraphs
2443 .first()
2444 .map(|paragraph| paragraph.level)
2445 .unwrap_or_else(unicode_bidi::Level::ltr);
2446 let paragraph_breaks = if no_wrap {
2449 HashSet::new()
2450 } else {
2451 multilingual_break_opportunities(¶graph_text)
2452 };
2453 let mut logical_index = 0usize;
2454 let mut shaped = Vec::new();
2455 for (((segment, language), byte_offset), (small_caps, all_caps)) in segments
2456 .into_iter()
2457 .zip(segment_starts)
2458 .zip(requests.iter().copied())
2459 {
2460 let byte_end = byte_offset + segment.text.len();
2461 if !segment.text.is_empty() {
2462 let forced_levels = (segment.direction != TextDirection::Auto)
2463 .then(|| {
2464 explicit_direction_levels(&segment.text, segment.direction, paragraph_level)
2465 })
2466 .transpose()?;
2467 let levels = forced_levels
2468 .as_deref()
2469 .unwrap_or(&bidi.levels[byte_offset..byte_end]);
2470 let break_offsets = paragraph_breaks
2471 .iter()
2472 .filter(|offset| (byte_offset + 1..=byte_end).contains(*offset))
2473 .map(|offset| offset - byte_offset)
2474 .collect();
2475 let spans = self.shape_multilingual_with_levels(
2476 segment,
2477 language.as_deref(),
2478 &break_offsets,
2479 levels,
2480 logical_index,
2481 (small_caps, all_caps, &mut diagnostics),
2482 )?;
2483 logical_index += spans.len();
2484 shaped.extend(spans);
2485 }
2486 }
2487 Ok((shaped, diagnostics))
2488 }
2489
2490 fn shape_multilingual_with_levels(
2493 &mut self,
2494 segment: TextSegment,
2495 language: Option<&str>,
2496 break_offsets: &HashSet<usize>,
2497 levels: &[unicode_bidi::Level],
2498 logical_index_base: usize,
2499 caps: (bool, bool, &mut Vec<String>),
2500 ) -> Result<Vec<MultilingualTextSegment>> {
2501 let (small_caps, all_caps, diagnostics) = caps;
2502 let grapheme_boundaries = icu_segmenter::GraphemeClusterSegmenter::new()
2503 .segment_str(&segment.text)
2504 .collect::<Vec<_>>();
2505 let hanging_starts = break_offsets
2508 .iter()
2509 .map(|offset| segment.text[..*offset].trim_end_matches(' ').len())
2510 .collect::<HashSet<_>>();
2511
2512 let mut logical_ranges = Vec::<(usize, usize, TextScript, unicode_bidi::Level)>::new();
2513 let mut start = 0usize;
2514 let mut current_script = TextScript::Common;
2515 let mut current_level = levels[0];
2516 for window in grapheme_boundaries.windows(2) {
2517 let grapheme_start = window[0];
2518 let grapheme_end = window[1];
2519 let script = script_for_grapheme(&segment.text[grapheme_start..grapheme_end]);
2520 let script = if script == TextScript::Common {
2521 current_script
2522 } else {
2523 script
2524 };
2525 let level = levels[grapheme_start];
2526 if grapheme_start > start && (script != current_script || level != current_level) {
2527 logical_ranges.push((start, grapheme_start, current_script, current_level));
2528 start = grapheme_start;
2529 }
2530 current_script = script;
2531 current_level = level;
2532 if (break_offsets.contains(&grapheme_end) || hanging_starts.contains(&grapheme_end))
2533 && grapheme_end < segment.text.len()
2534 {
2535 logical_ranges.push((start, grapheme_end, current_script, current_level));
2536 start = grapheme_end;
2537 }
2538 }
2539 if start < segment.text.len() {
2540 logical_ranges.push((start, segment.text.len(), current_script, current_level));
2541 }
2542
2543 let mut font_ranges = Vec::new();
2544 for (start, end, script, level) in logical_ranges {
2545 let boundaries = icu_segmenter::GraphemeClusterSegmenter::new()
2546 .segment_str(&segment.text[start..end])
2547 .map(|offset| start + offset)
2548 .collect::<Vec<_>>();
2549 let mut range_start = start;
2550 let mut range_font = None;
2551 for window in boundaries.windows(2) {
2552 let grapheme_start = window[0];
2553 let grapheme_end = window[1];
2554 let font_id = self.font_for_multilingual_span(
2555 segment.font_id,
2556 &segment.text[grapheme_start..grapheme_end],
2557 segment.bold,
2558 segment.italic,
2559 );
2560 if let Some(current_font) = range_font
2561 && current_font != font_id
2562 {
2563 font_ranges.push((range_start, grapheme_start, script, level, current_font));
2564 range_start = grapheme_start;
2565 }
2566 range_font = Some(font_id);
2567 }
2568 if let Some(font_id) = range_font {
2569 font_ranges.push((range_start, end, script, level, font_id));
2570 }
2571 }
2572
2573 let mut cap_ranges = Vec::new();
2574 for (start, end, script, level, font_id) in font_ranges {
2575 let direction = if level.is_rtl() {
2576 TextDirection::RightToLeft
2577 } else {
2578 TextDirection::LeftToRight
2579 };
2580 let text = &segment.text[start..end];
2581 let lowercase = text.to_lowercase();
2582 let has_lowercase = text.chars().any(char::is_lowercase);
2585 let feature_applies = if small_caps && has_lowercase {
2586 let plain = self.shape_explicit(
2587 font_id,
2588 &lowercase,
2589 segment.font_size,
2590 script,
2591 language,
2592 direction,
2593 )?;
2594 let caps = self.shape_explicit_with_small_caps(
2595 font_id,
2596 &lowercase,
2597 segment.font_size,
2598 (script, language, direction),
2599 true,
2600 )?;
2601 plain.glyph_ids != caps.glyph_ids
2602 } else {
2603 false
2604 };
2605 if all_caps {
2606 cap_ranges.push((
2607 start,
2608 end,
2609 script,
2610 level,
2611 font_id,
2612 segment.font_size,
2613 false,
2614 true,
2615 ));
2616 } else if !small_caps || !has_lowercase || feature_applies {
2617 cap_ranges.push((
2618 start,
2619 end,
2620 script,
2621 level,
2622 font_id,
2623 segment.font_size,
2624 feature_applies,
2625 false,
2626 ));
2627 } else {
2628 let font = self.get_font(font_id)?;
2629 let message = format!(
2630 "small caps: selected face '{}' lacks an applicable smcp substitution for {:?}; using 80 percent uppercase fallback",
2631 font.family, script
2632 );
2633 if !diagnostics.contains(&message) {
2634 diagnostics.push(message);
2635 }
2636 let boundaries = icu_segmenter::GraphemeClusterSegmenter::new()
2639 .segment_str(text)
2640 .collect::<Vec<_>>();
2641 for window in boundaries.windows(2) {
2642 let lo = start + window[0];
2643 let hi = start + window[1];
2644 let lower = segment.text[lo..hi].chars().any(char::is_lowercase);
2645 cap_ranges.push((
2646 lo,
2647 hi,
2648 script,
2649 level,
2650 font_id,
2651 segment.font_size * if lower { 0.8 } else { 1.0 },
2652 false,
2653 lower,
2654 ));
2655 }
2656 }
2657 }
2658 let mut logical = Vec::with_capacity(cap_ranges.len());
2659 for (
2660 logical_index,
2661 (start, end, script, level, font_id, size_pt, feature_applies, fallback),
2662 ) in cap_ranges.into_iter().enumerate()
2663 {
2664 let text = &segment.text[start..end];
2665 let metrics = self.metrics(font_id, size_pt)?;
2666 let direction = if level.is_rtl() {
2667 TextDirection::RightToLeft
2668 } else {
2669 TextDirection::LeftToRight
2670 };
2671 let rendered = if fallback {
2672 text.to_uppercase()
2673 } else {
2674 text.to_owned()
2675 };
2676 let mut positioned = self.shape_explicit_with_small_caps(
2677 font_id,
2678 &rendered,
2679 size_pt,
2680 (script, language, direction),
2681 feature_applies,
2682 )?;
2683 if fallback {
2684 let mut owners = Vec::new();
2685 for (index, character) in text.chars().enumerate() {
2686 owners.extend(character.to_uppercase().map(|_| index as u32));
2687 }
2688 for cluster in &mut positioned.clusters {
2689 let lo = cluster.char_start as usize;
2690 let hi = cluster.char_end as usize;
2691 cluster.char_start = *owners.get(lo).ok_or_else(|| {
2692 LayoutError::Shaping(
2693 "uppercase cluster starts outside its logical owner".to_owned(),
2694 )
2695 })?;
2696 cluster.char_end = owners
2697 .get(hi.checked_sub(1).ok_or_else(|| {
2698 LayoutError::Shaping("empty uppercase cluster".to_owned())
2699 })?)
2700 .ok_or_else(|| {
2701 LayoutError::Shaping(
2702 "uppercase cluster ends outside its logical owner".to_owned(),
2703 )
2704 })?
2705 + 1;
2706 }
2707 let mut merged: Vec<GlyphCluster> = Vec::new();
2708 for cluster in positioned.clusters {
2709 if let Some(last) = merged.last_mut()
2710 && last.char_start < cluster.char_end
2711 && cluster.char_start < last.char_end
2712 {
2713 last.glyph_end = cluster.glyph_end;
2714 last.char_start = last.char_start.min(cluster.char_start);
2715 last.char_end = last.char_end.max(cluster.char_end);
2716 } else {
2717 merged.push(cluster);
2718 }
2719 }
2720 positioned.clusters = merged;
2721 }
2722 let char_start = segment.text[..start].chars().count() as u32;
2723 let char_end = segment.text[..end].chars().count() as u32;
2724 let source = segment.source.map(|source| SourceSpan {
2725 node: source.node,
2726 char_start: source.char_start + char_start,
2727 char_end: source.char_start + char_end,
2728 });
2729 let mut base = segment.clone();
2730 base.text = text.to_owned();
2731 base.source = source;
2732 base.font_id = font_id;
2733 base.font_size = size_pt;
2734 base.glyph_ids = positioned.glyph_ids;
2735 base.advances = positioned.x_advances.clone();
2736 base.width = positioned.x_advances.iter().sum();
2737 base.ascent = metrics.ascent;
2738 base.descent = metrics.descent;
2739 base.line_gap = metrics.line_gap;
2740 logical.push(MultilingualTextSegment::new(
2741 base,
2742 logical_index_base + logical_index,
2743 language.map(str::to_owned),
2744 script,
2745 direction,
2746 level.number(),
2747 positioned.x_advances,
2748 positioned.y_advances,
2749 positioned.x_offsets,
2750 positioned.y_offsets,
2751 positioned.clusters,
2752 break_offsets.contains(&end),
2753 )?);
2754 }
2755
2756 Ok(logical)
2757 }
2758
2759 fn font_for_multilingual_span(
2760 &mut self,
2761 preferred: FontId,
2762 text: &str,
2763 bold: bool,
2764 italic: bool,
2765 ) -> FontId {
2766 let Some(index) = self.index_of(preferred) else {
2767 return preferred;
2768 };
2769 if self.uncovered(index, text).is_empty() {
2770 return preferred;
2771 }
2772 let required = text
2773 .chars()
2774 .filter(|character| !character.is_whitespace() && !character.is_control())
2775 .collect::<Vec<_>>();
2776 self.font_covering(&required, bold, italic)
2777 .unwrap_or(preferred)
2778 }
2779
2780 fn shape_explicit(
2781 &self,
2782 font_id: FontId,
2783 text: &str,
2784 size_pt: f64,
2785 script: TextScript,
2786 language: Option<&str>,
2787 direction: TextDirection,
2788 ) -> Result<PositionedShape> {
2789 self.shape_explicit_with_small_caps(
2790 font_id,
2791 text,
2792 size_pt,
2793 (script, language, direction),
2794 false,
2795 )
2796 }
2797
2798 fn shape_explicit_with_small_caps(
2799 &self,
2800 font_id: FontId,
2801 text: &str,
2802 size_pt: f64,
2803 properties: (TextScript, Option<&str>, TextDirection),
2804 small_caps: bool,
2805 ) -> Result<PositionedShape> {
2806 let (script, language, direction) = properties;
2807 let font = self.get_font(font_id)?;
2808 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
2809 .map_err(|error| LayoutError::Shaping(format!("failed to read font face: {error}")))?;
2810 let shaper = font
2811 .shaper_data
2812 .shaper(&face)
2813 .instance(font.shaper_instance.as_ref())
2814 .build();
2815 let mut buffer = harfrust::UnicodeBuffer::new();
2816 buffer.push_str(text);
2817 buffer.set_script(harfrust_script(script));
2818 buffer.set_direction(match direction {
2819 TextDirection::RightToLeft => harfrust::Direction::RightToLeft,
2820 TextDirection::Auto | TextDirection::LeftToRight => harfrust::Direction::LeftToRight,
2821 });
2822 if let Some(language) = language.and_then(harfrust::Language::new) {
2823 buffer.set_language(language);
2824 }
2825 let features = [harfrust::Feature::new(harfrust::Tag::new(b"smcp"), 1, ..)];
2826 let options = if small_caps {
2827 harfrust::ShapeOptions::default().features(&features)
2828 } else {
2829 harfrust::ShapeOptions::default()
2830 };
2831 let output = shaper.shape(buffer, options);
2832 let infos = output.glyph_infos();
2833 let positions = output.glyph_positions();
2834 let scale = size_pt / f64::from(font.units_per_em);
2835 let glyph_ids = infos
2836 .iter()
2837 .map(|info| info.glyph_id as u16)
2838 .collect::<Vec<_>>();
2839 let x_advances = positions
2840 .iter()
2841 .map(|pos| f64::from(pos.x_advance) * scale)
2842 .collect();
2843 let y_advances = positions
2844 .iter()
2845 .map(|pos| f64::from(pos.y_advance) * scale)
2846 .collect();
2847 let x_offsets = positions
2848 .iter()
2849 .map(|pos| f64::from(pos.x_offset) * scale)
2850 .collect();
2851 let y_offsets = positions
2852 .iter()
2853 .map(|pos| f64::from(pos.y_offset) * scale)
2854 .collect();
2855 let clusters = glyph_clusters(infos, text);
2856 Ok(PositionedShape {
2857 glyph_ids,
2858 x_advances,
2859 y_advances,
2860 x_offsets,
2861 y_offsets,
2862 clusters,
2863 })
2864 }
2865
2866 pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
2868 let font = self.get_font(font_id)?;
2869 Ok(crate::output::FontData {
2870 id: font.id,
2871 family: font.family.clone(),
2872 data: Arc::clone(&font.data),
2873 face_index: font.face_index,
2874 bold: font.bold,
2875 italic: font.italic,
2876 variations: font.variations.clone(),
2877 synthetic_bold: font.synthetic_bold,
2878 synthetic_italic: font.synthetic_italic,
2879 })
2880 }
2881
2882 pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
2884 self.fonts
2885 .iter()
2886 .map(|f| crate::output::FontData {
2887 id: f.id,
2888 family: f.family.clone(),
2889 data: Arc::clone(&f.data),
2890 face_index: f.face_index,
2891 bold: f.bold,
2892 italic: f.italic,
2893 variations: f.variations.clone(),
2894 synthetic_bold: f.synthetic_bold,
2895 synthetic_italic: f.synthetic_italic,
2896 })
2897 .collect()
2898 }
2899
2900 fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
2901 self.fonts
2902 .iter()
2903 .find(|f| f.id == font_id)
2904 .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
2905 }
2906
2907 fn remember_font_key(&mut self, key: FontKey, index: usize) {
2908 if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
2909 self.cache.insert(key, index);
2910 }
2911 }
2912
2913 fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
2914 let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
2915 if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
2916 known.push(index);
2917 }
2918 }
2919
2920 fn remember_coverage_misses(&mut self, missing: &[char]) {
2921 for &ch in missing {
2922 if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
2923 break;
2924 }
2925 self.coverage_misses.insert(ch);
2926 }
2927 }
2928
2929 fn record_layout_font(&mut self, font_id: FontId) {
2930 if let Some(trace) = self.paragraph_font_trace.as_mut() {
2931 if !trace.overflowed && trace.events.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
2932 trace.events.push(FontTraceEvent::Resolution(font_id));
2933 if trace
2934 .events
2935 .capacity()
2936 .saturating_mul(std::mem::size_of::<FontTraceEvent>())
2937 .saturating_add(trace.owned_family_bytes)
2938 > PARAGRAPH_FONT_TRACE_MAX_BYTES
2939 {
2940 trace.events = Vec::new();
2941 trace.owned_family_bytes = 0;
2942 trace.overflowed = true;
2943 }
2944 } else if !trace.overflowed {
2945 trace.events = Vec::new();
2946 trace.owned_family_bytes = 0;
2947 trace.overflowed = true;
2948 }
2949 }
2950 if !self.layout_fonts.contains(&font_id) {
2951 self.layout_fonts.push(font_id);
2952 }
2953 }
2954
2955 #[cfg(test)]
2956 fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
2957 let memo = self
2958 .shaping_memo
2959 .lock()
2960 .unwrap_or_else(std::sync::PoisonError::into_inner);
2961 (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
2962 }
2963}
2964
2965struct PositionedShape {
2966 glyph_ids: Vec<u16>,
2967 x_advances: Vec<f64>,
2968 y_advances: Vec<f64>,
2969 x_offsets: Vec<f64>,
2970 y_offsets: Vec<f64>,
2971 clusters: Vec<GlyphCluster>,
2972}
2973
2974fn script_for_grapheme(grapheme: &str) -> TextScript {
2975 grapheme
2976 .chars()
2977 .map(script_for_char)
2978 .find(|script| *script != TextScript::Common)
2979 .unwrap_or(TextScript::Common)
2980}
2981
2982fn script_for_char(character: char) -> TextScript {
2983 match character as u32 {
2984 0x0041..=0x024f | 0x1e00..=0x1eff => TextScript::Latin,
2985 0x0590..=0x05ff => TextScript::Hebrew,
2986 0x0600..=0x06ff | 0x0750..=0x077f | 0x08a0..=0x08ff => TextScript::Arabic,
2987 0x0900..=0x097f | 0xa8e0..=0xa8ff => TextScript::Devanagari,
2988 0x0e00..=0x0e7f => TextScript::Thai,
2989 0x1100..=0x11ff | 0x3130..=0x318f | 0xa960..=0xa97f | 0xac00..=0xd7ff | 0xffa0..=0xffdc => {
2998 TextScript::Hangul
2999 }
3000 0x3040..=0x30ff | 0x31f0..=0x31ff | 0xff66..=0xff9f => TextScript::Kana,
3001 0x3400..=0x4dbf | 0x4e00..=0x9fff | 0xf900..=0xfaff => TextScript::Han,
3002 _ => TextScript::Common,
3003 }
3004}
3005
3006fn harfrust_script(script: TextScript) -> harfrust::Script {
3007 match script {
3008 TextScript::Latin => harfrust::script::LATIN,
3009 TextScript::Arabic => harfrust::script::ARABIC,
3010 TextScript::Hebrew => harfrust::script::HEBREW,
3011 TextScript::Devanagari => harfrust::script::DEVANAGARI,
3012 TextScript::Thai => harfrust::script::THAI,
3013 TextScript::Han => harfrust::script::HAN,
3014 TextScript::Hangul => harfrust::script::HANGUL,
3015 TextScript::Kana => harfrust::script::HIRAGANA,
3018 TextScript::Common => harfrust::script::COMMON,
3019 }
3020}
3021
3022fn multilingual_break_opportunities(text: &str) -> HashSet<usize> {
3028 let complex_context = |character: Option<char>| {
3029 character.is_some_and(|character| {
3030 unicode_linebreak::break_property(character as u32)
3031 == unicode_linebreak::BreakClass::ComplexContext
3032 })
3033 };
3034 let mut opportunities = icu_segmenter::WordSegmenter::new_auto(Default::default())
3035 .segment_str(text)
3036 .filter(|offset| {
3037 *offset > 0
3038 && *offset < text.len()
3039 && complex_context(text[..*offset].chars().next_back())
3040 && complex_context(text[*offset..].chars().next())
3041 })
3042 .collect::<HashSet<_>>();
3043 for (offset, _) in unicode_linebreak::linebreaks(text) {
3044 if offset > 0 {
3045 opportunities.insert(offset);
3046 }
3047 }
3048 opportunities.retain(|offset| {
3049 let before = text[..*offset].chars().next_back();
3050 let after = text[*offset..].chars().next();
3051 before
3052 .zip(after)
3053 .is_none_or(|(before, after)| crate::line::multilingual_break_allowed(before, after))
3054 });
3055 opportunities
3056}
3057
3058fn glyph_clusters(infos: &[harfrust::GlyphInfo], text: &str) -> Vec<GlyphCluster> {
3059 let mut byte_starts = infos
3060 .iter()
3061 .map(|info| info.cluster as usize)
3062 .collect::<Vec<_>>();
3063 byte_starts.push(text.len());
3064 byte_starts.sort_unstable();
3065 byte_starts.dedup();
3066
3067 let mut clusters = Vec::new();
3068 let mut glyph_start = 0usize;
3069 while glyph_start < infos.len() {
3070 let cluster_byte = infos[glyph_start].cluster as usize;
3071 let mut glyph_end = glyph_start + 1;
3072 while glyph_end < infos.len() && infos[glyph_end].cluster as usize == cluster_byte {
3073 glyph_end += 1;
3074 }
3075 let byte_end = byte_starts
3076 .iter()
3077 .copied()
3078 .find(|candidate| *candidate > cluster_byte)
3079 .unwrap_or(text.len());
3080 clusters.push(GlyphCluster {
3081 glyph_start: glyph_start as u32,
3082 glyph_end: glyph_end as u32,
3083 char_start: text[..cluster_byte].chars().count() as u32,
3084 char_end: text[..byte_end].chars().count() as u32,
3085 });
3086 glyph_start = glyph_end;
3087 }
3088 clusters
3089}
3090
3091fn best_caller_face(
3092 db: &fontdb::Database,
3093 ids: &[fontdb::ID],
3094 weight: fontdb::Weight,
3095 style: fontdb::Style,
3096) -> Option<fontdb::ID> {
3097 const CANDIDATE_FAMILY: &str = "__rdocx_caller_candidate__";
3098
3099 let mut candidates = fontdb::Database::new();
3100 let mut candidate_ids = Vec::with_capacity(ids.len());
3101 for id in ids.iter().rev() {
3102 let mut face = db.face(*id)?.clone();
3103 for (family, _) in &mut face.families {
3104 CANDIDATE_FAMILY.clone_into(family);
3105 }
3106 let candidate_id = candidates.push_face_info(face);
3107 candidate_ids.push((candidate_id, *id));
3108 }
3109
3110 let selected = candidates.query(&fontdb::Query {
3111 families: &[fontdb::Family::Name(CANDIDATE_FAMILY)],
3112 weight,
3113 style,
3114 stretch: fontdb::Stretch::Normal,
3115 })?;
3116 candidate_ids
3117 .into_iter()
3118 .find_map(|(candidate, original)| (candidate == selected).then_some(original))
3119}
3120
3121fn bundled_font_database() -> fontdb::Database {
3122 let mut db = fontdb::Database::new();
3123 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
3124 db.load_font_data(data.to_vec());
3125 }
3126 db
3127}
3128
3129fn font_data_for_face(
3130 db: &fontdb::Database,
3131 id: fontdb::ID,
3132 memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
3133) -> Option<(Arc<[u8]>, u32)> {
3134 let face = db.face(id)?;
3135 let face_index = face.index;
3136 match &face.source {
3137 fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
3138 Some(data) => Some((Arc::clone(data), face_index)),
3139 None => {
3140 let shared = db.faces().find_map(|other| match &other.source {
3143 fontdb::Source::Binary(source) if Arc::ptr_eq(source, data) => {
3144 memory_face_data.get(&other.id).cloned()
3145 }
3146 _ => None,
3147 });
3148 let data: Arc<[u8]> =
3149 shared.unwrap_or_else(|| Arc::from(data.as_ref().as_ref().to_vec()));
3150 memory_face_data.insert(id, Arc::clone(&data));
3151 Some((data, face_index))
3152 }
3153 },
3154 #[cfg(feature = "system-fonts")]
3155 fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
3156 #[allow(unreachable_patterns)]
3159 _ => db.with_face_data(id, |data, index| (Arc::from(data), index)),
3160 }
3161}
3162
3163#[cfg(feature = "system-fonts")]
3164fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
3165 let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
3166 shared_file_font_bytes_from_cache(cache, path)
3167}
3168
3169#[cfg(feature = "system-fonts")]
3170fn shared_file_font_bytes_from_cache(
3171 cache_lock: &Mutex<FileFontCache>,
3172 path: &Path,
3173) -> Option<Arc<[u8]>> {
3174 let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
3175 let mut cache = match cache_lock.lock() {
3176 Ok(cache) => cache,
3177 Err(poisoned) => {
3178 let mut cache = poisoned.into_inner();
3179 cache.clear();
3180 cache_lock.clear_poison();
3181 cache
3182 }
3183 };
3184 if let Some(index) = cache
3185 .entries
3186 .iter()
3187 .position(|(candidate, _, _)| candidate == &identity)
3188 {
3189 let entry = cache.entries.remove(index).expect("cache index exists");
3190 let bytes = Arc::clone(&entry.1);
3191 cache.entries.push_back(entry);
3192 return Some(bytes);
3193 }
3194
3195 let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
3196 cache_file_font_bytes(&mut cache, identity, bytes)
3197}
3198
3199#[cfg(feature = "system-fonts")]
3200fn cache_file_font_bytes(
3201 cache: &mut FileFontCache,
3202 identity: PathBuf,
3203 bytes: Arc<[u8]>,
3204) -> Option<Arc<[u8]>> {
3205 let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
3206 .saturating_add(identity.as_os_str().len())
3207 .saturating_add(bytes.len());
3208 if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
3209 while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
3210 || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
3211 {
3212 let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
3213 break;
3214 };
3215 cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
3216 }
3217 cache.bytes += entry_bytes;
3218 cache
3219 .entries
3220 .push_back((identity, Arc::clone(&bytes), entry_bytes));
3221 }
3222 Some(bytes)
3223}
3224
3225fn map_font_name(name: &str) -> &[&str] {
3232 match name {
3233 "Calibri" => &["Calibri", "Carlito"],
3234 "Calibri Light" => &["Calibri Light", "Carlito"],
3235 "Cambria" => &["Cambria", "Caladea"],
3236 "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
3237 "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
3238 "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
3239 "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
3240 "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
3241 "Segoe UI" => &["Segoe UI", "Selawik", "Carlito", "Liberation Sans"],
3242 "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
3243 "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
3244 "Georgia" => &["Georgia", "Gelasio", "Caladea", "Liberation Serif"],
3245 "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
3246 "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
3247 "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
3248 "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
3249 "Impact" => &["Impact", "Liberation Sans", "Arial"],
3250 "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
3251 "Symbol" => &["Symbol", "DejaVu Sans"],
3252 "Wingdings" => &["Wingdings", "Symbol"],
3253 _ => &[],
3254 }
3255}
3256
3257#[cfg(test)]
3258mod tests {
3259 use super::*;
3260 use crate::Color;
3261 use crate::bundled_fonts::bundled_font_data;
3262
3263 #[test]
3266 fn fx195_strict_directories_preserve_lexical_order_and_refuse_bad_inputs() {
3267 let path =
3268 std::env::temp_dir().join(format!("fx195-font-directory-{}", std::process::id()));
3269 std::fs::create_dir(&path).unwrap();
3270 assert!(FontFile::from_dir(&path).unwrap().is_empty());
3271 let bytes = include_bytes!("../fonts/Carlito-Regular.ttf");
3272 std::fs::write(path.join("z.ttf"), bytes).unwrap();
3273 std::fs::write(path.join("a.TTF"), bytes).unwrap();
3274 let fonts = FontFile::from_dir(&path).unwrap();
3275 assert_eq!(
3276 fonts
3277 .iter()
3278 .map(|font| font.family.as_str())
3279 .collect::<Vec<_>>(),
3280 ["a", "z"]
3281 );
3282 assert!(
3283 fonts
3284 .iter()
3285 .all(|font| font.data.as_slice() == bytes.as_slice())
3286 );
3287 assert!(FontFile::from_dir(&path.join("z.ttf")).is_err());
3288 std::fs::write(path.join("bad.otf"), b"invalid font").unwrap();
3289 assert!(FontFile::from_dir(&path).is_err());
3290 std::fs::remove_dir_all(&path).unwrap();
3291 assert!(FontFile::from_dir(&path).is_err());
3292 }
3293
3294 #[test]
3295 fn fx195_variable_bold_matches_independent_harfbuzz_advances() {
3296 let mut mismatches = Vec::new();
3297 for (bytes, text, expected) in [
3298 (
3299 include_bytes!("../fonts/NotoSansThai.ttf").as_slice(),
3300 "ภาษาไทยยินดีต้อนรับ",
3301 8742.0,
3302 ),
3303 (
3304 include_bytes!("../fonts/NotoSansArabic.ttf").as_slice(),
3305 "العربية مرحبا بالعالم",
3306 8427.0,
3307 ),
3308 (
3309 include_bytes!("../fonts/NotoSansDevanagari.ttf").as_slice(),
3310 "देवनागरी नमस्ते दुनिया",
3311 8656.0,
3312 ),
3313 ] {
3314 let mut fonts =
3315 FontManager::new_with_fonts(vec![("FX195 caller".into(), bytes.to_vec())]);
3316 let bold = fonts
3317 .resolve_font(Some("FX195 caller"), true, false)
3318 .unwrap();
3319 let shaped = fonts.shape_text(bold, text, 1000.0).unwrap();
3320 if (shaped.width - expected).abs() > 1.0 {
3321 mismatches.push(format!(
3322 "bold {text:?}: expected {expected} design units, got {}",
3323 shaped.width
3324 ));
3325 }
3326 }
3327 assert!(mismatches.is_empty(), "{mismatches:?}");
3328 }
3329
3330 #[test]
3331 fn fx195_regular_variable_preserves_independent_default_advances() {
3332 for (bytes, text, expected) in [
3333 (
3334 include_bytes!("../fonts/NotoSansThai.ttf").as_slice(),
3335 "ภาษาไทยยินดีต้อนรับ",
3336 8300.0,
3337 ),
3338 (
3339 include_bytes!("../fonts/NotoSansArabic.ttf").as_slice(),
3340 "العربية مرحبا بالعالم",
3341 7591.0,
3342 ),
3343 (
3344 include_bytes!("../fonts/NotoSansDevanagari.ttf").as_slice(),
3345 "देवनागरी नमस्ते दुनिया",
3346 7948.0,
3347 ),
3348 ] {
3349 let mut fonts =
3350 FontManager::new_with_fonts(vec![("FX195 caller".into(), bytes.to_vec())]);
3351 let regular = fonts
3352 .resolve_font(Some("FX195 caller"), false, false)
3353 .unwrap();
3354 let shaped = fonts.shape_text(regular, text, 1000.0).unwrap();
3355 assert!(
3356 (shaped.width - expected).abs() <= 1.0,
3357 "regular {text:?}: expected {expected} design units, got {}",
3358 shaped.width
3359 );
3360 }
3361 }
3362
3363 #[test]
3364 fn fx195_bundled_variable_bold_is_not_exempt_from_weight_selection() {
3365 let mut fonts = FontManager::new_deterministic().unwrap();
3366 let bold = fonts
3367 .resolve_font(Some("Noto Sans Thai"), true, false)
3368 .unwrap();
3369 let shaped = fonts.shape_text(bold, "ภาษาไทยยินดีต้อนรับ", 1000.0).unwrap();
3370 assert!(
3371 (shaped.width - 8742.0).abs() <= 1.0,
3372 "bundled bold must use the independently measured 700 instance, got {}",
3373 shaped.width
3374 );
3375 }
3376
3377 #[test]
3378 fn fx195_variable_ttc_keeps_face_index_and_shared_original_bytes() {
3379 let collection = test_ttc(&[
3380 include_bytes!("../fonts/NotoSansThai.ttf"),
3381 include_bytes!("../fonts/Carlito-Bold.ttf"),
3382 ]);
3383 let mut fonts =
3384 FontManager::new_with_fonts(vec![("FX195 collection".into(), collection.clone())]);
3385 let thai = fonts
3386 .resolve_font(Some("Noto Sans Thai"), true, false)
3387 .unwrap();
3388 let carlito = fonts.resolve_font(Some("Carlito"), true, false).unwrap();
3389 let thai_data = fonts.font_data(thai).unwrap();
3390 let carlito_data = fonts.font_data(carlito).unwrap();
3391 assert_eq!(thai_data.face_index, 0);
3392 assert_eq!(carlito_data.face_index, 1);
3393 assert_eq!(thai_data.data.as_ref(), collection);
3394 assert_eq!(carlito_data.data.as_ref(), collection);
3395 assert!(Arc::ptr_eq(&thai_data.data, &carlito_data.data));
3396 let static_face = ttf_parser::Face::parse(&carlito_data.data, 1).unwrap();
3397 assert!(!static_face.is_variable());
3398 assert!(static_face.is_bold());
3399 let shaped = fonts.shape_text(thai, "ภาษาไทยยินดีต้อนรับ", 1000.0).unwrap();
3400 assert!(
3401 (shaped.width - 8742.0).abs() <= 1.0,
3402 "TTC selected700 must match independent advance, got {}",
3403 shaped.width
3404 );
3405 }
3406
3407 #[test]
3408 fn fx195_static_four_style_faces_keep_their_original_bytes() {
3409 let mut fonts = FontManager::new_deterministic().unwrap();
3410 for (bold, italic, expected) in [
3411 (
3412 false,
3413 false,
3414 include_bytes!("../fonts/Carlito-Regular.ttf").as_slice(),
3415 ),
3416 (
3417 true,
3418 false,
3419 include_bytes!("../fonts/Carlito-Bold.ttf").as_slice(),
3420 ),
3421 (
3422 false,
3423 true,
3424 include_bytes!("../fonts/Carlito-Italic.ttf").as_slice(),
3425 ),
3426 (
3427 true,
3428 true,
3429 include_bytes!("../fonts/Carlito-BoldItalic.ttf").as_slice(),
3430 ),
3431 ] {
3432 let id = fonts.resolve_font(Some("Carlito"), bold, italic).unwrap();
3433 assert_eq!(fonts.font_data(id).unwrap().data.as_ref(), expected);
3434 }
3435 }
3436
3437 #[test]
3438 fn fx206_caps_requests_keep_occurrence_identity_expansion_and_default_projection() {
3439 let mut fonts = FontManager::new_deterministic().unwrap();
3440 let source = SourceSpan {
3441 node: crate::SourceNodeId::new(7).unwrap(),
3442 char_start: 40,
3443 char_end: 43,
3444 };
3445 let first = multilingual_test_segment(&mut fonts, "aBß", 24.0, Some(source));
3446 let second = first.clone();
3447 let plain = fonts
3448 .shape_multilingual_paragraph(
3449 vec![(first.clone(), Some("en".to_owned()))],
3450 TextDirection::Auto,
3451 false,
3452 )
3453 .unwrap();
3454 let (defaults, diagnostics) = fonts
3455 .shape_multilingual_paragraph_with_caps(
3456 vec![(first.clone(), Some("en".to_owned()))],
3457 TextDirection::Auto,
3458 false,
3459 &[(false, false)],
3460 )
3461 .unwrap();
3462 assert_eq!(defaults.len(), plain.len());
3463 for (actual, expected) in defaults.iter().zip(&plain) {
3464 assert_eq!(actual.text(), expected.text());
3465 assert_eq!(actual.glyph_ids(), expected.glyph_ids());
3466 assert_eq!(actual.x_advances(), expected.x_advances());
3467 assert_eq!(actual.clusters(), expected.clusters());
3468 }
3469 assert!(diagnostics.is_empty());
3470 let (caps, diagnostics) = fonts
3471 .shape_multilingual_paragraph_with_caps(
3472 vec![
3473 (first, Some("en".to_owned())),
3474 (second, Some("en".to_owned())),
3475 ],
3476 TextDirection::Auto,
3477 false,
3478 &[(true, false), (false, false)],
3479 )
3480 .unwrap();
3481 assert!(!diagnostics.is_empty());
3482 assert_eq!(
3483 caps.iter()
3484 .map(MultilingualTextSegment::text)
3485 .collect::<String>(),
3486 "aBßaBß"
3487 );
3488 let sharp = caps.iter().find(|span| span.text() == "ß").unwrap();
3489 assert_eq!(sharp.glyph_ids().len(), 2);
3490 assert_eq!(sharp.clusters().len(), 1);
3491 assert_eq!(sharp.base().source.unwrap().char_start, 42);
3492 assert_eq!(sharp.base().source.unwrap().char_end, 43);
3493 assert!((sharp.base().font_size - 19.2).abs() < 1e-9);
3494 assert!(
3495 caps.iter()
3496 .any(|span| span.text() == "aBß" && span.base().font_size == 24.0),
3497 "equal text in a distinct occurrence retains its own request"
3498 );
3499 assert!(
3500 fonts
3501 .shape_multilingual_paragraph_with_caps(
3502 Vec::new(),
3503 TextDirection::Auto,
3504 false,
3505 &[(true, false)]
3506 )
3507 .is_err()
3508 );
3509 }
3510
3511 #[test]
3512 #[ignore = "required F206 physical host smcp oracle stage, run explicitly with pinned HB_SHAPE and FONT_ORACLE_PYTHON"]
3513 fn fx206_selected_smcp_matches_independent_harfbuzz_and_original_clusters() {
3514 let path = std::path::Path::new(
3515 "/Applications/Microsoft PowerPoint.app/Contents/Resources/DFonts/calibri.ttf",
3516 );
3517 let data = std::fs::read(path).expect("authenticated private host oracle face must exist");
3518 let mut fonts = FontManager::new_deterministic().unwrap();
3519 fonts.load_additional_fonts(&[FontFile {
3520 family: "F206 private smcp oracle".to_owned(),
3521 data,
3522 }]);
3523 let text = "Small caps Straße";
3524 let mut seed = multilingual_test_segment(&mut fonts, text, 24.0, None);
3525 seed.font_id = fonts
3526 .resolve_font_for_text(Some("F206 private smcp oracle"), false, false, text)
3527 .unwrap();
3528 let font = fonts.get_font(seed.font_id).unwrap();
3529 assert_eq!(font.family, "Calibri");
3530 let upem = font.units_per_em;
3531 let script = r#"import json,subprocess,sys
3532h,p,t,u=sys.argv[1:]
3533for feature in ['smcp=0','smcp=1']:
3534 d=json.loads(subprocess.check_output([h,p,t,'--no-glyph-names','--output-format=json','--script=latn','--language=en','--direction=ltr','--font-size='+u,'--features='+feature],text=True))
3535 print(','.join(str(x['g']) for x in d))
3536 print(','.join(str(x['ax']) for x in d))
3537"#;
3538 let output = std::process::Command::new(
3539 std::env::var("FONT_ORACLE_PYTHON").expect("explicit pinned interpreter"),
3540 )
3541 .args([
3542 "-c",
3543 script,
3544 &std::env::var("HB_SHAPE").expect("explicit pinned HarfBuzz"),
3545 path.to_str().unwrap(),
3546 text,
3547 &upem.to_string(),
3548 ])
3549 .output()
3550 .unwrap();
3551 assert!(
3552 output.status.success(),
3553 "independent oracle precondition: {}",
3554 String::from_utf8_lossy(&output.stderr)
3555 );
3556 let stdout = String::from_utf8(output.stdout).unwrap();
3557 let rows = stdout.lines().collect::<Vec<_>>();
3558 assert_eq!(rows.len(), 4);
3559 let gids = |row: &str| {
3560 row.split(',')
3561 .map(|item| item.parse::<u16>().unwrap())
3562 .collect::<Vec<_>>()
3563 };
3564 let advances = |row: &str| {
3565 row.split(',')
3566 .map(|item| item.parse::<f64>().unwrap() * 24.0 / f64::from(upem))
3567 .collect::<Vec<_>>()
3568 };
3569 assert_ne!(
3570 gids(rows[0]),
3571 gids(rows[2]),
3572 "real smcp application differs from features-off shape"
3573 );
3574 let (actual, diagnostics) = fonts
3575 .shape_multilingual_paragraph_with_caps(
3576 vec![(seed, Some("en".to_owned()))],
3577 TextDirection::LeftToRight,
3578 false,
3579 &[(true, false)],
3580 )
3581 .unwrap();
3582 assert!(diagnostics.is_empty());
3583 assert_eq!(
3584 actual
3585 .iter()
3586 .map(MultilingualTextSegment::text)
3587 .collect::<String>(),
3588 text
3589 );
3590 assert_eq!(
3591 actual
3592 .iter()
3593 .flat_map(|span| span.glyph_ids().iter().copied())
3594 .collect::<Vec<_>>(),
3595 gids(rows[2])
3596 );
3597 let actual_advances = actual
3598 .iter()
3599 .flat_map(|span| span.x_advances().iter().copied())
3600 .collect::<Vec<_>>();
3601 let expected = advances(rows[3]);
3602 assert_eq!(actual_advances.len(), expected.len());
3603 for (actual, expected) in actual_advances.iter().zip(expected) {
3604 assert!((actual - expected).abs() < 1e-9);
3605 }
3606 }
3607
3608 fn multilingual_test_segment(
3609 manager: &mut FontManager,
3610 text: &str,
3611 size_pt: f64,
3612 source: Option<SourceSpan>,
3613 ) -> TextSegment {
3614 let font_id = manager
3615 .resolve_font_for_text(None, false, false, text)
3616 .expect("test font resolves");
3617 let metrics = manager
3618 .metrics(font_id, size_pt)
3619 .expect("test font metrics");
3620 let shaped = manager
3621 .shape_text(font_id, text, size_pt)
3622 .expect("test seed shapes");
3623 TextSegment {
3624 text: text.to_owned(),
3625 direction: TextDirection::Auto,
3626 source,
3627 font_id,
3628 font_size: size_pt,
3629 glyph_ids: shaped.glyph_ids,
3630 advances: shaped.advances,
3631 width: shaped.width,
3632 ascent: metrics.ascent,
3633 descent: metrics.descent,
3634 line_gap: metrics.line_gap,
3635 color: Color::BLACK,
3636 bold: false,
3637 italic: false,
3638 underline: None,
3639 strike: false,
3640 dstrike: false,
3641 highlight: None,
3642 baseline_offset: 0.0,
3643 hyperlink_url: None,
3644 field_kind: None,
3645 field_source: None,
3646 note: None,
3647 note_reference_source: None,
3648 }
3649 }
3650
3651 #[test]
3657 fn word_line_metrics_are_the_windows_extent_and_its_external_leading() {
3658 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
3659 for (family, units_per_em, ascent, descent, leading) in [
3660 ("Carlito", 2048.0, 1950.0, 550.0, 0.0),
3661 ("Liberation Sans", 2048.0, 1854.0, 434.0, 67.0),
3662 ("Liberation Serif", 2048.0, 1825.0, 443.0, 87.0),
3663 ("Caladea", 1000.0, 950.0, 222.0, 0.0),
3664 ("Liberation Mono", 2048.0, 1705.0, 615.0, 0.0),
3665 ("Noto Sans Devanagari", 1000.0, 896.0, 408.0, 0.0),
3668 ] {
3669 let font_id = manager
3670 .resolve_font(Some(family), false, false)
3671 .expect("bundled face resolves");
3672 let metrics = manager
3673 .word_line_metrics(font_id, 12.0)
3674 .expect("loaded face has metrics");
3675 let scale = 12.0 / units_per_em;
3676 assert_eq!(metrics.units_per_em as f64, units_per_em, "{family}");
3677 assert!((metrics.ascent - ascent * scale).abs() < 1e-9, "{family}");
3678 assert!((metrics.descent - descent * scale).abs() < 1e-9, "{family}");
3679 assert!(
3680 (metrics.line_gap - leading * scale).abs() < 1e-9,
3681 "{family}"
3682 );
3683 }
3684 }
3685
3686 #[test]
3687 fn caller_font_labels_override_bundled_families_and_keep_explicit_aliases() {
3688 let caladea = bundled_font_data()
3689 .into_iter()
3690 .find(|(family, _)| *family == "Caladea")
3691 .expect("Caladea is bundled")
3692 .1;
3693 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
3694 manager.load_additional_fonts(&[
3695 FontFile {
3696 family: "Document Serif".to_owned(),
3697 data: caladea.to_vec(),
3698 },
3699 FontFile {
3700 family: "Carlito".to_owned(),
3701 data: caladea.to_vec(),
3702 },
3703 ]);
3704
3705 let aliased = manager
3706 .resolve_font(Some("Document Serif"), false, false)
3707 .expect("caller label resolves");
3708 assert_eq!(
3709 manager.fonts[manager.index_of(aliased).unwrap()].family,
3710 "Caladea"
3711 );
3712
3713 let exact = manager
3714 .resolve_font(Some("Carlito"), false, false)
3715 .expect("caller label overrides the bundled family");
3716 assert_eq!(
3717 manager.fonts[manager.index_of(exact).unwrap()].family,
3718 "Caladea"
3719 );
3720
3721 manager.set_caller_aliases(&[("Document Serif".to_owned(), "Carlito".to_owned())]);
3722 let explicit = manager
3723 .resolve_font(Some("Document Serif"), false, false)
3724 .expect("explicit alias precedes label-derived alias");
3725 assert_eq!(
3726 manager.fonts[manager.index_of(explicit).unwrap()].family,
3727 "Carlito"
3728 );
3729
3730 manager.load_additional_fonts(&[FontFile {
3731 family: "Caladea".to_owned(),
3732 data: caladea.to_vec(),
3733 }]);
3734 assert!(manager.caller_aliases.is_empty());
3735
3736 manager.set_caller_aliases(&[("Arial".to_owned(), "Caladea".to_owned())]);
3737 let caller_alias = manager
3738 .resolve_font(Some("Arial"), false, false)
3739 .expect("explicit alias resolves before mapped fallback");
3740 assert_eq!(
3741 manager.fonts[manager.index_of(caller_alias).unwrap()].family,
3742 "Caladea"
3743 );
3744 let generic = manager
3745 .resolve_font(Some("Unmapped Document Family"), false, false)
3746 .expect("generic fallback remains available");
3747 assert_eq!(
3748 manager.fonts[manager.index_of(generic).unwrap()].family,
3749 "Carlito"
3750 );
3751 }
3752
3753 #[test]
3754 fn caller_alias_updates_preserve_bytes_and_invalidate_resolution_state() {
3755 let caladea = bundled_font_data()
3756 .into_iter()
3757 .find(|(family, _)| *family == "Caladea")
3758 .expect("Caladea is bundled")
3759 .1;
3760 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
3761 manager.load_additional_fonts(&[FontFile {
3762 family: "Caladea".to_owned(),
3763 data: caladea.to_vec(),
3764 }]);
3765 let aliases = vec![
3766 ("Document Serif A".to_owned(), "Caladea".to_owned()),
3767 ("Document Serif B".to_owned(), "Caladea".to_owned()),
3768 ];
3769 assert!(manager.set_caller_aliases(&aliases));
3770
3771 let first = manager
3772 .resolve_font(Some("Document Serif A"), false, false)
3773 .expect("first alias resolves");
3774 let second = manager
3775 .resolve_font(Some("Document Serif B"), false, false)
3776 .expect("second alias resolves");
3777 let first_data = &manager.fonts[manager.index_of(first).unwrap()].data;
3778 let second_data = &manager.fonts[manager.index_of(second).unwrap()].data;
3779 assert!(Arc::ptr_eq(first_data, second_data));
3780 assert!(!manager.set_caller_aliases(&aliases));
3781
3782 assert!(
3783 manager.set_caller_aliases(&[("Document Serif A".to_owned(), "Carlito".to_owned(),)])
3784 );
3785 let changed = manager
3786 .resolve_font(Some("Document Serif A"), false, false)
3787 .expect("changed alias resolves");
3788 assert_eq!(
3789 manager.fonts[manager.index_of(changed).unwrap()].family,
3790 "Carlito"
3791 );
3792 assert!(
3793 manager.index_of(first).is_some(),
3794 "loaded faces are retained"
3795 );
3796 }
3797
3798 #[test]
3799 fn explicit_alias_state_respects_entry_and_retained_byte_ceilings() {
3800 let aliases = (0..CALLER_ALIAS_MAX_ENTRIES + 32)
3801 .map(|index| (format!("Document Serif {index}"), "Caladea".to_owned()))
3802 .collect::<Vec<_>>();
3803 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
3804 manager.set_caller_aliases(&aliases);
3805 assert_eq!(
3806 manager.explicit_aliases.as_slice(),
3807 &aliases[..CALLER_ALIAS_MAX_ENTRIES]
3808 );
3809 assert!(manager.explicit_aliases.len() <= CALLER_ALIAS_MAX_ENTRIES);
3810 assert!(manager.explicit_alias_map.len() <= CALLER_ALIAS_MAX_ENTRIES);
3811
3812 let retained_large = ("x".repeat(32_760), String::new());
3813 let byte_limited = vec![
3814 retained_large.clone(),
3815 ("discarded bytes".to_owned(), "Caladea".to_owned()),
3816 ];
3817 manager.set_caller_aliases(&byte_limited);
3818 assert_eq!(manager.explicit_aliases, vec![retained_large]);
3819
3820 let oversized = vec![("x".repeat(40_000), "Caladea".to_owned())];
3821 manager.set_caller_aliases(&oversized);
3822 let retained_bytes = manager
3823 .explicit_aliases
3824 .iter()
3825 .map(|(requested, target)| requested.len() + target.len())
3826 .sum::<usize>()
3827 + manager
3828 .explicit_alias_map
3829 .iter()
3830 .map(|(requested, target)| requested.len() + target.len())
3831 .sum::<usize>();
3832 assert!(retained_bytes <= CALLER_ALIAS_MAX_RETAINED_BYTES);
3833 assert!(manager.explicit_aliases.is_empty());
3834 assert!(manager.explicit_alias_map.is_empty());
3835 }
3836
3837 #[test]
3838 fn label_alias_prefers_caller_bytes_over_bundled_same_family() {
3839 let bundled = bundled_font_data()
3840 .into_iter()
3841 .find(|(family, _)| *family == "Caladea")
3842 .expect("Caladea is bundled")
3843 .1;
3844 let mut caller = bundled.to_vec();
3845 caller.push(0);
3846 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
3847 manager.load_additional_fonts(&[FontFile {
3848 family: "Document Serif".to_owned(),
3849 data: caller.clone(),
3850 }]);
3851
3852 let resolved = manager
3853 .resolve_font(Some("Document Serif"), false, false)
3854 .expect("caller label resolves");
3855 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
3856 assert_eq!(loaded.family, "Caladea");
3857 assert_eq!(loaded.data.as_ref(), caller.as_slice());
3858 assert_ne!(loaded.data.as_ref(), bundled);
3859 }
3860
3861 #[test]
3862 fn case_only_caller_labels_resolve_to_the_supplied_face() {
3863 let bundled = bundled_font_data()
3864 .into_iter()
3865 .find(|(family, _)| *family == "Caladea")
3866 .expect("Caladea is bundled")
3867 .1;
3868 let mut caller = bundled.to_vec();
3869 caller.push(0);
3870 let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
3871 manager.load_additional_fonts(&[FontFile {
3872 family: "caladea".to_owned(),
3873 data: caller.clone(),
3874 }]);
3875
3876 let resolved = manager
3877 .resolve_font(Some("caladea"), false, false)
3878 .expect("case-only caller label resolves");
3879 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
3880 assert_eq!(loaded.family, "Caladea");
3881 assert_eq!(loaded.data.as_ref(), caller.as_slice());
3882 }
3883
3884 #[test]
3885 fn constructor_label_alias_survives_additional_font_replacement() {
3886 let caladea = bundled_font_data()
3887 .into_iter()
3888 .find(|(family, _)| *family == "Caladea")
3889 .expect("Caladea is bundled")
3890 .1;
3891 let carlito = bundled_font_data()
3892 .into_iter()
3893 .find(|(family, _)| *family == "Carlito")
3894 .expect("Carlito is bundled")
3895 .1;
3896 let mut constructor = caladea.to_vec();
3897 constructor.push(0);
3898 let mut manager =
3899 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
3900
3901 manager.load_additional_fonts(&[FontFile {
3902 family: "Additional Sans".to_owned(),
3903 data: carlito.to_vec(),
3904 }]);
3905
3906 let resolved = manager
3907 .resolve_font(Some("Document Serif"), false, false)
3908 .expect("constructor label still resolves");
3909 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
3910 assert_eq!(loaded.family, "Caladea");
3911 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
3912 }
3913
3914 #[test]
3915 fn constructor_family_priority_survives_additional_font_replacement() {
3916 let caladea = bundled_font_data()
3917 .into_iter()
3918 .find(|(family, _)| *family == "Caladea")
3919 .expect("Caladea is bundled")
3920 .1;
3921 let mut constructor = caladea.to_vec();
3922 constructor.push(0);
3923 let mut replacement = caladea.to_vec();
3924 replacement.extend_from_slice(&[0, 0]);
3925 let mut manager =
3926 FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
3927
3928 manager.load_additional_fonts(&[FontFile {
3929 family: "Caladea".to_owned(),
3930 data: replacement,
3931 }]);
3932
3933 let resolved = manager
3934 .resolve_font(Some("Caladea"), false, false)
3935 .expect("constructor family still resolves");
3936 let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
3937 assert_eq!(loaded.data.as_ref(), constructor.as_slice());
3938 }
3939
3940 #[test]
3941 fn caller_face_selection_matches_fontdb_css_rules() {
3942 let caladea = bundled_font_data()
3943 .into_iter()
3944 .find(|(family, _)| *family == "Caladea")
3945 .expect("Caladea is bundled")
3946 .1;
3947 let mut source = fontdb::Database::new();
3948 source.load_font_data(caladea.to_vec());
3949 let template = source.faces().next().expect("Caladea has a face").clone();
3950 let mut db = fontdb::Database::new();
3951 let mut add_face = |weight: u16, stretch: fontdb::Stretch, style: fontdb::Style| {
3952 let mut face = template.clone();
3953 face.weight = fontdb::Weight(weight);
3954 face.stretch = stretch;
3955 face.style = style;
3956 db.push_face_info(face)
3957 };
3958
3959 let weight_300 = add_face(300, fontdb::Stretch::Normal, fontdb::Style::Normal);
3960 let weight_500 = add_face(500, fontdb::Stretch::Normal, fontdb::Style::Normal);
3961 let weight_600 = add_face(600, fontdb::Stretch::Normal, fontdb::Style::Normal);
3962 let weight_800 = add_face(800, fontdb::Stretch::Normal, fontdb::Style::Normal);
3963 let expanded = add_face(400, fontdb::Stretch::SemiExpanded, fontdb::Style::Normal);
3964 let condensed = add_face(400, fontdb::Stretch::SemiCondensed, fontdb::Style::Normal);
3965 let normal = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Normal);
3966 let italic = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Italic);
3967
3968 assert_eq!(
3969 best_caller_face(
3970 &db,
3971 &[weight_300, weight_500],
3972 fontdb::Weight::NORMAL,
3973 fontdb::Style::Normal,
3974 ),
3975 Some(weight_500)
3976 );
3977 assert_eq!(
3978 best_caller_face(
3979 &db,
3980 &[weight_600, weight_800],
3981 fontdb::Weight::BOLD,
3982 fontdb::Style::Normal,
3983 ),
3984 Some(weight_800)
3985 );
3986 assert_eq!(
3987 best_caller_face(
3988 &db,
3989 &[expanded, condensed],
3990 fontdb::Weight::NORMAL,
3991 fontdb::Style::Normal,
3992 ),
3993 Some(condensed)
3994 );
3995 assert_eq!(
3996 best_caller_face(
3997 &db,
3998 &[normal, italic],
3999 fontdb::Weight::NORMAL,
4000 fontdb::Style::Oblique,
4001 ),
4002 Some(italic)
4003 );
4004 }
4005
4006 #[test]
4007 fn fx220_embedded_physical_body_serves_two_thousand_aliases_atomically() {
4008 let body: Arc<[u8]> = Arc::from(include_bytes!("../fonts/Caladea-Regular.ttf").as_slice());
4009 let names = (0..2000)
4010 .map(|index| format!("FX220 Alias {index}"))
4011 .collect::<Vec<_>>();
4012 let sources = vec![(names, body.clone())];
4013 let mut manager = FontManager::new_deterministic().unwrap();
4014 let base = manager.db.len();
4015 assert!(manager.load_embedded_fonts(&sources).unwrap());
4016 assert_eq!(manager.db.len(), base + 1);
4017 let mut ids = Vec::new();
4018 let mut data = Vec::new();
4019 for index in [0, 999, 1999] {
4020 let id = manager
4021 .resolve_font(Some(&format!("FX220 Alias {index}")), false, false)
4022 .unwrap();
4023 data.push(manager.font_data(id).unwrap().data);
4024 ids.push(id);
4025 }
4026 assert!(Arc::ptr_eq(&data[0], &data[1]) && Arc::ptr_eq(&data[1], &data[2]));
4027 assert_eq!(data[0].as_ref(), body.as_ref());
4028 let cache = manager.cache.len();
4029 assert!(!manager.load_embedded_fonts(&sources).unwrap());
4030 assert_eq!(manager.cache.len(), cache);
4031 assert_eq!(
4032 manager
4033 .resolve_font(Some("FX220 Alias 0"), false, false)
4034 .unwrap(),
4035 ids[0]
4036 );
4037 let refused = vec![(vec!["too many".to_owned(); 16_385], body.clone())];
4038 assert!(manager.load_embedded_fonts(&refused).is_err());
4039 assert!(
4040 manager
4041 .load_embedded_fonts(&[(
4042 vec!["malformed".to_owned()],
4043 Arc::from([0_u8; 16].as_slice())
4044 )])
4045 .is_err()
4046 );
4047 let mut hostile_collection = b"ttcf\x00\x01\x00\x00".to_vec();
4048 hostile_collection.extend_from_slice(&u32::MAX.to_be_bytes());
4049 assert!(
4050 manager
4051 .load_embedded_fonts(&[(
4052 vec!["hostile collection".to_owned()],
4053 Arc::from(hostile_collection)
4054 )])
4055 .is_err()
4056 );
4057 assert_eq!(manager.cache.len(), cache);
4058 assert_eq!(manager.db.len(), base + 1);
4059 assert_eq!(
4060 manager
4061 .resolve_font(Some("FX220 Alias 0"), false, false)
4062 .unwrap(),
4063 ids[0]
4064 );
4065 let caller = include_bytes!("../fonts/Carlito-Regular.ttf").to_vec();
4066 manager.load_additional_fonts(&[FontFile {
4067 family: "FX220 Alias 1999".to_owned(),
4068 data: caller.clone(),
4069 }]);
4070 let id = manager
4071 .resolve_font(Some("FX220 Alias 1999"), false, false)
4072 .unwrap();
4073 assert_eq!(manager.font_data(id).unwrap().data.as_ref(), caller);
4074 let id = manager
4075 .resolve_font(Some("FX220 Alias 999"), false, false)
4076 .unwrap();
4077 assert_eq!(manager.font_data(id).unwrap().data.as_ref(), body.as_ref());
4078 assert!(manager.load_embedded_fonts(&[]).unwrap());
4079 assert!(manager.embedded_aliases.is_empty());
4080 }
4081
4082 #[test]
4083 fn fx220_embedded_four_faces_preserve_css_and_caller_priority() {
4084 let face_data: [&[u8]; 4] = [
4085 include_bytes!("../fonts/Caladea-Regular.ttf"),
4086 include_bytes!("../fonts/Caladea-Bold.ttf"),
4087 include_bytes!("../fonts/Caladea-Italic.ttf"),
4088 include_bytes!("../fonts/Caladea-BoldItalic.ttf"),
4089 ];
4090 let sources = face_data
4091 .iter()
4092 .map(|data| (vec!["Embedded FX220".to_owned()], Arc::<[u8]>::from(*data)))
4093 .collect::<Vec<_>>();
4094 let mut manager = FontManager::new_deterministic().unwrap();
4095 manager.load_embedded_fonts(&sources).unwrap();
4096 for (index, (bold, italic)) in [(false, false), (true, false), (false, true), (true, true)]
4097 .into_iter()
4098 .enumerate()
4099 {
4100 let id = manager
4101 .resolve_font(Some("Embedded FX220"), bold, italic)
4102 .unwrap();
4103 assert_eq!(
4104 manager.font_data(id).unwrap().data.as_ref(),
4105 face_data[index]
4106 );
4107 }
4108 let caller = include_bytes!("../fonts/Carlito-Regular.ttf").to_vec();
4109 let mut constructor =
4110 FontManager::new_with_fonts(vec![("Embedded FX220".to_owned(), caller.clone())]);
4111 constructor.load_embedded_fonts(&sources).unwrap();
4112 let id = constructor
4113 .resolve_font(Some("Embedded FX220"), false, false)
4114 .unwrap();
4115 assert_eq!(constructor.font_data(id).unwrap().data.as_ref(), caller);
4116 manager.load_additional_fonts(&[FontFile {
4117 family: "Embedded FX220".to_owned(),
4118 data: caller.clone(),
4119 }]);
4120 let id = manager
4121 .resolve_font(Some("Embedded FX220"), false, false)
4122 .unwrap();
4123 assert_eq!(manager.font_data(id).unwrap().data.as_ref(), caller);
4124 }
4125
4126 #[test]
4127 fn fx220_cross_bucket_ranking_retains_constructor_and_css_precedence() {
4128 let family = "FX220CX";
4129 let exact = font_with_family(include_bytes!("../fonts/Caladea-Regular.ttf"), family);
4130 let alias = include_bytes!("../fonts/Carlito-Regular.ttf").to_vec();
4131 let bold = include_bytes!("../fonts/Carlito-Bold.ttf").to_vec();
4132 let italic = include_bytes!("../fonts/Carlito-Italic.ttf").to_vec();
4133 let bold_italic = include_bytes!("../fonts/Carlito-BoldItalic.ttf").to_vec();
4134 for (constructor, additional) in [
4135 (exact.clone(), alias.clone()),
4136 (alias.clone(), exact.clone()),
4137 ] {
4138 let mut manager =
4139 FontManager::new_with_fonts(vec![(family.to_owned(), constructor.clone())]);
4140 let files = vec![FontFile {
4141 family: family.to_owned(),
4142 data: additional,
4143 }];
4144 assert!(manager.load_additional_fonts(&files));
4145 let id = manager.resolve_font(Some(family), false, false).unwrap();
4146 assert_eq!(manager.font_data(id).unwrap().data.as_ref(), constructor);
4147 assert!(!manager.load_additional_fonts(&files));
4148 assert_eq!(
4149 manager.resolve_font(Some(family), false, false).unwrap(),
4150 id
4151 );
4152 assert!(manager.load_additional_fonts(&[
4153 FontFile {
4154 family: family.to_owned(),
4155 data: bold.clone()
4156 },
4157 FontFile {
4158 family: family.to_owned(),
4159 data: italic.clone()
4160 },
4161 FontFile {
4162 family: family.to_owned(),
4163 data: bold_italic.clone()
4164 },
4165 ]));
4166 for (is_bold, is_italic, expected) in [
4167 (true, false, &bold),
4168 (false, true, &italic),
4169 (true, true, &bold_italic),
4170 ] {
4171 let id = manager
4172 .resolve_font(Some(family), is_bold, is_italic)
4173 .unwrap();
4174 assert_eq!(manager.font_data(id).unwrap().data.as_ref(), expected);
4175 }
4176 }
4177 let mut manager = FontManager::new_deterministic().unwrap();
4178 for files in [
4179 vec![exact.clone(), alias.clone()],
4180 vec![alias.clone(), exact.clone()],
4181 ] {
4182 let expected = files.last().unwrap().clone();
4183 manager.load_additional_fonts(
4184 &files
4185 .into_iter()
4186 .map(|data| FontFile {
4187 family: family.to_owned(),
4188 data,
4189 })
4190 .collect::<Vec<_>>(),
4191 );
4192 let id = manager.resolve_font(Some(family), false, false).unwrap();
4193 assert_eq!(manager.font_data(id).unwrap().data.as_ref(), expected);
4194 }
4195 manager.set_caller_aliases(&[("Explicit FX220".to_owned(), family.to_owned())]);
4196 let direct = manager.resolve_font(Some(family), false, false).unwrap();
4197 let indirect = manager
4198 .resolve_font(Some("Explicit FX220"), false, false)
4199 .unwrap();
4200 assert_eq!(
4201 manager.font_data(direct).unwrap().data,
4202 manager.font_data(indirect).unwrap().data
4203 );
4204 assert!(
4205 manager
4206 .resolve_font(Some("Missing FX220"), false, false)
4207 .is_ok()
4208 );
4209 }
4210
4211 #[test]
4212 fn fx195_equal_rank_caller_faces_select_last_loaded_original_bytes() {
4213 let family = "FX195CX";
4214 let first = font_with_family(include_bytes!("../fonts/Carlito-Regular.ttf"), family);
4215 let second = font_with_family(include_bytes!("../fonts/Caladea-Regular.ttf"), family);
4216 assert_ne!(first, second);
4217 let mut constructor = FontManager::new_with_fonts(vec![
4218 ("Constructor Alias".to_owned(), first.clone()),
4219 ("Constructor Alias".to_owned(), second.clone()),
4220 ]);
4221 for requested in [family, "Constructor Alias"] {
4222 let selected = constructor
4223 .resolve_font(Some(requested), false, false)
4224 .unwrap();
4225 assert_eq!(
4226 constructor.font_data(selected).unwrap().data.as_ref(),
4227 first
4228 );
4229 }
4230 assert!(constructor.load_additional_fonts(&[FontFile {
4231 family: family.to_owned(),
4232 data: second.clone(),
4233 }]));
4234 for requested in [family, "Constructor Alias"] {
4235 let selected = constructor
4236 .resolve_font(Some(requested), false, false)
4237 .unwrap();
4238 assert_eq!(
4239 constructor.font_data(selected).unwrap().data.as_ref(),
4240 first
4241 );
4242 }
4243 let mut manager = FontManager::new_deterministic().unwrap();
4244 let mut files = vec![
4245 FontFile {
4246 family: family.to_owned(),
4247 data: first.clone(),
4248 },
4249 FontFile {
4250 family: family.to_owned(),
4251 data: second.clone(),
4252 },
4253 ];
4254 assert!(manager.load_additional_fonts(&files));
4255 let selected = manager.resolve_font(Some(family), false, false).unwrap();
4256 assert_eq!(manager.font_data(selected).unwrap().data.as_ref(), second);
4257 assert_eq!(manager.font_data(selected).unwrap().face_index, 0);
4258 assert!(!manager.load_additional_fonts(&files));
4259 assert_eq!(
4260 manager.resolve_font(Some(family), false, false).unwrap(),
4261 selected
4262 );
4263
4264 files.reverse();
4265 assert!(manager.load_additional_fonts(&files));
4266 let selected = manager.resolve_font(Some(family), false, false).unwrap();
4267 assert_eq!(manager.font_data(selected).unwrap().data.as_ref(), first);
4268 assert_eq!(manager.font_data(selected).unwrap().face_index, 0);
4269
4270 let bold = font_with_family(include_bytes!("../fonts/Carlito-Bold.ttf"), family);
4271 assert!(constructor.load_additional_fonts(&[FontFile {
4272 family: family.to_owned(),
4273 data: bold.clone(),
4274 }]));
4275 let selected = constructor.resolve_font(Some(family), true, false).unwrap();
4276 assert_eq!(constructor.font_data(selected).unwrap().data.as_ref(), bold);
4277 assert!(!constructor.font_data(selected).unwrap().synthetic_bold);
4278 assert!(manager.load_additional_fonts(&[
4279 FontFile {
4280 family: family.to_owned(),
4281 data: bold.clone(),
4282 },
4283 FontFile {
4284 family: family.to_owned(),
4285 data: second.clone(),
4286 },
4287 ]));
4288 let selected = manager.resolve_font(Some(family), true, false).unwrap();
4289 assert_eq!(manager.font_data(selected).unwrap().data.as_ref(), bold);
4290 assert!(!manager.font_data(selected).unwrap().synthetic_bold);
4291 let regular = manager.resolve_font(Some(family), false, false).unwrap();
4292 assert_eq!(manager.font_data(regular).unwrap().data.as_ref(), second);
4293 }
4294
4295 fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
4296 assert!(family.is_ascii() && !family.is_empty());
4297 let mut font = source.to_vec();
4298 let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
4299 let record = (0..table_count)
4300 .map(|table| 12 + table * 16)
4301 .find(|record| &font[*record..*record + 4] == b"name")
4302 .expect("font has name table");
4303 let offset = u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize;
4304 let length =
4305 u32::from_be_bytes(font[record + 12..record + 16].try_into().unwrap()) as usize;
4306 let mut names = font[offset..offset + length].to_vec();
4307 let count = u16::from_be_bytes([names[2], names[3]]) as usize;
4308 let strings = u16::from_be_bytes([names[4], names[5]]) as usize;
4309 for index in 0..count {
4310 let entry = 6 + index * 12;
4311 let platform = u16::from_be_bytes([names[entry], names[entry + 1]]);
4312 let name_id = u16::from_be_bytes([names[entry + 6], names[entry + 7]]);
4313 if !matches!(name_id, 1 | 16) {
4314 continue;
4315 }
4316 let encoded = match platform {
4317 0 | 3 => family
4318 .encode_utf16()
4319 .flat_map(u16::to_be_bytes)
4320 .collect::<Vec<_>>(),
4321 1 => family.as_bytes().to_vec(),
4322 _ => continue,
4323 };
4324 let string_offset = u16::try_from(names.len() - strings).unwrap();
4325 let length = u16::try_from(encoded.len()).unwrap();
4326 names[entry + 8..entry + 10].copy_from_slice(&length.to_be_bytes());
4327 names[entry + 10..entry + 12].copy_from_slice(&string_offset.to_be_bytes());
4328 names.extend_from_slice(&encoded);
4329 }
4330 let checksum = |data: &[u8]| {
4331 data.chunks(4).fold(0u32, |sum, chunk| {
4332 let mut word = [0; 4];
4333 word[..chunk.len()].copy_from_slice(chunk);
4334 sum.wrapping_add(u32::from_be_bytes(word))
4335 })
4336 };
4337 while !font.len().is_multiple_of(4) {
4338 font.push(0);
4339 }
4340 let new_offset = u32::try_from(font.len()).unwrap();
4341 font[record + 4..record + 8].copy_from_slice(&checksum(&names).to_be_bytes());
4342 font[record + 8..record + 12].copy_from_slice(&new_offset.to_be_bytes());
4343 font[record + 12..record + 16]
4344 .copy_from_slice(&u32::try_from(names.len()).unwrap().to_be_bytes());
4345 font.extend_from_slice(&names);
4346 let head = (0..table_count)
4347 .map(|table| 12 + table * 16)
4348 .find(|record| &font[*record..*record + 4] == b"head")
4349 .unwrap();
4350 let head_offset =
4351 u32::from_be_bytes(font[head + 8..head + 12].try_into().unwrap()) as usize;
4352 let head_length =
4353 u32::from_be_bytes(font[head + 12..head + 16].try_into().unwrap()) as usize;
4354 font[head_offset + 8..head_offset + 12].fill(0);
4355 let head_checksum = checksum(&font[head_offset..head_offset + head_length]);
4356 font[head + 4..head + 8].copy_from_slice(&head_checksum.to_be_bytes());
4357 let adjustment = 0xb1b0afba_u32.wrapping_sub(checksum(&font));
4358 font[head_offset + 8..head_offset + 12].copy_from_slice(&adjustment.to_be_bytes());
4359 assert_eq!(checksum(&font), 0xb1b0afba);
4360 font
4361 }
4362
4363 fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
4364 let header_len = 12 + fonts.len() * 4;
4365 let mut collection = vec![0u8; header_len];
4366 collection[0..4].copy_from_slice(b"ttcf");
4367 collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
4368 collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
4369
4370 for (font_number, font) in fonts.iter().enumerate() {
4371 while !collection.len().is_multiple_of(4) {
4372 collection.push(0);
4373 }
4374 let collection_offset = collection.len();
4375 collection[12 + font_number * 4..16 + font_number * 4]
4376 .copy_from_slice(&(collection_offset as u32).to_be_bytes());
4377
4378 let mut adjusted = font.to_vec();
4379 let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
4380 for table in 0..table_count {
4381 let offset_position = 12 + table * 16 + 8;
4382 let offset = u32::from_be_bytes(
4383 adjusted[offset_position..offset_position + 4]
4384 .try_into()
4385 .expect("table offset"),
4386 );
4387 adjusted[offset_position..offset_position + 4]
4388 .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
4389 }
4390 collection.extend_from_slice(&adjusted);
4391 }
4392 collection
4393 }
4394
4395 #[test]
4396 fn deterministic_font_manager_uses_only_bundled_fonts() {
4397 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4398
4399 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
4400 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
4401 }
4402
4403 #[cfg(feature = "system-fonts")]
4404 #[test]
4405 fn normal_font_discovery_initializes_once_per_process() {
4406 let _first = FontManager::new();
4407 let _second = FontManager::new();
4408 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
4409
4410 let _deterministic =
4411 FontManager::new_deterministic().expect("bundled font manager should load");
4412 let _caller = FontManager::new_with_fonts(Vec::new());
4413 assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
4414 }
4415
4416 #[cfg(feature = "system-fonts")]
4417 #[test]
4418 fn file_backed_collection_faces_share_one_byte_buffer() {
4419 let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
4420 let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
4421 let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
4422 let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
4423 std::fs::write(&first_path, &collection).expect("write first temporary collection");
4424 std::fs::write(&second_path, &collection).expect("write second temporary collection");
4425
4426 let mut db = fontdb::Database::new();
4427 db.load_font_file(&first_path).expect("load first TTC");
4428 db.load_font_file(&second_path).expect("load second TTC");
4429 let canonical_first = std::fs::canonicalize(&first_path).unwrap();
4430 let canonical_second = std::fs::canonicalize(&second_path).unwrap();
4431 let first_ids = db
4432 .faces()
4433 .filter_map(|face| match &face.source {
4434 fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
4435 Some(face.id)
4436 }
4437 _ => None,
4438 })
4439 .collect::<Vec<_>>();
4440 let second_id = db
4441 .faces()
4442 .find_map(|face| match &face.source {
4443 fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
4444 Some(face.id)
4445 }
4446 _ => None,
4447 })
4448 .expect("second TTC face");
4449 assert_eq!(first_ids.len(), 2);
4450
4451 let mut memory = HashMap::new();
4452 let (first_face, first_index) =
4453 font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
4454 let (second_face, second_index) =
4455 font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
4456 let (other_file, _) =
4457 font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
4458 assert_ne!(first_index, second_index);
4459 assert!(Arc::ptr_eq(&first_face, &second_face));
4460 assert!(!Arc::ptr_eq(&first_face, &other_file));
4461
4462 std::fs::remove_file(first_path).expect("remove first temporary font");
4463 std::fs::remove_file(second_path).expect("remove second temporary font");
4464 }
4465
4466 #[test]
4467 fn shaping_memo_uses_complete_text_size_and_font_identity() {
4468 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4469 let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4470 let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
4471
4472 let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
4473 let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
4474 assert_eq!(first.glyph_ids, repeat.glyph_ids);
4475 assert_eq!(fm.shaping_memo_counts().0, 1);
4476
4477 fm.shape_text(regular, "different text", 11.0).unwrap();
4478 fm.shape_text(regular, "exact text", 12.0).unwrap();
4479 fm.shape_text(bold, "exact text", 11.0).unwrap();
4480 assert_eq!(fm.shaping_memo_counts().1, 4);
4481
4482 let replacement = FontFile {
4483 family: "Carlito".to_owned(),
4484 data: bundled_font_data()[1].1.to_vec(),
4485 };
4486 fm.load_additional_fonts(&[replacement]);
4487 assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
4488 let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4489 fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
4490 assert_eq!(fm.shaping_memo_counts().1, 1);
4491 }
4492
4493 #[test]
4494 fn arabic_joining_survives_script_and_line_break_boundaries() {
4495 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4496 let segment = multilingual_test_segment(&mut fm, "العربية", 18.0, None);
4497 let shaped = fm
4498 .shape_multilingual_text(segment, Some("ar"), TextDirection::RightToLeft, false)
4499 .unwrap();
4500 assert_eq!(shaped.len(), 1);
4501 assert_eq!(
4502 shaped[0].glyph_ids(),
4503 &[288, 85, 319, 18, 317, 19, 31, 48, 72, 8]
4504 );
4505 assert_eq!(
4506 shaped[0]
4507 .clusters()
4508 .iter()
4509 .map(|cluster| (cluster.glyph_start..cluster.glyph_end, cluster.char_range()))
4510 .collect::<Vec<_>>(),
4511 vec![
4512 (0..2, 6..7),
4513 (2..4, 5..6),
4514 (4..6, 4..5),
4515 (6..7, 3..4),
4516 (7..8, 2..3),
4517 (8..9, 1..2),
4518 (9..10, 0..1),
4519 ]
4520 );
4521 }
4522
4523 #[test]
4524 fn indic_clusters_are_never_split_or_mapped_as_independent_scalars() {
4525 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4526 let segment = multilingual_test_segment(&mut fm, "कि", 18.0, None);
4527 let shaped = fm
4528 .shape_multilingual_text(segment, Some("hi"), TextDirection::LeftToRight, false)
4529 .unwrap();
4530
4531 assert_eq!(shaped[0].clusters()[0].char_range(), 0..2);
4532 assert_eq!(shaped[0].x_offsets().len(), shaped[0].glyph_ids().len());
4533 assert_eq!(shaped[0].y_offsets().len(), shaped[0].glyph_ids().len());
4534 }
4535
4536 #[test]
4537 fn same_script_coverage_changes_split_only_between_graphemes() {
4538 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4539 let font_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4540 let metrics = fm.metrics(font_id, 18.0).unwrap();
4541 let seed = fm.shape_text(font_id, "A☀∙", 18.0).unwrap();
4542 let shaped = fm
4543 .shape_multilingual_text(
4544 TextSegment {
4545 text: "A☀∙".to_owned(),
4546 direction: TextDirection::Auto,
4547 source: None,
4548 font_id,
4549 font_size: 18.0,
4550 glyph_ids: seed.glyph_ids,
4551 advances: seed.advances,
4552 width: seed.width,
4553 ascent: metrics.ascent,
4554 descent: metrics.descent,
4555 line_gap: metrics.line_gap,
4556 color: Color::BLACK,
4557 bold: false,
4558 italic: false,
4559 underline: None,
4560 strike: false,
4561 dstrike: false,
4562 highlight: None,
4563 baseline_offset: 0.0,
4564 hyperlink_url: None,
4565 field_kind: None,
4566 field_source: None,
4567 note: None,
4568 note_reference_source: None,
4569 },
4570 None,
4571 TextDirection::LeftToRight,
4572 false,
4573 )
4574 .unwrap();
4575
4576 assert_eq!(
4577 shaped.iter().map(|span| span.text()).collect::<Vec<_>>(),
4578 ["A", "☀", "∙"]
4579 );
4580 assert!(shaped.iter().all(|span| span.script() == TextScript::Latin));
4581 assert_ne!(shaped[1].font_id(), shaped[2].font_id());
4582 assert!(shaped.iter().all(|span| {
4583 let index = fm.index_of(span.font_id()).unwrap();
4584 fm.uncovered(index, span.text()).is_empty()
4585 }));
4586 }
4587
4588 #[test]
4589 fn multilingual_constructor_rejects_invalid_levels_and_cluster_maps() {
4590 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4591 let segment = multilingual_test_segment(&mut fm, "ab", 18.0, None);
4592 let valid = fm
4593 .shape_multilingual_text(segment, None, TextDirection::LeftToRight, true)
4594 .unwrap()
4595 .remove(0);
4596 let rebuild = |bidi_level, clusters| {
4597 MultilingualTextSegment::new(
4598 valid.base().clone(),
4599 valid.logical_index(),
4600 valid.language().map(str::to_owned),
4601 valid.script(),
4602 valid.direction(),
4603 bidi_level,
4604 valid.x_advances().to_vec(),
4605 valid.y_advances().to_vec(),
4606 valid.x_offsets().to_vec(),
4607 valid.y_offsets().to_vec(),
4608 clusters,
4609 valid.break_after(),
4610 )
4611 };
4612
4613 assert!(rebuild(255, valid.clusters().to_vec()).is_err());
4614 let mut out_of_bounds = valid.clusters().to_vec();
4615 out_of_bounds.last_mut().unwrap().char_end = 3;
4616 assert!(rebuild(valid.bidi_level(), out_of_bounds).is_err());
4617 let mut glyph_gap = valid.clusters().to_vec();
4618 glyph_gap[0].glyph_start = 1;
4619 assert!(rebuild(valid.bidi_level(), glyph_gap).is_err());
4620 }
4621
4622 #[test]
4623 fn thai_words_offer_approved_breaks_without_losing_source_text() {
4624 let text = "ภาษาไทยยินดีต้อนรับ";
4625 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4626 let source = SourceSpan {
4627 node: crate::SourceNodeId::new(3).unwrap(),
4628 char_start: 10,
4629 char_end: 29,
4630 };
4631 let segment = multilingual_test_segment(&mut fm, text, 18.0, Some(source));
4632 let shaped = fm
4633 .shape_multilingual_text(segment, Some("th"), TextDirection::LeftToRight, false)
4634 .unwrap();
4635 assert_eq!(
4636 shaped
4637 .iter()
4638 .map(|span| {
4639 let source = span.base().source.unwrap();
4640 (
4641 span.text(),
4642 span.break_after(),
4643 source.char_start..source.char_end,
4644 )
4645 })
4646 .collect::<Vec<_>>(),
4647 vec![
4648 ("ภาษา", true, 10..14),
4649 ("ไทยยิน", true, 14..20),
4650 ("ดี", true, 20..22),
4651 ("ต้อน", true, 22..26),
4652 ("รับ", true, 26..29),
4653 ]
4654 );
4655 assert_eq!(
4656 shaped.iter().map(|span| span.text()).collect::<String>(),
4657 text
4658 );
4659 }
4660
4661 #[test]
4662 fn latin_spans_end_at_line_break_opportunities_with_their_spaces_apart() {
4663 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4664 let segment = multilingual_test_segment(&mut fm, "in service, or re-coated", 18.0, None);
4665 let shaped = fm
4666 .shape_multilingual_text(segment, None, TextDirection::LeftToRight, false)
4667 .unwrap();
4668
4669 assert_eq!(
4672 shaped
4673 .iter()
4674 .map(|span| (span.text(), span.break_after()))
4675 .collect::<Vec<_>>(),
4676 vec![
4677 ("in", false),
4678 (" ", true),
4679 ("service,", false),
4680 (" ", true),
4681 ("or", false),
4682 (" ", true),
4683 ("re-", true),
4684 ("coated", true),
4685 ]
4686 );
4687 }
4688
4689 #[test]
4690 fn a_word_split_across_styled_segments_has_no_break_where_they_meet() {
4691 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4692 let first = multilingual_test_segment(&mut fm, "Well-kno", 18.0, None);
4693 let second = multilingual_test_segment(&mut fm, "wn state", 18.0, None);
4694 let shaped = fm
4695 .shape_multilingual_paragraph(
4696 vec![(first, None), (second, None)],
4697 TextDirection::LeftToRight,
4698 false,
4699 )
4700 .unwrap();
4701
4702 assert_eq!(
4703 shaped
4704 .iter()
4705 .map(|span| (span.text(), span.break_after()))
4706 .collect::<Vec<_>>(),
4707 vec![
4708 ("Well-", true),
4709 ("kno", false),
4710 ("wn", false),
4711 (" ", true),
4712 ("state", true),
4713 ]
4714 );
4715 }
4716
4717 #[test]
4718 fn shaping_memo_hits_preserve_fifo_order() {
4719 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4720 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4721 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
4722 fm.shape_text(font, "newest exact shape", 11.0).unwrap();
4723 fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
4724
4725 let memo = fm.shaping_memo.lock().unwrap();
4726 assert_eq!(memo.hits, 1);
4727 assert_eq!(memo.entries.front().unwrap().0.text, "oldest exact shape");
4728 assert_eq!(memo.entries.back().unwrap().0.text, "newest exact shape");
4729 }
4730
4731 #[test]
4732 fn shaping_memo_fingerprint_collision_requires_exact_key_equality() {
4733 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4734 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4735 fm.shape_text(font, "first collision candidate", 11.0)
4736 .unwrap();
4737 let forced = shaping_fingerprint(font, "second collision candidate", 11.0f64.to_bits());
4738 fm.shaping_memo.lock().unwrap().entries[0].3 = forced;
4739
4740 fm.shape_text(font, "second collision candidate", 11.0)
4741 .unwrap();
4742
4743 let (hits, misses, entries, _) = fm.shaping_memo_counts();
4744 assert_eq!((hits, misses, entries), (0, 2, 2));
4745 }
4746
4747 #[test]
4748 fn shaping_memo_is_bounded_and_recovers_from_poison() {
4749 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
4750 let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4751 for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
4752 fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
4753 .unwrap();
4754 }
4755 let (_, _, entries, bytes) = fm.shaping_memo_counts();
4756 assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
4757 assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
4758
4759 let fm = Arc::new(fm);
4760 let poison = Arc::clone(&fm);
4761 assert!(
4762 std::thread::spawn(move || {
4763 let _guard = poison.shaping_memo.lock().unwrap();
4764 panic!("poison shaping cache for recovery coverage");
4765 })
4766 .join()
4767 .is_err()
4768 );
4769 let first = fm.shape_text(font, "after poison", 11.0).unwrap();
4770 let second = fm.shape_text(font, "after poison", 11.0).unwrap();
4771 assert_eq!(first.glyph_ids, second.glyph_ids);
4772 let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
4773 assert_eq!((hits, misses, entries), (1, 1, 1));
4774 assert!(bytes > 0);
4775 }
4776
4777 #[test]
4778 fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
4779 let mut memo = ShapingMemo::new();
4780 for suffix in ['a', 'b'] {
4781 memo.insert(
4782 ShapingKey {
4783 font_id: FontId(0),
4784 text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
4785 size_bits: 11.0f64.to_bits(),
4786 },
4787 ShapedText {
4788 glyph_ids: Vec::new(),
4789 advances: Vec::new(),
4790 width: 0.0,
4791 },
4792 );
4793 }
4794 assert_eq!(memo.entries.len(), 1);
4795 assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
4796 }
4797
4798 #[test]
4799 fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
4800 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
4801 for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
4802 fm.remember_coverage_fallback(false, false, 0);
4803 }
4804 assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
4805
4806 let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
4807 .filter_map(|value| char::from_u32(0x10_000 + value as u32))
4808 .collect::<Vec<_>>();
4809 fm.remember_coverage_misses(&misses);
4810 assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
4811
4812 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
4813 fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
4814 .expect("bounded fallback resolves");
4815 }
4816 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
4817 assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
4818 }
4819
4820 #[test]
4821 fn active_document_may_resolve_more_than_256_distinct_faces() {
4822 let source = bundled_font_data()[4].1;
4823 let mut db = fontdb::Database::new();
4824 for index in 0..257 {
4825 db.load_font_data(font_with_family(source, &format!("F{index:06}")));
4826 }
4827 let mut fm = FontManager::from_base_database(db);
4828 fm.begin_layout();
4829 let mut ids = HashSet::new();
4830 for index in 0..257 {
4831 let family = format!("F{index:06}");
4832 let id = fm
4833 .resolve_font(Some(&family), false, false)
4834 .expect("distinct active face resolves");
4835 assert_eq!(fm.font_data(id).unwrap().family, family);
4836 ids.insert(id);
4837 }
4838 assert_eq!(ids.len(), 257);
4839 fm.retain_current_fonts();
4840 assert_eq!(fm.fonts.len(), 257);
4841 }
4842
4843 #[test]
4844 fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
4845 let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
4846 fm.begin_layout();
4847 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
4848 fm.resolve_font(Some("Carlito"), false, false).unwrap();
4849 }
4850 assert!(fm.paragraph_font_trace.is_none());
4851
4852 fm.begin_paragraph_font_trace();
4853 for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
4854 fm.resolve_font(Some("Carlito"), false, false).unwrap();
4855 }
4856 assert!(fm.finish_paragraph_font_trace().is_none());
4857
4858 fm.begin_layout();
4859 assert_eq!(fm.layout_fonts.capacity(), 0);
4860 fm.begin_paragraph_font_trace();
4861 fm.resolve_font(Some("Carlito"), false, false).unwrap();
4862 let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
4863 assert_eq!(trace.len(), 1);
4864 assert_eq!(trace.capacity(), trace.len());
4865 }
4866
4867 #[test]
4868 fn fx195_contribution_scope_preserves_legacy_and_original_requests() {
4869 let mut fm = FontManager::new_deterministic().unwrap();
4870 fm.begin_layout();
4871 fm.begin_paragraph_font_trace();
4872 let id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4873 let scope = fm.begin_font_contribution_scope().unwrap();
4874 fm.record_font_contribution(id, Some("Aptos"), "A").unwrap();
4875 fm.record_font_contribution(id, Some("Calibri"), "A")
4876 .unwrap();
4877 let staged = fm.finish_font_contribution_scope(scope).unwrap().unwrap();
4878 assert_eq!(staged.len(), 2);
4879 let projected = staged
4880 .iter()
4881 .filter_map(|event| match event {
4882 FontTraceEvent::Contribution(contribution) => Some(contribution),
4883 FontTraceEvent::Resolution(_) => None,
4884 })
4885 .collect::<Vec<_>>();
4886 assert_eq!(projected[0].font_id, projected[1].font_id);
4887 assert_ne!(projected[0].requested_family, projected[1].requested_family);
4888 assert!(fm.finish_paragraph_font_contributions().unwrap().is_empty());
4889 fm.begin_paragraph_font_trace();
4890 fm.replay_layout_font_trace(&[id]);
4891 fm.replay_font_trace_events(&staged).unwrap();
4892 assert_eq!(fm.finish_paragraph_font_trace().unwrap(), vec![id]);
4893 fm.begin_paragraph_font_trace();
4894 fm.replay_font_trace_events(&staged).unwrap();
4895 assert_eq!(
4896 fm.finish_paragraph_font_contributions().unwrap(),
4897 projected.into_iter().cloned().collect::<Vec<_>>()
4898 );
4899 }
4900
4901 #[test]
4902 fn fx195_contribution_nested_stale_and_empty_scopes_are_atomic() {
4903 let mut fm = FontManager::new_deterministic().unwrap();
4904 let id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4905 fm.begin_layout();
4906 fm.begin_paragraph_font_trace();
4907 fm.record_font_contribution(id, Some("outer"), "A").unwrap();
4908 let outer = fm.begin_font_contribution_scope().unwrap();
4909 let inner = fm.begin_font_contribution_scope().unwrap();
4910 fm.record_font_contribution(id, Some("inner"), "A").unwrap();
4911 let before = format!("{:?}", fm.paragraph_font_trace.as_ref().unwrap().events);
4912 assert!(fm.finish_font_contribution_scope(outer).is_err());
4913 assert_eq!(
4914 format!("{:?}", fm.paragraph_font_trace.as_ref().unwrap().events),
4915 before
4916 );
4917 let captured = fm.finish_font_contribution_scope(inner).unwrap().unwrap();
4918 assert_eq!(captured.len(), 1);
4919 let stale = fm.begin_font_contribution_scope().unwrap();
4920 fm.begin_paragraph_font_trace();
4921 fm.record_font_contribution(id, Some("replacement"), "A")
4922 .unwrap();
4923 let inventory = fm.current_layout_fonts().to_vec();
4924 assert!(fm.finish_font_contribution_scope(stale).is_err());
4925 assert_eq!(fm.current_layout_fonts(), inventory);
4926 let remaining = fm.finish_paragraph_font_contributions().unwrap();
4927 assert_eq!(remaining.len(), 1);
4928 assert_eq!(
4929 remaining[0].requested_family.as_deref(),
4930 Some("replacement")
4931 );
4932 fm.begin_paragraph_font_trace();
4933 fm.record_font_contribution(id, Some("empty"), "").unwrap();
4934 assert!(fm.finish_paragraph_font_contributions().unwrap().is_empty());
4935 fm.begin_paragraph_font_trace();
4936 fm.resolve_font(Some("Carlito"), false, false).unwrap();
4937 assert!(fm.finish_paragraph_font_contributions().unwrap().is_empty());
4938 }
4939
4940 #[test]
4941 fn fx195_contribution_replay_refusal_and_capture_overflow_are_atomic() {
4942 let mut fm = FontManager::new_deterministic().unwrap();
4943 let id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
4944 fm.begin_layout();
4945 fm.begin_paragraph_font_trace();
4946 fm.record_font_contribution(id, Some("kept"), "A").unwrap();
4947 let before = format!("{:?}", fm.paragraph_font_trace.as_ref().unwrap().events);
4948 let inventory = fm.current_layout_fonts().to_vec();
4949 let bad = vec![
4950 FontTraceEvent::Contribution(FontContribution {
4951 font_id: id,
4952 requested_family: Some("valid first".into()),
4953 }),
4954 FontTraceEvent::Contribution(FontContribution {
4955 font_id: FontId(u32::MAX),
4956 requested_family: None,
4957 }),
4958 ];
4959 assert!(fm.replay_font_trace_events(&bad).is_err());
4960 assert_eq!(fm.current_layout_fonts(), inventory);
4961 assert_eq!(
4962 format!("{:?}", fm.paragraph_font_trace.as_ref().unwrap().events),
4963 before
4964 );
4965 let huge = "x".repeat(PARAGRAPH_FONT_TRACE_MAX_BYTES + 1);
4966 assert!(
4967 fm.replay_font_trace_events(&[FontTraceEvent::Contribution(FontContribution {
4968 font_id: id,
4969 requested_family: Some(huge.clone()),
4970 })])
4971 .is_err()
4972 );
4973 assert_eq!(fm.current_layout_fonts(), inventory);
4974 assert_eq!(
4975 format!("{:?}", fm.paragraph_font_trace.as_ref().unwrap().events),
4976 before
4977 );
4978 let scope = fm.begin_font_contribution_scope().unwrap();
4979 fm.record_font_contribution(id, Some(&huge), "A").unwrap();
4980 assert!(fm.finish_font_contribution_scope(scope).unwrap().is_none());
4981 assert!(fm.finish_paragraph_font_contributions().is_none());
4982 assert!(fm.paragraph_font_trace.is_none());
4983 fm.begin_paragraph_font_trace();
4984 for _ in 0..=PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
4985 fm.record_font_contribution(id, Some("bounded"), "A")
4986 .unwrap();
4987 }
4988 assert!(fm.finish_paragraph_font_contributions().is_none());
4989 assert!(fm.paragraph_font_trace.is_none());
4990 }
4991
4992 #[test]
4993 fn fx195_contribution_scope_rejects_other_manager_and_bounds_private_context() {
4994 let mut first = FontManager::new_deterministic().unwrap();
4995 let mut second = FontManager::new_deterministic().unwrap();
4996 let id = second.resolve_font(Some("Carlito"), false, false).unwrap();
4997 let foreign_scope = first.begin_font_contribution_scope().unwrap();
4998 let own_scope = second.begin_font_contribution_scope().unwrap();
4999 second
5000 .record_font_contribution(id, Some("kept"), "A")
5001 .unwrap();
5002 let before = format!("{:?}", second.paragraph_font_trace.as_ref().unwrap().events);
5003 let inventory = second.current_layout_fonts().to_vec();
5004 assert!(
5005 second
5006 .finish_font_contribution_scope(foreign_scope)
5007 .is_err()
5008 );
5009 assert_eq!(
5010 format!("{:?}", second.paragraph_font_trace.as_ref().unwrap().events),
5011 before
5012 );
5013 assert_eq!(second.current_layout_fonts(), inventory);
5014 assert!(
5015 second
5016 .finish_font_contribution_scope(own_scope)
5017 .unwrap()
5018 .is_some()
5019 );
5020 second.begin_paragraph_font_trace();
5021 let mut bytes = PARAGRAPH_FONT_TRACE_MAX_BYTES;
5022 let mut entries = 0;
5023 assert!(
5024 second
5025 .copy_font_request_context(Some("no extra allocation"), &mut bytes, &mut entries)
5026 .is_none()
5027 );
5028 assert_eq!(entries, 0);
5029 assert!(second.finish_paragraph_font_events().is_none());
5030 second.begin_paragraph_font_trace();
5031 let mut bytes = 0;
5032 let mut entries = PARAGRAPH_FONT_TRACE_MAX_ENTRIES;
5033 assert!(
5034 second
5035 .copy_font_request_context(Some("entry overflow"), &mut bytes, &mut entries)
5036 .is_none()
5037 );
5038 assert_eq!(bytes, PARAGRAPH_FONT_TRACE_MAX_BYTES);
5039 assert!(second.finish_paragraph_font_events().is_none());
5040 }
5041
5042 #[cfg(feature = "system-fonts")]
5043 #[test]
5044 fn file_byte_cache_is_bounded_and_recovers_from_poison() {
5045 let cache = Arc::new(Mutex::new(FileFontCache::new()));
5046 {
5047 let mut cache = cache
5048 .lock()
5049 .unwrap_or_else(std::sync::PoisonError::into_inner);
5050 cache.clear();
5051 let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
5052 let returned = cache_file_font_bytes(
5053 &mut cache,
5054 PathBuf::from("oversized-font.ttc"),
5055 Arc::clone(&oversized),
5056 )
5057 .expect("oversized bytes are returned uncached");
5058 assert!(Arc::ptr_eq(&oversized, &returned));
5059 assert!(cache.entries.is_empty());
5060 assert_eq!(cache.bytes, 0);
5061 }
5062
5063 let poison = Arc::clone(&cache);
5064 assert!(
5065 std::thread::spawn(move || {
5066 let cache = poison;
5067 let _guard = cache.lock().unwrap();
5068 panic!("poison file byte cache for recovery coverage");
5069 })
5070 .join()
5071 .is_err()
5072 );
5073
5074 let path = std::env::temp_dir().join(format!(
5075 "rdocx-font-cache-poison-{}-{:?}.ttf",
5076 std::process::id(),
5077 std::thread::current().id()
5078 ));
5079 std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
5080 let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
5081 let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
5082 assert!(Arc::ptr_eq(&first, &second));
5083 std::fs::remove_file(path).expect("remove recovery font");
5084 }
5085
5086 #[cfg(not(feature = "system-fonts"))]
5087 #[test]
5088 fn no_default_features_omits_system_font_discovery() {
5089 let fm = FontManager::new();
5090 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
5091 }
5092
5093 #[test]
5094 fn font_manager_with_no_fonts_returns_an_error() {
5095 let mut fm = FontManager::new_with_fonts(Vec::new());
5096 assert!(matches!(
5097 fm.resolve_font(None, false, false),
5098 Err(LayoutError::FontNotFound(_))
5099 ));
5100 }
5101
5102 #[test]
5103 fn load_system_font() {
5104 let mut fm = FontManager::new();
5105 let result = fm.resolve_font(None, false, false);
5107 if let Ok(id) = result {
5109 assert_eq!(id.0, 0);
5110 }
5111 }
5112
5113 #[test]
5114 fn font_metrics_positive() {
5115 let mut fm = FontManager::new();
5116 if let Ok(id) = fm.resolve_font(None, false, false) {
5117 let metrics = fm.metrics(id, 12.0).unwrap();
5118 assert!(metrics.ascent > 0.0);
5119 assert!(metrics.descent > 0.0);
5120 assert!(metrics.units_per_em > 0);
5121 }
5122 }
5123
5124 #[test]
5125 fn shape_hello_world() {
5126 let mut fm = FontManager::new();
5127 if let Ok(id) = fm.resolve_font(None, false, false) {
5128 let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
5129 assert!(!shaped.glyph_ids.is_empty());
5130 assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
5131 assert!(shaped.width > 0.0);
5132 }
5133 }
5134
5135 #[test]
5136 fn font_caching() {
5137 let mut fm = FontManager::new();
5138 if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
5139 let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
5140 assert_eq!(id1, id2);
5141 }
5142 }
5143
5144 #[test]
5145 fn font_resolution_alias_cache_is_bounded() {
5146 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
5147 for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
5148 fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
5149 .expect("fallback font resolves");
5150 }
5151 assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
5152 assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
5153 }
5154
5155 #[test]
5156 fn bold_italic_variants() {
5157 let mut fm = FontManager::new();
5158 let regular = fm.resolve_font(None, false, false);
5159 let bold = fm.resolve_font(None, true, false);
5160 if let (Ok(r), Ok(b)) = (regular, bold) {
5161 assert_ne!(r, b);
5163 }
5164 }
5165
5166 #[test]
5169 fn latin_text_resolves_the_same_as_by_name() {
5170 let mut fm = FontManager::new();
5171 let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
5172 return;
5173 };
5174 let for_text = fm
5175 .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
5176 .unwrap();
5177 assert_eq!(by_name, for_text);
5178 }
5179
5180 #[test]
5186 fn text_no_font_can_draw_keeps_the_requested_font() {
5187 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
5188 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
5189 let resolved = fm
5190 .resolve_font_for_text(Some("Carlito"), false, false, "🀄")
5191 .unwrap();
5192 assert_eq!(
5193 primary, resolved,
5194 "with no covering font available the original must be kept"
5195 );
5196 }
5197
5198 #[test]
5200 fn whitespace_does_not_trigger_a_fallback() {
5201 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
5202 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
5203 let idx = fm.index_of(by_name).unwrap();
5204 assert!(
5206 fm.uncovered(idx, "a\u{00a0}b\tc")
5207 .iter()
5208 .all(|c| *c != '\t'),
5209 "control and whitespace characters must be ignored"
5210 );
5211 }
5212
5213 #[test]
5219 fn cjk_text_moves_off_a_latin_font_when_possible() {
5220 let mut fm = FontManager::new();
5221 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
5222 return;
5223 };
5224 let Some(idx) = fm.index_of(latin) else {
5225 return;
5226 };
5227 if fm.uncovered(idx, "这是中文").is_empty() {
5228 return; }
5230 let resolved = fm
5231 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
5232 .unwrap();
5233 if resolved == latin {
5234 return; }
5236 let new_idx = fm.index_of(resolved).unwrap();
5237 assert!(
5238 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
5239 "the replacement must cover more of the text than the original"
5240 );
5241 }
5242
5243 #[test]
5244 fn hangul_and_kana_receive_their_own_script_and_shaper_tag() {
5245 for character in [
5246 '\u{1100}', '\u{1112}', '\u{3131}', '\u{318e}', '\u{a960}', '\u{a97c}', '한', '힣',
5247 '\u{d7b0}', '\u{d7ff}', '\u{ffa0}', '\u{ffdc}',
5248 ] {
5249 assert_eq!(
5250 script_for_char(character),
5251 TextScript::Hangul,
5252 "{character:?} is Hangul"
5253 );
5254 }
5255 for character in [
5256 '\u{3041}', 'こ', 'ン', '\u{30ff}', '\u{31f0}', '\u{31ff}', '\u{ff66}', '\u{ff9f}',
5257 ] {
5258 assert_eq!(
5259 script_for_char(character),
5260 TextScript::Kana,
5261 "{character:?} is Kana"
5262 );
5263 }
5264 assert_eq!(
5265 harfrust_script(TextScript::Hangul),
5266 harfrust::script::HANGUL
5267 );
5268 assert_eq!(
5269 harfrust_script(TextScript::Kana),
5270 harfrust::script::HIRAGANA
5271 );
5272
5273 let previously_mapped = |codepoint: u32| match codepoint {
5277 0x0041..=0x024f | 0x1e00..=0x1eff => Some(TextScript::Latin),
5278 0x0590..=0x05ff => Some(TextScript::Hebrew),
5279 0x0600..=0x06ff | 0x0750..=0x077f | 0x08a0..=0x08ff => Some(TextScript::Arabic),
5280 0x0900..=0x097f | 0xa8e0..=0xa8ff => Some(TextScript::Devanagari),
5281 0x0e00..=0x0e7f => Some(TextScript::Thai),
5282 0x3400..=0x4dbf | 0x4e00..=0x9fff | 0xf900..=0xfaff => Some(TextScript::Han),
5283 _ => None,
5284 };
5285 for codepoint in 0..=0xffff_u32 {
5286 let Some(character) = char::from_u32(codepoint) else {
5287 continue;
5288 };
5289 let Some(expected) = previously_mapped(codepoint) else {
5290 continue;
5291 };
5292 assert_eq!(
5293 script_for_char(character),
5294 expected,
5295 "U+{codepoint:04X} changed script"
5296 );
5297 }
5298 }
5299
5300 #[test]
5301 fn a_mixed_script_string_shapes_every_script_with_a_covering_bundled_face() {
5302 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
5303 for text in ["안녕하세요", "こんにちは", "カタカナ", "שלום"] {
5304 let font_id = fm
5305 .resolve_font_for_text(Some("Carlito"), false, false, text)
5306 .expect("a bundled face covers the fixture repertoire");
5307 let index = fm.index_of(font_id).expect("resolved font is indexed");
5308 assert!(
5309 fm.uncovered(index, text).is_empty(),
5310 "no bundled face covers {text}"
5311 );
5312 }
5313 }
5314
5315 #[test]
5316 fn fx210_owned_diagnostics_roll_back_provisional_scopes_and_replay_final_events() {
5317 let mut fonts = FontManager::new_deterministic().unwrap();
5318 fonts.begin_layout();
5319 let id = fonts.resolve_font(Some("Calibri"), false, false).unwrap();
5320 fonts
5321 .record_font_contribution(id, Some("Aptos"), "accepted")
5322 .unwrap();
5323 let accepted = fonts.current_font_diagnostics().to_vec();
5324 assert_eq!(accepted.len(), 1);
5325 let outer = fonts.begin_font_contribution_scope().unwrap();
5326 fonts
5327 .record_font_contribution(id, Some("Aptos Display"), "provisional")
5328 .unwrap();
5329 let inner = fonts.begin_font_contribution_scope().unwrap();
5330 fonts
5331 .record_font_contribution(id, Some("Aptos"), "nested")
5332 .unwrap();
5333 let nested = fonts
5334 .finish_font_contribution_scope(inner)
5335 .unwrap()
5336 .unwrap();
5337 assert_eq!(fonts.current_font_diagnostics().len(), 2);
5338 let staged = fonts
5339 .finish_font_contribution_scope(outer)
5340 .unwrap()
5341 .unwrap();
5342 assert_eq!(fonts.current_font_diagnostics(), accepted);
5343 fonts.replay_font_trace_events(&staged).unwrap();
5344 assert_eq!(fonts.current_font_diagnostics().len(), 2);
5345 fonts.replay_font_trace_events(&nested).unwrap();
5346 assert_eq!(fonts.current_font_diagnostics().len(), 2);
5347 let final_facts = fonts.current_font_diagnostics().to_vec();
5348 fonts.begin_layout();
5349 assert!(fonts.current_font_diagnostics().is_empty());
5350 fonts
5351 .record_font_contribution(id, Some("Aptos"), "")
5352 .unwrap();
5353 fonts
5354 .record_font_contribution(id, Some("Calibri"), "accepted")
5355 .unwrap();
5356 assert!(fonts.current_font_diagnostics().is_empty());
5357 fonts.replay_font_trace_events(&staged).unwrap();
5358 assert_eq!(fonts.current_font_diagnostics(), &final_facts[1..]);
5359 }
5360
5361 #[test]
5362 fn fx210_overflow_cannot_lose_committed_warning_or_publish_provisional_warning() {
5363 let mut fonts = FontManager::new_deterministic().unwrap();
5364 fonts.begin_layout();
5365 let id = fonts.resolve_font(Some("Calibri"), false, false).unwrap();
5366 fonts
5367 .record_font_contribution(id, Some("Aptos"), "accepted")
5368 .unwrap();
5369 let accepted = fonts.current_font_diagnostics().to_vec();
5370 let scope = fonts.begin_font_contribution_scope().unwrap();
5371 for _ in 0..=PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
5372 fonts
5373 .record_font_contribution(id, Some("Aptos Display"), "staged")
5374 .unwrap();
5375 }
5376 assert!(
5377 fonts
5378 .finish_font_contribution_scope(scope)
5379 .unwrap()
5380 .is_none()
5381 );
5382 assert_eq!(fonts.current_font_diagnostics(), accepted);
5383 fonts.begin_paragraph_font_trace();
5384 for _ in 0..=PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
5385 fonts
5386 .record_font_contribution(id, Some("Calibri"), "accepted")
5387 .unwrap();
5388 }
5389 fonts
5390 .record_font_contribution(id, Some("Aptos Display"), "accepted")
5391 .unwrap();
5392 assert!(fonts.finish_paragraph_font_events().is_none());
5393 assert_eq!(fonts.current_font_diagnostics().len(), 2);
5394 let before = fonts.current_font_diagnostics().to_vec();
5395 assert!(
5396 fonts
5397 .replay_font_trace_events(&[FontTraceEvent::Contribution(FontContribution {
5398 font_id: FontId(u32::MAX),
5399 requested_family: Some("Aptos".into()),
5400 })])
5401 .is_err()
5402 );
5403 assert_eq!(fonts.current_font_diagnostics(), before);
5404 }
5405
5406 #[test]
5407 fn fx211c_original_symbol_coverage_authenticates_physical_family_and_glyphs() {
5408 let source = include_bytes!("../fonts/Carlito-Regular.ttf");
5409 let mut fonts = FontManager::new_deterministic().unwrap();
5410 fonts.load_additional_fonts(&[FontFile {
5411 family: "Wingdings".to_owned(),
5412 data: source.to_vec(),
5413 }]);
5414 let alias = fonts.resolve_font(Some("Wingdings"), false, false).unwrap();
5415 assert!(!fonts.has_original_symbol_coverage(alias, Some("Wingdings"), "ÿ"));
5416 fonts.load_additional_fonts(&[FontFile {
5417 family: "Wingdings".to_owned(),
5418 data: font_with_family(source, "Wingdings"),
5419 }]);
5420 let actual = fonts.resolve_font(Some("Wingdings"), false, false).unwrap();
5421 assert!(fonts.has_original_symbol_coverage(actual, Some("Wingdings"), "ÿ"));
5422 assert!(
5423 fonts
5424 .unknown_symbol_diagnostics(actual, Some("Wingdings"), "ÿ", "ÿ")
5425 .is_empty()
5426 );
5427 assert!(
5428 fonts
5429 .unknown_symbol_diagnostics(
5430 FontId(u32::MAX),
5431 Some("Wingdings"),
5432 "\u{f0ff}",
5433 "\u{f0ff}"
5434 )
5435 .is_empty()
5436 );
5437 let genuine = fonts.font_data(actual).unwrap();
5438 let original = ttf_parser::Face::parse(source, 0).unwrap();
5439 let renamed = ttf_parser::Face::parse(genuine.data.as_ref(), 0).unwrap();
5440 assert_eq!(renamed.glyph_index('ÿ'), original.glyph_index('ÿ'));
5441 let count = u16::from_be_bytes([source[4], source[5]]) as usize;
5442 for table in 0..count {
5443 let tag = ttf_parser::Tag::from_bytes(
5444 source[12 + table * 16..16 + table * 16].try_into().unwrap(),
5445 );
5446 if tag != ttf_parser::Tag::from_bytes(b"name")
5447 && tag != ttf_parser::Tag::from_bytes(b"head")
5448 {
5449 assert_eq!(
5450 renamed.raw_face().table(tag),
5451 original.raw_face().table(tag)
5452 );
5453 }
5454 }
5455
5456 assert!(!fonts.has_original_symbol_coverage(actual, Some("Wingdings"), "\u{f0ff}"));
5457 assert!(!fonts.has_original_symbol_coverage(actual, Some("Symbol"), "ÿ"));
5458 let resolved = fonts
5459 .resolve_symbol_text(Some("Wingdings"), false, false, "ÿ", true)
5460 .unwrap();
5461 assert_eq!(
5462 resolved,
5463 ("ÿ".to_owned(), actual),
5464 "genuine original glyph beats semantic fallback"
5465 );
5466 }
5467
5468 #[test]
5469 fn fx210_explicit_symbol_table_is_bounded_and_covered() {
5470 let mut fonts = FontManager::new_deterministic().unwrap();
5471 for (family, code, target) in [
5472 ("Wingdings", 0xa7, '\u{25aa}'),
5473 ("Wingdings", 0xd8, '\u{27a2}'),
5474 ("Wingdings", 0xfc, '\u{2713}'),
5475 ("Wingdings", 0x76, '\u{2756}'),
5476 ("Wingdings", 0x71, '\u{2751}'),
5477 ("Wingdings", 0x6e, '\u{25a0}'),
5478 ("Wingdings", 0xfe, '\u{2611}'),
5479 ("Symbol", 0xb7, '\u{2022}'),
5480 ] {
5481 for code in [code, code + 0xf000] {
5482 let original = char::from_u32(code).unwrap().to_string();
5483 let (display, id) = fonts
5484 .resolve_symbol_text(Some(family), false, false, &original, false)
5485 .unwrap();
5486 assert_eq!(display, target.to_string());
5487 let shaped = fonts.shape_text(id, &display, 12.0).unwrap();
5488 assert!(!shaped.glyph_ids.is_empty());
5489 assert!(shaped.glyph_ids.iter().all(|glyph| *glyph != 0));
5490 }
5491 }
5492 for (family, original) in [
5493 ("Wingdings 2", "\u{f0a7}"),
5494 ("Unrelated", "\u{f0a7}"),
5495 ("Wingdings", "\u{f001}"),
5496 ("Wingdings", "\u{f0b7}"),
5497 ] {
5498 assert_eq!(
5499 fonts
5500 .resolve_symbol_text(Some(family), false, false, original, false)
5501 .unwrap()
5502 .0,
5503 original
5504 );
5505 }
5506 assert_eq!(
5507 fonts
5508 .resolve_symbol_text(Some("Wingdings"), false, false, "\u{f0b7}", true)
5509 .unwrap()
5510 .0,
5511 "\u{2022}"
5512 );
5513 let id = fonts.resolve_font(Some("Carlito"), false, false).unwrap();
5514 assert_eq!(
5515 fonts
5516 .resolve_symbol_text(Some("Carlito"), false, false, "v", false)
5517 .unwrap(),
5518 ("v".into(), id)
5519 );
5520 }
5521 #[test]
5522 fn fx210_caller_labels_cannot_authenticate_aptos_or_original_symbol_faces() {
5523 let source = bundled_font_data()
5524 .into_iter()
5525 .find(|(family, _)| *family == "Carlito")
5526 .unwrap()
5527 .1;
5528 let mut fonts = FontManager::new_deterministic().unwrap();
5529 fonts.load_additional_fonts(&[FontFile {
5530 family: "Aptos".to_owned(),
5531 data: source.to_vec(),
5532 }]);
5533 fonts.begin_layout();
5534 let selected = fonts.resolve_font(Some("Aptos"), false, false).unwrap();
5535 let index = fonts.index_of(selected).unwrap();
5536 assert_eq!(fonts.fonts[index].data.as_ref(), source);
5537 assert_eq!(fonts.fonts[index].face_index, 0);
5538 assert!(fonts.physical_requested_family_matches(index, "Carlito"));
5539 assert!(!fonts.physical_requested_family_matches(index, "Aptos"));
5540 fonts
5541 .record_font_contribution(selected, Some("Aptos"), "actual")
5542 .unwrap();
5543 assert_eq!(fonts.current_font_diagnostics().len(), 1);
5544 assert!(
5545 fonts.current_font_diagnostics()[0]
5546 .message
5547 .contains("Font 'Aptos' is approximate")
5548 );
5549 assert!(
5550 fonts.current_font_diagnostics()[0]
5551 .message
5552 .contains("Carlito")
5553 );
5554 fonts.begin_paragraph_font_trace();
5556 fonts
5557 .record_font_contribution(selected, Some("Aptos"), "actual")
5558 .unwrap();
5559 let events = fonts.finish_paragraph_font_events().unwrap();
5560 fonts.begin_layout();
5561 fonts.replay_font_trace_events(&events).unwrap();
5562 assert_eq!(fonts.current_font_diagnostics().len(), 1);
5563 assert_eq!(fonts.font_data(selected).unwrap().data.as_ref(), source);
5564
5565 fonts.load_additional_fonts(&[FontFile {
5566 family: "Wingdings".to_owned(),
5567 data: source.to_vec(),
5568 }]);
5569 let aliased = fonts.resolve_font(Some("Wingdings"), false, false).unwrap();
5570 let index = fonts.index_of(aliased).unwrap();
5571 assert_eq!(fonts.fonts[index].data.as_ref(), source);
5572 assert!(
5573 fonts.uncovered(index, "ü").is_empty(),
5574 "Carlito genuinely covers the low-byte operand"
5575 );
5576 assert!(!fonts.physical_requested_family_matches(index, "Wingdings"));
5577 let (display, id) = fonts
5578 .resolve_symbol_text(Some("Wingdings"), false, false, "ü", false)
5579 .unwrap();
5580 assert_eq!(
5581 display, "✓",
5582 "a caller label cannot give Carlito's Latin glyph original Wingdings precedence"
5583 );
5584 assert!(
5585 fonts
5586 .shape_text(id, &display, 12.0)
5587 .unwrap()
5588 .glyph_ids
5589 .iter()
5590 .all(|glyph| *glyph != 0)
5591 );
5592 let (original, genuine) = fonts
5593 .resolve_symbol_text(Some("Carlito"), false, false, "ü", false)
5594 .unwrap();
5595 assert_eq!(original, "ü");
5596 let index = fonts.index_of(genuine).unwrap();
5597 assert!(fonts.physical_requested_family_matches(index, "Carlito"));
5598 assert!(fonts.uncovered(index, &original).is_empty());
5599 assert_eq!(fonts.fonts[index].data.as_ref(), source);
5600 }
5601
5602 #[test]
5603 fn fx210_physical_family_authentication_reads_the_selected_collection_face() {
5604 let bundled = bundled_font_data();
5605 let caladea = bundled
5606 .iter()
5607 .find(|(family, _)| *family == "Caladea")
5608 .unwrap()
5609 .1;
5610 let carlito = bundled
5611 .iter()
5612 .find(|(family, _)| *family == "Carlito")
5613 .unwrap()
5614 .1;
5615 let collection = test_ttc(&[caladea, carlito]);
5616 let mut fonts = FontManager::new_deterministic().unwrap();
5617 fonts.load_additional_fonts(&[FontFile {
5618 family: "Two physical families".to_owned(),
5619 data: collection.clone(),
5620 }]);
5621 let selected = fonts.resolve_font(Some("Carlito"), false, false).unwrap();
5622 let index = fonts.index_of(selected).unwrap();
5623 assert_eq!(fonts.fonts[index].data.as_ref(), collection);
5624 assert_eq!(fonts.fonts[index].face_index, 1);
5625 assert!(fonts.physical_requested_family_matches(index, "Carlito"));
5626 assert!(!fonts.physical_requested_family_matches(index, "Caladea"));
5627 assert!(!fonts.physical_requested_family_matches(index, "Two physical families"));
5628 let first = fonts.resolve_font(Some("Caladea"), false, false).unwrap();
5629 let index = fonts.index_of(first).unwrap();
5630 assert_eq!(fonts.fonts[index].face_index, 0);
5631 assert!(fonts.physical_requested_family_matches(index, "Caladea"));
5632 assert!(!fonts.physical_requested_family_matches(index, "Carlito"));
5633 assert!(!fonts.physical_requested_family_matches(usize::MAX, "Carlito"));
5634 }
5635}