Skip to main content

rust_fontconfig/
lib.rs

1//! # rust-fontconfig
2//!
3//! Pure-Rust rewrite of the Linux fontconfig library (no system dependencies). Enable the `parsing` feature to parse `.woff`, `.woff2`, `.ttc`, `.otf` and `.ttf` with allsorts.
4//!
5//! **NOTE**: Also works on Windows, macOS and WASM - without external dependencies!
6//!
7//! ## Usage
8//!
9//! ### Basic Font Query
10//!
11//! ```rust,no_run
12//! use rust_fontconfig::{FcFontCache, FcPattern};
13//!
14//! fn main() {
15//!     // Build the font cache
16//!     let cache = FcFontCache::build();
17//!
18//!     // Query a font by name
19//!     let results = cache.query(
20//!         &FcPattern {
21//!             name: Some(String::from("Arial")),
22//!             ..Default::default()
23//!         },
24//!         &mut Vec::new() // Trace messages container
25//!     );
26//!
27//!     if let Some(font_match) = results {
28//!         println!("Font match ID: {:?}", font_match.id);
29//!         println!("Font unicode ranges: {:?}", font_match.unicode_ranges);
30//!     } else {
31//!         println!("No matching font found");
32//!     }
33//! }
34//! ```
35//!
36//! ### Resolve Font Chain and Query for Text
37//!
38//! ```rust,no_run
39//! use rust_fontconfig::{FcFontCache, FcWeight, PatternMatch};
40//!
41//! fn main() {
42//!     # #[cfg(feature = "std")]
43//!     # {
44//!     let cache = FcFontCache::build();
45//!
46//!     // Build font fallback chain (without text parameter)
47//!     let font_chain = cache.resolve_font_chain(
48//!         &["Arial".to_string(), "sans-serif".to_string()],
49//!         FcWeight::Normal,
50//!         PatternMatch::DontCare,
51//!         PatternMatch::DontCare,
52//!         &mut Vec::new(),
53//!     );
54//!
55//!     // Query which fonts to use for specific text
56//!     let text = "Hello 你好 Здравствуйте";
57//!     let font_runs = font_chain.query_for_text(&cache, text);
58//!
59//!     println!("Text split into {} font runs:", font_runs.len());
60//!     for run in font_runs {
61//!         println!("  '{}' -> font {:?}", run.text, run.font_id);
62//!     }
63//!     # }
64//! }
65//! ```
66
67#![allow(non_snake_case)]
68
69extern crate alloc;
70
71use alloc::collections::btree_map::BTreeMap;
72use alloc::string::{String, ToString};
73use alloc::vec::Vec;
74#[cfg(all(feature = "std", feature = "parsing"))]
75use allsorts::binary::read::ReadScope;
76#[cfg(all(feature = "std", feature = "parsing"))]
77use allsorts::get_name::fontcode_get_name;
78#[cfg(all(feature = "std", feature = "parsing"))]
79use allsorts::tables::os2::Os2;
80#[cfg(all(feature = "std", feature = "parsing"))]
81use allsorts::tables::{FontTableProvider, HheaTable, HmtxTable, MaxpTable};
82#[cfg(all(feature = "std", feature = "parsing"))]
83use allsorts::tag;
84#[cfg(feature = "std")]
85use std::path::PathBuf;
86
87#[cfg(feature = "std")]
88pub mod config;
89pub mod fallback;
90pub mod utils;
91#[cfg(feature = "std")]
92pub use config::{FcFallbackConfig, FcScriptFallback, GenericFamily};
93#[cfg(feature = "std")]
94use fallback::FontChainCacheKey;
95#[cfg(feature = "std")]
96pub use fallback::{CssFallbackGroup, FontFallbackChain, ScriptFallbackGroup};
97
98#[cfg(feature = "ffi")]
99pub mod ffi;
100
101#[cfg(feature = "cache")]
102pub mod disk_cache;
103#[cfg(feature = "async-registry")]
104pub mod multithread;
105#[cfg(feature = "async-registry")]
106pub mod registry;
107#[cfg(feature = "async-registry")]
108pub mod scoring;
109
110#[cfg(all(target_os = "ios", feature = "std", feature = "parsing"))]
111mod mobile_ios;
112
113/// Operating system type for generic font family resolution.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub enum OperatingSystem {
116    Windows,
117    Linux,
118    MacOS,
119    IOS,
120    Android,
121    Wasm,
122}
123
124impl OperatingSystem {
125    /// Detect the current operating system at compile time.
126    pub fn current() -> Self {
127        #[cfg(target_os = "windows")]
128        return OperatingSystem::Windows;
129
130        #[cfg(target_os = "linux")]
131        return OperatingSystem::Linux;
132
133        #[cfg(target_os = "macos")]
134        return OperatingSystem::MacOS;
135
136        #[cfg(target_os = "ios")]
137        return OperatingSystem::IOS;
138
139        #[cfg(target_os = "android")]
140        return OperatingSystem::Android;
141
142        #[cfg(target_family = "wasm")]
143        return OperatingSystem::Wasm;
144
145        #[cfg(not(any(
146            target_os = "windows",
147            target_os = "linux",
148            target_os = "macos",
149            target_os = "ios",
150            target_os = "android",
151            target_family = "wasm"
152        )))]
153        return OperatingSystem::Linux; // Default fallback
154    }
155
156    /// Built-in `serif` candidates for this OS, script-specific entries for.
157    #[cfg(feature = "std")]
158    #[deprecated(
159        since = "5.0.0",
160        note = "use `FcFallbackConfig::os_defaults(os).expand_generic(GenericFamily::Serif, ranges)`"
161    )]
162    pub fn get_serif_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
163        FcFallbackConfig::os_defaults(*self).expand_generic(GenericFamily::Serif, unicode_ranges)
164    }
165
166    /// Built-in `sans-serif` candidates for this OS, script-specific entries.
167    #[cfg(feature = "std")]
168    #[deprecated(
169        since = "5.0.0",
170        note = "use `FcFallbackConfig::os_defaults(os).expand_generic(GenericFamily::SansSerif, ranges)`"
171    )]
172    pub fn get_sans_serif_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
173        FcFallbackConfig::os_defaults(*self)
174            .expand_generic(GenericFamily::SansSerif, unicode_ranges)
175    }
176
177    /// Built-in `monospace` candidates for this OS, script-specific entries.
178    #[cfg(feature = "std")]
179    #[deprecated(
180        since = "5.0.0",
181        note = "use `FcFallbackConfig::os_defaults(os).expand_generic(GenericFamily::Monospace, ranges)`"
182    )]
183    pub fn get_monospace_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
184        FcFallbackConfig::os_defaults(*self)
185            .expand_generic(GenericFamily::Monospace, unicode_ranges)
186    }
187
188    /// Expand one CSS family entry against the built-in tables for this OS.
189    #[cfg(feature = "std")]
190    #[deprecated(
191        since = "5.0.0",
192        note = "use `FcFallbackConfig::os_defaults(os).expand_family(family, ranges)`"
193    )]
194    pub fn expand_generic_family(
195        &self,
196        family: &str,
197        unicode_ranges: &[UnicodeRange],
198    ) -> Vec<String> {
199        FcFallbackConfig::os_defaults(*self).expand_family(family, unicode_ranges)
200    }
201}
202
203/// Expand a CSS font stack against the built-in per-OS tables.
204#[cfg(feature = "std")]
205#[deprecated(
206    since = "5.0.0",
207    note = "use `FcFallbackConfig::os_defaults(os).candidate_families(families, ranges)`"
208)]
209pub fn expand_font_families(
210    families: &[String],
211    os: OperatingSystem,
212    unicode_ranges: &[UnicodeRange],
213) -> Vec<String> {
214    FcFallbackConfig::os_defaults(os).candidate_families(families, unicode_ranges)
215}
216
217/// UUID to identify a font (collections are broken up into separate fonts).
218#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
219#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
220pub struct FontId(pub u128);
221
222impl core::fmt::Debug for FontId {
223    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224        core::fmt::Display::fmt(self, f)
225    }
226}
227
228impl core::fmt::Display for FontId {
229    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
230        let id = self.0;
231        write!(
232            f,
233            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
234            (id >> 96) & 0xFFFFFFFF,
235            (id >> 80) & 0xFFFF,
236            (id >> 64) & 0xFFFF,
237            (id >> 48) & 0xFFFF,
238            id & 0xFFFFFFFFFFFF
239        )
240    }
241}
242
243impl FontId {
244    /// Generate a new unique FontId using an atomic counter.
245    pub fn new() -> Self {
246        use core::sync::atomic::{AtomicU64, Ordering};
247        static COUNTER: AtomicU64 = AtomicU64::new(1);
248        let id = COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
249        FontId(id)
250    }
251}
252
253/// Whether a field is required to match (yes / no / don't care).
254#[derive(Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
255#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
256#[repr(C)]
257pub enum PatternMatch {
258    /// Default: don't particularly care whether the requirement matches.
259    #[default]
260    DontCare = 2,
261    /// Requirement has to be true for the selected font.
262    True = 0,
263    /// Requirement has to be false for the selected font.
264    False = 1,
265}
266
267impl PatternMatch {
268    fn needs_to_match(&self) -> bool {
269        matches!(self, PatternMatch::True | PatternMatch::False)
270    }
271
272    fn matches(&self, other: &PatternMatch) -> bool {
273        match (self, other) {
274            (PatternMatch::DontCare, _) => true,
275            (_, PatternMatch::DontCare) => true,
276            (a, b) => a == b,
277        }
278    }
279}
280
281/// Font weight values as defined in CSS specification.
282#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
283#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
284#[repr(C)]
285pub enum FcWeight {
286    Thin = 100,
287    ExtraLight = 200,
288    Light = 300,
289    Normal = 400,
290    Medium = 500,
291    SemiBold = 600,
292    Bold = 700,
293    ExtraBold = 800,
294    Black = 900,
295}
296
297impl FcWeight {
298    pub fn from_u16(weight: u16) -> Self {
299        match weight {
300            0..=149 => FcWeight::Thin,
301            150..=249 => FcWeight::ExtraLight,
302            250..=349 => FcWeight::Light,
303            350..=449 => FcWeight::Normal,
304            450..=549 => FcWeight::Medium,
305            550..=649 => FcWeight::SemiBold,
306            650..=749 => FcWeight::Bold,
307            750..=849 => FcWeight::ExtraBold,
308            _ => FcWeight::Black,
309        }
310    }
311
312    pub fn find_best_match(&self, available: &[FcWeight]) -> Option<FcWeight> {
313        if available.is_empty() {
314            return None;
315        }
316
317        // Exact match
318        if available.contains(self) {
319            return Some(*self);
320        }
321
322        // Get numeric value
323        let self_value = *self as u16;
324
325        match *self {
326            FcWeight::Normal => {
327                // For Normal (400), try Medium (500) first
328                if available.contains(&FcWeight::Medium) {
329                    return Some(FcWeight::Medium);
330                }
331                // Then try lighter weights
332                for weight in &[FcWeight::Light, FcWeight::ExtraLight, FcWeight::Thin] {
333                    if available.contains(weight) {
334                        return Some(*weight);
335                    }
336                }
337                // Last, try heavier weights
338                for weight in &[
339                    FcWeight::SemiBold,
340                    FcWeight::Bold,
341                    FcWeight::ExtraBold,
342                    FcWeight::Black,
343                ] {
344                    if available.contains(weight) {
345                        return Some(*weight);
346                    }
347                }
348            }
349            FcWeight::Medium => {
350                // For Medium (500), try Normal (400) first
351                if available.contains(&FcWeight::Normal) {
352                    return Some(FcWeight::Normal);
353                }
354                // Then try lighter weights
355                for weight in &[FcWeight::Light, FcWeight::ExtraLight, FcWeight::Thin] {
356                    if available.contains(weight) {
357                        return Some(*weight);
358                    }
359                }
360                // Last, try heavier weights
361                for weight in &[
362                    FcWeight::SemiBold,
363                    FcWeight::Bold,
364                    FcWeight::ExtraBold,
365                    FcWeight::Black,
366                ] {
367                    if available.contains(weight) {
368                        return Some(*weight);
369                    }
370                }
371            }
372            FcWeight::Thin | FcWeight::ExtraLight | FcWeight::Light => {
373                // For lightweight fonts (<400), first try lighter or equal weights
374                let mut best_match = None;
375                let mut smallest_diff = u16::MAX;
376
377                // Find the closest lighter weight
378                for weight in available {
379                    let weight_value = *weight as u16;
380                    // Only consider weights <= self (per test expectation)
381                    if weight_value <= self_value {
382                        let diff = self_value - weight_value;
383                        if diff < smallest_diff {
384                            smallest_diff = diff;
385                            best_match = Some(*weight);
386                        }
387                    }
388                }
389
390                if best_match.is_some() {
391                    return best_match;
392                }
393
394                // If no lighter weight, find the closest heavier weight
395                best_match = None;
396                smallest_diff = u16::MAX;
397
398                for weight in available {
399                    let weight_value = *weight as u16;
400                    if weight_value > self_value {
401                        let diff = weight_value - self_value;
402                        if diff < smallest_diff {
403                            smallest_diff = diff;
404                            best_match = Some(*weight);
405                        }
406                    }
407                }
408
409                return best_match;
410            }
411            FcWeight::SemiBold | FcWeight::Bold | FcWeight::ExtraBold | FcWeight::Black => {
412                // For heavyweight fonts (>500), first try heavier or equal weights
413                let mut best_match = None;
414                let mut smallest_diff = u16::MAX;
415
416                // Find the closest heavier weight
417                for weight in available {
418                    let weight_value = *weight as u16;
419                    // Only consider weights >= self
420                    if weight_value >= self_value {
421                        let diff = weight_value - self_value;
422                        if diff < smallest_diff {
423                            smallest_diff = diff;
424                            best_match = Some(*weight);
425                        }
426                    }
427                }
428
429                if best_match.is_some() {
430                    return best_match;
431                }
432
433                // If no heavier weight, find the closest lighter weight
434                best_match = None;
435                smallest_diff = u16::MAX;
436
437                for weight in available {
438                    let weight_value = *weight as u16;
439                    if weight_value < self_value {
440                        let diff = self_value - weight_value;
441                        if diff < smallest_diff {
442                            smallest_diff = diff;
443                            best_match = Some(*weight);
444                        }
445                    }
446                }
447
448                return best_match;
449            }
450        }
451
452        // If nothing matches by now, return the first available weight
453        Some(available[0])
454    }
455}
456
457impl Default for FcWeight {
458    fn default() -> Self {
459        FcWeight::Normal
460    }
461}
462
463/// CSS font-stretch values.
464#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
465#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
466#[repr(C)]
467pub enum FcStretch {
468    UltraCondensed = 1,
469    ExtraCondensed = 2,
470    Condensed = 3,
471    SemiCondensed = 4,
472    Normal = 5,
473    SemiExpanded = 6,
474    Expanded = 7,
475    ExtraExpanded = 8,
476    UltraExpanded = 9,
477}
478
479impl FcStretch {
480    pub fn is_condensed(&self) -> bool {
481        use self::FcStretch::*;
482        match self {
483            UltraCondensed => true,
484            ExtraCondensed => true,
485            Condensed => true,
486            SemiCondensed => true,
487            Normal => false,
488            SemiExpanded => false,
489            Expanded => false,
490            ExtraExpanded => false,
491            UltraExpanded => false,
492        }
493    }
494    pub fn from_u16(width_class: u16) -> Self {
495        match width_class {
496            1 => FcStretch::UltraCondensed,
497            2 => FcStretch::ExtraCondensed,
498            3 => FcStretch::Condensed,
499            4 => FcStretch::SemiCondensed,
500            5 => FcStretch::Normal,
501            6 => FcStretch::SemiExpanded,
502            7 => FcStretch::Expanded,
503            8 => FcStretch::ExtraExpanded,
504            9 => FcStretch::UltraExpanded,
505            _ => FcStretch::Normal,
506        }
507    }
508
509    /// Follows CSS spec for stretch matching.
510    pub fn find_best_match(&self, available: &[FcStretch]) -> Option<FcStretch> {
511        if available.is_empty() {
512            return None;
513        }
514
515        if available.contains(self) {
516            return Some(*self);
517        }
518
519        // For 'normal' or condensed values, narrower widths are checked first, then wider values
520        if *self <= FcStretch::Normal {
521            // Find narrower values first
522            let mut closest_narrower = None;
523            for stretch in available.iter() {
524                if *stretch < *self
525                    && (closest_narrower.is_none() || *stretch > closest_narrower.unwrap())
526                {
527                    closest_narrower = Some(*stretch);
528                }
529            }
530
531            if closest_narrower.is_some() {
532                return closest_narrower;
533            }
534
535            // Otherwise, find wider values
536            let mut closest_wider = None;
537            for stretch in available.iter() {
538                if *stretch > *self
539                    && (closest_wider.is_none() || *stretch < closest_wider.unwrap())
540                {
541                    closest_wider = Some(*stretch);
542                }
543            }
544
545            return closest_wider;
546        } else {
547            // For expanded values, wider values are checked first, then narrower values
548            let mut closest_wider = None;
549            for stretch in available.iter() {
550                if *stretch > *self
551                    && (closest_wider.is_none() || *stretch < closest_wider.unwrap())
552                {
553                    closest_wider = Some(*stretch);
554                }
555            }
556
557            if closest_wider.is_some() {
558                return closest_wider;
559            }
560
561            // Otherwise, find narrower values
562            let mut closest_narrower = None;
563            for stretch in available.iter() {
564                if *stretch < *self
565                    && (closest_narrower.is_none() || *stretch > closest_narrower.unwrap())
566                {
567                    closest_narrower = Some(*stretch);
568                }
569            }
570
571            return closest_narrower;
572        }
573    }
574}
575
576impl Default for FcStretch {
577    fn default() -> Self {
578        FcStretch::Normal
579    }
580}
581
582/// Unicode range representation for font matching.
583#[repr(C)]
584#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
585#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
586pub struct UnicodeRange {
587    pub start: u32,
588    pub end: u32,
589}
590
591/// The default set of Unicode-block fallback scripts that.
592pub const DEFAULT_UNICODE_FALLBACK_SCRIPTS: &[UnicodeRange] = &[
593    UnicodeRange {
594        start: 0x0400,
595        end: 0x04FF,
596    }, // Cyrillic
597    UnicodeRange {
598        start: 0x0600,
599        end: 0x06FF,
600    }, // Arabic
601    UnicodeRange {
602        start: 0x0900,
603        end: 0x097F,
604    }, // Devanagari
605    UnicodeRange {
606        start: 0x3040,
607        end: 0x309F,
608    }, // Hiragana
609    UnicodeRange {
610        start: 0x30A0,
611        end: 0x30FF,
612    }, // Katakana
613    UnicodeRange {
614        start: 0x4E00,
615        end: 0x9FFF,
616    }, // CJK Unified Ideographs
617    UnicodeRange {
618        start: 0xAC00,
619        end: 0xD7A3,
620    }, // Hangul Syllables
621];
622
623impl UnicodeRange {
624    pub fn contains(&self, c: char) -> bool {
625        let c = c as u32;
626        c >= self.start && c <= self.end
627    }
628
629    pub fn overlaps(&self, other: &UnicodeRange) -> bool {
630        self.start <= other.end && other.start <= self.end
631    }
632
633    pub fn is_subset_of(&self, other: &UnicodeRange) -> bool {
634        self.start >= other.start && self.end <= other.end
635    }
636}
637
638/// Check if any range covers CJK Unified Ideographs, Hiragana, Katakana, or Hangul.
639pub fn has_cjk_ranges(ranges: &[UnicodeRange]) -> bool {
640    const BLOCKS: [UnicodeRange; 4] = [
641        UnicodeRange {
642            start: 0x3040,
643            end: 0x309F,
644        }, // Hiragana
645        UnicodeRange {
646            start: 0x30A0,
647            end: 0x30FF,
648        }, // Katakana
649        UnicodeRange {
650            start: 0x4E00,
651            end: 0x9FFF,
652        }, // CJK Unified Ideographs
653        UnicodeRange {
654            start: 0xAC00,
655            end: 0xD7AF,
656        }, // Hangul Syllables
657    ];
658    ranges.iter().any(|r| BLOCKS.iter().any(|b| r.overlaps(b)))
659}
660
661/// Check if any range covers the Arabic block.
662pub fn has_arabic_ranges(ranges: &[UnicodeRange]) -> bool {
663    ranges.iter().any(|r| {
664        r.overlaps(&UnicodeRange {
665            start: 0x0600,
666            end: 0x06FF,
667        })
668    })
669}
670
671/// Check if any range covers the Cyrillic block.
672pub fn has_cyrillic_ranges(ranges: &[UnicodeRange]) -> bool {
673    ranges.iter().any(|r| {
674        r.overlaps(&UnicodeRange {
675            start: 0x0400,
676            end: 0x04FF,
677        })
678    })
679}
680
681/// Check if any range covers the Hebrew block.
682pub fn has_hebrew_ranges(ranges: &[UnicodeRange]) -> bool {
683    ranges.iter().any(|r| {
684        r.overlaps(&UnicodeRange {
685            start: 0x0590,
686            end: 0x05FF,
687        })
688    })
689}
690
691/// Check if any range covers the Thai block.
692pub fn has_thai_ranges(ranges: &[UnicodeRange]) -> bool {
693    ranges.iter().any(|r| {
694        r.overlaps(&UnicodeRange {
695            start: 0x0E00,
696            end: 0x0E7F,
697        })
698    })
699}
700
701/// Log levels for trace messages.
702#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
703pub enum TraceLevel {
704    Debug,
705    Info,
706    Warning,
707    Error,
708}
709
710/// Reason for font matching failure or success.
711#[derive(Debug, Clone, PartialEq, Eq, Hash)]
712pub enum MatchReason {
713    NameMismatch {
714        requested: Option<String>,
715        found: Option<String>,
716    },
717    FamilyMismatch {
718        requested: Option<String>,
719        found: Option<String>,
720    },
721    StyleMismatch {
722        property: &'static str,
723        requested: String,
724        found: String,
725    },
726    WeightMismatch {
727        requested: FcWeight,
728        found: FcWeight,
729    },
730    StretchMismatch {
731        requested: FcStretch,
732        found: FcStretch,
733    },
734    UnicodeRangeMismatch {
735        character: char,
736        ranges: Vec<UnicodeRange>,
737    },
738    Success,
739}
740
741/// Trace message for debugging font matching.
742#[derive(Debug, Clone, PartialEq, Eq)]
743pub struct TraceMsg {
744    pub level: TraceLevel,
745    pub path: String,
746    pub reason: MatchReason,
747}
748
749/// Hinting style for font rendering.
750#[repr(C)]
751#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
752#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
753pub enum FcHintStyle {
754    #[default]
755    None = 0,
756    Slight = 1,
757    Medium = 2,
758    Full = 3,
759}
760
761/// Subpixel rendering order.
762#[repr(C)]
763#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
764#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
765pub enum FcRgba {
766    #[default]
767    Unknown = 0,
768    Rgb = 1,
769    Bgr = 2,
770    Vrgb = 3,
771    Vbgr = 4,
772    None = 5,
773}
774
775/// LCD filter mode for subpixel rendering.
776#[repr(C)]
777#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
778#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
779pub enum FcLcdFilter {
780    #[default]
781    None = 0,
782    Default = 1,
783    Light = 2,
784    Legacy = 3,
785}
786
787/// Per-font rendering configuration from system font config (Linux fonts.conf).
788#[derive(Debug, Default, Clone)]
789#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
790pub struct FcFontRenderConfig {
791    pub antialias: Option<bool>,
792    pub hinting: Option<bool>,
793    pub hintstyle: Option<FcHintStyle>,
794    pub autohint: Option<bool>,
795    pub rgba: Option<FcRgba>,
796    pub lcdfilter: Option<FcLcdFilter>,
797    pub embeddedbitmap: Option<bool>,
798    pub embolden: Option<bool>,
799    pub dpi: Option<f64>,
800    pub scale: Option<f64>,
801    pub minspace: Option<bool>,
802}
803
804/// Helper newtype to provide Eq/Ord for Option<f64> via total-order bit comparison.
805impl Eq for FcFontRenderConfig {}
806
807// Manual PartialOrd/Ord for f64 field bit pattern consistency.
808impl PartialEq for FcFontRenderConfig {
809    fn eq(&self, other: &Self) -> bool {
810        self.cmp(other) == core::cmp::Ordering::Equal
811    }
812}
813
814impl PartialOrd for FcFontRenderConfig {
815    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
816        Some(self.cmp(other))
817    }
818}
819
820impl Ord for FcFontRenderConfig {
821    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
822        // Compare all non-f64 fields first
823        let ord = self
824            .antialias
825            .cmp(&other.antialias)
826            .then_with(|| self.hinting.cmp(&other.hinting))
827            .then_with(|| self.hintstyle.cmp(&other.hintstyle))
828            .then_with(|| self.autohint.cmp(&other.autohint))
829            .then_with(|| self.rgba.cmp(&other.rgba))
830            .then_with(|| self.lcdfilter.cmp(&other.lcdfilter))
831            .then_with(|| self.embeddedbitmap.cmp(&other.embeddedbitmap))
832            .then_with(|| self.embolden.cmp(&other.embolden))
833            .then_with(|| self.minspace.cmp(&other.minspace));
834
835        // For f64 fields, use to_bits() for total ordering
836        let ord = ord.then_with(|| {
837            let a = self.dpi.map(|v| v.to_bits());
838            let b = other.dpi.map(|v| v.to_bits());
839            a.cmp(&b)
840        });
841        ord.then_with(|| {
842            let a = self.scale.map(|v| v.to_bits());
843            let b = other.scale.map(|v| v.to_bits());
844            a.cmp(&b)
845        })
846    }
847}
848
849/// Font pattern for matching.
850#[derive(Default, Clone, PartialOrd, Ord, PartialEq, Eq)]
851#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
852#[repr(C)]
853pub struct FcPattern {
854    // font name
855    pub name: Option<String>,
856    // family name
857    pub family: Option<String>,
858    // "italic" property
859    pub italic: PatternMatch,
860    // "oblique" property
861    pub oblique: PatternMatch,
862    // "bold" property
863    pub bold: PatternMatch,
864    // "monospace" property
865    pub monospace: PatternMatch,
866    // "condensed" property
867    pub condensed: PatternMatch,
868    // font weight
869    pub weight: FcWeight,
870    // font stretch
871    pub stretch: FcStretch,
872    // unicode ranges to match
873    pub unicode_ranges: Vec<UnicodeRange>,
874    // extended font metadata
875    pub metadata: FcFontMetadata,
876    // per-font rendering configuration (from system fonts.conf on Linux)
877    pub render_config: FcFontRenderConfig,
878}
879
880impl core::fmt::Debug for FcPattern {
881    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
882        let mut d = f.debug_struct("FcPattern");
883
884        if let Some(name) = &self.name {
885            d.field("name", name);
886        }
887
888        if let Some(family) = &self.family {
889            d.field("family", family);
890        }
891
892        if self.italic != PatternMatch::DontCare {
893            d.field("italic", &self.italic);
894        }
895
896        if self.oblique != PatternMatch::DontCare {
897            d.field("oblique", &self.oblique);
898        }
899
900        if self.bold != PatternMatch::DontCare {
901            d.field("bold", &self.bold);
902        }
903
904        if self.monospace != PatternMatch::DontCare {
905            d.field("monospace", &self.monospace);
906        }
907
908        if self.condensed != PatternMatch::DontCare {
909            d.field("condensed", &self.condensed);
910        }
911
912        if self.weight != FcWeight::Normal {
913            d.field("weight", &self.weight);
914        }
915
916        if self.stretch != FcStretch::Normal {
917            d.field("stretch", &self.stretch);
918        }
919
920        if !self.unicode_ranges.is_empty() {
921            d.field("unicode_ranges", &self.unicode_ranges);
922        }
923
924        // Only show non-empty metadata fields
925        let empty_metadata = FcFontMetadata::default();
926        if self.metadata != empty_metadata {
927            d.field("metadata", &self.metadata);
928        }
929
930        // Only show render_config when it differs from default
931        let empty_render_config = FcFontRenderConfig::default();
932        if self.render_config != empty_render_config {
933            d.field("render_config", &self.render_config);
934        }
935
936        d.finish()
937    }
938}
939
940/// Font metadata from the OS/2 table.
941#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
942#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
943pub struct FcFontMetadata {
944    pub copyright: Option<String>,
945    pub designer: Option<String>,
946    pub designer_url: Option<String>,
947    pub font_family: Option<String>,
948    pub font_subfamily: Option<String>,
949    pub full_name: Option<String>,
950    pub id_description: Option<String>,
951    pub license: Option<String>,
952    pub license_url: Option<String>,
953    pub manufacturer: Option<String>,
954    pub manufacturer_url: Option<String>,
955    pub postscript_name: Option<String>,
956    pub preferred_family: Option<String>,
957    pub preferred_subfamily: Option<String>,
958    pub trademark: Option<String>,
959    pub unique_id: Option<String>,
960    pub version: Option<String>,
961}
962
963impl FcPattern {
964    /// Check if this pattern would match the given character.
965    pub fn contains_char(&self, c: char) -> bool {
966        if self.unicode_ranges.is_empty() {
967            return true; // No ranges specified means match all characters
968        }
969
970        for range in &self.unicode_ranges {
971            if range.contains(c) {
972                return true;
973            }
974        }
975
976        false
977    }
978}
979
980/// Font match result with UUID.
981#[derive(Debug, Clone, PartialEq, Eq)]
982pub struct FontMatch {
983    pub id: FontId,
984    pub unicode_ranges: Vec<UnicodeRange>,
985    pub fallbacks: Vec<FontMatchNoFallback>,
986}
987
988/// Font match result with UUID (without fallback).
989#[derive(Debug, Clone, PartialEq, Eq)]
990pub struct FontMatchNoFallback {
991    pub id: FontId,
992    pub unicode_ranges: Vec<UnicodeRange>,
993}
994
995/// A run of text that uses the same font.
996#[derive(Debug, Clone, PartialEq, Eq)]
997pub struct ResolvedFontRun {
998    /// The text content of this run.
999    pub text: String,
1000    /// Start byte index in the original text.
1001    pub start_byte: usize,
1002    /// End byte index in the original text (exclusive).
1003    pub end_byte: usize,
1004    /// The font to use for this run (None if no font found).
1005    pub font_id: Option<FontId>,
1006    /// Which CSS font-family this came from.
1007    pub css_source: String,
1008}
1009
1010/// Path to a font file.
1011#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
1012#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
1013#[repr(C)]
1014pub struct FcFontPath {
1015    pub path: String,
1016    pub font_index: usize,
1017    /// 64-bit content hash of the file's bytes.
1018    #[cfg_attr(feature = "cache", serde(default))]
1019    pub bytes_hash: u64,
1020}
1021
1022/// In-memory font data.
1023#[derive(Debug, Clone, PartialEq, Eq)]
1024#[repr(C)]
1025pub struct FcFont {
1026    pub bytes: Vec<u8>,
1027    pub font_index: usize,
1028    pub id: String, // For identification in tests
1029}
1030
1031/// Owned font-source descriptor, returned by.
1032#[derive(Debug, Clone)]
1033pub enum OwnedFontSource {
1034    /// Font loaded from memory (small metadata + owned `Vec<u8>`).
1035    Memory(FcFont),
1036    /// Font loaded from disk.
1037    Disk(FcFontPath),
1038}
1039
1040/// A handle to font bytes returned by [`FcFontCache::get_font_bytes`].
1041#[cfg(feature = "std")]
1042pub enum FontBytes {
1043    /// Heap-owned bytes.
1044    Owned(std::sync::Arc<[u8]>),
1045    /// File-backed mmap.
1046    #[cfg(not(target_family = "wasm"))]
1047    Mmapped(mmapio::Mmap),
1048}
1049
1050#[cfg(feature = "std")]
1051impl FontBytes {
1052    /// Borrow the underlying byte slice.
1053    #[inline]
1054    pub fn as_slice(&self) -> &[u8] {
1055        match self {
1056            FontBytes::Owned(arc) => arc,
1057            #[cfg(not(target_family = "wasm"))]
1058            FontBytes::Mmapped(m) => &m[..],
1059        }
1060    }
1061}
1062
1063#[cfg(feature = "std")]
1064impl core::ops::Deref for FontBytes {
1065    type Target = [u8];
1066    #[inline]
1067    fn deref(&self) -> &[u8] {
1068        self.as_slice()
1069    }
1070}
1071
1072#[cfg(feature = "std")]
1073impl AsRef<[u8]> for FontBytes {
1074    #[inline]
1075    fn as_ref(&self) -> &[u8] {
1076        self.as_slice()
1077    }
1078}
1079
1080#[cfg(feature = "std")]
1081impl core::fmt::Debug for FontBytes {
1082    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1083        let kind = match self {
1084            FontBytes::Owned(_) => "Owned",
1085            #[cfg(not(target_family = "wasm"))]
1086            FontBytes::Mmapped(_) => "Mmapped",
1087        };
1088        write!(f, "FontBytes::{}({} bytes)", kind, self.as_slice().len())
1089    }
1090}
1091
1092/// Open a font file as an mmap-backed [`FontBytes`].
1093#[cfg(feature = "std")]
1094fn open_font_bytes_mmap(path: &str) -> Option<std::sync::Arc<FontBytes>> {
1095    use std::fs::File;
1096    use std::sync::Arc;
1097
1098    #[cfg(not(target_family = "wasm"))]
1099    {
1100        if let Ok(file) = File::open(path) {
1101            // Safety: Mmap requires file is not mutated. For system fonts this is fine.
1102            if let Ok(mmap) = unsafe { mmapio::MmapOptions::new().map(&file) } {
1103                return Some(Arc::new(FontBytes::Mmapped(mmap)));
1104            }
1105        }
1106    }
1107    let bytes = std::fs::read(path).ok()?;
1108    Some(Arc::new(FontBytes::Owned(Arc::from(bytes))))
1109}
1110
1111/// A named font to be added to the font cache from memory.
1112#[derive(Debug, Clone)]
1113pub struct NamedFont {
1114    /// Human-readable name for this font (e.g., "My Custom Font").
1115    pub name: String,
1116    /// The raw font file bytes (TTF, OTF, WOFF, WOFF2, TTC).
1117    pub bytes: Vec<u8>,
1118}
1119
1120impl NamedFont {
1121    /// Create a new named font from bytes.
1122    pub fn new(name: impl Into<String>, bytes: Vec<u8>) -> Self {
1123        Self {
1124            name: name.into(),
1125            bytes,
1126        }
1127    }
1128}
1129
1130/// Font cache, initialized at startup.
1131pub struct FcFontCache {
1132    pub(crate) shared: std::sync::Arc<FcFontCacheShared>,
1133}
1134
1135/// Shared interior of `FcFontCache`.
1136// Internal lock wrapper: `RwLock` by default, or `UnsafeCell` under `single-thread-unsafe-locks` (WASM).
1137
1138#[cfg(not(feature = "single-thread-unsafe-locks"))]
1139pub struct StLock<T> {
1140    lock: std::sync::RwLock<T>,
1141}
1142#[cfg(not(feature = "single-thread-unsafe-locks"))]
1143impl<T> core::fmt::Debug for StLock<T> {
1144    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1145        f.write_str("StLock(..)")
1146    }
1147}
1148#[cfg(not(feature = "single-thread-unsafe-locks"))]
1149impl<T> StLock<T> {
1150    pub fn new(v: T) -> Self {
1151        Self {
1152            lock: std::sync::RwLock::new(v),
1153        }
1154    }
1155    pub fn read(&self) -> Result<StReadGuard<'_, T>, core::convert::Infallible> {
1156        Ok(StReadGuard {
1157            g: self.lock.read().unwrap_or_else(|e| e.into_inner()),
1158        })
1159    }
1160    pub fn write(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1161        Ok(StWriteGuard {
1162            g: self.lock.write().unwrap_or_else(|e| e.into_inner()),
1163        })
1164    }
1165    pub fn lock(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1166        self.write()
1167    }
1168}
1169#[cfg(not(feature = "single-thread-unsafe-locks"))]
1170pub struct StReadGuard<'a, T> {
1171    g: std::sync::RwLockReadGuard<'a, T>,
1172}
1173#[cfg(not(feature = "single-thread-unsafe-locks"))]
1174impl<'a, T> core::ops::Deref for StReadGuard<'a, T> {
1175    type Target = T;
1176    fn deref(&self) -> &T {
1177        &self.g
1178    }
1179}
1180#[cfg(not(feature = "single-thread-unsafe-locks"))]
1181pub struct StWriteGuard<'a, T> {
1182    g: std::sync::RwLockWriteGuard<'a, T>,
1183}
1184#[cfg(not(feature = "single-thread-unsafe-locks"))]
1185impl<'a, T> core::ops::Deref for StWriteGuard<'a, T> {
1186    type Target = T;
1187    fn deref(&self) -> &T {
1188        &self.g
1189    }
1190}
1191#[cfg(not(feature = "single-thread-unsafe-locks"))]
1192impl<'a, T> core::ops::DerefMut for StWriteGuard<'a, T> {
1193    fn deref_mut(&mut self) -> &mut T {
1194        &mut self.g
1195    }
1196}
1197
1198#[cfg(feature = "single-thread-unsafe-locks")]
1199pub struct StLock<T> {
1200    cell: std::cell::UnsafeCell<T>,
1201}
1202#[cfg(feature = "single-thread-unsafe-locks")]
1203unsafe impl<T> Sync for StLock<T> {}
1204#[cfg(feature = "single-thread-unsafe-locks")]
1205unsafe impl<T> Send for StLock<T> {}
1206#[cfg(feature = "single-thread-unsafe-locks")]
1207impl<T> core::fmt::Debug for StLock<T> {
1208    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1209        f.write_str("StLock(..)")
1210    }
1211}
1212#[cfg(feature = "single-thread-unsafe-locks")]
1213impl<T> StLock<T> {
1214    pub fn new(v: T) -> Self {
1215        Self {
1216            cell: std::cell::UnsafeCell::new(v),
1217        }
1218    }
1219    pub fn read(&self) -> Result<StReadGuard<'_, T>, core::convert::Infallible> {
1220        Ok(StReadGuard {
1221            r: unsafe { &*self.cell.get() },
1222        })
1223    }
1224    pub fn write(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1225        Ok(StWriteGuard {
1226            r: unsafe { &mut *self.cell.get() },
1227        })
1228    }
1229    pub fn lock(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1230        Ok(StWriteGuard {
1231            r: unsafe { &mut *self.cell.get() },
1232        })
1233    }
1234}
1235#[cfg(feature = "single-thread-unsafe-locks")]
1236pub struct StReadGuard<'a, T> {
1237    r: &'a T,
1238}
1239#[cfg(feature = "single-thread-unsafe-locks")]
1240impl<'a, T> core::ops::Deref for StReadGuard<'a, T> {
1241    type Target = T;
1242    fn deref(&self) -> &T {
1243        self.r
1244    }
1245}
1246#[cfg(feature = "single-thread-unsafe-locks")]
1247pub struct StWriteGuard<'a, T> {
1248    r: &'a mut T,
1249}
1250#[cfg(feature = "single-thread-unsafe-locks")]
1251impl<'a, T> core::ops::Deref for StWriteGuard<'a, T> {
1252    type Target = T;
1253    fn deref(&self) -> &T {
1254        self.r
1255    }
1256}
1257#[cfg(feature = "single-thread-unsafe-locks")]
1258impl<'a, T> core::ops::DerefMut for StWriteGuard<'a, T> {
1259    fn deref_mut(&mut self) -> &mut T {
1260        self.r
1261    }
1262}
1263
1264pub(crate) struct FcFontCacheShared {
1265    /// Main pattern/metadata state, guarded by a reader-writer lock.
1266    pub(crate) state: StLock<FcFontCacheInner>,
1267    /// Font fallback chain cache.
1268    pub(crate) chain_cache: StLock<std::collections::HashMap<FontChainCacheKey, FontFallbackChain>>,
1269    /// Shared file-bytes cache: content-hash → weak [`FontBytes`].
1270    pub(crate) shared_bytes: StLock<std::collections::HashMap<u64, std::sync::Weak<FontBytes>>>,
1271}
1272
1273/// The actual font-pattern state, held behind the RwLock in.
1274#[derive(Default, Debug)]
1275pub(crate) struct FcFontCacheInner {
1276    /// Disk font path -> the ids of its faces.
1277    pub(crate) by_path: BTreeMap<String, Vec<FontId>>,
1278    /// On-disk font paths.
1279    pub(crate) disk_fonts: BTreeMap<FontId, FcFontPath>,
1280    /// In-memory fonts.
1281    pub(crate) memory_fonts: BTreeMap<FontId, FcFont>,
1282    /// Metadata cache (patterns stored by ID for quick lookup).
1283    pub(crate) metadata: BTreeMap<FontId, FcPattern>,
1284    /// Normalized family/name -> the fonts that carry it.
1285    pub(crate) family_index: BTreeMap<String, alloc::vec::Vec<FontId>>,
1286    /// What generic families, missing named families, script blocks and.
1287    pub(crate) fallback_config: FcFallbackConfig,
1288}
1289
1290impl FcFontCacheInner {
1291    /// Record `id` under the normalized spellings of its family and name.
1292    pub(crate) fn index_pattern_family(&mut self, pattern: &FcPattern, id: FontId) {
1293        for key in [pattern.family.as_deref(), pattern.name.as_deref()]
1294            .into_iter()
1295            .flatten()
1296            .map(crate::utils::normalize_family_name)
1297            .filter(|k| !k.is_empty())
1298        {
1299            let slot = self.family_index.entry(key).or_default();
1300            if !slot.contains(&id) {
1301                slot.push(id);
1302            }
1303        }
1304    }
1305
1306    /// Register a font backed by a file.
1307    pub(crate) fn insert_disk_font(
1308        &mut self,
1309        mut pattern: FcPattern,
1310        id: FontId,
1311        path: FcFontPath,
1312    ) -> FontId {
1313        pattern.unicode_ranges =
1314            FcFontCache::normalize_unicode_ranges(core::mem::take(&mut pattern.unicode_ranges));
1315        if let Some(existing) = self.by_path.get(&path.path).and_then(|ids| {
1316            ids.iter().copied().find(|existing| {
1317                self.disk_fonts
1318                    .get(existing)
1319                    .is_some_and(|p| p.font_index == path.font_index)
1320                    && self.metadata.get(existing) == Some(&pattern)
1321            })
1322        }) {
1323            return existing;
1324        }
1325        self.index_pattern_family(&pattern, id);
1326        self.by_path.entry(path.path.clone()).or_default().push(id);
1327        self.disk_fonts.insert(id, path);
1328        self.metadata.insert(id, pattern);
1329        id
1330    }
1331
1332    /// Register a font held in memory.
1333    pub(crate) fn insert_memory_font(
1334        &mut self,
1335        mut pattern: FcPattern,
1336        id: FontId,
1337        font: FcFont,
1338    ) -> FontId {
1339        pattern.unicode_ranges =
1340            FcFontCache::normalize_unicode_ranges(core::mem::take(&mut pattern.unicode_ranges));
1341        let hash = crate::utils::content_dedup_hash_u64(&font.bytes);
1342        if let Some(existing) = self.memory_fonts.iter().find_map(|(existing, f)| {
1343            (f.font_index == font.font_index
1344                && self.metadata.get(existing) == Some(&pattern)
1345                && crate::utils::content_dedup_hash_u64(&f.bytes) == hash)
1346                .then_some(*existing)
1347        }) {
1348            return existing;
1349        }
1350        self.index_pattern_family(&pattern, id);
1351        self.memory_fonts.insert(id, font);
1352        self.metadata.insert(id, pattern);
1353        id
1354    }
1355}
1356
1357impl Clone for FcFontCache {
1358    /// Shallow clone — the returned handle shares the same underlying.
1359    fn clone(&self) -> Self {
1360        Self {
1361            shared: std::sync::Arc::clone(&self.shared),
1362        }
1363    }
1364}
1365
1366impl core::fmt::Debug for FcFontCache {
1367    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1368        let state = self.state_read();
1369        f.debug_struct("FcFontCache")
1370            .field("fonts", &state.metadata.len())
1371            .field("metadata_len", &state.metadata.len())
1372            .field("disk_fonts_len", &state.disk_fonts.len())
1373            .field("memory_fonts_len", &state.memory_fonts.len())
1374            .finish()
1375    }
1376}
1377
1378impl Default for FcFontCache {
1379    fn default() -> Self {
1380        Self {
1381            shared: std::sync::Arc::new(FcFontCacheShared {
1382                state: StLock::new(FcFontCacheInner::default()),
1383                chain_cache: StLock::new(std::collections::HashMap::new()),
1384                shared_bytes: StLock::new(std::collections::HashMap::new()),
1385            }),
1386        }
1387    }
1388}
1389
1390impl FcFontCache {
1391    /// The fallback configuration this cache resolves with (a copy).
1392    pub fn fallback_config(&self) -> FcFallbackConfig {
1393        self.state_read().fallback_config.clone()
1394    }
1395
1396    /// Replace the fallback configuration.
1397    pub fn set_fallback_config(&self, config: FcFallbackConfig) -> &Self {
1398        self.state_write().fallback_config = config;
1399        self.clear_chain_cache();
1400        self
1401    }
1402
1403    /// Builder-style [`set_fallback_config`](Self::set_fallback_config).
1404    pub fn with_fallback_config(self, config: FcFallbackConfig) -> Self {
1405        self.set_fallback_config(config);
1406        self
1407    }
1408
1409    /// Drop every memoized chain.
1410    pub(crate) fn clear_chain_cache(&self) {
1411        match self.shared.chain_cache.lock() {
1412            Ok(mut memo) => memo.clear(),
1413            Err(e) => match e {},
1414        }
1415    }
1416
1417    /// The configured candidates for `family`: a generic keyword's base.
1418    #[deprecated(
1419        since = "5.0.0",
1420        note = "read `fallback_config().generic_candidates(..)` / `substitutions_for(..)`"
1421    )]
1422    pub fn system_alias_prefs(&self, family: &str) -> Vec<String> {
1423        let state = self.state_read();
1424        match GenericFamily::from_css(family) {
1425            Some(generic) => state.fallback_config.generic_candidates(generic).to_vec(),
1426            None => state.fallback_config.substitutions_for(family).to_vec(),
1427        }
1428    }
1429
1430    /// Expand a CSS stack through this cache's configuration, filling gaps.
1431    #[deprecated(
1432        since = "5.0.0",
1433        note = "use `fallback_config().candidate_families(families, ranges)`"
1434    )]
1435    pub fn expand_font_families_config_first(
1436        &self,
1437        families: &[String],
1438        os: OperatingSystem,
1439        unicode_ranges: &[UnicodeRange],
1440    ) -> Vec<String> {
1441        let mut config = self.fallback_config();
1442        config.merge_defaults(&FcFallbackConfig::os_defaults(os));
1443        config.candidate_families(families, unicode_ranges)
1444    }
1445
1446    /// Acquire a read guard on the cache's state.
1447    #[inline]
1448    pub(crate) fn state_read(&self) -> StReadGuard<'_, FcFontCacheInner> {
1449        // [az-web-lift] StLock::read() is Infallible (never poisons/spins).
1450        match self.shared.state.read() {
1451            Ok(g) => g,
1452            Err(e) => match e {},
1453        }
1454    }
1455
1456    /// Acquire a write guard on the cache's state.
1457    #[inline]
1458    pub(crate) fn state_write(&self) -> StWriteGuard<'_, FcFontCacheInner> {
1459        // [az-web-lift] StLock::write() is Infallible (never poisons/spins).
1460        match self.shared.state.write() {
1461            Ok(g) => g,
1462            Err(e) => match e {},
1463        }
1464    }
1465
1466    /// Adds in-memory font files.
1467    pub fn with_memory_fonts(&self, fonts: Vec<(FcPattern, FcFont)>) -> &Self {
1468        // Auto-detect Unicode coverage for naively-registered fonts before locking.
1469        let fonts: Vec<(FcPattern, FcFont)> = fonts
1470            .into_iter()
1471            .map(|(pattern, font)| (Self::populate_memory_font_ranges(pattern, &font), font))
1472            .collect();
1473        let mut state = self.state_write();
1474        for (pattern, font) in fonts {
1475            let id = FontId::new();
1476            state.insert_memory_font(pattern, id, font);
1477        }
1478        self
1479    }
1480
1481    /// Adds a memory font with a specific ID (for testing).
1482    pub fn with_memory_font_with_id(&self, id: FontId, pattern: FcPattern, font: FcFont) -> &Self {
1483        let pattern = Self::populate_memory_font_ranges(pattern, &font);
1484        let mut state = self.state_write();
1485        state.insert_memory_font(pattern, id, font);
1486        self
1487    }
1488
1489    /// Fill in a memory font's `unicode_ranges` from its raw bytes when the.
1490    #[cfg(all(feature = "std", feature = "parsing"))]
1491    fn populate_memory_font_ranges(mut pattern: FcPattern, font: &FcFont) -> FcPattern {
1492        if !pattern.unicode_ranges.is_empty() {
1493            return pattern;
1494        }
1495        if let Some(faces) = FcParseFontBytes(&font.bytes, &font.id) {
1496            // Pick face matching index, else fallback to first.
1497            let ranges = faces
1498                .iter()
1499                .find(|(_, f)| f.font_index == font.font_index)
1500                .or_else(|| faces.first())
1501                .map(|(p, _)| p.unicode_ranges.clone())
1502                .unwrap_or_default();
1503            if !ranges.is_empty() {
1504                pattern.unicode_ranges = ranges;
1505            }
1506        }
1507        pattern
1508    }
1509
1510    /// Without the `parsing` feature there is no cmap/OS2 parser available,.
1511    #[cfg(not(all(feature = "std", feature = "parsing")))]
1512    fn populate_memory_font_ranges(pattern: FcPattern, _font: &FcFont) -> FcPattern {
1513        pattern
1514    }
1515
1516    /// Register a newly-parsed on-disk font.
1517    pub fn insert_builder_font(&self, pattern: FcPattern, path: FcFontPath) {
1518        let id = FontId::new();
1519        {
1520            let mut state = self.state_write();
1521            state.insert_disk_font(pattern, id, path);
1522        }
1523        // Invalidate chain cache so callers see the new font.
1524        self.clear_chain_cache();
1525    }
1526
1527    #[cfg(feature = "std")]
1528    #[doc(hidden)]
1529    pub fn chain_cache_len(&self) -> usize {
1530        self.shared.chain_cache.lock().map(|c| c.len()).unwrap_or(0)
1531    }
1532
1533    /// Insert a *fast-probed* pattern into the cache and return its.
1534    pub fn insert_fast_pattern(&self, pattern: FcPattern, path: FcFontPath) -> FontId {
1535        let id = {
1536            let mut state = self.state_write();
1537            state.insert_disk_font(pattern, FontId::new(), path)
1538        };
1539        self.clear_chain_cache();
1540        id
1541    }
1542
1543    /// Every `FontId` registered from the file at `path` (one per face and.
1544    pub fn lookup_paths_cached(&self, path: &str) -> Option<Vec<FontId>> {
1545        self.state_read()
1546            .by_path
1547            .get(path)
1548            .cloned()
1549            .filter(|ids| !ids.is_empty())
1550    }
1551
1552    /// Get font data for a given font ID.
1553    pub fn get_font_by_id(&self, id: &FontId) -> Option<OwnedFontSource> {
1554        let state = self.state_read();
1555        if let Some(font) = state.memory_fonts.get(id) {
1556            return Some(OwnedFontSource::Memory(font.clone()));
1557        }
1558        if let Some(path) = state.disk_fonts.get(id) {
1559            return Some(OwnedFontSource::Disk(path.clone()));
1560        }
1561        None
1562    }
1563
1564    /// Get metadata for a font ID.
1565    pub fn get_metadata_by_id(&self, id: &FontId) -> Option<FcPattern> {
1566        self.state_read().metadata.get(id).cloned()
1567    }
1568
1569    /// Get the font bytes for `id` as a shared [`FontBytes`].
1570    #[cfg(feature = "std")]
1571    pub fn get_font_bytes(&self, id: &FontId) -> Option<std::sync::Arc<FontBytes>> {
1572        use std::sync::Arc;
1573        match self.get_font_by_id(id)? {
1574            OwnedFontSource::Memory(font) => {
1575                Some(Arc::new(FontBytes::Owned(Arc::from(font.bytes.as_slice()))))
1576            }
1577            OwnedFontSource::Disk(path) => {
1578                let hash = path.bytes_hash;
1579                if hash != 0 {
1580                    let guard = self.shared.shared_bytes.lock().unwrap();
1581                    {
1582                        if let Some(weak) = guard.get(&hash) {
1583                            if let Some(arc) = weak.upgrade() {
1584                                return Some(arc);
1585                            }
1586                        }
1587                    }
1588                }
1589
1590                let arc = open_font_bytes_mmap(&path.path)?;
1591                if hash != 0 {
1592                    let mut guard = self.shared.shared_bytes.lock().unwrap();
1593                    {
1594                        // Overwrite any stale weak ref that failed to upgrade.
1595                        guard.insert(hash, Arc::downgrade(&arc));
1596                    }
1597                }
1598                Some(arc)
1599            }
1600        }
1601    }
1602
1603    /// Returns an empty font cache (no_std / no filesystem).
1604    #[cfg(not(feature = "std"))]
1605    pub fn build() -> Self {
1606        Self::default()
1607    }
1608
1609    /// Scans system font directories using filename heuristics (no allsorts).
1610    #[cfg(all(feature = "std", not(feature = "parsing")))]
1611    pub fn build() -> Self {
1612        Self::build_from_filenames()
1613    }
1614
1615    /// Scans and parses all system fonts via allsorts for full metadata.
1616    #[cfg(all(feature = "std", feature = "parsing"))]
1617    pub fn build() -> Self {
1618        Self::build_inner(None)
1619    }
1620
1621    /// Filename-only scan: discovers fonts on disk, guesses metadata from.
1622    #[cfg(all(feature = "std", not(feature = "parsing")))]
1623    fn build_from_filenames() -> Self {
1624        let cache = Self::default();
1625        {
1626            let mut state = cache.state_write();
1627            state.fallback_config = FcFallbackConfig::os_defaults(OperatingSystem::current());
1628            for dir in crate::config::font_directories(OperatingSystem::current()) {
1629                for path in FcCollectFontFilesRecursive(dir) {
1630                    let pattern = match pattern_from_filename(&path) {
1631                        Some(p) => p,
1632                        None => continue,
1633                    };
1634                    state.insert_disk_font(
1635                        pattern,
1636                        FontId::new(),
1637                        FcFontPath {
1638                            path: path.to_string_lossy().to_string(),
1639                            font_index: 0,
1640                            // Filename-only scan — we never read the bytes,
1641                            // so there's no dedup key. Leave as 0.
1642                            bytes_hash: 0,
1643                        },
1644                    );
1645                }
1646            }
1647        }
1648        cache
1649    }
1650
1651    /// Builds a font cache with only specific font families (and their fallbacks).
1652    #[cfg(all(feature = "std", feature = "parsing"))]
1653    pub fn build_with_families(families: &[impl AsRef<str>]) -> Self {
1654        // Expand generic families to OS-specific names using built-in lists.
1655        let os = OperatingSystem::current();
1656        let mut target_families: Vec<String> = Vec::new();
1657
1658        for family in families {
1659            let family_str = family.as_ref();
1660            let expanded = FcFallbackConfig::os_defaults(os)
1661                .expand_family(family_str, DEFAULT_UNICODE_FALLBACK_SCRIPTS);
1662            if expanded.is_empty() || (expanded.len() == 1 && expanded[0] == family_str) {
1663                target_families.push(family_str.to_string());
1664            } else {
1665                target_families.extend(expanded);
1666            }
1667        }
1668
1669        Self::build_inner(Some(&target_families))
1670    }
1671
1672    /// Inner build function that handles both filtered and unfiltered font loading.
1673    #[cfg(all(feature = "std", feature = "parsing"))]
1674    fn build_inner(family_filter: Option<&[String]>) -> Self {
1675        let cache = FcFontCache::default();
1676
1677        // Normalize filter families for matching
1678        let filter_normalized: Option<Vec<String>> = family_filter.map(|families| {
1679            families
1680                .iter()
1681                .map(|f| crate::utils::normalize_family_name(f))
1682                .collect()
1683        });
1684
1685        // Helper closure to check if a pattern matches the filter
1686        let matches_filter = |pattern: &FcPattern| -> bool {
1687            match &filter_normalized {
1688                None => true, // No filter = accept all
1689                Some(targets) => {
1690                    pattern.name.as_ref().map_or(false, |name| {
1691                        let name_norm = crate::utils::normalize_family_name(name);
1692                        targets.iter().any(|target| name_norm.contains(target))
1693                    }) || pattern.family.as_ref().map_or(false, |family| {
1694                        let family_norm = crate::utils::normalize_family_name(family);
1695                        targets.iter().any(|target| family_norm.contains(target))
1696                    })
1697                }
1698            }
1699        };
1700
1701        let mut state = cache.state_write();
1702        state.fallback_config = FcFallbackConfig::os_defaults(OperatingSystem::current());
1703
1704        #[cfg(target_os = "linux")]
1705        {
1706            if let Some((font_entries, render_configs, system_aliases)) = FcScanDirectories() {
1707                // The platform configuration is the authority; the built-in
1708                // tables only fill what it leaves unsaid.
1709                let mut config = FcFallbackConfig::default();
1710                config.absorb_system_aliases(system_aliases);
1711                config.merge_defaults(&state.fallback_config);
1712                state.fallback_config = config;
1713                for (mut pattern, path) in font_entries {
1714                    if matches_filter(&pattern) {
1715                        // Apply per-font render config if a matching family rule exists
1716                        if let Some(family) = pattern.name.as_ref().or(pattern.family.as_ref()) {
1717                            if let Some(rc) = render_configs.get(family) {
1718                                pattern.render_config = rc.clone();
1719                            }
1720                        }
1721                        let id = FontId::new();
1722                        state.insert_disk_font(pattern, id, path);
1723                    }
1724                }
1725            }
1726        }
1727
1728        #[cfg(target_os = "windows")]
1729        {
1730            let system_root = std::env::var("SystemRoot")
1731                .or_else(|_| std::env::var("WINDIR"))
1732                .unwrap_or_else(|_| "C:\\Windows".to_string());
1733
1734            let user_profile =
1735                std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\Default".to_string());
1736
1737            let font_dirs = vec![
1738                (None, format!("{}\\Fonts\\", system_root)),
1739                (
1740                    None,
1741                    format!(
1742                        "{}\\AppData\\Local\\Microsoft\\Windows\\Fonts\\",
1743                        user_profile
1744                    ),
1745                ),
1746            ];
1747
1748            let font_entries = FcScanDirectoriesInner(&font_dirs);
1749            for (pattern, path) in font_entries {
1750                if matches_filter(&pattern) {
1751                    let id = FontId::new();
1752                    state.insert_disk_font(pattern, id, path);
1753                }
1754            }
1755        }
1756
1757        #[cfg(target_os = "macos")]
1758        {
1759            let font_dirs = vec![
1760                (None, "~/Library/Fonts".to_owned()),
1761                (None, "/System/Library/Fonts".to_owned()),
1762                (None, "/Library/Fonts".to_owned()),
1763                (None, "/System/Library/AssetsV2".to_owned()),
1764            ];
1765
1766            let font_entries = FcScanDirectoriesInner(&font_dirs);
1767            for (pattern, path) in font_entries {
1768                if matches_filter(&pattern) {
1769                    let id = FontId::new();
1770                    state.insert_disk_font(pattern, id, path);
1771                }
1772            }
1773        }
1774
1775        // iOS: Enumerate fonts via CoreText to bypass sandbox read_dir restrictions.
1776        #[cfg(target_os = "ios")]
1777        {
1778            let font_files = crate::mobile_ios::copy_available_font_urls();
1779            let font_entries = FcParseFontFiles(&font_files);
1780            for (pattern, path) in font_entries {
1781                if matches_filter(&pattern) {
1782                    let id = FontId::new();
1783                    state.insert_disk_font(pattern, id, path);
1784                }
1785            }
1786        }
1787
1788        // Android: Enumerate world-readable system and vendor font directories.
1789        #[cfg(target_os = "android")]
1790        {
1791            let font_dirs = vec![
1792                (None, "/system/fonts".to_owned()),
1793                (None, "/product/fonts".to_owned()),
1794                (None, "/system_ext/fonts".to_owned()),
1795                (None, "/data/fonts".to_owned()),
1796            ];
1797
1798            let font_entries = FcScanDirectoriesInner(&font_dirs);
1799            for (pattern, path) in font_entries {
1800                if matches_filter(&pattern) {
1801                    let id = FontId::new();
1802                    state.insert_disk_font(pattern, id, path);
1803                }
1804            }
1805        }
1806
1807        drop(state);
1808        cache
1809    }
1810
1811    /// Check if a font ID is a memory font (preferred over disk fonts).
1812    pub fn is_memory_font(&self, id: &FontId) -> bool {
1813        self.state_read().memory_fonts.contains_key(id)
1814    }
1815
1816    /// Every registered font with its pattern, in registration order.
1817    pub fn list(&self) -> Vec<(FcPattern, FontId)> {
1818        self.state_read()
1819            .metadata
1820            .iter()
1821            .map(|(id, pattern)| (pattern.clone(), *id))
1822            .collect()
1823    }
1824
1825    /// Visit every registered font without cloning.
1826    pub fn for_each_pattern<F: FnMut(&FcPattern, &FontId)>(&self, mut f: F) {
1827        let state = self.state_read();
1828        for (id, pattern) in &state.metadata {
1829            f(pattern, id);
1830        }
1831    }
1832
1833    pub fn is_empty(&self) -> bool {
1834        self.state_read().metadata.is_empty()
1835    }
1836
1837    /// Number of registered fonts (one per face and name record).
1838    pub fn len(&self) -> usize {
1839        self.state_read().metadata.len()
1840    }
1841
1842    /// Like [`FcFontCache::query`], but **total**: it returns `None` only when the.
1843    pub fn query_with_fallback(
1844        &self,
1845        pattern: &FcPattern,
1846        trace: &mut Vec<TraceMsg>,
1847    ) -> Option<FontMatch> {
1848        if let Some(m) = self.query(pattern, trace) {
1849            return Some(m);
1850        }
1851
1852        // 2. Drop the family/name constraint, keep how it should LOOK.
1853        if pattern.name.is_some() || pattern.family.is_some() {
1854            let relaxed = FcPattern {
1855                name: None,
1856                family: None,
1857                ..pattern.clone()
1858            };
1859            if let Some(m) = self.query(&relaxed, trace) {
1860                return Some(m);
1861            }
1862        }
1863
1864        // 3. Coverage only. Anything that can render the requested ranges.
1865        let bare = FcPattern {
1866            unicode_ranges: pattern.unicode_ranges.clone(),
1867            ..FcPattern::default()
1868        };
1869        self.query(&bare, trace)
1870    }
1871
1872    /// Queries a font from the in-memory cache, returns the first found font (early return).
1873    pub fn query(&self, pattern: &FcPattern, trace: &mut Vec<TraceMsg>) -> Option<FontMatch> {
1874        let state = self.state_read();
1875
1876        // Sort by memory vs disk, then fallback::RankKey (style, coverage, width, name).
1877        let mut matches: Vec<(bool, fallback::RankKey, FontId, &FcPattern)> = Vec::new();
1878
1879        for (id, metadata) in &state.metadata {
1880            if Self::query_matches_internal(metadata, pattern, trace) {
1881                let is_disk = !state.memory_fonts.contains_key(id);
1882                matches.push((
1883                    is_disk,
1884                    fallback::RankKey::for_request(pattern, metadata, &pattern.unicode_ranges),
1885                    *id,
1886                    metadata,
1887                ));
1888            }
1889        }
1890
1891        matches.sort();
1892
1893        matches.first().map(|(_, _, id, metadata)| FontMatch {
1894            id: *id,
1895            unicode_ranges: metadata.unicode_ranges.clone(),
1896            fallbacks: Vec::new(),
1897        })
1898    }
1899
1900    /// Get in-memory font data (cloned out of the shared state).
1901    pub fn get_memory_font(&self, id: &FontId) -> Option<FcFont> {
1902        self.state_read().memory_fonts.get(id).cloned()
1903    }
1904
1905    /// Check if a pattern matches the query, with detailed tracing.
1906    fn trace_path(k: &FcPattern) -> String {
1907        k.name
1908            .as_ref()
1909            .cloned()
1910            .unwrap_or_else(|| "<unknown>".to_string())
1911    }
1912
1913    pub fn query_matches_internal(
1914        k: &FcPattern,
1915        pattern: &FcPattern,
1916        trace: &mut Vec<TraceMsg>,
1917    ) -> bool {
1918        // Check name - substring match
1919        if let Some(ref name) = pattern.name {
1920            if !k.name.as_ref().map_or(false, |kn| kn.contains(name)) {
1921                trace.push(TraceMsg {
1922                    level: TraceLevel::Info,
1923                    path: Self::trace_path(k),
1924                    reason: MatchReason::NameMismatch {
1925                        requested: pattern.name.clone(),
1926                        found: k.name.clone(),
1927                    },
1928                });
1929                return false;
1930            }
1931        }
1932
1933        // Check family - substring match
1934        if let Some(ref family) = pattern.family {
1935            if !k.family.as_ref().map_or(false, |kf| kf.contains(family)) {
1936                trace.push(TraceMsg {
1937                    level: TraceLevel::Info,
1938                    path: Self::trace_path(k),
1939                    reason: MatchReason::FamilyMismatch {
1940                        requested: pattern.family.clone(),
1941                        found: k.family.clone(),
1942                    },
1943                });
1944                return false;
1945            }
1946        }
1947
1948        // Check style properties
1949        let style_properties = [
1950            (
1951                "italic",
1952                pattern.italic.needs_to_match(),
1953                pattern.italic.matches(&k.italic),
1954            ),
1955            (
1956                "oblique",
1957                pattern.oblique.needs_to_match(),
1958                pattern.oblique.matches(&k.oblique),
1959            ),
1960            (
1961                "bold",
1962                pattern.bold.needs_to_match(),
1963                pattern.bold.matches(&k.bold),
1964            ),
1965            (
1966                "monospace",
1967                pattern.monospace.needs_to_match(),
1968                pattern.monospace.matches(&k.monospace),
1969            ),
1970            (
1971                "condensed",
1972                pattern.condensed.needs_to_match(),
1973                pattern.condensed.matches(&k.condensed),
1974            ),
1975        ];
1976
1977        for (property_name, needs_to_match, matches) in style_properties {
1978            if needs_to_match && !matches {
1979                let (requested, found) = match property_name {
1980                    "italic" => (format!("{:?}", pattern.italic), format!("{:?}", k.italic)),
1981                    "oblique" => (format!("{:?}", pattern.oblique), format!("{:?}", k.oblique)),
1982                    "bold" => (format!("{:?}", pattern.bold), format!("{:?}", k.bold)),
1983                    "monospace" => (
1984                        format!("{:?}", pattern.monospace),
1985                        format!("{:?}", k.monospace),
1986                    ),
1987                    "condensed" => (
1988                        format!("{:?}", pattern.condensed),
1989                        format!("{:?}", k.condensed),
1990                    ),
1991                    _ => (String::new(), String::new()),
1992                };
1993
1994                trace.push(TraceMsg {
1995                    level: TraceLevel::Info,
1996                    path: Self::trace_path(k),
1997                    reason: MatchReason::StyleMismatch {
1998                        property: property_name,
1999                        requested,
2000                        found,
2001                    },
2002                });
2003                return false;
2004            }
2005        }
2006
2007        // Check weight - hard filter if non-normal weight is requested
2008        if pattern.weight != FcWeight::Normal && pattern.weight != k.weight {
2009            trace.push(TraceMsg {
2010                level: TraceLevel::Info,
2011                path: Self::trace_path(k),
2012                reason: MatchReason::WeightMismatch {
2013                    requested: pattern.weight,
2014                    found: k.weight,
2015                },
2016            });
2017            return false;
2018        }
2019
2020        // Check stretch - hard filter if non-normal stretch is requested
2021        if pattern.stretch != FcStretch::Normal && pattern.stretch != k.stretch {
2022            trace.push(TraceMsg {
2023                level: TraceLevel::Info,
2024                path: Self::trace_path(k),
2025                reason: MatchReason::StretchMismatch {
2026                    requested: pattern.stretch,
2027                    found: k.stretch,
2028                },
2029            });
2030            return false;
2031        }
2032
2033        // Check unicode ranges if specified
2034        if !pattern.unicode_ranges.is_empty() {
2035            let mut has_overlap = false;
2036
2037            for p_range in &pattern.unicode_ranges {
2038                for k_range in &k.unicode_ranges {
2039                    if p_range.overlaps(k_range) {
2040                        has_overlap = true;
2041                        break;
2042                    }
2043                }
2044                if has_overlap {
2045                    break;
2046                }
2047            }
2048
2049            if !has_overlap {
2050                trace.push(TraceMsg {
2051                    level: TraceLevel::Info,
2052                    path: Self::trace_path(k),
2053                    reason: MatchReason::UnicodeRangeMismatch {
2054                        character: '\0', // No specific character to report
2055                        ranges: k.unicode_ranges.clone(),
2056                    },
2057                });
2058                return false;
2059            }
2060        }
2061
2062        true
2063    }
2064
2065    /// Extract tokens from a font name.
2066    pub fn extract_font_name_tokens(name: &str) -> Vec<String> {
2067        let mut tokens = Vec::new();
2068        let mut current_token = String::new();
2069        let mut last_was_lower = false;
2070
2071        for c in name.chars() {
2072            if c.is_whitespace() || c == '-' || c == '_' {
2073                // Word separator
2074                if !current_token.is_empty() {
2075                    tokens.push(current_token.clone());
2076                    current_token.clear();
2077                }
2078                last_was_lower = false;
2079            } else if c.is_uppercase() && last_was_lower && !current_token.is_empty() {
2080                // CamelCase boundary (e.g., "Noto" | "Sans")
2081                tokens.push(current_token.clone());
2082                current_token.clear();
2083                current_token.push(c);
2084                last_was_lower = false;
2085            } else {
2086                current_token.push(c);
2087                last_was_lower = c.is_lowercase();
2088            }
2089        }
2090
2091        if !current_token.is_empty() {
2092            tokens.push(current_token);
2093        }
2094
2095        tokens
2096    }
2097
2098    /// Total coverage of `ranges` in codepoints (widths summed; callers.
2099    // Helper to calculate total unicode coverage
2100    pub fn calculate_unicode_coverage(ranges: &[UnicodeRange]) -> u64 {
2101        ranges
2102            .iter()
2103            .map(|range| (range.end - range.start + 1) as u64)
2104            .sum()
2105    }
2106
2107    /// Coalesce ranges into a sorted, **disjoint** set.
2108    pub fn normalize_unicode_ranges(mut ranges: Vec<UnicodeRange>) -> Vec<UnicodeRange> {
2109        if ranges.len() < 2 {
2110            return ranges;
2111        }
2112
2113        ranges.sort_unstable();
2114
2115        let mut out: Vec<UnicodeRange> = Vec::with_capacity(ranges.len());
2116        for range in ranges {
2117            match out.last_mut() {
2118                // Overlapping or touching: extend. `saturating_add` so an `end` of
2119                // u32::MAX cannot wrap around into a bogus failure-to-merge.
2120                Some(prev) if range.start <= prev.end.saturating_add(1) => {
2121                    prev.end = prev.end.max(range.end);
2122                }
2123                _ => out.push(range),
2124            }
2125        }
2126        out
2127    }
2128
2129    /// Calculate how well a font's Unicode ranges cover the requested ranges.
2130    pub fn calculate_unicode_compatibility(
2131        requested: &[UnicodeRange],
2132        available: &[UnicodeRange],
2133    ) -> i32 {
2134        if requested.is_empty() {
2135            // No specific requirements, return total coverage
2136            return Self::calculate_unicode_coverage(available) as i32;
2137        }
2138
2139        let mut total_coverage = 0u32;
2140
2141        for req_range in requested {
2142            for avail_range in available {
2143                // Calculate overlap between requested and available ranges
2144                let overlap_start = req_range.start.max(avail_range.start);
2145                let overlap_end = req_range.end.min(avail_range.end);
2146
2147                if overlap_start <= overlap_end {
2148                    // There is overlap
2149                    let overlap_size = overlap_end - overlap_start + 1;
2150                    total_coverage += overlap_size;
2151                }
2152            }
2153        }
2154
2155        total_coverage as i32
2156    }
2157
2158    pub fn calculate_style_score(original: &FcPattern, candidate: &FcPattern) -> i32 {
2159        let mut score = 0_i32;
2160
2161        // Weight calculation with special handling for bold property
2162        if (original.bold == PatternMatch::True && candidate.weight == FcWeight::Bold)
2163            || (original.bold == PatternMatch::False && candidate.weight != FcWeight::Bold)
2164        {
2165            // No weight penalty when bold is requested and font has Bold weight
2166            // No weight penalty when non-bold is requested and font has non-Bold weight
2167        } else {
2168            // Apply normal weight difference penalty
2169            let weight_diff = (original.weight as i32 - candidate.weight as i32).abs();
2170            score += weight_diff as i32;
2171        }
2172
2173        // Exact weight match bonus: reward fonts whose weight matches the request exactly,
2174        // with an extra bonus when both are Normal (the most common case for body text)
2175        if original.weight == candidate.weight {
2176            score -= 15;
2177            if original.weight == FcWeight::Normal {
2178                score -= 10; // Extra bonus for Normal-Normal match
2179            }
2180        }
2181
2182        // Stretch calculation with special handling for condensed property
2183        if (original.condensed == PatternMatch::True && candidate.stretch.is_condensed())
2184            || (original.condensed == PatternMatch::False && !candidate.stretch.is_condensed())
2185        {
2186            // No stretch penalty when condensed is requested and font has condensed stretch
2187            // No stretch penalty when non-condensed is requested and font has non-condensed stretch
2188        } else {
2189            // Apply normal stretch difference penalty
2190            let stretch_diff = (original.stretch as i32 - candidate.stretch as i32).abs();
2191            score += (stretch_diff * 100) as i32;
2192        }
2193
2194        // Handle style properties with standard penalties and bonuses
2195        let style_props = [
2196            (original.italic, candidate.italic, 300, 150),
2197            (original.oblique, candidate.oblique, 200, 100),
2198            (original.bold, candidate.bold, 300, 150),
2199            (original.monospace, candidate.monospace, 100, 50),
2200            (original.condensed, candidate.condensed, 100, 50),
2201        ];
2202
2203        for (orig, cand, mismatch_penalty, dontcare_penalty) in style_props {
2204            if orig.needs_to_match() {
2205                if orig == PatternMatch::False && cand == PatternMatch::DontCare {
2206                    // Requesting non-italic but font doesn't declare: small penalty
2207                    // (less than a full mismatch but more than a perfect match)
2208                    score += dontcare_penalty / 2;
2209                } else if !orig.matches(&cand) {
2210                    if cand == PatternMatch::DontCare {
2211                        score += dontcare_penalty;
2212                    } else {
2213                        score += mismatch_penalty;
2214                    }
2215                } else if orig == PatternMatch::True && cand == PatternMatch::True {
2216                    // Give bonus for exact True match
2217                    score -= 20;
2218                } else if orig == PatternMatch::False && cand == PatternMatch::False {
2219                    // Give bonus for exact False match (prefer explicitly non-italic
2220                    // over fonts with unknown/DontCare italic status)
2221                    score -= 20;
2222                }
2223            } else {
2224                // orig == DontCare: prefer "normal" fonts over styled ones.
2225                if cand == PatternMatch::True {
2226                    score += dontcare_penalty / 3;
2227                }
2228            }
2229        }
2230
2231        // ── Name-based "base font" detection ──
2232        // Shorter font names relative to their family imply a more "basic" variant.
2233        if let (Some(name), Some(family)) = (&candidate.name, &candidate.family) {
2234            let name_lower = name.to_ascii_lowercase();
2235            let family_lower = family.to_ascii_lowercase();
2236
2237            // Strip the family prefix from the name to get the "extra" part
2238            let extra = if name_lower.starts_with(&family_lower) {
2239                name_lower[family_lower.len()..].to_string()
2240            } else {
2241                String::new()
2242            };
2243
2244            // Strip common neutral descriptors that don't indicate a style variant
2245            let stripped = extra
2246                .replace("regular", "")
2247                .replace("normal", "")
2248                .replace("book", "")
2249                .replace("roman", "");
2250            let stripped = stripped.trim();
2251
2252            if stripped.is_empty() {
2253                // This is a "base font" – name is just the family (± "Regular")
2254                score -= 50;
2255            } else {
2256                // Name has extra style descriptors – add a penalty per extra word
2257                let extra_words = stripped.split_whitespace().count();
2258                score += (extra_words as i32) * 25;
2259            }
2260        }
2261
2262        // ── Subfamily "Regular" bonus ──
2263        if let Some(ref subfamily) = candidate.metadata.font_subfamily {
2264            let sf_lower = subfamily.to_ascii_lowercase();
2265            if sf_lower == "regular" {
2266                score -= 30;
2267            }
2268        }
2269
2270        score
2271    }
2272}
2273
2274#[cfg(all(feature = "std", feature = "parsing"))]
2275#[allow(non_snake_case, dead_code)]
2276fn FcScanDirectories() -> Option<(
2277    Vec<(FcPattern, FcFontPath)>,
2278    BTreeMap<String, FcFontRenderConfig>,
2279    BTreeMap<String, Vec<String>>,
2280)> {
2281    let config = FcSystemConfig::from_system()?;
2282    if config.font_dirs.is_empty() {
2283        return None;
2284    }
2285    let dirs: Vec<(Option<String>, String)> = config
2286        .font_dirs
2287        .iter()
2288        .map(|dir| (None, dir.to_string_lossy().into_owned()))
2289        .collect();
2290    Some((
2291        FcScanDirectoriesInner(&dirs),
2292        config.render_configs,
2293        config.aliases,
2294    ))
2295}
2296
2297/// Deepest chain of `<include>`s [`FcSystemConfig::parse_tree`] follows.
2298#[cfg(all(feature = "std", feature = "parsing"))]
2299const MAX_INCLUDE_DEPTH: usize = 64;
2300
2301/// What a fontconfig configuration tree says, as far as this crate reads.
2302#[cfg(all(feature = "std", feature = "parsing"))]
2303#[derive(Debug, Clone, Default, PartialEq)]
2304pub struct FcSystemConfig {
2305    /// `<dir>` entries, resolved (prefixes and `~` expanded), in order,.
2306    pub font_dirs: Vec<PathBuf>,
2307    /// `<match target="font">` rendering settings keyed by family.
2308    pub render_configs: BTreeMap<String, FcFontRenderConfig>,
2309    /// `<alias><family>X</family><prefer>…</prefer></alias>` entries keyed by.
2310    pub aliases: BTreeMap<String, Vec<String>>,
2311    /// Every configuration file that was read, in the order it was read.
2312    pub files: Vec<PathBuf>,
2313}
2314
2315#[cfg(all(feature = "std", feature = "parsing"))]
2316impl FcSystemConfig {
2317    /// The platform configuration: `$FONTCONFIG_FILE` when set and.
2318    pub fn from_system() -> Option<Self> {
2319        let root = std::env::var("FONTCONFIG_FILE")
2320            .ok()
2321            .filter(|p| !p.is_empty())
2322            .unwrap_or_else(|| "/etc/fonts/fonts.conf".to_string());
2323        let root = PathBuf::from(root);
2324        if !root.is_file() {
2325            return None;
2326        }
2327        Self::parse_tree(&root)
2328    }
2329
2330    /// Parse `root` and everything it includes, the way fontconfig does:.
2331    pub fn parse_tree(root: &std::path::Path) -> Option<Self> {
2332        use std::collections::VecDeque;
2333
2334        let root_dir = root.parent().map(|d| d.to_path_buf()).unwrap_or_default();
2335        let search_dirs: Vec<PathBuf> = std::env::var_os("FONTCONFIG_PATH")
2336            .map(|v| std::env::split_paths(&v).collect::<Vec<_>>())
2337            .unwrap_or_default()
2338            .into_iter()
2339            .chain(core::iter::once(root_dir))
2340            .collect();
2341
2342        let mut config = Self::default();
2343        let mut visited: alloc::collections::BTreeSet<PathBuf> =
2344            alloc::collections::BTreeSet::new();
2345        let mut queue: VecDeque<(PathBuf, usize)> = VecDeque::new();
2346        queue.push_back((root.to_path_buf(), 0));
2347
2348        while let Some((path, depth)) = queue.pop_front() {
2349            if depth > MAX_INCLUDE_DEPTH {
2350                continue;
2351            }
2352            let identity = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
2353            if !visited.insert(identity) {
2354                continue;
2355            }
2356            let Ok(metadata) = std::fs::metadata(&path) else {
2357                continue;
2358            };
2359
2360            if metadata.is_dir() {
2361                // The directory's files come next, before anything queued
2362                // after the include that named the directory.
2363                let mut entries: Vec<PathBuf> = std::fs::read_dir(&path)
2364                    .ok()?
2365                    .filter_map(|entry| entry.ok().map(|e| e.path()))
2366                    .filter(|p| std::fs::metadata(p).map(|m| m.is_file()).unwrap_or(false))
2367                    .filter(|p| {
2368                        p.file_name().map(|n| n.to_string_lossy()).is_some_and(|n| {
2369                            n.starts_with(|c: char| c.is_ascii_digit()) && n.ends_with(".conf")
2370                        })
2371                    })
2372                    .collect();
2373                entries.sort();
2374                for (i, entry) in entries.into_iter().enumerate() {
2375                    queue.insert(i, (entry, depth));
2376                }
2377                continue;
2378            }
2379            if !metadata.is_file() {
2380                continue;
2381            }
2382
2383            let Ok(xml) = std::fs::read_to_string(&path) else {
2384                continue;
2385            };
2386            let mut includes: Vec<(Option<String>, PathBuf)> = Vec::new();
2387            let mut dirs: Vec<(Option<String>, String)> = Vec::new();
2388            if ParseFontsConf(&xml, &mut includes, &mut dirs).is_none() {
2389                continue;
2390            }
2391            ParseFontsConfRenderConfig(&xml, &mut config.render_configs);
2392            ParseFontsConfAliases(&xml, &mut config.aliases);
2393            config.files.push(path.clone());
2394
2395            let here = path.parent().map(|d| d.to_path_buf()).unwrap_or_default();
2396            for (prefix, dir) in dirs {
2397                let resolved = match prefix.as_deref() {
2398                    Some("relative") => Some(here.join(dir)),
2399                    _ => process_path(&prefix, PathBuf::from(dir), false),
2400                };
2401                if let Some(dir) = resolved {
2402                    if !config.font_dirs.contains(&dir) {
2403                        config.font_dirs.push(dir);
2404                    }
2405                }
2406            }
2407
2408            // This file's includes come next, in document order, before
2409            // whatever was queued by the file that included this one.
2410            let mut position = 0;
2411            for (prefix, include) in includes {
2412                let resolved = match prefix.as_deref() {
2413                    Some("relative") => Some(here.join(include)),
2414                    Some(_) => process_path(&prefix, include, true),
2415                    None => process_path(&None, include, true).map(|expanded| {
2416                        if expanded.is_absolute() {
2417                            expanded
2418                        } else {
2419                            search_dirs
2420                                .iter()
2421                                .map(|dir| dir.join(&expanded))
2422                                .find(|candidate| candidate.exists())
2423                                .unwrap_or_else(|| {
2424                                    search_dirs
2425                                        .last()
2426                                        .map(|dir| dir.join(&expanded))
2427                                        .unwrap_or(expanded)
2428                                })
2429                        }
2430                    }),
2431                };
2432                if let Some(resolved) = resolved {
2433                    queue.insert(position, (resolved, depth + 1));
2434                    position += 1;
2435                }
2436            }
2437        }
2438
2439        // A root that could not be read (or parsed) yields no files at all.
2440        if config.files.is_empty() {
2441            return None;
2442        }
2443        Some(config)
2444    }
2445}
2446
2447/// Parse `<alias><family>NAME</family><prefer><family>...</family>...</prefer></alias>`.
2448#[cfg(all(feature = "std", feature = "parsing"))]
2449fn ParseFontsConfAliases(input: &str, aliases: &mut BTreeMap<String, Vec<String>>) {
2450    use xmlparser::Token::*;
2451    use xmlparser::Tokenizer;
2452
2453    #[derive(Clone, Copy, PartialEq)]
2454    enum State {
2455        Idle,
2456        InAlias,
2457        InAliasFamily,
2458        InPrefer,
2459        InPreferFamily,
2460    }
2461
2462    let mut state = State::Idle;
2463    let mut alias_key: Option<String> = None;
2464    let mut preferred: Vec<String> = Vec::new();
2465    let mut text_buf = String::new();
2466
2467    for token_result in Tokenizer::from(input) {
2468        let token = match token_result {
2469            Ok(token) => token,
2470            Err(_) => continue,
2471        };
2472        match token {
2473            ElementStart { local, .. } => match local.as_str() {
2474                "alias" => {
2475                    state = State::InAlias;
2476                    alias_key = None;
2477                    preferred.clear();
2478                }
2479                "family" if state == State::InAlias => {
2480                    state = State::InAliasFamily;
2481                    text_buf.clear();
2482                }
2483                "prefer" if state == State::InAlias => {
2484                    state = State::InPrefer;
2485                }
2486                "family" if state == State::InPrefer => {
2487                    state = State::InPreferFamily;
2488                    text_buf.clear();
2489                }
2490                _ => {}
2491            },
2492            Text { text } => {
2493                if state == State::InAliasFamily || state == State::InPreferFamily {
2494                    text_buf.push_str(text.as_str());
2495                }
2496            }
2497            ElementEnd { end, .. } => {
2498                use xmlparser::ElementEnd;
2499                let closed = match end {
2500                    ElementEnd::Close(_, local) => Some(local.as_str().to_owned()),
2501                    _ => None,
2502                };
2503                let Some(closed) = closed else { continue };
2504                match closed.as_str() {
2505                    "family" => match state {
2506                        State::InAliasFamily => {
2507                            let t = text_buf.trim();
2508                            if !t.is_empty() && alias_key.is_none() {
2509                                alias_key = Some(t.to_owned());
2510                            }
2511                            state = State::InAlias;
2512                        }
2513                        State::InPreferFamily => {
2514                            let t = text_buf.trim();
2515                            if !t.is_empty() {
2516                                preferred.push(t.to_owned());
2517                            }
2518                            state = State::InPrefer;
2519                        }
2520                        _ => {}
2521                    },
2522                    "prefer" if state == State::InPrefer => {
2523                        state = State::InAlias;
2524                    }
2525                    "alias" => {
2526                        if let Some(key) = alias_key.take() {
2527                            if !preferred.is_empty() {
2528                                let norm = crate::utils::normalize_family_name(&key);
2529                                let entry = aliases.entry(norm).or_default();
2530                                for fam in preferred.drain(..) {
2531                                    if !entry.iter().any(|e| e == &fam) {
2532                                        entry.push(fam);
2533                                    }
2534                                }
2535                            }
2536                        }
2537                        state = State::Idle;
2538                    }
2539                    _ => {}
2540                }
2541            }
2542            _ => {}
2543        }
2544    }
2545}
2546
2547// Parses the fonts.conf file
2548#[cfg(all(feature = "std", feature = "parsing"))]
2549fn ParseFontsConf(
2550    input: &str,
2551    paths_to_visit: &mut Vec<(Option<String>, PathBuf)>,
2552    font_paths: &mut Vec<(Option<String>, String)>,
2553) -> Option<()> {
2554    use xmlparser::Token::*;
2555    use xmlparser::Tokenizer;
2556
2557    const TAG_INCLUDE: &str = "include";
2558    const TAG_DIR: &str = "dir";
2559    const ATTRIBUTE_PREFIX: &str = "prefix";
2560
2561    let mut current_prefix: Option<&str> = None;
2562    let mut current_path: Option<&str> = None;
2563    let mut is_in_include = false;
2564    let mut is_in_dir = false;
2565
2566    for token_result in Tokenizer::from(input) {
2567        let token = match token_result {
2568            Ok(token) => token,
2569            Err(_) => return None,
2570        };
2571
2572        match token {
2573            ElementStart { local, .. } => {
2574                if is_in_include || is_in_dir {
2575                    return None; /* error: nested tags */
2576                }
2577
2578                match local.as_str() {
2579                    TAG_INCLUDE => {
2580                        is_in_include = true;
2581                    }
2582                    TAG_DIR => {
2583                        is_in_dir = true;
2584                    }
2585                    _ => continue,
2586                }
2587
2588                current_path = None;
2589            }
2590            Text { text, .. } => {
2591                let text = text.as_str().trim();
2592                if text.is_empty() {
2593                    continue;
2594                }
2595                if is_in_include || is_in_dir {
2596                    current_path = Some(text);
2597                }
2598            }
2599            Attribute { local, value, .. } => {
2600                if !is_in_include && !is_in_dir {
2601                    continue;
2602                }
2603                // attribute on <include> or <dir> node
2604                if local.as_str() == ATTRIBUTE_PREFIX {
2605                    current_prefix = Some(value.as_str());
2606                }
2607            }
2608            ElementEnd { end, .. } => {
2609                let end_tag = match end {
2610                    xmlparser::ElementEnd::Close(_, a) => a,
2611                    _ => continue,
2612                };
2613
2614                match end_tag.as_str() {
2615                    TAG_INCLUDE => {
2616                        if !is_in_include {
2617                            continue;
2618                        }
2619
2620                        if let Some(current_path) = current_path.as_ref() {
2621                            paths_to_visit.push((
2622                                current_prefix.map(ToOwned::to_owned),
2623                                PathBuf::from(*current_path),
2624                            ));
2625                        }
2626                    }
2627                    TAG_DIR => {
2628                        if !is_in_dir {
2629                            continue;
2630                        }
2631
2632                        if let Some(current_path) = current_path.as_ref() {
2633                            font_paths.push((
2634                                current_prefix.map(ToOwned::to_owned),
2635                                (*current_path).to_owned(),
2636                            ));
2637                        }
2638                    }
2639                    _ => continue,
2640                }
2641
2642                is_in_include = false;
2643                is_in_dir = false;
2644                current_path = None;
2645                current_prefix = None;
2646            }
2647            _ => {}
2648        }
2649    }
2650
2651    Some(())
2652}
2653
2654/// Parses `<match target="font">` blocks from fonts.conf XML and returns.
2655#[cfg(all(feature = "std", feature = "parsing"))]
2656fn ParseFontsConfRenderConfig(input: &str, configs: &mut BTreeMap<String, FcFontRenderConfig>) {
2657    use xmlparser::Token::*;
2658    use xmlparser::Tokenizer;
2659
2660    // Parser state machine
2661    #[derive(Clone, Copy, PartialEq)]
2662    enum State {
2663        /// Outside any relevant block.
2664        Idle,
2665        /// Inside <match target="font">.
2666        InMatchFont,
2667        /// Inside <test name="family"> within a match block.
2668        InTestFamily,
2669        /// Inside <edit name="..."> within a match block.
2670        InEdit,
2671    }
2672
2673    let mut state = State::Idle;
2674    let mut match_is_font_target = false;
2675    let mut current_family: Option<String> = None;
2676    let mut current_edit_name: Option<String> = None;
2677    let mut current_value: Option<String> = None;
2678    let mut value_tag: Option<String> = None;
2679    let mut config = FcFontRenderConfig::default();
2680    let mut in_test = false;
2681    let mut test_name: Option<String> = None;
2682
2683    for token_result in Tokenizer::from(input) {
2684        let token = match token_result {
2685            Ok(token) => token,
2686            Err(_) => continue,
2687        };
2688
2689        match token {
2690            ElementStart { local, .. } => {
2691                let tag = local.as_str();
2692                match tag {
2693                    "match" => {
2694                        // Reset state for a new match block
2695                        match_is_font_target = false;
2696                        current_family = None;
2697                        config = FcFontRenderConfig::default();
2698                    }
2699                    "test" if state == State::InMatchFont => {
2700                        in_test = true;
2701                        test_name = None;
2702                    }
2703                    "edit" if state == State::InMatchFont => {
2704                        current_edit_name = None;
2705                    }
2706                    "bool" | "double" | "const" | "string" | "int" => {
2707                        if state == State::InTestFamily || state == State::InEdit {
2708                            value_tag = Some(tag.to_owned());
2709                            current_value = None;
2710                        }
2711                    }
2712                    _ => {}
2713                }
2714            }
2715            Attribute { local, value, .. } => {
2716                let attr_name = local.as_str();
2717                let attr_value = value.as_str();
2718
2719                match attr_name {
2720                    "target" => {
2721                        if attr_value == "font" {
2722                            match_is_font_target = true;
2723                        }
2724                    }
2725                    "name" => {
2726                        if in_test && state == State::InMatchFont {
2727                            test_name = Some(attr_value.to_owned());
2728                        } else if state == State::InMatchFont {
2729                            current_edit_name = Some(attr_value.to_owned());
2730                        }
2731                    }
2732                    _ => {}
2733                }
2734            }
2735            Text { text, .. } => {
2736                let text = text.as_str().trim();
2737                if !text.is_empty() && (state == State::InTestFamily || state == State::InEdit) {
2738                    current_value = Some(text.to_owned());
2739                }
2740            }
2741            ElementEnd { end, .. } => {
2742                match end {
2743                    xmlparser::ElementEnd::Open => {
2744                        // Tag just opened (after attributes processed)
2745                        if match_is_font_target && state == State::Idle {
2746                            state = State::InMatchFont;
2747                            match_is_font_target = false;
2748                        } else if in_test {
2749                            if test_name.as_deref() == Some("family") {
2750                                state = State::InTestFamily;
2751                            }
2752                            in_test = false;
2753                        } else if current_edit_name.is_some() && state == State::InMatchFont {
2754                            state = State::InEdit;
2755                        }
2756                    }
2757                    xmlparser::ElementEnd::Close(_, local) => {
2758                        let tag = local.as_str();
2759                        match tag {
2760                            "match" => {
2761                                // End of match block: store config if we have a family
2762                                if let Some(family) = current_family.take() {
2763                                    let empty = FcFontRenderConfig::default();
2764                                    if config != empty {
2765                                        configs.insert(family, config.clone());
2766                                    }
2767                                }
2768                                state = State::Idle;
2769                                config = FcFontRenderConfig::default();
2770                            }
2771                            "test" => {
2772                                if state == State::InTestFamily {
2773                                    // Extract the family name from the value we collected
2774                                    if let Some(ref val) = current_value {
2775                                        current_family = Some(val.clone());
2776                                    }
2777                                    state = State::InMatchFont;
2778                                }
2779                                current_value = None;
2780                                value_tag = None;
2781                            }
2782                            "edit" => {
2783                                if state == State::InEdit {
2784                                    // Apply the collected value to the config
2785                                    if let (Some(ref name), Some(ref val)) =
2786                                        (&current_edit_name, &current_value)
2787                                    {
2788                                        apply_edit_value(
2789                                            &mut config,
2790                                            name,
2791                                            val,
2792                                            value_tag.as_deref(),
2793                                        );
2794                                    }
2795                                    state = State::InMatchFont;
2796                                }
2797                                current_edit_name = None;
2798                                current_value = None;
2799                                value_tag = None;
2800                            }
2801                            "bool" | "double" | "const" | "string" | "int" => {
2802                                // value_tag and current_value already set by Text handler
2803                            }
2804                            _ => {}
2805                        }
2806                    }
2807                    xmlparser::ElementEnd::Empty => {
2808                        // Self-closing tags: nothing to do
2809                    }
2810                }
2811            }
2812            _ => {}
2813        }
2814    }
2815}
2816
2817/// Apply a parsed edit value to the render config.
2818#[cfg(all(feature = "std", feature = "parsing"))]
2819fn apply_edit_value(
2820    config: &mut FcFontRenderConfig,
2821    edit_name: &str,
2822    value: &str,
2823    _value_tag: Option<&str>,
2824) {
2825    match edit_name {
2826        "antialias" => {
2827            config.antialias = parse_bool_value(value);
2828        }
2829        "hinting" => {
2830            config.hinting = parse_bool_value(value);
2831        }
2832        "autohint" => {
2833            config.autohint = parse_bool_value(value);
2834        }
2835        "embeddedbitmap" => {
2836            config.embeddedbitmap = parse_bool_value(value);
2837        }
2838        "embolden" => {
2839            config.embolden = parse_bool_value(value);
2840        }
2841        "minspace" => {
2842            config.minspace = parse_bool_value(value);
2843        }
2844        "hintstyle" => {
2845            config.hintstyle = parse_hintstyle_const(value);
2846        }
2847        "rgba" => {
2848            config.rgba = parse_rgba_const(value);
2849        }
2850        "lcdfilter" => {
2851            config.lcdfilter = parse_lcdfilter_const(value);
2852        }
2853        "dpi" => {
2854            if let Ok(v) = value.parse::<f64>() {
2855                config.dpi = Some(v);
2856            }
2857        }
2858        "scale" => {
2859            if let Ok(v) = value.parse::<f64>() {
2860                config.scale = Some(v);
2861            }
2862        }
2863        _ => {
2864            // Unknown edit property, ignore
2865        }
2866    }
2867}
2868
2869#[cfg(all(feature = "std", feature = "parsing"))]
2870fn parse_bool_value(value: &str) -> Option<bool> {
2871    match value {
2872        "true" => Some(true),
2873        "false" => Some(false),
2874        _ => None,
2875    }
2876}
2877
2878#[cfg(all(feature = "std", feature = "parsing"))]
2879fn parse_hintstyle_const(value: &str) -> Option<FcHintStyle> {
2880    match value {
2881        "hintnone" => Some(FcHintStyle::None),
2882        "hintslight" => Some(FcHintStyle::Slight),
2883        "hintmedium" => Some(FcHintStyle::Medium),
2884        "hintfull" => Some(FcHintStyle::Full),
2885        _ => None,
2886    }
2887}
2888
2889#[cfg(all(feature = "std", feature = "parsing"))]
2890fn parse_rgba_const(value: &str) -> Option<FcRgba> {
2891    match value {
2892        "unknown" => Some(FcRgba::Unknown),
2893        "rgb" => Some(FcRgba::Rgb),
2894        "bgr" => Some(FcRgba::Bgr),
2895        "vrgb" => Some(FcRgba::Vrgb),
2896        "vbgr" => Some(FcRgba::Vbgr),
2897        "none" => Some(FcRgba::None),
2898        _ => None,
2899    }
2900}
2901
2902#[cfg(all(feature = "std", feature = "parsing"))]
2903fn parse_lcdfilter_const(value: &str) -> Option<FcLcdFilter> {
2904    match value {
2905        "lcdnone" => Some(FcLcdFilter::None),
2906        "lcddefault" => Some(FcLcdFilter::Default),
2907        "lcdlight" => Some(FcLcdFilter::Light),
2908        "lcdlegacy" => Some(FcLcdFilter::Legacy),
2909        _ => None,
2910    }
2911}
2912
2913/// Intermediate parsed data from a single font face within a font file.
2914#[cfg(all(feature = "std", feature = "parsing"))]
2915struct ParsedFontFace {
2916    pattern: FcPattern,
2917    font_index: usize,
2918}
2919
2920/// Parse all font table data from a single font face and return the extracted patterns.
2921#[cfg(all(feature = "std", feature = "parsing"))]
2922fn parse_font_faces(font_bytes: &[u8]) -> Option<Vec<ParsedFontFace>> {
2923    use allsorts::{
2924        binary::read::ReadScope,
2925        font_data::FontData,
2926        get_name::fontcode_get_name,
2927        post::PostTable,
2928        tables::{os2::Os2, HeadTable, NameTable},
2929        tag,
2930    };
2931    use std::collections::BTreeSet;
2932
2933    const FONT_SPECIFIER_NAME_ID: u16 = 4;
2934    const FONT_SPECIFIER_FAMILY_ID: u16 = 1;
2935
2936    let max_fonts = if font_bytes.len() >= 12 && &font_bytes[0..4] == b"ttcf" {
2937        // Read numFonts from TTC header (offset 8, 4 bytes)
2938        let num_fonts =
2939            u32::from_be_bytes([font_bytes[8], font_bytes[9], font_bytes[10], font_bytes[11]]);
2940        // Cap at a reasonable maximum as a safety measure
2941        std::cmp::min(num_fonts as usize, 100)
2942    } else {
2943        // Not a collection, just one font
2944        1
2945    };
2946
2947    let scope = ReadScope::new(font_bytes);
2948    let font_file = scope.read::<FontData<'_>>().ok()?;
2949
2950    // Handle collections properly by iterating through all fonts
2951    let mut results = Vec::new();
2952
2953    for font_index in 0..max_fonts {
2954        let provider = font_file.table_provider(font_index).ok()?;
2955        let head_data = provider.table_data(tag::HEAD).ok()??.into_owned();
2956        let head_table = ReadScope::new(&head_data).read::<HeadTable>().ok()?;
2957
2958        let is_bold = head_table.is_bold();
2959        let is_italic = head_table.is_italic();
2960        let mut detected_monospace = None;
2961
2962        let post_data = provider.table_data(tag::POST).ok()??;
2963        if let Ok(post_table) = ReadScope::new(&post_data).read::<PostTable>() {
2964            // isFixedPitch here - https://learn.microsoft.com/en-us/typography/opentype/spec/post#header
2965            detected_monospace = Some(post_table.header.is_fixed_pitch != 0);
2966        }
2967
2968        // OS/2 table is optional; get properties if present.
2969        let os2_data = provider.table_data(tag::OS_2).ok().flatten();
2970        let os2_table = os2_data
2971            .as_deref()
2972            .and_then(|data| ReadScope::new(data).read_dep::<Os2>(data.len()).ok());
2973
2974        // Extract additional style information
2975        let is_oblique = os2_table.as_ref().is_some_and(|os2| {
2976            os2.fs_selection
2977                .contains(allsorts::tables::os2::FsSelectionFlag::OBLIQUE)
2978        });
2979        // Without OS/2 the only weight signal is the `head.macStyle` bold bit, so
2980        // the face lands on Bold or Normal rather than a precise class.
2981        let weight = os2_table.as_ref().map_or(
2982            if is_bold {
2983                FcWeight::Bold
2984            } else {
2985                FcWeight::Normal
2986            },
2987            |os2| FcWeight::from_u16(os2.us_weight_class),
2988        );
2989        let stretch = os2_table.as_ref().map_or(FcStretch::Normal, |os2| {
2990            FcStretch::from_u16(os2.us_width_class)
2991        });
2992
2993        // Coverage comes from the cmap and nothing else: every codepoint the
2994        // face maps to a real glyph, exactly. See `cmap_coverage`.
2995        let unicode_ranges = cmap_coverage(&provider).unwrap_or_default();
2996
2997        // Use the shared detect_monospace helper for PANOSE + hmtx fallback
2998        let is_monospace =
2999            detect_monospace(&provider, os2_table.as_ref(), detected_monospace).unwrap_or(false);
3000
3001        let name_data = provider.table_data(tag::NAME).ok()??.into_owned();
3002        let name_table = ReadScope::new(&name_data).read::<NameTable>().ok()?;
3003
3004        // Extract metadata from name table
3005        let mut metadata = FcFontMetadata::default();
3006
3007        const NAME_ID_COPYRIGHT: u16 = 0;
3008        const NAME_ID_FAMILY: u16 = 1;
3009        const NAME_ID_SUBFAMILY: u16 = 2;
3010        const NAME_ID_UNIQUE_ID: u16 = 3;
3011        const NAME_ID_FULL_NAME: u16 = 4;
3012        const NAME_ID_VERSION: u16 = 5;
3013        const NAME_ID_POSTSCRIPT_NAME: u16 = 6;
3014        const NAME_ID_TRADEMARK: u16 = 7;
3015        const NAME_ID_MANUFACTURER: u16 = 8;
3016        const NAME_ID_DESIGNER: u16 = 9;
3017        const NAME_ID_DESCRIPTION: u16 = 10;
3018        const NAME_ID_VENDOR_URL: u16 = 11;
3019        const NAME_ID_DESIGNER_URL: u16 = 12;
3020        const NAME_ID_LICENSE: u16 = 13;
3021        const NAME_ID_LICENSE_URL: u16 = 14;
3022        const NAME_ID_PREFERRED_FAMILY: u16 = 16;
3023        const NAME_ID_PREFERRED_SUBFAMILY: u16 = 17;
3024
3025        metadata.copyright = get_name_string(&name_data, NAME_ID_COPYRIGHT);
3026        metadata.font_family = get_name_string(&name_data, NAME_ID_FAMILY);
3027        metadata.font_subfamily = get_name_string(&name_data, NAME_ID_SUBFAMILY);
3028        metadata.full_name = get_name_string(&name_data, NAME_ID_FULL_NAME);
3029        metadata.unique_id = get_name_string(&name_data, NAME_ID_UNIQUE_ID);
3030        metadata.version = get_name_string(&name_data, NAME_ID_VERSION);
3031        metadata.postscript_name = get_name_string(&name_data, NAME_ID_POSTSCRIPT_NAME);
3032        metadata.trademark = get_name_string(&name_data, NAME_ID_TRADEMARK);
3033        metadata.manufacturer = get_name_string(&name_data, NAME_ID_MANUFACTURER);
3034        metadata.designer = get_name_string(&name_data, NAME_ID_DESIGNER);
3035        metadata.id_description = get_name_string(&name_data, NAME_ID_DESCRIPTION);
3036        metadata.designer_url = get_name_string(&name_data, NAME_ID_DESIGNER_URL);
3037        metadata.manufacturer_url = get_name_string(&name_data, NAME_ID_VENDOR_URL);
3038        metadata.license = get_name_string(&name_data, NAME_ID_LICENSE);
3039        metadata.license_url = get_name_string(&name_data, NAME_ID_LICENSE_URL);
3040        metadata.preferred_family = get_name_string(&name_data, NAME_ID_PREFERRED_FAMILY);
3041        metadata.preferred_subfamily = get_name_string(&name_data, NAME_ID_PREFERRED_SUBFAMILY);
3042
3043        // One font can support multiple patterns
3044        let mut f_family = None;
3045
3046        let patterns = name_table
3047            .name_records
3048            .iter()
3049            .filter_map(|name_record| {
3050                let name_id = name_record.name_id;
3051                if name_id == FONT_SPECIFIER_FAMILY_ID {
3052                    if let Ok(Some(family)) =
3053                        fontcode_get_name(&name_data, FONT_SPECIFIER_FAMILY_ID)
3054                    {
3055                        f_family = Some(family);
3056                    }
3057                    None
3058                } else if name_id == FONT_SPECIFIER_NAME_ID {
3059                    let family = f_family.as_ref()?;
3060                    let name = fontcode_get_name(&name_data, FONT_SPECIFIER_NAME_ID).ok()??;
3061                    if name.to_bytes().is_empty() {
3062                        None
3063                    } else {
3064                        let mut name_str = String::from_utf8_lossy(name.to_bytes()).to_string();
3065                        let mut family_str = String::from_utf8_lossy(family.as_bytes()).to_string();
3066                        if name_str.starts_with('.') {
3067                            name_str = name_str[1..].to_string();
3068                        }
3069                        if family_str.starts_with('.') {
3070                            family_str = family_str[1..].to_string();
3071                        }
3072                        Some((
3073                            FcPattern {
3074                                name: Some(name_str),
3075                                family: Some(family_str),
3076                                bold: if is_bold {
3077                                    PatternMatch::True
3078                                } else {
3079                                    PatternMatch::False
3080                                },
3081                                italic: if is_italic {
3082                                    PatternMatch::True
3083                                } else {
3084                                    PatternMatch::False
3085                                },
3086                                oblique: if is_oblique {
3087                                    PatternMatch::True
3088                                } else {
3089                                    PatternMatch::False
3090                                },
3091                                monospace: if is_monospace {
3092                                    PatternMatch::True
3093                                } else {
3094                                    PatternMatch::False
3095                                },
3096                                condensed: if stretch <= FcStretch::Condensed {
3097                                    PatternMatch::True
3098                                } else {
3099                                    PatternMatch::False
3100                                },
3101                                weight,
3102                                stretch,
3103                                unicode_ranges: unicode_ranges.clone(),
3104                                metadata: metadata.clone(),
3105                                render_config: FcFontRenderConfig::default(),
3106                            },
3107                            font_index,
3108                        ))
3109                    }
3110                } else {
3111                    None
3112                }
3113            })
3114            .collect::<BTreeSet<_>>();
3115
3116        results.extend(patterns.into_iter().map(|(pat, idx)| ParsedFontFace {
3117            pattern: pat,
3118            font_index: idx,
3119        }));
3120    }
3121
3122    if results.is_empty() {
3123        None
3124    } else {
3125        Some(results)
3126    }
3127}
3128
3129// Remaining implementation for font scanning, parsing, etc.
3130#[cfg(all(feature = "std", feature = "parsing"))]
3131pub(crate) fn FcParseFont(filepath: &PathBuf) -> Option<Vec<(FcPattern, FcFontPath)>> {
3132    #[cfg(all(not(target_family = "wasm"), feature = "std"))]
3133    use mmapio::MmapOptions;
3134    use std::fs::File;
3135
3136    // Try parsing the font file and see if the postscript name matches
3137    let file = File::open(filepath).ok()?;
3138
3139    #[cfg(all(not(target_family = "wasm"), feature = "std"))]
3140    let font_bytes = unsafe { MmapOptions::new().map(&file).ok()? };
3141
3142    #[cfg(not(all(not(target_family = "wasm"), feature = "std")))]
3143    let font_bytes = std::fs::read(filepath).ok()?;
3144
3145    let faces = parse_font_faces(&font_bytes[..])?;
3146    let path_str = filepath.to_string_lossy().to_string();
3147    // Hash once per file using cheap sampled hash to avoid full file page-fault.
3148    let bytes_hash = crate::utils::content_dedup_hash_u64(&font_bytes[..]);
3149
3150    Some(
3151        faces
3152            .into_iter()
3153            .map(|face| {
3154                (
3155                    face.pattern,
3156                    FcFontPath {
3157                        path: path_str.clone(),
3158                        font_index: face.font_index,
3159                        bytes_hash,
3160                    },
3161                )
3162            })
3163            .collect(),
3164    )
3165}
3166
3167/// Coverage info returned by a fast-probe parse.
3168#[cfg(all(feature = "std", feature = "parsing"))]
3169#[derive(Debug, Clone)]
3170pub struct FastCoverage {
3171    /// Metadata pattern with `unicode_ranges` populated from the.
3172    pub pattern: FcPattern,
3173    /// Subset of the input codepoints that this face covers (maps.
3174    pub covered: alloc::collections::BTreeSet<char>,
3175    /// `head.macStyle.bold` (bit 0).
3176    pub is_bold: bool,
3177    /// `head.macStyle.italic` (bit 1).
3178    pub is_italic: bool,
3179}
3180
3181/// Fast per-face coverage probe.
3182#[cfg(all(feature = "std", feature = "parsing"))]
3183#[allow(non_snake_case)]
3184pub fn FcParseFontFaceFast(
3185    font_bytes: &[u8],
3186    font_index: usize,
3187    codepoints: &alloc::collections::BTreeSet<char>,
3188) -> Option<FastCoverage> {
3189    use allsorts::{
3190        binary::read::ReadScope,
3191        font_data::FontData,
3192        tables::{
3193            cmap::{Cmap, CmapSubtable},
3194            FontTableProvider, HeadTable,
3195        },
3196        tag,
3197    };
3198
3199    let scope = ReadScope::new(font_bytes);
3200    let font_file = scope.read::<FontData<'_>>().ok()?;
3201    let provider = font_file.table_provider(font_index).ok()?;
3202
3203    // head — 54 bytes, macStyle at offset 44. Cheap.
3204    let head_data = provider.table_data(tag::HEAD).ok()??;
3205    let head_table = ReadScope::new(&head_data).read::<HeadTable>().ok()?;
3206    let is_bold = head_table.is_bold();
3207    let is_italic = head_table.is_italic();
3208
3209    // cmap — find the best Unicode subtable, probe each codepoint.
3210    // The mmap page-cache only faults in the bytes we touch.
3211    let cmap_data = provider.table_data(tag::CMAP).ok()??;
3212    let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
3213    let encoding_record = find_best_cmap_subtable(&cmap)?;
3214    let cmap_subtable = ReadScope::new(&cmap_data)
3215        .offset(encoding_record.offset as usize)
3216        .read::<CmapSubtable<'_>>()
3217        .ok()?;
3218
3219    let mut covered: alloc::collections::BTreeSet<char> = alloc::collections::BTreeSet::new();
3220    for ch in codepoints {
3221        if matches!(cmap_subtable.map_glyph(*ch as u32), Ok(Some(gid)) if gid != 0) {
3222            covered.insert(*ch);
3223        }
3224    }
3225    // Full coverage from subtable segments, matching scan path storage.
3226    let covered_ranges =
3227        coverage_from_subtable(&cmap_subtable, &cmap_data, encoding_record.offset as usize)
3228            .unwrap_or_default();
3229
3230    let weight = if is_bold {
3231        FcWeight::Bold
3232    } else {
3233        FcWeight::Normal
3234    };
3235    let italic_match = if is_italic {
3236        PatternMatch::True
3237    } else {
3238        PatternMatch::False
3239    };
3240
3241    let pattern = FcPattern {
3242        name: None,
3243        family: None,
3244        weight,
3245        italic: italic_match,
3246        oblique: PatternMatch::DontCare,
3247        monospace: PatternMatch::DontCare,
3248        unicode_ranges: covered_ranges,
3249        ..Default::default()
3250    };
3251
3252    Some(FastCoverage {
3253        pattern,
3254        covered,
3255        is_bold,
3256        is_italic,
3257    })
3258}
3259
3260/// Count the number of faces inside a TTC, or `1` for a single-face.
3261#[cfg(all(feature = "std", feature = "parsing"))]
3262#[allow(non_snake_case)]
3263pub fn FcCountFontFaces(font_bytes: &[u8]) -> usize {
3264    if font_bytes.len() >= 12 && &font_bytes[0..4] == b"ttcf" {
3265        let num_fonts =
3266            u32::from_be_bytes([font_bytes[8], font_bytes[9], font_bytes[10], font_bytes[11]]);
3267        // Same cap as parse_font_faces, for safety.
3268        std::cmp::min(num_fonts as usize, 100).max(1)
3269    } else {
3270        1
3271    }
3272}
3273
3274/// Parse font bytes and extract font patterns for in-memory fonts.
3275#[cfg(all(feature = "std", feature = "parsing"))]
3276#[allow(non_snake_case)]
3277pub fn FcParseFontBytes(font_bytes: &[u8], font_id: &str) -> Option<Vec<(FcPattern, FcFont)>> {
3278    FcParseFontBytesInner(font_bytes, font_id)
3279}
3280
3281/// Internal implementation for parsing font bytes.
3282#[cfg(all(feature = "std", feature = "parsing"))]
3283fn FcParseFontBytesInner(font_bytes: &[u8], font_id: &str) -> Option<Vec<(FcPattern, FcFont)>> {
3284    let faces = parse_font_faces(font_bytes)?;
3285    let id = font_id.to_string();
3286    let bytes = font_bytes.to_vec();
3287
3288    Some(
3289        faces
3290            .into_iter()
3291            .map(|face| {
3292                (
3293                    face.pattern,
3294                    FcFont {
3295                        bytes: bytes.clone(),
3296                        font_index: face.font_index,
3297                        id: id.clone(),
3298                    },
3299                )
3300            })
3301            .collect(),
3302    )
3303}
3304
3305#[cfg(all(feature = "std", feature = "parsing"))]
3306fn FcScanDirectoriesInner(paths: &[(Option<String>, String)]) -> Vec<(FcPattern, FcFontPath)> {
3307    #[cfg(all(feature = "multithreading", not(target_family = "wasm")))]
3308    {
3309        use rayon::prelude::*;
3310
3311        // scan directories in parallel
3312        paths
3313            .par_iter()
3314            .filter_map(|(prefix, p)| {
3315                process_path(prefix, PathBuf::from(p), false).map(FcScanSingleDirectoryRecursive)
3316            })
3317            .flatten()
3318            .collect()
3319    }
3320    // wasm has no rayon (it's target-gated off), so even with `multithreading`
3321    // enabled wasm falls back to the sequential path.
3322    #[cfg(not(all(feature = "multithreading", not(target_family = "wasm"))))]
3323    {
3324        paths
3325            .iter()
3326            .filter_map(|(prefix, p)| {
3327                process_path(prefix, PathBuf::from(p), false).map(FcScanSingleDirectoryRecursive)
3328            })
3329            .flatten()
3330            .collect()
3331    }
3332}
3333
3334/// Font files under `dir`: see [`crate::utils::collect_font_files`] (cycle-safe,.
3335#[cfg(feature = "std")]
3336#[allow(non_snake_case)]
3337fn FcCollectFontFilesRecursive(dir: PathBuf) -> Vec<PathBuf> {
3338    crate::utils::collect_font_files(&dir)
3339}
3340
3341#[cfg(all(feature = "std", feature = "parsing"))]
3342fn FcScanSingleDirectoryRecursive(dir: PathBuf) -> Vec<(FcPattern, FcFontPath)> {
3343    let files = FcCollectFontFilesRecursive(dir);
3344    FcParseFontFiles(&files)
3345}
3346
3347#[cfg(all(feature = "std", feature = "parsing"))]
3348fn FcParseFontFiles(files_to_parse: &[PathBuf]) -> Vec<(FcPattern, FcFontPath)> {
3349    let result = {
3350        #[cfg(all(feature = "multithreading", not(target_family = "wasm")))]
3351        {
3352            use rayon::prelude::*;
3353
3354            files_to_parse
3355                .par_iter()
3356                .filter_map(|file| FcParseFont(file))
3357                .collect::<Vec<Vec<_>>>()
3358        }
3359        #[cfg(not(all(feature = "multithreading", not(target_family = "wasm"))))]
3360        {
3361            files_to_parse
3362                .iter()
3363                .filter_map(|file| FcParseFont(file))
3364                .collect::<Vec<Vec<_>>>()
3365        }
3366    };
3367
3368    result.into_iter().flat_map(|f| f.into_iter()).collect()
3369}
3370
3371#[cfg(all(feature = "std", feature = "parsing"))]
3372/// Takes a path & prefix and resolves them to a usable path, or `None` if they're unsupported/unavailable.
3373fn process_path(
3374    prefix: &Option<String>,
3375    mut path: PathBuf,
3376    is_include_path: bool,
3377) -> Option<PathBuf> {
3378    use std::env::var;
3379
3380    const HOME_SHORTCUT: &str = "~";
3381    const CWD_PATH: &str = ".";
3382
3383    const HOME_ENV_VAR: &str = "HOME";
3384    const XDG_CONFIG_HOME_ENV_VAR: &str = "XDG_CONFIG_HOME";
3385    const XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX: &str = ".config";
3386    const XDG_DATA_HOME_ENV_VAR: &str = "XDG_DATA_HOME";
3387    const XDG_DATA_HOME_DEFAULT_PATH_SUFFIX: &str = ".local/share";
3388
3389    const PREFIX_CWD: &str = "cwd";
3390    const PREFIX_DEFAULT: &str = "default";
3391    const PREFIX_XDG: &str = "xdg";
3392
3393    // These three could, in theory, be cached, but the work required to do so outweighs the minor benefits
3394    fn get_home_value() -> Option<PathBuf> {
3395        var(HOME_ENV_VAR).ok().map(PathBuf::from)
3396    }
3397    fn get_xdg_config_home_value() -> Option<PathBuf> {
3398        var(XDG_CONFIG_HOME_ENV_VAR)
3399            .ok()
3400            .map(PathBuf::from)
3401            .or_else(|| {
3402                get_home_value()
3403                    .map(|home_path| home_path.join(XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX))
3404            })
3405    }
3406    fn get_xdg_data_home_value() -> Option<PathBuf> {
3407        var(XDG_DATA_HOME_ENV_VAR)
3408            .ok()
3409            .map(PathBuf::from)
3410            .or_else(|| {
3411                get_home_value().map(|home_path| home_path.join(XDG_DATA_HOME_DEFAULT_PATH_SUFFIX))
3412            })
3413    }
3414
3415    // Resolve the tilde character in the path, if present
3416    if path.starts_with(HOME_SHORTCUT) {
3417        if let Some(home_path) = get_home_value() {
3418            path = home_path.join(
3419                path.strip_prefix(HOME_SHORTCUT)
3420                    .expect("already checked that it starts with the prefix"),
3421            );
3422        } else {
3423            return None;
3424        }
3425    }
3426
3427    // Resolve prefix values
3428    match prefix {
3429        Some(prefix) => match prefix.as_str() {
3430            PREFIX_CWD | PREFIX_DEFAULT => {
3431                let mut new_path = PathBuf::from(CWD_PATH);
3432                new_path.push(path);
3433
3434                Some(new_path)
3435            }
3436            PREFIX_XDG => {
3437                if is_include_path {
3438                    get_xdg_config_home_value()
3439                        .map(|xdg_config_home_path| xdg_config_home_path.join(path))
3440                } else {
3441                    get_xdg_data_home_value()
3442                        .map(|xdg_data_home_path| xdg_data_home_path.join(path))
3443                }
3444            }
3445            _ => None, // Unsupported prefix
3446        },
3447        None => Some(path),
3448    }
3449}
3450
3451// Helper function to extract a string from the name table
3452#[cfg(all(feature = "std", feature = "parsing"))]
3453fn get_name_string(name_data: &[u8], name_id: u16) -> Option<String> {
3454    fontcode_get_name(name_data, name_id)
3455        .ok()
3456        .flatten()
3457        .map(|name| String::from_utf8_lossy(name.to_bytes()).to_string())
3458}
3459
3460/// Find the best Unicode CMAP subtable from a font provider.
3461#[cfg(all(feature = "std", feature = "parsing"))]
3462fn find_best_cmap_subtable<'a>(
3463    cmap: &allsorts::tables::cmap::Cmap<'a>,
3464) -> Option<allsorts::tables::cmap::EncodingRecord> {
3465    use allsorts::tables::cmap::{EncodingId, PlatformId};
3466
3467    // Full-repertoire subtables first, BMP-only ones after.
3468    cmap.find_subtable(PlatformId::UNICODE, EncodingId(4))
3469        .or_else(|| cmap.find_subtable(PlatformId::WINDOWS, EncodingId(10)))
3470        .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(3)))
3471        .or_else(|| cmap.find_subtable(PlatformId::WINDOWS, EncodingId(1)))
3472        .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(0)))
3473        .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(1)))
3474}
3475
3476/// Exact coverage of a font face: every codepoint its best Unicode cmap.
3477#[cfg(all(feature = "std", feature = "parsing"))]
3478fn cmap_coverage(provider: &impl FontTableProvider) -> Option<Vec<UnicodeRange>> {
3479    use allsorts::binary::read::ReadScope;
3480    use allsorts::tables::cmap::{Cmap, CmapSubtable};
3481
3482    let cmap_data = provider.table_data(tag::CMAP).ok()??;
3483    let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
3484    let record = find_best_cmap_subtable(&cmap)?;
3485    let subtable = ReadScope::new(&cmap_data)
3486        .offset(record.offset as usize)
3487        .read::<CmapSubtable<'_>>()
3488        .ok()?;
3489    coverage_from_subtable(&subtable, &cmap_data, record.offset as usize)
3490}
3491
3492/// See [`cmap_coverage`].
3493#[cfg(all(feature = "std", feature = "parsing"))]
3494fn coverage_from_subtable(
3495    subtable: &allsorts::tables::cmap::CmapSubtable<'_>,
3496    cmap_data: &[u8],
3497    offset: usize,
3498) -> Option<Vec<UnicodeRange>> {
3499    use allsorts::tables::cmap::CmapSubtable;
3500
3501    let mut ranges: Vec<UnicodeRange> = Vec::new();
3502    let mut push = |start: u32, end: u32| {
3503        if start > end {
3504            return;
3505        }
3506        match ranges.last_mut() {
3507            Some(last) if start <= last.end.saturating_add(1) => last.end = last.end.max(end),
3508            _ => ranges.push(UnicodeRange { start, end }),
3509        }
3510    };
3511
3512    match subtable {
3513        CmapSubtable::Format4(f4) => {
3514            let seg_count = f4.start_codes.len();
3515            let glyph_ids: Vec<u16> = f4.glyph_id_array.iter().collect();
3516            let segments = f4
3517                .start_codes
3518                .iter()
3519                .zip(f4.end_codes.iter())
3520                .zip(f4.id_deltas.iter())
3521                .zip(f4.id_range_offsets.iter())
3522                .enumerate();
3523            for (i, (((start, end), delta), range_offset)) in segments {
3524                if start == 0xFFFF {
3525                    continue; // the mandatory terminal segment
3526                }
3527                let (start, end) = (start as u32, end as u32);
3528                if range_offset == 0 {
3529                    // gid = code + delta (mod 2^16): exactly one code of the
3530                    // segment can land on glyph 0.
3531                    let zero_code = (delta as u16).wrapping_neg() as u32;
3532                    if zero_code >= start && zero_code <= end {
3533                        if zero_code > start {
3534                            push(start, zero_code - 1);
3535                        }
3536                        if zero_code < end {
3537                            push(zero_code + 1, end);
3538                        }
3539                    } else {
3540                        push(start, end);
3541                    }
3542                } else {
3543                    // glyphIdArray-indexed segment (OpenType cmap format 4).
3544                    let base = (range_offset as usize / 2).wrapping_sub(seg_count - i);
3545                    for code in start..=end {
3546                        let index = base.wrapping_add((code - start) as usize);
3547                        let Some(&value) = glyph_ids.get(index) else {
3548                            continue;
3549                        };
3550                        if value != 0 && value.wrapping_add(delta as u16) != 0 {
3551                            push(code, code);
3552                        }
3553                    }
3554                }
3555            }
3556        }
3557        CmapSubtable::Format12 { .. } => {
3558            for (start, end, start_gid) in format12_groups(cmap_data, offset)? {
3559                // gid = start_gid + (code - start): only the first code of a
3560                // group that starts at glyph 0 maps to .notdef.
3561                let first = if start_gid == 0 {
3562                    start.saturating_add(1)
3563                } else {
3564                    start
3565                };
3566                push(first, end.min(0x10FFFF));
3567            }
3568        }
3569        CmapSubtable::Format0 { glyph_id_array, .. } => {
3570            for (code, gid) in glyph_id_array.iter().enumerate() {
3571                if gid != 0 {
3572                    push(code as u32, code as u32);
3573                }
3574            }
3575        }
3576        CmapSubtable::Format6 {
3577            first_code,
3578            glyph_id_array,
3579            ..
3580        } => {
3581            for (i, gid) in glyph_id_array.iter().enumerate() {
3582                if gid != 0 {
3583                    let code = *first_code as u32 + i as u32;
3584                    push(code, code);
3585                }
3586            }
3587        }
3588        CmapSubtable::Format10 {
3589            start_char_code,
3590            glyph_id_array,
3591            ..
3592        } => {
3593            for (i, gid) in glyph_id_array.iter().enumerate() {
3594                if gid != 0 {
3595                    let code = *start_char_code + i as u32;
3596                    push(code, code);
3597                }
3598            }
3599        }
3600        CmapSubtable::Format2 { .. } => {
3601            // Legacy mixed 8/16-bit CJK encodings — never a Unicode subtable,
3602            // but if it is all the font has, enumerate it.
3603            let mut codes: Vec<u32> = Vec::new();
3604            subtable
3605                .mappings_fn(|code, gid| {
3606                    if gid != 0 {
3607                        codes.push(code);
3608                    }
3609                })
3610                .ok()?;
3611            codes.sort_unstable();
3612            for code in codes {
3613                push(code, code);
3614            }
3615        }
3616    }
3617
3618    if ranges.is_empty() {
3619        None
3620    } else {
3621        Some(FcFontCache::normalize_unicode_ranges(ranges))
3622    }
3623}
3624
3625/// The `(startCharCode, endCharCode, startGlyphID)` groups of the format-12.
3626#[cfg(all(feature = "std", feature = "parsing"))]
3627fn format12_groups(cmap_data: &[u8], offset: usize) -> Option<Vec<(u32, u32, u32)>> {
3628    let table = cmap_data.get(offset..)?;
3629    let u16_at = |at: usize| {
3630        table
3631            .get(at..at + 2)
3632            .map(|b| u16::from_be_bytes([b[0], b[1]]))
3633    };
3634    let u32_at = |at: usize| {
3635        table
3636            .get(at..at + 4)
3637            .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
3638    };
3639    if u16_at(0)? != 12 {
3640        return None;
3641    }
3642    let num_groups = u32_at(12)? as usize;
3643    let mut groups = Vec::with_capacity(num_groups.min(1 << 16));
3644    for i in 0..num_groups {
3645        let at = 16 + i * 12;
3646        groups.push((u32_at(at)?, u32_at(at + 4)?, u32_at(at + 8)?));
3647    }
3648    Some(groups)
3649}
3650
3651// Helper function to detect if a font is monospace
3652#[cfg(all(feature = "std", feature = "parsing"))]
3653fn detect_monospace(
3654    provider: &impl FontTableProvider,
3655    os2_table: Option<&Os2>,
3656    detected_monospace: Option<bool>,
3657) -> Option<bool> {
3658    if let Some(is_monospace) = detected_monospace {
3659        return Some(is_monospace);
3660    }
3661
3662    // Try using PANOSE classification, when there is an OS/2 table to read it
3663    // from; otherwise fall straight through to the hmtx width check.
3664    if let Some(os2_table) = os2_table {
3665        if os2_table.panose[0] == 2 {
3666            // 2 = Latin Text
3667            return Some(os2_table.panose[3] == 9); // 9 = Monospaced
3668        }
3669    }
3670
3671    // Check glyph widths in hmtx table
3672    let hhea_data = provider.table_data(tag::HHEA).ok()??;
3673    let hhea_table = ReadScope::new(&hhea_data).read::<HheaTable>().ok()?;
3674    let maxp_data = provider.table_data(tag::MAXP).ok()??;
3675    let maxp_table = ReadScope::new(&maxp_data).read::<MaxpTable>().ok()?;
3676    let hmtx_data = provider.table_data(tag::HMTX).ok()??;
3677    let hmtx_table = ReadScope::new(&hmtx_data)
3678        .read_dep::<HmtxTable<'_>>((
3679            usize::from(maxp_table.num_glyphs),
3680            usize::from(hhea_table.num_h_metrics),
3681        ))
3682        .ok()?;
3683
3684    let mut monospace = true;
3685    let mut last_advance = 0;
3686
3687    // Check if all advance widths are the same
3688    for i in 0..hhea_table.num_h_metrics as usize {
3689        let advance = hmtx_table.h_metrics.read_item(i).ok()?.advance_width;
3690        if i > 0 && advance != last_advance {
3691            monospace = false;
3692            break;
3693        }
3694        last_advance = advance;
3695    }
3696
3697    Some(monospace)
3698}
3699
3700/// Guess font metadata from a filename using the existing tokenizer.
3701#[cfg(all(feature = "std", not(feature = "parsing")))]
3702fn pattern_from_filename(path: &std::path::Path) -> Option<FcPattern> {
3703    let ext = path.extension()?.to_str()?.to_ascii_lowercase();
3704    match ext.as_str() {
3705        "ttf" | "otf" | "ttc" | "woff" | "woff2" => {}
3706        _ => return None,
3707    }
3708
3709    let stem = path.file_stem()?.to_str()?;
3710    let all_tokens = crate::config::tokenize_lowercase(stem);
3711
3712    // Style detection: check if any token matches a known style keyword
3713    let has_token = |kw: &str| all_tokens.iter().any(|t| t == kw);
3714    let is_bold = has_token("bold") || has_token("heavy");
3715    let is_italic = has_token("italic");
3716    let is_oblique = has_token("oblique");
3717    let is_mono = has_token("mono") || has_token("monospace");
3718    let is_condensed = has_token("condensed");
3719
3720    // Family = non-style tokens joined
3721    let family_tokens = crate::config::tokenize_font_stem(stem);
3722    if family_tokens.is_empty() {
3723        return None;
3724    }
3725    let family = family_tokens.join(" ");
3726
3727    Some(FcPattern {
3728        name: Some(stem.to_string()),
3729        family: Some(family),
3730        bold: if is_bold {
3731            PatternMatch::True
3732        } else {
3733            PatternMatch::False
3734        },
3735        italic: if is_italic {
3736            PatternMatch::True
3737        } else {
3738            PatternMatch::False
3739        },
3740        oblique: if is_oblique {
3741            PatternMatch::True
3742        } else {
3743            PatternMatch::DontCare
3744        },
3745        monospace: if is_mono {
3746            PatternMatch::True
3747        } else {
3748            PatternMatch::DontCare
3749        },
3750        condensed: if is_condensed {
3751            PatternMatch::True
3752        } else {
3753            PatternMatch::DontCare
3754        },
3755        weight: if is_bold {
3756            FcWeight::Bold
3757        } else {
3758            FcWeight::Normal
3759        },
3760        stretch: if is_condensed {
3761            FcStretch::Condensed
3762        } else {
3763            FcStretch::Normal
3764        },
3765        unicode_ranges: Vec::new(),
3766        metadata: FcFontMetadata::default(),
3767        render_config: FcFontRenderConfig::default(),
3768    })
3769}
3770
3771#[cfg(all(test, feature = "std", feature = "parsing"))]
3772mod system_alias_tests {
3773    use super::*;
3774
3775    const SAMPLE: &str = r#"<?xml version="1.0"?>
3776<fontconfig>
3777  <alias>
3778    <family>sans-serif</family>
3779    <prefer>
3780      <family>Noto Sans</family>
3781      <family>DejaVu Sans</family>
3782    </prefer>
3783  </alias>
3784  <alias>
3785    <family>Arial</family>
3786    <prefer><family>Liberation Sans</family></prefer>
3787  </alias>
3788  <alias binding="same">
3789    <family>monospace</family>
3790    <prefer><family>Noto Sans Mono</family></prefer>
3791  </alias>
3792</fontconfig>"#;
3793
3794    const SECOND_FILE: &str = r#"<fontconfig>
3795  <alias>
3796    <family>sans-serif</family>
3797    <prefer>
3798      <family>Ubuntu</family>
3799      <family>Noto Sans</family>
3800    </prefer>
3801  </alias>
3802</fontconfig>"#;
3803
3804    #[test]
3805    fn alias_blocks_parse_with_order_and_dedup_across_files() {
3806        let mut aliases = BTreeMap::new();
3807        ParseFontsConfAliases(SAMPLE, &mut aliases);
3808        ParseFontsConfAliases(SECOND_FILE, &mut aliases);
3809        let key = crate::utils::normalize_family_name("sans-serif");
3810        assert_eq!(
3811            aliases.get(&key).map(Vec::as_slice),
3812            Some(
3813                &[
3814                    "Noto Sans".to_string(),
3815                    "DejaVu Sans".to_string(),
3816                    "Ubuntu".to_string()
3817                ][..]
3818            ),
3819            "prefer entries append across files in include order, deduplicated"
3820        );
3821        assert_eq!(
3822            aliases.get("arial").map(Vec::as_slice),
3823            Some(&["Liberation Sans".to_string()][..]),
3824            "named-family aliases parse too (key normalized)"
3825        );
3826        assert_eq!(
3827            aliases.get("monospace").map(Vec::as_slice),
3828            Some(&["Noto Sans Mono".to_string()][..]),
3829            "alias attributes (binding=...) do not confuse the parser"
3830        );
3831    }
3832
3833    #[test]
3834    fn config_first_expansion_beats_the_builtin_lists() {
3835        // What `build_inner` does on Linux: the parsed aliases are the
3836        // authority, the built-in tables only fill what they leave unsaid.
3837        let mut aliases = BTreeMap::new();
3838        ParseFontsConfAliases(SAMPLE, &mut aliases);
3839        let mut config = FcFallbackConfig::default();
3840        config.absorb_system_aliases(aliases);
3841        config.merge_defaults(&FcFallbackConfig::os_defaults(OperatingSystem::Linux));
3842
3843        let cache = FcFontCache::default().with_fallback_config(config);
3844        let out = cache
3845            .fallback_config()
3846            .candidate_families(&["Arial".to_string(), "sans-serif".to_string()], &[]);
3847        assert_eq!(
3848            out,
3849            vec![
3850                "Arial".to_string(),           // named family keeps itself first
3851                "Liberation Sans".to_string(), // its configured substitution
3852                "Noto Sans".to_string(),       // sans-serif configured prefer list
3853                "DejaVu Sans".to_string(),
3854            ],
3855            "configured preferences resolve the stack; no built-in list entries leak in"
3856        );
3857    }
3858
3859    #[test]
3860    fn generic_family_without_config_falls_back_to_builtin_lists() {
3861        let mut config = FcFallbackConfig::default();
3862        config.merge_defaults(&FcFallbackConfig::os_defaults(OperatingSystem::Linux));
3863        let out = config.candidate_families(&["sans-serif".to_string()], &[]);
3864        assert!(
3865            !out.is_empty() && out.iter().any(|f| f == "DejaVu Sans"),
3866            "no configuration parsed -> the built-in candidates are the last resort: {out:?}"
3867        );
3868    }
3869}
3870
3871#[cfg(all(test, feature = "std", feature = "parsing"))]
3872mod coverage_tests {
3873    use super::*;
3874    use allsorts::binary::read::ReadScope;
3875    use allsorts::font_data::FontData;
3876    use allsorts::tables::cmap::{Cmap, CmapSubtable};
3877    use allsorts::tables::FontTableProvider;
3878
3879    const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/InstrumentSerif-Regular.ttf");
3880
3881    /// Every codepoint up to `max` the face's best subtable maps to a real.
3882    fn brute_force(bytes: &[u8], face: usize, max: u32) -> Vec<UnicodeRange> {
3883        let font = ReadScope::new(bytes)
3884            .read::<FontData<'_>>()
3885            .expect("font data");
3886        let provider = font.table_provider(face).expect("face");
3887        let cmap_data = provider
3888            .table_data(tag::CMAP)
3889            .expect("cmap")
3890            .expect("cmap present");
3891        let cmap = ReadScope::new(&cmap_data)
3892            .read::<Cmap<'_>>()
3893            .expect("cmap header");
3894        let record = find_best_cmap_subtable(&cmap).expect("a Unicode subtable");
3895        let subtable = ReadScope::new(&cmap_data)
3896            .offset(record.offset as usize)
3897            .read::<CmapSubtable<'_>>()
3898            .expect("subtable");
3899        // allsorts' format-12 `map_glyph` is a linear scan over the groups,
3900        // so enumerate those subtables once instead of probing per codepoint.
3901        let mut codes: Vec<u32> = Vec::new();
3902        if matches!(subtable, CmapSubtable::Format12 { .. }) {
3903            subtable
3904                .mappings_fn(|cp, gid| {
3905                    if gid != 0 && cp <= max {
3906                        codes.push(cp);
3907                    }
3908                })
3909                .expect("format-12 mappings");
3910            codes.sort_unstable();
3911            codes.dedup();
3912        } else {
3913            for cp in 0..=max {
3914                if (0xD800..=0xDFFF).contains(&cp) {
3915                    continue;
3916                }
3917                if matches!(subtable.map_glyph(cp), Ok(Some(gid)) if gid != 0) {
3918                    codes.push(cp);
3919                }
3920            }
3921        }
3922        let mut out: Vec<UnicodeRange> = Vec::new();
3923        for cp in codes {
3924            match out.last_mut() {
3925                Some(last) if last.end + 1 == cp => last.end = cp,
3926                _ => out.push(UnicodeRange { start: cp, end: cp }),
3927            }
3928        }
3929        out
3930    }
3931
3932    fn clipped(ranges: &[UnicodeRange], max: u32) -> Vec<UnicodeRange> {
3933        ranges
3934            .iter()
3935            .filter(|r| r.start <= max)
3936            .map(|r| UnicodeRange {
3937                start: r.start,
3938                end: r.end.min(max),
3939            })
3940            .collect()
3941    }
3942
3943    #[test]
3944    fn format12_groups_are_read_from_the_raw_table() {
3945        let mut table = Vec::new();
3946        table.extend_from_slice(&12u16.to_be_bytes()); // format
3947        table.extend_from_slice(&0u16.to_be_bytes()); // reserved
3948        table.extend_from_slice(&(16u32 + 2 * 12).to_be_bytes()); // length
3949        table.extend_from_slice(&0u32.to_be_bytes()); // language
3950        table.extend_from_slice(&2u32.to_be_bytes()); // numGroups
3951        for (start, end, gid) in [(0x20u32, 0x7Eu32, 3u32), (0x1F600, 0x1F64F, 200)] {
3952            table.extend_from_slice(&start.to_be_bytes());
3953            table.extend_from_slice(&end.to_be_bytes());
3954            table.extend_from_slice(&gid.to_be_bytes());
3955        }
3956        // Embedded at an offset, as inside a real cmap table.
3957        let mut cmap = vec![0u8; 40];
3958        cmap.extend_from_slice(&table);
3959
3960        assert_eq!(
3961            format12_groups(&cmap, 40),
3962            Some(vec![(0x20, 0x7E, 3), (0x1F600, 0x1F64F, 200)])
3963        );
3964        assert_eq!(
3965            format12_groups(&cmap, 0),
3966            None,
3967            "offset 0 is not a format-12 subtable"
3968        );
3969        assert_eq!(
3970            format12_groups(&cmap[..50], 40),
3971            None,
3972            "a truncated table is rejected"
3973        );
3974    }
3975
3976    /// The parsed coverage of the bundled fixture is exactly the set of.
3977    #[test]
3978    fn fixture_coverage_equals_the_cmap_exactly() {
3979        let faces = FcParseFontBytes(FIXTURE, "fixture").expect("the fixture parses");
3980        let parsed = &faces[0].0.unicode_ranges;
3981        assert!(!parsed.is_empty());
3982        assert_eq!(
3983            *parsed,
3984            FcFontCache::normalize_unicode_ranges(parsed.clone()),
3985            "stored coverage is normalized"
3986        );
3987
3988        let exact = brute_force(FIXTURE, 0, 0x10FFFF);
3989        assert_eq!(
3990            *parsed, exact,
3991            "segment walk and per-codepoint lookup disagree"
3992        );
3993
3994        // Sanity on the shape: a Latin text face, not a block-rounded one.
3995        assert!(crate::fallback::covers(parsed, 'A' as u32));
3996        assert!(!crate::fallback::covers(parsed, 0x4E00));
3997        let latin_ext_a = UnicodeRange {
3998            start: 0x0100,
3999            end: 0x017F,
4000        };
4001        let overlap = crate::fallback::overlap_size(parsed, &latin_ext_a);
4002        assert!(
4003            overlap > 0 && overlap < 128,
4004            "the fixture covers part of Latin Extended-A ({overlap} of 128); a block-rounded \
4005             coverage would report all or nothing"
4006        );
4007    }
4008
4009    fn with_best_subtable<R>(
4010        bytes: &[u8],
4011        face: usize,
4012        f: impl FnOnce(&CmapSubtable<'_>, &[u8], usize) -> R,
4013    ) -> Option<R> {
4014        let font = ReadScope::new(bytes).read::<FontData<'_>>().ok()?;
4015        let provider = font.table_provider(face).ok()?;
4016        let cmap_data = provider.table_data(tag::CMAP).ok()??;
4017        let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
4018        let record = find_best_cmap_subtable(&cmap)?;
4019        let subtable = ReadScope::new(&cmap_data)
4020            .offset(record.offset as usize)
4021            .read::<CmapSubtable<'_>>()
4022            .ok()?;
4023        Some(f(&subtable, &cmap_data, record.offset as usize))
4024    }
4025
4026    /// Every installed face: each parsed range starts and ends on a mapped.
4027    #[test]
4028    #[ignore]
4029    fn every_installed_font_coverage_matches_its_cmap_at_every_boundary() {
4030        fn walk(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
4031            let Ok(entries) = std::fs::read_dir(dir) else {
4032                return;
4033            };
4034            for entry in entries.flatten() {
4035                let path = entry.path();
4036                if path.is_dir() {
4037                    walk(&path, out);
4038                } else if crate::utils::is_font_file(&path) {
4039                    out.push(path);
4040                }
4041            }
4042        }
4043        let mut files = Vec::new();
4044        for dir in crate::config::font_directories(OperatingSystem::current()) {
4045            walk(&dir, &mut files);
4046        }
4047        // Bounded sample to keep tests fast (set RFC_COVERAGE_CHECK_ALL for all files).
4048        if std::env::var_os("RFC_COVERAGE_CHECK_ALL").is_none() {
4049            files.truncate(400);
4050        }
4051        let surrogate = |cp: u32| (0xD800..=0xDFFF).contains(&cp);
4052        let (mut faces_checked, mut skipped) = (0usize, 0usize);
4053        for path in &files {
4054            let Ok(bytes) = std::fs::read(path) else {
4055                continue;
4056            };
4057            let Some(faces) = FcParseFontBytes(&bytes, &path.to_string_lossy()) else {
4058                skipped += 1;
4059                continue;
4060            };
4061            let mut seen_faces = alloc::collections::BTreeSet::new();
4062            for (pattern, font) in &faces {
4063                if !seen_faces.insert(font.font_index) {
4064                    continue;
4065                }
4066                let where_ = format!("{}#{}", path.display(), font.font_index);
4067                let checked =
4068                    with_best_subtable(&bytes, font.font_index, |subtable, cmap_data, offset| {
4069                        // allsorts' format-12 `map_glyph` scans the groups linearly;
4070                        // a CJK face has thousands, so look those up by binary search.
4071                        let groups = match subtable {
4072                            CmapSubtable::Format12 { .. } => format12_groups(cmap_data, offset),
4073                            _ => None,
4074                        };
4075                        let mapped = |cp: u32| match &groups {
4076                            Some(groups) => {
4077                                let i = groups.partition_point(|g| g.1 < cp);
4078                                groups.get(i).is_some_and(|&(start, end, gid)| {
4079                                    start <= cp && cp <= end && (gid != 0 || cp != start)
4080                                })
4081                            }
4082                            None => matches!(subtable.map_glyph(cp), Ok(Some(gid)) if gid != 0),
4083                        };
4084                        for r in &pattern.unicode_ranges {
4085                            assert!(
4086                                mapped(r.start) && mapped(r.end),
4087                                "{where_}: {r:?} does not end on mapped codepoints"
4088                            );
4089                            if r.start > 0 && !surrogate(r.start - 1) {
4090                                assert!(!mapped(r.start - 1), "{where_}: {r:?} starts late");
4091                            }
4092                            if r.end < 0x10FFFF && !surrogate(r.end + 1) {
4093                                assert!(!mapped(r.end + 1), "{where_}: {r:?} ends early");
4094                            }
4095                        }
4096                    });
4097                if checked.is_some() {
4098                    faces_checked += 1;
4099                }
4100            }
4101        }
4102        println!(
4103            "checked {faces_checked} faces in {} files ({skipped} unparsable)",
4104            files.len()
4105        );
4106        assert!(faces_checked > 0, "no fonts found to check");
4107    }
4108}