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        let family_name = family.unwrap_or("Arial");
620
621        let key = FontKey {
622            family: family_name.to_string(),
623            bold,
624            italic,
625        };
626
627        if let Some(idx) = self.cache.get(&key).copied() {
628            let id = self.fonts[idx].id;
629            self.record_layout_font(id);
630            return Ok(id);
631        }
632
633        // Map common Word font names to metric-compatible alternatives
634        let mapped = map_font_name(family_name);
635
636        // Try the requested font, mapped alternatives, then generic fallbacks
637        let mut fallbacks: Vec<&str> = Vec::with_capacity(10);
638        fallbacks.push(family_name);
639        for alt in mapped {
640            if *alt != family_name {
641                fallbacks.push(alt);
642            }
643        }
644        for generic in &[
645            "Carlito",
646            "Arial",
647            "Liberation Sans",
648            "Helvetica",
649            "DejaVu Sans",
650            "Noto Sans",
651        ] {
652            if !fallbacks.contains(generic) {
653                fallbacks.push(generic);
654            }
655        }
656
657        let style = if italic {
658            fontdb::Style::Italic
659        } else {
660            fontdb::Style::Normal
661        };
662        let weight = if bold {
663            fontdb::Weight::BOLD
664        } else {
665            fontdb::Weight::NORMAL
666        };
667
668        let mut found_id = None;
669        for fallback in &fallbacks {
670            let query = fontdb::Query {
671                families: &[fontdb::Family::Name(fallback)],
672                weight,
673                style,
674                stretch: fontdb::Stretch::Normal,
675            };
676
677            if let Some(id) = self.db.query(&query) {
678                found_id = Some(id);
679                break;
680            }
681        }
682
683        // Last resort: try generic families
684        if found_id.is_none() {
685            for generic_family in &[
686                fontdb::Family::SansSerif,
687                fontdb::Family::Serif,
688                fontdb::Family::Monospace,
689            ] {
690                let query = fontdb::Query {
691                    families: &[*generic_family],
692                    weight,
693                    style,
694                    stretch: fontdb::Stretch::Normal,
695                };
696                if let Some(id) = self.db.query(&query) {
697                    found_id = Some(id);
698                    break;
699                }
700            }
701        }
702
703        let db_id = found_id.ok_or_else(|| {
704            LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
705        })?;
706
707        // Preserve the established one-loaded-font-per-request-key behavior
708        // while the bounded alias cache has room. At the ceiling, reuse the
709        // exact resolved face rather than growing without limit.
710        if self.cache.len() >= RESOLUTION_CACHE_MAX_ENTRIES
711            && let Some(idx) = self
712                .fonts
713                .iter()
714                .position(|font| font.db_id == db_id && font.bold == bold && font.italic == italic)
715        {
716            let id = self.fonts[idx].id;
717            self.record_layout_font(id);
718            return Ok(id);
719        }
720
721        let font_id = FontId(self.next_id);
722        self.next_id += 1;
723
724        // Load file-backed data through the process cache. All faces in a TTC
725        // carry the same source path, so their collection indices share bytes.
726        let (data, face_index) = font_data_for_face(&self.db, db_id, &mut self.memory_face_data)
727            .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
728
729        let (units_per_em, ascender, descender, line_gap) = {
730            let face = ttf_parser::Face::parse(&data, face_index)
731                .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
732            (
733                face.units_per_em(),
734                face.ascender(),
735                face.descender(),
736                face.line_gap(),
737            )
738        };
739
740        // Every metric and advance is scaled by size/upem, so a zero here would
741        // turn the whole layout into infinities.
742        if units_per_em == 0 {
743            return Err(LayoutError::FontParse(format!(
744                "font '{family_name}' declares zero units per em"
745            )));
746        }
747
748        let shaper_data = {
749            let face = harfrust::FontRef::from_index(&data, face_index)
750                .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
751            harfrust::ShaperData::new(&face)
752        };
753
754        let actual_family = self
755            .db
756            .face(db_id)
757            .map(|f| {
758                f.families
759                    .first()
760                    .map(|(name, _)| name.clone())
761                    .unwrap_or_else(|| family_name.to_string())
762            })
763            .unwrap_or_else(|| family_name.to_string());
764
765        let idx = self.fonts.len();
766        self.fonts.push(LoadedFont {
767            db_id,
768            id: font_id,
769            family: actual_family,
770            bold,
771            italic,
772            data,
773            face_index,
774            units_per_em,
775            ascender,
776            descender,
777            line_gap,
778            shaper_data,
779        });
780        self.remember_font_key(key, idx);
781        self.record_layout_font(font_id);
782
783        Ok(font_id)
784    }
785
786    /// Get font metrics at a given size in points.
787    pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
788        let font = self.get_font(font_id)?;
789        let scale = size_pt / font.units_per_em as f64;
790
791        Ok(FontMetrics {
792            ascent: font.ascender as f64 * scale,
793            descent: -(font.descender as f64) * scale, // make positive
794            line_gap: font.line_gap as f64 * scale,
795            units_per_em: font.units_per_em,
796        })
797    }
798
799    /// Shape a text string using HarfRust. Returns glyph IDs and advances.
800    pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
801        // HarfRust cannot derive segment properties from an empty buffer, and
802        // there is nothing to shape anyway.
803        if text.is_empty() {
804            return Ok(ShapedText {
805                glyph_ids: Vec::new(),
806                advances: Vec::new(),
807                width: 0.0,
808            });
809        }
810
811        let key = ShapingKey {
812            font_id,
813            text: text.to_owned(),
814            size_bits: size_pt.to_bits(),
815        };
816        let mut memo = match self.shaping_memo.lock() {
817            Ok(memo) => memo,
818            Err(poisoned) => {
819                let mut memo = poisoned.into_inner();
820                memo.clear();
821                self.shaping_memo.clear_poison();
822                memo
823            }
824        };
825        if let Some(index) = memo
826            .entries
827            .iter()
828            .position(|(candidate, _, _)| candidate == &key)
829        {
830            let entry = memo.entries.remove(index).expect("cache index exists");
831            let shaped = entry.1.clone();
832            memo.entries.push_back(entry);
833            #[cfg(test)]
834            {
835                memo.hits += 1;
836            }
837            return Ok(shaped);
838        }
839        #[cfg(test)]
840        {
841            memo.misses += 1;
842        }
843
844        let font = self.get_font(font_id)?;
845
846        let face = harfrust::FontRef::from_index(&font.data, font.face_index)
847            .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
848
849        let shaper = font.shaper_data.shaper(&face).build();
850
851        let mut buffer = harfrust::UnicodeBuffer::new();
852        buffer.push_str(text);
853        // Infer direction, script and language from the text. Unlike rustybuzz,
854        // HarfRust does not do this implicitly and panics on an unset direction.
855        buffer.guess_segment_properties();
856
857        let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
858        let infos = output.glyph_infos();
859        let positions = output.glyph_positions();
860
861        let upem = font.units_per_em as f64;
862        let scale = size_pt / upem;
863
864        let mut glyph_ids = Vec::with_capacity(infos.len());
865        let mut advances = Vec::with_capacity(positions.len());
866        let mut total_width = 0.0;
867
868        for (info, pos) in infos.iter().zip(positions.iter()) {
869            glyph_ids.push(info.glyph_id as u16);
870            let advance = pos.x_advance as f64 * scale;
871            advances.push(advance);
872            total_width += advance;
873        }
874
875        let shaped = ShapedText {
876            glyph_ids,
877            advances,
878            width: total_width,
879        };
880        memo.insert(key, shaped.clone());
881        Ok(shaped)
882    }
883
884    /// Get font data for PDF embedding.
885    pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
886        let font = self.get_font(font_id)?;
887        Ok(crate::output::FontData {
888            id: font.id,
889            family: font.family.clone(),
890            data: font.data.to_vec(),
891            face_index: font.face_index,
892            bold: font.bold,
893            italic: font.italic,
894        })
895    }
896
897    /// Get all used font data.
898    pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
899        self.fonts
900            .iter()
901            .map(|f| crate::output::FontData {
902                id: f.id,
903                family: f.family.clone(),
904                data: f.data.to_vec(),
905                face_index: f.face_index,
906                bold: f.bold,
907                italic: f.italic,
908            })
909            .collect()
910    }
911
912    fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
913        self.fonts
914            .iter()
915            .find(|f| f.id == font_id)
916            .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
917    }
918
919    fn remember_font_key(&mut self, key: FontKey, index: usize) {
920        if self.cache.len() < RESOLUTION_CACHE_MAX_ENTRIES {
921            self.cache.insert(key, index);
922        }
923    }
924
925    fn remember_coverage_fallback(&mut self, bold: bool, italic: bool, index: usize) {
926        let known = self.coverage_fallbacks.entry((bold, italic)).or_default();
927        if known.len() < COVERAGE_FALLBACK_MAX_ENTRIES && !known.contains(&index) {
928            known.push(index);
929        }
930    }
931
932    fn remember_coverage_misses(&mut self, missing: &[char]) {
933        for &ch in missing {
934            if self.coverage_misses.len() >= COVERAGE_MISS_MAX_ENTRIES {
935                break;
936            }
937            self.coverage_misses.insert(ch);
938        }
939    }
940
941    fn record_layout_font(&mut self, font_id: FontId) {
942        if let Some(trace) = self.paragraph_font_trace.as_mut() {
943            if trace.ids.len() < PARAGRAPH_FONT_TRACE_MAX_ENTRIES {
944                trace.ids.push(font_id);
945            } else {
946                trace.overflowed = true;
947            }
948        }
949        if !self.layout_fonts.contains(&font_id) {
950            self.layout_fonts.push(font_id);
951        }
952    }
953
954    #[cfg(test)]
955    fn shaping_memo_counts(&self) -> (usize, usize, usize, usize) {
956        let memo = self
957            .shaping_memo
958            .lock()
959            .unwrap_or_else(std::sync::PoisonError::into_inner);
960        (memo.hits, memo.misses, memo.entries.len(), memo.bytes)
961    }
962}
963
964fn bundled_font_database() -> fontdb::Database {
965    let mut db = fontdb::Database::new();
966    for (_family, data) in crate::bundled_fonts::bundled_font_data() {
967        db.load_font_data(data.to_vec());
968    }
969    db
970}
971
972fn font_data_for_face(
973    db: &fontdb::Database,
974    id: fontdb::ID,
975    memory_face_data: &mut HashMap<fontdb::ID, Arc<[u8]>>,
976) -> Option<(Arc<[u8]>, u32)> {
977    let face = db.face(id)?;
978    let face_index = face.index;
979    match &face.source {
980        fontdb::Source::Binary(data) => match memory_face_data.get(&id) {
981            Some(data) => Some((Arc::clone(data), face_index)),
982            None => {
983                let data: Arc<[u8]> = Arc::from(data.as_ref().as_ref().to_vec());
984                memory_face_data.insert(id, Arc::clone(&data));
985                Some((data, face_index))
986            }
987        },
988        #[cfg(feature = "system-fonts")]
989        fontdb::Source::File(path) => shared_file_font_bytes(path).map(|data| (data, face_index)),
990    }
991}
992
993#[cfg(feature = "system-fonts")]
994fn shared_file_font_bytes(path: &Path) -> Option<Arc<[u8]>> {
995    let cache = FILE_FONT_CACHE.get_or_init(|| Mutex::new(FileFontCache::new()));
996    shared_file_font_bytes_from_cache(cache, path)
997}
998
999#[cfg(feature = "system-fonts")]
1000fn shared_file_font_bytes_from_cache(
1001    cache_lock: &Mutex<FileFontCache>,
1002    path: &Path,
1003) -> Option<Arc<[u8]>> {
1004    let identity = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1005    let mut cache = match cache_lock.lock() {
1006        Ok(cache) => cache,
1007        Err(poisoned) => {
1008            let mut cache = poisoned.into_inner();
1009            cache.clear();
1010            cache_lock.clear_poison();
1011            cache
1012        }
1013    };
1014    if let Some(index) = cache
1015        .entries
1016        .iter()
1017        .position(|(candidate, _, _)| candidate == &identity)
1018    {
1019        let entry = cache.entries.remove(index).expect("cache index exists");
1020        let bytes = Arc::clone(&entry.1);
1021        cache.entries.push_back(entry);
1022        return Some(bytes);
1023    }
1024
1025    let bytes: Arc<[u8]> = Arc::from(std::fs::read(&identity).ok()?);
1026    cache_file_font_bytes(&mut cache, identity, bytes)
1027}
1028
1029#[cfg(feature = "system-fonts")]
1030fn cache_file_font_bytes(
1031    cache: &mut FileFontCache,
1032    identity: PathBuf,
1033    bytes: Arc<[u8]>,
1034) -> Option<Arc<[u8]>> {
1035    let entry_bytes = std::mem::size_of::<(PathBuf, Arc<[u8]>, usize)>()
1036        .saturating_add(identity.as_os_str().len())
1037        .saturating_add(bytes.len());
1038    if entry_bytes <= FILE_FONT_CACHE_MAX_BYTES {
1039        while cache.entries.len() >= FILE_FONT_CACHE_MAX_ENTRIES
1040            || cache.bytes.saturating_add(entry_bytes) > FILE_FONT_CACHE_MAX_BYTES
1041        {
1042            let Some((_, _, evicted_bytes)) = cache.entries.pop_front() else {
1043                break;
1044            };
1045            cache.bytes = cache.bytes.saturating_sub(evicted_bytes);
1046        }
1047        cache.bytes += entry_bytes;
1048        cache
1049            .entries
1050            .push_back((identity, Arc::clone(&bytes), entry_bytes));
1051    }
1052    Some(bytes)
1053}
1054
1055/// Map common Word font names to metric-compatible alternatives.
1056/// Returns a list of candidate names to try (including the original).
1057///
1058/// Priority: original font → metric-compatible open-source clone → generic fallback.
1059/// Carlito is metric-compatible with Calibri, Caladea with Cambria,
1060/// Liberation Sans/Serif/Mono with Arial/Times New Roman/Courier New.
1061fn map_font_name(name: &str) -> &[&str] {
1062    match name {
1063        "Calibri" => &["Calibri", "Carlito"],
1064        "Calibri Light" => &["Calibri Light", "Carlito"],
1065        "Cambria" => &["Cambria", "Caladea"],
1066        "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
1067        "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
1068        "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
1069        "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
1070        "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
1071        "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
1072        "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
1073        "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
1074        "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
1075        "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
1076        "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
1077        "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
1078        "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
1079        "Impact" => &["Impact", "Liberation Sans", "Arial"],
1080        "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
1081        "Symbol" => &["Symbol", "DejaVu Sans"],
1082        "Wingdings" => &["Wingdings", "Symbol"],
1083        _ => &[],
1084    }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090    use crate::bundled_fonts::bundled_font_data;
1091
1092    fn font_with_family(source: &[u8], family: &str) -> Vec<u8> {
1093        assert_eq!(family.len(), 7);
1094        let mut font = source.to_vec();
1095        let table_count = u16::from_be_bytes([font[4], font[5]]) as usize;
1096        let name_offset = (0..table_count)
1097            .find_map(|table| {
1098                let record = 12 + table * 16;
1099                (&font[record..record + 4] == b"name").then(|| {
1100                    u32::from_be_bytes(font[record + 8..record + 12].try_into().unwrap()) as usize
1101                })
1102            })
1103            .expect("font has name table");
1104        let count = u16::from_be_bytes([font[name_offset + 2], font[name_offset + 3]]) as usize;
1105        let strings = name_offset
1106            + u16::from_be_bytes([font[name_offset + 4], font[name_offset + 5]]) as usize;
1107        for index in 0..count {
1108            let record = name_offset + 6 + index * 12;
1109            let platform = u16::from_be_bytes([font[record], font[record + 1]]);
1110            let name_id = u16::from_be_bytes([font[record + 6], font[record + 7]]);
1111            let length = u16::from_be_bytes([font[record + 8], font[record + 9]]) as usize;
1112            let offset = u16::from_be_bytes([font[record + 10], font[record + 11]]) as usize;
1113            if !matches!(name_id, 1 | 16) {
1114                continue;
1115            }
1116            let destination = &mut font[strings + offset..strings + offset + length];
1117            match (platform, length) {
1118                (0 | 3, 14) => {
1119                    for (bytes, ch) in destination.chunks_exact_mut(2).zip(family.bytes()) {
1120                        bytes.copy_from_slice(&(ch as u16).to_be_bytes());
1121                    }
1122                }
1123                (1, 7) => destination.copy_from_slice(family.as_bytes()),
1124                _ => {}
1125            }
1126        }
1127        font
1128    }
1129
1130    #[cfg(feature = "system-fonts")]
1131    fn test_ttc(fonts: &[&[u8]]) -> Vec<u8> {
1132        let header_len = 12 + fonts.len() * 4;
1133        let mut collection = vec![0u8; header_len];
1134        collection[0..4].copy_from_slice(b"ttcf");
1135        collection[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes());
1136        collection[8..12].copy_from_slice(&(fonts.len() as u32).to_be_bytes());
1137
1138        for (font_number, font) in fonts.iter().enumerate() {
1139            while !collection.len().is_multiple_of(4) {
1140                collection.push(0);
1141            }
1142            let collection_offset = collection.len();
1143            collection[12 + font_number * 4..16 + font_number * 4]
1144                .copy_from_slice(&(collection_offset as u32).to_be_bytes());
1145
1146            let mut adjusted = font.to_vec();
1147            let table_count = u16::from_be_bytes([adjusted[4], adjusted[5]]) as usize;
1148            for table in 0..table_count {
1149                let offset_position = 12 + table * 16 + 8;
1150                let offset = u32::from_be_bytes(
1151                    adjusted[offset_position..offset_position + 4]
1152                        .try_into()
1153                        .expect("table offset"),
1154                );
1155                adjusted[offset_position..offset_position + 4]
1156                    .copy_from_slice(&(offset + collection_offset as u32).to_be_bytes());
1157            }
1158            collection.extend_from_slice(&adjusted);
1159        }
1160        collection
1161    }
1162
1163    #[test]
1164    fn deterministic_font_manager_uses_only_bundled_fonts() {
1165        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1166
1167        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1168        assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
1169    }
1170
1171    #[cfg(feature = "system-fonts")]
1172    #[test]
1173    fn normal_font_discovery_initializes_once_per_process() {
1174        let _first = FontManager::new();
1175        let _second = FontManager::new();
1176        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1177
1178        let _deterministic =
1179            FontManager::new_deterministic().expect("bundled font manager should load");
1180        let _caller = FontManager::new_with_fonts(Vec::new());
1181        assert_eq!(SYSTEM_FONT_DISCOVERY_RUNS.load(Ordering::Relaxed), 1);
1182    }
1183
1184    #[cfg(feature = "system-fonts")]
1185    #[test]
1186    fn file_backed_collection_faces_share_one_byte_buffer() {
1187        let suffix = format!("{}-{:?}", std::process::id(), std::thread::current().id());
1188        let first_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-a.ttf"));
1189        let second_path = std::env::temp_dir().join(format!("rdocx-font-cache-{suffix}-b.ttf"));
1190        let collection = test_ttc(&[bundled_font_data()[0].1, bundled_font_data()[4].1]);
1191        std::fs::write(&first_path, &collection).expect("write first temporary collection");
1192        std::fs::write(&second_path, &collection).expect("write second temporary collection");
1193
1194        let mut db = fontdb::Database::new();
1195        db.load_font_file(&first_path).expect("load first TTC");
1196        db.load_font_file(&second_path).expect("load second TTC");
1197        let canonical_first = std::fs::canonicalize(&first_path).unwrap();
1198        let canonical_second = std::fs::canonicalize(&second_path).unwrap();
1199        let first_ids = db
1200            .faces()
1201            .filter_map(|face| match &face.source {
1202                fontdb::Source::File(path) if path == &first_path || path == &canonical_first => {
1203                    Some(face.id)
1204                }
1205                _ => None,
1206            })
1207            .collect::<Vec<_>>();
1208        let second_id = db
1209            .faces()
1210            .find_map(|face| match &face.source {
1211                fontdb::Source::File(path) if path == &second_path || path == &canonical_second => {
1212                    Some(face.id)
1213                }
1214                _ => None,
1215            })
1216            .expect("second TTC face");
1217        assert_eq!(first_ids.len(), 2);
1218
1219        let mut memory = HashMap::new();
1220        let (first_face, first_index) =
1221            font_data_for_face(&db, first_ids[0], &mut memory).expect("first TTC face bytes");
1222        let (second_face, second_index) =
1223            font_data_for_face(&db, first_ids[1], &mut memory).expect("second TTC face bytes");
1224        let (other_file, _) =
1225            font_data_for_face(&db, second_id, &mut memory).expect("other TTC bytes");
1226        assert_ne!(first_index, second_index);
1227        assert!(Arc::ptr_eq(&first_face, &second_face));
1228        assert!(!Arc::ptr_eq(&first_face, &other_file));
1229
1230        std::fs::remove_file(first_path).expect("remove first temporary font");
1231        std::fs::remove_file(second_path).expect("remove second temporary font");
1232    }
1233
1234    #[test]
1235    fn shaping_memo_uses_complete_text_size_and_font_identity() {
1236        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1237        let regular = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1238        let bold = fm.resolve_font(Some("Carlito"), true, false).unwrap();
1239
1240        let first = fm.shape_text(regular, "exact text", 11.0).unwrap();
1241        let repeat = fm.shape_text(regular, "exact text", 11.0).unwrap();
1242        assert_eq!(first.glyph_ids, repeat.glyph_ids);
1243        assert_eq!(fm.shaping_memo_counts().0, 1);
1244
1245        fm.shape_text(regular, "different text", 11.0).unwrap();
1246        fm.shape_text(regular, "exact text", 12.0).unwrap();
1247        fm.shape_text(bold, "exact text", 11.0).unwrap();
1248        assert_eq!(fm.shaping_memo_counts().1, 4);
1249
1250        let replacement = FontFile {
1251            family: "Carlito".to_owned(),
1252            data: bundled_font_data()[1].1.to_vec(),
1253        };
1254        fm.load_additional_fonts(&[replacement]);
1255        assert_eq!(fm.shaping_memo_counts(), (0, 0, 0, 0));
1256        let replacement_id = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1257        fm.shape_text(replacement_id, "exact text", 11.0).unwrap();
1258        assert_eq!(fm.shaping_memo_counts().1, 1);
1259    }
1260
1261    #[test]
1262    fn shaping_memo_is_bounded_and_recovers_from_poison() {
1263        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1264        let font = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1265        for index in 0..(SHAPING_CACHE_MAX_ENTRIES + 20) {
1266            fm.shape_text(font, &format!("bounded shaping entry {index}"), 11.0)
1267                .unwrap();
1268        }
1269        let (_, _, entries, bytes) = fm.shaping_memo_counts();
1270        assert!(entries <= SHAPING_CACHE_MAX_ENTRIES);
1271        assert!(bytes <= SHAPING_CACHE_MAX_BYTES);
1272
1273        let fm = Arc::new(fm);
1274        let poison = Arc::clone(&fm);
1275        assert!(
1276            std::thread::spawn(move || {
1277                let _guard = poison.shaping_memo.lock().unwrap();
1278                panic!("poison shaping cache for recovery coverage");
1279            })
1280            .join()
1281            .is_err()
1282        );
1283        let first = fm.shape_text(font, "after poison", 11.0).unwrap();
1284        let second = fm.shape_text(font, "after poison", 11.0).unwrap();
1285        assert_eq!(first.glyph_ids, second.glyph_ids);
1286        let (hits, misses, entries, bytes) = fm.shaping_memo_counts();
1287        assert_eq!((hits, misses, entries), (1, 1, 1));
1288        assert!(bytes > 0);
1289    }
1290
1291    #[test]
1292    fn shaping_memo_enforces_its_byte_ceiling_in_production_insertion() {
1293        let mut memo = ShapingMemo::new();
1294        for suffix in ['a', 'b'] {
1295            memo.insert(
1296                ShapingKey {
1297                    font_id: FontId(0),
1298                    text: std::iter::repeat_n(suffix, 9 * 1024 * 1024).collect(),
1299                    size_bits: 11.0f64.to_bits(),
1300                },
1301                ShapedText {
1302                    glyph_ids: Vec::new(),
1303                    advances: Vec::new(),
1304                    width: 0.0,
1305                },
1306            );
1307        }
1308        assert_eq!(memo.entries.len(), 1);
1309        assert!(memo.bytes <= SHAPING_CACHE_MAX_BYTES);
1310    }
1311
1312    #[test]
1313    fn persistent_coverage_and_loaded_face_state_is_bounded_and_deduplicated() {
1314        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1315        for _ in 0..(COVERAGE_FALLBACK_MAX_ENTRIES + 20) {
1316            fm.remember_coverage_fallback(false, false, 0);
1317        }
1318        assert_eq!(fm.coverage_fallbacks[&(false, false)], vec![0]);
1319
1320        let misses = (0..(COVERAGE_MISS_MAX_ENTRIES + 20))
1321            .filter_map(|value| char::from_u32(0x10_000 + value as u32))
1322            .collect::<Vec<_>>();
1323        fm.remember_coverage_misses(&misses);
1324        assert_eq!(fm.coverage_misses.len(), COVERAGE_MISS_MAX_ENTRIES);
1325
1326        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1327            fm.resolve_font(Some(&format!("missing alias {index}")), false, false)
1328                .expect("bounded fallback resolves");
1329        }
1330        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1331        assert_eq!(fm.fonts.len(), RESOLUTION_CACHE_MAX_ENTRIES);
1332    }
1333
1334    #[test]
1335    fn active_document_may_resolve_more_than_256_distinct_faces() {
1336        let source = bundled_font_data()[4].1;
1337        let mut db = fontdb::Database::new();
1338        for index in 0..257 {
1339            db.load_font_data(font_with_family(source, &format!("F{index:06}")));
1340        }
1341        let mut fm = FontManager::from_base_database(db);
1342        fm.begin_layout();
1343        let mut ids = HashSet::new();
1344        for index in 0..257 {
1345            let family = format!("F{index:06}");
1346            let id = fm
1347                .resolve_font(Some(&family), false, false)
1348                .expect("distinct active face resolves");
1349            assert_eq!(fm.font_data(id).unwrap().family, family);
1350            ids.insert(id);
1351        }
1352        assert_eq!(ids.len(), 257);
1353        fm.retain_current_fonts();
1354        assert_eq!(fm.fonts.len(), 257);
1355    }
1356
1357    #[test]
1358    fn font_trace_is_bounded_to_one_candidate_and_releases_capacity() {
1359        let mut fm = FontManager::new_deterministic().expect("bundled fonts load");
1360        fm.begin_layout();
1361        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1362            fm.resolve_font(Some("Carlito"), false, false).unwrap();
1363        }
1364        assert!(fm.paragraph_font_trace.is_none());
1365
1366        fm.begin_paragraph_font_trace();
1367        for _ in 0..(PARAGRAPH_FONT_TRACE_MAX_ENTRIES + 20) {
1368            fm.resolve_font(Some("Carlito"), false, false).unwrap();
1369        }
1370        assert!(fm.finish_paragraph_font_trace().is_none());
1371
1372        fm.begin_layout();
1373        assert_eq!(fm.layout_fonts.capacity(), 0);
1374        fm.begin_paragraph_font_trace();
1375        fm.resolve_font(Some("Carlito"), false, false).unwrap();
1376        let trace = fm.finish_paragraph_font_trace().expect("bounded trace");
1377        assert_eq!(trace.len(), 1);
1378        assert_eq!(trace.capacity(), trace.len());
1379    }
1380
1381    #[cfg(feature = "system-fonts")]
1382    #[test]
1383    fn file_byte_cache_is_bounded_and_recovers_from_poison() {
1384        let cache = Arc::new(Mutex::new(FileFontCache::new()));
1385        {
1386            let mut cache = cache
1387                .lock()
1388                .unwrap_or_else(std::sync::PoisonError::into_inner);
1389            cache.clear();
1390            let oversized: Arc<[u8]> = Arc::from(vec![0; FILE_FONT_CACHE_MAX_BYTES + 1]);
1391            let returned = cache_file_font_bytes(
1392                &mut cache,
1393                PathBuf::from("oversized-font.ttc"),
1394                Arc::clone(&oversized),
1395            )
1396            .expect("oversized bytes are returned uncached");
1397            assert!(Arc::ptr_eq(&oversized, &returned));
1398            assert!(cache.entries.is_empty());
1399            assert_eq!(cache.bytes, 0);
1400        }
1401
1402        let poison = Arc::clone(&cache);
1403        assert!(
1404            std::thread::spawn(move || {
1405                let cache = poison;
1406                let _guard = cache.lock().unwrap();
1407                panic!("poison file byte cache for recovery coverage");
1408            })
1409            .join()
1410            .is_err()
1411        );
1412
1413        let path = std::env::temp_dir().join(format!(
1414            "rdocx-font-cache-poison-{}-{:?}.ttf",
1415            std::process::id(),
1416            std::thread::current().id()
1417        ));
1418        std::fs::write(&path, bundled_font_data()[0].1).expect("write recovery font");
1419        let first = shared_file_font_bytes_from_cache(&cache, &path).expect("recover cache");
1420        let second = shared_file_font_bytes_from_cache(&cache, &path).expect("reuse cache");
1421        assert!(Arc::ptr_eq(&first, &second));
1422        std::fs::remove_file(path).expect("remove recovery font");
1423    }
1424
1425    #[cfg(not(feature = "system-fonts"))]
1426    #[test]
1427    fn no_default_features_omits_system_font_discovery() {
1428        let fm = FontManager::new();
1429        assert_eq!(fm.db.faces().count(), bundled_font_data().len());
1430    }
1431
1432    #[test]
1433    fn font_manager_with_no_fonts_returns_an_error() {
1434        let mut fm = FontManager::new_with_fonts(Vec::new());
1435        assert!(matches!(
1436            fm.resolve_font(None, false, false),
1437            Err(LayoutError::FontNotFound(_))
1438        ));
1439    }
1440
1441    #[test]
1442    fn load_system_font() {
1443        let mut fm = FontManager::new();
1444        // Should be able to resolve at least one font via fallback
1445        let result = fm.resolve_font(None, false, false);
1446        // On CI or systems without fonts this might fail, so we just check it doesn't panic
1447        if let Ok(id) = result {
1448            assert_eq!(id.0, 0);
1449        }
1450    }
1451
1452    #[test]
1453    fn font_metrics_positive() {
1454        let mut fm = FontManager::new();
1455        if let Ok(id) = fm.resolve_font(None, false, false) {
1456            let metrics = fm.metrics(id, 12.0).unwrap();
1457            assert!(metrics.ascent > 0.0);
1458            assert!(metrics.descent > 0.0);
1459            assert!(metrics.units_per_em > 0);
1460        }
1461    }
1462
1463    #[test]
1464    fn shape_hello_world() {
1465        let mut fm = FontManager::new();
1466        if let Ok(id) = fm.resolve_font(None, false, false) {
1467            let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
1468            assert!(!shaped.glyph_ids.is_empty());
1469            assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
1470            assert!(shaped.width > 0.0);
1471        }
1472    }
1473
1474    #[test]
1475    fn font_caching() {
1476        let mut fm = FontManager::new();
1477        if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
1478            let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
1479            assert_eq!(id1, id2);
1480        }
1481    }
1482
1483    #[test]
1484    fn font_resolution_alias_cache_is_bounded() {
1485        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1486        for index in 0..(RESOLUTION_CACHE_MAX_ENTRIES + 20) {
1487            fm.resolve_font(Some(&format!("Missing family {index}")), false, false)
1488                .expect("fallback font resolves");
1489        }
1490        assert!(fm.cache.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1491        assert!(fm.fonts.len() <= RESOLUTION_CACHE_MAX_ENTRIES);
1492    }
1493
1494    #[test]
1495    fn bold_italic_variants() {
1496        let mut fm = FontManager::new();
1497        let regular = fm.resolve_font(None, false, false);
1498        let bold = fm.resolve_font(None, true, false);
1499        if let (Ok(r), Ok(b)) = (regular, bold) {
1500            // Bold should get a different font ID (different variant)
1501            assert_ne!(r, b);
1502        }
1503    }
1504
1505    /// Latin text must resolve exactly as it did before, so the coverage check
1506    /// cannot disturb the overwhelmingly common case.
1507    #[test]
1508    fn latin_text_resolves_the_same_as_by_name() {
1509        let mut fm = FontManager::new();
1510        let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
1511            return;
1512        };
1513        let for_text = fm
1514            .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
1515            .unwrap();
1516        assert_eq!(by_name, for_text);
1517    }
1518
1519    /// Text nothing can draw must keep the requested font rather than failing.
1520    ///
1521    /// The bundled fonts have no CJK coverage, so in deterministic mode the
1522    /// search is guaranteed to come up empty. The text still needs a font so
1523    /// it occupies the right space.
1524    #[test]
1525    fn text_no_font_can_draw_keeps_the_requested_font() {
1526        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1527        let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1528        let resolved = fm
1529            .resolve_font_for_text(Some("Carlito"), false, false, "这是中文")
1530            .unwrap();
1531        assert_eq!(
1532            primary, resolved,
1533            "with no covering font available the original must be kept"
1534        );
1535    }
1536
1537    /// Whitespace absent from a font is not a reason to go hunting for another.
1538    #[test]
1539    fn whitespace_does_not_trigger_a_fallback() {
1540        let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
1541        let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
1542        let idx = fm.index_of(by_name).unwrap();
1543        // A non-breaking space and a tab, neither of which every face carries.
1544        assert!(
1545            fm.uncovered(idx, "a\u{00a0}b\tc")
1546                .iter()
1547                .all(|c| *c != '\t'),
1548            "control and whitespace characters must be ignored"
1549        );
1550    }
1551
1552    /// When the machine does have a CJK font, CJK text must not keep a Latin
1553    /// font that cannot draw it.
1554    ///
1555    /// Skipped where no such font is installed, which is why it asserts
1556    /// nothing about which font is chosen.
1557    #[test]
1558    fn cjk_text_moves_off_a_latin_font_when_possible() {
1559        let mut fm = FontManager::new();
1560        let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
1561            return;
1562        };
1563        let Some(idx) = fm.index_of(latin) else {
1564            return;
1565        };
1566        if fm.uncovered(idx, "这是中文").is_empty() {
1567            return; // that font somehow covers it, nothing to prove
1568        }
1569        let resolved = fm
1570            .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
1571            .unwrap();
1572        if resolved == latin {
1573            return; // no covering font installed on this machine
1574        }
1575        let new_idx = fm.index_of(resolved).unwrap();
1576        assert!(
1577            fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
1578            "the replacement must cover more of the text than the original"
1579        );
1580    }
1581}