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            field_source: None,
2037            note: None,
2038        }
2039    }
2040
2041    #[test]
2042    fn caller_font_labels_resolve_after_exact_embedded_families() {
2043        let caladea = bundled_font_data()
2044            .into_iter()
2045            .find(|(family, _)| *family == "Caladea")
2046            .expect("Caladea is bundled")
2047            .1;
2048        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2049        manager.load_additional_fonts(&[
2050            FontFile {
2051                family: "Document Serif".to_owned(),
2052                data: caladea.to_vec(),
2053            },
2054            FontFile {
2055                family: "Carlito".to_owned(),
2056                data: caladea.to_vec(),
2057            },
2058        ]);
2059
2060        let aliased = manager
2061            .resolve_font(Some("Document Serif"), false, false)
2062            .expect("caller label resolves");
2063        assert_eq!(
2064            manager.fonts[manager.index_of(aliased).unwrap()].family,
2065            "Caladea"
2066        );
2067
2068        let exact = manager
2069            .resolve_font(Some("Carlito"), false, false)
2070            .expect("embedded family resolves exactly");
2071        assert_eq!(
2072            manager.fonts[manager.index_of(exact).unwrap()].family,
2073            "Carlito"
2074        );
2075
2076        manager.set_caller_aliases(&[("Document Serif".to_owned(), "Carlito".to_owned())]);
2077        let explicit = manager
2078            .resolve_font(Some("Document Serif"), false, false)
2079            .expect("explicit alias precedes label-derived alias");
2080        assert_eq!(
2081            manager.fonts[manager.index_of(explicit).unwrap()].family,
2082            "Carlito"
2083        );
2084
2085        manager.load_additional_fonts(&[FontFile {
2086            family: "Caladea".to_owned(),
2087            data: caladea.to_vec(),
2088        }]);
2089        assert!(manager.caller_aliases.is_empty());
2090
2091        manager.set_caller_aliases(&[("Arial".to_owned(), "Caladea".to_owned())]);
2092        let caller_alias = manager
2093            .resolve_font(Some("Arial"), false, false)
2094            .expect("explicit alias resolves before mapped fallback");
2095        assert_eq!(
2096            manager.fonts[manager.index_of(caller_alias).unwrap()].family,
2097            "Caladea"
2098        );
2099        let generic = manager
2100            .resolve_font(Some("Unmapped Document Family"), false, false)
2101            .expect("generic fallback remains available");
2102        assert_eq!(
2103            manager.fonts[manager.index_of(generic).unwrap()].family,
2104            "Carlito"
2105        );
2106    }
2107
2108    #[test]
2109    fn caller_alias_updates_preserve_bytes_and_invalidate_resolution_state() {
2110        let caladea = bundled_font_data()
2111            .into_iter()
2112            .find(|(family, _)| *family == "Caladea")
2113            .expect("Caladea is bundled")
2114            .1;
2115        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2116        manager.load_additional_fonts(&[FontFile {
2117            family: "Caladea".to_owned(),
2118            data: caladea.to_vec(),
2119        }]);
2120        let aliases = vec![
2121            ("Document Serif A".to_owned(), "Caladea".to_owned()),
2122            ("Document Serif B".to_owned(), "Caladea".to_owned()),
2123        ];
2124        assert!(manager.set_caller_aliases(&aliases));
2125
2126        let first = manager
2127            .resolve_font(Some("Document Serif A"), false, false)
2128            .expect("first alias resolves");
2129        let second = manager
2130            .resolve_font(Some("Document Serif B"), false, false)
2131            .expect("second alias resolves");
2132        let first_data = &manager.fonts[manager.index_of(first).unwrap()].data;
2133        let second_data = &manager.fonts[manager.index_of(second).unwrap()].data;
2134        assert!(Arc::ptr_eq(first_data, second_data));
2135        assert!(!manager.set_caller_aliases(&aliases));
2136
2137        assert!(
2138            manager.set_caller_aliases(&[("Document Serif A".to_owned(), "Carlito".to_owned(),)])
2139        );
2140        let changed = manager
2141            .resolve_font(Some("Document Serif A"), false, false)
2142            .expect("changed alias resolves");
2143        assert_eq!(
2144            manager.fonts[manager.index_of(changed).unwrap()].family,
2145            "Carlito"
2146        );
2147        assert!(
2148            manager.index_of(first).is_some(),
2149            "loaded faces are retained"
2150        );
2151    }
2152
2153    #[test]
2154    fn explicit_alias_state_respects_entry_and_retained_byte_ceilings() {
2155        let aliases = (0..CALLER_ALIAS_MAX_ENTRIES + 32)
2156            .map(|index| (format!("Document Serif {index}"), "Caladea".to_owned()))
2157            .collect::<Vec<_>>();
2158        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2159        manager.set_caller_aliases(&aliases);
2160        assert_eq!(
2161            manager.explicit_aliases.as_slice(),
2162            &aliases[..CALLER_ALIAS_MAX_ENTRIES]
2163        );
2164        assert!(manager.explicit_aliases.len() <= CALLER_ALIAS_MAX_ENTRIES);
2165        assert!(manager.explicit_alias_map.len() <= CALLER_ALIAS_MAX_ENTRIES);
2166
2167        let retained_large = ("x".repeat(32_760), String::new());
2168        let byte_limited = vec![
2169            retained_large.clone(),
2170            ("discarded bytes".to_owned(), "Caladea".to_owned()),
2171        ];
2172        manager.set_caller_aliases(&byte_limited);
2173        assert_eq!(manager.explicit_aliases, vec![retained_large]);
2174
2175        let oversized = vec![("x".repeat(40_000), "Caladea".to_owned())];
2176        manager.set_caller_aliases(&oversized);
2177        let retained_bytes = manager
2178            .explicit_aliases
2179            .iter()
2180            .map(|(requested, target)| requested.len() + target.len())
2181            .sum::<usize>()
2182            + manager
2183                .explicit_alias_map
2184                .iter()
2185                .map(|(requested, target)| requested.len() + target.len())
2186                .sum::<usize>();
2187        assert!(retained_bytes <= CALLER_ALIAS_MAX_RETAINED_BYTES);
2188        assert!(manager.explicit_aliases.is_empty());
2189        assert!(manager.explicit_alias_map.is_empty());
2190    }
2191
2192    #[test]
2193    fn label_alias_prefers_caller_bytes_over_bundled_same_family() {
2194        let bundled = bundled_font_data()
2195            .into_iter()
2196            .find(|(family, _)| *family == "Caladea")
2197            .expect("Caladea is bundled")
2198            .1;
2199        let mut caller = bundled.to_vec();
2200        caller.push(0);
2201        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2202        manager.load_additional_fonts(&[FontFile {
2203            family: "Document Serif".to_owned(),
2204            data: caller.clone(),
2205        }]);
2206
2207        let resolved = manager
2208            .resolve_font(Some("Document Serif"), false, false)
2209            .expect("caller label resolves");
2210        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2211        assert_eq!(loaded.family, "Caladea");
2212        assert_eq!(loaded.data.as_ref(), caller.as_slice());
2213        assert_ne!(loaded.data.as_ref(), bundled);
2214    }
2215
2216    #[test]
2217    fn case_only_caller_labels_resolve_to_the_supplied_face() {
2218        let bundled = bundled_font_data()
2219            .into_iter()
2220            .find(|(family, _)| *family == "Caladea")
2221            .expect("Caladea is bundled")
2222            .1;
2223        let mut caller = bundled.to_vec();
2224        caller.push(0);
2225        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
2226        manager.load_additional_fonts(&[FontFile {
2227            family: "caladea".to_owned(),
2228            data: caller.clone(),
2229        }]);
2230
2231        let resolved = manager
2232            .resolve_font(Some("caladea"), false, false)
2233            .expect("case-only caller label resolves");
2234        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2235        assert_eq!(loaded.family, "Caladea");
2236        assert_eq!(loaded.data.as_ref(), caller.as_slice());
2237    }
2238
2239    #[test]
2240    fn constructor_label_alias_survives_additional_font_replacement() {
2241        let caladea = bundled_font_data()
2242            .into_iter()
2243            .find(|(family, _)| *family == "Caladea")
2244            .expect("Caladea is bundled")
2245            .1;
2246        let carlito = bundled_font_data()
2247            .into_iter()
2248            .find(|(family, _)| *family == "Carlito")
2249            .expect("Carlito is bundled")
2250            .1;
2251        let mut constructor = caladea.to_vec();
2252        constructor.push(0);
2253        let mut manager =
2254            FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2255
2256        manager.load_additional_fonts(&[FontFile {
2257            family: "Additional Sans".to_owned(),
2258            data: carlito.to_vec(),
2259        }]);
2260
2261        let resolved = manager
2262            .resolve_font(Some("Document Serif"), false, false)
2263            .expect("constructor label still resolves");
2264        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2265        assert_eq!(loaded.family, "Caladea");
2266        assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2267    }
2268
2269    #[test]
2270    fn constructor_family_priority_survives_additional_font_replacement() {
2271        let caladea = bundled_font_data()
2272            .into_iter()
2273            .find(|(family, _)| *family == "Caladea")
2274            .expect("Caladea is bundled")
2275            .1;
2276        let mut constructor = caladea.to_vec();
2277        constructor.push(0);
2278        let mut replacement = caladea.to_vec();
2279        replacement.extend_from_slice(&[0, 0]);
2280        let mut manager =
2281            FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
2282
2283        manager.load_additional_fonts(&[FontFile {
2284            family: "Caladea".to_owned(),
2285            data: replacement,
2286        }]);
2287
2288        let resolved = manager
2289            .resolve_font(Some("Caladea"), false, false)
2290            .expect("constructor family still resolves");
2291        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
2292        assert_eq!(loaded.data.as_ref(), constructor.as_slice());
2293    }
2294
2295    #[test]
2296    fn caller_face_selection_matches_fontdb_css_rules() {
2297        let caladea = bundled_font_data()
2298            .into_iter()
2299            .find(|(family, _)| *family == "Caladea")
2300            .expect("Caladea is bundled")
2301            .1;
2302        let mut source = fontdb::Database::new();
2303        source.load_font_data(caladea.to_vec());
2304        let template = source.faces().next().expect("Caladea has a face").clone();
2305        let mut db = fontdb::Database::new();
2306        let mut add_face = |weight: u16, stretch: fontdb::Stretch, style: fontdb::Style| {
2307            let mut face = template.clone();
2308            face.weight = fontdb::Weight(weight);
2309            face.stretch = stretch;
2310            face.style = style;
2311            db.push_face_info(face)
2312        };
2313
2314        let weight_300 = add_face(300, fontdb::Stretch::Normal, fontdb::Style::Normal);
2315        let weight_500 = add_face(500, fontdb::Stretch::Normal, fontdb::Style::Normal);
2316        let weight_600 = add_face(600, fontdb::Stretch::Normal, fontdb::Style::Normal);
2317        let weight_800 = add_face(800, fontdb::Stretch::Normal, fontdb::Style::Normal);
2318        let expanded = add_face(400, fontdb::Stretch::SemiExpanded, fontdb::Style::Normal);
2319        let condensed = add_face(400, fontdb::Stretch::SemiCondensed, fontdb::Style::Normal);
2320        let normal = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Normal);
2321        let italic = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Italic);
2322
2323        assert_eq!(
2324            best_caller_face(
2325                &db,
2326                &[weight_300, weight_500],
2327                fontdb::Weight::NORMAL,
2328                fontdb::Style::Normal,
2329            ),
2330            Some(weight_500)
2331        );
2332        assert_eq!(
2333            best_caller_face(
2334                &db,
2335                &[weight_600, weight_800],
2336                fontdb::Weight::BOLD,
2337                fontdb::Style::Normal,
2338            ),
2339            Some(weight_800)
2340        );
2341        assert_eq!(
2342            best_caller_face(
2343                &db,
2344                &[expanded, condensed],
2345                fontdb::Weight::NORMAL,
2346                fontdb::Style::Normal,
2347            ),
2348            Some(condensed)
2349        );
2350        assert_eq!(
2351            best_caller_face(
2352                &db,
2353                &[normal, italic],
2354                fontdb::Weight::NORMAL,
2355                fontdb::Style::Oblique,
2356            ),
2357            Some(italic)
2358        );
2359    }
2360
2361    fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
2362        assert_eq!(family.len(), 7);
2363        let mut font = source.to_vec();
2364        let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
2365        let name_offset = (0..table_count)
2366            .find_map(|table| {
2367                let record = 12 + table * 16;
2368                (&font[record..record + 4] == b"name").then(|| {
2369                    u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
2370                })
2371            })
2372            .expect("font has name table");
2373        let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
2374        let strings = name_offset
2375            + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
2376        for index in 0..count {
2377            let record = name_offset + 6 + index * 12;
2378            let platform = u16::from_be_bytes([font[record], font[record + 1]]);
2379            let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
2380            let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
2381            let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
2382            if !matches!(name_id, 1 | 16) {
2383                continue;
2384            }
2385            let destination = &mut font[strings + offset..strings + offset + length];
2386            match (platform, length) {
2387                (0 | 3, 14) => {
2388                    for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
2389                        bytes.copy_from_slice(&(ch as u16).to_be_bytes());
2390                    }
2391                }
2392                (1, 7) => destination.copy_from_slice(family.as_bytes()),
2393                _ => {}
2394            }
2395        }
2396        font
2397    }
2398
2399    #[cfg(feature = "system-fonts")]
2400    fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
2401        let header_len = 12 + fonts.len() * 4;
2402        let mut collection = vec![0u8; header_len];
2403        collection[0..4].copy_from_slice(b"ttcf");
2404        collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
2405        collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
2406
2407        for (font_number, font) in fonts.iter().enumerate() {
2408            while !collection.len().is_multiple_of(4) {
2409                collection.push(0);
2410            }
2411            let collection_offset = collection.len();
2412            collection[12 + font_number * 4..16 + font_number * 4]
2413                .copy_from_slice(&(collection_offset as u32).to_be_bytes());
2414
2415            let mut adjusted = font.to_vec();
2416            let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
2417            for table in 0..table_count {
2418                let offset_position = 12 + table * 16 + 8;
2419                let offset = u32::from_be_bytes(
2420                    adjusted[offset_position..offset_position + 4]
2421                        .try_into()
2422                        .expect("table offset"),
2423                );
2424                adjusted[offset_position..offset_position + 4]
2425                    .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
2426            }
2427            collection.extend_from_slice(&adjusted);
2428        }
2429        collection
2430    }
2431
2432    #[test]
2433    fn deterministic_font_manager_uses_only_bundled_fonts() {
2434        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2435
2436        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2437        assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
2438    }
2439
2440    #[cfg(feature = "system-fonts")]
2441    #[test]
2442    fn normal_font_discovery_initializes_once_per_process() {
2443        let _first = FontManager::new();
2444        let _second = FontManager::new();
2445        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2446
2447        let _deterministic =
2448            FontManager::new_deterministic().expect("bundled font manager should load");
2449        let _caller = FontManager::new_with_fonts(Vec::new());
2450        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
2451    }
2452
2453    #[cfg(feature = "system-fonts")]
2454    #[test]
2455    fn file_backed_collection_faces_share_one_byte_buffer() {
2456        let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
2457        let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
2458        let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
2459        let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
2460        std::fs::write(&first_path, &collection).expect("write first temporary collection");
2461        std::fs::write(&second_path, &collection).expect("write second temporary collection");
2462
2463        let mut db = fontdb::Database::new();
2464        db.load_font_file(&first_path).expect("load first TTC");
2465        db.load_font_file(&second_path).expect("load second TTC");
2466        let canonical_first = std::fs::canonicalize(&first_path).unwrap();
2467        let canonical_second = std::fs::canonicalize(&second_path).unwrap();
2468        let first_ids = db
2469            .faces()
2470            .filter_map(|face| match &face.source {
2471                fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
2472                    Some(face.id)
2473                }
2474                _ => None,
2475            })
2476            .collect::<Vec<_>>();
2477        let second_id = db
2478            .faces()
2479            .find_map(|face| match &face.source {
2480                fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
2481                    Some(face.id)
2482                }
2483                _ => None,
2484            })
2485            .expect("second TTC face");
2486        assert_eq!(first_ids.len(), 2);
2487
2488        let mut memory = HashMap::new();
2489        let (first_face, first_index) =
2490            font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
2491        let (second_face, second_index) =
2492            font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
2493        let (other_file, _) =
2494            font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
2495        assert_ne!(first_index, second_index);
2496        assert!(Arc::ptr_eq(&first_face, &second_face));
2497        assert!(!Arc::ptr_eq(&first_face, &other_file));
2498
2499        std::fs::remove_file(first_path).expect("remove first temporary font");
2500        std::fs::remove_file(second_path).expect("remove second temporary font");
2501    }
2502
2503    #[test]
2504    fn shaping_memo_uses_complete_text_size_and_font_identity() {
2505        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2506        let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2507        let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
2508
2509        let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
2510        let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
2511        assert_eq!(first.glyph_ids, repeat.glyph_ids);
2512        assert_eq!(fm.shaping_memo_counts().0, 1);
2513
2514        fm.shape_text(regular, "different text", 11.0).unwrap();
2515        fm.shape_text(regular, "exact text", 12.0).unwrap();
2516        fm.shape_text(bold, "exact text", 11.0).unwrap();
2517        assert_eq!(fm.shaping_memo_counts().1, 4);
2518
2519        let replacement = FontFile {
2520            family: "Carlito".to_owned(),
2521            data: bundled_font_data()[1].1.to_vec(),
2522        };
2523        fm.load_additional_fonts(&[replacement]);
2524        assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
2525        let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2526        fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
2527        assert_eq!(fm.shaping_memo_counts().1, 1);
2528    }
2529
2530    #[test]
2531    fn arabic_joining_survives_script_and_line_break_boundaries() {
2532        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2533        let segment = multilingual_test_segment(&mut fm, "العربية", 18.0, None);
2534        let shaped = fm
2535            .shape_multilingual_text(segment, Some("ar"), TextDirection::RightToLeft, false)
2536            .unwrap();
2537        assert_eq!(shaped.len(), 1);
2538        assert_eq!(
2539            shaped[0].glyph_ids(),
2540            &[288, 85, 319, 18, 317, 19, 31, 48, 72, 8]
2541        );
2542        assert_eq!(
2543            shaped[0]
2544                .clusters()
2545                .iter()
2546                .map(|cluster| (cluster.glyph_start..cluster.glyph_end, cluster.char_range()))
2547                .collect::<Vec<_>>(),
2548            vec![
2549                (0..2, 6..7),
2550                (2..4, 5..6),
2551                (4..6, 4..5),
2552                (6..7, 3..4),
2553                (7..8, 2..3),
2554                (8..9, 1..2),
2555                (9..10, 0..1),
2556            ]
2557        );
2558    }
2559
2560    #[test]
2561    fn indic_clusters_are_never_split_or_mapped_as_independent_scalars() {
2562        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2563        let segment = multilingual_test_segment(&mut fm, "कि", 18.0, None);
2564        let shaped = fm
2565            .shape_multilingual_text(segment, Some("hi"), TextDirection::LeftToRight, false)
2566            .unwrap();
2567
2568        assert_eq!(shaped[0].clusters()[0].char_range(), 0..2);
2569        assert_eq!(shaped[0].x_offsets().len(), shaped[0].glyph_ids().len());
2570        assert_eq!(shaped[0].y_offsets().len(), shaped[0].glyph_ids().len());
2571    }
2572
2573    #[test]
2574    fn same_script_coverage_changes_split_only_between_graphemes() {
2575        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2576        let font_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2577        let metrics = fm.metrics(font_id, 18.0).unwrap();
2578        let seed = fm.shape_text(font_id, "A☀∙", 18.0).unwrap();
2579        let shaped = fm
2580            .shape_multilingual_text(
2581                TextSegment {
2582                    text: "A☀∙".to_owned(),
2583                    direction: TextDirection::Auto,
2584                    source: None,
2585                    font_id,
2586                    font_size: 18.0,
2587                    glyph_ids: seed.glyph_ids,
2588                    advances: seed.advances,
2589                    width: seed.width,
2590                    ascent: metrics.ascent,
2591                    descent: metrics.descent,
2592                    line_gap: metrics.line_gap,
2593                    color: Color::BLACK,
2594                    bold: false,
2595                    italic: false,
2596                    underline: None,
2597                    strike: false,
2598                    dstrike: false,
2599                    highlight: None,
2600                    baseline_offset: 0.0,
2601                    hyperlink_url: None,
2602                    field_kind: None,
2603                    field_source: None,
2604                    note: None,
2605                },
2606                None,
2607                TextDirection::LeftToRight,
2608                false,
2609            )
2610            .unwrap();
2611
2612        assert_eq!(
2613            shaped.iter().map(|span| span.text()).collect::<Vec<_>>(),
2614            ["A", "☀", "∙"]
2615        );
2616        assert!(shaped.iter().all(|span| span.script() == TextScript::Latin));
2617        assert_ne!(shaped[1].font_id(), shaped[2].font_id());
2618        assert!(shaped.iter().all(|span| {
2619            let index = fm.index_of(span.font_id()).unwrap();
2620            fm.uncovered(index, span.text()).is_empty()
2621        }));
2622    }
2623
2624    #[test]
2625    fn multilingual_constructor_rejects_invalid_levels_and_cluster_maps() {
2626        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2627        let segment = multilingual_test_segment(&mut fm, "ab", 18.0, None);
2628        let valid = fm
2629            .shape_multilingual_text(segment, None, TextDirection::LeftToRight, true)
2630            .unwrap()
2631            .remove(0);
2632        let rebuild = |bidi_level, clusters| {
2633            MultilingualTextSegment::new(
2634                valid.base().clone(),
2635                valid.logical_index(),
2636                valid.language().map(str::to_owned),
2637                valid.script(),
2638                valid.direction(),
2639                bidi_level,
2640                valid.x_advances().to_vec(),
2641                valid.y_advances().to_vec(),
2642                valid.x_offsets().to_vec(),
2643                valid.y_offsets().to_vec(),
2644                clusters,
2645                valid.break_after(),
2646            )
2647        };
2648
2649        assert!(rebuild(255, valid.clusters().to_vec()).is_err());
2650        let mut out_of_bounds = valid.clusters().to_vec();
2651        out_of_bounds.last_mut().unwrap().char_end = 3;
2652        assert!(rebuild(valid.bidi_level(), out_of_bounds).is_err());
2653        let mut glyph_gap = valid.clusters().to_vec();
2654        glyph_gap[0].glyph_start = 1;
2655        assert!(rebuild(valid.bidi_level(), glyph_gap).is_err());
2656    }
2657
2658    #[test]
2659    fn thai_words_offer_approved_breaks_without_losing_source_text() {
2660        let text = "ภาษาไทยยินดีต้อนรับ";
2661        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2662        let source = SourceSpan {
2663            node: crate::SourceNodeId::new(3).unwrap(),
2664            char_start: 10,
2665            char_end: 29,
2666        };
2667        let segment = multilingual_test_segment(&mut fm, text, 18.0, Some(source));
2668        let shaped = fm
2669            .shape_multilingual_text(segment, Some("th"), TextDirection::LeftToRight, false)
2670            .unwrap();
2671        assert_eq!(
2672            shaped
2673                .iter()
2674                .map(|span| {
2675                    let source = span.base().source.unwrap();
2676                    (
2677                        span.text(),
2678                        span.break_after(),
2679                        source.char_start..source.char_end,
2680                    )
2681                })
2682                .collect::<Vec<_>>(),
2683            vec![
2684                ("ภาษา", true, 10..14),
2685                ("ไทยยิน", true, 14..20),
2686                ("ดี", true, 20..22),
2687                ("ต้อน", true, 22..26),
2688                ("รับ", true, 26..29),
2689            ]
2690        );
2691        assert_eq!(
2692            shaped.iter().map(|span| span.text()).collect::<String>(),
2693            text
2694        );
2695    }
2696
2697    #[test]
2698    fn shaping_memo_hits_preserve_fifo_order() {
2699        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2700        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2701        fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2702        fm.shape_text(font, "newest exact shape", 11.0).unwrap();
2703        fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
2704
2705        let memo = fm.shaping_memo.lock().unwrap();
2706        assert_eq!(memo.hits, 1);
2707        assert_eq!(memo.entries.front().unwrap().0.text, "oldest exact shape");
2708        assert_eq!(memo.entries.back().unwrap().0.text, "newest exact shape");
2709    }
2710
2711    #[test]
2712    fn shaping_memo_fingerprint_collision_requires_exact_key_equality() {
2713        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2714        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2715        fm.shape_text(font, "first collision candidate", 11.0)
2716            .unwrap();
2717        let forced = shaping_fingerprint(font, "second collision candidate", 11.0f64.to_bits());
2718        fm.shaping_memo.lock().unwrap().entries[0].3 = forced;
2719
2720        fm.shape_text(font, "second collision candidate", 11.0)
2721            .unwrap();
2722
2723        let (hits, misses, entries, _) = fm.shaping_memo_counts();
2724        assert_eq!((hits, misses, entries), (0, 2, 2));
2725    }
2726
2727    #[test]
2728    fn shaping_memo_is_bounded_and_recovers_from_poison() {
2729        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2730        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2731        for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
2732            fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
2733                .unwrap();
2734        }
2735        let (_, _, entries, bytes) = fm.shaping_memo_counts();
2736        assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
2737        assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
2738
2739        let fm = Arc::new(fm);
2740        let poison = Arc::clone(&fm);
2741        assert!(
2742            std::thread::spawn(move || {
2743                let _guard = poison.shaping_memo.lock().unwrap();
2744                panic!("poison shaping cache for recovery coverage");
2745            })
2746            .join()
2747            .is_err()
2748        );
2749        let first = fm.shape_text(font, "after poison", 11.0).unwrap();
2750        let second = fm.shape_text(font, "after poison", 11.0).unwrap();
2751        assert_eq!(first.glyph_ids, second.glyph_ids);
2752        let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
2753        assert_eq!((hits, misses, entries), (1, 1, 1));
2754        assert!(bytes > 0);
2755    }
2756
2757    #[test]
2758    fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
2759        let mut memo = ShapingMemo::new();
2760        for suffix in ['a', 'b'] {
2761            memo.insert(
2762                ShapingKey {
2763                    font_id: FontId(0),
2764                    text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
2765                    size_bits: 11.0f64.to_bits(),
2766                },
2767                ShapedText {
2768                    glyph_ids: Vec::new(),
2769                    advances: Vec::new(),
2770                    width: 0.0,
2771                },
2772            );
2773        }
2774        assert_eq!(memo.entries.len(), 1);
2775        assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
2776    }
2777
2778    #[test]
2779    fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
2780        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
2781        for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
2782            fm.remember_coverage_fallback(false, false, 0);
2783        }
2784        assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
2785
2786        let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
2787            .filter_map(|value| char::from_u32(0x10_000 + value as u32))
2788            .collect::<Vec<_>>();
2789        fm.remember_coverage_misses(&misses);
2790        assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
2791
2792        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2793            fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
2794                .expect("bounded fallback resolves");
2795        }
2796        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2797        assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
2798    }
2799
2800    #[test]
2801    fn active_document_may_resolve_more_than_256_distinct_faces() {
2802        let source = bundled_font_data()[4].1;
2803        let mut db = fontdb::Database::new();
2804        for index in 0..257 {
2805            db.load_font_data(font_with_family(source, &format!("F{index:06}")));
2806        }
2807        let mut fm = FontManager::from_base_database(db);
2808        fm.begin_layout();
2809        let mut ids = HashSet::new();
2810        for index in 0..257 {
2811            let family = format!("F{index:06}");
2812            let id = fm
2813                .resolve_font(Some(&family), false, false)
2814                .expect("distinct active face resolves");
2815            assert_eq!(fm.font_data(id).unwrap().family, family);
2816            ids.insert(id);
2817        }
2818        assert_eq!(ids.len(), 257);
2819        fm.retain_current_fonts();
2820        assert_eq!(fm.fonts.len(), 257);
2821    }
2822
2823    #[test]
2824    fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
2825        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
2826        fm.begin_layout();
2827        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
2828            fm.resolve_font(Some("Carlito"), false, false).unwrap();
2829        }
2830        assert!(fm.paragraph_font_trace.is_none());
2831
2832        fm.begin_paragraph_font_trace();
2833        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
2834            fm.resolve_font(Some("Carlito"), false, false).unwrap();
2835        }
2836        assert!(fm.finish_paragraph_font_trace().is_none());
2837
2838        fm.begin_layout();
2839        assert_eq!(fm.layout_fonts.capacity(), 0);
2840        fm.begin_paragraph_font_trace();
2841        fm.resolve_font(Some("Carlito"), false, false).unwrap();
2842        let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
2843        assert_eq!(trace.len(), 1);
2844        assert_eq!(trace.capacity(), trace.len());
2845    }
2846
2847    #[cfg(feature = "system-fonts")]
2848    #[test]
2849    fn file_byte_cache_is_bounded_and_recovers_from_poison() {
2850        let cache = Arc::new(Mutex::new(FileFontCache::new()));
2851        {
2852            let mut cache = cache
2853                .lock()
2854                .unwrap_or_else(std::sync::PoisonError::into_inner);
2855            cache.clear();
2856            let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
2857            let returned = cache_file_font_bytes(
2858                &mut cache,
2859                PathBuf::from("oversized-font.ttc"),
2860                Arc::clone(&oversized),
2861            )
2862            .expect("oversized bytes are returned uncached");
2863            assert!(Arc::ptr_eq(&oversized, &returned));
2864            assert!(cache.entries.is_empty());
2865            assert_eq!(cache.bytes, 0);
2866        }
2867
2868        let poison = Arc::clone(&cache);
2869        assert!(
2870            std::thread::spawn(move || {
2871                let cache = poison;
2872                let _guard = cache.lock().unwrap();
2873                panic!("poison file byte cache for recovery coverage");
2874            })
2875            .join()
2876            .is_err()
2877        );
2878
2879        let path = std::env::temp_dir().join(format!(
2880            "rdocx-font-cache-poison-{}-{:?}.ttf",
2881            std::process::id(),
2882            std::thread::current().id()
2883        ));
2884        std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
2885        let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
2886        let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
2887        assert!(Arc::ptr_eq(&first, &second));
2888        std::fs::remove_file(path).expect("remove recovery font");
2889    }
2890
2891    #[cfg(not(feature = "system-fonts"))]
2892    #[test]
2893    fn no_default_features_omits_system_font_discovery() {
2894        let fm = FontManager::new();
2895        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2896    }
2897
2898    #[test]
2899    fn font_manager_with_no_fonts_returns_an_error() {
2900        let mut fm = FontManager::new_with_fonts(Vec::new());
2901        assert!(matches!(
2902            fm.resolve_font(None, false, false),
2903            Err(LayoutError::FontNotFound(_))
2904        ));
2905    }
2906
2907    #[test]
2908    fn load_system_font() {
2909        let mut fm = FontManager::new();
2910        // Should be able to resolve at least one font via fallback
2911        let result = fm.resolve_font(None, false, false);
2912        // On CI or systems without fonts this might fail, so we just check it doesn't panic
2913        if let Ok(id) = result {
2914            assert_eq!(id.0, 0);
2915        }
2916    }
2917
2918    #[test]
2919    fn font_metrics_positive() {
2920        let mut fm = FontManager::new();
2921        if let Ok(id) = fm.resolve_font(None, false, false) {
2922            let metrics = fm.metrics(id, 12.0).unwrap();
2923            assert!(metrics.ascent > 0.0);
2924            assert!(metrics.descent > 0.0);
2925            assert!(metrics.units_per_em > 0);
2926        }
2927    }
2928
2929    #[test]
2930    fn shape_hello_world() {
2931        let mut fm = FontManager::new();
2932        if let Ok(id) = fm.resolve_font(None, false, false) {
2933            let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
2934            assert!(!shaped.glyph_ids.is_empty());
2935            assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
2936            assert!(shaped.width > 0.0);
2937        }
2938    }
2939
2940    #[test]
2941    fn font_caching() {
2942        let mut fm = FontManager::new();
2943        if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
2944            let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
2945            assert_eq!(id1, id2);
2946        }
2947    }
2948
2949    #[test]
2950    fn font_resolution_alias_cache_is_bounded() {
2951        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2952        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2953            fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
2954                .expect("fallback font resolves");
2955        }
2956        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2957        assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2958    }
2959
2960    #[test]
2961    fn bold_italic_variants() {
2962        let mut fm = FontManager::new();
2963        let regular = fm.resolve_font(None, false, false);
2964        let bold = fm.resolve_font(None, true, false);
2965        if let (Ok(r), Ok(b)) = (regular, bold) {
2966            // Bold should get a different font ID (different variant)
2967            assert_ne!(r, b);
2968        }
2969    }
2970
2971    /// Latin text must resolve exactly as it did before, so the coverage check
2972    /// cannot disturb the overwhelmingly common case.
2973    #[test]
2974    fn latin_text_resolves_the_same_as_by_name() {
2975        let mut fm = FontManager::new();
2976        let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
2977            return;
2978        };
2979        let for_text = fm
2980            .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
2981            .unwrap();
2982        assert_eq!(by_name, for_text);
2983    }
2984
2985    /// Text nothing can draw must keep the requested font rather than failing.
2986    ///
2987    /// The approved deterministic fallbacks intentionally do not cover emoji,
2988    /// so the search is guaranteed to come up empty. The text still needs a
2989    /// font so it occupies the right space.
2990    #[test]
2991    fn text_no_font_can_draw_keeps_the_requested_font() {
2992        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2993        let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2994        let resolved = fm
2995            .resolve_font_for_text(Some("Carlito"), false, false, "🀄")
2996            .unwrap();
2997        assert_eq!(
2998            primary, resolved,
2999            "with no covering font available the original must be kept"
3000        );
3001    }
3002
3003    /// Whitespace absent from a font is not a reason to go hunting for another.
3004    #[test]
3005    fn whitespace_does_not_trigger_a_fallback() {
3006        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
3007        let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
3008        let idx = fm.index_of(by_name).unwrap();
3009        // A non-breaking space and a tab, neither of which every face carries.
3010        assert!(
3011            fm.uncovered(idx, "a\u{00a0}b\tc")
3012                .iter()
3013                .all(|c| *c != '\t'),
3014            "control and whitespace characters must be ignored"
3015        );
3016    }
3017
3018    /// When the machine does have a CJK font, CJK text must not keep a Latin
3019    /// font that cannot draw it.
3020    ///
3021    /// Skipped where no such font is installed, which is why it asserts
3022    /// nothing about which font is chosen.
3023    #[test]
3024    fn cjk_text_moves_off_a_latin_font_when_possible() {
3025        let mut fm = FontManager::new();
3026        let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
3027            return;
3028        };
3029        let Some(idx) = fm.index_of(latin) else {
3030            return;
3031        };
3032        if fm.uncovered(idx, "这是中文").is_empty() {
3033            return; // that font somehow covers it, nothing to prove
3034        }
3035        let resolved = fm
3036            .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
3037            .unwrap();
3038        if resolved == latin {
3039            return; // no covering font installed on this machine
3040        }
3041        let new_idx = fm.index_of(resolved).unwrap();
3042        assert!(
3043            fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
3044            "the replacement must cover more of the text than the original"
3045        );
3046    }
3047}