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
14#[cfg(all(test, feature = "system-fonts"))]
15use std::sync::atomic::{AtomicUsize, Ordering};
16
17use crate::error::{LayoutError, Result};
18use crate::line::TextSegment;
19use crate::output::{FontId, SourceSpan};
20
21/// Font data provided by the user or extracted from an OOXML file.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct FontFile {
24    /// Font family name (e.g., "Calibri", "Arial").
25    pub family: String,
26    /// Raw font file bytes (TTF/OTF).
27    pub data: Vec<u8>,
28}
29
30/// Key for caching resolved fonts.
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32struct FontKey {
33    family: String,
34    bold: bool,
35    italic: bool,
36}
37
38/// Metrics for a font at a given size.
39#[derive(Debug, Clone, Copy)]
40pub struct FontMetrics {
41    /// Ascent in points (positive, above baseline).
42    pub ascent: f64,
43    /// Descent in points (positive, below baseline).
44    pub descent: f64,
45    /// Line gap in points.
46    pub line_gap: f64,
47    /// Units per em.
48    pub units_per_em: u16,
49}
50
51/// Result of shaping a text string.
52#[derive(Debug, Clone)]
53pub struct ShapedText {
54    /// Glyph IDs from shaping.
55    pub glyph_ids: Vec<u16>,
56    /// Per-glyph advances in points.
57    pub advances: Vec<f64>,
58    /// Total width in points.
59    pub width: f64,
60}
61
62/// Requested or resolved direction for one logical text span.
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
64pub enum TextDirection {
65    #[default]
66    Auto,
67    LeftToRight,
68    RightToLeft,
69}
70
71pub(crate) fn directional_level(
72    direction: TextDirection,
73    paragraph_level: unicode_bidi::Level,
74) -> unicode_bidi::Level {
75    let paragraph_number = paragraph_level.number();
76    let number = match direction {
77        TextDirection::Auto => paragraph_number,
78        TextDirection::LeftToRight if paragraph_level.is_rtl() => paragraph_number + 1,
79        TextDirection::LeftToRight => paragraph_number,
80        TextDirection::RightToLeft if paragraph_level.is_rtl() => paragraph_number,
81        TextDirection::RightToLeft => paragraph_number + 1,
82    };
83    unicode_bidi::Level::new(number).expect("one directional embedding fits the bidi level limit")
84}
85
86pub(crate) fn explicit_direction_levels(
87    text: &str,
88    direction: TextDirection,
89    paragraph_level: unicode_bidi::Level,
90) -> Result<Vec<unicode_bidi::Level>> {
91    let local_base = match direction {
92        TextDirection::Auto => paragraph_level,
93        TextDirection::LeftToRight => unicode_bidi::Level::ltr(),
94        TextDirection::RightToLeft => unicode_bidi::Level::rtl(),
95    };
96    let target_base = directional_level(direction, paragraph_level);
97    let offset = target_base.number() - local_base.number();
98    unicode_bidi::BidiInfo::new(text, Some(local_base))
99        .levels
100        .into_iter()
101        .map(|level| {
102            level
103                .number()
104                .checked_add(offset)
105                .and_then(|number| unicode_bidi::Level::new(number).ok())
106                .ok_or_else(|| {
107                    LayoutError::Layout(
108                        "run direction exceeded the Unicode bidi level limit".to_owned(),
109                    )
110                })
111        })
112        .collect()
113}
114
115/// Script identity used to select shaping behavior and deterministic fallback.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum TextScript {
118    Latin,
119    Arabic,
120    Hebrew,
121    Devanagari,
122    Thai,
123    Han,
124    Common,
125}
126
127/// One glyph interval mapped to an exclusive logical Unicode-scalar interval.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct GlyphCluster {
130    pub glyph_start: u32,
131    pub glyph_end: u32,
132    pub char_start: u32,
133    pub char_end: u32,
134}
135
136impl GlyphCluster {
137    pub fn is_valid(&self) -> bool {
138        self.glyph_start < self.glyph_end && self.char_start < self.char_end
139    }
140
141    pub fn char_range(&self) -> Range<u32> {
142        self.char_start..self.char_end
143    }
144}
145
146/// One script, font, and bidi-level span shaped with complete positioning data.
147#[derive(Debug, Clone)]
148pub struct MultilingualTextSegment {
149    base: TextSegment,
150    logical_index: usize,
151    language: Option<String>,
152    script: TextScript,
153    direction: TextDirection,
154    bidi_level: u8,
155    x_advances: Vec<f64>,
156    y_advances: Vec<f64>,
157    x_offsets: Vec<f64>,
158    y_offsets: Vec<f64>,
159    clusters: Vec<GlyphCluster>,
160    break_after: bool,
161}
162
163impl MultilingualTextSegment {
164    #[allow(clippy::too_many_arguments)]
165    pub fn new(
166        base: TextSegment,
167        logical_index: usize,
168        language: Option<String>,
169        script: TextScript,
170        direction: TextDirection,
171        bidi_level: u8,
172        x_advances: Vec<f64>,
173        y_advances: Vec<f64>,
174        x_offsets: Vec<f64>,
175        y_offsets: Vec<f64>,
176        clusters: Vec<GlyphCluster>,
177        break_after: bool,
178    ) -> Result<Self> {
179        let glyph_count = base.glyph_ids.len();
180        let char_count = base.text.chars().count();
181        let valid_level = unicode_bidi::Level::new(bidi_level).is_ok();
182        if x_advances.len() != glyph_count
183            || y_advances.len() != glyph_count
184            || x_offsets.len() != glyph_count
185            || y_offsets.len() != glyph_count
186            || base.advances.len() != glyph_count
187            || !valid_level
188            || !position_values_are_finite(
189                &x_advances,
190                &y_advances,
191                &x_offsets,
192                &y_offsets,
193                base.width,
194            )
195            || !cluster_ranges_are_valid(&clusters, glyph_count, char_count, bidi_level % 2 == 1)
196        {
197            return Err(LayoutError::Layout(
198                "invalid multilingual glyph positioning or cluster range".to_owned(),
199            ));
200        }
201        Ok(Self {
202            base,
203            logical_index,
204            language,
205            script,
206            direction,
207            bidi_level,
208            x_advances,
209            y_advances,
210            x_offsets,
211            y_offsets,
212            clusters,
213            break_after,
214        })
215    }
216
217    pub fn text(&self) -> &str {
218        &self.base.text
219    }
220
221    pub fn base(&self) -> &TextSegment {
222        &self.base
223    }
224
225    pub fn font_id(&self) -> FontId {
226        self.base.font_id
227    }
228
229    pub fn language(&self) -> Option<&str> {
230        self.language.as_deref()
231    }
232
233    pub fn script(&self) -> TextScript {
234        self.script
235    }
236
237    pub fn direction(&self) -> TextDirection {
238        self.direction
239    }
240
241    pub fn bidi_level(&self) -> u8 {
242        self.bidi_level
243    }
244
245    pub fn logical_index(&self) -> usize {
246        self.logical_index
247    }
248
249    pub fn glyph_ids(&self) -> &[u16] {
250        &self.base.glyph_ids
251    }
252
253    pub fn x_advances(&self) -> &[f64] {
254        &self.x_advances
255    }
256
257    pub fn y_advances(&self) -> &[f64] {
258        &self.y_advances
259    }
260
261    pub fn x_offsets(&self) -> &[f64] {
262        &self.x_offsets
263    }
264
265    pub fn y_offsets(&self) -> &[f64] {
266        &self.y_offsets
267    }
268
269    pub fn clusters(&self) -> &[GlyphCluster] {
270        &self.clusters
271    }
272
273    pub fn width(&self) -> f64 {
274        self.base.width
275    }
276
277    pub fn break_after(&self) -> bool {
278        self.break_after
279    }
280}
281
282pub(crate) fn position_values_are_finite(
283    x_advances: &[f64],
284    y_advances: &[f64],
285    x_offsets: &[f64],
286    y_offsets: &[f64],
287    width: f64,
288) -> bool {
289    width.is_finite()
290        && x_advances
291            .iter()
292            .chain(y_advances)
293            .chain(x_offsets)
294            .chain(y_offsets)
295            .all(|value| value.is_finite())
296}
297
298pub(crate) fn cluster_ranges_are_valid(
299    clusters: &[GlyphCluster],
300    glyph_count: usize,
301    char_count: usize,
302    right_to_left: bool,
303) -> bool {
304    if glyph_count == 0 || char_count == 0 {
305        return glyph_count == 0 && char_count == 0 && clusters.is_empty();
306    }
307    let mut previous_glyph_end = 0u32;
308    let mut previous_char_range = None::<Range<u32>>;
309    let mut covered_chars = 0usize;
310    for cluster in clusters {
311        if !cluster.is_valid()
312            || cluster.glyph_start != previous_glyph_end
313            || cluster.glyph_end as usize > glyph_count
314            || cluster.char_end as usize > char_count
315        {
316            return false;
317        }
318        if let Some(previous) = previous_char_range {
319            let contiguous = if right_to_left {
320                cluster.char_end == previous.start
321            } else {
322                cluster.char_start == previous.end
323            };
324            if !contiguous {
325                return false;
326            }
327        }
328        previous_glyph_end = cluster.glyph_end;
329        previous_char_range = Some(cluster.char_range());
330        covered_chars += (cluster.char_end - cluster.char_start) as usize;
331    }
332    previous_glyph_end as usize == glyph_count
333        && covered_chars == char_count
334        && previous_char_range.is_some_and(|last| {
335            if right_to_left {
336                clusters
337                    .first()
338                    .is_some_and(|first| first.char_end as usize == char_count)
339                    && last.start == 0
340            } else {
341                clusters.first().is_some_and(|first| first.char_start == 0)
342                    && last.end as usize == char_count
343            }
344        })
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348struct ShapingKey {
349    font_id: FontId,
350    text: String,
351    size_bits: u64,
352}
353
354struct ShapingMemo {
355    entries: VecDeque<(ShapingKey, ShapedText, usize, u64)>,
356    bytes: usize,
357    #[cfg(test)]
358    hits: usize,
359    #[cfg(test)]
360    misses: usize,
361}
362
363impl ShapingMemo {
364    fn new() -> Self {
365        Self {
366            entries: VecDeque::new(),
367            bytes: 0,
368            #[cfg(test)]
369            hits: 0,
370            #[cfg(test)]
371            misses: 0,
372        }
373    }
374
375    fn clear(&mut self) {
376        self.entries.clear();
377        self.bytes = 0;
378        #[cfg(test)]
379        {
380            self.hits = 0;
381            self.misses = 0;
382        }
383    }
384
385    fn insert(&mut self, key: ShapingKey, shaped: ShapedText) {
386        let entry_bytes = std::mem::size_of::<(ShapingKey, ShapedText, usize, u64)>()
387            + key.text.len()
388            + shaped.glyph_ids.len() * std::mem::size_of::<u16>()
389            + shaped.advances.len() * std::mem::size_of::<f64>();
390        if entry_bytes > SHAPING_CACHE_MAX_BYTES {
391            return;
392        }
393        while self.entries.len() >= SHAPING_CACHE_MAX_ENTRIES
394            || self.bytes.saturating_add(entry_bytes) > SHAPING_CACHE_MAX_BYTES
395        {
396            let Some((_, _, evicted_bytes, _)) = self.entries.pop_front() else {
397                break;
398            };
399            self.bytes = self.bytes.saturating_sub(evicted_bytes);
400        }
401        self.bytes += entry_bytes;
402        let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
403        self.entries
404            .push_back((key, shaped, entry_bytes, fingerprint));
405    }
406}
407
408fn shaping_fingerprint(font_id: FontId, text: &str, size_bits: u64) -> u64 {
409    use std::hash::{Hash, Hasher};
410
411    let mut hasher = std::collections::hash_map::DefaultHasher::new();
412    font_id.hash(&mut hasher);
413    text.hash(&mut hasher);
414    size_bits.hash(&mut hasher);
415    hasher.finish()
416}
417
418const SHAPING_CACHE_MAX_ENTRIES: usize = 2_048;
419const SHAPING_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
420
421#[cfg(feature = "system-fonts")]
422struct FileFontCache {
423    entries: VecDeque<(PathBuf, Arc<[u8]>, usize)>,
424    bytes: usize,
425}
426
427#[cfg(feature = "system-fonts")]
428impl FileFontCache {
429    fn new() -> Self {
430        Self {
431            entries: VecDeque::new(),
432            bytes: 0,
433        }
434    }
435
436    fn clear(&mut self) {
437        self.entries.clear();
438        self.bytes = 0;
439    }
440}
441
442#[cfg(feature = "system-fonts")]
443const FILE_FONT_CACHE_MAX_ENTRIES: usize = 256;
444#[cfg(feature = "system-fonts")]
445const FILE_FONT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
446
447#[cfg(feature = "system-fonts")]
448static NORMAL_FONT_DATABASE: OnceLock<fontdb::Database> = OnceLock::new();
449#[cfg(feature = "system-fonts")]
450static FILE_FONT_CACHE: OnceLock<Mutex<FileFontCache>> = OnceLock::new();
451#[cfg(all(test, feature = "system-fonts"))]
452static SYSTEM_FONT_DISCOVERY_RUNS: AtomicUsize = AtomicUsize::new(0);
453
454/// Internal record for a loaded font face.
455struct LoadedFont {
456    db_id: fontdb::ID,
457    id: FontId,
458    family: String,
459    bold: bool,
460    italic: bool,
461    data: Arc<[u8]>,
462    face_index: u32,
463    units_per_em: u16,
464    /// Vertical metrics in design units, read once when the face is loaded.
465    ascender: i16,
466    descender: i16,
467    line_gap: i16,
468    /// HarfRust's per-face shaping caches. Building these is the expensive
469    /// part of shaping, so it happens once per face instead of once per run.
470    shaper_data: harfrust::ShaperData,
471}
472
473struct ParagraphFontTrace {
474    ids: Vec<FontId>,
475    overflowed: bool,
476}
477
478/// Manages font discovery, loading, shaping, and metrics.
479pub struct FontManager {
480    db: fontdb::Database,
481    /// Database before document-embedded or caller fonts are applied.
482    base_db: fontdb::Database,
483    /// Caller labels whose faces are retained in `base_db`.
484    base_caller_aliases: HashMap<String, Vec<fontdb::ID>>,
485    /// Caller embedded families whose faces are retained in `base_db`.
486    base_caller_families: HashMap<String, Vec<fontdb::ID>>,
487    /// Map from FontKey to loaded font info.
488    cache: HashMap<FontKey, usize>,
489    /// Manager-owned bytes for bundled, embedded, and caller-provided faces.
490    memory_face_data: HashMap<fontdb::ID, Arc<[u8]>>,
491    /// All loaded fonts.
492    fonts: Vec<LoadedFont>,
493    /// Next font ID counter.
494    next_id: u32,
495    /// Fonts already discovered as covering something the requested family
496    /// could not, keyed by (bold, italic).
497    ///
498    /// Finding a font that covers a character means loading and inspecting
499    /// faces, which is far too slow to repeat per character. Once a CJK face
500    /// has been found for one character it almost always covers the rest of
501    /// the run, so it is tried first next time.
502    coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
503    /// Characters already searched for and not found in any available font, so
504    /// the scan is not repeated for every occurrence.
505    coverage_misses: HashSet<char>,
506    /// Exact additional font set currently loaded into `db`.
507    additional_fonts: Vec<FontFile>,
508    /// Lowercased caller labels mapped to the exact faces loaded from their
509    /// bytes. Rebuilt together with `additional_fonts`.
510    caller_aliases: HashMap<String, Vec<fontdb::ID>>,
511    /// Exact embedded caller families mapped to their loaded faces, so caller
512    /// bytes take priority over bundled faces with the same family.
513    caller_families: HashMap<String, Vec<fontdb::ID>>,
514    /// Exact caller-declared alias identity for cheap change detection.
515    explicit_aliases: Vec<(String, String)>,
516    /// Lowercased requested family mapped to the caller-declared target.
517    explicit_alias_map: HashMap<String, String>,
518    /// Bounded exact-key shaping results.
519    shaping_memo: Mutex<ShapingMemo>,
520    /// Exact resolution events for one cache-candidate paragraph.
521    paragraph_font_trace: Option<ParagraphFontTrace>,
522    /// Distinct current-layout fonts in first-resolution order.
523    layout_fonts: Vec<FontId>,
524}
525
526/// Families with broad non-Latin coverage, tried before scanning everything.
527///
528/// Ordered roughly by how likely each is to be installed. This is only a fast
529/// path: if none of them is present the full font database is still searched.
530const BROAD_COVERAGE_FAMILIES: &[&str] = &[
531    // Deterministic complex-script fallbacks bundled by this crate
532    "Noto Sans Arabic",
533    "Noto Sans Devanagari",
534    "Noto Sans Thai",
535    "Noto Sans SC",
536    // Bundled with or shipped alongside many Linux distributions
537    "Noto Sans CJK SC",
538    "Noto Sans CJK JP",
539    "Noto Sans CJK KR",
540    "Noto Sans CJK TC",
541    "Noto Serif CJK SC",
542    "Source Han Sans SC",
543    "WenQuanYi Zen Hei",
544    "WenQuanYi Micro Hei",
545    // macOS
546    "PingFang SC",
547    "PingFang TC",
548    "Hiragino Sans",
549    "Hiragino Kaku Gothic ProN",
550    "Apple SD Gothic Neo",
551    "Songti SC",
552    "STHeiti",
553    // Windows
554    "Microsoft YaHei",
555    "Microsoft JhengHei",
556    "SimSun",
557    "SimHei",
558    "NSimSun",
559    "Yu Gothic",
560    "MS Gothic",
561    "Meiryo",
562    "Malgun Gothic",
563    // Wide-coverage generalists
564    "Arial Unicode MS",
565    "DejaVu Sans",
566];
567
568const RESOLUTION_CACHE_MAX_ENTRIES: usize = 256;
569const COVERAGE_FALLBACK_MAX_ENTRIES: usize = 256;
570const COVERAGE_MISS_MAX_ENTRIES: usize = 4_096;
571const PARAGRAPH_FONT_TRACE_MAX_ENTRIES: usize = 4_096;
572
573/// Maximum caller-declared aliases retained for resolution identity.
574const CALLER_ALIAS_MAX_ENTRIES: usize = 256;
575
576/// Maximum aggregate UTF-8 payload retained by the ordered caller-alias
577/// identity and its lowercased lookup map.
578const CALLER_ALIAS_MAX_RETAINED_BYTES: usize = 64 * 1024;
579
580/// Return the deterministic prefix that fits both caller-alias ceilings.
581///
582/// The byte accounting includes requested and target strings in the ordered
583/// identity plus the normalized requested key and target value in the lookup
584/// map. The first entry that would exceed either ceiling and every later entry
585/// are discarded.
586fn bounded_caller_aliases(aliases: &[(String, String)]) -> Vec<(String, String)> {
587    let mut bounded = Vec::with_capacity(aliases.len().min(CALLER_ALIAS_MAX_ENTRIES));
588    let mut retained_bytes = 0usize;
589    for (requested, target) in aliases {
590        if bounded.len() == CALLER_ALIAS_MAX_ENTRIES {
591            break;
592        }
593        let normalized_requested = requested.to_lowercase();
594        let entry_bytes = requested
595            .len()
596            .saturating_add(target.len())
597            .saturating_add(normalized_requested.len())
598            .saturating_add(target.len());
599        let next_retained_bytes = retained_bytes.saturating_add(entry_bytes);
600        if next_retained_bytes > CALLER_ALIAS_MAX_RETAINED_BYTES {
601            break;
602        }
603        bounded.push((requested.to_owned(), target.to_owned()));
604        retained_bytes = next_retained_bytes;
605    }
606    bounded
607}
608
609impl Default for FontManager {
610    fn default() -> Self {
611        Self::new()
612    }
613}
614
615impl FontManager {
616    fn from_base_database(db: fontdb::Database) -> Self {
617        Self {
618            base_db: db.clone(),
619            base_caller_aliases: HashMap::new(),
620            base_caller_families: HashMap::new(),
621            db,
622            cache: HashMap::new(),
623            memory_face_data: HashMap::new(),
624            fonts: Vec::new(),
625            next_id: 0,
626            coverage_fallbacks: HashMap::new(),
627            coverage_misses: HashSet::new(),
628            additional_fonts: Vec::new(),
629            caller_aliases: HashMap::new(),
630            caller_families: HashMap::new(),
631            explicit_aliases: Vec::new(),
632            explicit_alias_map: HashMap::new(),
633            shaping_memo: Mutex::new(ShapingMemo::new()),
634            paragraph_font_trace: None,
635            layout_fonts: Vec::new(),
636        }
637    }
638
639    /// Create a new FontManager and load system fonts.
640    ///
641    /// Bundled fonts (Carlito, Caladea, Liberation) are loaded as fallbacks.
642    /// System fonts are discovered when the `system-fonts` feature is enabled.
643    pub fn new() -> Self {
644        #[cfg(feature = "system-fonts")]
645        {
646            let db = NORMAL_FONT_DATABASE.get_or_init(|| {
647                let mut db = bundled_font_database();
648                db.load_system_fonts();
649                #[cfg(test)]
650                SYSTEM_FONT_DISCOVERY_RUNS.fetch_add(1, Ordering::Relaxed);
651                db
652            });
653            Self::from_base_database(db.clone())
654        }
655
656        #[cfg(not(feature = "system-fonts"))]
657        Self::from_base_database(bundled_font_database())
658    }
659
660    /// Create a font manager that loads bundled fonts without discovering
661    /// system fonts.
662    ///
663    /// This mode makes font resolution reproducible across machines.
664    pub fn new_deterministic() -> Result<Self> {
665        Ok(Self::from_base_database(bundled_font_database()))
666    }
667
668    /// Load or replace additional font files (user-provided or extracted from
669    /// an OOXML package).
670    ///
671    /// An unchanged set is a no-op so a reusable engine retains resolution and
672    /// shaping state. A changed set rebuilds from the isolated base database,
673    /// which prevents stale face ids and bounds repeated document edits.
674    pub fn load_additional_fonts(&mut self, font_files: &[FontFile]) -> bool {
675        if self.additional_fonts == font_files {
676            return false;
677        }
678
679        self.db = self.base_db.clone();
680        self.caller_aliases.clone_from(&self.base_caller_aliases);
681        self.caller_families.clone_from(&self.base_caller_families);
682        for font_file in font_files {
683            Self::load_caller_font(
684                &mut self.db,
685                &mut self.caller_aliases,
686                &mut self.caller_families,
687                &font_file.family,
688                font_file.data.clone(),
689            );
690        }
691        self.cache.clear();
692        self.memory_face_data.clear();
693        self.fonts.clear();
694        self.next_id = 0;
695        self.coverage_fallbacks.clear();
696        self.coverage_misses.clear();
697        self.additional_fonts = font_files.to_vec();
698        if self.shaping_memo.is_poisoned() {
699            self.shaping_memo.clear_poison();
700        }
701        self.shaping_memo
702            .get_mut()
703            .expect("shaping cache poison was cleared")
704            .clear();
705        true
706    }
707
708    /// Create a FontManager with user-provided fonts (no system font loading).
709    ///
710    /// Each entry is `(family_name, font_bytes)`. This is useful in environments
711    /// where system fonts are not available, such as WASM.
712    pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
713        let mut db = fontdb::Database::new();
714        let mut caller_aliases = HashMap::new();
715        let mut caller_families = HashMap::new();
716        for (family, data) in &fonts {
717            Self::load_caller_font(
718                &mut db,
719                &mut caller_aliases,
720                &mut caller_families,
721                family,
722                data.clone(),
723            );
724        }
725        let mut manager = Self::from_base_database(db);
726        manager.base_caller_aliases = caller_aliases.clone();
727        manager.base_caller_families = caller_families.clone();
728        manager.caller_aliases = caller_aliases;
729        manager.caller_families = caller_families;
730        manager
731    }
732
733    /// Replace byte-free caller aliases from requested family to loaded family.
734    ///
735    /// An unchanged slice is a no-op. A changed slice invalidates only name
736    /// resolution and coverage state. Loaded faces and shaping entries remain
737    /// valid because their `FontId` values do not change.
738    pub fn set_caller_aliases(&mut self, aliases: &[(String, String)]) -> bool {
739        let aliases = bounded_caller_aliases(aliases);
740        if self.explicit_aliases == aliases {
741            return false;
742        }
743
744        self.explicit_alias_map = aliases
745            .iter()
746            .map(|(requested, target)| (requested.to_lowercase(), target.clone()))
747            .collect();
748        self.explicit_aliases = aliases;
749        self.cache.clear();
750        self.coverage_fallbacks.clear();
751        self.coverage_misses.clear();
752        true
753    }
754
755    fn load_caller_font(
756        db: &mut fontdb::Database,
757        aliases: &mut HashMap<String, Vec<fontdb::ID>>,
758        families: &mut HashMap<String, Vec<fontdb::ID>>,
759        family: &str,
760        data: Vec<u8>,
761    ) {
762        let before = db.len();
763        db.load_font_data(data);
764        let loaded_faces = db
765            .faces()
766            .skip(before)
767            .map(|face| {
768                (
769                    face.id,
770                    face.families
771                        .iter()
772                        .map(|(name, _)| name.clone())
773                        .collect::<Vec<_>>(),
774                )
775            })
776            .collect::<Vec<_>>();
777        for (id, loaded_families) in &loaded_faces {
778            for loaded_family in loaded_families {
779                families.entry(loaded_family.clone()).or_default().push(*id);
780            }
781        }
782        if !loaded_faces.is_empty()
783            && loaded_faces
784                .iter()
785                .all(|(_, families)| !families.iter().any(|loaded| loaded == family))
786        {
787            aliases
788                .entry(family.to_lowercase())
789                .or_default()
790                .extend(loaded_faces.into_iter().map(|(id, _)| id));
791        }
792    }
793
794    /// Begin one complete layout attempt's exact font-usage trace.
795    #[doc(hidden)]
796    pub fn begin_layout(&mut self) {
797        self.paragraph_font_trace = None;
798        self.layout_fonts = Vec::new();
799    }
800
801    /// Begin recording the exact resolution events for one cache candidate.
802    #[doc(hidden)]
803    pub fn begin_paragraph_font_trace(&mut self) {
804        self.paragraph_font_trace = Some(ParagraphFontTrace {
805            ids: Vec::new(),
806            overflowed: false,
807        });
808    }
809
810    /// Finish one bounded paragraph trace. An overflowed paragraph bypasses reuse.
811    #[doc(hidden)]
812    pub fn finish_paragraph_font_trace(&mut self) -> Option<Vec<FontId>> {
813        let mut trace = self.paragraph_font_trace.take()?;
814        if trace.overflowed {
815            return None;
816        }
817        trace.ids.shrink_to_fit();
818        Some(trace.ids)
819    }
820
821    /// Replay the exact resolution events attached to a cached paragraph.
822    #[doc(hidden)]
823    pub fn replay_layout_font_trace(&mut self, trace: &[FontId]) {
824        for &font_id in trace {
825            self.record_layout_font(font_id);
826        }
827    }
828
829    /// Distinct current-layout fonts in first-resolution order.
830    #[doc(hidden)]
831    pub fn current_layout_fonts(&self) -> &[FontId] {
832        &self.layout_fonts
833    }
834
835    /// Whether no historical loaded face is absent from this layout.
836    #[doc(hidden)]
837    pub fn every_loaded_font_is_current(&self) -> bool {
838        self.fonts
839            .iter()
840            .map(|font| font.id)
841            .eq(self.layout_fonts.iter().copied())
842            && self
843                .layout_fonts
844                .iter()
845                .enumerate()
846                .all(|(index, font_id)| *font_id == FontId(index as u32))
847    }
848
849    /// Drop faces that were loaded by an older successful layout but are no
850    /// longer active. Every face used by the current document is retained,
851    /// even when that working set contains more than the cache ceilings.
852    #[doc(hidden)]
853    pub fn retain_current_fonts(&mut self) {
854        let current = self.layout_fonts.iter().copied().collect::<HashSet<_>>();
855        let old_index_ids = self
856            .fonts
857            .iter()
858            .enumerate()
859            .map(|(index, font)| (index, font.id))
860            .collect::<HashMap<_, _>>();
861        self.fonts.retain(|font| current.contains(&font.id));
862        let current_order = self
863            .layout_fonts
864            .iter()
865            .enumerate()
866            .map(|(index, font_id)| (*font_id, index))
867            .collect::<HashMap<_, _>>();
868        self.fonts
869            .sort_by_key(|font| current_order.get(&font.id).copied().unwrap_or(usize::MAX));
870
871        let indices = self
872            .fonts
873            .iter()
874            .enumerate()
875            .map(|(index, font)| (font.id, index))
876            .collect::<HashMap<_, _>>();
877        let old_cache = std::mem::take(&mut self.cache);
878        self.cache = old_cache
879            .into_iter()
880            .filter_map(|(key, old_index)| {
881                let font_id = old_index_ids.get(&old_index)?;
882                Some((key, *indices.get(font_id)?))
883            })
884            .collect();
885        self.coverage_fallbacks.clear();
886        self.coverage_misses.clear();
887        let active_db_ids = self
888            .fonts
889            .iter()
890            .map(|font| font.db_id)
891            .collect::<HashSet<_>>();
892        self.memory_face_data
893            .retain(|db_id, _| active_db_ids.contains(db_id));
894
895        let memo = self
896            .shaping_memo
897            .get_mut()
898            .unwrap_or_else(std::sync::PoisonError::into_inner);
899        memo.entries
900            .retain(|(key, _, _, _)| current.contains(&key.font_id));
901        memo.bytes = memo.entries.iter().map(|(_, _, bytes, _)| bytes).sum();
902    }
903
904    /// Resolve a font for `text`, falling back on glyph coverage.
905    ///
906    /// `resolve_font` picks by family name alone. That is enough for Latin
907    /// text, but a run asking for a Chinese family on a machine without it
908    /// falls down the name chain and lands on a Latin font, which has no CJK
909    /// glyphs, so every character renders as a missing-glyph box. Name
910    /// matching cannot detect that, because the font it chose exists and is
911    /// perfectly valid, it simply cannot draw this text.
912    ///
913    /// So the resolved font is checked against the text, and when a character
914    /// is missing another font that can draw it is looked for.
915    ///
916    /// This is per run rather than per character: the font that covers the
917    /// first missing character is used for the whole run. Text that mixes
918    /// scripts inside one run is therefore still imperfect, but it is a large
919    /// improvement on drawing boxes.
920    pub fn resolve_font_for_text(
921        &mut self,
922        family: Option<&str>,
923        bold: bool,
924        italic: bool,
925        text: &str,
926    ) -> Result<FontId> {
927        let primary = self.resolve_font(family, bold, italic)?;
928
929        let Some(idx) = self.index_of(primary) else {
930            return Ok(primary);
931        };
932        let missing = self.uncovered(idx, text);
933        if missing.is_empty() {
934            return Ok(primary);
935        }
936
937        match self.font_covering(&missing, bold, italic) {
938            // Nothing installed can draw it. Keep the original font so the
939            // text still occupies the right space.
940            None => Ok(primary),
941            Some(id) => Ok(id),
942        }
943    }
944
945    /// The characters in `text` that the font at `idx` cannot draw.
946    ///
947    /// Whitespace and control characters are skipped: a font without a glyph
948    /// for a space is not a reason to go looking for another one.
949    fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
950        let font = &self.fonts[idx];
951        let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
952            return Vec::new();
953        };
954        let mut seen = HashSet::new();
955        text.chars()
956            .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
957            .filter(|&ch| face.glyph_index(ch).is_none())
958            .filter(|&ch| seen.insert(ch))
959            .collect()
960    }
961
962    /// Whether the font at `idx` has a glyph for `ch`.
963    fn covers(&self, idx: usize, ch: char) -> bool {
964        let font = &self.fonts[idx];
965        ttf_parser::Face::parse(&font.data, font.face_index)
966            .map(|face| face.glyph_index(ch).is_some())
967            .unwrap_or(false)
968    }
969
970    /// Find a font that can draw `missing`.
971    ///
972    /// A font covering every missing character wins. Failing that the one
973    /// covering the most is used, because a single run gets a single font and
974    /// partial coverage still beats a row of boxes. Picking on the first
975    /// missing character alone is not enough: a Japanese face may have the
976    /// characters shared with Chinese and not the simplified-only ones, so it
977    /// would look like a fix and still leave gaps.
978    fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
979        if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
980            return None;
981        }
982
983        let mut best: Option<(usize, usize)> = None; // (covered count, font index)
984        let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
985            let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
986            if covered == 0 {
987                return false;
988            }
989            if best.map(|(n, _)| covered > n).unwrap_or(true) {
990                *best = Some((covered, idx));
991            }
992            covered == missing.len()
993        };
994
995        // Fonts that already rescued an earlier run, which for a document in
996        // one script is almost always the answer again.
997        if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
998            for idx in known {
999                if consider(self, idx, &mut best) {
1000                    let id = self.fonts[idx].id;
1001                    self.record_layout_font(id);
1002                    return Some(id);
1003                }
1004            }
1005        }
1006
1007        // Families with broad coverage, then everything else the database
1008        // knows about. Both go through resolve_font so loading and caching
1009        // stay in one place.
1010        let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
1011            .iter()
1012            .map(|s| s.to_string())
1013            .chain(
1014                self.db
1015                    .faces()
1016                    .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
1017            )
1018            .collect();
1019
1020        for name in candidates {
1021            let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
1022                continue;
1023            };
1024            let Some(idx) = self.index_of(id) else {
1025                continue;
1026            };
1027            let complete = consider(self, idx, &mut best);
1028            if complete {
1029                self.remember_coverage_fallback(bold, italic, idx);
1030                return Some(id);
1031            }
1032        }
1033
1034        match best {
1035            Some((_, idx)) => {
1036                self.remember_coverage_fallback(bold, italic, idx);
1037                let id = self.fonts[idx].id;
1038                self.record_layout_font(id);
1039                Some(id)
1040            }
1041            None => {
1042                self.remember_coverage_misses(missing);
1043                None
1044            }
1045        }
1046    }
1047
1048    /// Index into `fonts` for a FontId.
1049    fn index_of(&self, id: FontId) -> Option<usize> {
1050        self.fonts.iter().position(|f| f.id == id)
1051    }
1052
1053    /// Resolve a font by family name, bold, and italic flags.
1054    /// Returns a FontId. Uses fallback chain if the requested font is not found.
1055    pub fn resolve_font(
1056        &mut self,
1057        family: Option<&str>,
1058        bold: bool,
1059        italic: bool,
1060    ) -> Result<FontId> {
1061        self.resolve_font_inner(family, bold, italic, true)
1062    }
1063
1064    /// Resolve a font for metrics without claiming that it emitted glyphs.
1065    #[doc(hidden)]
1066    pub fn resolve_font_for_metrics(
1067        &mut self,
1068        family: Option<&str>,
1069        bold: bool,
1070        italic: bool,
1071    ) -> Result<FontId> {
1072        self.resolve_font_inner(family, bold, italic, false)
1073    }
1074
1075    fn resolve_font_inner(
1076        &mut self,
1077        family: Option<&str>,
1078        bold: bool,
1079        italic: bool,
1080        record_layout_use: bool,
1081    ) -> Result<FontId> {
1082        let family_name = family.unwrap_or("Arial");
1083
1084        let key = FontKey {
1085            family: family_name.to_string(),
1086            bold,
1087            italic,
1088        };
1089
1090        if let Some(idx) = self.cache.get(&key).copied() {
1091            let id = self.fonts[idx].id;
1092            if record_layout_use {
1093                self.record_layout_font(id);
1094            }
1095            return Ok(id);
1096        }
1097
1098        let requested_key = family_name.to_lowercase();
1099        let style = if italic {
1100            fontdb::Style::Italic
1101        } else {
1102            fontdb::Style::Normal
1103        };
1104        let weight = if bold {
1105            fontdb::Weight::BOLD
1106        } else {
1107            fontdb::Weight::NORMAL
1108        };
1109
1110        let query_family = |family: &str| {
1111            if let Some(ids) = self.caller_families.get(family)
1112                && let Some(id) = best_caller_face(&self.db, ids, weight, style)
1113            {
1114                return Some(id);
1115            }
1116            let query = fontdb::Query {
1117                families: &[fontdb::Family::Name(family)],
1118                weight,
1119                style,
1120                stretch: fontdb::Stretch::Normal,
1121            };
1122            self.db.query(&query)
1123        };
1124
1125        let mut found_id = query_family(family_name);
1126        if found_id.is_none()
1127            && let Some(alias) = self.explicit_alias_map.get(&requested_key)
1128        {
1129            found_id = query_family(alias);
1130        }
1131        if found_id.is_none()
1132            && let Some(ids) = self.caller_aliases.get(&requested_key)
1133        {
1134            found_id = best_caller_face(&self.db, ids, weight, style);
1135        }
1136
1137        // Map common Word font names to metric-compatible alternatives, then
1138        // try generic fallbacks.
1139        let mut fallbacks: Vec<&str> = map_font_name(family_name).to_vec();
1140        for generic in &[
1141            "Carlito",
1142            "Arial",
1143            "Liberation Sans",
1144            "Helvetica",
1145            "DejaVu Sans",
1146            "Noto Sans",
1147        ] {
1148            if !fallbacks.contains(generic) {
1149                fallbacks.push(generic);
1150            }
1151        }
1152
1153        if found_id.is_none() {
1154            for fallback in &fallbacks {
1155                let Some(id) = query_family(fallback) else {
1156                    continue;
1157                };
1158                found_id = Some(id);
1159                break;
1160            }
1161        }
1162
1163        // Last resort: try generic families
1164        if found_id.is_none() {
1165            for generic_family in &[
1166                fontdb::Family::SansSerif,
1167                fontdb::Family::Serif,
1168                fontdb::Family::Monospace,
1169            ] {
1170                let query = fontdb::Query {
1171                    families: &[*generic_family],
1172                    weight,
1173                    style,
1174                    stretch: fontdb::Stretch::Normal,
1175                };
1176                if let Some(id) = self.db.query(&query) {
1177                    found_id = Some(id);
1178                    break;
1179                }
1180            }
1181        }
1182
1183        let db_id = found_id.ok_or_else(|| {
1184            LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
1185        })?;
1186
1187        // Preserve the established one-loaded-font-per-request-key behavior
1188        // while the bounded alias cache has room. At the ceiling, reuse the
1189        // exact resolved face rather than growing without limit.
1190        if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
1191            && let Some(idx) = self
1192                .fonts
1193                .iter()
1194                .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
1195        {
1196            let id = self.fonts[idx].id;
1197            if record_layout_use {
1198                self.record_layout_font(id);
1199            }
1200            return Ok(id);
1201        }
1202
1203        let font_id = FontId(self.next_id);
1204        self.next_id += 1;
1205
1206        // Load file-backed data through the process cache. All faces in a TTC
1207        // carry the same source path, so their collection indices share bytes.
1208        let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
1209            .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
1210
1211        let (units_per_em, ascender, descender, line_gap) = {
1212            let face = ttf_parser::Face::parse(&data, face_index)
1213                .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
1214            (
1215                face.units_per_em(),
1216                face.ascender(),
1217                face.descender(),
1218                face.line_gap(),
1219            )
1220        };
1221
1222        // Every metric and advance is scaled by size/upem, so a zero here would
1223        // turn the whole layout into infinities.
1224        if units_per_em == 0 {
1225            return Err(LayoutError::FontParse(format!(
1226                "font '{family_name}' declares zero units per em"
1227            )));
1228        }
1229
1230        let shaper_data = {
1231            let face = harfrust::FontRef::from_index(&data, face_index)
1232                .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
1233            harfrust::ShaperData::new(&face)
1234        };
1235
1236        let actual_family = self
1237            .db
1238            .face(db_id)
1239            .map(|f| {
1240                f.families
1241                    .first()
1242                    .map(|(name, _)| name.clone())
1243                    .unwrap_or_else(|| family_name.to_string())
1244            })
1245            .unwrap_or_else(|| family_name.to_string());
1246
1247        let idx = self.fonts.len();
1248        self.fonts.push(LoadedFont {
1249            db_id,
1250            id: font_id,
1251            family: actual_family,
1252            bold,
1253            italic,
1254            data,
1255            face_index,
1256            units_per_em,
1257            ascender,
1258            descender,
1259            line_gap,
1260            shaper_data,
1261        });
1262        self.remember_font_key(key, idx);
1263        if record_layout_use {
1264            self.record_layout_font(font_id);
1265        }
1266
1267        Ok(font_id)
1268    }
1269
1270    /// Get font metrics at a given size in points.
1271    pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
1272        let font = self.get_font(font_id)?;
1273        let scale = size_pt / font.units_per_em as f64;
1274
1275        Ok(FontMetrics {
1276            ascent: font.ascender as f64 * scale,
1277            descent: -(font.descender as f64) * scale, // make positive
1278            line_gap: font.line_gap as f64 * scale,
1279            units_per_em: font.units_per_em,
1280        })
1281    }
1282
1283    /// Shape a text string using HarfRust. Returns glyph IDs and advances.
1284    pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
1285        // HarfRust cannot derive segment properties from an empty buffer, and
1286        // there is nothing to shape anyway.
1287        if text.is_empty() {
1288            return Ok(ShapedText {
1289                glyph_ids: Vec::new(),
1290                advances: Vec::new(),
1291                width: 0.0,
1292            });
1293        }
1294
1295        let key = ShapingKey {
1296            font_id,
1297            text: text.to_owned(),
1298            size_bits: size_pt.to_bits(),
1299        };
1300        let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
1301        let mut memo = match self.shaping_memo.lock() {
1302            Ok(memo) => memo,
1303            Err(poisoned) => {
1304                let mut memo = poisoned.into_inner();
1305                memo.clear();
1306                self.shaping_memo.clear_poison();
1307                memo
1308            }
1309        };
1310        if let Some(shaped) = memo
1311            .entries
1312            .iter()
1313            .rev()
1314            .find(|(candidate, _, _, candidate_fingerprint)| {
1315                *candidate_fingerprint == fingerprint && candidate == &key
1316            })
1317            .map(|(_, shaped, _, _)| shaped.clone())
1318        {
1319            #[cfg(test)]
1320            {
1321                memo.hits += 1;
1322            }
1323            return Ok(shaped);
1324        }
1325        #[cfg(test)]
1326        {
1327            memo.misses += 1;
1328        }
1329
1330        let font = self.get_font(font_id)?;
1331
1332        let face = harfrust::FontRef::from_index(&font.data, font.face_index)
1333            .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
1334
1335        let shaper = font.shaper_data.shaper(&face).build();
1336
1337        let mut buffer = harfrust::UnicodeBuffer::new();
1338        buffer.push_str(text);
1339        // Infer direction, script and language from the text. Unlike rustybuzz,
1340        // HarfRust does not do this implicitly and panics on an unset direction.
1341        buffer.guess_segment_properties();
1342
1343        let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
1344        let infos = output.glyph_infos();
1345        let positions = output.glyph_positions();
1346
1347        let upem = font.units_per_em as f64;
1348        let scale = size_pt / upem;
1349
1350        let mut glyph_ids = Vec::with_capacity(infos.len());
1351        let mut advances = Vec::with_capacity(positions.len());
1352        let mut total_width = 0.0;
1353
1354        for (info, pos) in infos.iter().zip(positions.iter()) {
1355            glyph_ids.push(info.glyph_id as u16);
1356            let advance = pos.x_advance as f64 * scale;
1357            advances.push(advance);
1358            total_width += advance;
1359        }
1360
1361        let shaped = ShapedText {
1362            glyph_ids,
1363            advances,
1364            width: total_width,
1365        };
1366        memo.insert(key, shaped.clone());
1367        Ok(shaped)
1368    }
1369
1370    /// Shape one logical text segment into script, font, and bidi-level spans.
1371    ///
1372    /// The returned spans are in logical order. The rich line breaker applies
1373    /// UAX 9 visual order only after it knows the final line boundaries.
1374    pub fn shape_multilingual_text(
1375        &mut self,
1376        segment: TextSegment,
1377        language: Option<&str>,
1378        base_direction: TextDirection,
1379        no_wrap: bool,
1380    ) -> Result<Vec<MultilingualTextSegment>> {
1381        if segment.text.is_empty() {
1382            return Ok(Vec::new());
1383        }
1384
1385        let paragraph_level = match base_direction {
1386            TextDirection::Auto => None,
1387            TextDirection::LeftToRight => Some(unicode_bidi::Level::ltr()),
1388            TextDirection::RightToLeft => Some(unicode_bidi::Level::rtl()),
1389        };
1390        let bidi = unicode_bidi::BidiInfo::new(&segment.text, paragraph_level);
1391        let levels = bidi.levels.clone();
1392        self.shape_multilingual_with_levels(segment, language, no_wrap, &levels, 0)
1393    }
1394
1395    /// Shape styled spans with one paragraph-wide bidi resolution.
1396    pub fn shape_multilingual_paragraph(
1397        &mut self,
1398        segments: Vec<(TextSegment, Option<String>)>,
1399        base_direction: TextDirection,
1400        no_wrap: bool,
1401    ) -> Result<Vec<MultilingualTextSegment>> {
1402        let paragraph_text = segments
1403            .iter()
1404            .map(|(segment, _)| segment.text.as_str())
1405            .collect::<String>();
1406        let mut paragraph_offset = 0usize;
1407        let segment_starts = segments
1408            .iter()
1409            .map(|(segment, _)| {
1410                let start = paragraph_offset;
1411                paragraph_offset += segment.text.len();
1412                start
1413            })
1414            .collect::<Vec<_>>();
1415        if paragraph_text.is_empty() {
1416            return Ok(Vec::new());
1417        }
1418        let paragraph_level = match base_direction {
1419            TextDirection::Auto => None,
1420            TextDirection::LeftToRight => Some(unicode_bidi::Level::ltr()),
1421            TextDirection::RightToLeft => Some(unicode_bidi::Level::rtl()),
1422        };
1423        let bidi = unicode_bidi::BidiInfo::new(&paragraph_text, paragraph_level);
1424        let paragraph_level = bidi
1425            .paragraphs
1426            .first()
1427            .map(|paragraph| paragraph.level)
1428            .unwrap_or_else(unicode_bidi::Level::ltr);
1429        let mut logical_index = 0usize;
1430        let mut shaped = Vec::new();
1431        for ((segment, language), byte_offset) in segments.into_iter().zip(segment_starts) {
1432            let byte_end = byte_offset + segment.text.len();
1433            if !segment.text.is_empty() {
1434                let forced_levels = (segment.direction != TextDirection::Auto)
1435                    .then(|| {
1436                        explicit_direction_levels(&segment.text, segment.direction, paragraph_level)
1437                    })
1438                    .transpose()?;
1439                let levels = forced_levels
1440                    .as_deref()
1441                    .unwrap_or(&bidi.levels[byte_offset..byte_end]);
1442                let spans = self.shape_multilingual_with_levels(
1443                    segment,
1444                    language.as_deref(),
1445                    no_wrap,
1446                    levels,
1447                    logical_index,
1448                )?;
1449                logical_index += spans.len();
1450                shaped.extend(spans);
1451            }
1452        }
1453        Ok(shaped)
1454    }
1455
1456    fn shape_multilingual_with_levels(
1457        &mut self,
1458        segment: TextSegment,
1459        language: Option<&str>,
1460        no_wrap: bool,
1461        levels: &[unicode_bidi::Level],
1462        logical_index_base: usize,
1463    ) -> Result<Vec<MultilingualTextSegment>> {
1464        let grapheme_boundaries = icu_segmenter::GraphemeClusterSegmenter::new()
1465            .segment_str(&segment.text)
1466            .collect::<Vec<_>>();
1467        let break_offsets = if no_wrap {
1468            HashSet::new()
1469        } else {
1470            multilingual_break_opportunities(&segment.text)
1471        };
1472
1473        let mut logical_ranges = Vec::<(usize, usize, TextScript, unicode_bidi::Level)>::new();
1474        let mut start = 0usize;
1475        let mut current_script = TextScript::Common;
1476        let mut current_level = levels[0];
1477        for window in grapheme_boundaries.windows(2) {
1478            let grapheme_start = window[0];
1479            let grapheme_end = window[1];
1480            let script = script_for_grapheme(&segment.text[grapheme_start..grapheme_end]);
1481            let script = if script == TextScript::Common {
1482                current_script
1483            } else {
1484                script
1485            };
1486            let level = levels[grapheme_start];
1487            if grapheme_start > start && (script != current_script || level != current_level) {
1488                logical_ranges.push((start, grapheme_start, current_script, current_level));
1489                start = grapheme_start;
1490            }
1491            current_script = script;
1492            current_level = level;
1493            if break_offsets.contains(&grapheme_end) && grapheme_end < segment.text.len() {
1494                logical_ranges.push((start, grapheme_end, current_script, current_level));
1495                start = grapheme_end;
1496            }
1497        }
1498        if start < segment.text.len() {
1499            logical_ranges.push((start, segment.text.len(), current_script, current_level));
1500        }
1501
1502        let mut font_ranges = Vec::new();
1503        for (start, end, script, level) in logical_ranges {
1504            let boundaries = icu_segmenter::GraphemeClusterSegmenter::new()
1505                .segment_str(&segment.text[start..end])
1506                .map(|offset| start + offset)
1507                .collect::<Vec<_>>();
1508            let mut range_start = start;
1509            let mut range_font = None;
1510            for window in boundaries.windows(2) {
1511                let grapheme_start = window[0];
1512                let grapheme_end = window[1];
1513                let font_id = self.font_for_multilingual_span(
1514                    segment.font_id,
1515                    &segment.text[grapheme_start..grapheme_end],
1516                    segment.bold,
1517                    segment.italic,
1518                );
1519                if let Some(current_font) = range_font
1520                    && current_font != font_id
1521                {
1522                    font_ranges.push((range_start, grapheme_start, script, level, current_font));
1523                    range_start = grapheme_start;
1524                }
1525                range_font = Some(font_id);
1526            }
1527            if let Some(font_id) = range_font {
1528                font_ranges.push((range_start, end, script, level, font_id));
1529            }
1530        }
1531
1532        let mut logical = Vec::with_capacity(font_ranges.len());
1533        for (logical_index, (start, end, script, level, font_id)) in
1534            font_ranges.into_iter().enumerate()
1535        {
1536            let text = &segment.text[start..end];
1537            let metrics = self.metrics(font_id, segment.font_size)?;
1538            let direction = if level.is_rtl() {
1539                TextDirection::RightToLeft
1540            } else {
1541                TextDirection::LeftToRight
1542            };
1543            let positioned = self.shape_explicit(
1544                font_id,
1545                text,
1546                segment.font_size,
1547                script,
1548                language,
1549                direction,
1550            )?;
1551            let char_start = segment.text[..start].chars().count() as u32;
1552            let char_end = segment.text[..end].chars().count() as u32;
1553            let source = segment.source.map(|source| SourceSpan {
1554                node: source.node,
1555                char_start: source.char_start + char_start,
1556                char_end: source.char_start + char_end,
1557            });
1558            let mut base = segment.clone();
1559            base.text = text.to_owned();
1560            base.source = source;
1561            base.font_id = font_id;
1562            base.glyph_ids = positioned.glyph_ids;
1563            base.advances = positioned.x_advances.clone();
1564            base.width = positioned.x_advances.iter().sum();
1565            base.ascent = metrics.ascent;
1566            base.descent = metrics.descent;
1567            base.line_gap = metrics.line_gap;
1568            logical.push(MultilingualTextSegment::new(
1569                base,
1570                logical_index_base + logical_index,
1571                language.map(str::to_owned),
1572                script,
1573                direction,
1574                level.number(),
1575                positioned.x_advances,
1576                positioned.y_advances,
1577                positioned.x_offsets,
1578                positioned.y_offsets,
1579                positioned.clusters,
1580                break_offsets.contains(&end),
1581            )?);
1582        }
1583
1584        Ok(logical)
1585    }
1586
1587    fn font_for_multilingual_span(
1588        &mut self,
1589        preferred: FontId,
1590        text: &str,
1591        bold: bool,
1592        italic: bool,
1593    ) -> FontId {
1594        let Some(index) = self.index_of(preferred) else {
1595            return preferred;
1596        };
1597        if self.uncovered(index, text).is_empty() {
1598            return preferred;
1599        }
1600        let required = text
1601            .chars()
1602            .filter(|character| !character.is_whitespace() && !character.is_control())
1603            .collect::<Vec<_>>();
1604        self.font_covering(&required, bold, italic)
1605            .unwrap_or(preferred)
1606    }
1607
1608    fn shape_explicit(
1609        &self,
1610        font_id: FontId,
1611        text: &str,
1612        size_pt: f64,
1613        script: TextScript,
1614        language: Option<&str>,
1615        direction: TextDirection,
1616    ) -> Result<PositionedShape> {
1617        let font = self.get_font(font_id)?;
1618        let face = harfrust::FontRef::from_index(&font.data, font.face_index)
1619            .map_err(|error| LayoutError::Shaping(format!("failed to read font face: {error}")))?;
1620        let shaper = font.shaper_data.shaper(&face).build();
1621        let mut buffer = harfrust::UnicodeBuffer::new();
1622        buffer.push_str(text);
1623        buffer.set_script(harfrust_script(script));
1624        buffer.set_direction(match direction {
1625            TextDirection::RightToLeft => harfrust::Direction::RightToLeft,
1626            TextDirection::Auto | TextDirection::LeftToRight => harfrust::Direction::LeftToRight,
1627        });
1628        if let Some(language) = language.and_then(harfrust::Language::new) {
1629            buffer.set_language(language);
1630        }
1631        let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
1632        let infos = output.glyph_infos();
1633        let positions = output.glyph_positions();
1634        let scale = size_pt / f64::from(font.units_per_em);
1635        let glyph_ids = infos
1636            .iter()
1637            .map(|info| info.glyph_id as u16)
1638            .collect::<Vec<_>>();
1639        let x_advances = positions
1640            .iter()
1641            .map(|pos| f64::from(pos.x_advance) * scale)
1642            .collect();
1643        let y_advances = positions
1644            .iter()
1645            .map(|pos| f64::from(pos.y_advance) * scale)
1646            .collect();
1647        let x_offsets = positions
1648            .iter()
1649            .map(|pos| f64::from(pos.x_offset) * scale)
1650            .collect();
1651        let y_offsets = positions
1652            .iter()
1653            .map(|pos| f64::from(pos.y_offset) * scale)
1654            .collect();
1655        let clusters = glyph_clusters(infos, text);
1656        Ok(PositionedShape {
1657            glyph_ids,
1658            x_advances,
1659            y_advances,
1660            x_offsets,
1661            y_offsets,
1662            clusters,
1663        })
1664    }
1665
1666    /// Get font data for PDF embedding.
1667    pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
1668        let font = self.get_font(font_id)?;
1669        Ok(crate::output::FontData {
1670            id: font.id,
1671            family: font.family.clone(),
1672            data: Arc::clone(&font.data),
1673            face_index: font.face_index,
1674            bold: font.bold,
1675            italic: font.italic,
1676        })
1677    }
1678
1679    /// Get all used font data.
1680    pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
1681        self.fonts
1682            .iter()
1683            .map(|f| crate::output::FontData {
1684                id: f.id,
1685                family: f.family.clone(),
1686                data: Arc::clone(&f.data),
1687                face_index: f.face_index,
1688                bold: f.bold,
1689                italic: f.italic,
1690            })
1691            .collect()
1692    }
1693
1694    fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
1695        self.fonts
1696            .iter()
1697            .find(|f| f.id == font_id)
1698            .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
1699    }
1700
1701    fn remember_font_key(&mut self, key: FontKey, index: usize) {
1702        if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
1703            self.cache.insert(key, index);
1704        }
1705    }
1706
1707    fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
1708        let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
1709        if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
1710            known.push(index);
1711        }
1712    }
1713
1714    fn remember_coverage_misses(&mut self, missing: &[char]) {
1715        for &ch in missing {
1716            if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
1717                break;
1718            }
1719            self.coverage_misses.insert(ch);
1720        }
1721    }
1722
1723    fn record_layout_font(&mut self, font_id: FontId) {
1724        if let Some(trace) = self.paragraph_font_trace.as_mut() {
1725            if trace.ids.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
1726                trace.ids.push(font_id);
1727            } else {
1728                trace.overflowed = true;
1729            }
1730        }
1731        if !self.layout_fonts.contains(&font_id) {
1732            self.layout_fonts.push(font_id);
1733        }
1734    }
1735
1736    #[cfg(test)]
1737    fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
1738        let memo = self
1739            .shaping_memo
1740            .lock()
1741            .unwrap_or_else(std::sync::PoisonError::into_inner);
1742        (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
1743    }
1744}
1745
1746struct PositionedShape {
1747    glyph_ids: Vec<u16>,
1748    x_advances: Vec<f64>,
1749    y_advances: Vec<f64>,
1750    x_offsets: Vec<f64>,
1751    y_offsets: Vec<f64>,
1752    clusters: Vec<GlyphCluster>,
1753}
1754
1755fn script_for_grapheme(grapheme: &str) -> TextScript {
1756    grapheme
1757        .chars()
1758        .map(script_for_char)
1759        .find(|script| *script != TextScript::Common)
1760        .unwrap_or(TextScript::Common)
1761}
1762
1763fn script_for_char(character: char) -> TextScript {
1764    match character as u32 {
1765        0x0041..=0x024f | 0x1e00..=0x1eff => TextScript::Latin,
1766        0x0590..=0x05ff => TextScript::Hebrew,
1767        0x0600..=0x06ff | 0x0750..=0x077f | 0x08a0..=0x08ff => TextScript::Arabic,
1768        0x0900..=0x097f | 0xa8e0..=0xa8ff => TextScript::Devanagari,
1769        0x0e00..=0x0e7f => TextScript::Thai,
1770        0x3400..=0x4dbf | 0x4e00..=0x9fff | 0xf900..=0xfaff => TextScript::Han,
1771        _ => TextScript::Common,
1772    }
1773}
1774
1775fn harfrust_script(script: TextScript) -> harfrust::Script {
1776    match script {
1777        TextScript::Latin => harfrust::script::LATIN,
1778        TextScript::Arabic => harfrust::script::ARABIC,
1779        TextScript::Hebrew => harfrust::script::HEBREW,
1780        TextScript::Devanagari => harfrust::script::DEVANAGARI,
1781        TextScript::Thai => harfrust::script::THAI,
1782        TextScript::Han => harfrust::script::HAN,
1783        TextScript::Common => harfrust::script::COMMON,
1784    }
1785}
1786
1787fn multilingual_break_opportunities(text: &str) -> HashSet<usize> {
1788    let mut opportunities = icu_segmenter::WordSegmenter::new_auto(Default::default())
1789        .segment_str(text)
1790        .filter(|offset| *offset > 0 && *offset <= text.len())
1791        .collect::<HashSet<_>>();
1792    for (offset, _) in unicode_linebreak::linebreaks(text) {
1793        if offset > 0 {
1794            opportunities.insert(offset);
1795        }
1796    }
1797    opportunities.retain(|offset| {
1798        let before = text[..*offset].chars().next_back();
1799        let after = text[*offset..].chars().next();
1800        before
1801            .zip(after)
1802            .is_none_or(|(before, after)| crate::line::multilingual_break_allowed(before, after))
1803    });
1804    opportunities
1805}
1806
1807fn glyph_clusters(infos: &[harfrust::GlyphInfo], text: &str) -> Vec<GlyphCluster> {
1808    let mut byte_starts = infos
1809        .iter()
1810        .map(|info| info.cluster as usize)
1811        .collect::<Vec<_>>();
1812    byte_starts.push(text.len());
1813    byte_starts.sort_unstable();
1814    byte_starts.dedup();
1815
1816    let mut clusters = Vec::new();
1817    let mut glyph_start = 0usize;
1818    while glyph_start < infos.len() {
1819        let cluster_byte = infos[glyph_start].cluster as usize;
1820        let mut glyph_end = glyph_start + 1;
1821        while glyph_end < infos.len() && infos[glyph_end].cluster as usize == cluster_byte {
1822            glyph_end += 1;
1823        }
1824        let byte_end = byte_starts
1825            .iter()
1826            .copied()
1827            .find(|candidate| *candidate > cluster_byte)
1828            .unwrap_or(text.len());
1829        clusters.push(GlyphCluster {
1830            glyph_start: glyph_start as u32,
1831            glyph_end: glyph_end as u32,
1832            char_start: text[..cluster_byte].chars().count() as u32,
1833            char_end: text[..byte_end].chars().count() as u32,
1834        });
1835        glyph_start = glyph_end;
1836    }
1837    clusters
1838}
1839
1840fn best_caller_face(
1841    db: &fontdb::Database,
1842    ids: &[fontdb::ID],
1843    weight: fontdb::Weight,
1844    style: fontdb::Style,
1845) -> Option<fontdb::ID> {
1846    const CANDIDATE_FAMILY: &str = "__rdocx_caller_candidate__";
1847
1848    let mut candidates = fontdb::Database::new();
1849    let mut candidate_ids = Vec::with_capacity(ids.len());
1850    for id in ids {
1851        let mut face = db.face(*id)?.clone();
1852        for (family, _) in &mut face.families {
1853            CANDIDATE_FAMILY.clone_into(family);
1854        }
1855        let candidate_id = candidates.push_face_info(face);
1856        candidate_ids.push((candidate_id, *id));
1857    }
1858
1859    let selected = candidates.query(&fontdb::Query {
1860        families: &[fontdb::Family::Name(CANDIDATE_FAMILY)],
1861        weight,
1862        style,
1863        stretch: fontdb::Stretch::Normal,
1864    })?;
1865    candidate_ids
1866        .into_iter()
1867        .find_map(|(candidate, original)| (candidate == selected).then_some(original))
1868}
1869
1870fn bundled_font_database() -> fontdb::Database {
1871    let mut db = fontdb::Database::new();
1872    for (_family, data) in crate::bundled_fonts::bundled_font_data() {
1873        db.load_font_data(data.to_vec());
1874    }
1875    db
1876}
1877
1878fn font_data_for_face(
1879    db: &fontdb::Database,
1880    id: fontdb::ID,
1881    memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
1882) -> Option<(Arc<[u8]>, u32)> {
1883    let face = db.face(id)?;
1884    let face_index = face.index;
1885    match &face.source {
1886        fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
1887            Some(data) => Some((Arc::clone(data), face_index)),
1888            None => {
1889                let data: Arc<[u8]> = Arc::from(data.as_ref().as_ref().to_vec());
1890                memory_face_data.insert(id, Arc::clone(&data));
1891                Some((data, face_index))
1892            }
1893        },
1894        #[cfg(feature = "system-fonts")]
1895        fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
1896    }
1897}
1898
1899#[cfg(feature = "system-fonts")]
1900fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
1901    let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
1902    shared_file_font_bytes_from_cache(cache, path)
1903}
1904
1905#[cfg(feature = "system-fonts")]
1906fn shared_file_font_bytes_from_cache(
1907    cache_lock: &Mutex<FileFontCache>,
1908    path: &Path,
1909) -> Option<Arc<[u8]>> {
1910    let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1911    let mut cache = match cache_lock.lock() {
1912        Ok(cache) => cache,
1913        Err(poisoned) => {
1914            let mut cache = poisoned.into_inner();
1915            cache.clear();
1916            cache_lock.clear_poison();
1917            cache
1918        }
1919    };
1920    if let Some(index) = cache
1921        .entries
1922        .iter()
1923        .position(|(candidate, _, _)| candidate == &identity)
1924    {
1925        let entry = cache.entries.remove(index).expect("cache index exists");
1926        let bytes = Arc::clone(&entry.1);
1927        cache.entries.push_back(entry);
1928        return Some(bytes);
1929    }
1930
1931    let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
1932    cache_file_font_bytes(&mut cache, identity, bytes)
1933}
1934
1935#[cfg(feature = "system-fonts")]
1936fn cache_file_font_bytes(
1937    cache: &mut FileFontCache,
1938    identity: PathBuf,
1939    bytes: Arc<[u8]>,
1940) -> Option<Arc<[u8]>> {
1941    let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
1942        .saturating_add(identity.as_os_str().len())
1943        .saturating_add(bytes.len());
1944    if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
1945        while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
1946            || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
1947        {
1948            let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
1949                break;
1950            };
1951            cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
1952        }
1953        cache.bytes += entry_bytes;
1954        cache
1955            .entries
1956            .push_back((identity, Arc::clone(&bytes), entry_bytes));
1957    }
1958    Some(bytes)
1959}
1960
1961/// Map common Word font names to metric-compatible alternatives.
1962/// Returns a list of candidate names to try (including the original).
1963///
1964/// Priority: original font → metric-compatible open-source clone → generic fallback.
1965/// Carlito is metric-compatible with Calibri, Caladea with Cambria,
1966/// Liberation Sans/Serif/Mono with Arial/Times New Roman/Courier New.
1967fn map_font_name(name: &str) -> &[&str] {
1968    match name {
1969        "Calibri" => &["Calibri", "Carlito"],
1970        "Calibri Light" => &["Calibri Light", "Carlito"],
1971        "Cambria" => &["Cambria", "Caladea"],
1972        "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
1973        "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
1974        "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
1975        "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
1976        "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
1977        "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
1978        "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
1979        "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
1980        "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
1981        "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
1982        "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
1983        "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
1984        "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
1985        "Impact" => &["Impact", "Liberation Sans", "Arial"],
1986        "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
1987        "Symbol" => &["Symbol", "DejaVu Sans"],
1988        "Wingdings" => &["Wingdings", "Symbol"],
1989        _ => &[],
1990    }
1991}
1992
1993#[cfg(test)]
1994mod tests {
1995    use super::*;
1996    use crate::Color;
1997    use crate::bundled_fonts::bundled_font_data;
1998
1999    fn multilingual_test_segment(
2000        manager: &mut FontManager,
2001        text: &str,
2002        size_pt: f64,
2003        source: Option<SourceSpan>,
2004    ) -> TextSegment {
2005        let font_id = manager
2006            .resolve_font_for_text(None, false, false, text)
2007            .expect("test font resolves");
2008        let metrics = manager
2009            .metrics(font_id, size_pt)
2010            .expect("test font metrics");
2011        let shaped = manager
2012            .shape_text(font_id, text, size_pt)
2013            .expect("test seed shapes");
2014        TextSegment {
2015            text: text.to_owned(),
2016            direction: TextDirection::Auto,
2017            source,
2018            font_id,
2019            font_size: size_pt,
2020            glyph_ids: shaped.glyph_ids,
2021            advances: shaped.advances,
2022            width: shaped.width,
2023            ascent: metrics.ascent,
2024            descent: metrics.descent,
2025            line_gap: metrics.line_gap,
2026            color: Color::BLACK,
2027            bold: false,
2028            italic: false,
2029            underline: None,
2030            strike: false,
2031            dstrike: false,
2032            highlight: None,
2033            baseline_offset: 0.0,
2034            hyperlink_url: None,
2035            field_kind: None,
2036            note: None,
2037        }
2038    }
2039
2040    #[test]
2041    fn caller_font_labels_resolve_after_exact_embedded_families() {
2042        let caladea = bundled_font_data()
2043            .into_iter()
2044            .find(|(family, _)| *family == "Caladea")
2045            .expect("Caladea is bundled")
2046            .1;
2047        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2048        manager.load_additional_fonts(&[
2049            FontFile {
2050                family: "Document Serif".to_owned(),
2051                data: caladea.to_vec(),
2052            },
2053            FontFile {
2054                family: "Carlito".to_owned(),
2055                data: caladea.to_vec(),
2056            },
2057        ]);
2058
2059        let aliased = manager
2060            .resolve_font(Some("Document Serif"), false, false)
2061            .expect("caller label resolves");
2062        assert_eq!(
2063            manager.fonts[manager.index_of(aliased).unwrap()].family,
2064            "Caladea"
2065        );
2066
2067        let exact = manager
2068            .resolve_font(Some("Carlito"), false, false)
2069            .expect("embedded family resolves exactly");
2070        assert_eq!(
2071            manager.fonts[manager.index_of(exact).unwrap()].family,
2072            "Carlito"
2073        );
2074
2075        manager.set_caller_aliases(&[("Document Serif".to_owned(), "Carlito".to_owned())]);
2076        let explicit = manager
2077            .resolve_font(Some("Document Serif"), false, false)
2078            .expect("explicit alias precedes label-derived alias");
2079        assert_eq!(
2080            manager.fonts[manager.index_of(explicit).unwrap()].family,
2081            "Carlito"
2082        );
2083
2084        manager.load_additional_fonts(&[FontFile {
2085            family: "Caladea".to_owned(),
2086            data: caladea.to_vec(),
2087        }]);
2088        assert!(manager.caller_aliases.is_empty());
2089
2090        manager.set_caller_aliases(&[("Arial".to_owned(), "Caladea".to_owned())]);
2091        let caller_alias = manager
2092            .resolve_font(Some("Arial"), false, false)
2093            .expect("explicit alias resolves before mapped fallback");
2094        assert_eq!(
2095            manager.fonts[manager.index_of(caller_alias).unwrap()].family,
2096            "Caladea"
2097        );
2098        let generic = manager
2099            .resolve_font(Some("Unmapped Document Family"), false, false)
2100            .expect("generic fallback remains available");
2101        assert_eq!(
2102            manager.fonts[manager.index_of(generic).unwrap()].family,
2103            "Carlito"
2104        );
2105    }
2106
2107    #[test]
2108    fn caller_alias_updates_preserve_bytes_and_invalidate_resolution_state() {
2109        let caladea = bundled_font_data()
2110            .into_iter()
2111            .find(|(family, _)| *family == "Caladea")
2112            .expect("Caladea is bundled")
2113            .1;
2114        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2115        manager.load_additional_fonts(&[FontFile {
2116            family: "Caladea".to_owned(),
2117            data: caladea.to_vec(),
2118        }]);
2119        let aliases = vec![
2120            ("Document Serif A".to_owned(), "Caladea".to_owned()),
2121            ("Document Serif B".to_owned(), "Caladea".to_owned()),
2122        ];
2123        assert!(manager.set_caller_aliases(&aliases));
2124
2125        let first = manager
2126            .resolve_font(Some("Document Serif A"), false, false)
2127            .expect("first alias resolves");
2128        let second = manager
2129            .resolve_font(Some("Document Serif B"), false, false)
2130            .expect("second alias resolves");
2131        let first_data = &manager.fonts[manager.index_of(first).unwrap()].data;
2132        let second_data = &manager.fonts[manager.index_of(second).unwrap()].data;
2133        assert!(Arc::ptr_eq(first_data, second_data));
2134        assert!(!manager.set_caller_aliases(&aliases));
2135
2136        assert!(
2137            manager.set_caller_aliases(&[("Document Serif A".to_owned(), "Carlito".to_owned(),)])
2138        );
2139        let changed = manager
2140            .resolve_font(Some("Document Serif A"), false, false)
2141            .expect("changed alias resolves");
2142        assert_eq!(
2143            manager.fonts[manager.index_of(changed).unwrap()].family,
2144            "Carlito"
2145        );
2146        assert!(
2147            manager.index_of(first).is_some(),
2148            "loaded faces are retained"
2149        );
2150    }
2151
2152    #[test]
2153    fn explicit_alias_state_respects_entry_and_retained_byte_ceilings() {
2154        let aliases = (0..CALLER_ALIAS_MAX_ENTRIES + 32)
2155            .map(|index| (format!("Document Serif {index}"), "Caladea".to_owned()))
2156            .collect::<Vec<_>>();
2157        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2158        manager.set_caller_aliases(&aliases);
2159        assert_eq!(
2160            manager.explicit_aliases.as_slice(),
2161            &aliases[..CALLER_ALIAS_MAX_ENTRIES]
2162        );
2163        assert!(manager.explicit_aliases.len() <= CALLER_ALIAS_MAX_ENTRIES);
2164        assert!(manager.explicit_alias_map.len() <= CALLER_ALIAS_MAX_ENTRIES);
2165
2166        let retained_large = ("x".repeat(32_760), String::new());
2167        let byte_limited = vec![
2168            retained_large.clone(),
2169            ("discarded bytes".to_owned(), "Caladea".to_owned()),
2170        ];
2171        manager.set_caller_aliases(&byte_limited);
2172        assert_eq!(manager.explicit_aliases, vec![retained_large]);
2173
2174        let oversized = vec![("x".repeat(40_000), "Caladea".to_owned())];
2175        manager.set_caller_aliases(&oversized);
2176        let retained_bytes = manager
2177            .explicit_aliases
2178            .iter()
2179            .map(|(requested, target)| requested.len() + target.len())
2180            .sum::<usize>()
2181            + manager
2182                .explicit_alias_map
2183                .iter()
2184                .map(|(requested, target)| requested.len() + target.len())
2185                .sum::<usize>();
2186        assert!(retained_bytes <= CALLER_ALIAS_MAX_RETAINED_BYTES);
2187        assert!(manager.explicit_aliases.is_empty());
2188        assert!(manager.explicit_alias_map.is_empty());
2189    }
2190
2191    #[test]
2192    fn label_alias_prefers_caller_bytes_over_bundled_same_family() {
2193        let bundled = bundled_font_data()
2194            .into_iter()
2195            .find(|(family, _)| *family == "Caladea")
2196            .expect("Caladea is bundled")
2197            .1;
2198        let mut caller = bundled.to_vec();
2199        caller.push(0);
2200        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2201        manager.load_additional_fonts(&[FontFile {
2202            family: "Document Serif".to_owned(),
2203            data: caller.clone(),
2204        }]);
2205
2206        let resolved = manager
2207            .resolve_font(Some("Document Serif"), false, false)
2208            .expect("caller label resolves");
2209        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2210        assert_eq!(loaded.family, "Caladea");
2211        assert_eq!(loaded.data.as_ref(), caller.as_slice());
2212        assert_ne!(loaded.data.as_ref(), bundled);
2213    }
2214
2215    #[test]
2216    fn case_only_caller_labels_resolve_to_the_supplied_face() {
2217        let bundled = bundled_font_data()
2218            .into_iter()
2219            .find(|(family, _)| *family == "Caladea")
2220            .expect("Caladea is bundled")
2221            .1;
2222        let mut caller = bundled.to_vec();
2223        caller.push(0);
2224        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2225        manager.load_additional_fonts(&[FontFile {
2226            family: "caladea".to_owned(),
2227            data: caller.clone(),
2228        }]);
2229
2230        let resolved = manager
2231            .resolve_font(Some("caladea"), false, false)
2232            .expect("case-only caller label resolves");
2233        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2234        assert_eq!(loaded.family, "Caladea");
2235        assert_eq!(loaded.data.as_ref(), caller.as_slice());
2236    }
2237
2238    #[test]
2239    fn constructor_label_alias_survives_additional_font_replacement() {
2240        let caladea = bundled_font_data()
2241            .into_iter()
2242            .find(|(family, _)| *family == "Caladea")
2243            .expect("Caladea is bundled")
2244            .1;
2245        let carlito = bundled_font_data()
2246            .into_iter()
2247            .find(|(family, _)| *family == "Carlito")
2248            .expect("Carlito is bundled")
2249            .1;
2250        let mut constructor = caladea.to_vec();
2251        constructor.push(0);
2252        let mut manager =
2253            FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2254
2255        manager.load_additional_fonts(&[FontFile {
2256            family: "Additional Sans".to_owned(),
2257            data: carlito.to_vec(),
2258        }]);
2259
2260        let resolved = manager
2261            .resolve_font(Some("Document Serif"), false, false)
2262            .expect("constructor label still resolves");
2263        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2264        assert_eq!(loaded.family, "Caladea");
2265        assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2266    }
2267
2268    #[test]
2269    fn constructor_family_priority_survives_additional_font_replacement() {
2270        let caladea = bundled_font_data()
2271            .into_iter()
2272            .find(|(family, _)| *family == "Caladea")
2273            .expect("Caladea is bundled")
2274            .1;
2275        let mut constructor = caladea.to_vec();
2276        constructor.push(0);
2277        let mut replacement = caladea.to_vec();
2278        replacement.extend_from_slice(&[0, 0]);
2279        let mut manager =
2280            FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2281
2282        manager.load_additional_fonts(&[FontFile {
2283            family: "Caladea".to_owned(),
2284            data: replacement,
2285        }]);
2286
2287        let resolved = manager
2288            .resolve_font(Some("Caladea"), false, false)
2289            .expect("constructor family still resolves");
2290        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2291        assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2292    }
2293
2294    #[test]
2295    fn caller_face_selection_matches_fontdb_css_rules() {
2296        let caladea = bundled_font_data()
2297            .into_iter()
2298            .find(|(family, _)| *family == "Caladea")
2299            .expect("Caladea is bundled")
2300            .1;
2301        let mut source = fontdb::Database::new();
2302        source.load_font_data(caladea.to_vec());
2303        let template = source.faces().next().expect("Caladea has a face").clone();
2304        let mut db = fontdb::Database::new();
2305        let mut add_face = |weight: u16, stretch: fontdb::Stretch, style: fontdb::Style| {
2306            let mut face = template.clone();
2307            face.weight = fontdb::Weight(weight);
2308            face.stretch = stretch;
2309            face.style = style;
2310            db.push_face_info(face)
2311        };
2312
2313        let weight_300 = add_face(300, fontdb::Stretch::Normal, fontdb::Style::Normal);
2314        let weight_500 = add_face(500, fontdb::Stretch::Normal, fontdb::Style::Normal);
2315        let weight_600 = add_face(600, fontdb::Stretch::Normal, fontdb::Style::Normal);
2316        let weight_800 = add_face(800, fontdb::Stretch::Normal, fontdb::Style::Normal);
2317        let expanded = add_face(400, fontdb::Stretch::SemiExpanded, fontdb::Style::Normal);
2318        let condensed = add_face(400, fontdb::Stretch::SemiCondensed, fontdb::Style::Normal);
2319        let normal = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Normal);
2320        let italic = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Italic);
2321
2322        assert_eq!(
2323            best_caller_face(
2324                &db,
2325                &[weight_300, weight_500],
2326                fontdb::Weight::NORMAL,
2327                fontdb::Style::Normal,
2328            ),
2329            Some(weight_500)
2330        );
2331        assert_eq!(
2332            best_caller_face(
2333                &db,
2334                &[weight_600, weight_800],
2335                fontdb::Weight::BOLD,
2336                fontdb::Style::Normal,
2337            ),
2338            Some(weight_800)
2339        );
2340        assert_eq!(
2341            best_caller_face(
2342                &db,
2343                &[expanded, condensed],
2344                fontdb::Weight::NORMAL,
2345                fontdb::Style::Normal,
2346            ),
2347            Some(condensed)
2348        );
2349        assert_eq!(
2350            best_caller_face(
2351                &db,
2352                &[normal, italic],
2353                fontdb::Weight::NORMAL,
2354                fontdb::Style::Oblique,
2355            ),
2356            Some(italic)
2357        );
2358    }
2359
2360    fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
2361        assert_eq!(family.len(), 7);
2362        let mut font = source.to_vec();
2363        let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
2364        let name_offset = (0..table_count)
2365            .find_map(|table| {
2366                let record = 12 + table * 16;
2367                (&font[record..record + 4] == b"name").then(|| {
2368                    u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
2369                })
2370            })
2371            .expect("font has name table");
2372        let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
2373        let strings = name_offset
2374            + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
2375        for index in 0..count {
2376            let record = name_offset + 6 + index * 12;
2377            let platform = u16::from_be_bytes([font[record], font[record + 1]]);
2378            let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
2379            let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
2380            let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
2381            if !matches!(name_id, 1 | 16) {
2382                continue;
2383            }
2384            let destination = &mut font[strings + offset..strings + offset + length];
2385            match (platform, length) {
2386                (0 | 3, 14) => {
2387                    for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
2388                        bytes.copy_from_slice(&(ch as u16).to_be_bytes());
2389                    }
2390                }
2391                (1, 7) => destination.copy_from_slice(family.as_bytes()),
2392                _ => {}
2393            }
2394        }
2395        font
2396    }
2397
2398    #[cfg(feature = "system-fonts")]
2399    fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
2400        let header_len = 12 + fonts.len() * 4;
2401        let mut collection = vec![0u8; header_len];
2402        collection[0..4].copy_from_slice(b"ttcf");
2403        collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
2404        collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
2405
2406        for (font_number, font) in fonts.iter().enumerate() {
2407            while !collection.len().is_multiple_of(4) {
2408                collection.push(0);
2409            }
2410            let collection_offset = collection.len();
2411            collection[12 + font_number * 4..16 + font_number * 4]
2412                .copy_from_slice(&(collection_offset as u32).to_be_bytes());
2413
2414            let mut adjusted = font.to_vec();
2415            let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
2416            for table in 0..table_count {
2417                let offset_position = 12 + table * 16 + 8;
2418                let offset = u32::from_be_bytes(
2419                    adjusted[offset_position..offset_position + 4]
2420                        .try_into()
2421                        .expect("table offset"),
2422                );
2423                adjusted[offset_position..offset_position + 4]
2424                    .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
2425            }
2426            collection.extend_from_slice(&adjusted);
2427        }
2428        collection
2429    }
2430
2431    #[test]
2432    fn deterministic_font_manager_uses_only_bundled_fonts() {
2433        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2434
2435        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2436        assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
2437    }
2438
2439    #[cfg(feature = "system-fonts")]
2440    #[test]
2441    fn normal_font_discovery_initializes_once_per_process() {
2442        let _first = FontManager::new();
2443        let _second = FontManager::new();
2444        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2445
2446        let _deterministic =
2447            FontManager::new_deterministic().expect("bundled font manager should load");
2448        let _caller = FontManager::new_with_fonts(Vec::new());
2449        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2450    }
2451
2452    #[cfg(feature = "system-fonts")]
2453    #[test]
2454    fn file_backed_collection_faces_share_one_byte_buffer() {
2455        let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
2456        let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
2457        let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
2458        let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
2459        std::fs::write(&first_path, &collection).expect("write first temporary collection");
2460        std::fs::write(&second_path, &collection).expect("write second temporary collection");
2461
2462        let mut db = fontdb::Database::new();
2463        db.load_font_file(&first_path).expect("load first TTC");
2464        db.load_font_file(&second_path).expect("load second TTC");
2465        let canonical_first = std::fs::canonicalize(&first_path).unwrap();
2466        let canonical_second = std::fs::canonicalize(&second_path).unwrap();
2467        let first_ids = db
2468            .faces()
2469            .filter_map(|face| match &face.source {
2470                fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
2471                    Some(face.id)
2472                }
2473                _ => None,
2474            })
2475            .collect::<Vec<_>>();
2476        let second_id = db
2477            .faces()
2478            .find_map(|face| match &face.source {
2479                fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
2480                    Some(face.id)
2481                }
2482                _ => None,
2483            })
2484            .expect("second TTC face");
2485        assert_eq!(first_ids.len(), 2);
2486
2487        let mut memory = HashMap::new();
2488        let (first_face, first_index) =
2489            font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
2490        let (second_face, second_index) =
2491            font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
2492        let (other_file, _) =
2493            font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
2494        assert_ne!(first_index, second_index);
2495        assert!(Arc::ptr_eq(&first_face, &second_face));
2496        assert!(!Arc::ptr_eq(&first_face, &other_file));
2497
2498        std::fs::remove_file(first_path).expect("remove first temporary font");
2499        std::fs::remove_file(second_path).expect("remove second temporary font");
2500    }
2501
2502    #[test]
2503    fn shaping_memo_uses_complete_text_size_and_font_identity() {
2504        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2505        let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2506        let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
2507
2508        let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
2509        let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
2510        assert_eq!(first.glyph_ids, repeat.glyph_ids);
2511        assert_eq!(fm.shaping_memo_counts().0, 1);
2512
2513        fm.shape_text(regular, "different text", 11.0).unwrap();
2514        fm.shape_text(regular, "exact text", 12.0).unwrap();
2515        fm.shape_text(bold, "exact text", 11.0).unwrap();
2516        assert_eq!(fm.shaping_memo_counts().1, 4);
2517
2518        let replacement = FontFile {
2519            family: "Carlito".to_owned(),
2520            data: bundled_font_data()[1].1.to_vec(),
2521        };
2522        fm.load_additional_fonts(&[replacement]);
2523        assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
2524        let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2525        fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
2526        assert_eq!(fm.shaping_memo_counts().1, 1);
2527    }
2528
2529    #[test]
2530    fn arabic_joining_survives_script_and_line_break_boundaries() {
2531        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2532        let segment = multilingual_test_segment(&mut fm, "العربية", 18.0, None);
2533        let shaped = fm
2534            .shape_multilingual_text(segment, Some("ar"), TextDirection::RightToLeft, false)
2535            .unwrap();
2536        assert_eq!(shaped.len(), 1);
2537        assert_eq!(
2538            shaped[0].glyph_ids(),
2539            &[288, 85, 319, 18, 317, 19, 31, 48, 72, 8]
2540        );
2541        assert_eq!(
2542            shaped[0]
2543                .clusters()
2544                .iter()
2545                .map(|cluster| (cluster.glyph_start..cluster.glyph_end, cluster.char_range()))
2546                .collect::<Vec<_>>(),
2547            vec![
2548                (0..2, 6..7),
2549                (2..4, 5..6),
2550                (4..6, 4..5),
2551                (6..7, 3..4),
2552                (7..8, 2..3),
2553                (8..9, 1..2),
2554                (9..10, 0..1),
2555            ]
2556        );
2557    }
2558
2559    #[test]
2560    fn indic_clusters_are_never_split_or_mapped_as_independent_scalars() {
2561        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2562        let segment = multilingual_test_segment(&mut fm, "कि", 18.0, None);
2563        let shaped = fm
2564            .shape_multilingual_text(segment, Some("hi"), TextDirection::LeftToRight, false)
2565            .unwrap();
2566
2567        assert_eq!(shaped[0].clusters()[0].char_range(), 0..2);
2568        assert_eq!(shaped[0].x_offsets().len(), shaped[0].glyph_ids().len());
2569        assert_eq!(shaped[0].y_offsets().len(), shaped[0].glyph_ids().len());
2570    }
2571
2572    #[test]
2573    fn same_script_coverage_changes_split_only_between_graphemes() {
2574        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2575        let font_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2576        let metrics = fm.metrics(font_id, 18.0).unwrap();
2577        let seed = fm.shape_text(font_id, "A☀∙", 18.0).unwrap();
2578        let shaped = fm
2579            .shape_multilingual_text(
2580                TextSegment {
2581                    text: "A☀∙".to_owned(),
2582                    direction: TextDirection::Auto,
2583                    source: None,
2584                    font_id,
2585                    font_size: 18.0,
2586                    glyph_ids: seed.glyph_ids,
2587                    advances: seed.advances,
2588                    width: seed.width,
2589                    ascent: metrics.ascent,
2590                    descent: metrics.descent,
2591                    line_gap: metrics.line_gap,
2592                    color: Color::BLACK,
2593                    bold: false,
2594                    italic: false,
2595                    underline: None,
2596                    strike: false,
2597                    dstrike: false,
2598                    highlight: None,
2599                    baseline_offset: 0.0,
2600                    hyperlink_url: None,
2601                    field_kind: None,
2602                    note: None,
2603                },
2604                None,
2605                TextDirection::LeftToRight,
2606                false,
2607            )
2608            .unwrap();
2609
2610        assert_eq!(
2611            shaped.iter().map(|span| span.text()).collect::<Vec<_>>(),
2612            ["A", "☀", "∙"]
2613        );
2614        assert!(shaped.iter().all(|span| span.script() == TextScript::Latin));
2615        assert_ne!(shaped[1].font_id(), shaped[2].font_id());
2616        assert!(shaped.iter().all(|span| {
2617            let index = fm.index_of(span.font_id()).unwrap();
2618            fm.uncovered(index, span.text()).is_empty()
2619        }));
2620    }
2621
2622    #[test]
2623    fn multilingual_constructor_rejects_invalid_levels_and_cluster_maps() {
2624        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2625        let segment = multilingual_test_segment(&mut fm, "ab", 18.0, None);
2626        let valid = fm
2627            .shape_multilingual_text(segment, None, TextDirection::LeftToRight, true)
2628            .unwrap()
2629            .remove(0);
2630        let rebuild = |bidi_level, clusters| {
2631            MultilingualTextSegment::new(
2632                valid.base().clone(),
2633                valid.logical_index(),
2634                valid.language().map(str::to_owned),
2635                valid.script(),
2636                valid.direction(),
2637                bidi_level,
2638                valid.x_advances().to_vec(),
2639                valid.y_advances().to_vec(),
2640                valid.x_offsets().to_vec(),
2641                valid.y_offsets().to_vec(),
2642                clusters,
2643                valid.break_after(),
2644            )
2645        };
2646
2647        assert!(rebuild(255, valid.clusters().to_vec()).is_err());
2648        let mut out_of_bounds = valid.clusters().to_vec();
2649        out_of_bounds.last_mut().unwrap().char_end = 3;
2650        assert!(rebuild(valid.bidi_level(), out_of_bounds).is_err());
2651        let mut glyph_gap = valid.clusters().to_vec();
2652        glyph_gap[0].glyph_start = 1;
2653        assert!(rebuild(valid.bidi_level(), glyph_gap).is_err());
2654    }
2655
2656    #[test]
2657    fn thai_words_offer_approved_breaks_without_losing_source_text() {
2658        let text = "ภาษาไทยยินดีต้อนรับ";
2659        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2660        let source = SourceSpan {
2661            node: crate::SourceNodeId::new(3).unwrap(),
2662            char_start: 10,
2663            char_end: 29,
2664        };
2665        let segment = multilingual_test_segment(&mut fm, text, 18.0, Some(source));
2666        let shaped = fm
2667            .shape_multilingual_text(segment, Some("th"), TextDirection::LeftToRight, false)
2668            .unwrap();
2669        assert_eq!(
2670            shaped
2671                .iter()
2672                .map(|span| {
2673                    let source = span.base().source.unwrap();
2674                    (
2675                        span.text(),
2676                        span.break_after(),
2677                        source.char_start..source.char_end,
2678                    )
2679                })
2680                .collect::<Vec<_>>(),
2681            vec![
2682                ("ภาษา", true, 10..14),
2683                ("ไทยยิน", true, 14..20),
2684                ("ดี", true, 20..22),
2685                ("ต้อน", true, 22..26),
2686                ("รับ", true, 26..29),
2687            ]
2688        );
2689        assert_eq!(
2690            shaped.iter().map(|span| span.text()).collect::<String>(),
2691            text
2692        );
2693    }
2694
2695    #[test]
2696    fn shaping_memo_hits_preserve_fifo_order() {
2697        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2698        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2699        fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2700        fm.shape_text(font, "newest exact shape", 11.0).unwrap();
2701        fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2702
2703        let memo = fm.shaping_memo.lock().unwrap();
2704        assert_eq!(memo.hits, 1);
2705        assert_eq!(memo.entries.front().unwrap().0.text, "oldest exact shape");
2706        assert_eq!(memo.entries.back().unwrap().0.text, "newest exact shape");
2707    }
2708
2709    #[test]
2710    fn shaping_memo_fingerprint_collision_requires_exact_key_equality() {
2711        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2712        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2713        fm.shape_text(font, "first collision candidate", 11.0)
2714            .unwrap();
2715        let forced = shaping_fingerprint(font, "second collision candidate", 11.0f64.to_bits());
2716        fm.shaping_memo.lock().unwrap().entries[0].3 = forced;
2717
2718        fm.shape_text(font, "second collision candidate", 11.0)
2719            .unwrap();
2720
2721        let (hits, misses, entries, _) = fm.shaping_memo_counts();
2722        assert_eq!((hits, misses, entries), (0, 2, 2));
2723    }
2724
2725    #[test]
2726    fn shaping_memo_is_bounded_and_recovers_from_poison() {
2727        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2728        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2729        for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
2730            fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
2731                .unwrap();
2732        }
2733        let (_, _, entries, bytes) = fm.shaping_memo_counts();
2734        assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
2735        assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
2736
2737        let fm = Arc::new(fm);
2738        let poison = Arc::clone(&fm);
2739        assert!(
2740            std::thread::spawn(move || {
2741                let _guard = poison.shaping_memo.lock().unwrap();
2742                panic!("poison shaping cache for recovery coverage");
2743            })
2744            .join()
2745            .is_err()
2746        );
2747        let first = fm.shape_text(font, "after poison", 11.0).unwrap();
2748        let second = fm.shape_text(font, "after poison", 11.0).unwrap();
2749        assert_eq!(first.glyph_ids, second.glyph_ids);
2750        let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
2751        assert_eq!((hits, misses, entries), (1, 1, 1));
2752        assert!(bytes > 0);
2753    }
2754
2755    #[test]
2756    fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
2757        let mut memo = ShapingMemo::new();
2758        for suffix in ['a', 'b'] {
2759            memo.insert(
2760                ShapingKey {
2761                    font_id: FontId(0),
2762                    text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
2763                    size_bits: 11.0f64.to_bits(),
2764                },
2765                ShapedText {
2766                    glyph_ids: Vec::new(),
2767                    advances: Vec::new(),
2768                    width: 0.0,
2769                },
2770            );
2771        }
2772        assert_eq!(memo.entries.len(), 1);
2773        assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
2774    }
2775
2776    #[test]
2777    fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
2778        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
2779        for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
2780            fm.remember_coverage_fallback(false, false, 0);
2781        }
2782        assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
2783
2784        let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
2785            .filter_map(|value| char::from_u32(0x10_000 + value as u32))
2786            .collect::<Vec<_>>();
2787        fm.remember_coverage_misses(&misses);
2788        assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
2789
2790        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2791            fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
2792                .expect("bounded fallback resolves");
2793        }
2794        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2795        assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
2796    }
2797
2798    #[test]
2799    fn active_document_may_resolve_more_than_256_distinct_faces() {
2800        let source = bundled_font_data()[4].1;
2801        let mut db = fontdb::Database::new();
2802        for index in 0..257 {
2803            db.load_font_data(font_with_family(source, &format!("F{index:06}")));
2804        }
2805        let mut fm = FontManager::from_base_database(db);
2806        fm.begin_layout();
2807        let mut ids = HashSet::new();
2808        for index in 0..257 {
2809            let family = format!("F{index:06}");
2810            let id = fm
2811                .resolve_font(Some(&family), false, false)
2812                .expect("distinct active face resolves");
2813            assert_eq!(fm.font_data(id).unwrap().family, family);
2814            ids.insert(id);
2815        }
2816        assert_eq!(ids.len(), 257);
2817        fm.retain_current_fonts();
2818        assert_eq!(fm.fonts.len(), 257);
2819    }
2820
2821    #[test]
2822    fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
2823        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
2824        fm.begin_layout();
2825        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
2826            fm.resolve_font(Some("Carlito"), false, false).unwrap();
2827        }
2828        assert!(fm.paragraph_font_trace.is_none());
2829
2830        fm.begin_paragraph_font_trace();
2831        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
2832            fm.resolve_font(Some("Carlito"), false, false).unwrap();
2833        }
2834        assert!(fm.finish_paragraph_font_trace().is_none());
2835
2836        fm.begin_layout();
2837        assert_eq!(fm.layout_fonts.capacity(), 0);
2838        fm.begin_paragraph_font_trace();
2839        fm.resolve_font(Some("Carlito"), false, false).unwrap();
2840        let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
2841        assert_eq!(trace.len(), 1);
2842        assert_eq!(trace.capacity(), trace.len());
2843    }
2844
2845    #[cfg(feature = "system-fonts")]
2846    #[test]
2847    fn file_byte_cache_is_bounded_and_recovers_from_poison() {
2848        let cache = Arc::new(Mutex::new(FileFontCache::new()));
2849        {
2850            let mut cache = cache
2851                .lock()
2852                .unwrap_or_else(std::sync::PoisonError::into_inner);
2853            cache.clear();
2854            let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
2855            let returned = cache_file_font_bytes(
2856                &mut cache,
2857                PathBuf::from("oversized-font.ttc"),
2858                Arc::clone(&oversized),
2859            )
2860            .expect("oversized bytes are returned uncached");
2861            assert!(Arc::ptr_eq(&oversized, &returned));
2862            assert!(cache.entries.is_empty());
2863            assert_eq!(cache.bytes, 0);
2864        }
2865
2866        let poison = Arc::clone(&cache);
2867        assert!(
2868            std::thread::spawn(move || {
2869                let cache = poison;
2870                let _guard = cache.lock().unwrap();
2871                panic!("poison file byte cache for recovery coverage");
2872            })
2873            .join()
2874            .is_err()
2875        );
2876
2877        let path = std::env::temp_dir().join(format!(
2878            "rdocx-font-cache-poison-{}-{:?}.ttf",
2879            std::process::id(),
2880            std::thread::current().id()
2881        ));
2882        std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
2883        let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
2884        let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
2885        assert!(Arc::ptr_eq(&first, &second));
2886        std::fs::remove_file(path).expect("remove recovery font");
2887    }
2888
2889    #[cfg(not(feature = "system-fonts"))]
2890    #[test]
2891    fn no_default_features_omits_system_font_discovery() {
2892        let fm = FontManager::new();
2893        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2894    }
2895
2896    #[test]
2897    fn font_manager_with_no_fonts_returns_an_error() {
2898        let mut fm = FontManager::new_with_fonts(Vec::new());
2899        assert!(matches!(
2900            fm.resolve_font(None, false, false),
2901            Err(LayoutError::FontNotFound(_))
2902        ));
2903    }
2904
2905    #[test]
2906    fn load_system_font() {
2907        let mut fm = FontManager::new();
2908        // Should be able to resolve at least one font via fallback
2909        let result = fm.resolve_font(None, false, false);
2910        // On CI or systems without fonts this might fail, so we just check it doesn't panic
2911        if let Ok(id) = result {
2912            assert_eq!(id.0, 0);
2913        }
2914    }
2915
2916    #[test]
2917    fn font_metrics_positive() {
2918        let mut fm = FontManager::new();
2919        if let Ok(id) = fm.resolve_font(None, false, false) {
2920            let metrics = fm.metrics(id, 12.0).unwrap();
2921            assert!(metrics.ascent > 0.0);
2922            assert!(metrics.descent > 0.0);
2923            assert!(metrics.units_per_em > 0);
2924        }
2925    }
2926
2927    #[test]
2928    fn shape_hello_world() {
2929        let mut fm = FontManager::new();
2930        if let Ok(id) = fm.resolve_font(None, false, false) {
2931            let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
2932            assert!(!shaped.glyph_ids.is_empty());
2933            assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
2934            assert!(shaped.width > 0.0);
2935        }
2936    }
2937
2938    #[test]
2939    fn font_caching() {
2940        let mut fm = FontManager::new();
2941        if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
2942            let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
2943            assert_eq!(id1, id2);
2944        }
2945    }
2946
2947    #[test]
2948    fn font_resolution_alias_cache_is_bounded() {
2949        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2950        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2951            fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
2952                .expect("fallback font resolves");
2953        }
2954        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2955        assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2956    }
2957
2958    #[test]
2959    fn bold_italic_variants() {
2960        let mut fm = FontManager::new();
2961        let regular = fm.resolve_font(None, false, false);
2962        let bold = fm.resolve_font(None, true, false);
2963        if let (Ok(r), Ok(b)) = (regular, bold) {
2964            // Bold should get a different font ID (different variant)
2965            assert_ne!(r, b);
2966        }
2967    }
2968
2969    /// Latin text must resolve exactly as it did before, so the coverage check
2970    /// cannot disturb the overwhelmingly common case.
2971    #[test]
2972    fn latin_text_resolves_the_same_as_by_name() {
2973        let mut fm = FontManager::new();
2974        let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
2975            return;
2976        };
2977        let for_text = fm
2978            .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
2979            .unwrap();
2980        assert_eq!(by_name, for_text);
2981    }
2982
2983    /// Text nothing can draw must keep the requested font rather than failing.
2984    ///
2985    /// The approved deterministic fallbacks intentionally do not cover emoji,
2986    /// so the search is guaranteed to come up empty. The text still needs a
2987    /// font so it occupies the right space.
2988    #[test]
2989    fn text_no_font_can_draw_keeps_the_requested_font() {
2990        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2991        let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2992        let resolved = fm
2993            .resolve_font_for_text(Some("Carlito"), false, false, "🀄")
2994            .unwrap();
2995        assert_eq!(
2996            primary, resolved,
2997            "with no covering font available the original must be kept"
2998        );
2999    }
3000
3001    /// Whitespace absent from a font is not a reason to go hunting for another.
3002    #[test]
3003    fn whitespace_does_not_trigger_a_fallback() {
3004        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
3005        let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
3006        let idx = fm.index_of(by_name).unwrap();
3007        // A non-breaking space and a tab, neither of which every face carries.
3008        assert!(
3009            fm.uncovered(idx, "a\u{00a0}b\tc")
3010                .iter()
3011                .all(|c| *c != '\t'),
3012            "control and whitespace characters must be ignored"
3013        );
3014    }
3015
3016    /// When the machine does have a CJK font, CJK text must not keep a Latin
3017    /// font that cannot draw it.
3018    ///
3019    /// Skipped where no such font is installed, which is why it asserts
3020    /// nothing about which font is chosen.
3021    #[test]
3022    fn cjk_text_moves_off_a_latin_font_when_possible() {
3023        let mut fm = FontManager::new();
3024        let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
3025            return;
3026        };
3027        let Some(idx) = fm.index_of(latin) else {
3028            return;
3029        };
3030        if fm.uncovered(idx, "这是中文").is_empty() {
3031            return; // that font somehow covers it, nothing to prove
3032        }
3033        let resolved = fm
3034            .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
3035            .unwrap();
3036        if resolved == latin {
3037            return; // no covering font installed on this machine
3038        }
3039        let new_idx = fm.index_of(resolved).unwrap();
3040        assert!(
3041            fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
3042            "the replacement must cover more of the text than the original"
3043        );
3044    }
3045}