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