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)>,
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)>()
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        self.entries.push_back((key, shaped, entry_bytes));
116    }
117}
118
119const SHAPING_CACHE_MAX_ENTRIES: usize = 2_048;
120const SHAPING_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
121
122#[cfg(feature = "system-fonts")]
123struct FileFontCache {
124    entries: VecDeque<(PathBuf, Arc<[u8]>, usize)>,
125    bytes: usize,
126}
127
128#[cfg(feature = "system-fonts")]
129impl FileFontCache {
130    fn new() -> Self {
131        Self {
132            entries: VecDeque::new(),
133            bytes: 0,
134        }
135    }
136
137    fn clear(&mut self) {
138        self.entries.clear();
139        self.bytes = 0;
140    }
141}
142
143#[cfg(feature = "system-fonts")]
144const FILE_FONT_CACHE_MAX_ENTRIES: usize = 256;
145#[cfg(feature = "system-fonts")]
146const FILE_FONT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
147
148#[cfg(feature = "system-fonts")]
149static NORMAL_FONT_DATABASE: OnceLock<fontdb::Database> = OnceLock::new();
150#[cfg(feature = "system-fonts")]
151static FILE_FONT_CACHE: OnceLock<Mutex<FileFontCache>> = OnceLock::new();
152#[cfg(all(test, feature = "system-fonts"))]
153static SYSTEM_FONT_DISCOVERY_RUNS: AtomicUsize = AtomicUsize::new(0);
154
155/// Internal record for a loaded font face.
156struct LoadedFont {
157    db_id: fontdb::ID,
158    id: FontId,
159    family: String,
160    bold: bool,
161    italic: bool,
162    data: Arc<[u8]>,
163    face_index: u32,
164    units_per_em: u16,
165    /// Vertical metrics in design units, read once when the face is loaded.
166    ascender: i16,
167    descender: i16,
168    line_gap: i16,
169    /// HarfRust's per-face shaping caches. Building these is the expensive
170    /// part of shaping, so it happens once per face instead of once per run.
171    shaper_data: harfrust::ShaperData,
172}
173
174struct ParagraphFontTrace {
175    ids: Vec<FontId>,
176    overflowed: bool,
177}
178
179/// Manages font discovery, loading, shaping, and metrics.
180pub struct FontManager {
181    db: fontdb::Database,
182    /// Database before document-embedded or caller fonts are applied.
183    base_db: fontdb::Database,
184    /// Map from FontKey to loaded font info.
185    cache: HashMap<FontKey, usize>,
186    /// Manager-owned bytes for bundled, embedded, and caller-provided faces.
187    memory_face_data: HashMap<fontdb::ID, Arc<[u8]>>,
188    /// All loaded fonts.
189    fonts: Vec<LoadedFont>,
190    /// Next font ID counter.
191    next_id: u32,
192    /// Fonts already discovered as covering something the requested family
193    /// could not, keyed by (bold, italic).
194    ///
195    /// Finding a font that covers a character means loading and inspecting
196    /// faces, which is far too slow to repeat per character. Once a CJK face
197    /// has been found for one character it almost always covers the rest of
198    /// the run, so it is tried first next time.
199    coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
200    /// Characters already searched for and not found in any available font, so
201    /// the scan is not repeated for every occurrence.
202    coverage_misses: HashSet<char>,
203    /// Exact additional font set currently loaded into `db`.
204    additional_fonts: Vec<FontFile>,
205    /// Bounded exact-key shaping results.
206    shaping_memo: Mutex<ShapingMemo>,
207    /// Exact resolution events for one cache-candidate paragraph.
208    paragraph_font_trace: Option<ParagraphFontTrace>,
209    /// Distinct current-layout fonts in first-resolution order.
210    layout_fonts: Vec<FontId>,
211}
212
213/// Families with broad non-Latin coverage, tried before scanning everything.
214///
215/// Ordered roughly by how likely each is to be installed. This is only a fast
216/// path: if none of them is present the full font database is still searched.
217const BROAD_COVERAGE_FAMILIES: &[&str] = &[
218    // Bundled with or shipped alongside many Linux distributions
219    "Noto Sans CJK SC",
220    "Noto Sans CJK JP",
221    "Noto Sans CJK KR",
222    "Noto Sans CJK TC",
223    "Noto Serif CJK SC",
224    "Source Han Sans SC",
225    "WenQuanYi Zen Hei",
226    "WenQuanYi Micro Hei",
227    // macOS
228    "PingFang SC",
229    "PingFang TC",
230    "Hiragino Sans",
231    "Hiragino Kaku Gothic ProN",
232    "Apple SD Gothic Neo",
233    "Songti SC",
234    "STHeiti",
235    // Windows
236    "Microsoft YaHei",
237    "Microsoft JhengHei",
238    "SimSun",
239    "SimHei",
240    "NSimSun",
241    "Yu Gothic",
242    "MS Gothic",
243    "Meiryo",
244    "Malgun Gothic",
245    // Wide-coverage generalists
246    "Arial Unicode MS",
247    "DejaVu Sans",
248];
249
250const RESOLUTION_CACHE_MAX_ENTRIES: usize = 256;
251const COVERAGE_FALLBACK_MAX_ENTRIES: usize = 256;
252const COVERAGE_MISS_MAX_ENTRIES: usize = 4_096;
253const PARAGRAPH_FONT_TRACE_MAX_ENTRIES: usize = 4_096;
254
255impl Default for FontManager {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261impl FontManager {
262    fn from_base_database(db: fontdb::Database) -> Self {
263        Self {
264            base_db: db.clone(),
265            db,
266            cache: HashMap::new(),
267            memory_face_data: HashMap::new(),
268            fonts: Vec::new(),
269            next_id: 0,
270            coverage_fallbacks: HashMap::new(),
271            coverage_misses: HashSet::new(),
272            additional_fonts: Vec::new(),
273            shaping_memo: Mutex::new(ShapingMemo::new()),
274            paragraph_font_trace: None,
275            layout_fonts: Vec::new(),
276        }
277    }
278
279    /// Create a new FontManager and load system fonts.
280    ///
281    /// Bundled fonts (Carlito, Caladea, Liberation) are loaded as fallbacks.
282    /// System fonts are discovered when the `system-fonts` feature is enabled.
283    pub fn new() -> Self {
284        #[cfg(feature = "system-fonts")]
285        {
286            let db = NORMAL_FONT_DATABASE.get_or_init(|| {
287                let mut db = bundled_font_database();
288                db.load_system_fonts();
289                #[cfg(test)]
290                SYSTEM_FONT_DISCOVERY_RUNS.fetch_add(1, Ordering::Relaxed);
291                db
292            });
293            Self::from_base_database(db.clone())
294        }
295
296        #[cfg(not(feature = "system-fonts"))]
297        Self::from_base_database(bundled_font_database())
298    }
299
300    /// Create a font manager that loads bundled fonts without discovering
301    /// system fonts.
302    ///
303    /// This mode makes font resolution reproducible across machines.
304    pub fn new_deterministic() -> Result<Self> {
305        Ok(Self::from_base_database(bundled_font_database()))
306    }
307
308    /// Load or replace additional font files (user-provided or extracted from
309    /// an OOXML package).
310    ///
311    /// An unchanged set is a no-op so a reusable engine retains resolution and
312    /// shaping state. A changed set rebuilds from the isolated base database,
313    /// which prevents stale face ids and bounds repeated document edits.
314    pub fn load_additional_fonts(&mut self, font_files: &[FontFile]) -> bool {
315        if self.additional_fonts == font_files {
316            return false;
317        }
318
319        self.db = self.base_db.clone();
320        for font_file in font_files {
321            self.db.load_font_data(font_file.data.clone());
322        }
323        self.cache.clear();
324        self.memory_face_data.clear();
325        self.fonts.clear();
326        self.next_id = 0;
327        self.coverage_fallbacks.clear();
328        self.coverage_misses.clear();
329        self.additional_fonts = font_files.to_vec();
330        if self.shaping_memo.is_poisoned() {
331            self.shaping_memo.clear_poison();
332        }
333        self.shaping_memo
334            .get_mut()
335            .expect("shaping cache poison was cleared")
336            .clear();
337        true
338    }
339
340    /// Create a FontManager with user-provided fonts (no system font loading).
341    ///
342    /// Each entry is `(family_name, font_bytes)`. This is useful in environments
343    /// where system fonts are not available, such as WASM.
344    pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
345        let mut db = fontdb::Database::new();
346        for (_name, data) in &fonts {
347            db.load_font_data(data.clone());
348        }
349        Self::from_base_database(db)
350    }
351
352    /// Begin one complete layout attempt's exact font-usage trace.
353    #[doc(hidden)]
354    pub fn begin_layout(&mut self) {
355        self.paragraph_font_trace = None;
356        self.layout_fonts = Vec::new();
357    }
358
359    /// Begin recording the exact resolution events for one cache candidate.
360    #[doc(hidden)]
361    pub fn begin_paragraph_font_trace(&mut self) {
362        self.paragraph_font_trace = Some(ParagraphFontTrace {
363            ids: Vec::new(),
364            overflowed: false,
365        });
366    }
367
368    /// Finish one bounded paragraph trace. An overflowed paragraph bypasses reuse.
369    #[doc(hidden)]
370    pub fn finish_paragraph_font_trace(&mut self) -> Option<Vec<FontId>> {
371        let mut trace = self.paragraph_font_trace.take()?;
372        if trace.overflowed {
373            return None;
374        }
375        trace.ids.shrink_to_fit();
376        Some(trace.ids)
377    }
378
379    /// Replay the exact resolution events attached to a cached paragraph.
380    #[doc(hidden)]
381    pub fn replay_layout_font_trace(&mut self, trace: &[FontId]) {
382        for &font_id in trace {
383            self.record_layout_font(font_id);
384        }
385    }
386
387    /// Distinct current-layout fonts in first-resolution order.
388    #[doc(hidden)]
389    pub fn current_layout_fonts(&self) -> &[FontId] {
390        &self.layout_fonts
391    }
392
393    /// Whether no historical loaded face is absent from this layout.
394    #[doc(hidden)]
395    pub fn every_loaded_font_is_current(&self) -> bool {
396        self.fonts
397            .iter()
398            .map(|font| font.id)
399            .eq(self.layout_fonts.iter().copied())
400            && self
401                .layout_fonts
402                .iter()
403                .enumerate()
404                .all(|(index, font_id)| *font_id == FontId(index as u32))
405    }
406
407    /// Drop faces that were loaded by an older successful layout but are no
408    /// longer active. Every face used by the current document is retained,
409    /// even when that working set contains more than the cache ceilings.
410    #[doc(hidden)]
411    pub fn retain_current_fonts(&mut self) {
412        let current = self.layout_fonts.iter().copied().collect::<HashSet<_>>();
413        let old_index_ids = self
414            .fonts
415            .iter()
416            .enumerate()
417            .map(|(index, font)| (index, font.id))
418            .collect::<HashMap<_, _>>();
419        self.fonts.retain(|font| current.contains(&font.id));
420        let current_order = self
421            .layout_fonts
422            .iter()
423            .enumerate()
424            .map(|(index, font_id)| (*font_id, index))
425            .collect::<HashMap<_, _>>();
426        self.fonts
427            .sort_by_key(|font| current_order.get(&font.id).copied().unwrap_or(usize::MAX));
428
429        let indices = self
430            .fonts
431            .iter()
432            .enumerate()
433            .map(|(index, font)| (font.id, index))
434            .collect::<HashMap<_, _>>();
435        let old_cache = std::mem::take(&mut self.cache);
436        self.cache = old_cache
437            .into_iter()
438            .filter_map(|(key, old_index)| {
439                let font_id = old_index_ids.get(&old_index)?;
440                Some((key, *indices.get(font_id)?))
441            })
442            .collect();
443        self.coverage_fallbacks.clear();
444        self.coverage_misses.clear();
445        let active_db_ids = self
446            .fonts
447            .iter()
448            .map(|font| font.db_id)
449            .collect::<HashSet<_>>();
450        self.memory_face_data
451            .retain(|db_id, _| active_db_ids.contains(db_id));
452
453        let memo = self
454            .shaping_memo
455            .get_mut()
456            .unwrap_or_else(std::sync::PoisonError::into_inner);
457        memo.entries
458            .retain(|(key, _, _)| current.contains(&key.font_id));
459        memo.bytes = memo.entries.iter().map(|(_, _, bytes)| bytes).sum();
460    }
461
462    /// Resolve a font for `text`, falling back on glyph coverage.
463    ///
464    /// `resolve_font` picks by family name alone. That is enough for Latin
465    /// text, but a run asking for a Chinese family on a machine without it
466    /// falls down the name chain and lands on a Latin font, which has no CJK
467    /// glyphs, so every character renders as a missing-glyph box. Name
468    /// matching cannot detect that, because the font it chose exists and is
469    /// perfectly valid, it simply cannot draw this text.
470    ///
471    /// So the resolved font is checked against the text, and when a character
472    /// is missing another font that can draw it is looked for.
473    ///
474    /// This is per run rather than per character: the font that covers the
475    /// first missing character is used for the whole run. Text that mixes
476    /// scripts inside one run is therefore still imperfect, but it is a large
477    /// improvement on drawing boxes.
478    pub fn resolve_font_for_text(
479        &mut self,
480        family: Option<&str>,
481        bold: bool,
482        italic: bool,
483        text: &str,
484    ) -> Result<FontId> {
485        let primary = self.resolve_font(family, bold, italic)?;
486
487        let Some(idx) = self.index_of(primary) else {
488            return Ok(primary);
489        };
490        let missing = self.uncovered(idx, text);
491        if missing.is_empty() {
492            return Ok(primary);
493        }
494
495        match self.font_covering(&missing, bold, italic) {
496            // Nothing installed can draw it. Keep the original font so the
497            // text still occupies the right space.
498            None => Ok(primary),
499            Some(id) => Ok(id),
500        }
501    }
502
503    /// The characters in `text` that the font at `idx` cannot draw.
504    ///
505    /// Whitespace and control characters are skipped: a font without a glyph
506    /// for a space is not a reason to go looking for another one.
507    fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
508        let font = &self.fonts[idx];
509        let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
510            return Vec::new();
511        };
512        let mut seen = HashSet::new();
513        text.chars()
514            .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
515            .filter(|&ch| face.glyph_index(ch).is_none())
516            .filter(|&ch| seen.insert(ch))
517            .collect()
518    }
519
520    /// Whether the font at `idx` has a glyph for `ch`.
521    fn covers(&self, idx: usize, ch: char) -> bool {
522        let font = &self.fonts[idx];
523        ttf_parser::Face::parse(&font.data, font.face_index)
524            .map(|face| face.glyph_index(ch).is_some())
525            .unwrap_or(false)
526    }
527
528    /// Find a font that can draw `missing`.
529    ///
530    /// A font covering every missing character wins. Failing that the one
531    /// covering the most is used, because a single run gets a single font and
532    /// partial coverage still beats a row of boxes. Picking on the first
533    /// missing character alone is not enough: a Japanese face may have the
534    /// characters shared with Chinese and not the simplified-only ones, so it
535    /// would look like a fix and still leave gaps.
536    fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
537        if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
538            return None;
539        }
540
541        let mut best: Option<(usize, usize)> = None; // (covered count, font index)
542        let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
543            let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
544            if covered == 0 {
545                return false;
546            }
547            if best.map(|(n, _)| covered > n).unwrap_or(true) {
548                *best = Some((covered, idx));
549            }
550            covered == missing.len()
551        };
552
553        // Fonts that already rescued an earlier run, which for a document in
554        // one script is almost always the answer again.
555        if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
556            for idx in known {
557                if consider(self, idx, &mut best) {
558                    let id = self.fonts[idx].id;
559                    self.record_layout_font(id);
560                    return Some(id);
561                }
562            }
563        }
564
565        // Families with broad coverage, then everything else the database
566        // knows about. Both go through resolve_font so loading and caching
567        // stay in one place.
568        let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
569            .iter()
570            .map(|s| s.to_string())
571            .chain(
572                self.db
573                    .faces()
574                    .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
575            )
576            .collect();
577
578        for name in candidates {
579            let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
580                continue;
581            };
582            let Some(idx) = self.index_of(id) else {
583                continue;
584            };
585            let complete = consider(self, idx, &mut best);
586            if complete {
587                self.remember_coverage_fallback(bold, italic, idx);
588                return Some(id);
589            }
590        }
591
592        match best {
593            Some((_, idx)) => {
594                self.remember_coverage_fallback(bold, italic, idx);
595                let id = self.fonts[idx].id;
596                self.record_layout_font(id);
597                Some(id)
598            }
599            None => {
600                self.remember_coverage_misses(missing);
601                None
602            }
603        }
604    }
605
606    /// Index into `fonts` for a FontId.
607    fn index_of(&self, id: FontId) -> Option<usize> {
608        self.fonts.iter().position(|f| f.id == id)
609    }
610
611    /// Resolve a font by family name, bold, and italic flags.
612    /// Returns a FontId. Uses fallback chain if the requested font is not found.
613    pub fn resolve_font(
614        &mut self,
615        family: Option<&str>,
616        bold: bool,
617        italic: bool,
618    ) -> Result<FontId> {
619        self.resolve_font_inner(family, bold, italic, true)
620    }
621
622    /// Resolve a font for metrics without claiming that it emitted glyphs.
623    #[doc(hidden)]
624    pub fn resolve_font_for_metrics(
625        &mut self,
626        family: Option<&str>,
627        bold: bool,
628        italic: bool,
629    ) -> Result<FontId> {
630        self.resolve_font_inner(family, bold, italic, false)
631    }
632
633    fn resolve_font_inner(
634        &mut self,
635        family: Option<&str>,
636        bold: bool,
637        italic: bool,
638        record_layout_use: bool,
639    ) -> Result<FontId> {
640        let family_name = family.unwrap_or("Arial");
641
642        let key = FontKey {
643            family: family_name.to_string(),
644            bold,
645            italic,
646        };
647
648        if let Some(idx) = self.cache.get(&key).copied() {
649            let id = self.fonts[idx].id;
650            if record_layout_use {
651                self.record_layout_font(id);
652            }
653            return Ok(id);
654        }
655
656        // Map common Word font names to metric-compatible alternatives
657        let mapped = map_font_name(family_name);
658
659        // Try the requested font, mapped alternatives, then generic fallbacks
660        let mut fallbacks: Vec<&str> = Vec::with_capacity(10);
661        fallbacks.push(family_name);
662        for alt in mapped {
663            if *alt != family_name {
664                fallbacks.push(alt);
665            }
666        }
667        for generic in &[
668            "Carlito",
669            "Arial",
670            "Liberation Sans",
671            "Helvetica",
672            "DejaVu Sans",
673            "Noto Sans",
674        ] {
675            if !fallbacks.contains(generic) {
676                fallbacks.push(generic);
677            }
678        }
679
680        let style = if italic {
681            fontdb::Style::Italic
682        } else {
683            fontdb::Style::Normal
684        };
685        let weight = if bold {
686            fontdb::Weight::BOLD
687        } else {
688            fontdb::Weight::NORMAL
689        };
690
691        let mut found_id = None;
692        for fallback in &fallbacks {
693            let query = fontdb::Query {
694                families: &[fontdb::Family::Name(fallback)],
695                weight,
696                style,
697                stretch: fontdb::Stretch::Normal,
698            };
699
700            if let Some(id) = self.db.query(&query) {
701                found_id = Some(id);
702                break;
703            }
704        }
705
706        // Last resort: try generic families
707        if found_id.is_none() {
708            for generic_family in &[
709                fontdb::Family::SansSerif,
710                fontdb::Family::Serif,
711                fontdb::Family::Monospace,
712            ] {
713                let query = fontdb::Query {
714                    families: &[*generic_family],
715                    weight,
716                    style,
717                    stretch: fontdb::Stretch::Normal,
718                };
719                if let Some(id) = self.db.query(&query) {
720                    found_id = Some(id);
721                    break;
722                }
723            }
724        }
725
726        let db_id = found_id.ok_or_else(|| {
727            LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
728        })?;
729
730        // Preserve the established one-loaded-font-per-request-key behavior
731        // while the bounded alias cache has room. At the ceiling, reuse the
732        // exact resolved face rather than growing without limit.
733        if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
734            && let Some(idx) = self
735                .fonts
736                .iter()
737                .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
738        {
739            let id = self.fonts[idx].id;
740            if record_layout_use {
741                self.record_layout_font(id);
742            }
743            return Ok(id);
744        }
745
746        let font_id = FontId(self.next_id);
747        self.next_id += 1;
748
749        // Load file-backed data through the process cache. All faces in a TTC
750        // carry the same source path, so their collection indices share bytes.
751        let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
752            .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
753
754        let (units_per_em, ascender, descender, line_gap) = {
755            let face = ttf_parser::Face::parse(&data, face_index)
756                .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
757            (
758                face.units_per_em(),
759                face.ascender(),
760                face.descender(),
761                face.line_gap(),
762            )
763        };
764
765        // Every metric and advance is scaled by size/upem, so a zero here would
766        // turn the whole layout into infinities.
767        if units_per_em == 0 {
768            return Err(LayoutError::FontParse(format!(
769                "font '{family_name}' declares zero units per em"
770            )));
771        }
772
773        let shaper_data = {
774            let face = harfrust::FontRef::from_index(&data, face_index)
775                .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
776            harfrust::ShaperData::new(&face)
777        };
778
779        let actual_family = self
780            .db
781            .face(db_id)
782            .map(|f| {
783                f.families
784                    .first()
785                    .map(|(name, _)| name.clone())
786                    .unwrap_or_else(|| family_name.to_string())
787            })
788            .unwrap_or_else(|| family_name.to_string());
789
790        let idx = self.fonts.len();
791        self.fonts.push(LoadedFont {
792            db_id,
793            id: font_id,
794            family: actual_family,
795            bold,
796            italic,
797            data,
798            face_index,
799            units_per_em,
800            ascender,
801            descender,
802            line_gap,
803            shaper_data,
804        });
805        self.remember_font_key(key, idx);
806        if record_layout_use {
807            self.record_layout_font(font_id);
808        }
809
810        Ok(font_id)
811    }
812
813    /// Get font metrics at a given size in points.
814    pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
815        let font = self.get_font(font_id)?;
816        let scale = size_pt / font.units_per_em as f64;
817
818        Ok(FontMetrics {
819            ascent: font.ascender as f64 * scale,
820            descent: -(font.descender as f64) * scale, // make positive
821            line_gap: font.line_gap as f64 * scale,
822            units_per_em: font.units_per_em,
823        })
824    }
825
826    /// Shape a text string using HarfRust. Returns glyph IDs and advances.
827    pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
828        // HarfRust cannot derive segment properties from an empty buffer, and
829        // there is nothing to shape anyway.
830        if text.is_empty() {
831            return Ok(ShapedText {
832                glyph_ids: Vec::new(),
833                advances: Vec::new(),
834                width: 0.0,
835            });
836        }
837
838        let key = ShapingKey {
839            font_id,
840            text: text.to_owned(),
841            size_bits: size_pt.to_bits(),
842        };
843        let mut memo = match self.shaping_memo.lock() {
844            Ok(memo) => memo,
845            Err(poisoned) => {
846                let mut memo = poisoned.into_inner();
847                memo.clear();
848                self.shaping_memo.clear_poison();
849                memo
850            }
851        };
852        if let Some(index) = memo
853            .entries
854            .iter()
855            .position(|(candidate, _, _)| candidate == &key)
856        {
857            let entry = memo.entries.remove(index).expect("cache index exists");
858            let shaped = entry.1.clone();
859            memo.entries.push_back(entry);
860            #[cfg(test)]
861            {
862                memo.hits += 1;
863            }
864            return Ok(shaped);
865        }
866        #[cfg(test)]
867        {
868            memo.misses += 1;
869        }
870
871        let font = self.get_font(font_id)?;
872
873        let face = harfrust::FontRef::from_index(&font.data, font.face_index)
874            .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
875
876        let shaper = font.shaper_data.shaper(&face).build();
877
878        let mut buffer = harfrust::UnicodeBuffer::new();
879        buffer.push_str(text);
880        // Infer direction, script and language from the text. Unlike rustybuzz,
881        // HarfRust does not do this implicitly and panics on an unset direction.
882        buffer.guess_segment_properties();
883
884        let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
885        let infos = output.glyph_infos();
886        let positions = output.glyph_positions();
887
888        let upem = font.units_per_em as f64;
889        let scale = size_pt / upem;
890
891        let mut glyph_ids = Vec::with_capacity(infos.len());
892        let mut advances = Vec::with_capacity(positions.len());
893        let mut total_width = 0.0;
894
895        for (info, pos) in infos.iter().zip(positions.iter()) {
896            glyph_ids.push(info.glyph_id as u16);
897            let advance = pos.x_advance as f64 * scale;
898            advances.push(advance);
899            total_width += advance;
900        }
901
902        let shaped = ShapedText {
903            glyph_ids,
904            advances,
905            width: total_width,
906        };
907        memo.insert(key, shaped.clone());
908        Ok(shaped)
909    }
910
911    /// Get font data for PDF embedding.
912    pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
913        let font = self.get_font(font_id)?;
914        Ok(crate::output::FontData {
915            id: font.id,
916            family: font.family.clone(),
917            data: Arc::clone(&font.data),
918            face_index: font.face_index,
919            bold: font.bold,
920            italic: font.italic,
921        })
922    }
923
924    /// Get all used font data.
925    pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
926        self.fonts
927            .iter()
928            .map(|f| crate::output::FontData {
929                id: f.id,
930                family: f.family.clone(),
931                data: Arc::clone(&f.data),
932                face_index: f.face_index,
933                bold: f.bold,
934                italic: f.italic,
935            })
936            .collect()
937    }
938
939    fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
940        self.fonts
941            .iter()
942            .find(|f| f.id == font_id)
943            .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
944    }
945
946    fn remember_font_key(&mut self, key: FontKey, index: usize) {
947        if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
948            self.cache.insert(key, index);
949        }
950    }
951
952    fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
953        let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
954        if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
955            known.push(index);
956        }
957    }
958
959    fn remember_coverage_misses(&mut self, missing: &[char]) {
960        for &ch in missing {
961            if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
962                break;
963            }
964            self.coverage_misses.insert(ch);
965        }
966    }
967
968    fn record_layout_font(&mut self, font_id: FontId) {
969        if let Some(trace) = self.paragraph_font_trace.as_mut() {
970            if trace.ids.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
971                trace.ids.push(font_id);
972            } else {
973                trace.overflowed = true;
974            }
975        }
976        if !self.layout_fonts.contains(&font_id) {
977            self.layout_fonts.push(font_id);
978        }
979    }
980
981    #[cfg(test)]
982    fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
983        let memo = self
984            .shaping_memo
985            .lock()
986            .unwrap_or_else(std::sync::PoisonError::into_inner);
987        (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
988    }
989}
990
991fn bundled_font_database() -> fontdb::Database {
992    let mut db = fontdb::Database::new();
993    for (_family, data) in crate::bundled_fonts::bundled_font_data() {
994        db.load_font_data(data.to_vec());
995    }
996    db
997}
998
999fn font_data_for_face(
1000    db: &fontdb::Database,
1001    id: fontdb::ID,
1002    memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
1003) -> Option<(Arc<[u8]>, u32)> {
1004    let face = db.face(id)?;
1005    let face_index = face.index;
1006    match &face.source {
1007        fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
1008            Some(data) => Some((Arc::clone(data), face_index)),
1009            None => {
1010                let data: Arc<[u8]> = Arc::from(data.as_ref().as_ref().to_vec());
1011                memory_face_data.insert(id, Arc::clone(&data));
1012                Some((data, face_index))
1013            }
1014        },
1015        #[cfg(feature = "system-fonts")]
1016        fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
1017    }
1018}
1019
1020#[cfg(feature = "system-fonts")]
1021fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
1022    let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
1023    shared_file_font_bytes_from_cache(cache, path)
1024}
1025
1026#[cfg(feature = "system-fonts")]
1027fn shared_file_font_bytes_from_cache(
1028    cache_lock: &Mutex<FileFontCache>,
1029    path: &Path,
1030) -> Option<Arc<[u8]>> {
1031    let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1032    let mut cache = match cache_lock.lock() {
1033        Ok(cache) => cache,
1034        Err(poisoned) => {
1035            let mut cache = poisoned.into_inner();
1036            cache.clear();
1037            cache_lock.clear_poison();
1038            cache
1039        }
1040    };
1041    if let Some(index) = cache
1042        .entries
1043        .iter()
1044        .position(|(candidate, _, _)| candidate == &identity)
1045    {
1046        let entry = cache.entries.remove(index).expect("cache index exists");
1047        let bytes = Arc::clone(&entry.1);
1048        cache.entries.push_back(entry);
1049        return Some(bytes);
1050    }
1051
1052    let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
1053    cache_file_font_bytes(&mut cache, identity, bytes)
1054}
1055
1056#[cfg(feature = "system-fonts")]
1057fn cache_file_font_bytes(
1058    cache: &mut FileFontCache,
1059    identity: PathBuf,
1060    bytes: Arc<[u8]>,
1061) -> Option<Arc<[u8]>> {
1062    let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
1063        .saturating_add(identity.as_os_str().len())
1064        .saturating_add(bytes.len());
1065    if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
1066        while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
1067            || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
1068        {
1069            let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
1070                break;
1071            };
1072            cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
1073        }
1074        cache.bytes += entry_bytes;
1075        cache
1076            .entries
1077            .push_back((identity, Arc::clone(&bytes), entry_bytes));
1078    }
1079    Some(bytes)
1080}
1081
1082/// Map common Word font names to metric-compatible alternatives.
1083/// Returns a list of candidate names to try (including the original).
1084///
1085/// Priority: original font → metric-compatible open-source clone → generic fallback.
1086/// Carlito is metric-compatible with Calibri, Caladea with Cambria,
1087/// Liberation Sans/Serif/Mono with Arial/Times New Roman/Courier New.
1088fn map_font_name(name: &str) -> &[&str] {
1089    match name {
1090        "Calibri" => &["Calibri", "Carlito"],
1091        "Calibri Light" => &["Calibri Light", "Carlito"],
1092        "Cambria" => &["Cambria", "Caladea"],
1093        "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
1094        "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
1095        "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
1096        "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
1097        "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
1098        "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
1099        "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
1100        "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
1101        "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
1102        "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
1103        "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
1104        "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
1105        "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
1106        "Impact" => &["Impact", "Liberation Sans", "Arial"],
1107        "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
1108        "Symbol" => &["Symbol", "DejaVu Sans"],
1109        "Wingdings" => &["Wingdings", "Symbol"],
1110        _ => &[],
1111    }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116    use super::*;
1117    use crate::bundled_fonts::bundled_font_data;
1118
1119    fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
1120        assert_eq!(family.len(), 7);
1121        let mut font = source.to_vec();
1122        let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
1123        let name_offset = (0..table_count)
1124            .find_map(|table| {
1125                let record = 12 + table * 16;
1126                (&font[record..record + 4] == b"name").then(|| {
1127                    u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
1128                })
1129            })
1130            .expect("font has name table");
1131        let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
1132        let strings = name_offset
1133            + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
1134        for index in 0..count {
1135            let record = name_offset + 6 + index * 12;
1136            let platform = u16::from_be_bytes([font[record], font[record + 1]]);
1137            let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
1138            let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
1139            let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
1140            if !matches!(name_id, 1 | 16) {
1141                continue;
1142            }
1143            let destination = &mut font[strings + offset..strings + offset + length];
1144            match (platform, length) {
1145                (0 | 3, 14) => {
1146                    for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
1147                        bytes.copy_from_slice(&(ch as u16).to_be_bytes());
1148                    }
1149                }
1150                (1, 7) => destination.copy_from_slice(family.as_bytes()),
1151                _ => {}
1152            }
1153        }
1154        font
1155    }
1156
1157    #[cfg(feature = "system-fonts")]
1158    fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
1159        let header_len = 12 + fonts.len() * 4;
1160        let mut collection = vec![0u8; header_len];
1161        collection[0..4].copy_from_slice(b"ttcf");
1162        collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
1163        collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
1164
1165        for (font_number, font) in fonts.iter().enumerate() {
1166            while !collection.len().is_multiple_of(4) {
1167                collection.push(0);
1168            }
1169            let collection_offset = collection.len();
1170            collection[12 + font_number * 4..16 + font_number * 4]
1171                .copy_from_slice(&(collection_offset as u32).to_be_bytes());
1172
1173            let mut adjusted = font.to_vec();
1174            let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
1175            for table in 0..table_count {
1176                let offset_position = 12 + table * 16 + 8;
1177                let offset = u32::from_be_bytes(
1178                    adjusted[offset_position..offset_position + 4]
1179                        .try_into()
1180                        .expect("table offset"),
1181                );
1182                adjusted[offset_position..offset_position + 4]
1183                    .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
1184            }
1185            collection.extend_from_slice(&adjusted);
1186        }
1187        collection
1188    }
1189
1190    #[test]
1191    fn deterministic_font_manager_uses_only_bundled_fonts() {
1192        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1193
1194        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1195        assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
1196    }
1197
1198    #[cfg(feature = "system-fonts")]
1199    #[test]
1200    fn normal_font_discovery_initializes_once_per_process() {
1201        let _first = FontManager::new();
1202        let _second = FontManager::new();
1203        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1204
1205        let _deterministic =
1206            FontManager::new_deterministic().expect("bundled font manager should load");
1207        let _caller = FontManager::new_with_fonts(Vec::new());
1208        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1209    }
1210
1211    #[cfg(feature = "system-fonts")]
1212    #[test]
1213    fn file_backed_collection_faces_share_one_byte_buffer() {
1214        let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
1215        let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
1216        let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
1217        let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
1218        std::fs::write(&first_path, &collection).expect("write first temporary collection");
1219        std::fs::write(&second_path, &collection).expect("write second temporary collection");
1220
1221        let mut db = fontdb::Database::new();
1222        db.load_font_file(&first_path).expect("load first TTC");
1223        db.load_font_file(&second_path).expect("load second TTC");
1224        let canonical_first = std::fs::canonicalize(&first_path).unwrap();
1225        let canonical_second = std::fs::canonicalize(&second_path).unwrap();
1226        let first_ids = db
1227            .faces()
1228            .filter_map(|face| match &face.source {
1229                fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
1230                    Some(face.id)
1231                }
1232                _ => None,
1233            })
1234            .collect::<Vec<_>>();
1235        let second_id = db
1236            .faces()
1237            .find_map(|face| match &face.source {
1238                fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
1239                    Some(face.id)
1240                }
1241                _ => None,
1242            })
1243            .expect("second TTC face");
1244        assert_eq!(first_ids.len(), 2);
1245
1246        let mut memory = HashMap::new();
1247        let (first_face, first_index) =
1248            font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
1249        let (second_face, second_index) =
1250            font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
1251        let (other_file, _) =
1252            font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
1253        assert_ne!(first_index, second_index);
1254        assert!(Arc::ptr_eq(&first_face, &second_face));
1255        assert!(!Arc::ptr_eq(&first_face, &other_file));
1256
1257        std::fs::remove_file(first_path).expect("remove first temporary font");
1258        std::fs::remove_file(second_path).expect("remove second temporary font");
1259    }
1260
1261    #[test]
1262    fn shaping_memo_uses_complete_text_size_and_font_identity() {
1263        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1264        let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1265        let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
1266
1267        let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
1268        let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
1269        assert_eq!(first.glyph_ids, repeat.glyph_ids);
1270        assert_eq!(fm.shaping_memo_counts().0, 1);
1271
1272        fm.shape_text(regular, "different text", 11.0).unwrap();
1273        fm.shape_text(regular, "exact text", 12.0).unwrap();
1274        fm.shape_text(bold, "exact text", 11.0).unwrap();
1275        assert_eq!(fm.shaping_memo_counts().1, 4);
1276
1277        let replacement = FontFile {
1278            family: "Carlito".to_owned(),
1279            data: bundled_font_data()[1].1.to_vec(),
1280        };
1281        fm.load_additional_fonts(&[replacement]);
1282        assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
1283        let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1284        fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
1285        assert_eq!(fm.shaping_memo_counts().1, 1);
1286    }
1287
1288    #[test]
1289    fn shaping_memo_is_bounded_and_recovers_from_poison() {
1290        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1291        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1292        for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
1293            fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
1294                .unwrap();
1295        }
1296        let (_, _, entries, bytes) = fm.shaping_memo_counts();
1297        assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
1298        assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
1299
1300        let fm = Arc::new(fm);
1301        let poison = Arc::clone(&fm);
1302        assert!(
1303            std::thread::spawn(move || {
1304                let _guard = poison.shaping_memo.lock().unwrap();
1305                panic!("poison shaping cache for recovery coverage");
1306            })
1307            .join()
1308            .is_err()
1309        );
1310        let first = fm.shape_text(font, "after poison", 11.0).unwrap();
1311        let second = fm.shape_text(font, "after poison", 11.0).unwrap();
1312        assert_eq!(first.glyph_ids, second.glyph_ids);
1313        let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
1314        assert_eq!((hits, misses, entries), (1, 1, 1));
1315        assert!(bytes > 0);
1316    }
1317
1318    #[test]
1319    fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
1320        let mut memo = ShapingMemo::new();
1321        for suffix in ['a', 'b'] {
1322            memo.insert(
1323                ShapingKey {
1324                    font_id: FontId(0),
1325                    text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
1326                    size_bits: 11.0f64.to_bits(),
1327                },
1328                ShapedText {
1329                    glyph_ids: Vec::new(),
1330                    advances: Vec::new(),
1331                    width: 0.0,
1332                },
1333            );
1334        }
1335        assert_eq!(memo.entries.len(), 1);
1336        assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
1337    }
1338
1339    #[test]
1340    fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
1341        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1342        for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
1343            fm.remember_coverage_fallback(false, false, 0);
1344        }
1345        assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
1346
1347        let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
1348            .filter_map(|value| char::from_u32(0x10_000 + value as u32))
1349            .collect::<Vec<_>>();
1350        fm.remember_coverage_misses(&misses);
1351        assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
1352
1353        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1354            fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
1355                .expect("bounded fallback resolves");
1356        }
1357        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1358        assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
1359    }
1360
1361    #[test]
1362    fn active_document_may_resolve_more_than_256_distinct_faces() {
1363        let source = bundled_font_data()[4].1;
1364        let mut db = fontdb::Database::new();
1365        for index in 0..257 {
1366            db.load_font_data(font_with_family(source, &format!("F{index:06}")));
1367        }
1368        let mut fm = FontManager::from_base_database(db);
1369        fm.begin_layout();
1370        let mut ids = HashSet::new();
1371        for index in 0..257 {
1372            let family = format!("F{index:06}");
1373            let id = fm
1374                .resolve_font(Some(&family), false, false)
1375                .expect("distinct active face resolves");
1376            assert_eq!(fm.font_data(id).unwrap().family, family);
1377            ids.insert(id);
1378        }
1379        assert_eq!(ids.len(), 257);
1380        fm.retain_current_fonts();
1381        assert_eq!(fm.fonts.len(), 257);
1382    }
1383
1384    #[test]
1385    fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
1386        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1387        fm.begin_layout();
1388        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1389            fm.resolve_font(Some("Carlito"), false, false).unwrap();
1390        }
1391        assert!(fm.paragraph_font_trace.is_none());
1392
1393        fm.begin_paragraph_font_trace();
1394        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1395            fm.resolve_font(Some("Carlito"), false, false).unwrap();
1396        }
1397        assert!(fm.finish_paragraph_font_trace().is_none());
1398
1399        fm.begin_layout();
1400        assert_eq!(fm.layout_fonts.capacity(), 0);
1401        fm.begin_paragraph_font_trace();
1402        fm.resolve_font(Some("Carlito"), false, false).unwrap();
1403        let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
1404        assert_eq!(trace.len(), 1);
1405        assert_eq!(trace.capacity(), trace.len());
1406    }
1407
1408    #[cfg(feature = "system-fonts")]
1409    #[test]
1410    fn file_byte_cache_is_bounded_and_recovers_from_poison() {
1411        let cache = Arc::new(Mutex::new(FileFontCache::new()));
1412        {
1413            let mut cache = cache
1414                .lock()
1415                .unwrap_or_else(std::sync::PoisonError::into_inner);
1416            cache.clear();
1417            let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
1418            let returned = cache_file_font_bytes(
1419                &mut cache,
1420                PathBuf::from("oversized-font.ttc"),
1421                Arc::clone(&oversized),
1422            )
1423            .expect("oversized bytes are returned uncached");
1424            assert!(Arc::ptr_eq(&oversized, &returned));
1425            assert!(cache.entries.is_empty());
1426            assert_eq!(cache.bytes, 0);
1427        }
1428
1429        let poison = Arc::clone(&cache);
1430        assert!(
1431            std::thread::spawn(move || {
1432                let cache = poison;
1433                let _guard = cache.lock().unwrap();
1434                panic!("poison file byte cache for recovery coverage");
1435            })
1436            .join()
1437            .is_err()
1438        );
1439
1440        let path = std::env::temp_dir().join(format!(
1441            "rdocx-font-cache-poison-{}-{:?}.ttf",
1442            std::process::id(),
1443            std::thread::current().id()
1444        ));
1445        std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
1446        let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
1447        let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
1448        assert!(Arc::ptr_eq(&first, &second));
1449        std::fs::remove_file(path).expect("remove recovery font");
1450    }
1451
1452    #[cfg(not(feature = "system-fonts"))]
1453    #[test]
1454    fn no_default_features_omits_system_font_discovery() {
1455        let fm = FontManager::new();
1456        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1457    }
1458
1459    #[test]
1460    fn font_manager_with_no_fonts_returns_an_error() {
1461        let mut fm = FontManager::new_with_fonts(Vec::new());
1462        assert!(matches!(
1463            fm.resolve_font(None, false, false),
1464            Err(LayoutError::FontNotFound(_))
1465        ));
1466    }
1467
1468    #[test]
1469    fn load_system_font() {
1470        let mut fm = FontManager::new();
1471        // Should be able to resolve at least one font via fallback
1472        let result = fm.resolve_font(None, false, false);
1473        // On CI or systems without fonts this might fail, so we just check it doesn't panic
1474        if let Ok(id) = result {
1475            assert_eq!(id.0, 0);
1476        }
1477    }
1478
1479    #[test]
1480    fn font_metrics_positive() {
1481        let mut fm = FontManager::new();
1482        if let Ok(id) = fm.resolve_font(None, false, false) {
1483            let metrics = fm.metrics(id, 12.0).unwrap();
1484            assert!(metrics.ascent > 0.0);
1485            assert!(metrics.descent > 0.0);
1486            assert!(metrics.units_per_em > 0);
1487        }
1488    }
1489
1490    #[test]
1491    fn shape_hello_world() {
1492        let mut fm = FontManager::new();
1493        if let Ok(id) = fm.resolve_font(None, false, false) {
1494            let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
1495            assert!(!shaped.glyph_ids.is_empty());
1496            assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
1497            assert!(shaped.width > 0.0);
1498        }
1499    }
1500
1501    #[test]
1502    fn font_caching() {
1503        let mut fm = FontManager::new();
1504        if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
1505            let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
1506            assert_eq!(id1, id2);
1507        }
1508    }
1509
1510    #[test]
1511    fn font_resolution_alias_cache_is_bounded() {
1512        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1513        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1514            fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
1515                .expect("fallback font resolves");
1516        }
1517        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1518        assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1519    }
1520
1521    #[test]
1522    fn bold_italic_variants() {
1523        let mut fm = FontManager::new();
1524        let regular = fm.resolve_font(None, false, false);
1525        let bold = fm.resolve_font(None, true, false);
1526        if let (Ok(r), Ok(b)) = (regular, bold) {
1527            // Bold should get a different font ID (different variant)
1528            assert_ne!(r, b);
1529        }
1530    }
1531
1532    /// Latin text must resolve exactly as it did before, so the coverage check
1533    /// cannot disturb the overwhelmingly common case.
1534    #[test]
1535    fn latin_text_resolves_the_same_as_by_name() {
1536        let mut fm = FontManager::new();
1537        let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
1538            return;
1539        };
1540        let for_text = fm
1541            .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
1542            .unwrap();
1543        assert_eq!(by_name, for_text);
1544    }
1545
1546    /// Text nothing can draw must keep the requested font rather than failing.
1547    ///
1548    /// The bundled fonts have no CJK coverage, so in deterministic mode the
1549    /// search is guaranteed to come up empty. The text still needs a font so
1550    /// it occupies the right space.
1551    #[test]
1552    fn text_no_font_can_draw_keeps_the_requested_font() {
1553        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1554        let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1555        let resolved = fm
1556            .resolve_font_for_text(Some("Carlito"), false, false, "这是中文")
1557            .unwrap();
1558        assert_eq!(
1559            primary, resolved,
1560            "with no covering font available the original must be kept"
1561        );
1562    }
1563
1564    /// Whitespace absent from a font is not a reason to go hunting for another.
1565    #[test]
1566    fn whitespace_does_not_trigger_a_fallback() {
1567        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1568        let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1569        let idx = fm.index_of(by_name).unwrap();
1570        // A non-breaking space and a tab, neither of which every face carries.
1571        assert!(
1572            fm.uncovered(idx, "a\u{00a0}b\tc")
1573                .iter()
1574                .all(|c| *c != '\t'),
1575            "control and whitespace characters must be ignored"
1576        );
1577    }
1578
1579    /// When the machine does have a CJK font, CJK text must not keep a Latin
1580    /// font that cannot draw it.
1581    ///
1582    /// Skipped where no such font is installed, which is why it asserts
1583    /// nothing about which font is chosen.
1584    #[test]
1585    fn cjk_text_moves_off_a_latin_font_when_possible() {
1586        let mut fm = FontManager::new();
1587        let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
1588            return;
1589        };
1590        let Some(idx) = fm.index_of(latin) else {
1591            return;
1592        };
1593        if fm.uncovered(idx, "这是中文").is_empty() {
1594            return; // that font somehow covers it, nothing to prove
1595        }
1596        let resolved = fm
1597            .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
1598            .unwrap();
1599        if resolved == latin {
1600            return; // no covering font installed on this machine
1601        }
1602        let new_idx = fm.index_of(resolved).unwrap();
1603        assert!(
1604            fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
1605            "the replacement must cover more of the text than the original"
1606        );
1607    }
1608}