Skip to main content

oxml_layout/
font.rs

1//! Font loading, resolution, shaping, and metrics.
2//!
3//! Uses fontdb for system font discovery, ttf-parser for metrics,
4//! and HarfRust for text shaping.
5
6use 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/// Font data provided by the user or extracted from an OOXML file.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct FontFile {
23    /// Font family name (e.g., "Calibri", "Arial").
24    pub family: String,
25    /// Raw font file bytes (TTF/OTF).
26    pub data: Vec<u8>,
27}
28
29impl FontFile {
30    /// Read supported caller font files in lexical path order.
31    /// A missing directory, unreadable entry or malformed supported font fails
32    /// instead of silently changing the requested fallback configuration.
33    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/// Key for caching resolved fonts.
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85struct FontKey {
86    family: String,
87    bold: bool,
88    italic: bool,
89}
90
91/// Metrics for a font at a given size.
92#[derive(Debug, Clone, Copy)]
93pub struct FontMetrics {
94    /// Ascent in points (positive, above baseline).
95    pub ascent: f64,
96    /// Descent in points (positive, below baseline).
97    pub descent: f64,
98    /// Line gap in points.
99    pub line_gap: f64,
100    /// Units per em.
101    pub units_per_em: u16,
102}
103
104/// Result of shaping a text string.
105#[derive(Debug, Clone)]
106pub struct ShapedText {
107    /// Glyph IDs from shaping.
108    pub glyph_ids: Vec<u16>,
109    /// Per-glyph advances in points.
110    pub advances: Vec<f64>,
111    /// Total width in points.
112    pub width: f64,
113}
114
115/// Requested or resolved direction for one logical text span.
116#[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/// Script identity used to select shaping behavior and deterministic fallback.
169///
170/// This enum keeps growing as scripts are added, and every addition is a
171/// breaking change for a downstream exhaustive match. `#[non_exhaustive]`
172/// takes that break once, at the pre-1.0 moment it is cheapest.
173#[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 jamo, compatibility jamo and precomposed syllables.
183    Hangul,
184    /// Hiragana, Katakana and the Katakana phonetic extensions.
185    Kana,
186    Common,
187}
188
189/// One glyph interval mapped to an exclusive logical Unicode-scalar interval.
190#[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/// One script, font, and bidi-level span shaped with complete positioning data.
209#[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
516/// Internal record for a loaded font face.
517struct 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    /// Vertical metrics in design units, read once when the face is loaded.
527    ascender: i16,
528    descender: i16,
529    line_gap: i16,
530    /// The line metrics Word measures this face on, in design units, or
531    /// `None` for a face without Windows metrics. See
532    /// [`FontManager::word_line_metrics`].
533    word_line: Option<WordLineUnits>,
534    /// HarfRust's per-face shaping caches. Building these is the expensive
535    /// part of shaping, so it happens once per face instead of once per run.
536    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
543/// Preserve defaults unless a style was explicitly requested. Keep restricted
544/// instances distinct from synthesis rather than claiming an unavailable style.
545fn 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/// A face's line metrics as Windows reports them to Word, in design units.
586#[derive(Debug, Clone, Copy, PartialEq, Eq)]
587struct WordLineUnits {
588    /// Above the baseline.
589    ascent: i32,
590    /// Below the baseline, positive.
591    descent: i32,
592    /// External leading, never negative.
593    leading: i32,
594}
595
596impl WordLineUnits {
597    /// Read from the face's OS/2 table, or `None` when it has no Windows
598    /// metrics.
599    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        // `usWinAscent` and `usWinDescent` are unsigned. ttf-parser reads
609        // them as signed and negates the descent, which overflows at 32768,
610        // so the two fields are read here directly.
611        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/// One successfully shaped nonempty final text contribution.
635#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct FontContribution {
637    pub font_id: FontId,
638    pub requested_family: Option<String>,
639}
640
641/// A transactional local capture within the current bounded trace.
642#[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
665/// Manages font discovery, loading, shaping, and metrics.
666pub struct FontManager {
667    db: fontdb::Database,
668    /// Database before document-embedded or caller fonts are applied.
669    base_db: fontdb::Database,
670    /// Caller labels whose faces are retained in `base_db`.
671    base_caller_aliases: HashMap<String, Vec<fontdb::ID>>,
672    /// Caller embedded families whose faces are retained in `base_db`.
673    base_caller_families: HashMap<String, Vec<fontdb::ID>>,
674    /// Map from FontKey to loaded font info.
675    cache: HashMap<FontKey, usize>,
676    /// Manager-owned bytes for bundled, embedded, and caller-provided faces.
677    memory_face_data: HashMap<fontdb::ID, Arc<[u8]>>,
678    /// All loaded fonts.
679    fonts: Vec<LoadedFont>,
680    /// Next font ID counter.
681    next_id: u32,
682    /// Fonts already discovered as covering something the requested family
683    /// could not, keyed by (bold, italic).
684    ///
685    /// Finding a font that covers a character means loading and inspecting
686    /// faces, which is far too slow to repeat per character. Once a CJK face
687    /// has been found for one character it almost always covers the rest of
688    /// the run, so it is tried first next time.
689    coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
690    /// Characters already searched for and not found in any available font, so
691    /// the scan is not repeated for every occurrence.
692    coverage_misses: HashSet<char>,
693    /// Exact additional font set currently loaded into `db`.
694    additional_fonts: Vec<FontFile>,
695    /// One decoded body per physical embedded part and every owned alias.
696    embedded_fonts: Vec<(Vec<String>, Arc<[u8]>)>,
697    /// Owned embedded typeface aliases, separate from caller provenance.
698    embedded_aliases: HashMap<String, Vec<fontdb::ID>>,
699    /// Lowercased caller labels mapped to the exact faces loaded from their
700    /// bytes. Rebuilt together with `additional_fonts`.
701    caller_aliases: HashMap<String, Vec<fontdb::ID>>,
702    /// Exact embedded caller families mapped to their loaded faces, so caller
703    /// bytes take priority over bundled faces with the same family.
704    caller_families: HashMap<String, Vec<fontdb::ID>>,
705    /// Exact caller-declared alias identity for cheap change detection.
706    explicit_aliases: Vec<(String, String)>,
707    /// Lowercased requested family mapped to the caller-declared target.
708    explicit_alias_map: HashMap<String, String>,
709    /// Bounded exact-key shaping results.
710    shaping_memo: Mutex<ShapingMemo>,
711    /// Exact resolution events for one cache-candidate paragraph.
712    paragraph_font_trace: Option<ParagraphFontTrace>,
713    /// Distinct current-layout fonts in first-resolution order.
714    layout_fonts: Vec<FontId>,
715    // At most one current-output message per Aptos request spelling.
716    // Scope capture rolls provisional messages back with their contributions.
717    layout_font_diagnostics: Vec<Diagnostic>,
718}
719
720/// Families with broad non-Latin coverage, tried before scanning everything.
721///
722/// Ordered roughly by how likely each is to be installed. This is only a fast
723/// path: if none of them is present the full font database is still searched.
724const BROAD_COVERAGE_FAMILIES: &[&str] = &[
725    // Deterministic complex-script fallbacks bundled by this crate
726    "Noto Sans Arabic",
727    "Noto Sans Devanagari",
728    "Noto Sans Thai",
729    "Noto Sans SC",
730    // Bundled with or shipped alongside many Linux distributions
731    "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    // macOS
740    "PingFang SC",
741    "PingFang TC",
742    "Hiragino Sans",
743    "Hiragino Kaku Gothic ProN",
744    "Apple SD Gothic Neo",
745    "Songti SC",
746    "STHeiti",
747    // Windows
748    "Microsoft YaHei",
749    "Microsoft JhengHei",
750    "SimSun",
751    "SimHei",
752    "NSimSun",
753    "Yu Gothic",
754    "MS Gothic",
755    "Meiryo",
756    "Malgun Gothic",
757    // Wide-coverage generalists
758    "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
769/// Maximum caller-declared aliases retained for resolution identity.
770const CALLER_ALIAS_MAX_ENTRIES: usize = 256;
771
772/// Maximum aggregate UTF-8 payload retained by the ordered caller-alias
773/// identity and its lowercased lookup map.
774const CALLER_ALIAS_MAX_RETAINED_BYTES: usize = 64 * 1024;
775
776/// Return the deterministic prefix that fits both caller-alias ceilings.
777///
778/// The byte accounting includes requested and target strings in the ordered
779/// identity plus the normalized requested key and target value in the lookup
780/// map. The first entry that would exceed either ceiling and every later entry
781/// are discarded.
782fn 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    /// Create a new FontManager and load system fonts.
839    ///
840    /// Bundled fonts (Carlito, Caladea, Liberation) are loaded as fallbacks.
841    /// System fonts are discovered when the `system-fonts` feature is enabled.
842    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    /// Create a font manager that loads bundled fonts without discovering
860    /// system fonts.
861    ///
862    /// This mode makes font resolution reproducible across machines.
863    pub fn new_deterministic() -> Result<Self> {
864        Ok(Self::from_base_database(bundled_font_database()))
865    }
866
867    /// Load or replace additional font files (user-provided or extracted from
868    /// an OOXML package).
869    ///
870    /// An unchanged set is a no-op so a reusable engine retains resolution and
871    /// shaping state. A changed set rebuilds from the isolated base database,
872    /// which prevents stale face ids and bounds repeated document edits.
873    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        // Previously admitted immutable embedded sources remain valid. Refuse
879        // atomically if rebuilding ever fails instead of clearing active state.
880        self.rebuild_font_sources(&embedded, font_files).is_ok()
881    }
882
883    /// Load bounded physical embedded font sources and all their owned aliases.
884    /// Caller faces retain priority. An unchanged set preserves loaded IDs and caches.
885    #[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        // CSS ranking still compares every face. On an equal rank, retain
1010        // constructor provenance ahead of the additional tier. Reverse the
1011        // constructor block so its established first-loaded tie stays first
1012        // when best_caller_face visits the additional tier last-loaded first.
1013        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    /// Create a FontManager with user-provided fonts (no system font loading).
1048    ///
1049    /// Each entry is `(family_name, font_bytes)`. This is useful in environments
1050    /// where system fonts are not available, such as WASM.
1051    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        // Constructor-only ties retain their original first-loaded selection.
1070        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    /// Replace byte-free caller aliases from requested family to loaded family.
1079    ///
1080    /// An unchanged slice is a no-op. A changed slice invalidates only name
1081    /// resolution and coverage state. Loaded faces and shaping entries remain
1082    /// valid because their `FontId` values do not change.
1083    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    /// Begin one complete layout attempt's exact font-usage trace.
1140    #[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    /// Begin recording exact events for one bounded cache candidate.
1148    #[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    /// Finish the legacy resolution-only projection without adding contribution IDs.
1169    #[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    /// Finish only final contributions. Resolution attempts never establish identity.
1191    #[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    /// Finish one ordered bounded cache carrier, preserving resolution inventory.
1213    #[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    /// Capture a local shape without replacing a caller's active trace.
1227    #[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    /// Return bounded staged contributions, preserving caller resolution events.
1249    #[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    /// Copy private native shaping context within one bounded admission budget.
1339    #[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    /// Admit a successfully shaped final segment, retaining its original request.
1366    #[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    /// Validate the entire cached contribution before any trace or inventory mutation.
1427    #[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    /// Atomically replay an admitted final contribution vector.
1477    #[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    /// Replay the exact legacy resolution events attached to a cached paragraph.
1532    #[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    /// Distinct current-layout fonts in first-resolution order.
1540    #[doc(hidden)]
1541    pub fn current_layout_fonts(&self) -> &[FontId] {
1542        &self.layout_fonts
1543    }
1544
1545    /// Whether no historical loaded face is absent from this layout.
1546    #[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    /// Drop faces that were loaded by an older successful layout but are no
1560    /// longer active. Every face used by the current document is retained,
1561    /// even when that working set contains more than the cache ceilings.
1562    #[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    /// Resolve a font for `text`, falling back on glyph coverage.
1615    ///
1616    /// `resolve_font` picks by family name alone. That is enough for Latin
1617    /// text, but a run asking for a Chinese family on a machine without it
1618    /// falls down the name chain and lands on a Latin font, which has no CJK
1619    /// glyphs, so every character renders as a missing-glyph box. Name
1620    /// matching cannot detect that, because the font it chose exists and is
1621    /// perfectly valid, it simply cannot draw this text.
1622    ///
1623    /// So the resolved font is checked against the text, and when a character
1624    /// is missing another font that can draw it is looked for.
1625    ///
1626    /// This is per run rather than per character: the font that covers the
1627    /// first missing character is used for the whole run. Text that mixes
1628    /// scripts inside one run is therefore still imperfect, but it is a large
1629    /// improvement on drawing boxes.
1630    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            // Nothing installed can draw it. Keep the original font so the
1649            // text still occupies the right space.
1650            None => Ok(primary),
1651            Some(id) => Ok(id),
1652        }
1653    }
1654
1655    /// Current layout's owned approximation facts, never loaded-font history.
1656    #[doc(hidden)]
1657    pub fn current_font_diagnostics(&self) -> &[Diagnostic] {
1658        &self.layout_font_diagnostics
1659    }
1660
1661    // Selection aliases name requests, not the physical family of a face.
1662    // Authenticate the selected collection face from its original source bytes.
1663    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    /// Select an explicit symbol or bullet without changing its source owner.
1716    /// Genuine original-code coverage wins over the bounded semantic substitute.
1717    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    /// Whether the selected physical requested face covers the original symbol codes.
1767    /// Caller labels alone cannot establish this coverage or symbol meaning.
1768    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    /// Owned unknown semantic mappings for accepted original symbol text.
1781    /// This query neither records font history nor claims glyphs are missing.
1782    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        // One canonical family and at most 512 original low-byte/PUA scalars.
1821        result
1822    }
1823
1824    /// The characters in `text` that the font at `idx` cannot draw.
1825    ///
1826    /// Whitespace and control characters are skipped: a font without a glyph
1827    /// for a space is not a reason to go looking for another one.
1828    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    /// Whether the font at `idx` has a glyph for `ch`.
1842    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    /// Find a font that can draw `missing`.
1850    ///
1851    /// A font covering every missing character wins. Failing that the one
1852    /// covering the most is used, because a single run gets a single font and
1853    /// partial coverage still beats a row of boxes. Picking on the first
1854    /// missing character alone is not enough: a Japanese face may have the
1855    /// characters shared with Chinese and not the simplified-only ones, so it
1856    /// would look like a fix and still leave gaps.
1857    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; // (covered count, font index)
1863        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        // Fonts that already rescued an earlier run, which for a document in
1875        // one script is almost always the answer again.
1876        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        // Families with broad coverage, then everything else the database
1887        // knows about. Both go through resolve_font so loading and caching
1888        // stay in one place.
1889        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    /// Index into `fonts` for a FontId.
1928    fn index_of(&self, id: FontId) -> Option<usize> {
1929        self.fonts.iter().position(|f| f.id == id)
1930    }
1931
1932    /// Resolve a font by family name, bold, and italic flags.
1933    /// Returns a FontId. Uses fallback chain if the requested font is not found.
1934    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    /// Resolve a font for metrics without claiming that it emitted glyphs.
1944    #[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                    // CSS compares both buckets together. Equal ranks retain
1995                    // constructor-first ties, then the latest additional face.
1996                    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        // Map common Word font names to metric-compatible alternatives, then
2049        // try generic fallbacks.
2050        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        // Last resort: try generic families
2075        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        // Preserve the established one-loaded-font-per-request-key behavior
2099        // while the bounded alias cache has room. At the ceiling, reuse the
2100        // exact resolved face rather than growing without limit.
2101        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        // Load file-backed data through the process cache. All faces in a TTC
2118        // carry the same source path, so their collection indices share bytes.
2119        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        // Every metric and advance is scaled by size/upem, so a zero here would
2155        // turn the whole layout into infinities.
2156        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    /// Get font metrics at a given size in points.
2217    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, // make positive
2224            line_gap: font.line_gap as f64 * scale,
2225            units_per_em: font.units_per_em,
2226        })
2227    }
2228
2229    /// The metrics Word measures one line of a font on, at a given size.
2230    ///
2231    /// Word reads a face the way Windows reports it (`TEXTMETRIC`): `ascent`
2232    /// and `descent` are the OS/2 `usWinAscent` and `usWinDescent`, and
2233    /// `line_gap` is the external leading, the part of the `hhea` line gap
2234    /// that the Windows extent does not already cover. A single line is the
2235    /// sum of the three, so Calibri and Carlito are 2500/2048 em and Arial and
2236    /// Liberation Sans 2355/2048 em. A face that sets the OS/2
2237    /// `USE_TYPO_METRICS` flag is measured on its typographic values instead,
2238    /// which is what Word does with Aptos, and a face without Windows metrics
2239    /// on the values [`FontManager::metrics`] reports.
2240    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    /// Shape a text string using HarfRust. Returns glyph IDs and advances.
2258    pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
2259        // HarfRust cannot derive segment properties from an empty buffer, and
2260        // there is nothing to shape anyway.
2261        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        // Infer direction, script and language from the text. Unlike rustybuzz,
2318        // HarfRust does not do this implicitly and panics on an unset direction.
2319        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    /// Shape one logical text segment into script, font, and bidi-level spans.
2349    ///
2350    /// The returned spans are in logical order. The rich line breaker applies
2351    /// UAX 9 visual order only after it knows the final line boundaries.
2352    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    /// Shape styled spans with one paragraph-wide bidi resolution.
2386    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    /// Shape PowerPoint small-caps requests in the same paragraph-wide bidi pass.
2398    /// Requests correspond to input occurrences, including equal-text runs.
2399    /// Logical text and source clusters remain the original input. Unsupported
2400    /// selected faces use uppercase lowercase graphemes at 80 percent size.
2401    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(&paragraph_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        // Opportunities come from the whole paragraph, so a word that changes
2447        // formatting partway through does not break where its runs meet.
2448        let paragraph_breaks = if no_wrap {
2449            HashSet::new()
2450        } else {
2451            multilingual_break_opportunities(&paragraph_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    /// `break_offsets` are the line break opportunities within the segment,
2491    /// as byte offsets. An offset at its end lets a line end after it.
2492    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        // Spaces before a break opportunity get a span of their own, so a
2506        // line can let them hang past its end.
2507        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            // Test actual feature application on this face, script and language.
2583            // A global GSUB tag alone does not prove a reachable substitution.
2584            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                // Keep every original grapheme together. An expanding uppercase
2637                // mapping belongs to that original grapheme's logical cluster.
2638                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    /// Get font data for PDF embedding.
2867    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    /// Get all used font data.
2883    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        // Hangul and Kana are deliberately not folded into Han. They need
2990        // different shaper feature sets, and folding them would silently
2991        // change how existing Han text shapes.
2992        //
2993        // Hangul is the jamo, the Extended-A and Extended-B jamo, the
2994        // compatibility jamo, the precomposed syllables, and the halfwidth
2995        // jamo. Kana is Hiragana, Katakana, the Katakana phonetic
2996        // extensions, and the halfwidth Katakana.
2997        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        // HarfBuzz unifies Hiragana and Katakana under one shaping model, and
3016        // HIRAGANA is the tag it canonicalises the pair to.
3017        TextScript::Kana => harfrust::script::HIRAGANA,
3018        TextScript::Common => harfrust::script::COMMON,
3019    }
3020}
3021
3022/// UAX 14 line break opportunities, plus dictionary word boundaries inside
3023/// Thai, Lao, Khmer and Myanmar text, which UAX 14 leaves to a dictionary.
3024///
3025/// A word boundary elsewhere is not a line break opportunity: it falls before
3026/// a comma, a space or the hyphen of a compound.
3027fn 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                // Collection faces share one original source allocation. Match
3141                // source identity rather than copying identical bytes per face.
3142                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        // A source that fontdb features enabled elsewhere in the build add,
3157        // such as `memmap`'s shared file, read through fontdb itself.
3158        #[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
3225/// Map common Word font names to metric-compatible alternatives.
3226/// Returns a list of candidate names to try (including the original).
3227///
3228/// Priority: original font → metric-compatible open-source clone → generic fallback.
3229/// Carlito is metric-compatible with Calibri, Caladea with Cambria,
3230/// Liberation Sans/Serif/Mono with Arial/Times New Roman/Courier New.
3231fn 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    // Independent hb-subset/hb-shape 14.4.0 static instances, 1000 upem.
3264    // The S91 oracle receipt pins original bytes, commands and rounding limits.
3265    #[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    /// Word's single line is the Windows extent of a face plus its external
3652    /// leading. Each row is a bundled face's OS/2 and `hhea` values, whose
3653    /// sum is the pitch Word gives the Microsoft face it stands in for:
3654    /// Calibri 2500/2048 em, Arial and Times New Roman 2355/2048, Cambria
3655    /// 1.172 and Courier New 2320/2048.
3656    #[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            // Sets `USE_TYPO_METRICS`, so its typographic 896 and 408 win
3666            // over its Windows 1348 and 558.
3667            ("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        // No opportunity before the comma, the space or the hyphen, and each
4670        // space before an opportunity is a span of its own.
4671        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        // Should be able to resolve at least one font via fallback
5106        let result = fm.resolve_font(None, false, false);
5107        // On CI or systems without fonts this might fail, so we just check it doesn't panic
5108        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            // Bold should get a different font ID (different variant)
5162            assert_ne!(r, b);
5163        }
5164    }
5165
5166    /// Latin text must resolve exactly as it did before, so the coverage check
5167    /// cannot disturb the overwhelmingly common case.
5168    #[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    /// Text nothing can draw must keep the requested font rather than failing.
5181    ///
5182    /// The approved deterministic fallbacks intentionally do not cover emoji,
5183    /// so the search is guaranteed to come up empty. The text still needs a
5184    /// font so it occupies the right space.
5185    #[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    /// Whitespace absent from a font is not a reason to go hunting for another.
5199    #[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        // A non-breaking space and a tab, neither of which every face carries.
5205        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    /// When the machine does have a CJK font, CJK text must not keep a Latin
5214    /// font that cannot draw it.
5215    ///
5216    /// Skipped where no such font is installed, which is why it asserts
5217    /// nothing about which font is chosen.
5218    #[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; // that font somehow covers it, nothing to prove
5229        }
5230        let resolved = fm
5231            .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
5232            .unwrap();
5233        if resolved == latin {
5234            return; // no covering font installed on this machine
5235        }
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        // The two new ranges must be additions, never a re-routing. Sweeping
5274        // the whole Basic Multilingual Plane proves no codepoint that already
5275        // had a script identity moved to a different one.
5276        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        // Accepted caller aliases still choose the same original bytes on replay.
5555        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}