Skip to main content

oxml_layout/
font.rs

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