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};
7#[cfg(feature = "system-fonts")]
8use std::path::{Path, PathBuf};
9#[cfg(feature = "system-fonts")]
10use std::sync::OnceLock;
11use std::sync::{Arc, Mutex};
12
13#[cfg(all(test, feature = "system-fonts"))]
14use std::sync::atomic::{AtomicUsize, Ordering};
15
16use crate::error::{LayoutError, Result};
17use crate::output::FontId;
18
19/// Font data provided by the user or extracted from an OOXML file.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct FontFile {
22    /// Font family name (e.g., "Calibri", "Arial").
23    pub family: String,
24    /// Raw font file bytes (TTF/OTF).
25    pub data: Vec<u8>,
26}
27
28/// Key for caching resolved fonts.
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30struct FontKey {
31    family: String,
32    bold: bool,
33    italic: bool,
34}
35
36/// Metrics for a font at a given size.
37#[derive(Debug, Clone, Copy)]
38pub struct FontMetrics {
39    /// Ascent in points (positive, above baseline).
40    pub ascent: f64,
41    /// Descent in points (positive, below baseline).
42    pub descent: f64,
43    /// Line gap in points.
44    pub line_gap: f64,
45    /// Units per em.
46    pub units_per_em: u16,
47}
48
49/// Result of shaping a text string.
50#[derive(Debug, Clone)]
51pub struct ShapedText {
52    /// Glyph IDs from shaping.
53    pub glyph_ids: Vec<u16>,
54    /// Per-glyph advances in points.
55    pub advances: Vec<f64>,
56    /// Total width in points.
57    pub width: f64,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61struct ShapingKey {
62    font_id: FontId,
63    text: String,
64    size_bits: u64,
65}
66
67struct ShapingMemo {
68    entries: VecDeque<(ShapingKey, ShapedText, usize, u64)>,
69    bytes: usize,
70    #[cfg(test)]
71    hits: usize,
72    #[cfg(test)]
73    misses: usize,
74}
75
76impl ShapingMemo {
77    fn new() -> Self {
78        Self {
79            entries: VecDeque::new(),
80            bytes: 0,
81            #[cfg(test)]
82            hits: 0,
83            #[cfg(test)]
84            misses: 0,
85        }
86    }
87
88    fn clear(&mut self) {
89        self.entries.clear();
90        self.bytes = 0;
91        #[cfg(test)]
92        {
93            self.hits = 0;
94            self.misses = 0;
95        }
96    }
97
98    fn insert(&mut self, key: ShapingKey, shaped: ShapedText) {
99        let entry_bytes = std::mem::size_of::<(ShapingKey, ShapedText, usize, u64)>()
100            + key.text.len()
101            + shaped.glyph_ids.len() * std::mem::size_of::<u16>()
102            + shaped.advances.len() * std::mem::size_of::<f64>();
103        if entry_bytes > SHAPING_CACHE_MAX_BYTES {
104            return;
105        }
106        while self.entries.len() >= SHAPING_CACHE_MAX_ENTRIES
107            || self.bytes.saturating_add(entry_bytes) > SHAPING_CACHE_MAX_BYTES
108        {
109            let Some((_, _, evicted_bytes, _)) = self.entries.pop_front() else {
110                break;
111            };
112            self.bytes = self.bytes.saturating_sub(evicted_bytes);
113        }
114        self.bytes += entry_bytes;
115        let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
116        self.entries
117            .push_back((key, shaped, entry_bytes, fingerprint));
118    }
119}
120
121fn shaping_fingerprint(font_id: FontId, text: &str, size_bits: u64) -> u64 {
122    use std::hash::{Hash, Hasher};
123
124    let mut hasher = std::collections::hash_map::DefaultHasher::new();
125    font_id.hash(&mut hasher);
126    text.hash(&mut hasher);
127    size_bits.hash(&mut hasher);
128    hasher.finish()
129}
130
131const SHAPING_CACHE_MAX_ENTRIES: usize = 2_048;
132const SHAPING_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
133
134#[cfg(feature = "system-fonts")]
135struct FileFontCache {
136    entries: VecDeque<(PathBuf, Arc<[u8]>, usize)>,
137    bytes: usize,
138}
139
140#[cfg(feature = "system-fonts")]
141impl FileFontCache {
142    fn new() -> Self {
143        Self {
144            entries: VecDeque::new(),
145            bytes: 0,
146        }
147    }
148
149    fn clear(&mut self) {
150        self.entries.clear();
151        self.bytes = 0;
152    }
153}
154
155#[cfg(feature = "system-fonts")]
156const FILE_FONT_CACHE_MAX_ENTRIES: usize = 256;
157#[cfg(feature = "system-fonts")]
158const FILE_FONT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
159
160#[cfg(feature = "system-fonts")]
161static NORMAL_FONT_DATABASE: OnceLock<fontdb::Database> = OnceLock::new();
162#[cfg(feature = "system-fonts")]
163static FILE_FONT_CACHE: OnceLock<Mutex<FileFontCache>> = OnceLock::new();
164#[cfg(all(test, feature = "system-fonts"))]
165static SYSTEM_FONT_DISCOVERY_RUNS: AtomicUsize = AtomicUsize::new(0);
166
167/// Internal record for a loaded font face.
168struct LoadedFont {
169    db_id: fontdb::ID,
170    id: FontId,
171    family: String,
172    bold: bool,
173    italic: bool,
174    data: Arc<[u8]>,
175    face_index: u32,
176    units_per_em: u16,
177    /// Vertical metrics in design units, read once when the face is loaded.
178    ascender: i16,
179    descender: i16,
180    line_gap: i16,
181    /// HarfRust's per-face shaping caches. Building these is the expensive
182    /// part of shaping, so it happens once per face instead of once per run.
183    shaper_data: harfrust::ShaperData,
184}
185
186struct ParagraphFontTrace {
187    ids: Vec<FontId>,
188    overflowed: bool,
189}
190
191/// Manages font discovery, loading, shaping, and metrics.
192pub struct FontManager {
193    db: fontdb::Database,
194    /// Database before document-embedded or caller fonts are applied.
195    base_db: fontdb::Database,
196    /// Caller labels whose faces are retained in `base_db`.
197    base_caller_aliases: HashMap<String, Vec<fontdb::ID>>,
198    /// Caller embedded families whose faces are retained in `base_db`.
199    base_caller_families: HashMap<String, Vec<fontdb::ID>>,
200    /// Map from FontKey to loaded font info.
201    cache: HashMap<FontKey, usize>,
202    /// Manager-owned bytes for bundled, embedded, and caller-provided faces.
203    memory_face_data: HashMap<fontdb::ID, Arc<[u8]>>,
204    /// All loaded fonts.
205    fonts: Vec<LoadedFont>,
206    /// Next font ID counter.
207    next_id: u32,
208    /// Fonts already discovered as covering something the requested family
209    /// could not, keyed by (bold, italic).
210    ///
211    /// Finding a font that covers a character means loading and inspecting
212    /// faces, which is far too slow to repeat per character. Once a CJK face
213    /// has been found for one character it almost always covers the rest of
214    /// the run, so it is tried first next time.
215    coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
216    /// Characters already searched for and not found in any available font, so
217    /// the scan is not repeated for every occurrence.
218    coverage_misses: HashSet<char>,
219    /// Exact additional font set currently loaded into `db`.
220    additional_fonts: Vec<FontFile>,
221    /// Lowercased caller labels mapped to the exact faces loaded from their
222    /// bytes. Rebuilt together with `additional_fonts`.
223    caller_aliases: HashMap<String, Vec<fontdb::ID>>,
224    /// Exact embedded caller families mapped to their loaded faces, so caller
225    /// bytes take priority over bundled faces with the same family.
226    caller_families: HashMap<String, Vec<fontdb::ID>>,
227    /// Exact caller-declared alias identity for cheap change detection.
228    explicit_aliases: Vec<(String, String)>,
229    /// Lowercased requested family mapped to the caller-declared target.
230    explicit_alias_map: HashMap<String, String>,
231    /// Bounded exact-key shaping results.
232    shaping_memo: Mutex<ShapingMemo>,
233    /// Exact resolution events for one cache-candidate paragraph.
234    paragraph_font_trace: Option<ParagraphFontTrace>,
235    /// Distinct current-layout fonts in first-resolution order.
236    layout_fonts: Vec<FontId>,
237}
238
239/// Families with broad non-Latin coverage, tried before scanning everything.
240///
241/// Ordered roughly by how likely each is to be installed. This is only a fast
242/// path: if none of them is present the full font database is still searched.
243const BROAD_COVERAGE_FAMILIES: &[&str] = &[
244    // Bundled with or shipped alongside many Linux distributions
245    "Noto Sans CJK SC",
246    "Noto Sans CJK JP",
247    "Noto Sans CJK KR",
248    "Noto Sans CJK TC",
249    "Noto Serif CJK SC",
250    "Source Han Sans SC",
251    "WenQuanYi Zen Hei",
252    "WenQuanYi Micro Hei",
253    // macOS
254    "PingFang SC",
255    "PingFang TC",
256    "Hiragino Sans",
257    "Hiragino Kaku Gothic ProN",
258    "Apple SD Gothic Neo",
259    "Songti SC",
260    "STHeiti",
261    // Windows
262    "Microsoft YaHei",
263    "Microsoft JhengHei",
264    "SimSun",
265    "SimHei",
266    "NSimSun",
267    "Yu Gothic",
268    "MS Gothic",
269    "Meiryo",
270    "Malgun Gothic",
271    // Wide-coverage generalists
272    "Arial Unicode MS",
273    "DejaVu Sans",
274];
275
276const RESOLUTION_CACHE_MAX_ENTRIES: usize = 256;
277const COVERAGE_FALLBACK_MAX_ENTRIES: usize = 256;
278const COVERAGE_MISS_MAX_ENTRIES: usize = 4_096;
279const PARAGRAPH_FONT_TRACE_MAX_ENTRIES: usize = 4_096;
280
281/// Maximum caller-declared aliases retained for resolution identity.
282const CALLER_ALIAS_MAX_ENTRIES: usize = 256;
283
284/// Maximum aggregate UTF-8 payload retained by the ordered caller-alias
285/// identity and its lowercased lookup map.
286const CALLER_ALIAS_MAX_RETAINED_BYTES: usize = 64 * 1024;
287
288/// Return the deterministic prefix that fits both caller-alias ceilings.
289///
290/// The byte accounting includes requested and target strings in the ordered
291/// identity plus the normalized requested key and target value in the lookup
292/// map. The first entry that would exceed either ceiling and every later entry
293/// are discarded.
294fn bounded_caller_aliases(aliases: &[(String, String)]) -> Vec<(String, String)> {
295    let mut bounded = Vec::with_capacity(aliases.len().min(CALLER_ALIAS_MAX_ENTRIES));
296    let mut retained_bytes = 0usize;
297    for (requested, target) in aliases {
298        if bounded.len() == CALLER_ALIAS_MAX_ENTRIES {
299            break;
300        }
301        let normalized_requested = requested.to_lowercase();
302        let entry_bytes = requested
303            .len()
304            .saturating_add(target.len())
305            .saturating_add(normalized_requested.len())
306            .saturating_add(target.len());
307        let next_retained_bytes = retained_bytes.saturating_add(entry_bytes);
308        if next_retained_bytes > CALLER_ALIAS_MAX_RETAINED_BYTES {
309            break;
310        }
311        bounded.push((requested.to_owned(), target.to_owned()));
312        retained_bytes = next_retained_bytes;
313    }
314    bounded
315}
316
317impl Default for FontManager {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323impl FontManager {
324    fn from_base_database(db: fontdb::Database) -> Self {
325        Self {
326            base_db: db.clone(),
327            base_caller_aliases: HashMap::new(),
328            base_caller_families: HashMap::new(),
329            db,
330            cache: HashMap::new(),
331            memory_face_data: HashMap::new(),
332            fonts: Vec::new(),
333            next_id: 0,
334            coverage_fallbacks: HashMap::new(),
335            coverage_misses: HashSet::new(),
336            additional_fonts: Vec::new(),
337            caller_aliases: HashMap::new(),
338            caller_families: HashMap::new(),
339            explicit_aliases: Vec::new(),
340            explicit_alias_map: HashMap::new(),
341            shaping_memo: Mutex::new(ShapingMemo::new()),
342            paragraph_font_trace: None,
343            layout_fonts: Vec::new(),
344        }
345    }
346
347    /// Create a new FontManager and load system fonts.
348    ///
349    /// Bundled fonts (Carlito, Caladea, Liberation) are loaded as fallbacks.
350    /// System fonts are discovered when the `system-fonts` feature is enabled.
351    pub fn new() -> Self {
352        #[cfg(feature = "system-fonts")]
353        {
354            let db = NORMAL_FONT_DATABASE.get_or_init(|| {
355                let mut db = bundled_font_database();
356                db.load_system_fonts();
357                #[cfg(test)]
358                SYSTEM_FONT_DISCOVERY_RUNS.fetch_add(1, Ordering::Relaxed);
359                db
360            });
361            Self::from_base_database(db.clone())
362        }
363
364        #[cfg(not(feature = "system-fonts"))]
365        Self::from_base_database(bundled_font_database())
366    }
367
368    /// Create a font manager that loads bundled fonts without discovering
369    /// system fonts.
370    ///
371    /// This mode makes font resolution reproducible across machines.
372    pub fn new_deterministic() -> Result<Self> {
373        Ok(Self::from_base_database(bundled_font_database()))
374    }
375
376    /// Load or replace additional font files (user-provided or extracted from
377    /// an OOXML package).
378    ///
379    /// An unchanged set is a no-op so a reusable engine retains resolution and
380    /// shaping state. A changed set rebuilds from the isolated base database,
381    /// which prevents stale face ids and bounds repeated document edits.
382    pub fn load_additional_fonts(&mut self, font_files: &[FontFile]) -> bool {
383        if self.additional_fonts == font_files {
384            return false;
385        }
386
387        self.db = self.base_db.clone();
388        self.caller_aliases.clone_from(&self.base_caller_aliases);
389        self.caller_families.clone_from(&self.base_caller_families);
390        for font_file in font_files {
391            Self::load_caller_font(
392                &mut self.db,
393                &mut self.caller_aliases,
394                &mut self.caller_families,
395                &font_file.family,
396                font_file.data.clone(),
397            );
398        }
399        self.cache.clear();
400        self.memory_face_data.clear();
401        self.fonts.clear();
402        self.next_id = 0;
403        self.coverage_fallbacks.clear();
404        self.coverage_misses.clear();
405        self.additional_fonts = font_files.to_vec();
406        if self.shaping_memo.is_poisoned() {
407            self.shaping_memo.clear_poison();
408        }
409        self.shaping_memo
410            .get_mut()
411            .expect("shaping cache poison was cleared")
412            .clear();
413        true
414    }
415
416    /// Create a FontManager with user-provided fonts (no system font loading).
417    ///
418    /// Each entry is `(family_name, font_bytes)`. This is useful in environments
419    /// where system fonts are not available, such as WASM.
420    pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
421        let mut db = fontdb::Database::new();
422        let mut caller_aliases = HashMap::new();
423        let mut caller_families = HashMap::new();
424        for (family, data) in &fonts {
425            Self::load_caller_font(
426                &mut db,
427                &mut caller_aliases,
428                &mut caller_families,
429                family,
430                data.clone(),
431            );
432        }
433        let mut manager = Self::from_base_database(db);
434        manager.base_caller_aliases = caller_aliases.clone();
435        manager.base_caller_families = caller_families.clone();
436        manager.caller_aliases = caller_aliases;
437        manager.caller_families = caller_families;
438        manager
439    }
440
441    /// Replace byte-free caller aliases from requested family to loaded family.
442    ///
443    /// An unchanged slice is a no-op. A changed slice invalidates only name
444    /// resolution and coverage state. Loaded faces and shaping entries remain
445    /// valid because their `FontId` values do not change.
446    pub fn set_caller_aliases(&mut self, aliases: &[(String, String)]) -> bool {
447        let aliases = bounded_caller_aliases(aliases);
448        if self.explicit_aliases == aliases {
449            return false;
450        }
451
452        self.explicit_alias_map = aliases
453            .iter()
454            .map(|(requested, target)| (requested.to_lowercase(), target.clone()))
455            .collect();
456        self.explicit_aliases = aliases;
457        self.cache.clear();
458        self.coverage_fallbacks.clear();
459        self.coverage_misses.clear();
460        true
461    }
462
463    fn load_caller_font(
464        db: &mut fontdb::Database,
465        aliases: &mut HashMap<String, Vec<fontdb::ID>>,
466        families: &mut HashMap<String, Vec<fontdb::ID>>,
467        family: &str,
468        data: Vec<u8>,
469    ) {
470        let before = db.len();
471        db.load_font_data(data);
472        let loaded_faces = db
473            .faces()
474            .skip(before)
475            .map(|face| {
476                (
477                    face.id,
478                    face.families
479                        .iter()
480                        .map(|(name, _)| name.clone())
481                        .collect::<Vec<_>>(),
482                )
483            })
484            .collect::<Vec<_>>();
485        for (id, loaded_families) in &loaded_faces {
486            for loaded_family in loaded_families {
487                families.entry(loaded_family.clone()).or_default().push(*id);
488            }
489        }
490        if !loaded_faces.is_empty()
491            && loaded_faces
492                .iter()
493                .all(|(_, families)| !families.iter().any(|loaded| loaded == family))
494        {
495            aliases
496                .entry(family.to_lowercase())
497                .or_default()
498                .extend(loaded_faces.into_iter().map(|(id, _)| id));
499        }
500    }
501
502    /// Begin one complete layout attempt's exact font-usage trace.
503    #[doc(hidden)]
504    pub fn begin_layout(&mut self) {
505        self.paragraph_font_trace = None;
506        self.layout_fonts = Vec::new();
507    }
508
509    /// Begin recording the exact resolution events for one cache candidate.
510    #[doc(hidden)]
511    pub fn begin_paragraph_font_trace(&mut self) {
512        self.paragraph_font_trace = Some(ParagraphFontTrace {
513            ids: Vec::new(),
514            overflowed: false,
515        });
516    }
517
518    /// Finish one bounded paragraph trace. An overflowed paragraph bypasses reuse.
519    #[doc(hidden)]
520    pub fn finish_paragraph_font_trace(&mut self) -> Option<Vec<FontId>> {
521        let mut trace = self.paragraph_font_trace.take()?;
522        if trace.overflowed {
523            return None;
524        }
525        trace.ids.shrink_to_fit();
526        Some(trace.ids)
527    }
528
529    /// Replay the exact resolution events attached to a cached paragraph.
530    #[doc(hidden)]
531    pub fn replay_layout_font_trace(&mut self, trace: &[FontId]) {
532        for &font_id in trace {
533            self.record_layout_font(font_id);
534        }
535    }
536
537    /// Distinct current-layout fonts in first-resolution order.
538    #[doc(hidden)]
539    pub fn current_layout_fonts(&self) -> &[FontId] {
540        &self.layout_fonts
541    }
542
543    /// Whether no historical loaded face is absent from this layout.
544    #[doc(hidden)]
545    pub fn every_loaded_font_is_current(&self) -> bool {
546        self.fonts
547            .iter()
548            .map(|font| font.id)
549            .eq(self.layout_fonts.iter().copied())
550            && self
551                .layout_fonts
552                .iter()
553                .enumerate()
554                .all(|(index, font_id)| *font_id == FontId(index as u32))
555    }
556
557    /// Drop faces that were loaded by an older successful layout but are no
558    /// longer active. Every face used by the current document is retained,
559    /// even when that working set contains more than the cache ceilings.
560    #[doc(hidden)]
561    pub fn retain_current_fonts(&mut self) {
562        let current = self.layout_fonts.iter().copied().collect::<HashSet<_>>();
563        let old_index_ids = self
564            .fonts
565            .iter()
566            .enumerate()
567            .map(|(index, font)| (index, font.id))
568            .collect::<HashMap<_, _>>();
569        self.fonts.retain(|font| current.contains(&font.id));
570        let current_order = self
571            .layout_fonts
572            .iter()
573            .enumerate()
574            .map(|(index, font_id)| (*font_id, index))
575            .collect::<HashMap<_, _>>();
576        self.fonts
577            .sort_by_key(|font| current_order.get(&font.id).copied().unwrap_or(usize::MAX));
578
579        let indices = self
580            .fonts
581            .iter()
582            .enumerate()
583            .map(|(index, font)| (font.id, index))
584            .collect::<HashMap<_, _>>();
585        let old_cache = std::mem::take(&mut self.cache);
586        self.cache = old_cache
587            .into_iter()
588            .filter_map(|(key, old_index)| {
589                let font_id = old_index_ids.get(&old_index)?;
590                Some((key, *indices.get(font_id)?))
591            })
592            .collect();
593        self.coverage_fallbacks.clear();
594        self.coverage_misses.clear();
595        let active_db_ids = self
596            .fonts
597            .iter()
598            .map(|font| font.db_id)
599            .collect::<HashSet<_>>();
600        self.memory_face_data
601            .retain(|db_id, _| active_db_ids.contains(db_id));
602
603        let memo = self
604            .shaping_memo
605            .get_mut()
606            .unwrap_or_else(std::sync::PoisonError::into_inner);
607        memo.entries
608            .retain(|(key, _, _, _)| current.contains(&key.font_id));
609        memo.bytes = memo.entries.iter().map(|(_, _, bytes, _)| bytes).sum();
610    }
611
612    /// Resolve a font for `text`, falling back on glyph coverage.
613    ///
614    /// `resolve_font` picks by family name alone. That is enough for Latin
615    /// text, but a run asking for a Chinese family on a machine without it
616    /// falls down the name chain and lands on a Latin font, which has no CJK
617    /// glyphs, so every character renders as a missing-glyph box. Name
618    /// matching cannot detect that, because the font it chose exists and is
619    /// perfectly valid, it simply cannot draw this text.
620    ///
621    /// So the resolved font is checked against the text, and when a character
622    /// is missing another font that can draw it is looked for.
623    ///
624    /// This is per run rather than per character: the font that covers the
625    /// first missing character is used for the whole run. Text that mixes
626    /// scripts inside one run is therefore still imperfect, but it is a large
627    /// improvement on drawing boxes.
628    pub fn resolve_font_for_text(
629        &mut self,
630        family: Option<&str>,
631        bold: bool,
632        italic: bool,
633        text: &str,
634    ) -> Result<FontId> {
635        let primary = self.resolve_font(family, bold, italic)?;
636
637        let Some(idx) = self.index_of(primary) else {
638            return Ok(primary);
639        };
640        let missing = self.uncovered(idx, text);
641        if missing.is_empty() {
642            return Ok(primary);
643        }
644
645        match self.font_covering(&missing, bold, italic) {
646            // Nothing installed can draw it. Keep the original font so the
647            // text still occupies the right space.
648            None => Ok(primary),
649            Some(id) => Ok(id),
650        }
651    }
652
653    /// The characters in `text` that the font at `idx` cannot draw.
654    ///
655    /// Whitespace and control characters are skipped: a font without a glyph
656    /// for a space is not a reason to go looking for another one.
657    fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
658        let font = &self.fonts[idx];
659        let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
660            return Vec::new();
661        };
662        let mut seen = HashSet::new();
663        text.chars()
664            .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
665            .filter(|&ch| face.glyph_index(ch).is_none())
666            .filter(|&ch| seen.insert(ch))
667            .collect()
668    }
669
670    /// Whether the font at `idx` has a glyph for `ch`.
671    fn covers(&self, idx: usize, ch: char) -> bool {
672        let font = &self.fonts[idx];
673        ttf_parser::Face::parse(&font.data, font.face_index)
674            .map(|face| face.glyph_index(ch).is_some())
675            .unwrap_or(false)
676    }
677
678    /// Find a font that can draw `missing`.
679    ///
680    /// A font covering every missing character wins. Failing that the one
681    /// covering the most is used, because a single run gets a single font and
682    /// partial coverage still beats a row of boxes. Picking on the first
683    /// missing character alone is not enough: a Japanese face may have the
684    /// characters shared with Chinese and not the simplified-only ones, so it
685    /// would look like a fix and still leave gaps.
686    fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
687        if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
688            return None;
689        }
690
691        let mut best: Option<(usize, usize)> = None; // (covered count, font index)
692        let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
693            let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
694            if covered == 0 {
695                return false;
696            }
697            if best.map(|(n, _)| covered > n).unwrap_or(true) {
698                *best = Some((covered, idx));
699            }
700            covered == missing.len()
701        };
702
703        // Fonts that already rescued an earlier run, which for a document in
704        // one script is almost always the answer again.
705        if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
706            for idx in known {
707                if consider(self, idx, &mut best) {
708                    let id = self.fonts[idx].id;
709                    self.record_layout_font(id);
710                    return Some(id);
711                }
712            }
713        }
714
715        // Families with broad coverage, then everything else the database
716        // knows about. Both go through resolve_font so loading and caching
717        // stay in one place.
718        let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
719            .iter()
720            .map(|s| s.to_string())
721            .chain(
722                self.db
723                    .faces()
724                    .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
725            )
726            .collect();
727
728        for name in candidates {
729            let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
730                continue;
731            };
732            let Some(idx) = self.index_of(id) else {
733                continue;
734            };
735            let complete = consider(self, idx, &mut best);
736            if complete {
737                self.remember_coverage_fallback(bold, italic, idx);
738                return Some(id);
739            }
740        }
741
742        match best {
743            Some((_, idx)) => {
744                self.remember_coverage_fallback(bold, italic, idx);
745                let id = self.fonts[idx].id;
746                self.record_layout_font(id);
747                Some(id)
748            }
749            None => {
750                self.remember_coverage_misses(missing);
751                None
752            }
753        }
754    }
755
756    /// Index into `fonts` for a FontId.
757    fn index_of(&self, id: FontId) -> Option<usize> {
758        self.fonts.iter().position(|f| f.id == id)
759    }
760
761    /// Resolve a font by family name, bold, and italic flags.
762    /// Returns a FontId. Uses fallback chain if the requested font is not found.
763    pub fn resolve_font(
764        &mut self,
765        family: Option<&str>,
766        bold: bool,
767        italic: bool,
768    ) -> Result<FontId> {
769        self.resolve_font_inner(family, bold, italic, true)
770    }
771
772    /// Resolve a font for metrics without claiming that it emitted glyphs.
773    #[doc(hidden)]
774    pub fn resolve_font_for_metrics(
775        &mut self,
776        family: Option<&str>,
777        bold: bool,
778        italic: bool,
779    ) -> Result<FontId> {
780        self.resolve_font_inner(family, bold, italic, false)
781    }
782
783    fn resolve_font_inner(
784        &mut self,
785        family: Option<&str>,
786        bold: bool,
787        italic: bool,
788        record_layout_use: bool,
789    ) -> Result<FontId> {
790        let family_name = family.unwrap_or("Arial");
791
792        let key = FontKey {
793            family: family_name.to_string(),
794            bold,
795            italic,
796        };
797
798        if let Some(idx) = self.cache.get(&key).copied() {
799            let id = self.fonts[idx].id;
800            if record_layout_use {
801                self.record_layout_font(id);
802            }
803            return Ok(id);
804        }
805
806        let requested_key = family_name.to_lowercase();
807        let style = if italic {
808            fontdb::Style::Italic
809        } else {
810            fontdb::Style::Normal
811        };
812        let weight = if bold {
813            fontdb::Weight::BOLD
814        } else {
815            fontdb::Weight::NORMAL
816        };
817
818        let query_family = |family: &str| {
819            if let Some(ids) = self.caller_families.get(family)
820                && let Some(id) = best_caller_face(&self.db, ids, weight, style)
821            {
822                return Some(id);
823            }
824            let query = fontdb::Query {
825                families: &[fontdb::Family::Name(family)],
826                weight,
827                style,
828                stretch: fontdb::Stretch::Normal,
829            };
830            self.db.query(&query)
831        };
832
833        let mut found_id = query_family(family_name);
834        if found_id.is_none()
835            && let Some(alias) = self.explicit_alias_map.get(&requested_key)
836        {
837            found_id = query_family(alias);
838        }
839        if found_id.is_none()
840            && let Some(ids) = self.caller_aliases.get(&requested_key)
841        {
842            found_id = best_caller_face(&self.db, ids, weight, style);
843        }
844
845        // Map common Word font names to metric-compatible alternatives, then
846        // try generic fallbacks.
847        let mut fallbacks: Vec<&str> = map_font_name(family_name).to_vec();
848        for generic in &[
849            "Carlito",
850            "Arial",
851            "Liberation Sans",
852            "Helvetica",
853            "DejaVu Sans",
854            "Noto Sans",
855        ] {
856            if !fallbacks.contains(generic) {
857                fallbacks.push(generic);
858            }
859        }
860
861        if found_id.is_none() {
862            for fallback in &fallbacks {
863                let Some(id) = query_family(fallback) else {
864                    continue;
865                };
866                found_id = Some(id);
867                break;
868            }
869        }
870
871        // Last resort: try generic families
872        if found_id.is_none() {
873            for generic_family in &[
874                fontdb::Family::SansSerif,
875                fontdb::Family::Serif,
876                fontdb::Family::Monospace,
877            ] {
878                let query = fontdb::Query {
879                    families: &[*generic_family],
880                    weight,
881                    style,
882                    stretch: fontdb::Stretch::Normal,
883                };
884                if let Some(id) = self.db.query(&query) {
885                    found_id = Some(id);
886                    break;
887                }
888            }
889        }
890
891        let db_id = found_id.ok_or_else(|| {
892            LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
893        })?;
894
895        // Preserve the established one-loaded-font-per-request-key behavior
896        // while the bounded alias cache has room. At the ceiling, reuse the
897        // exact resolved face rather than growing without limit.
898        if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
899            && let Some(idx) = self
900                .fonts
901                .iter()
902                .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
903        {
904            let id = self.fonts[idx].id;
905            if record_layout_use {
906                self.record_layout_font(id);
907            }
908            return Ok(id);
909        }
910
911        let font_id = FontId(self.next_id);
912        self.next_id += 1;
913
914        // Load file-backed data through the process cache. All faces in a TTC
915        // carry the same source path, so their collection indices share bytes.
916        let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
917            .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
918
919        let (units_per_em, ascender, descender, line_gap) = {
920            let face = ttf_parser::Face::parse(&data, face_index)
921                .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
922            (
923                face.units_per_em(),
924                face.ascender(),
925                face.descender(),
926                face.line_gap(),
927            )
928        };
929
930        // Every metric and advance is scaled by size/upem, so a zero here would
931        // turn the whole layout into infinities.
932        if units_per_em == 0 {
933            return Err(LayoutError::FontParse(format!(
934                "font '{family_name}' declares zero units per em"
935            )));
936        }
937
938        let shaper_data = {
939            let face = harfrust::FontRef::from_index(&data, face_index)
940                .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
941            harfrust::ShaperData::new(&face)
942        };
943
944        let actual_family = self
945            .db
946            .face(db_id)
947            .map(|f| {
948                f.families
949                    .first()
950                    .map(|(name, _)| name.clone())
951                    .unwrap_or_else(|| family_name.to_string())
952            })
953            .unwrap_or_else(|| family_name.to_string());
954
955        let idx = self.fonts.len();
956        self.fonts.push(LoadedFont {
957            db_id,
958            id: font_id,
959            family: actual_family,
960            bold,
961            italic,
962            data,
963            face_index,
964            units_per_em,
965            ascender,
966            descender,
967            line_gap,
968            shaper_data,
969        });
970        self.remember_font_key(key, idx);
971        if record_layout_use {
972            self.record_layout_font(font_id);
973        }
974
975        Ok(font_id)
976    }
977
978    /// Get font metrics at a given size in points.
979    pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
980        let font = self.get_font(font_id)?;
981        let scale = size_pt / font.units_per_em as f64;
982
983        Ok(FontMetrics {
984            ascent: font.ascender as f64 * scale,
985            descent: -(font.descender as f64) * scale, // make positive
986            line_gap: font.line_gap as f64 * scale,
987            units_per_em: font.units_per_em,
988        })
989    }
990
991    /// Shape a text string using HarfRust. Returns glyph IDs and advances.
992    pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
993        // HarfRust cannot derive segment properties from an empty buffer, and
994        // there is nothing to shape anyway.
995        if text.is_empty() {
996            return Ok(ShapedText {
997                glyph_ids: Vec::new(),
998                advances: Vec::new(),
999                width: 0.0,
1000            });
1001        }
1002
1003        let key = ShapingKey {
1004            font_id,
1005            text: text.to_owned(),
1006            size_bits: size_pt.to_bits(),
1007        };
1008        let fingerprint = shaping_fingerprint(key.font_id, &key.text, key.size_bits);
1009        let mut memo = match self.shaping_memo.lock() {
1010            Ok(memo) => memo,
1011            Err(poisoned) => {
1012                let mut memo = poisoned.into_inner();
1013                memo.clear();
1014                self.shaping_memo.clear_poison();
1015                memo
1016            }
1017        };
1018        if let Some(shaped) = memo
1019            .entries
1020            .iter()
1021            .rev()
1022            .find(|(candidate, _, _, candidate_fingerprint)| {
1023                *candidate_fingerprint == fingerprint && candidate == &key
1024            })
1025            .map(|(_, shaped, _, _)| shaped.clone())
1026        {
1027            #[cfg(test)]
1028            {
1029                memo.hits += 1;
1030            }
1031            return Ok(shaped);
1032        }
1033        #[cfg(test)]
1034        {
1035            memo.misses += 1;
1036        }
1037
1038        let font = self.get_font(font_id)?;
1039
1040        let face = harfrust::FontRef::from_index(&font.data, font.face_index)
1041            .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
1042
1043        let shaper = font.shaper_data.shaper(&face).build();
1044
1045        let mut buffer = harfrust::UnicodeBuffer::new();
1046        buffer.push_str(text);
1047        // Infer direction, script and language from the text. Unlike rustybuzz,
1048        // HarfRust does not do this implicitly and panics on an unset direction.
1049        buffer.guess_segment_properties();
1050
1051        let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
1052        let infos = output.glyph_infos();
1053        let positions = output.glyph_positions();
1054
1055        let upem = font.units_per_em as f64;
1056        let scale = size_pt / upem;
1057
1058        let mut glyph_ids = Vec::with_capacity(infos.len());
1059        let mut advances = Vec::with_capacity(positions.len());
1060        let mut total_width = 0.0;
1061
1062        for (info, pos) in infos.iter().zip(positions.iter()) {
1063            glyph_ids.push(info.glyph_id as u16);
1064            let advance = pos.x_advance as f64 * scale;
1065            advances.push(advance);
1066            total_width += advance;
1067        }
1068
1069        let shaped = ShapedText {
1070            glyph_ids,
1071            advances,
1072            width: total_width,
1073        };
1074        memo.insert(key, shaped.clone());
1075        Ok(shaped)
1076    }
1077
1078    /// Get font data for PDF embedding.
1079    pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
1080        let font = self.get_font(font_id)?;
1081        Ok(crate::output::FontData {
1082            id: font.id,
1083            family: font.family.clone(),
1084            data: Arc::clone(&font.data),
1085            face_index: font.face_index,
1086            bold: font.bold,
1087            italic: font.italic,
1088        })
1089    }
1090
1091    /// Get all used font data.
1092    pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
1093        self.fonts
1094            .iter()
1095            .map(|f| crate::output::FontData {
1096                id: f.id,
1097                family: f.family.clone(),
1098                data: Arc::clone(&f.data),
1099                face_index: f.face_index,
1100                bold: f.bold,
1101                italic: f.italic,
1102            })
1103            .collect()
1104    }
1105
1106    fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
1107        self.fonts
1108            .iter()
1109            .find(|f| f.id == font_id)
1110            .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
1111    }
1112
1113    fn remember_font_key(&mut self, key: FontKey, index: usize) {
1114        if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
1115            self.cache.insert(key, index);
1116        }
1117    }
1118
1119    fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
1120        let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
1121        if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
1122            known.push(index);
1123        }
1124    }
1125
1126    fn remember_coverage_misses(&mut self, missing: &[char]) {
1127        for &ch in missing {
1128            if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
1129                break;
1130            }
1131            self.coverage_misses.insert(ch);
1132        }
1133    }
1134
1135    fn record_layout_font(&mut self, font_id: FontId) {
1136        if let Some(trace) = self.paragraph_font_trace.as_mut() {
1137            if trace.ids.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
1138                trace.ids.push(font_id);
1139            } else {
1140                trace.overflowed = true;
1141            }
1142        }
1143        if !self.layout_fonts.contains(&font_id) {
1144            self.layout_fonts.push(font_id);
1145        }
1146    }
1147
1148    #[cfg(test)]
1149    fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
1150        let memo = self
1151            .shaping_memo
1152            .lock()
1153            .unwrap_or_else(std::sync::PoisonError::into_inner);
1154        (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
1155    }
1156}
1157
1158fn best_caller_face(
1159    db: &fontdb::Database,
1160    ids: &[fontdb::ID],
1161    weight: fontdb::Weight,
1162    style: fontdb::Style,
1163) -> Option<fontdb::ID> {
1164    const CANDIDATE_FAMILY: &str = "__rdocx_caller_candidate__";
1165
1166    let mut candidates = fontdb::Database::new();
1167    let mut candidate_ids = Vec::with_capacity(ids.len());
1168    for id in ids {
1169        let mut face = db.face(*id)?.clone();
1170        for (family, _) in &mut face.families {
1171            CANDIDATE_FAMILY.clone_into(family);
1172        }
1173        let candidate_id = candidates.push_face_info(face);
1174        candidate_ids.push((candidate_id, *id));
1175    }
1176
1177    let selected = candidates.query(&fontdb::Query {
1178        families: &[fontdb::Family::Name(CANDIDATE_FAMILY)],
1179        weight,
1180        style,
1181        stretch: fontdb::Stretch::Normal,
1182    })?;
1183    candidate_ids
1184        .into_iter()
1185        .find_map(|(candidate, original)| (candidate == selected).then_some(original))
1186}
1187
1188fn bundled_font_database() -> fontdb::Database {
1189    let mut db = fontdb::Database::new();
1190    for (_family, data) in crate::bundled_fonts::bundled_font_data() {
1191        db.load_font_data(data.to_vec());
1192    }
1193    db
1194}
1195
1196fn font_data_for_face(
1197    db: &fontdb::Database,
1198    id: fontdb::ID,
1199    memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
1200) -> Option<(Arc<[u8]>, u32)> {
1201    let face = db.face(id)?;
1202    let face_index = face.index;
1203    match &face.source {
1204        fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
1205            Some(data) => Some((Arc::clone(data), face_index)),
1206            None => {
1207                let data: Arc<[u8]> = Arc::from(data.as_ref().as_ref().to_vec());
1208                memory_face_data.insert(id, Arc::clone(&data));
1209                Some((data, face_index))
1210            }
1211        },
1212        #[cfg(feature = "system-fonts")]
1213        fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
1214    }
1215}
1216
1217#[cfg(feature = "system-fonts")]
1218fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
1219    let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
1220    shared_file_font_bytes_from_cache(cache, path)
1221}
1222
1223#[cfg(feature = "system-fonts")]
1224fn shared_file_font_bytes_from_cache(
1225    cache_lock: &Mutex<FileFontCache>,
1226    path: &Path,
1227) -> Option<Arc<[u8]>> {
1228    let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1229    let mut cache = match cache_lock.lock() {
1230        Ok(cache) => cache,
1231        Err(poisoned) => {
1232            let mut cache = poisoned.into_inner();
1233            cache.clear();
1234            cache_lock.clear_poison();
1235            cache
1236        }
1237    };
1238    if let Some(index) = cache
1239        .entries
1240        .iter()
1241        .position(|(candidate, _, _)| candidate == &identity)
1242    {
1243        let entry = cache.entries.remove(index).expect("cache index exists");
1244        let bytes = Arc::clone(&entry.1);
1245        cache.entries.push_back(entry);
1246        return Some(bytes);
1247    }
1248
1249    let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
1250    cache_file_font_bytes(&mut cache, identity, bytes)
1251}
1252
1253#[cfg(feature = "system-fonts")]
1254fn cache_file_font_bytes(
1255    cache: &mut FileFontCache,
1256    identity: PathBuf,
1257    bytes: Arc<[u8]>,
1258) -> Option<Arc<[u8]>> {
1259    let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
1260        .saturating_add(identity.as_os_str().len())
1261        .saturating_add(bytes.len());
1262    if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
1263        while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
1264            || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
1265        {
1266            let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
1267                break;
1268            };
1269            cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
1270        }
1271        cache.bytes += entry_bytes;
1272        cache
1273            .entries
1274            .push_back((identity, Arc::clone(&bytes), entry_bytes));
1275    }
1276    Some(bytes)
1277}
1278
1279/// Map common Word font names to metric-compatible alternatives.
1280/// Returns a list of candidate names to try (including the original).
1281///
1282/// Priority: original font → metric-compatible open-source clone → generic fallback.
1283/// Carlito is metric-compatible with Calibri, Caladea with Cambria,
1284/// Liberation Sans/Serif/Mono with Arial/Times New Roman/Courier New.
1285fn map_font_name(name: &str) -> &[&str] {
1286    match name {
1287        "Calibri" => &["Calibri", "Carlito"],
1288        "Calibri Light" => &["Calibri Light", "Carlito"],
1289        "Cambria" => &["Cambria", "Caladea"],
1290        "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
1291        "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
1292        "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
1293        "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
1294        "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
1295        "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
1296        "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
1297        "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
1298        "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
1299        "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
1300        "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
1301        "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
1302        "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
1303        "Impact" => &["Impact", "Liberation Sans", "Arial"],
1304        "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
1305        "Symbol" => &["Symbol", "DejaVu Sans"],
1306        "Wingdings" => &["Wingdings", "Symbol"],
1307        _ => &[],
1308    }
1309}
1310
1311#[cfg(test)]
1312mod tests {
1313    use super::*;
1314    use crate::bundled_fonts::bundled_font_data;
1315
1316    #[test]
1317    fn caller_font_labels_resolve_after_exact_embedded_families() {
1318        let caladea = bundled_font_data()
1319            .into_iter()
1320            .find(|(family, _)| *family == "Caladea")
1321            .expect("Caladea is bundled")
1322            .1;
1323        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1324        manager.load_additional_fonts(&[
1325            FontFile {
1326                family: "Document Serif".to_owned(),
1327                data: caladea.to_vec(),
1328            },
1329            FontFile {
1330                family: "Carlito".to_owned(),
1331                data: caladea.to_vec(),
1332            },
1333        ]);
1334
1335        let aliased = manager
1336            .resolve_font(Some("Document Serif"), false, false)
1337            .expect("caller label resolves");
1338        assert_eq!(
1339            manager.fonts[manager.index_of(aliased).unwrap()].family,
1340            "Caladea"
1341        );
1342
1343        let exact = manager
1344            .resolve_font(Some("Carlito"), false, false)
1345            .expect("embedded family resolves exactly");
1346        assert_eq!(
1347            manager.fonts[manager.index_of(exact).unwrap()].family,
1348            "Carlito"
1349        );
1350
1351        manager.set_caller_aliases(&[("Document Serif".to_owned(), "Carlito".to_owned())]);
1352        let explicit = manager
1353            .resolve_font(Some("Document Serif"), false, false)
1354            .expect("explicit alias precedes label-derived alias");
1355        assert_eq!(
1356            manager.fonts[manager.index_of(explicit).unwrap()].family,
1357            "Carlito"
1358        );
1359
1360        manager.load_additional_fonts(&[FontFile {
1361            family: "Caladea".to_owned(),
1362            data: caladea.to_vec(),
1363        }]);
1364        assert!(manager.caller_aliases.is_empty());
1365
1366        manager.set_caller_aliases(&[("Arial".to_owned(), "Caladea".to_owned())]);
1367        let caller_alias = manager
1368            .resolve_font(Some("Arial"), false, false)
1369            .expect("explicit alias resolves before mapped fallback");
1370        assert_eq!(
1371            manager.fonts[manager.index_of(caller_alias).unwrap()].family,
1372            "Caladea"
1373        );
1374        let generic = manager
1375            .resolve_font(Some("Unmapped Document Family"), false, false)
1376            .expect("generic fallback remains available");
1377        assert_eq!(
1378            manager.fonts[manager.index_of(generic).unwrap()].family,
1379            "Carlito"
1380        );
1381    }
1382
1383    #[test]
1384    fn caller_alias_updates_preserve_bytes_and_invalidate_resolution_state() {
1385        let caladea = bundled_font_data()
1386            .into_iter()
1387            .find(|(family, _)| *family == "Caladea")
1388            .expect("Caladea is bundled")
1389            .1;
1390        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1391        manager.load_additional_fonts(&[FontFile {
1392            family: "Caladea".to_owned(),
1393            data: caladea.to_vec(),
1394        }]);
1395        let aliases = vec![
1396            ("Document Serif A".to_owned(), "Caladea".to_owned()),
1397            ("Document Serif B".to_owned(), "Caladea".to_owned()),
1398        ];
1399        assert!(manager.set_caller_aliases(&aliases));
1400
1401        let first = manager
1402            .resolve_font(Some("Document Serif A"), false, false)
1403            .expect("first alias resolves");
1404        let second = manager
1405            .resolve_font(Some("Document Serif B"), false, false)
1406            .expect("second alias resolves");
1407        let first_data = &manager.fonts[manager.index_of(first).unwrap()].data;
1408        let second_data = &manager.fonts[manager.index_of(second).unwrap()].data;
1409        assert!(Arc::ptr_eq(first_data, second_data));
1410        assert!(!manager.set_caller_aliases(&aliases));
1411
1412        assert!(
1413            manager.set_caller_aliases(&[("Document Serif A".to_owned(), "Carlito".to_owned(),)])
1414        );
1415        let changed = manager
1416            .resolve_font(Some("Document Serif A"), false, false)
1417            .expect("changed alias resolves");
1418        assert_eq!(
1419            manager.fonts[manager.index_of(changed).unwrap()].family,
1420            "Carlito"
1421        );
1422        assert!(
1423            manager.index_of(first).is_some(),
1424            "loaded faces are retained"
1425        );
1426    }
1427
1428    #[test]
1429    fn explicit_alias_state_respects_entry_and_retained_byte_ceilings() {
1430        let aliases = (0..CALLER_ALIAS_MAX_ENTRIES + 32)
1431            .map(|index| (format!("Document Serif {index}"), "Caladea".to_owned()))
1432            .collect::<Vec<_>>();
1433        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1434        manager.set_caller_aliases(&aliases);
1435        assert_eq!(
1436            manager.explicit_aliases.as_slice(),
1437            &aliases[..CALLER_ALIAS_MAX_ENTRIES]
1438        );
1439        assert!(manager.explicit_aliases.len() <= CALLER_ALIAS_MAX_ENTRIES);
1440        assert!(manager.explicit_alias_map.len() <= CALLER_ALIAS_MAX_ENTRIES);
1441
1442        let retained_large = ("x".repeat(32_760), String::new());
1443        let byte_limited = vec![
1444            retained_large.clone(),
1445            ("discarded bytes".to_owned(), "Caladea".to_owned()),
1446        ];
1447        manager.set_caller_aliases(&byte_limited);
1448        assert_eq!(manager.explicit_aliases, vec![retained_large]);
1449
1450        let oversized = vec![("x".repeat(40_000), "Caladea".to_owned())];
1451        manager.set_caller_aliases(&oversized);
1452        let retained_bytes = manager
1453            .explicit_aliases
1454            .iter()
1455            .map(|(requested, target)| requested.len() + target.len())
1456            .sum::<usize>()
1457            + manager
1458                .explicit_alias_map
1459                .iter()
1460                .map(|(requested, target)| requested.len() + target.len())
1461                .sum::<usize>();
1462        assert!(retained_bytes <= CALLER_ALIAS_MAX_RETAINED_BYTES);
1463        assert!(manager.explicit_aliases.is_empty());
1464        assert!(manager.explicit_alias_map.is_empty());
1465    }
1466
1467    #[test]
1468    fn label_alias_prefers_caller_bytes_over_bundled_same_family() {
1469        let bundled = bundled_font_data()
1470            .into_iter()
1471            .find(|(family, _)| *family == "Caladea")
1472            .expect("Caladea is bundled")
1473            .1;
1474        let mut caller = bundled.to_vec();
1475        caller.push(0);
1476        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1477        manager.load_additional_fonts(&[FontFile {
1478            family: "Document Serif".to_owned(),
1479            data: caller.clone(),
1480        }]);
1481
1482        let resolved = manager
1483            .resolve_font(Some("Document Serif"), false, false)
1484            .expect("caller label resolves");
1485        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
1486        assert_eq!(loaded.family, "Caladea");
1487        assert_eq!(loaded.data.as_ref(), caller.as_slice());
1488        assert_ne!(loaded.data.as_ref(), bundled);
1489    }
1490
1491    #[test]
1492    fn case_only_caller_labels_resolve_to_the_supplied_face() {
1493        let bundled = bundled_font_data()
1494            .into_iter()
1495            .find(|(family, _)| *family == "Caladea")
1496            .expect("Caladea is bundled")
1497            .1;
1498        let mut caller = bundled.to_vec();
1499        caller.push(0);
1500        let mut manager = FontManager::new_deterministic().expect("bundled fonts load");
1501        manager.load_additional_fonts(&[FontFile {
1502            family: "caladea".to_owned(),
1503            data: caller.clone(),
1504        }]);
1505
1506        let resolved = manager
1507            .resolve_font(Some("caladea"), false, false)
1508            .expect("case-only caller label resolves");
1509        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
1510        assert_eq!(loaded.family, "Caladea");
1511        assert_eq!(loaded.data.as_ref(), caller.as_slice());
1512    }
1513
1514    #[test]
1515    fn constructor_label_alias_survives_additional_font_replacement() {
1516        let caladea = bundled_font_data()
1517            .into_iter()
1518            .find(|(family, _)| *family == "Caladea")
1519            .expect("Caladea is bundled")
1520            .1;
1521        let carlito = bundled_font_data()
1522            .into_iter()
1523            .find(|(family, _)| *family == "Carlito")
1524            .expect("Carlito is bundled")
1525            .1;
1526        let mut constructor = caladea.to_vec();
1527        constructor.push(0);
1528        let mut manager =
1529            FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
1530
1531        manager.load_additional_fonts(&[FontFile {
1532            family: "Additional Sans".to_owned(),
1533            data: carlito.to_vec(),
1534        }]);
1535
1536        let resolved = manager
1537            .resolve_font(Some("Document Serif"), false, false)
1538            .expect("constructor label still resolves");
1539        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
1540        assert_eq!(loaded.family, "Caladea");
1541        assert_eq!(loaded.data.as_ref(), constructor.as_slice());
1542    }
1543
1544    #[test]
1545    fn constructor_family_priority_survives_additional_font_replacement() {
1546        let caladea = bundled_font_data()
1547            .into_iter()
1548            .find(|(family, _)| *family == "Caladea")
1549            .expect("Caladea is bundled")
1550            .1;
1551        let mut constructor = caladea.to_vec();
1552        constructor.push(0);
1553        let mut replacement = caladea.to_vec();
1554        replacement.extend_from_slice(&[0, 0]);
1555        let mut manager =
1556            FontManager::new_with_fonts(vec![("Document Serif".to_owned(), constructor.clone())]);
1557
1558        manager.load_additional_fonts(&[FontFile {
1559            family: "Caladea".to_owned(),
1560            data: replacement,
1561        }]);
1562
1563        let resolved = manager
1564            .resolve_font(Some("Caladea"), false, false)
1565            .expect("constructor family still resolves");
1566        let loaded = &manager.fonts[manager.index_of(resolved).unwrap()];
1567        assert_eq!(loaded.data.as_ref(), constructor.as_slice());
1568    }
1569
1570    #[test]
1571    fn caller_face_selection_matches_fontdb_css_rules() {
1572        let caladea = bundled_font_data()
1573            .into_iter()
1574            .find(|(family, _)| *family == "Caladea")
1575            .expect("Caladea is bundled")
1576            .1;
1577        let mut source = fontdb::Database::new();
1578        source.load_font_data(caladea.to_vec());
1579        let template = source.faces().next().expect("Caladea has a face").clone();
1580        let mut db = fontdb::Database::new();
1581        let mut add_face = |weight: u16, stretch: fontdb::Stretch, style: fontdb::Style| {
1582            let mut face = template.clone();
1583            face.weight = fontdb::Weight(weight);
1584            face.stretch = stretch;
1585            face.style = style;
1586            db.push_face_info(face)
1587        };
1588
1589        let weight_300 = add_face(300, fontdb::Stretch::Normal, fontdb::Style::Normal);
1590        let weight_500 = add_face(500, fontdb::Stretch::Normal, fontdb::Style::Normal);
1591        let weight_600 = add_face(600, fontdb::Stretch::Normal, fontdb::Style::Normal);
1592        let weight_800 = add_face(800, fontdb::Stretch::Normal, fontdb::Style::Normal);
1593        let expanded = add_face(400, fontdb::Stretch::SemiExpanded, fontdb::Style::Normal);
1594        let condensed = add_face(400, fontdb::Stretch::SemiCondensed, fontdb::Style::Normal);
1595        let normal = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Normal);
1596        let italic = add_face(400, fontdb::Stretch::Normal, fontdb::Style::Italic);
1597
1598        assert_eq!(
1599            best_caller_face(
1600                &db,
1601                &[weight_300, weight_500],
1602                fontdb::Weight::NORMAL,
1603                fontdb::Style::Normal,
1604            ),
1605            Some(weight_500)
1606        );
1607        assert_eq!(
1608            best_caller_face(
1609                &db,
1610                &[weight_600, weight_800],
1611                fontdb::Weight::BOLD,
1612                fontdb::Style::Normal,
1613            ),
1614            Some(weight_800)
1615        );
1616        assert_eq!(
1617            best_caller_face(
1618                &db,
1619                &[expanded, condensed],
1620                fontdb::Weight::NORMAL,
1621                fontdb::Style::Normal,
1622            ),
1623            Some(condensed)
1624        );
1625        assert_eq!(
1626            best_caller_face(
1627                &db,
1628                &[normal, italic],
1629                fontdb::Weight::NORMAL,
1630                fontdb::Style::Oblique,
1631            ),
1632            Some(italic)
1633        );
1634    }
1635
1636    fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
1637        assert_eq!(family.len(), 7);
1638        let mut font = source.to_vec();
1639        let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
1640        let name_offset = (0..table_count)
1641            .find_map(|table| {
1642                let record = 12 + table * 16;
1643                (&font[record..record + 4] == b"name").then(|| {
1644                    u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
1645                })
1646            })
1647            .expect("font has name table");
1648        let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
1649        let strings = name_offset
1650            + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
1651        for index in 0..count {
1652            let record = name_offset + 6 + index * 12;
1653            let platform = u16::from_be_bytes([font[record], font[record + 1]]);
1654            let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
1655            let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
1656            let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
1657            if !matches!(name_id, 1 | 16) {
1658                continue;
1659            }
1660            let destination = &mut font[strings + offset..strings + offset + length];
1661            match (platform, length) {
1662                (0 | 3, 14) => {
1663                    for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
1664                        bytes.copy_from_slice(&(ch as u16).to_be_bytes());
1665                    }
1666                }
1667                (1, 7) => destination.copy_from_slice(family.as_bytes()),
1668                _ => {}
1669            }
1670        }
1671        font
1672    }
1673
1674    #[cfg(feature = "system-fonts")]
1675    fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
1676        let header_len = 12 + fonts.len() * 4;
1677        let mut collection = vec![0u8; header_len];
1678        collection[0..4].copy_from_slice(b"ttcf");
1679        collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
1680        collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
1681
1682        for (font_number, font) in fonts.iter().enumerate() {
1683            while !collection.len().is_multiple_of(4) {
1684                collection.push(0);
1685            }
1686            let collection_offset = collection.len();
1687            collection[12 + font_number * 4..16 + font_number * 4]
1688                .copy_from_slice(&(collection_offset as u32).to_be_bytes());
1689
1690            let mut adjusted = font.to_vec();
1691            let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
1692            for table in 0..table_count {
1693                let offset_position = 12 + table * 16 + 8;
1694                let offset = u32::from_be_bytes(
1695                    adjusted[offset_position..offset_position + 4]
1696                        .try_into()
1697                        .expect("table offset"),
1698                );
1699                adjusted[offset_position..offset_position + 4]
1700                    .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
1701            }
1702            collection.extend_from_slice(&adjusted);
1703        }
1704        collection
1705    }
1706
1707    #[test]
1708    fn deterministic_font_manager_uses_only_bundled_fonts() {
1709        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1710
1711        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1712        assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
1713    }
1714
1715    #[cfg(feature = "system-fonts")]
1716    #[test]
1717    fn normal_font_discovery_initializes_once_per_process() {
1718        let _first = FontManager::new();
1719        let _second = FontManager::new();
1720        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1721
1722        let _deterministic =
1723            FontManager::new_deterministic().expect("bundled font manager should load");
1724        let _caller = FontManager::new_with_fonts(Vec::new());
1725        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1726    }
1727
1728    #[cfg(feature = "system-fonts")]
1729    #[test]
1730    fn file_backed_collection_faces_share_one_byte_buffer() {
1731        let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
1732        let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
1733        let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
1734        let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
1735        std::fs::write(&first_path, &collection).expect("write first temporary collection");
1736        std::fs::write(&second_path, &collection).expect("write second temporary collection");
1737
1738        let mut db = fontdb::Database::new();
1739        db.load_font_file(&first_path).expect("load first TTC");
1740        db.load_font_file(&second_path).expect("load second TTC");
1741        let canonical_first = std::fs::canonicalize(&first_path).unwrap();
1742        let canonical_second = std::fs::canonicalize(&second_path).unwrap();
1743        let first_ids = db
1744            .faces()
1745            .filter_map(|face| match &face.source {
1746                fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
1747                    Some(face.id)
1748                }
1749                _ => None,
1750            })
1751            .collect::<Vec<_>>();
1752        let second_id = db
1753            .faces()
1754            .find_map(|face| match &face.source {
1755                fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
1756                    Some(face.id)
1757                }
1758                _ => None,
1759            })
1760            .expect("second TTC face");
1761        assert_eq!(first_ids.len(), 2);
1762
1763        let mut memory = HashMap::new();
1764        let (first_face, first_index) =
1765            font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
1766        let (second_face, second_index) =
1767            font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
1768        let (other_file, _) =
1769            font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
1770        assert_ne!(first_index, second_index);
1771        assert!(Arc::ptr_eq(&first_face, &second_face));
1772        assert!(!Arc::ptr_eq(&first_face, &other_file));
1773
1774        std::fs::remove_file(first_path).expect("remove first temporary font");
1775        std::fs::remove_file(second_path).expect("remove second temporary font");
1776    }
1777
1778    #[test]
1779    fn shaping_memo_uses_complete_text_size_and_font_identity() {
1780        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1781        let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1782        let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
1783
1784        let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
1785        let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
1786        assert_eq!(first.glyph_ids, repeat.glyph_ids);
1787        assert_eq!(fm.shaping_memo_counts().0, 1);
1788
1789        fm.shape_text(regular, "different text", 11.0).unwrap();
1790        fm.shape_text(regular, "exact text", 12.0).unwrap();
1791        fm.shape_text(bold, "exact text", 11.0).unwrap();
1792        assert_eq!(fm.shaping_memo_counts().1, 4);
1793
1794        let replacement = FontFile {
1795            family: "Carlito".to_owned(),
1796            data: bundled_font_data()[1].1.to_vec(),
1797        };
1798        fm.load_additional_fonts(&[replacement]);
1799        assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
1800        let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1801        fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
1802        assert_eq!(fm.shaping_memo_counts().1, 1);
1803    }
1804
1805    #[test]
1806    fn shaping_memo_hits_preserve_fifo_order() {
1807        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1808        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1809        fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
1810        fm.shape_text(font, "newest exact shape", 11.0).unwrap();
1811        fm.shape_text(font, "oldest exact shape", 11.0).unwrap();
1812
1813        let memo = fm.shaping_memo.lock().unwrap();
1814        assert_eq!(memo.hits, 1);
1815        assert_eq!(memo.entries.front().unwrap().0.text, "oldest exact shape");
1816        assert_eq!(memo.entries.back().unwrap().0.text, "newest exact shape");
1817    }
1818
1819    #[test]
1820    fn shaping_memo_fingerprint_collision_requires_exact_key_equality() {
1821        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1822        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1823        fm.shape_text(font, "first collision candidate", 11.0)
1824            .unwrap();
1825        let forced = shaping_fingerprint(font, "second collision candidate", 11.0f64.to_bits());
1826        fm.shaping_memo.lock().unwrap().entries[0].3 = forced;
1827
1828        fm.shape_text(font, "second collision candidate", 11.0)
1829            .unwrap();
1830
1831        let (hits, misses, entries, _) = fm.shaping_memo_counts();
1832        assert_eq!((hits, misses, entries), (0, 2, 2));
1833    }
1834
1835    #[test]
1836    fn shaping_memo_is_bounded_and_recovers_from_poison() {
1837        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1838        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1839        for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
1840            fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
1841                .unwrap();
1842        }
1843        let (_, _, entries, bytes) = fm.shaping_memo_counts();
1844        assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
1845        assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
1846
1847        let fm = Arc::new(fm);
1848        let poison = Arc::clone(&fm);
1849        assert!(
1850            std::thread::spawn(move || {
1851                let _guard = poison.shaping_memo.lock().unwrap();
1852                panic!("poison shaping cache for recovery coverage");
1853            })
1854            .join()
1855            .is_err()
1856        );
1857        let first = fm.shape_text(font, "after poison", 11.0).unwrap();
1858        let second = fm.shape_text(font, "after poison", 11.0).unwrap();
1859        assert_eq!(first.glyph_ids, second.glyph_ids);
1860        let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
1861        assert_eq!((hits, misses, entries), (1, 1, 1));
1862        assert!(bytes > 0);
1863    }
1864
1865    #[test]
1866    fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
1867        let mut memo = ShapingMemo::new();
1868        for suffix in ['a', 'b'] {
1869            memo.insert(
1870                ShapingKey {
1871                    font_id: FontId(0),
1872                    text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
1873                    size_bits: 11.0f64.to_bits(),
1874                },
1875                ShapedText {
1876                    glyph_ids: Vec::new(),
1877                    advances: Vec::new(),
1878                    width: 0.0,
1879                },
1880            );
1881        }
1882        assert_eq!(memo.entries.len(), 1);
1883        assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
1884    }
1885
1886    #[test]
1887    fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
1888        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1889        for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
1890            fm.remember_coverage_fallback(false, false, 0);
1891        }
1892        assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
1893
1894        let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
1895            .filter_map(|value| char::from_u32(0x10_000 + value as u32))
1896            .collect::<Vec<_>>();
1897        fm.remember_coverage_misses(&misses);
1898        assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
1899
1900        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1901            fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
1902                .expect("bounded fallback resolves");
1903        }
1904        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1905        assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
1906    }
1907
1908    #[test]
1909    fn active_document_may_resolve_more_than_256_distinct_faces() {
1910        let source = bundled_font_data()[4].1;
1911        let mut db = fontdb::Database::new();
1912        for index in 0..257 {
1913            db.load_font_data(font_with_family(source, &format!("F{index:06}")));
1914        }
1915        let mut fm = FontManager::from_base_database(db);
1916        fm.begin_layout();
1917        let mut ids = HashSet::new();
1918        for index in 0..257 {
1919            let family = format!("F{index:06}");
1920            let id = fm
1921                .resolve_font(Some(&family), false, false)
1922                .expect("distinct active face resolves");
1923            assert_eq!(fm.font_data(id).unwrap().family, family);
1924            ids.insert(id);
1925        }
1926        assert_eq!(ids.len(), 257);
1927        fm.retain_current_fonts();
1928        assert_eq!(fm.fonts.len(), 257);
1929    }
1930
1931    #[test]
1932    fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
1933        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1934        fm.begin_layout();
1935        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1936            fm.resolve_font(Some("Carlito"), false, false).unwrap();
1937        }
1938        assert!(fm.paragraph_font_trace.is_none());
1939
1940        fm.begin_paragraph_font_trace();
1941        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1942            fm.resolve_font(Some("Carlito"), false, false).unwrap();
1943        }
1944        assert!(fm.finish_paragraph_font_trace().is_none());
1945
1946        fm.begin_layout();
1947        assert_eq!(fm.layout_fonts.capacity(), 0);
1948        fm.begin_paragraph_font_trace();
1949        fm.resolve_font(Some("Carlito"), false, false).unwrap();
1950        let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
1951        assert_eq!(trace.len(), 1);
1952        assert_eq!(trace.capacity(), trace.len());
1953    }
1954
1955    #[cfg(feature = "system-fonts")]
1956    #[test]
1957    fn file_byte_cache_is_bounded_and_recovers_from_poison() {
1958        let cache = Arc::new(Mutex::new(FileFontCache::new()));
1959        {
1960            let mut cache = cache
1961                .lock()
1962                .unwrap_or_else(std::sync::PoisonError::into_inner);
1963            cache.clear();
1964            let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
1965            let returned = cache_file_font_bytes(
1966                &mut cache,
1967                PathBuf::from("oversized-font.ttc"),
1968                Arc::clone(&oversized),
1969            )
1970            .expect("oversized bytes are returned uncached");
1971            assert!(Arc::ptr_eq(&oversized, &returned));
1972            assert!(cache.entries.is_empty());
1973            assert_eq!(cache.bytes, 0);
1974        }
1975
1976        let poison = Arc::clone(&cache);
1977        assert!(
1978            std::thread::spawn(move || {
1979                let cache = poison;
1980                let _guard = cache.lock().unwrap();
1981                panic!("poison file byte cache for recovery coverage");
1982            })
1983            .join()
1984            .is_err()
1985        );
1986
1987        let path = std::env::temp_dir().join(format!(
1988            "rdocx-font-cache-poison-{}-{:?}.ttf",
1989            std::process::id(),
1990            std::thread::current().id()
1991        ));
1992        std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
1993        let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
1994        let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
1995        assert!(Arc::ptr_eq(&first, &second));
1996        std::fs::remove_file(path).expect("remove recovery font");
1997    }
1998
1999    #[cfg(not(feature = "system-fonts"))]
2000    #[test]
2001    fn no_default_features_omits_system_font_discovery() {
2002        let fm = FontManager::new();
2003        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
2004    }
2005
2006    #[test]
2007    fn font_manager_with_no_fonts_returns_an_error() {
2008        let mut fm = FontManager::new_with_fonts(Vec::new());
2009        assert!(matches!(
2010            fm.resolve_font(None, false, false),
2011            Err(LayoutError::FontNotFound(_))
2012        ));
2013    }
2014
2015    #[test]
2016    fn load_system_font() {
2017        let mut fm = FontManager::new();
2018        // Should be able to resolve at least one font via fallback
2019        let result = fm.resolve_font(None, false, false);
2020        // On CI or systems without fonts this might fail, so we just check it doesn't panic
2021        if let Ok(id) = result {
2022            assert_eq!(id.0, 0);
2023        }
2024    }
2025
2026    #[test]
2027    fn font_metrics_positive() {
2028        let mut fm = FontManager::new();
2029        if let Ok(id) = fm.resolve_font(None, false, false) {
2030            let metrics = fm.metrics(id, 12.0).unwrap();
2031            assert!(metrics.ascent > 0.0);
2032            assert!(metrics.descent > 0.0);
2033            assert!(metrics.units_per_em > 0);
2034        }
2035    }
2036
2037    #[test]
2038    fn shape_hello_world() {
2039        let mut fm = FontManager::new();
2040        if let Ok(id) = fm.resolve_font(None, false, false) {
2041            let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
2042            assert!(!shaped.glyph_ids.is_empty());
2043            assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
2044            assert!(shaped.width > 0.0);
2045        }
2046    }
2047
2048    #[test]
2049    fn font_caching() {
2050        let mut fm = FontManager::new();
2051        if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
2052            let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
2053            assert_eq!(id1, id2);
2054        }
2055    }
2056
2057    #[test]
2058    fn font_resolution_alias_cache_is_bounded() {
2059        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2060        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
2061            fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
2062                .expect("fallback font resolves");
2063        }
2064        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2065        assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
2066    }
2067
2068    #[test]
2069    fn bold_italic_variants() {
2070        let mut fm = FontManager::new();
2071        let regular = fm.resolve_font(None, false, false);
2072        let bold = fm.resolve_font(None, true, false);
2073        if let (Ok(r), Ok(b)) = (regular, bold) {
2074            // Bold should get a different font ID (different variant)
2075            assert_ne!(r, b);
2076        }
2077    }
2078
2079    /// Latin text must resolve exactly as it did before, so the coverage check
2080    /// cannot disturb the overwhelmingly common case.
2081    #[test]
2082    fn latin_text_resolves_the_same_as_by_name() {
2083        let mut fm = FontManager::new();
2084        let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
2085            return;
2086        };
2087        let for_text = fm
2088            .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
2089            .unwrap();
2090        assert_eq!(by_name, for_text);
2091    }
2092
2093    /// Text nothing can draw must keep the requested font rather than failing.
2094    ///
2095    /// The bundled fonts have no CJK coverage, so in deterministic mode the
2096    /// search is guaranteed to come up empty. The text still needs a font so
2097    /// it occupies the right space.
2098    #[test]
2099    fn text_no_font_can_draw_keeps_the_requested_font() {
2100        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2101        let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2102        let resolved = fm
2103            .resolve_font_for_text(Some("Carlito"), false, false, "这是中文")
2104            .unwrap();
2105        assert_eq!(
2106            primary, resolved,
2107            "with no covering font available the original must be kept"
2108        );
2109    }
2110
2111    /// Whitespace absent from a font is not a reason to go hunting for another.
2112    #[test]
2113    fn whitespace_does_not_trigger_a_fallback() {
2114        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
2115        let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
2116        let idx = fm.index_of(by_name).unwrap();
2117        // A non-breaking space and a tab, neither of which every face carries.
2118        assert!(
2119            fm.uncovered(idx, "a\u{00a0}b\tc")
2120                .iter()
2121                .all(|c| *c != '\t'),
2122            "control and whitespace characters must be ignored"
2123        );
2124    }
2125
2126    /// When the machine does have a CJK font, CJK text must not keep a Latin
2127    /// font that cannot draw it.
2128    ///
2129    /// Skipped where no such font is installed, which is why it asserts
2130    /// nothing about which font is chosen.
2131    #[test]
2132    fn cjk_text_moves_off_a_latin_font_when_possible() {
2133        let mut fm = FontManager::new();
2134        let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
2135            return;
2136        };
2137        let Some(idx) = fm.index_of(latin) else {
2138            return;
2139        };
2140        if fm.uncovered(idx, "这是中文").is_empty() {
2141            return; // that font somehow covers it, nothing to prove
2142        }
2143        let resolved = fm
2144            .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
2145            .unwrap();
2146        if resolved == latin {
2147            return; // no covering font installed on this machine
2148        }
2149        let new_idx = fm.index_of(resolved).unwrap();
2150        assert!(
2151            fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
2152            "the replacement must cover more of the text than the original"
2153        );
2154    }
2155}