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
69// As of v4.1 this crate is std-only. The v4.0 `no_std` path is gone —
70// it never supported the registry / multi-thread parsing anyway, and
71// the shared-state `FcFontCache` refactor depends on `std::sync::RwLock`
72// which is unavailable without std. Keeping the `alloc::` import paths
73// means the existing call sites in this file and submodules keep
74// compiling — in std builds `alloc` is just `core::alloc`'s companion
75// crate already linked by the standard library.
76extern crate alloc;
77
78use alloc::collections::btree_map::BTreeMap;
79use alloc::string::{String, ToString};
80use alloc::vec::Vec;
81#[cfg(all(feature = "std", feature = "parsing"))]
82use allsorts::binary::read::ReadScope;
83#[cfg(all(feature = "std", feature = "parsing"))]
84use allsorts::get_name::fontcode_get_name;
85#[cfg(all(feature = "std", feature = "parsing"))]
86use allsorts::tables::os2::Os2;
87#[cfg(all(feature = "std", feature = "parsing"))]
88use allsorts::tables::{FontTableProvider, HheaTable, HmtxTable, MaxpTable};
89#[cfg(all(feature = "std", feature = "parsing"))]
90use allsorts::tag;
91#[cfg(feature = "std")]
92use std::path::PathBuf;
93
94pub mod utils;
95#[cfg(feature = "std")]
96pub mod config;
97
98#[cfg(feature = "ffi")]
99pub mod ffi;
100
101#[cfg(feature = "async-registry")]
102pub mod scoring;
103#[cfg(feature = "async-registry")]
104pub mod registry;
105#[cfg(feature = "async-registry")]
106pub mod multithread;
107#[cfg(feature = "cache")]
108pub mod disk_cache;
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(target_os = "windows", target_os = "linux", target_os = "macos", target_os = "ios", target_os = "android", target_family = "wasm")))]
146        return OperatingSystem::Linux; // Default fallback
147    }
148    
149    /// Get system-specific fonts for the "serif" generic family
150    /// Prioritizes fonts based on Unicode range coverage
151    pub fn get_serif_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
152        let has_cjk = has_cjk_ranges(unicode_ranges);
153        let has_arabic = has_arabic_ranges(unicode_ranges);
154        let _has_cyrillic = has_cyrillic_ranges(unicode_ranges);
155        
156        match self {
157            OperatingSystem::Windows => {
158                let mut fonts = Vec::new();
159                if has_cjk {
160                    fonts.extend_from_slice(&["MS Mincho", "SimSun", "MingLiU"]);
161                }
162                if has_arabic {
163                    fonts.push("Traditional Arabic");
164                }
165                fonts.push("Times New Roman");
166                fonts.iter().map(|s| s.to_string()).collect()
167            }
168            OperatingSystem::Linux => {
169                let mut fonts = Vec::new();
170                if has_cjk {
171                    fonts.extend_from_slice(&["Noto Serif CJK SC", "Noto Serif CJK JP", "Noto Serif CJK KR"]);
172                }
173                if has_arabic {
174                    fonts.push("Noto Serif Arabic");
175                }
176                fonts.extend_from_slice(&[
177                    "Times", "Times New Roman", "DejaVu Serif", "Free Serif", 
178                    "Noto Serif", "Bitstream Vera Serif", "Roman", "Regular"
179                ]);
180                fonts.iter().map(|s| s.to_string()).collect()
181            }
182            OperatingSystem::MacOS | OperatingSystem::IOS => {
183                let mut fonts = Vec::new();
184                if has_cjk {
185                    fonts.extend_from_slice(&["Hiragino Mincho ProN", "STSong", "AppleMyungjo"]);
186                }
187                if has_arabic {
188                    fonts.push("Geeza Pro");
189                }
190                fonts.extend_from_slice(&["Times New Roman", "Times", "New York", "Palatino"]);
191                fonts.iter().map(|s| s.to_string()).collect()
192            }
193            OperatingSystem::Android => {
194                let mut fonts = Vec::new();
195                if has_cjk {
196                    fonts.extend_from_slice(&["Noto Serif CJK SC", "Noto Serif CJK JP", "Noto Serif CJK KR"]);
197                }
198                if has_arabic {
199                    fonts.push("Noto Naskh Arabic");
200                }
201                fonts.extend_from_slice(&["Noto Serif", "Roboto Serif", "Droid Serif"]);
202                fonts.iter().map(|s| s.to_string()).collect()
203            }
204            OperatingSystem::Wasm => Vec::new(),
205        }
206    }
207
208    /// Get system-specific fonts for the "sans-serif" generic family
209    /// Prioritizes fonts based on Unicode range coverage
210    pub fn get_sans_serif_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
211        let has_cjk = has_cjk_ranges(unicode_ranges);
212        let has_arabic = has_arabic_ranges(unicode_ranges);
213        let _has_cyrillic = has_cyrillic_ranges(unicode_ranges);
214        let has_hebrew = has_hebrew_ranges(unicode_ranges);
215        let has_thai = has_thai_ranges(unicode_ranges);
216        
217        match self {
218            OperatingSystem::Windows => {
219                let mut fonts = Vec::new();
220                if has_cjk {
221                    fonts.extend_from_slice(&["Microsoft YaHei", "MS Gothic", "Malgun Gothic", "SimHei"]);
222                }
223                if has_arabic {
224                    fonts.push("Segoe UI Arabic");
225                }
226                if has_hebrew {
227                    fonts.push("Segoe UI Hebrew");
228                }
229                if has_thai {
230                    fonts.push("Leelawadee UI");
231                }
232                fonts.extend_from_slice(&["Segoe UI", "Tahoma", "Microsoft Sans Serif", "MS Sans Serif", "Helv"]);
233                fonts.iter().map(|s| s.to_string()).collect()
234            }
235            OperatingSystem::Linux => {
236                let mut fonts = Vec::new();
237                if has_cjk {
238                    fonts.extend_from_slice(&[
239                        "Noto Sans CJK SC", "Noto Sans CJK JP", "Noto Sans CJK KR",
240                        "WenQuanYi Micro Hei", "Droid Sans Fallback"
241                    ]);
242                }
243                if has_arabic {
244                    fonts.push("Noto Sans Arabic");
245                }
246                if has_hebrew {
247                    fonts.push("Noto Sans Hebrew");
248                }
249                if has_thai {
250                    fonts.push("Noto Sans Thai");
251                }
252                fonts.extend_from_slice(&["Ubuntu", "Arial", "DejaVu Sans", "Noto Sans", "Liberation Sans"]);
253                fonts.iter().map(|s| s.to_string()).collect()
254            }
255            OperatingSystem::MacOS | OperatingSystem::IOS => {
256                let mut fonts = Vec::new();
257                if has_cjk {
258                    fonts.extend_from_slice(&[
259                        "Hiragino Sans", "Hiragino Kaku Gothic ProN",
260                        "PingFang SC", "PingFang TC", "Apple SD Gothic Neo"
261                    ]);
262                }
263                if has_arabic {
264                    fonts.push("Geeza Pro");
265                }
266                if has_hebrew {
267                    fonts.push("Arial Hebrew");
268                }
269                if has_thai {
270                    fonts.push("Thonburi");
271                }
272                fonts.extend_from_slice(&[
273                    "San Francisco", ".AppleSystemUIFont", ".SFUIText", ".SFUI-Regular",
274                    "Helvetica Neue", "Helvetica", "Lucida Grande",
275                ]);
276                fonts.iter().map(|s| s.to_string()).collect()
277            }
278            OperatingSystem::Android => {
279                let mut fonts = Vec::new();
280                if has_cjk {
281                    fonts.extend_from_slice(&[
282                        "Noto Sans CJK SC", "Noto Sans CJK JP", "Noto Sans CJK KR",
283                        "Droid Sans Fallback",
284                    ]);
285                }
286                if has_arabic {
287                    fonts.push("Noto Sans Arabic");
288                }
289                if has_hebrew {
290                    fonts.push("Noto Sans Hebrew");
291                }
292                if has_thai {
293                    fonts.push("Noto Sans Thai");
294                }
295                fonts.extend_from_slice(&[
296                    "Roboto", "Roboto-Regular", "Noto Sans", "Droid Sans",
297                ]);
298                fonts.iter().map(|s| s.to_string()).collect()
299            }
300            OperatingSystem::Wasm => Vec::new(),
301        }
302    }
303
304    /// Get system-specific fonts for the "monospace" generic family
305    /// Prioritizes fonts based on Unicode range coverage
306    pub fn get_monospace_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
307        let has_cjk = has_cjk_ranges(unicode_ranges);
308        
309        match self {
310            OperatingSystem::Windows => {
311                let mut fonts = Vec::new();
312                if has_cjk {
313                    fonts.extend_from_slice(&["MS Gothic", "SimHei"]);
314                }
315                fonts.extend_from_slice(&["Segoe UI Mono", "Courier New", "Cascadia Code", "Cascadia Mono", "Consolas"]);
316                fonts.iter().map(|s| s.to_string()).collect()
317            }
318            OperatingSystem::Linux => {
319                let mut fonts = Vec::new();
320                if has_cjk {
321                    fonts.extend_from_slice(&["Noto Sans Mono CJK SC", "Noto Sans Mono CJK JP", "WenQuanYi Zen Hei Mono"]);
322                }
323                fonts.extend_from_slice(&[
324                    "Source Code Pro", "Cantarell", "DejaVu Sans Mono", 
325                    "Roboto Mono", "Ubuntu Monospace", "Droid Sans Mono"
326                ]);
327                fonts.iter().map(|s| s.to_string()).collect()
328            }
329            OperatingSystem::MacOS | OperatingSystem::IOS => {
330                let mut fonts = Vec::new();
331                if has_cjk {
332                    fonts.extend_from_slice(&["Hiragino Sans", "PingFang SC"]);
333                }
334                fonts.extend_from_slice(&["SF Mono", "Menlo", "Monaco", "Courier", "Oxygen Mono", "Source Code Pro", "Fira Mono"]);
335                fonts.iter().map(|s| s.to_string()).collect()
336            }
337            OperatingSystem::Android => {
338                let mut fonts = Vec::new();
339                if has_cjk {
340                    fonts.extend_from_slice(&["Noto Sans Mono CJK SC", "Noto Sans Mono CJK JP"]);
341                }
342                fonts.extend_from_slice(&["Roboto Mono", "Droid Sans Mono", "Noto Sans Mono", "DejaVu Sans Mono"]);
343                fonts.iter().map(|s| s.to_string()).collect()
344            }
345            OperatingSystem::Wasm => Vec::new(),
346        }
347    }
348    
349    /// Expand a generic CSS font family to system-specific font names
350    /// Returns the original name if not a generic family
351    /// Prioritizes fonts based on Unicode range coverage
352    pub fn expand_generic_family(&self, family: &str, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
353        match family.to_ascii_lowercase().as_str() {
354            "serif" => self.get_serif_fonts(unicode_ranges),
355            "sans-serif" => self.get_sans_serif_fonts(unicode_ranges),
356            "monospace" => self.get_monospace_fonts(unicode_ranges),
357            "cursive" | "fantasy" | "system-ui" => {
358                // Use sans-serif as fallback for these
359                self.get_sans_serif_fonts(unicode_ranges)
360            }
361            _ => vec![family.to_string()],
362        }
363    }
364}
365
366/// Expand a CSS font-family stack with generic families resolved to OS-specific fonts
367/// Prioritizes fonts based on Unicode range coverage
368/// Example: ["Arial", "sans-serif"] on macOS with CJK ranges -> ["Arial", "PingFang SC", "Hiragino Sans", ...]
369///
370/// NOTE: this free function only sees the per-OS LAST-RESORT lists. When a
371/// system font configuration is available, prefer
372/// [`FcFontCache::expand_font_families_config_first`], which consults the
373/// parsed `<alias>`/`<prefer>` preferences (the machine's ACTUAL
374/// configuration) before any built-in list.
375pub fn expand_font_families(families: &[String], os: OperatingSystem, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
376    let mut expanded = Vec::new();
377    
378    for family in families {
379        expanded.extend(os.expand_generic_family(family, unicode_ranges));
380    }
381    
382    expanded
383}
384
385/// UUID to identify a font (collections are broken up into separate fonts)
386#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
387#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
388pub struct FontId(pub u128);
389
390impl core::fmt::Debug for FontId {
391    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
392        core::fmt::Display::fmt(self, f)
393    }
394}
395
396impl core::fmt::Display for FontId {
397    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
398        let id = self.0;
399        write!(
400            f,
401            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
402            (id >> 96) & 0xFFFFFFFF,
403            (id >> 80) & 0xFFFF,
404            (id >> 64) & 0xFFFF,
405            (id >> 48) & 0xFFFF,
406            id & 0xFFFFFFFFFFFF
407        )
408    }
409}
410
411impl FontId {
412    /// Generate a new unique FontId using an atomic counter
413    pub fn new() -> Self {
414        use core::sync::atomic::{AtomicU64, Ordering};
415        static COUNTER: AtomicU64 = AtomicU64::new(1);
416        let id = COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
417        FontId(id)
418    }
419}
420
421/// Whether a field is required to match (yes / no / don't care)
422#[derive(Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
423#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
424#[repr(C)]
425pub enum PatternMatch {
426    /// Default: don't particularly care whether the requirement matches
427    #[default]
428    DontCare,
429    /// Requirement has to be true for the selected font
430    True,
431    /// Requirement has to be false for the selected font
432    False,
433}
434
435impl PatternMatch {
436    fn needs_to_match(&self) -> bool {
437        matches!(self, PatternMatch::True | PatternMatch::False)
438    }
439
440    fn matches(&self, other: &PatternMatch) -> bool {
441        match (self, other) {
442            (PatternMatch::DontCare, _) => true,
443            (_, PatternMatch::DontCare) => true,
444            (a, b) => a == b,
445        }
446    }
447}
448
449/// Font weight values as defined in CSS specification
450#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
451#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
452#[repr(C)]
453pub enum FcWeight {
454    Thin = 100,
455    ExtraLight = 200,
456    Light = 300,
457    Normal = 400,
458    Medium = 500,
459    SemiBold = 600,
460    Bold = 700,
461    ExtraBold = 800,
462    Black = 900,
463}
464
465impl FcWeight {
466    pub fn from_u16(weight: u16) -> Self {
467        match weight {
468            0..=149 => FcWeight::Thin,
469            150..=249 => FcWeight::ExtraLight,
470            250..=349 => FcWeight::Light,
471            350..=449 => FcWeight::Normal,
472            450..=549 => FcWeight::Medium,
473            550..=649 => FcWeight::SemiBold,
474            650..=749 => FcWeight::Bold,
475            750..=849 => FcWeight::ExtraBold,
476            _ => FcWeight::Black,
477        }
478    }
479
480    pub fn find_best_match(&self, available: &[FcWeight]) -> Option<FcWeight> {
481        if available.is_empty() {
482            return None;
483        }
484
485        // Exact match
486        if available.contains(self) {
487            return Some(*self);
488        }
489
490        // Get numeric value
491        let self_value = *self as u16;
492
493        match *self {
494            FcWeight::Normal => {
495                // For Normal (400), try Medium (500) first
496                if available.contains(&FcWeight::Medium) {
497                    return Some(FcWeight::Medium);
498                }
499                // Then try lighter weights
500                for weight in &[FcWeight::Light, FcWeight::ExtraLight, FcWeight::Thin] {
501                    if available.contains(weight) {
502                        return Some(*weight);
503                    }
504                }
505                // Last, try heavier weights
506                for weight in &[
507                    FcWeight::SemiBold,
508                    FcWeight::Bold,
509                    FcWeight::ExtraBold,
510                    FcWeight::Black,
511                ] {
512                    if available.contains(weight) {
513                        return Some(*weight);
514                    }
515                }
516            }
517            FcWeight::Medium => {
518                // For Medium (500), try Normal (400) first
519                if available.contains(&FcWeight::Normal) {
520                    return Some(FcWeight::Normal);
521                }
522                // Then try lighter weights
523                for weight in &[FcWeight::Light, FcWeight::ExtraLight, FcWeight::Thin] {
524                    if available.contains(weight) {
525                        return Some(*weight);
526                    }
527                }
528                // Last, try heavier weights
529                for weight in &[
530                    FcWeight::SemiBold,
531                    FcWeight::Bold,
532                    FcWeight::ExtraBold,
533                    FcWeight::Black,
534                ] {
535                    if available.contains(weight) {
536                        return Some(*weight);
537                    }
538                }
539            }
540            FcWeight::Thin | FcWeight::ExtraLight | FcWeight::Light => {
541                // For lightweight fonts (<400), first try lighter or equal weights
542                let mut best_match = None;
543                let mut smallest_diff = u16::MAX;
544
545                // Find the closest lighter weight
546                for weight in available {
547                    let weight_value = *weight as u16;
548                    // Only consider weights <= self (per test expectation)
549                    if weight_value <= self_value {
550                        let diff = self_value - weight_value;
551                        if diff < smallest_diff {
552                            smallest_diff = diff;
553                            best_match = Some(*weight);
554                        }
555                    }
556                }
557
558                if best_match.is_some() {
559                    return best_match;
560                }
561
562                // If no lighter weight, find the closest heavier weight
563                best_match = None;
564                smallest_diff = u16::MAX;
565
566                for weight in available {
567                    let weight_value = *weight as u16;
568                    if weight_value > self_value {
569                        let diff = weight_value - self_value;
570                        if diff < smallest_diff {
571                            smallest_diff = diff;
572                            best_match = Some(*weight);
573                        }
574                    }
575                }
576
577                return best_match;
578            }
579            FcWeight::SemiBold | FcWeight::Bold | FcWeight::ExtraBold | FcWeight::Black => {
580                // For heavyweight fonts (>500), first try heavier or equal weights
581                let mut best_match = None;
582                let mut smallest_diff = u16::MAX;
583
584                // Find the closest heavier weight
585                for weight in available {
586                    let weight_value = *weight as u16;
587                    // Only consider weights >= self
588                    if weight_value >= self_value {
589                        let diff = weight_value - self_value;
590                        if diff < smallest_diff {
591                            smallest_diff = diff;
592                            best_match = Some(*weight);
593                        }
594                    }
595                }
596
597                if best_match.is_some() {
598                    return best_match;
599                }
600
601                // If no heavier weight, find the closest lighter weight
602                best_match = None;
603                smallest_diff = u16::MAX;
604
605                for weight in available {
606                    let weight_value = *weight as u16;
607                    if weight_value < self_value {
608                        let diff = self_value - weight_value;
609                        if diff < smallest_diff {
610                            smallest_diff = diff;
611                            best_match = Some(*weight);
612                        }
613                    }
614                }
615
616                return best_match;
617            }
618        }
619
620        // If nothing matches by now, return the first available weight
621        Some(available[0])
622    }
623}
624
625impl Default for FcWeight {
626    fn default() -> Self {
627        FcWeight::Normal
628    }
629}
630
631/// CSS font-stretch values
632#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
633#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
634#[repr(C)]
635pub enum FcStretch {
636    UltraCondensed = 1,
637    ExtraCondensed = 2,
638    Condensed = 3,
639    SemiCondensed = 4,
640    Normal = 5,
641    SemiExpanded = 6,
642    Expanded = 7,
643    ExtraExpanded = 8,
644    UltraExpanded = 9,
645}
646
647impl FcStretch {
648    pub fn is_condensed(&self) -> bool {
649        use self::FcStretch::*;
650        match self {
651            UltraCondensed => true,
652            ExtraCondensed => true,
653            Condensed => true,
654            SemiCondensed => true,
655            Normal => false,
656            SemiExpanded => false,
657            Expanded => false,
658            ExtraExpanded => false,
659            UltraExpanded => false,
660        }
661    }
662    pub fn from_u16(width_class: u16) -> Self {
663        match width_class {
664            1 => FcStretch::UltraCondensed,
665            2 => FcStretch::ExtraCondensed,
666            3 => FcStretch::Condensed,
667            4 => FcStretch::SemiCondensed,
668            5 => FcStretch::Normal,
669            6 => FcStretch::SemiExpanded,
670            7 => FcStretch::Expanded,
671            8 => FcStretch::ExtraExpanded,
672            9 => FcStretch::UltraExpanded,
673            _ => FcStretch::Normal,
674        }
675    }
676
677    /// Follows CSS spec for stretch matching
678    pub fn find_best_match(&self, available: &[FcStretch]) -> Option<FcStretch> {
679        if available.is_empty() {
680            return None;
681        }
682
683        if available.contains(self) {
684            return Some(*self);
685        }
686
687        // For 'normal' or condensed values, narrower widths are checked first, then wider values
688        if *self <= FcStretch::Normal {
689            // Find narrower values first
690            let mut closest_narrower = None;
691            for stretch in available.iter() {
692                if *stretch < *self
693                    && (closest_narrower.is_none() || *stretch > closest_narrower.unwrap())
694                {
695                    closest_narrower = Some(*stretch);
696                }
697            }
698
699            if closest_narrower.is_some() {
700                return closest_narrower;
701            }
702
703            // Otherwise, find wider values
704            let mut closest_wider = None;
705            for stretch in available.iter() {
706                if *stretch > *self
707                    && (closest_wider.is_none() || *stretch < closest_wider.unwrap())
708                {
709                    closest_wider = Some(*stretch);
710                }
711            }
712
713            return closest_wider;
714        } else {
715            // For expanded values, wider values are checked first, then narrower values
716            let mut closest_wider = None;
717            for stretch in available.iter() {
718                if *stretch > *self
719                    && (closest_wider.is_none() || *stretch < closest_wider.unwrap())
720                {
721                    closest_wider = Some(*stretch);
722                }
723            }
724
725            if closest_wider.is_some() {
726                return closest_wider;
727            }
728
729            // Otherwise, find narrower values
730            let mut closest_narrower = None;
731            for stretch in available.iter() {
732                if *stretch < *self
733                    && (closest_narrower.is_none() || *stretch > closest_narrower.unwrap())
734                {
735                    closest_narrower = Some(*stretch);
736                }
737            }
738
739            return closest_narrower;
740        }
741    }
742}
743
744impl Default for FcStretch {
745    fn default() -> Self {
746        FcStretch::Normal
747    }
748}
749
750/// Unicode range representation for font matching
751#[repr(C)]
752#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
753#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
754pub struct UnicodeRange {
755    pub start: u32,
756    pub end: u32,
757}
758
759/// The default set of Unicode-block fallback scripts that
760/// [`FcFontCache::resolve_font_chain`] pulls in when no explicit
761/// `scripts_hint` is supplied.
762///
763/// Keeping this exposed lets callers that *do* want the default
764/// behaviour build the set explicitly — typically by union-ing it
765/// with a detected-from-document set before calling
766/// [`FcFontCache::resolve_font_chain_with_scripts`].
767pub const DEFAULT_UNICODE_FALLBACK_SCRIPTS: &[UnicodeRange] = &[
768    UnicodeRange { start: 0x0400, end: 0x04FF }, // Cyrillic
769    UnicodeRange { start: 0x0600, end: 0x06FF }, // Arabic
770    UnicodeRange { start: 0x0900, end: 0x097F }, // Devanagari
771    UnicodeRange { start: 0x3040, end: 0x309F }, // Hiragana
772    UnicodeRange { start: 0x30A0, end: 0x30FF }, // Katakana
773    UnicodeRange { start: 0x4E00, end: 0x9FFF }, // CJK Unified Ideographs
774    UnicodeRange { start: 0xAC00, end: 0xD7A3 }, // Hangul Syllables
775];
776
777impl UnicodeRange {
778    pub fn contains(&self, c: char) -> bool {
779        let c = c as u32;
780        c >= self.start && c <= self.end
781    }
782
783    pub fn overlaps(&self, other: &UnicodeRange) -> bool {
784        self.start <= other.end && other.start <= self.end
785    }
786
787    pub fn is_subset_of(&self, other: &UnicodeRange) -> bool {
788        self.start >= other.start && self.end <= other.end
789    }
790}
791
792/// Check if any range covers CJK Unified Ideographs, Hiragana, Katakana, or Hangul
793pub fn has_cjk_ranges(ranges: &[UnicodeRange]) -> bool {
794    ranges.iter().any(|r| {
795        (r.start >= 0x4E00 && r.start <= 0x9FFF) ||
796        (r.start >= 0x3040 && r.start <= 0x309F) ||
797        (r.start >= 0x30A0 && r.start <= 0x30FF) ||
798        (r.start >= 0xAC00 && r.start <= 0xD7AF)
799    })
800}
801
802/// Check if any range covers the Arabic block
803pub fn has_arabic_ranges(ranges: &[UnicodeRange]) -> bool {
804    ranges.iter().any(|r| r.start >= 0x0600 && r.start <= 0x06FF)
805}
806
807/// Check if any range covers the Cyrillic block
808pub fn has_cyrillic_ranges(ranges: &[UnicodeRange]) -> bool {
809    ranges.iter().any(|r| r.start >= 0x0400 && r.start <= 0x04FF)
810}
811
812/// Check if any range covers the Hebrew block
813pub fn has_hebrew_ranges(ranges: &[UnicodeRange]) -> bool {
814    ranges.iter().any(|r| r.start >= 0x0590 && r.start <= 0x05FF)
815}
816
817/// Check if any range covers the Thai block
818pub fn has_thai_ranges(ranges: &[UnicodeRange]) -> bool {
819    ranges.iter().any(|r| r.start >= 0x0E00 && r.start <= 0x0E7F)
820}
821
822/// Log levels for trace messages
823#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
824pub enum TraceLevel {
825    Debug,
826    Info,
827    Warning,
828    Error,
829}
830
831/// Reason for font matching failure or success
832#[derive(Debug, Clone, PartialEq, Eq, Hash)]
833pub enum MatchReason {
834    NameMismatch {
835        requested: Option<String>,
836        found: Option<String>,
837    },
838    FamilyMismatch {
839        requested: Option<String>,
840        found: Option<String>,
841    },
842    StyleMismatch {
843        property: &'static str,
844        requested: String,
845        found: String,
846    },
847    WeightMismatch {
848        requested: FcWeight,
849        found: FcWeight,
850    },
851    StretchMismatch {
852        requested: FcStretch,
853        found: FcStretch,
854    },
855    UnicodeRangeMismatch {
856        character: char,
857        ranges: Vec<UnicodeRange>,
858    },
859    Success,
860}
861
862/// Trace message for debugging font matching
863#[derive(Debug, Clone, PartialEq, Eq)]
864pub struct TraceMsg {
865    pub level: TraceLevel,
866    pub path: String,
867    pub reason: MatchReason,
868}
869
870/// Hinting style for font rendering.
871#[repr(C)]
872#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
873#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
874pub enum FcHintStyle {
875    #[default]
876    None = 0,
877    Slight = 1,
878    Medium = 2,
879    Full = 3,
880}
881
882/// Subpixel rendering order.
883#[repr(C)]
884#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
885#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
886pub enum FcRgba {
887    #[default]
888    Unknown = 0,
889    Rgb = 1,
890    Bgr = 2,
891    Vrgb = 3,
892    Vbgr = 4,
893    None = 5,
894}
895
896/// LCD filter mode for subpixel rendering.
897#[repr(C)]
898#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
899#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
900pub enum FcLcdFilter {
901    #[default]
902    None = 0,
903    Default = 1,
904    Light = 2,
905    Legacy = 3,
906}
907
908/// Per-font rendering configuration from system font config (Linux fonts.conf).
909///
910/// All fields are `Option<T>` -- `None` means "use system default".
911/// On non-Linux platforms, this is always all-None (no per-font overrides).
912#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
913#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
914pub struct FcFontRenderConfig {
915    pub antialias: Option<bool>,
916    pub hinting: Option<bool>,
917    pub hintstyle: Option<FcHintStyle>,
918    pub autohint: Option<bool>,
919    pub rgba: Option<FcRgba>,
920    pub lcdfilter: Option<FcLcdFilter>,
921    pub embeddedbitmap: Option<bool>,
922    pub embolden: Option<bool>,
923    pub dpi: Option<f64>,
924    pub scale: Option<f64>,
925    pub minspace: Option<bool>,
926}
927
928/// Helper newtype to provide Eq/Ord for Option<f64> via total-order bit comparison.
929/// This allows FcFontRenderConfig to be used inside FcPattern which derives Eq + Ord.
930impl Eq for FcFontRenderConfig {}
931
932impl Ord for FcFontRenderConfig {
933    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
934        // Compare all non-f64 fields first
935        let ord = self.antialias.cmp(&other.antialias)
936            .then_with(|| self.hinting.cmp(&other.hinting))
937            .then_with(|| self.hintstyle.cmp(&other.hintstyle))
938            .then_with(|| self.autohint.cmp(&other.autohint))
939            .then_with(|| self.rgba.cmp(&other.rgba))
940            .then_with(|| self.lcdfilter.cmp(&other.lcdfilter))
941            .then_with(|| self.embeddedbitmap.cmp(&other.embeddedbitmap))
942            .then_with(|| self.embolden.cmp(&other.embolden))
943            .then_with(|| self.minspace.cmp(&other.minspace));
944
945        // For f64 fields, use to_bits() for total ordering
946        let ord = ord.then_with(|| {
947            let a = self.dpi.map(|v| v.to_bits());
948            let b = other.dpi.map(|v| v.to_bits());
949            a.cmp(&b)
950        });
951        ord.then_with(|| {
952            let a = self.scale.map(|v| v.to_bits());
953            let b = other.scale.map(|v| v.to_bits());
954            a.cmp(&b)
955        })
956    }
957}
958
959/// Font pattern for matching
960#[derive(Default, Clone, PartialOrd, Ord, PartialEq, Eq)]
961#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
962#[repr(C)]
963pub struct FcPattern {
964    // font name
965    pub name: Option<String>,
966    // family name
967    pub family: Option<String>,
968    // "italic" property
969    pub italic: PatternMatch,
970    // "oblique" property
971    pub oblique: PatternMatch,
972    // "bold" property
973    pub bold: PatternMatch,
974    // "monospace" property
975    pub monospace: PatternMatch,
976    // "condensed" property
977    pub condensed: PatternMatch,
978    // font weight
979    pub weight: FcWeight,
980    // font stretch
981    pub stretch: FcStretch,
982    // unicode ranges to match
983    pub unicode_ranges: Vec<UnicodeRange>,
984    // extended font metadata
985    pub metadata: FcFontMetadata,
986    // per-font rendering configuration (from system fonts.conf on Linux)
987    pub render_config: FcFontRenderConfig,
988}
989
990impl core::fmt::Debug for FcPattern {
991    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
992        let mut d = f.debug_struct("FcPattern");
993
994        if let Some(name) = &self.name {
995            d.field("name", name);
996        }
997
998        if let Some(family) = &self.family {
999            d.field("family", family);
1000        }
1001
1002        if self.italic != PatternMatch::DontCare {
1003            d.field("italic", &self.italic);
1004        }
1005
1006        if self.oblique != PatternMatch::DontCare {
1007            d.field("oblique", &self.oblique);
1008        }
1009
1010        if self.bold != PatternMatch::DontCare {
1011            d.field("bold", &self.bold);
1012        }
1013
1014        if self.monospace != PatternMatch::DontCare {
1015            d.field("monospace", &self.monospace);
1016        }
1017
1018        if self.condensed != PatternMatch::DontCare {
1019            d.field("condensed", &self.condensed);
1020        }
1021
1022        if self.weight != FcWeight::Normal {
1023            d.field("weight", &self.weight);
1024        }
1025
1026        if self.stretch != FcStretch::Normal {
1027            d.field("stretch", &self.stretch);
1028        }
1029
1030        if !self.unicode_ranges.is_empty() {
1031            d.field("unicode_ranges", &self.unicode_ranges);
1032        }
1033
1034        // Only show non-empty metadata fields
1035        let empty_metadata = FcFontMetadata::default();
1036        if self.metadata != empty_metadata {
1037            d.field("metadata", &self.metadata);
1038        }
1039
1040        // Only show render_config when it differs from default
1041        let empty_render_config = FcFontRenderConfig::default();
1042        if self.render_config != empty_render_config {
1043            d.field("render_config", &self.render_config);
1044        }
1045
1046        d.finish()
1047    }
1048}
1049
1050/// Font metadata from the OS/2 table
1051#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
1052#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
1053pub struct FcFontMetadata {
1054    pub copyright: Option<String>,
1055    pub designer: Option<String>,
1056    pub designer_url: Option<String>,
1057    pub font_family: Option<String>,
1058    pub font_subfamily: Option<String>,
1059    pub full_name: Option<String>,
1060    pub id_description: Option<String>,
1061    pub license: Option<String>,
1062    pub license_url: Option<String>,
1063    pub manufacturer: Option<String>,
1064    pub manufacturer_url: Option<String>,
1065    pub postscript_name: Option<String>,
1066    pub preferred_family: Option<String>,
1067    pub preferred_subfamily: Option<String>,
1068    pub trademark: Option<String>,
1069    pub unique_id: Option<String>,
1070    pub version: Option<String>,
1071}
1072
1073impl FcPattern {
1074    /// Check if this pattern would match the given character
1075    pub fn contains_char(&self, c: char) -> bool {
1076        if self.unicode_ranges.is_empty() {
1077            return true; // No ranges specified means match all characters
1078        }
1079
1080        for range in &self.unicode_ranges {
1081            if range.contains(c) {
1082                return true;
1083            }
1084        }
1085
1086        false
1087    }
1088}
1089
1090/// Font match result with UUID
1091#[derive(Debug, Clone, PartialEq, Eq)]
1092pub struct FontMatch {
1093    pub id: FontId,
1094    pub unicode_ranges: Vec<UnicodeRange>,
1095    pub fallbacks: Vec<FontMatchNoFallback>,
1096}
1097
1098/// Font match result with UUID (without fallback)
1099#[derive(Debug, Clone, PartialEq, Eq)]
1100pub struct FontMatchNoFallback {
1101    pub id: FontId,
1102    pub unicode_ranges: Vec<UnicodeRange>,
1103}
1104
1105/// A run of text that uses the same font
1106/// Returned by FontFallbackChain::query_for_text()
1107#[derive(Debug, Clone, PartialEq, Eq)]
1108pub struct ResolvedFontRun {
1109    /// The text content of this run
1110    pub text: String,
1111    /// Start byte index in the original text
1112    pub start_byte: usize,
1113    /// End byte index in the original text (exclusive)
1114    pub end_byte: usize,
1115    /// The font to use for this run (None if no font found)
1116    pub font_id: Option<FontId>,
1117    /// Which CSS font-family this came from
1118    pub css_source: String,
1119}
1120
1121/// Resolved font fallback chain for a CSS font-family stack
1122/// This represents the complete chain of fonts to use for rendering text
1123#[derive(Debug, Clone, PartialEq, Eq)]
1124pub struct FontFallbackChain {
1125    /// CSS-based fallbacks: Each CSS font expanded to its system fallbacks
1126    /// Example: ["NotoSansJP" -> [Hiragino Sans, PingFang SC], "sans-serif" -> [Helvetica]]
1127    pub css_fallbacks: Vec<CssFallbackGroup>,
1128    
1129    /// Unicode-based fallbacks: Fonts added to cover missing Unicode ranges
1130    /// Only populated if css_fallbacks don't cover all requested characters
1131    pub unicode_fallbacks: Vec<FontMatch>,
1132    
1133    /// The original CSS font-family stack that was requested
1134    pub original_stack: Vec<String>,
1135}
1136
1137impl FontFallbackChain {
1138    /// Resolve which font should be used for a specific character
1139    /// Returns (FontId, css_source_name) where css_source_name indicates which CSS font matched
1140    /// Returns None if no font in the chain can render this character
1141    pub fn resolve_char(&self, cache: &FcFontCache, ch: char) -> Option<(FontId, String)> {
1142        let codepoint = ch as u32;
1143
1144        // Check CSS fallbacks in order
1145        for group in &self.css_fallbacks {
1146            for font in &group.fonts {
1147                let Some(meta) = cache.get_metadata_by_id(&font.id) else { continue };
1148                if meta.unicode_ranges.is_empty() {
1149                    continue; // No range info — don't assume it covers everything
1150                }
1151                if meta.unicode_ranges.iter().any(|r| codepoint >= r.start && codepoint <= r.end) {
1152                    return Some((font.id, group.css_name.clone()));
1153                }
1154            }
1155        }
1156
1157        // Check Unicode fallbacks
1158        for font in &self.unicode_fallbacks {
1159            let Some(meta) = cache.get_metadata_by_id(&font.id) else { continue };
1160            if meta.unicode_ranges.iter().any(|r| codepoint >= r.start && codepoint <= r.end) {
1161                return Some((font.id, "(unicode-fallback)".to_string()));
1162            }
1163        }
1164
1165        // WEB-LIFT LAST-RESORT (re-added 2026-06-03; the `with_memory_fonts` trap that
1166        // previously made touching this file fatal is now fixed by the byte-atomic remill
1167        // fork support). The lifted web path fails coverage-based resolution above for TWO
1168        // reasons that both mis-lift: the chain mis-builds to empty AND/OR `get_metadata_by_id`
1169        // (a HashMap<FontId,_> lookup) returns None in the lift. So instead of gating on the
1170        // chain being empty, fire whenever NOTHING matched above AND the cache holds exactly
1171        // the single registered fallback font — the headless/web case. This bypasses BOTH the
1172        // chain and the metadata HashMap, returning the only font's id directly. Native caches
1173        // hold many system fonts, so `len()==1` is false there → native is unaffected.
1174        let registered = cache.list();
1175        if registered.len() == 1 {
1176            return Some((registered[0].1, "(web-last-resort)".to_string()));
1177        }
1178
1179        None
1180    }
1181    
1182    /// Resolve all characters in a text string to their fonts
1183    /// Returns a vector of (character, FontId, css_source) tuples
1184    pub fn resolve_text(&self, cache: &FcFontCache, text: &str) -> Vec<(char, Option<(FontId, String)>)> {
1185        text.chars()
1186            .map(|ch| (ch, self.resolve_char(cache, ch)))
1187            .collect()
1188    }
1189    
1190    /// Query which fonts should be used for a text string, grouped by font
1191    /// Returns runs of consecutive characters that use the same font
1192    /// This is the main API for text shaping - call this to get font runs, then shape each run
1193    pub fn query_for_text(&self, cache: &FcFontCache, text: &str) -> Vec<ResolvedFontRun> {
1194        if text.is_empty() {
1195            return Vec::new();
1196        }
1197        
1198        let mut runs: Vec<ResolvedFontRun> = Vec::new();
1199        let mut current_font: Option<FontId> = None;
1200        let mut current_css_source: Option<String> = None;
1201        let mut current_start_byte: usize = 0;
1202        
1203        for (byte_idx, ch) in text.char_indices() {
1204            let resolved = self.resolve_char(cache, ch);
1205            let (font_id, css_source) = match &resolved {
1206                Some((id, source)) => (Some(*id), Some(source.clone())),
1207                None => (None, None),
1208            };
1209            
1210            // Check if we need to start a new run
1211            let font_changed = font_id != current_font;
1212            
1213            if font_changed && byte_idx > 0 {
1214                // Finalize the current run
1215                let run_text = &text[current_start_byte..byte_idx];
1216                runs.push(ResolvedFontRun {
1217                    text: run_text.to_string(),
1218                    start_byte: current_start_byte,
1219                    end_byte: byte_idx,
1220                    font_id: current_font,
1221                    css_source: current_css_source.clone().unwrap_or_default(),
1222                });
1223                current_start_byte = byte_idx;
1224            }
1225            
1226            current_font = font_id;
1227            current_css_source = css_source;
1228        }
1229        
1230        // Finalize the last run
1231        if current_start_byte < text.len() {
1232            let run_text = &text[current_start_byte..];
1233            runs.push(ResolvedFontRun {
1234                text: run_text.to_string(),
1235                start_byte: current_start_byte,
1236                end_byte: text.len(),
1237                font_id: current_font,
1238                css_source: current_css_source.unwrap_or_default(),
1239            });
1240        }
1241        
1242        runs
1243    }
1244}
1245
1246/// A group of fonts that are fallbacks for a single CSS font-family name
1247#[derive(Debug, Clone, PartialEq, Eq)]
1248pub struct CssFallbackGroup {
1249    /// The CSS font name (e.g., "NotoSansJP", "sans-serif")
1250    pub css_name: String,
1251    
1252    /// System fonts that match this CSS name
1253    /// First font in list is the best match
1254    pub fonts: Vec<FontMatch>,
1255}
1256
1257/// Cache key for font fallback chain queries
1258///
1259/// IMPORTANT: This key intentionally does NOT include per-text unicode
1260/// ranges — fallback chains are cached by CSS properties only. Different
1261/// texts with the same CSS font-stack share the same chain.
1262///
1263/// `scripts_hint_hash` distinguishes *which set of Unicode-fallback
1264/// scripts* the caller asked for. `None` means "the default set of 7
1265/// major scripts" (Cyrillic/Arabic/Devanagari/Hiragana/Katakana/CJK/Hangul,
1266/// back-compat behaviour of `resolve_font_chain`). `Some(h)` is a
1267/// stable hash of a caller-supplied script list so an ASCII-only
1268/// query doesn't collide with a CJK-aware one.
1269#[cfg(feature = "std")]
1270#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1271pub(crate) struct FontChainCacheKey {
1272    /// CSS font stack (expanded to OS-specific fonts)
1273    pub(crate) font_families: Vec<String>,
1274    /// Font weight
1275    pub(crate) weight: FcWeight,
1276    /// Font style flags
1277    pub(crate) italic: PatternMatch,
1278    pub(crate) oblique: PatternMatch,
1279    /// Hash of the caller-supplied script hint (or `None` for the default set).
1280    pub(crate) scripts_hint_hash: Option<u64>,
1281}
1282
1283/// Hash a `scripts_hint` slice into a stable u64 for use as a
1284/// [`FontChainCacheKey`] component. Order-insensitive: we sort a
1285/// local copy before hashing so `[CJK, Arabic]` and `[Arabic, CJK]`
1286/// key into the same cache slot.
1287#[cfg(feature = "std")]
1288fn hash_scripts_hint(ranges: &[UnicodeRange]) -> u64 {
1289    let mut sorted: Vec<UnicodeRange> = ranges.to_vec();
1290    sorted.sort();
1291    let mut buf = Vec::with_capacity(sorted.len() * 8);
1292    for r in &sorted {
1293        buf.extend_from_slice(&r.start.to_le_bytes());
1294        buf.extend_from_slice(&r.end.to_le_bytes());
1295    }
1296    crate::utils::content_hash_u64(&buf)
1297}
1298
1299/// Path to a font file
1300///
1301/// `bytes_hash` is a deterministic 64-bit hash of the file's full
1302/// byte contents (see [`crate::utils::content_hash_u64`]). All faces
1303/// of a given `.ttc` file share the same `bytes_hash`, and two
1304/// different paths pointing at the same file contents also do —
1305/// so the cache can share a single `Arc<[u8]>` across them via
1306/// [`FcFontCache::get_font_bytes`]. A value of `0` means "hash
1307/// not computed" (e.g. built from a filename-only scan, or loaded
1308/// from a legacy v1 disk cache); callers must treat `0` as opaque
1309/// and fall back to unshared reads.
1310#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
1311#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
1312#[repr(C)]
1313pub struct FcFontPath {
1314    pub path: String,
1315    pub font_index: usize,
1316    /// 64-bit content hash of the file's bytes. 0 = not computed.
1317    #[cfg_attr(feature = "cache", serde(default))]
1318    pub bytes_hash: u64,
1319}
1320
1321/// In-memory font data
1322#[derive(Debug, Clone, PartialEq, Eq)]
1323#[repr(C)]
1324pub struct FcFont {
1325    pub bytes: Vec<u8>,
1326    pub font_index: usize,
1327    pub id: String, // For identification in tests
1328}
1329
1330/// Owned font-source descriptor, returned by
1331/// [`FcFontCache::get_font_by_id`].
1332///
1333/// In v4.0 this was a borrowed enum (`FontSource<'a>` with refs into
1334/// the pattern map). With v4.1's shared-state cache, the map lives
1335/// behind an `RwLock`, so returning a reference would require the
1336/// caller to hold a read guard for the full lifetime of the result —
1337/// which bleeds the locking strategy into every call site. The owned
1338/// variant clones the small `FcFont` / `FcFontPath` struct and
1339/// releases the lock immediately. Bytes/mmap are not cloned — those
1340/// go through `get_font_bytes` which hands out `Arc<FontBytes>`.
1341#[derive(Debug, Clone)]
1342pub enum OwnedFontSource {
1343    /// Font loaded from memory (small metadata + owned `Vec<u8>`).
1344    Memory(FcFont),
1345    /// Font loaded from disk.
1346    Disk(FcFontPath),
1347}
1348
1349/// A handle to font bytes returned by [`FcFontCache::get_font_bytes`].
1350///
1351/// On disk, an `Mmap` is used so untouched pages don't count toward
1352/// process RSS. In-memory fonts (`FcFont`) come back as `Owned` since
1353/// they're already on the heap.
1354///
1355/// `FontBytes` derefs to `[u8]` and implements `AsRef<[u8]>`, so any
1356/// existing API that wants `&[u8]` (allsorts, ttf-parser, …) can
1357/// accept it without code changes.
1358///
1359/// Both variants are `Send + Sync` (mmaps and `Arc<[u8]>` are both
1360/// safe to share across threads).
1361#[cfg(feature = "std")]
1362pub enum FontBytes {
1363    /// Heap-owned bytes. Used for `FontSource::Memory` and as a
1364    /// fallback when mmap is unavailable.
1365    Owned(std::sync::Arc<[u8]>),
1366    /// File-backed mmap. Read-only; pages are demand-loaded by the
1367    /// kernel. Absent on wasm targets, where `mmapio` is unavailable
1368    /// (the optional dep is gated to `cfg(not(target_family="wasm"))`).
1369    #[cfg(not(target_family = "wasm"))]
1370    Mmapped(mmapio::Mmap),
1371}
1372
1373#[cfg(feature = "std")]
1374impl FontBytes {
1375    /// Borrow the underlying byte slice.
1376    #[inline]
1377    pub fn as_slice(&self) -> &[u8] {
1378        match self {
1379            FontBytes::Owned(arc) => arc,
1380            #[cfg(not(target_family = "wasm"))]
1381            FontBytes::Mmapped(m) => &m[..],
1382        }
1383    }
1384}
1385
1386#[cfg(feature = "std")]
1387impl core::ops::Deref for FontBytes {
1388    type Target = [u8];
1389    #[inline]
1390    fn deref(&self) -> &[u8] {
1391        self.as_slice()
1392    }
1393}
1394
1395#[cfg(feature = "std")]
1396impl AsRef<[u8]> for FontBytes {
1397    #[inline]
1398    fn as_ref(&self) -> &[u8] {
1399        self.as_slice()
1400    }
1401}
1402
1403#[cfg(feature = "std")]
1404impl core::fmt::Debug for FontBytes {
1405    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1406        let kind = match self {
1407            FontBytes::Owned(_) => "Owned",
1408            #[cfg(not(target_family = "wasm"))]
1409            FontBytes::Mmapped(_) => "Mmapped",
1410        };
1411        write!(f, "FontBytes::{}({} bytes)", kind, self.as_slice().len())
1412    }
1413}
1414
1415/// Open a font file as an mmap-backed [`FontBytes`]. Falls back to a
1416/// heap read if mmap fails (e.g. the file is on a network share that
1417/// doesn't support mmap, or we're on a target without `std`-mmap).
1418#[cfg(feature = "std")]
1419fn open_font_bytes_mmap(path: &str) -> Option<std::sync::Arc<FontBytes>> {
1420    use std::fs::File;
1421    use std::sync::Arc;
1422
1423    #[cfg(not(target_family = "wasm"))]
1424    {
1425        if let Ok(file) = File::open(path) {
1426            // Safety: `Mmap::map` requires that the file is not
1427            // mutated while mapped. For system fonts that's the
1428            // overwhelming common case; if a user replaces the file
1429            // we accept reading the snapshot we mapped earlier.
1430            if let Ok(mmap) = unsafe { mmapio::MmapOptions::new().map(&file) } {
1431                return Some(Arc::new(FontBytes::Mmapped(mmap)));
1432            }
1433        }
1434    }
1435    let bytes = std::fs::read(path).ok()?;
1436    Some(Arc::new(FontBytes::Owned(Arc::from(bytes))))
1437}
1438
1439/// A named font to be added to the font cache from memory.
1440/// This is the primary way to supply custom fonts to the application.
1441#[derive(Debug, Clone)]
1442pub struct NamedFont {
1443    /// Human-readable name for this font (e.g., "My Custom Font")
1444    pub name: String,
1445    /// The raw font file bytes (TTF, OTF, WOFF, WOFF2, TTC)
1446    pub bytes: Vec<u8>,
1447}
1448
1449impl NamedFont {
1450    /// Create a new named font from bytes
1451    pub fn new(name: impl Into<String>, bytes: Vec<u8>) -> Self {
1452        Self {
1453            name: name.into(),
1454            bytes,
1455        }
1456    }
1457}
1458
1459/// Font cache, initialized at startup.
1460///
1461/// Thread-safe, shared font cache.
1462///
1463/// As of v4.1 the cache internally owns its state via
1464/// `Arc<RwLock<FcFontCacheInner>>`: cloning an `FcFontCache` returns
1465/// a handle that shares the same underlying data. Writes by one holder
1466/// (typically the background builder inside `FcFontRegistry`) become
1467/// immediately visible to every other holder (layout engines,
1468/// shape-time resolvers, etc.).
1469///
1470/// Before 4.1 the clone deep-copied every map, so external holders
1471/// were frozen at the moment they took the snapshot — the mismatch
1472/// between "live registry cache" and "frozen font manager cache"
1473/// was the root of the silent-text regression when lazy scout mode
1474/// was enabled. The shared-state design eliminates that entire class
1475/// of staleness bugs by construction.
1476pub struct FcFontCache {
1477    pub(crate) shared: std::sync::Arc<FcFontCacheShared>,
1478}
1479
1480/// Shared interior of `FcFontCache`. Always accessed through an
1481/// `Arc` — never referenced directly by external callers.
1482// Internal lock wrapper for the cache state. Two implementations selected by feature:
1483//
1484// DEFAULT (general builds): backed by std `RwLock`. `read`/`write`/`lock` return
1485// `Result<_, Infallible>` for a uniform call site (a poisoned lock is recovered via
1486// `into_inner` — a memoisation cache is still valid to read after a panic).
1487//
1488// `single-thread-unsafe-locks` feature: a bare `UnsafeCell` with NO atomics; `read`/`write`/
1489// `lock` hand out a guard immediately. UNSOUND in a multi-threaded program — enable ONLY for a
1490// known single-threaded environment. Exists for the azul remill-lifted web backend
1491// (single-threaded wasm), where std's queue-based RwLock `lock_contended` path spins forever
1492// (no other thread ever unparks it) and hangs the layout solver.
1493
1494#[cfg(not(feature = "single-thread-unsafe-locks"))]
1495pub struct StLock<T> {
1496    lock: std::sync::RwLock<T>,
1497}
1498#[cfg(not(feature = "single-thread-unsafe-locks"))]
1499impl<T> core::fmt::Debug for StLock<T> {
1500    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1501        f.write_str("StLock(..)")
1502    }
1503}
1504#[cfg(not(feature = "single-thread-unsafe-locks"))]
1505impl<T> StLock<T> {
1506    pub fn new(v: T) -> Self {
1507        Self { lock: std::sync::RwLock::new(v) }
1508    }
1509    pub fn read(&self) -> Result<StReadGuard<'_, T>, core::convert::Infallible> {
1510        Ok(StReadGuard { g: self.lock.read().unwrap_or_else(|e| e.into_inner()) })
1511    }
1512    pub fn write(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1513        Ok(StWriteGuard { g: self.lock.write().unwrap_or_else(|e| e.into_inner()) })
1514    }
1515    pub fn lock(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1516        self.write()
1517    }
1518}
1519#[cfg(not(feature = "single-thread-unsafe-locks"))]
1520pub struct StReadGuard<'a, T> {
1521    g: std::sync::RwLockReadGuard<'a, T>,
1522}
1523#[cfg(not(feature = "single-thread-unsafe-locks"))]
1524impl<'a, T> core::ops::Deref for StReadGuard<'a, T> {
1525    type Target = T;
1526    fn deref(&self) -> &T { &self.g }
1527}
1528#[cfg(not(feature = "single-thread-unsafe-locks"))]
1529pub struct StWriteGuard<'a, T> {
1530    g: std::sync::RwLockWriteGuard<'a, T>,
1531}
1532#[cfg(not(feature = "single-thread-unsafe-locks"))]
1533impl<'a, T> core::ops::Deref for StWriteGuard<'a, T> {
1534    type Target = T;
1535    fn deref(&self) -> &T { &self.g }
1536}
1537#[cfg(not(feature = "single-thread-unsafe-locks"))]
1538impl<'a, T> core::ops::DerefMut for StWriteGuard<'a, T> {
1539    fn deref_mut(&mut self) -> &mut T { &mut self.g }
1540}
1541
1542#[cfg(feature = "single-thread-unsafe-locks")]
1543pub struct StLock<T> {
1544    cell: std::cell::UnsafeCell<T>,
1545}
1546#[cfg(feature = "single-thread-unsafe-locks")]
1547unsafe impl<T> Sync for StLock<T> {}
1548#[cfg(feature = "single-thread-unsafe-locks")]
1549unsafe impl<T> Send for StLock<T> {}
1550#[cfg(feature = "single-thread-unsafe-locks")]
1551impl<T> core::fmt::Debug for StLock<T> {
1552    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1553        f.write_str("StLock(..)")
1554    }
1555}
1556#[cfg(feature = "single-thread-unsafe-locks")]
1557impl<T> StLock<T> {
1558    pub fn new(v: T) -> Self {
1559        Self { cell: std::cell::UnsafeCell::new(v) }
1560    }
1561    pub fn read(&self) -> Result<StReadGuard<'_, T>, core::convert::Infallible> {
1562        Ok(StReadGuard { r: unsafe { &*self.cell.get() } })
1563    }
1564    pub fn write(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1565        Ok(StWriteGuard { r: unsafe { &mut *self.cell.get() } })
1566    }
1567    pub fn lock(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1568        Ok(StWriteGuard { r: unsafe { &mut *self.cell.get() } })
1569    }
1570}
1571#[cfg(feature = "single-thread-unsafe-locks")]
1572pub struct StReadGuard<'a, T> {
1573    r: &'a T,
1574}
1575#[cfg(feature = "single-thread-unsafe-locks")]
1576impl<'a, T> core::ops::Deref for StReadGuard<'a, T> {
1577    type Target = T;
1578    fn deref(&self) -> &T { self.r }
1579}
1580#[cfg(feature = "single-thread-unsafe-locks")]
1581pub struct StWriteGuard<'a, T> {
1582    r: &'a mut T,
1583}
1584#[cfg(feature = "single-thread-unsafe-locks")]
1585impl<'a, T> core::ops::Deref for StWriteGuard<'a, T> {
1586    type Target = T;
1587    fn deref(&self) -> &T { self.r }
1588}
1589#[cfg(feature = "single-thread-unsafe-locks")]
1590impl<'a, T> core::ops::DerefMut for StWriteGuard<'a, T> {
1591    fn deref_mut(&mut self) -> &mut T { self.r }
1592}
1593
1594pub(crate) struct FcFontCacheShared {
1595    /// Main pattern/metadata state, guarded by a reader-writer lock.
1596    /// Builder threads take the write lock to insert a parsed font;
1597    /// all query paths take the read lock.
1598    pub(crate) state: StLock<FcFontCacheInner>,
1599    /// Font fallback chain cache. Not part of the RwLock-guarded
1600    /// state because cache insertions happen under `&self` on read
1601    /// paths (they're a memoisation, not observable state).
1602    pub(crate) chain_cache: StLock<std::collections::HashMap<FontChainCacheKey, FontFallbackChain>>,
1603    /// Shared file-bytes cache: content-hash → weak [`FontBytes`].
1604    ///
1605    /// [`FcFontCache::get_font_bytes`] populates this so that multiple
1606    /// FontIds backed by the same file (e.g. every face of a `.ttc`)
1607    /// return the same `Arc<FontBytes>` — and therefore the same mmap
1608    /// — instead of each allocating their own buffer. We hold `Weak`
1609    /// references so the mmap unmap as soon as no parsed font holds
1610    /// it alive.
1611    pub(crate) shared_bytes: StLock<std::collections::HashMap<u64, std::sync::Weak<FontBytes>>>,
1612}
1613
1614/// The actual font-pattern state, held behind the RwLock in
1615/// `FcFontCacheShared`. Private — all access goes through
1616/// `FcFontCache` methods which lock transparently.
1617#[derive(Default, Debug)]
1618pub(crate) struct FcFontCacheInner {
1619    /// Pattern to FontId mapping (query index)
1620    pub(crate) patterns: BTreeMap<FcPattern, FontId>,
1621    /// On-disk font paths
1622    pub(crate) disk_fonts: BTreeMap<FontId, FcFontPath>,
1623    /// In-memory fonts
1624    pub(crate) memory_fonts: BTreeMap<FontId, FcFont>,
1625    /// Metadata cache (patterns stored by ID for quick lookup)
1626    pub(crate) metadata: BTreeMap<FontId, FcPattern>,
1627    /// Token index: maps lowercase tokens ("noto", "sans", "jp") to sets of FontIds.
1628    /// Enables fast fuzzy search by intersecting token sets.
1629    pub(crate) token_index: BTreeMap<String, alloc::collections::BTreeSet<FontId>>,
1630    /// Pre-tokenized font names (lowercase): FontId -> Vec<lowercase tokens>.
1631    /// Avoids re-tokenization during fuzzy search.
1632    pub(crate) font_tokens: BTreeMap<FontId, Vec<String>>,
1633    /// System-configured family alias preferences, parsed from the
1634    /// platform font configuration (Linux: `$FONTCONFIG_FILE` or
1635    /// `/etc/fonts/fonts.conf` + included conf.d files, `<alias>` /
1636    /// `<prefer>` blocks). Keyed by the normalized alias family
1637    /// ("sans-serif", "arial", ...), values are the preferred concrete
1638    /// families in configuration order. THE authority for generic-family
1639    /// resolution: the hard-coded per-OS lists are only consulted when
1640    /// this map has no entry (e.g. no fontconfig installed).
1641    pub(crate) system_aliases: BTreeMap<String, Vec<String>>,
1642}
1643
1644impl FcFontCacheInner {
1645    /// Add a font pattern to the token index. Called under the
1646    /// write lock by insertion paths.
1647    pub(crate) fn index_pattern_tokens(&mut self, _pattern: &FcPattern, _id: FontId) {
1648        // WEB-LIFT (2026-06-02): no-op on the azul web fork. The tokenizer
1649        // (`extract_font_name_tokens` char-classification + lowercasing) pulls unicode tables
1650        // whose jump-tables the remill/web lift leaves un-devirt'd → MISSING_BLOCK trap inside
1651        // `with_memory_fonts`. `token_index`/`font_tokens` feed ONLY the separate token-fuzzy
1652        // search path (query_fuzzy); the main `query`→`query_internal_locked` scores by
1653        // unicode-compatibility + style over the registered patterns/metadata (populated before
1654        // this call), so leaving the token index empty does not affect normal font matching.
1655    }
1656}
1657
1658impl Clone for FcFontCache {
1659    /// Shallow clone — the returned handle shares the same underlying
1660    /// state as `self`. Writes through either are visible to both.
1661    /// This is the whole point of the v4.1 redesign; callers that need
1662    /// an isolated frozen copy must explicitly request one (e.g. via
1663    /// `snapshot_state`, which is intentionally not provided because
1664    /// we no longer have a use case for it).
1665    fn clone(&self) -> Self {
1666        Self {
1667            shared: std::sync::Arc::clone(&self.shared),
1668        }
1669    }
1670}
1671
1672impl core::fmt::Debug for FcFontCache {
1673    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1674        let state = self.state_read();
1675        f.debug_struct("FcFontCache")
1676            .field("patterns_len", &state.patterns.len())
1677            .field("metadata_len", &state.metadata.len())
1678            .field("disk_fonts_len", &state.disk_fonts.len())
1679            .field("memory_fonts_len", &state.memory_fonts.len())
1680            .finish()
1681    }
1682}
1683
1684impl Default for FcFontCache {
1685    fn default() -> Self {
1686        Self {
1687            shared: std::sync::Arc::new(FcFontCacheShared {
1688                state: StLock::new(FcFontCacheInner::default()),
1689                chain_cache: StLock::new(std::collections::HashMap::new()),
1690                shared_bytes: StLock::new(std::collections::HashMap::new()),
1691            }),
1692        }
1693    }
1694}
1695
1696impl FcFontCache {
1697    /// The system-configured preferred families for `family` (normalized
1698    /// lookup), parsed from the platform font configuration at build time.
1699    /// Empty when the platform has no such configuration.
1700    pub fn system_alias_prefs(&self, family: &str) -> Vec<String> {
1701        let norm = crate::utils::normalize_family_name(family);
1702        self.state_read()
1703            .system_aliases
1704            .get(&norm)
1705            .cloned()
1706            .unwrap_or_default()
1707    }
1708
1709    /// Expand a CSS font-family stack, resolving each entry through the
1710    /// SYSTEM configuration first and only falling back to the built-in
1711    /// per-OS lists when the configuration is silent.
1712    ///
1713    /// Resolution per family, in order:
1714    /// 1. `<alias>`/`<prefer>` preferences parsed from the platform font
1715    ///    configuration (generic families like `sans-serif` AND named
1716    ///    substitutions like `Arial` -> `Liberation Sans`). The machine's
1717    ///    actual configuration is the authority — this is what real
1718    ///    fontconfig does, and what makes azul agree with every other
1719    ///    application on the box.
1720    /// 2. For generic families with no configured preference: the built-in
1721    ///    per-OS candidates ([`OperatingSystem::expand_generic_family`]) as
1722    ///    a LAST resort (containers without any fontconfig installed).
1723    /// 3. Named families always keep themselves FIRST, before any
1724    ///    configured substitution (CSS: exact match wins when present;
1725    ///    the alias only helps when the named family is missing).
1726    pub fn expand_font_families_config_first(
1727        &self,
1728        families: &[String],
1729        os: OperatingSystem,
1730        unicode_ranges: &[UnicodeRange],
1731    ) -> Vec<String> {
1732        let mut expanded: Vec<String> = Vec::new();
1733        let mut push_unique = |v: &mut Vec<String>, f: String| {
1734            if !v.iter().any(|e| e.eq_ignore_ascii_case(&f)) {
1735                v.push(f);
1736            }
1737        };
1738        for family in families {
1739            let is_generic = matches!(
1740                family.to_ascii_lowercase().as_str(),
1741                "serif" | "sans-serif" | "monospace" | "cursive" | "fantasy" | "system-ui"
1742            );
1743            if !is_generic {
1744                push_unique(&mut expanded, family.clone());
1745            }
1746            let prefs = self.system_alias_prefs(family);
1747            if !prefs.is_empty() {
1748                for pref in prefs {
1749                    push_unique(&mut expanded, pref);
1750                }
1751            } else if is_generic {
1752                for fallback in os.expand_generic_family(family, unicode_ranges) {
1753                    push_unique(&mut expanded, fallback);
1754                }
1755            }
1756        }
1757        expanded
1758    }
1759
1760    /// Acquire a read guard on the cache's state. Panics if the lock
1761    /// was poisoned by a panic inside the write guard — same
1762    /// contract as `RwLock::read().expect(..)`.
1763    #[inline]
1764    pub(crate) fn state_read(
1765        &self,
1766    ) -> StReadGuard<'_, FcFontCacheInner> {
1767        // [az-web-lift] StLock::read() is Infallible (never poisons/spins).
1768        match self.shared.state.read() {
1769            Ok(g) => g,
1770            Err(e) => match e {},
1771        }
1772    }
1773
1774    /// Acquire a write guard on the cache's state. Panics on
1775    /// poisoning, same as `state_read`.
1776    #[inline]
1777    pub(crate) fn state_write(
1778        &self,
1779    ) -> StWriteGuard<'_, FcFontCacheInner> {
1780        // [az-web-lift] StLock::write() is Infallible (never poisons/spins).
1781        match self.shared.state.write() {
1782            Ok(g) => g,
1783            Err(e) => match e {},
1784        }
1785    }
1786
1787    /// Adds in-memory font files.
1788    ///
1789    /// Note: takes `&self` — the shared cache handles interior
1790    /// mutability via the RwLock.
1791    pub fn with_memory_fonts(&self, fonts: Vec<(FcPattern, FcFont)>) -> &Self {
1792        // Auto-detect Unicode coverage for any naively-registered font
1793        // (empty `unicode_ranges`) BEFORE taking the write lock, so we don't
1794        // hold it across font parsing. See `populate_memory_font_ranges`.
1795        let fonts: Vec<(FcPattern, FcFont)> = fonts
1796            .into_iter()
1797            .map(|(pattern, font)| (Self::populate_memory_font_ranges(pattern, &font), font))
1798            .collect();
1799        let mut state = self.state_write();
1800        for (pattern, font) in fonts {
1801            let id = FontId::new();
1802            state.patterns.insert(pattern.clone(), id);
1803            state.metadata.insert(id, pattern.clone());
1804            state.memory_fonts.insert(id, font);
1805            state.index_pattern_tokens(&pattern, id);
1806        }
1807        self
1808    }
1809
1810    /// Adds a memory font with a specific ID (for testing).
1811    pub fn with_memory_font_with_id(
1812        &self,
1813        id: FontId,
1814        pattern: FcPattern,
1815        font: FcFont,
1816    ) -> &Self {
1817        let pattern = Self::populate_memory_font_ranges(pattern, &font);
1818        let mut state = self.state_write();
1819        state.patterns.insert(pattern.clone(), id);
1820        state.metadata.insert(id, pattern.clone());
1821        state.memory_fonts.insert(id, font);
1822        state.index_pattern_tokens(&pattern, id);
1823        self
1824    }
1825
1826    /// Fill in a memory font's `unicode_ranges` from its raw bytes when the
1827    /// caller left them empty.
1828    ///
1829    /// A normal caller of [`FcFontCache::with_memory_fonts`] just hands over
1830    /// a name and the font bytes — they don't hand-compute the cmap. But
1831    /// [`FontFallbackChain::resolve_char`] deliberately skips any font that
1832    /// reports *no* coverage (it refuses to assume a blank range list means
1833    /// "covers everything"). Without this step a naively-registered bundled
1834    /// font could never be selected for any character — the exact bug that
1835    /// bites headless / wasm / embedder-bundled-font setups.
1836    ///
1837    /// With the `parsing` feature we reuse the *same* OS/2 + cmap detection
1838    /// pipeline the on-disk builder uses (via [`FcParseFontBytes`] →
1839    /// `parse_font_faces`). Without `parsing` the pattern is returned
1840    /// unchanged and the caller must populate `unicode_ranges` themselves.
1841    #[cfg(all(feature = "std", feature = "parsing"))]
1842    fn populate_memory_font_ranges(mut pattern: FcPattern, font: &FcFont) -> FcPattern {
1843        if !pattern.unicode_ranges.is_empty() {
1844            return pattern;
1845        }
1846        if let Some(faces) = FcParseFontBytes(&font.bytes, &font.id) {
1847            // A `.ttc` yields several faces; pick the one matching this
1848            // font's index, else fall back to the first parsed face. All
1849            // patterns of a single face share the same `unicode_ranges`.
1850            let ranges = faces
1851                .iter()
1852                .find(|(_, f)| f.font_index == font.font_index)
1853                .or_else(|| faces.first())
1854                .map(|(p, _)| p.unicode_ranges.clone())
1855                .unwrap_or_default();
1856            if !ranges.is_empty() {
1857                pattern.unicode_ranges = ranges;
1858            }
1859        }
1860        pattern
1861    }
1862
1863    /// Without the `parsing` feature there is no cmap/OS2 parser available,
1864    /// so the caller-provided pattern is stored verbatim.
1865    #[cfg(not(all(feature = "std", feature = "parsing")))]
1866    fn populate_memory_font_ranges(pattern: FcPattern, _font: &FcFont) -> FcPattern {
1867        pattern
1868    }
1869
1870    /// Register a newly-parsed on-disk font. Called by the builder
1871    /// thread inside `FcFontRegistry`. Allocates a fresh `FontId`,
1872    /// inserts the pattern + path + metadata in one write lock, and
1873    /// invalidates the chain cache so subsequent resolutions pick
1874    /// up the new font.
1875    pub fn insert_builder_font(&self, pattern: FcPattern, path: FcFontPath) {
1876        let id = FontId::new();
1877        {
1878            let mut state = self.state_write();
1879            state.index_pattern_tokens(&pattern, id);
1880            state.patterns.insert(pattern.clone(), id);
1881            state.disk_fonts.insert(id, path);
1882            state.metadata.insert(id, pattern);
1883        }
1884        // Invalidate chain cache so callers see the new font on the
1885        // next resolve. Scoped after the state write to keep lock
1886        // nesting shallow.
1887        if let Ok(mut cc) = self.shared.chain_cache.lock() {
1888            cc.clear();
1889        }
1890    }
1891
1892    #[cfg(feature = "std")]
1893    #[doc(hidden)]
1894    pub fn chain_cache_len(&self) -> usize {
1895        self.shared.chain_cache.lock().map(|c| c.len()).unwrap_or(0)
1896    }
1897
1898    /// Insert a *fast-probed* pattern into the cache and return its
1899    /// fresh `FontId`. Used by [`FcFontRegistry::request_fonts_fast`]
1900    /// when a cmap probe discovers a font that covers some subset of
1901    /// the requested codepoints. Unlike [`insert_builder_font`] this
1902    /// does **not** populate the token index (we don't have NAME
1903    /// table data), so fuzzy-name lookups on fast-probed fonts fall
1904    /// through to the filename-guess in `known_paths`.
1905    pub fn insert_fast_pattern(&self, pattern: FcPattern, path: FcFontPath) -> FontId {
1906        let id = FontId::new();
1907        let mut state = self.state_write();
1908        state.patterns.insert(pattern.clone(), id);
1909        state.disk_fonts.insert(id, path);
1910        state.metadata.insert(id, pattern);
1911        id
1912    }
1913
1914    /// Look up all `FontId`s whose `FcFontPath` matches `path`.
1915    /// Cheap way for `request_fonts_fast` to reuse fast-probed
1916    /// entries across layout passes without re-reading the cmap.
1917    ///
1918    /// O(n) over the disk_fonts map; fine for the typical case of
1919    /// <100 parsed fonts, and we skip the scan entirely when a
1920    /// stack's first candidate covers.
1921    pub fn lookup_paths_cached(&self, path: &str) -> Option<Vec<FontId>> {
1922        let state = self.state_read();
1923        let mut out = Vec::new();
1924        for (id, font_path) in &state.disk_fonts {
1925            if font_path.path == path {
1926                out.push(*id);
1927            }
1928        }
1929        if out.is_empty() { None } else { Some(out) }
1930    }
1931
1932    /// Get font data for a given font ID.
1933    ///
1934    /// Returns owned values (not references) because the underlying
1935    /// maps live behind an RwLock — a reference could not outlive
1936    /// the read guard. In-memory fonts come back as cloned `FcFont`
1937    /// instances; disk fonts return their `FcFontPath`.
1938    pub fn get_font_by_id(&self, id: &FontId) -> Option<OwnedFontSource> {
1939        let state = self.state_read();
1940        if let Some(font) = state.memory_fonts.get(id) {
1941            return Some(OwnedFontSource::Memory(font.clone()));
1942        }
1943        if let Some(path) = state.disk_fonts.get(id) {
1944            return Some(OwnedFontSource::Disk(path.clone()));
1945        }
1946        None
1947    }
1948
1949    /// Get metadata for a font ID. Returns an owned `FcPattern`
1950    /// (cloned out of the shared map) because we can't return a
1951    /// reference across the RwLock boundary.
1952    pub fn get_metadata_by_id(&self, id: &FontId) -> Option<FcPattern> {
1953        self.state_read().metadata.get(id).cloned()
1954    }
1955
1956    /// Get the font bytes for `id` as a shared [`FontBytes`].
1957    ///
1958    /// On disk the returned `Arc<FontBytes>` wraps an mmap of the file
1959    /// (`FontBytes::Mmapped`). Untouched pages of the file never count
1960    /// toward the process's RSS — for a font where layout shapes only
1961    /// a handful of glyphs, this is the difference between paying for
1962    /// the whole 4 MiB `.ttc` and paying for the cmap + a few glyf
1963    /// pages.
1964    ///
1965    /// In-memory fonts (`FontSource::Memory`) come back as
1966    /// `FontBytes::Owned`, since the bytes are already on the heap.
1967    ///
1968    /// Multiple `FontId`s backed by the same file content (every face
1969    /// of a `.ttc`, or two paths with identical bytes) return the
1970    /// *same* `Arc<FontBytes>` thanks to a content-hash → `Weak`
1971    /// cache. Bytes get unmapped automatically when the last consumer
1972    /// drops the Arc.
1973    ///
1974    /// `FontBytes` derefs to `[u8]`, so callers that only need
1975    /// `&[u8]` (allsorts, ttf-parser, …) can pass it through without
1976    /// thinking about the backing.
1977    ///
1978    /// Failure modes: returns `None` if the path is unknown, or the
1979    /// file no longer exists / cannot be opened, or the mmap call
1980    /// fails. Callers may retry with a fresh `get_font_bytes` if they
1981    /// suspect the file was replaced underneath them; the next call
1982    /// re-opens cleanly.
1983    #[cfg(feature = "std")]
1984    pub fn get_font_bytes(&self, id: &FontId) -> Option<std::sync::Arc<FontBytes>> {
1985        use std::sync::Arc;
1986        match self.get_font_by_id(id)? {
1987            OwnedFontSource::Memory(font) => Some(Arc::new(FontBytes::Owned(
1988                Arc::from(font.bytes.as_slice()),
1989            ))),
1990            OwnedFontSource::Disk(path) => {
1991                let hash = path.bytes_hash;
1992                if hash != 0 {
1993                    if let Ok(guard) = self.shared.shared_bytes.lock() {
1994                        if let Some(weak) = guard.get(&hash) {
1995                            if let Some(arc) = weak.upgrade() {
1996                                return Some(arc);
1997                            }
1998                        }
1999                    }
2000                }
2001
2002                let arc = open_font_bytes_mmap(&path.path)?;
2003                if hash != 0 {
2004                    if let Ok(mut guard) = self.shared.shared_bytes.lock() {
2005                        // Overwrite any stale weak ref that failed to upgrade.
2006                        guard.insert(hash, Arc::downgrade(&arc));
2007                    }
2008                }
2009                Some(arc)
2010            }
2011        }
2012    }
2013
2014    /// Returns an empty font cache (no_std / no filesystem).
2015    #[cfg(not(feature = "std"))]
2016    pub fn build() -> Self { Self::default() }
2017
2018    /// Scans system font directories using filename heuristics (no allsorts).
2019    #[cfg(all(feature = "std", not(feature = "parsing")))]
2020    pub fn build() -> Self { Self::build_from_filenames() }
2021
2022    /// Scans and parses all system fonts via allsorts for full metadata.
2023    #[cfg(all(feature = "std", feature = "parsing"))]
2024    pub fn build() -> Self { Self::build_inner(None) }
2025
2026    /// Filename-only scan: discovers fonts on disk, guesses metadata from
2027    /// the filename using [`config::tokenize_font_stem`].
2028    #[cfg(all(feature = "std", not(feature = "parsing")))]
2029    fn build_from_filenames() -> Self {
2030        let cache = Self::default();
2031        {
2032            let mut state = cache.state_write();
2033            for dir in crate::config::font_directories(OperatingSystem::current()) {
2034                for path in FcCollectFontFilesRecursive(dir) {
2035                    let pattern = match pattern_from_filename(&path) {
2036                        Some(p) => p,
2037                        None => continue,
2038                    };
2039                    let id = FontId::new();
2040                    state.disk_fonts.insert(id, FcFontPath {
2041                        path: path.to_string_lossy().to_string(),
2042                        font_index: 0,
2043                        // Filename-only scan — we never read the bytes,
2044                        // so there's no dedup key. Leave as 0.
2045                        bytes_hash: 0,
2046                    });
2047                    state.index_pattern_tokens(&pattern, id);
2048                    state.metadata.insert(id, pattern.clone());
2049                    state.patterns.insert(pattern, id);
2050                }
2051            }
2052        }
2053        cache
2054    }
2055    
2056    /// Builds a font cache with only specific font families (and their fallbacks).
2057    /// 
2058    /// This is a performance optimization for applications that know ahead of time
2059    /// which fonts they need. Instead of scanning all system fonts (which can be slow
2060    /// on systems with many fonts), only fonts matching the specified families are loaded.
2061    /// 
2062    /// Generic family names like "sans-serif", "serif", "monospace" are expanded
2063    /// to OS-specific font names (e.g., "sans-serif" on macOS becomes "Helvetica Neue", 
2064    /// "San Francisco", etc.).
2065    /// 
2066    /// **Note**: This will NOT automatically load fallback fonts for scripts not covered
2067    /// by the requested families. If you need Arabic, CJK, or emoji support, either:
2068    /// - Add those families explicitly to the filter
2069    /// - Use `with_memory_fonts()` to add bundled fonts
2070    /// - Use `build()` to load all system fonts
2071    /// 
2072    /// # Arguments
2073    /// * `families` - Font family names to load (e.g., ["Arial", "sans-serif"])
2074    /// 
2075    /// # Example
2076    /// ```ignore
2077    /// // Only load Arial and sans-serif fallback fonts
2078    /// let cache = FcFontCache::build_with_families(&["Arial", "sans-serif"]);
2079    /// ```
2080    #[cfg(all(feature = "std", feature = "parsing"))]
2081    pub fn build_with_families(families: &[impl AsRef<str>]) -> Self {
2082        // Expand generic families to OS-specific names. This runs BEFORE the
2083        // cache exists, so only the built-in lists are available here — the
2084        // filter is a superset selector (which files to parse), not the
2085        // final resolution, which goes config-first at query time.
2086        let os = OperatingSystem::current();
2087        let mut target_families: Vec<String> = Vec::new();
2088        
2089        for family in families {
2090            let family_str = family.as_ref();
2091            let expanded = os.expand_generic_family(family_str, &[]);
2092            if expanded.is_empty() || (expanded.len() == 1 && expanded[0] == family_str) {
2093                target_families.push(family_str.to_string());
2094            } else {
2095                target_families.extend(expanded);
2096            }
2097        }
2098        
2099        Self::build_inner(Some(&target_families))
2100    }
2101    
2102    /// Inner build function that handles both filtered and unfiltered font loading.
2103    /// 
2104    /// # Arguments
2105    /// * `family_filter` - If Some, only load fonts matching these family names.
2106    ///                     If None, load all fonts.
2107    #[cfg(all(feature = "std", feature = "parsing"))]
2108    fn build_inner(family_filter: Option<&[String]>) -> Self {
2109        let cache = FcFontCache::default();
2110
2111        // Normalize filter families for matching
2112        let filter_normalized: Option<Vec<String>> = family_filter.map(|families| {
2113            families
2114                .iter()
2115                .map(|f| crate::utils::normalize_family_name(f))
2116                .collect()
2117        });
2118
2119        // Helper closure to check if a pattern matches the filter
2120        let matches_filter = |pattern: &FcPattern| -> bool {
2121            match &filter_normalized {
2122                None => true, // No filter = accept all
2123                Some(targets) => {
2124                    pattern.name.as_ref().map_or(false, |name| {
2125                        let name_norm = crate::utils::normalize_family_name(name);
2126                        targets.iter().any(|target| name_norm.contains(target))
2127                    }) || pattern.family.as_ref().map_or(false, |family| {
2128                        let family_norm = crate::utils::normalize_family_name(family);
2129                        targets.iter().any(|target| family_norm.contains(target))
2130                    })
2131                }
2132            }
2133        };
2134
2135        let mut state = cache.state_write();
2136
2137        #[cfg(target_os = "linux")]
2138        {
2139            if let Some((font_entries, render_configs, system_aliases)) = FcScanDirectories() {
2140                state.system_aliases = system_aliases;
2141                for (mut pattern, path) in font_entries {
2142                    if matches_filter(&pattern) {
2143                        // Apply per-font render config if a matching family rule exists
2144                        if let Some(family) = pattern.name.as_ref().or(pattern.family.as_ref()) {
2145                            if let Some(rc) = render_configs.get(family) {
2146                                pattern.render_config = rc.clone();
2147                            }
2148                        }
2149                        let id = FontId::new();
2150                        state.patterns.insert(pattern.clone(), id);
2151                        state.metadata.insert(id, pattern.clone());
2152                        state.disk_fonts.insert(id, path);
2153                        state.index_pattern_tokens(&pattern, id);
2154                    }
2155                }
2156            }
2157        }
2158
2159        #[cfg(target_os = "windows")]
2160        {
2161            let system_root = std::env::var("SystemRoot")
2162                .or_else(|_| std::env::var("WINDIR"))
2163                .unwrap_or_else(|_| "C:\\Windows".to_string());
2164
2165            let user_profile = std::env::var("USERPROFILE")
2166                .unwrap_or_else(|_| "C:\\Users\\Default".to_string());
2167
2168            let font_dirs = vec![
2169                (None, format!("{}\\Fonts\\", system_root)),
2170                (None, format!("{}\\AppData\\Local\\Microsoft\\Windows\\Fonts\\", user_profile)),
2171            ];
2172
2173            let font_entries = FcScanDirectoriesInner(&font_dirs);
2174            for (pattern, path) in font_entries {
2175                if matches_filter(&pattern) {
2176                    let id = FontId::new();
2177                    state.patterns.insert(pattern.clone(), id);
2178                    state.metadata.insert(id, pattern.clone());
2179                    state.disk_fonts.insert(id, path);
2180                    state.index_pattern_tokens(&pattern, id);
2181                }
2182            }
2183        }
2184
2185        #[cfg(target_os = "macos")]
2186        {
2187            let font_dirs = vec![
2188                (None, "~/Library/Fonts".to_owned()),
2189                (None, "/System/Library/Fonts".to_owned()),
2190                (None, "/Library/Fonts".to_owned()),
2191                (None, "/System/Library/AssetsV2".to_owned()),
2192            ];
2193
2194            let font_entries = FcScanDirectoriesInner(&font_dirs);
2195            for (pattern, path) in font_entries {
2196                if matches_filter(&pattern) {
2197                    let id = FontId::new();
2198                    state.patterns.insert(pattern.clone(), id);
2199                    state.metadata.insert(id, pattern.clone());
2200                    state.disk_fonts.insert(id, path);
2201                    state.index_pattern_tokens(&pattern, id);
2202                }
2203            }
2204        }
2205
2206        // iOS: the app sandbox denies a plain `read_dir` on `/System/Library/...`,
2207        // but `CTFontManagerCopyAvailableFontURLs` returns sandbox-mediated
2208        // `CFURL`s that *are* openable. We enumerate via CoreText, then feed
2209        // each URL into the same `FcParseFont` path the desktop arms use.
2210        #[cfg(target_os = "ios")]
2211        {
2212            let font_files = crate::mobile_ios::copy_available_font_urls();
2213            let font_entries = FcParseFontFiles(&font_files);
2214            for (pattern, path) in font_entries {
2215                if matches_filter(&pattern) {
2216                    let id = FontId::new();
2217                    state.patterns.insert(pattern.clone(), id);
2218                    state.metadata.insert(id, pattern.clone());
2219                    state.disk_fonts.insert(id, path);
2220                    state.index_pattern_tokens(&pattern, id);
2221                }
2222            }
2223        }
2224
2225        // Android: system fonts live at world-readable paths. Vendor partitions
2226        // (`/product/fonts`, `/system_ext/fonts`) carry OEM-specific families
2227        // on Samsung One UI / MIUI / EMUI; `/data/fonts` is the per-user font
2228        // dir on recent ROMs.
2229        #[cfg(target_os = "android")]
2230        {
2231            let font_dirs = vec![
2232                (None, "/system/fonts".to_owned()),
2233                (None, "/product/fonts".to_owned()),
2234                (None, "/system_ext/fonts".to_owned()),
2235                (None, "/data/fonts".to_owned()),
2236            ];
2237
2238            let font_entries = FcScanDirectoriesInner(&font_dirs);
2239            for (pattern, path) in font_entries {
2240                if matches_filter(&pattern) {
2241                    let id = FontId::new();
2242                    state.patterns.insert(pattern.clone(), id);
2243                    state.metadata.insert(id, pattern.clone());
2244                    state.disk_fonts.insert(id, path);
2245                    state.index_pattern_tokens(&pattern, id);
2246                }
2247            }
2248        }
2249
2250        drop(state);
2251        cache
2252    }
2253    
2254    /// Check if a font ID is a memory font (preferred over disk fonts)
2255    pub fn is_memory_font(&self, id: &FontId) -> bool {
2256        self.state_read().memory_fonts.contains_key(id)
2257    }
2258
2259    /// Returns the list of fonts and font patterns.
2260    ///
2261    /// Returns owned `FcPattern` values (cloned out of the shared
2262    /// state) — this is the v4.1 API change described on
2263    /// [`FcFontCache`]. Callers that need to iterate without
2264    /// cloning should use [`FcFontCache::for_each_pattern`].
2265    pub fn list(&self) -> Vec<(FcPattern, FontId)> {
2266        self.state_read()
2267            .patterns
2268            .iter()
2269            .map(|(pattern, id)| (pattern.clone(), *id))
2270            .collect()
2271    }
2272
2273    /// Iterate over every `(pattern, id)` pair under a single read
2274    /// guard. `f` is called once per entry — avoids the per-entry
2275    /// clone that [`list`] incurs.
2276    pub fn for_each_pattern<F: FnMut(&FcPattern, &FontId)>(&self, mut f: F) {
2277        let state = self.state_read();
2278        for (pattern, id) in &state.patterns {
2279            f(pattern, id);
2280        }
2281    }
2282
2283    /// Returns true if the cache contains no font patterns
2284    pub fn is_empty(&self) -> bool {
2285        self.state_read().patterns.is_empty()
2286    }
2287
2288    /// Returns the number of font patterns in the cache
2289    pub fn len(&self) -> usize {
2290        self.state_read().patterns.len()
2291    }
2292
2293    /// Like [`FcFontCache::query`], but **total**: it returns `None` only when the
2294    /// cache holds no fonts at all.
2295    ///
2296    /// This is the `fc-match` contract. `fc-match` never fails — fontconfig
2297    /// substitutes through its config chain, which is why `fc-match Cantarell`
2298    /// answers with e.g. `NotoSans-Regular.ttf` on a machine that has no
2299    /// Cantarell. [`FcFontCache::query`] deliberately does NOT do that: it is the
2300    /// honest "was this exact request satisfiable?" answer, and a caller that
2301    /// wants to report an unresolved family needs it.
2302    ///
2303    /// A *rendering* caller must use this one instead. Handing a renderer `None`
2304    /// means one of two things, and both are bugs the caller usually discovers
2305    /// far from here: text silently vanishes, or the caller invents its own
2306    /// fallback whose font is not registered where the renderer later looks it
2307    /// up by hash — so layout succeeds and rendering cannot resolve what layout
2308    /// produced.
2309    ///
2310    /// Resolution order, mirroring fontconfig's own relaxation:
2311    ///   1. the pattern exactly as given;
2312    ///   2. the same pattern with `name`/`family` cleared — keeps weight, slant,
2313    ///      monospace and the requested unicode coverage, so a Bold request does
2314    ///      not silently become Regular;
2315    ///   3. coverage only — the last-resort "any font that can draw this text".
2316    ///
2317    /// Each step is a strictly wider query than the last, so this never returns a
2318    /// *worse* match than `query` would have.
2319    pub fn query_with_fallback(
2320        &self,
2321        pattern: &FcPattern,
2322        trace: &mut Vec<TraceMsg>,
2323    ) -> Option<FontMatch> {
2324        if let Some(m) = self.query(pattern, trace) {
2325            return Some(m);
2326        }
2327
2328        // 2. Drop the family/name constraint, keep how it should LOOK.
2329        if pattern.name.is_some() || pattern.family.is_some() {
2330            let relaxed = FcPattern {
2331                name: None,
2332                family: None,
2333                ..pattern.clone()
2334            };
2335            if let Some(m) = self.query(&relaxed, trace) {
2336                return Some(m);
2337            }
2338        }
2339
2340        // 3. Coverage only. Anything that can render the requested ranges.
2341        let bare = FcPattern {
2342            unicode_ranges: pattern.unicode_ranges.clone(),
2343            ..FcPattern::default()
2344        };
2345        self.query(&bare, trace)
2346    }
2347
2348    /// Queries a font from the in-memory cache, returns the first found font (early return)
2349    /// Memory fonts are always preferred over disk fonts with the same match quality.
2350    ///
2351    /// This is FALLIBLE by design — see [`FcFontCache::query_with_fallback`] for the
2352    /// `fc-match`-style total variant that a renderer should use.
2353    pub fn query(&self, pattern: &FcPattern, trace: &mut Vec<TraceMsg>) -> Option<FontMatch> {
2354        let state = self.state_read();
2355        let mut matches = Vec::new();
2356
2357        for (stored_pattern, id) in &state.patterns {
2358            if Self::query_matches_internal(stored_pattern, pattern, trace) {
2359                let metadata = state.metadata.get(id).unwrap_or(stored_pattern);
2360
2361                // Calculate Unicode compatibility score
2362                let unicode_compatibility = if pattern.unicode_ranges.is_empty() {
2363                    // No specific Unicode requirements, use general coverage
2364                    Self::calculate_unicode_coverage(&metadata.unicode_ranges) as i32
2365                } else {
2366                    // Calculate how well this font covers the requested Unicode ranges
2367                    Self::calculate_unicode_compatibility(&pattern.unicode_ranges, &metadata.unicode_ranges)
2368                };
2369
2370                let style_score = Self::calculate_style_score(pattern, metadata);
2371
2372                // Memory fonts get a bonus to prefer them over disk fonts
2373                let is_memory = state.memory_fonts.contains_key(id);
2374
2375                matches.push((*id, unicode_compatibility, style_score, metadata.clone(), is_memory));
2376            }
2377        }
2378
2379        // Sort by: 1. Memory font (preferred), 2. Unicode compatibility, 3. Style score
2380        matches.sort_by(|a, b| {
2381            // Memory fonts first
2382            b.4.cmp(&a.4)
2383                .then_with(|| b.1.cmp(&a.1)) // Unicode compatibility (higher is better)
2384                .then_with(|| a.2.cmp(&b.2)) // Style score (lower is better)
2385        });
2386
2387        matches.first().map(|(id, _, _, metadata, _)| {
2388            FontMatch {
2389                id: *id,
2390                unicode_ranges: metadata.unicode_ranges.clone(),
2391                fallbacks: Vec::new(), // Fallbacks computed lazily via compute_fallbacks()
2392            }
2393        })
2394    }
2395
2396    /// Queries all fonts matching a pattern (internal use only).
2397    ///
2398    /// Note: This function is now private. Use resolve_font_chain() to build a font fallback chain,
2399    /// then call FontFallbackChain::query_for_text() to resolve fonts for specific text.
2400    fn query_internal(&self, pattern: &FcPattern, trace: &mut Vec<TraceMsg>) -> Vec<FontMatch> {
2401        let state = self.state_read();
2402        self.query_internal_locked(&state, pattern, trace)
2403    }
2404
2405    /// Internal variant used when the caller already holds a read
2406    /// guard on the state. Avoids re-locking.
2407    fn query_internal_locked(
2408        &self,
2409        state: &FcFontCacheInner,
2410        pattern: &FcPattern,
2411        trace: &mut Vec<TraceMsg>,
2412    ) -> Vec<FontMatch> {
2413        let mut matches = Vec::new();
2414
2415        for (stored_pattern, id) in &state.patterns {
2416            if Self::query_matches_internal(stored_pattern, pattern, trace) {
2417                let metadata = state.metadata.get(id).unwrap_or(stored_pattern);
2418
2419                // Calculate Unicode compatibility score
2420                let unicode_compatibility = if pattern.unicode_ranges.is_empty() {
2421                    Self::calculate_unicode_coverage(&metadata.unicode_ranges) as i32
2422                } else {
2423                    Self::calculate_unicode_compatibility(&pattern.unicode_ranges, &metadata.unicode_ranges)
2424                };
2425
2426                let style_score = Self::calculate_style_score(pattern, metadata);
2427                matches.push((*id, unicode_compatibility, style_score, metadata.clone()));
2428            }
2429        }
2430
2431        // Sort by style score (lowest first), THEN by Unicode compatibility (highest first)
2432        // Style matching (weight, italic, etc.) is now the primary criterion
2433        // Deterministic tiebreaker: prefer non-italic, then alphabetical by name
2434        matches.sort_by(|a, b| {
2435            a.2.cmp(&b.2) // Style score (lower is better)
2436                .then_with(|| b.1.cmp(&a.1)) // Unicode compatibility (higher is better)
2437                .then_with(|| a.3.italic.cmp(&b.3.italic)) // Prefer non-italic
2438                .then_with(|| a.3.name.cmp(&b.3.name)) // Alphabetical tiebreaker
2439        });
2440
2441        matches
2442            .into_iter()
2443            .map(|(id, _, _, metadata)| {
2444                FontMatch {
2445                    id,
2446                    unicode_ranges: metadata.unicode_ranges.clone(),
2447                    fallbacks: Vec::new(), // Fallbacks computed lazily via compute_fallbacks()
2448                }
2449            })
2450            .collect()
2451    }
2452
2453    /// Compute fallback fonts for a given font
2454    /// This is a lazy operation that can be expensive - only call when actually needed
2455    /// (e.g., for FFI or debugging, not needed for resolve_char)
2456    pub fn compute_fallbacks(
2457        &self,
2458        font_id: &FontId,
2459        trace: &mut Vec<TraceMsg>,
2460    ) -> Vec<FontMatchNoFallback> {
2461        let state = self.state_read();
2462        let pattern = match state.metadata.get(font_id) {
2463            Some(p) => p.clone(),
2464            None => return Vec::new(),
2465        };
2466        drop(state);
2467
2468        self.compute_fallbacks_for_pattern(&pattern, Some(font_id), trace)
2469    }
2470
2471    fn compute_fallbacks_for_pattern(
2472        &self,
2473        pattern: &FcPattern,
2474        exclude_id: Option<&FontId>,
2475        _trace: &mut Vec<TraceMsg>,
2476    ) -> Vec<FontMatchNoFallback> {
2477        let state = self.state_read();
2478        let mut candidates = Vec::new();
2479
2480        // Collect all potential fallbacks (excluding original pattern)
2481        for (stored_pattern, id) in &state.patterns {
2482            // Skip if this is the original font
2483            if exclude_id.is_some() && exclude_id.unwrap() == id {
2484                continue;
2485            }
2486
2487            // Check if this font supports any of the unicode ranges
2488            if !stored_pattern.unicode_ranges.is_empty() && !pattern.unicode_ranges.is_empty() {
2489                // Calculate Unicode compatibility
2490                let unicode_compatibility = Self::calculate_unicode_compatibility(
2491                    &pattern.unicode_ranges,
2492                    &stored_pattern.unicode_ranges
2493                );
2494
2495                // Only include if there's actual overlap
2496                if unicode_compatibility > 0 {
2497                    let style_score = Self::calculate_style_score(pattern, stored_pattern);
2498                    candidates.push((
2499                        FontMatchNoFallback {
2500                            id: *id,
2501                            unicode_ranges: stored_pattern.unicode_ranges.clone(),
2502                        },
2503                        unicode_compatibility,
2504                        style_score,
2505                        stored_pattern.clone(),
2506                    ));
2507                }
2508            } else if pattern.unicode_ranges.is_empty() && !stored_pattern.unicode_ranges.is_empty() {
2509                // No specific Unicode requirements, use general coverage
2510                let coverage = Self::calculate_unicode_coverage(&stored_pattern.unicode_ranges) as i32;
2511                let style_score = Self::calculate_style_score(pattern, stored_pattern);
2512                candidates.push((
2513                    FontMatchNoFallback {
2514                        id: *id,
2515                        unicode_ranges: stored_pattern.unicode_ranges.clone(),
2516                    },
2517                    coverage,
2518                    style_score,
2519                    stored_pattern.clone(),
2520                ));
2521            }
2522        }
2523
2524        drop(state);
2525
2526        // Sort by Unicode compatibility (highest first), THEN by style score (lowest first)
2527        candidates.sort_by(|a, b| {
2528            b.1.cmp(&a.1)
2529                .then_with(|| a.2.cmp(&b.2))
2530        });
2531
2532        // Deduplicate by keeping only the best match per unique unicode range
2533        let mut seen_ranges = Vec::new();
2534        let mut deduplicated = Vec::new();
2535
2536        for (id, _, _, pattern) in candidates {
2537            let mut is_new_range = false;
2538
2539            for range in &pattern.unicode_ranges {
2540                if !seen_ranges.iter().any(|r: &UnicodeRange| r.overlaps(range)) {
2541                    seen_ranges.push(*range);
2542                    is_new_range = true;
2543                }
2544            }
2545
2546            if is_new_range {
2547                deduplicated.push(id);
2548            }
2549        }
2550
2551        deduplicated
2552    }
2553
2554    /// Get in-memory font data (cloned out of the shared state).
2555    pub fn get_memory_font(&self, id: &FontId) -> Option<FcFont> {
2556        self.state_read().memory_fonts.get(id).cloned()
2557    }
2558
2559    /// Check if a pattern matches the query, with detailed tracing
2560    fn trace_path(k: &FcPattern) -> String {
2561        k.name.as_ref().cloned().unwrap_or_else(|| "<unknown>".to_string())
2562    }
2563
2564    pub fn query_matches_internal(
2565        k: &FcPattern,
2566        pattern: &FcPattern,
2567        trace: &mut Vec<TraceMsg>,
2568    ) -> bool {
2569        // Check name - substring match
2570        if let Some(ref name) = pattern.name {
2571            if !k.name.as_ref().map_or(false, |kn| kn.contains(name)) {
2572                trace.push(TraceMsg {
2573                    level: TraceLevel::Info,
2574                    path: Self::trace_path(k),
2575                    reason: MatchReason::NameMismatch {
2576                        requested: pattern.name.clone(),
2577                        found: k.name.clone(),
2578                    },
2579                });
2580                return false;
2581            }
2582        }
2583
2584        // Check family - substring match
2585        if let Some(ref family) = pattern.family {
2586            if !k.family.as_ref().map_or(false, |kf| kf.contains(family)) {
2587                trace.push(TraceMsg {
2588                    level: TraceLevel::Info,
2589                    path: Self::trace_path(k),
2590                    reason: MatchReason::FamilyMismatch {
2591                        requested: pattern.family.clone(),
2592                        found: k.family.clone(),
2593                    },
2594                });
2595                return false;
2596            }
2597        }
2598
2599        // Check style properties
2600        let style_properties = [
2601            (
2602                "italic",
2603                pattern.italic.needs_to_match(),
2604                pattern.italic.matches(&k.italic),
2605            ),
2606            (
2607                "oblique",
2608                pattern.oblique.needs_to_match(),
2609                pattern.oblique.matches(&k.oblique),
2610            ),
2611            (
2612                "bold",
2613                pattern.bold.needs_to_match(),
2614                pattern.bold.matches(&k.bold),
2615            ),
2616            (
2617                "monospace",
2618                pattern.monospace.needs_to_match(),
2619                pattern.monospace.matches(&k.monospace),
2620            ),
2621            (
2622                "condensed",
2623                pattern.condensed.needs_to_match(),
2624                pattern.condensed.matches(&k.condensed),
2625            ),
2626        ];
2627
2628        for (property_name, needs_to_match, matches) in style_properties {
2629            if needs_to_match && !matches {
2630                let (requested, found) = match property_name {
2631                    "italic" => (format!("{:?}", pattern.italic), format!("{:?}", k.italic)),
2632                    "oblique" => (format!("{:?}", pattern.oblique), format!("{:?}", k.oblique)),
2633                    "bold" => (format!("{:?}", pattern.bold), format!("{:?}", k.bold)),
2634                    "monospace" => (
2635                        format!("{:?}", pattern.monospace),
2636                        format!("{:?}", k.monospace),
2637                    ),
2638                    "condensed" => (
2639                        format!("{:?}", pattern.condensed),
2640                        format!("{:?}", k.condensed),
2641                    ),
2642                    _ => (String::new(), String::new()),
2643                };
2644
2645                trace.push(TraceMsg {
2646                    level: TraceLevel::Info,
2647                    path: Self::trace_path(k),
2648                    reason: MatchReason::StyleMismatch {
2649                        property: property_name,
2650                        requested,
2651                        found,
2652                    },
2653                });
2654                return false;
2655            }
2656        }
2657
2658        // Check weight - hard filter if non-normal weight is requested
2659        if pattern.weight != FcWeight::Normal && pattern.weight != k.weight {
2660            trace.push(TraceMsg {
2661                level: TraceLevel::Info,
2662                path: Self::trace_path(k),
2663                reason: MatchReason::WeightMismatch {
2664                    requested: pattern.weight,
2665                    found: k.weight,
2666                },
2667            });
2668            return false;
2669        }
2670
2671        // Check stretch - hard filter if non-normal stretch is requested
2672        if pattern.stretch != FcStretch::Normal && pattern.stretch != k.stretch {
2673            trace.push(TraceMsg {
2674                level: TraceLevel::Info,
2675                path: Self::trace_path(k),
2676                reason: MatchReason::StretchMismatch {
2677                    requested: pattern.stretch,
2678                    found: k.stretch,
2679                },
2680            });
2681            return false;
2682        }
2683
2684        // Check unicode ranges if specified
2685        if !pattern.unicode_ranges.is_empty() {
2686            let mut has_overlap = false;
2687
2688            for p_range in &pattern.unicode_ranges {
2689                for k_range in &k.unicode_ranges {
2690                    if p_range.overlaps(k_range) {
2691                        has_overlap = true;
2692                        break;
2693                    }
2694                }
2695                if has_overlap {
2696                    break;
2697                }
2698            }
2699
2700            if !has_overlap {
2701                trace.push(TraceMsg {
2702                    level: TraceLevel::Info,
2703                    path: Self::trace_path(k),
2704                    reason: MatchReason::UnicodeRangeMismatch {
2705                        character: '\0', // No specific character to report
2706                        ranges: k.unicode_ranges.clone(),
2707                    },
2708                });
2709                return false;
2710            }
2711        }
2712
2713        true
2714    }
2715    
2716    /// Resolve a complete font fallback chain for a CSS font-family stack
2717    /// This is the main entry point for font resolution with caching
2718    /// Automatically expands generic CSS families (serif, sans-serif, monospace) to OS-specific fonts
2719    /// 
2720    /// # Arguments
2721    /// * `font_families` - CSS font-family stack (e.g., ["Arial", "sans-serif"])
2722    /// * `text` - The text to render (used to extract Unicode ranges)
2723    /// * `weight` - Font weight
2724    /// * `italic` - Italic style requirement
2725    /// * `oblique` - Oblique style requirement
2726    /// * `trace` - Debug trace messages
2727    /// 
2728    /// # Returns
2729    /// A complete font fallback chain with CSS fallbacks and Unicode fallbacks
2730    /// 
2731    /// # Example
2732    /// ```no_run
2733    /// # use rust_fontconfig::{FcFontCache, FcWeight, PatternMatch};
2734    /// let cache = FcFontCache::build();
2735    /// let families = vec!["Arial".to_string(), "sans-serif".to_string()];
2736    /// let chain = cache.resolve_font_chain(&families, FcWeight::Normal, 
2737    ///                                       PatternMatch::DontCare, PatternMatch::DontCare, 
2738    ///                                       &mut Vec::new());
2739    /// // On macOS: families expanded to ["Arial", "San Francisco", "Helvetica Neue", "Lucida Grande"]
2740    /// ```
2741    #[cfg(feature = "std")]
2742    pub fn resolve_font_chain(
2743        &self,
2744        font_families: &[String],
2745        weight: FcWeight,
2746        italic: PatternMatch,
2747        oblique: PatternMatch,
2748        trace: &mut Vec<TraceMsg>,
2749    ) -> FontFallbackChain {
2750        self.resolve_font_chain_with_os(font_families, weight, italic, oblique, trace, OperatingSystem::current())
2751    }
2752    
2753    /// Resolve font chain with explicit OS specification (useful for testing)
2754    #[cfg(feature = "std")]
2755    pub fn resolve_font_chain_with_os(
2756        &self,
2757        font_families: &[String],
2758        weight: FcWeight,
2759        italic: PatternMatch,
2760        oblique: PatternMatch,
2761        trace: &mut Vec<TraceMsg>,
2762        os: OperatingSystem,
2763    ) -> FontFallbackChain {
2764        self.resolve_font_chain_impl(font_families, weight, italic, oblique, None, trace, os)
2765    }
2766
2767    /// Resolve a font fallback chain, restricting Unicode fallbacks to the
2768    /// caller-supplied set of scripts (usually derived from the actual
2769    /// text content of the document).
2770    ///
2771    /// - `scripts_hint: None` → back-compat behaviour, equivalent to
2772    ///   [`FcFontCache::resolve_font_chain`]: pulls in fallback fonts for
2773    ///   the full [`DEFAULT_UNICODE_FALLBACK_SCRIPTS`] set.
2774    /// - `scripts_hint: Some(&[])` → no Unicode fallbacks attached. For
2775    ///   an ASCII-only page this avoids pulling Arial Unicode MS,
2776    ///   CJK fonts, etc. into memory when they're not needed.
2777    /// - `scripts_hint: Some(&[CJK])` → only CJK fallback attached.
2778    ///
2779    /// The chain cache is keyed so an ASCII-only resolution cannot be
2780    /// served from a slot populated by a default/all-scripts resolution.
2781    #[cfg(feature = "std")]
2782    pub fn resolve_font_chain_with_scripts(
2783        &self,
2784        font_families: &[String],
2785        weight: FcWeight,
2786        italic: PatternMatch,
2787        oblique: PatternMatch,
2788        scripts_hint: Option<&[UnicodeRange]>,
2789        trace: &mut Vec<TraceMsg>,
2790    ) -> FontFallbackChain {
2791        self.resolve_font_chain_impl(
2792            font_families, weight, italic, oblique, scripts_hint,
2793            trace, OperatingSystem::current(),
2794        )
2795    }
2796
2797    /// Shared entry used by [`resolve_font_chain_with_os`] and
2798    /// [`resolve_font_chain_with_scripts`]. Handles the cache lookup,
2799    /// generic-family expansion, and delegation to the uncached builder.
2800    #[cfg(feature = "std")]
2801    fn resolve_font_chain_impl(
2802        &self,
2803        font_families: &[String],
2804        weight: FcWeight,
2805        italic: PatternMatch,
2806        oblique: PatternMatch,
2807        scripts_hint: Option<&[UnicodeRange]>,
2808        trace: &mut Vec<TraceMsg>,
2809        os: OperatingSystem,
2810    ) -> FontFallbackChain {
2811        // Check cache FIRST - key uses original (unexpanded) families
2812        // plus a hash over the scripts_hint so ASCII-only callers don't
2813        // consume a slot filled by a default-scripts caller.
2814        let scripts_hint_hash = scripts_hint.map(hash_scripts_hint);
2815        let cache_key = FontChainCacheKey {
2816            font_families: font_families.to_vec(),
2817            weight,
2818            italic,
2819            oblique,
2820            scripts_hint_hash,
2821        };
2822
2823        if let Some(cached) = self
2824            .shared
2825            .chain_cache
2826            .lock()
2827            .ok()
2828            .and_then(|c| c.get(&cache_key).cloned())
2829        {
2830            return cached;
2831        }
2832
2833        // Expand generic CSS families to OS-specific fonts
2834        let expanded_families = expand_font_families(font_families, os, &[]);
2835
2836        // Keep the originally-requested generic families ("serif",
2837        // "sans-serif", "monospace", ...) around. The expansion above turns
2838        // them into a hardcoded list of real OS font names and drops the
2839        // generic name itself; the chain builder uses this list to fall back
2840        // to *registered* fonts when none of those OS names exist (wasm,
2841        // headless caches, or an embedder that only registered an in-memory
2842        // bundled font). See `resolve_font_chain_uncached`.
2843        let generic_fallbacks: Vec<String> = font_families
2844            .iter()
2845            .filter(|f| config::is_generic_family(f))
2846            .cloned()
2847            .collect();
2848
2849        // Build the chain
2850        let chain = self.resolve_font_chain_uncached(
2851            &expanded_families,
2852            &generic_fallbacks,
2853            weight,
2854            italic,
2855            oblique,
2856            scripts_hint,
2857            trace,
2858        );
2859
2860        // Cache the result
2861        if let Ok(mut cache) = self.shared.chain_cache.lock() {
2862            cache.insert(cache_key, chain.clone());
2863        }
2864
2865        chain
2866    }
2867    
2868    /// Internal implementation without caching.
2869    ///
2870    /// `scripts_hint`:
2871    /// - `None` pulls in the full [`DEFAULT_UNICODE_FALLBACK_SCRIPTS`]
2872    ///   set (the original, back-compat behaviour).
2873    /// - `Some(&[])` attaches no Unicode fallbacks.
2874    /// - `Some(ranges)` attaches fallbacks only for those ranges.
2875    #[cfg(feature = "std")]
2876    fn resolve_font_chain_uncached(
2877        &self,
2878        font_families: &[String],
2879        generic_fallbacks: &[String],
2880        weight: FcWeight,
2881        italic: PatternMatch,
2882        oblique: PatternMatch,
2883        scripts_hint: Option<&[UnicodeRange]>,
2884        trace: &mut Vec<TraceMsg>,
2885    ) -> FontFallbackChain {
2886        let mut css_fallbacks = Vec::new();
2887        
2888        // Resolve each CSS font-family to its system fallbacks
2889        for (_i, family) in font_families.iter().enumerate() {
2890            // Check if this is a generic font family
2891            let (pattern, is_generic) = if config::is_generic_family(family) {
2892                let monospace = if family.eq_ignore_ascii_case("monospace") {
2893                    PatternMatch::True
2894                } else {
2895                    PatternMatch::False
2896                };
2897                let pattern = FcPattern {
2898                    name: None,
2899                    weight,
2900                    italic,
2901                    oblique,
2902                    monospace,
2903                    unicode_ranges: Vec::new(),
2904                    ..Default::default()
2905                };
2906                (pattern, true)
2907            } else {
2908                // Specific font family name
2909                let pattern = FcPattern {
2910                    name: Some(family.clone()),
2911                    weight,
2912                    italic,
2913                    oblique,
2914                    unicode_ranges: Vec::new(),
2915                    ..Default::default()
2916                };
2917                (pattern, false)
2918            };
2919            
2920            // Use fuzzy matching for specific fonts (fast token-based lookup)
2921            // For generic families, use query (slower but necessary for property matching)
2922            let mut matches = if is_generic {
2923                // Generic families need full pattern matching
2924                self.query_internal(&pattern, trace)
2925            } else {
2926                // Specific font names: use fast token-based fuzzy matching.
2927                let mut m = self.fuzzy_query_by_name(family, weight, italic, oblique, &[], trace);
2928                // The token-fuzzy index is a no-op on the azul web-lift fork
2929                // (`index_pattern_tokens`), so `fuzzy_query_by_name` returns nothing
2930                // for every specific family name. Without a fallback here the whole
2931                // expanded CSS stack ("DejaVu Sans", "Noto Sans", "Liberation Sans",
2932                // …) resolves to NOTHING, and generic families collapse to the
2933                // coverage/style-ranked `name: None` fallback below — which grabs the
2934                // highest-Unicode-coverage CJK megafont (Noto Sans JP/CJK) for plain
2935                // Latin body text and picks arbitrary weights (a Bold-Italic for a
2936                // Regular request). Fall back to a normalized exact-family lookup so
2937                // the real Latin fallback names actually match. Normalized equality
2938                // ("noto sans" -> "notosans") also fixes the substring leak where
2939                // "Noto Sans" would otherwise latch onto "Noto Sans JP".
2940                if m.is_empty() {
2941                    m = self.query_by_family_normalized(family, weight, italic, oblique);
2942                }
2943                m
2944            };
2945            
2946            // For generic families, limit to top 5 fonts to avoid too many matches
2947            if is_generic && matches.len() > 5 {
2948                matches.truncate(5);
2949            }
2950            
2951            // Always add the CSS fallback group to preserve CSS ordering
2952            // even if no fonts were found for this family
2953            css_fallbacks.push(CssFallbackGroup {
2954                css_name: family.clone(),
2955                fonts: matches,
2956            });
2957        }
2958
2959        // Headless / wasm / memory-only fallback.
2960        //
2961        // Generic CSS families ("serif"/"sans-serif"/"monospace"/...) were
2962        // expanded by the caller to a hardcoded list of real OS font names.
2963        // On a system that actually has those fonts the loop above matched
2964        // them and we're done. But on wasm, a headless cache, or an embedder
2965        // that only registered an in-memory bundled font, NONE of those OS
2966        // names exist — and the original generic name was dropped, so a
2967        // registered font (whatever its family name) would never be reached.
2968        //
2969        // So: if the whole expanded stack matched nothing at all, retry each
2970        // originally-requested generic family as a generic `name: None`
2971        // query, which any registered font can satisfy. This runs ONLY when
2972        // nothing else matched, so on systems with real fonts it adds nothing
2973        // and never reorders real matches (any such fallback must come AFTER
2974        // real matches).
2975        if !generic_fallbacks.is_empty()
2976            && css_fallbacks.iter().all(|g| g.fonts.is_empty())
2977        {
2978            for generic in generic_fallbacks {
2979                let monospace = if generic.eq_ignore_ascii_case("monospace") {
2980                    PatternMatch::True
2981                } else {
2982                    PatternMatch::False
2983                };
2984                let pattern = FcPattern {
2985                    name: None,
2986                    weight,
2987                    italic,
2988                    oblique,
2989                    monospace,
2990                    unicode_ranges: Vec::new(),
2991                    ..Default::default()
2992                };
2993                let mut matches = self.query_internal(&pattern, trace);
2994                if matches.len() > 5 {
2995                    matches.truncate(5);
2996                }
2997                if !matches.is_empty() {
2998                    css_fallbacks.push(CssFallbackGroup {
2999                        css_name: generic.clone(),
3000                        fonts: matches,
3001                    });
3002                }
3003            }
3004        }
3005
3006        // Populate unicode_fallbacks. CSS fallback fonts may falsely claim
3007        // coverage of a script via the OS/2 unicode-range bits without
3008        // actually having glyphs, so we supplement the CSS chain with an
3009        // explicit lookup for each requested script block. resolve_char()
3010        // prefers CSS fallbacks first (earlier in the chain wins).
3011        //
3012        // The set of script blocks to cover is caller-controlled via
3013        // `scripts_hint`: `None` keeps the back-compat DEFAULT_UNICODE_FALLBACK_SCRIPTS
3014        // behaviour (7 scripts) so existing `resolve_font_chain` consumers
3015        // stay unchanged; `Some(&[])` opts into "no unicode fallbacks at all"
3016        // for ASCII-only documents, eliminating the big CJK / Arabic fonts
3017        // from the resolved chain (and therefore from eager downstream parses).
3018        let important_ranges: &[UnicodeRange] =
3019            scripts_hint.unwrap_or(DEFAULT_UNICODE_FALLBACK_SCRIPTS);
3020        let unicode_fallbacks = if important_ranges.is_empty() {
3021            Vec::new()
3022        } else {
3023            let all_uncovered = vec![false; important_ranges.len()];
3024            self.find_unicode_fallbacks(
3025                important_ranges,
3026                &all_uncovered,
3027                &css_fallbacks,
3028                weight,
3029                italic,
3030                oblique,
3031                trace,
3032            )
3033        };
3034
3035        // WEB-LIFT LAST-RESORT (2026-06-03; the `with_memory_fonts` trap that previously made
3036        // editing this file fatal is now fixed by the byte-atomic remill fork support). In the
3037        // lifted web backend `find_unicode_fallbacks` returns 0 fonts even though one IS
3038        // registered (the matching/iteration mis-lifts), so BOTH chain lists come back empty →
3039        // every consumer (resolve_char, query_for_text, prune_chain_to_used_chars) sees no font
3040        // → the layout unwraps a None → OOB. When the chain would be empty, append the first
3041        // registered font so the chain is non-empty. Native chains are never empty here.
3042        let mut unicode_fallbacks = unicode_fallbacks;
3043        if css_fallbacks.is_empty() && unicode_fallbacks.is_empty() {
3044            let st = self.state_read();
3045            if let Some((pat, id)) = st.patterns.iter().next() {
3046                unicode_fallbacks.push(FontMatch {
3047                    id: *id,
3048                    unicode_ranges: pat.unicode_ranges.clone(),
3049                    fallbacks: Vec::new(),
3050                });
3051            }
3052        }
3053
3054        FontFallbackChain {
3055            css_fallbacks,
3056            unicode_fallbacks,
3057            original_stack: font_families.to_vec(),
3058        }
3059    }
3060
3061    /// Extract Unicode ranges from text
3062    #[allow(dead_code)]
3063    fn extract_unicode_ranges(text: &str) -> Vec<UnicodeRange> {
3064        let mut chars: Vec<char> = text.chars().collect();
3065        chars.sort_unstable();
3066        chars.dedup();
3067        
3068        if chars.is_empty() {
3069            return Vec::new();
3070        }
3071        
3072        let mut ranges = Vec::new();
3073        let mut range_start = chars[0] as u32;
3074        let mut range_end = range_start;
3075        
3076        for &c in &chars[1..] {
3077            let codepoint = c as u32;
3078            if codepoint == range_end + 1 {
3079                range_end = codepoint;
3080            } else {
3081                ranges.push(UnicodeRange { start: range_start, end: range_end });
3082                range_start = codepoint;
3083                range_end = codepoint;
3084            }
3085        }
3086        
3087        ranges.push(UnicodeRange { start: range_start, end: range_end });
3088        ranges
3089    }
3090    
3091    /// Fuzzy query for fonts by name when exact match fails
3092    /// Uses intelligent token-based matching with inverted index for speed:
3093    /// 1. Break name into tokens (e.g., "NotoSansJP" -> ["noto", "sans", "jp"])
3094    /// 2. Use token_index to find candidate fonts via BTreeSet intersection
3095    /// 3. Score only the candidate fonts (instead of all 800+ patterns)
3096    /// 4. Prioritize fonts matching more tokens + Unicode coverage
3097    #[cfg(feature = "std")]
3098    fn fuzzy_query_by_name(
3099        &self,
3100        requested_name: &str,
3101        weight: FcWeight,
3102        italic: PatternMatch,
3103        oblique: PatternMatch,
3104        unicode_ranges: &[UnicodeRange],
3105        _trace: &mut Vec<TraceMsg>,
3106    ) -> Vec<FontMatch> {
3107        // Extract tokens from the requested name (e.g., "NotoSansJP" -> ["noto", "sans", "jp"])
3108        let tokens = Self::extract_font_name_tokens(requested_name);
3109        
3110        if tokens.is_empty() {
3111            return Vec::new();
3112        }
3113        
3114        // Convert tokens to lowercase for case-insensitive lookup
3115        let tokens_lower: Vec<String> = tokens.iter().map(|t| t.to_ascii_lowercase()).collect();
3116        
3117        // Progressive token matching strategy:
3118        // Start with first token, then progressively narrow down with each additional token
3119        // If adding a token results in 0 matches, use the previous (broader) set
3120        // Example: ["Noto"] -> 10 fonts, ["Noto","Sans"] -> 2 fonts, ["Noto","Sans","JP"] -> 0 fonts => use 2 fonts
3121        
3122        let state = self.state_read();
3123
3124        // Start with the first token
3125        let first_token = &tokens_lower[0];
3126        let mut candidate_ids = match state.token_index.get(first_token) {
3127            Some(ids) if !ids.is_empty() => ids.clone(),
3128            _ => {
3129                // First token not found - no fonts match, quit immediately
3130                return Vec::new();
3131            }
3132        };
3133
3134        // Progressively narrow down with each additional token
3135        for token in &tokens_lower[1..] {
3136            if let Some(token_ids) = state.token_index.get(token) {
3137                // Calculate intersection
3138                let intersection: alloc::collections::BTreeSet<FontId> =
3139                    candidate_ids.intersection(token_ids).copied().collect();
3140
3141                if intersection.is_empty() {
3142                    // Adding this token results in 0 matches - keep previous set and stop
3143                    break;
3144                } else {
3145                    // Successfully narrowed down - use intersection
3146                    candidate_ids = intersection;
3147                }
3148            } else {
3149                // Token not in index - keep current set and stop
3150                break;
3151            }
3152        }
3153
3154        // Now score only the candidate fonts (HUGE speedup!)
3155        let mut candidates = Vec::new();
3156
3157        for id in candidate_ids {
3158            let pattern = match state.metadata.get(&id) {
3159                Some(p) => p,
3160                None => continue,
3161            };
3162            
3163            // Get pre-tokenized font name (already lowercase)
3164            let font_tokens_lower = match state.font_tokens.get(&id) {
3165                Some(tokens) => tokens,
3166                None => continue,
3167            };
3168            
3169            if font_tokens_lower.is_empty() {
3170                continue;
3171            }
3172            
3173            // Calculate token match score (how many requested tokens appear in font name)
3174            // Both tokens_lower and font_tokens_lower are already lowercase, so direct comparison
3175            let token_matches = tokens_lower.iter()
3176                .filter(|req_token| {
3177                    font_tokens_lower.iter().any(|font_token| {
3178                        // Both already lowercase — exact token match (index guarantees candidates)
3179                        font_token == *req_token
3180                    })
3181                })
3182                .count();
3183            
3184            // Skip if no tokens match (shouldn't happen due to index, but safety check)
3185            if token_matches == 0 {
3186                continue;
3187            }
3188            
3189            // Calculate token similarity score (0-100)
3190            let token_similarity = (token_matches * 100 / tokens.len()) as i32;
3191            
3192            // Calculate Unicode range similarity
3193            let unicode_similarity = if !unicode_ranges.is_empty() && !pattern.unicode_ranges.is_empty() {
3194                Self::calculate_unicode_compatibility(unicode_ranges, &pattern.unicode_ranges)
3195            } else {
3196                0
3197            };
3198            
3199            // CRITICAL: If we have Unicode requirements, ONLY accept fonts that cover them
3200            // A font with great name match but no Unicode coverage is useless
3201            if !unicode_ranges.is_empty() && unicode_similarity == 0 {
3202                continue;
3203            }
3204            
3205            let style_score = Self::calculate_style_score(&FcPattern {
3206                weight,
3207                italic,
3208                oblique,
3209                ..Default::default()
3210            }, pattern);
3211            
3212            candidates.push((
3213                id,
3214                token_similarity,
3215                unicode_similarity,
3216                style_score,
3217                pattern.clone(),
3218            ));
3219        }
3220        
3221        // Sort by:
3222        // 1. Token matches (more matches = better)
3223        // 2. Unicode compatibility (if ranges provided)
3224        // 3. Style score (lower is better)
3225        // 4. Deterministic tiebreaker: prefer non-italic, then by font name
3226        candidates.sort_by(|a, b| {
3227            if !unicode_ranges.is_empty() {
3228                // When we have Unicode requirements, prioritize coverage
3229                b.1.cmp(&a.1) // Token similarity (higher is better) - PRIMARY
3230                    .then_with(|| b.2.cmp(&a.2)) // Unicode similarity (higher is better) - SECONDARY
3231                    .then_with(|| a.3.cmp(&b.3)) // Style score (lower is better) - TERTIARY
3232                    .then_with(|| a.4.italic.cmp(&b.4.italic)) // Prefer non-italic (False < True)
3233                    .then_with(|| a.4.name.cmp(&b.4.name)) // Alphabetical by name
3234            } else {
3235                // No Unicode requirements, token similarity is primary
3236                b.1.cmp(&a.1) // Token similarity (higher is better)
3237                    .then_with(|| a.3.cmp(&b.3)) // Style score (lower is better)
3238                    .then_with(|| a.4.italic.cmp(&b.4.italic)) // Prefer non-italic (False < True)
3239                    .then_with(|| a.4.name.cmp(&b.4.name)) // Alphabetical by name
3240            }
3241        });
3242        
3243        // Take top 5 matches
3244        candidates.truncate(5);
3245        
3246        // Convert to FontMatch
3247        candidates
3248            .into_iter()
3249            .map(|(id, _token_sim, _unicode_sim, _style, pattern)| {
3250                FontMatch {
3251                    id,
3252                    unicode_ranges: pattern.unicode_ranges.clone(),
3253                    fallbacks: Vec::new(), // Fallbacks computed lazily via compute_fallbacks()
3254                }
3255            })
3256            .collect()
3257    }
3258
3259    /// Resolve a specific CSS family name to registered faces by NORMALIZED
3260    /// family equality, ranked by style (weight/italic/oblique) closeness.
3261    ///
3262    /// This is the correct, stable matcher for a concrete `font-family` name
3263    /// (as opposed to a generic like `sans-serif`): it matches
3264    /// `font-family: "DejaVu Sans"` to the family whose normalized name is
3265    /// exactly `dejavusans` — never to `dejavusansmono` or `dejavusanscondensed`,
3266    /// and never `"Noto Sans"` to `"Noto Sans JP"`. `normalize_family_name`
3267    /// strips spaces/hyphens/case so the CSS spelling and the stored family
3268    /// spelling line up regardless of formatting.
3269    ///
3270    /// Among faces of the matched family the best style score wins (exact
3271    /// weight, then nearest weight; correct slant), so `font-weight: bold`
3272    /// selects the Bold face and a Regular request avoids Bold/Italic faces.
3273    /// Falls back to matching the stored `name` by the same normalized rule for
3274    /// fonts that carry no family field.
3275    fn query_by_family_normalized(
3276        &self,
3277        family: &str,
3278        weight: FcWeight,
3279        italic: PatternMatch,
3280        oblique: PatternMatch,
3281    ) -> Vec<FontMatch> {
3282        let target = crate::utils::normalize_family_name(family);
3283        if target.is_empty() {
3284            return Vec::new();
3285        }
3286        let query = FcPattern {
3287            weight,
3288            italic,
3289            oblique,
3290            ..Default::default()
3291        };
3292        let state = self.state_read();
3293        let mut candidates: Vec<(FontId, i32, FcPattern)> = Vec::new();
3294        for (stored_pattern, id) in &state.patterns {
3295            let meta = state.metadata.get(id).unwrap_or(stored_pattern);
3296            let fam_norm = meta
3297                .family
3298                .as_deref()
3299                .map(crate::utils::normalize_family_name)
3300                .unwrap_or_default();
3301            let matches_family = fam_norm == target
3302                || meta
3303                    .name
3304                    .as_deref()
3305                    .map(crate::utils::normalize_family_name)
3306                    .is_some_and(|n| n == target);
3307            if !matches_family {
3308                continue;
3309            }
3310            let style_score = Self::calculate_style_score(&query, meta);
3311            candidates.push((*id, style_score, meta.clone()));
3312        }
3313        drop(state);
3314
3315        // Lowest style score first; deterministic tiebreak: non-italic, then name.
3316        candidates.sort_by(|a, b| {
3317            a.1.cmp(&b.1)
3318                .then_with(|| a.2.italic.cmp(&b.2.italic))
3319                .then_with(|| a.2.name.cmp(&b.2.name))
3320        });
3321        candidates.truncate(5);
3322        candidates
3323            .into_iter()
3324            .map(|(id, _, pattern)| FontMatch {
3325                id,
3326                unicode_ranges: pattern.unicode_ranges.clone(),
3327                fallbacks: Vec::new(),
3328            })
3329            .collect()
3330    }
3331
3332    /// Extract tokens from a font name
3333    /// E.g., "NotoSansJP" -> ["Noto", "Sans", "JP"]
3334    /// E.g., "Noto Sans CJK JP" -> ["Noto", "Sans", "CJK", "JP"]
3335    pub fn extract_font_name_tokens(name: &str) -> Vec<String> {
3336        let mut tokens = Vec::new();
3337        let mut current_token = String::new();
3338        let mut last_was_lower = false;
3339        
3340        for c in name.chars() {
3341            if c.is_whitespace() || c == '-' || c == '_' {
3342                // Word separator
3343                if !current_token.is_empty() {
3344                    tokens.push(current_token.clone());
3345                    current_token.clear();
3346                }
3347                last_was_lower = false;
3348            } else if c.is_uppercase() && last_was_lower && !current_token.is_empty() {
3349                // CamelCase boundary (e.g., "Noto" | "Sans")
3350                tokens.push(current_token.clone());
3351                current_token.clear();
3352                current_token.push(c);
3353                last_was_lower = false;
3354            } else {
3355                current_token.push(c);
3356                last_was_lower = c.is_lowercase();
3357            }
3358        }
3359        
3360        if !current_token.is_empty() {
3361            tokens.push(current_token);
3362        }
3363        
3364        tokens
3365    }
3366    
3367    /// Find fonts to cover missing Unicode ranges
3368    /// Uses intelligent matching: prefers fonts with similar names to existing ones
3369    /// Early quits once all Unicode ranges are covered for performance
3370    fn find_unicode_fallbacks(
3371        &self,
3372        unicode_ranges: &[UnicodeRange],
3373        covered_chars: &[bool],
3374        existing_groups: &[CssFallbackGroup],
3375        _weight: FcWeight,
3376        _italic: PatternMatch,
3377        _oblique: PatternMatch,
3378        trace: &mut Vec<TraceMsg>,
3379    ) -> Vec<FontMatch> {
3380        // Extract uncovered ranges
3381        let mut uncovered_ranges = Vec::new();
3382        for (i, &covered) in covered_chars.iter().enumerate() {
3383            if !covered && i < unicode_ranges.len() {
3384                uncovered_ranges.push(unicode_ranges[i].clone());
3385            }
3386        }
3387        
3388        if uncovered_ranges.is_empty() {
3389            return Vec::new();
3390        }
3391
3392        // Query for fonts that cover these ranges.
3393        // Use DontCare for weight/italic/oblique — we want ANY font that covers
3394        // the missing characters, regardless of style. The similarity sort below
3395        // will prefer fonts matching the existing chain's style anyway.
3396        let pattern = FcPattern {
3397            name: None,
3398            weight: FcWeight::Normal, // Normal weight is not filtered by query_matches_internal (line 1836)
3399            italic: PatternMatch::DontCare,
3400            oblique: PatternMatch::DontCare,
3401            unicode_ranges: uncovered_ranges.clone(),
3402            ..Default::default()
3403        };
3404        
3405        let mut candidates = self.query_internal(&pattern, trace);
3406
3407        // Intelligent sorting: prefer fonts with similar names to existing ones
3408        // Extract font family prefixes from existing fonts (e.g., "Noto Sans" from "Noto Sans JP")
3409        let existing_prefixes: Vec<String> = existing_groups
3410            .iter()
3411            .flat_map(|group| {
3412                group.fonts.iter().filter_map(|font| {
3413                    self.get_metadata_by_id(&font.id)
3414                        .and_then(|meta| meta.family.clone())
3415                        .and_then(|family| {
3416                            // Extract prefix (e.g., "Noto Sans" from "Noto Sans JP")
3417                            family.split_whitespace()
3418                                .take(2)
3419                                .collect::<Vec<_>>()
3420                                .join(" ")
3421                                .into()
3422                        })
3423                })
3424            })
3425            .collect();
3426        
3427        // Sort candidates by:
3428        // 1. Name similarity to existing fonts (highest priority)
3429        // 2. Unicode coverage (secondary)
3430        candidates.sort_by(|a, b| {
3431            let a_meta = self.get_metadata_by_id(&a.id);
3432            let b_meta = self.get_metadata_by_id(&b.id);
3433
3434            let a_score = Self::calculate_font_similarity_score(a_meta.as_ref(), &existing_prefixes);
3435            let b_score = Self::calculate_font_similarity_score(b_meta.as_ref(), &existing_prefixes);
3436            
3437            b_score.cmp(&a_score) // Higher score = better match
3438                .then_with(|| {
3439                    let a_coverage = Self::calculate_unicode_compatibility(&uncovered_ranges, &a.unicode_ranges);
3440                    let b_coverage = Self::calculate_unicode_compatibility(&uncovered_ranges, &b.unicode_ranges);
3441                    b_coverage.cmp(&a_coverage)
3442                })
3443        });
3444        
3445        // Early quit optimization: only take fonts until all ranges are covered
3446        let mut result = Vec::new();
3447        let mut remaining_uncovered: Vec<bool> = vec![true; uncovered_ranges.len()];
3448        
3449        for candidate in candidates {
3450            // Check which ranges this font covers
3451            let mut covers_new_range = false;
3452            
3453            for (i, range) in uncovered_ranges.iter().enumerate() {
3454                if remaining_uncovered[i] {
3455                    // Check if this font covers this range
3456                    for font_range in &candidate.unicode_ranges {
3457                        if font_range.overlaps(range) {
3458                            remaining_uncovered[i] = false;
3459                            covers_new_range = true;
3460                            break;
3461                        }
3462                    }
3463                }
3464            }
3465            
3466            // Only add fonts that cover at least one new range
3467            if covers_new_range {
3468                result.push(candidate);
3469                
3470                // Early quit: if all ranges are covered, stop
3471                if remaining_uncovered.iter().all(|&uncovered| !uncovered) {
3472                    break;
3473                }
3474            }
3475        }
3476        
3477        result
3478    }
3479    
3480    /// Calculate similarity score between a font and existing font prefixes
3481    /// Higher score = more similar
3482    fn calculate_font_similarity_score(
3483        font_meta: Option<&FcPattern>,
3484        existing_prefixes: &[String],
3485    ) -> i32 {
3486        let Some(meta) = font_meta else { return 0; };
3487        let Some(family) = &meta.family else { return 0; };
3488        
3489        // Check if this font's family matches any existing prefix
3490        for prefix in existing_prefixes {
3491            if family.starts_with(prefix) {
3492                return 100; // Strong match
3493            }
3494            if family.contains(prefix) {
3495                return 50; // Partial match
3496            }
3497        }
3498        
3499        0 // No match
3500    }
3501    
3502    /// Find fallback fonts for a given pattern
3503    // Helper to calculate total unicode coverage
3504    pub fn calculate_unicode_coverage(ranges: &[UnicodeRange]) -> u64 {
3505        ranges
3506            .iter()
3507            .map(|range| (range.end - range.start + 1) as u64)
3508            .sum()
3509    }
3510
3511    /// Coalesce ranges into a sorted, **disjoint** set.
3512    ///
3513    /// [`FcFontCache::calculate_unicode_coverage`] sums `end - start + 1` with no
3514    /// overlap handling, and that sum ranks fallback candidates. A font's coverage
3515    /// is built from two sources whose block boundaries do not align — the OS/2
3516    /// `ulUnicodeRange` bit mappings and the cmap block probe — so merging them
3517    /// naively double-counts the overlap and inflates the score. That is exactly
3518    /// how a CJK megafont wins a Latin run it has no business winning.
3519    ///
3520    /// Touching ranges (`prev.end + 1 == next.start`) are merged as well: they
3521    /// describe the same contiguous coverage, and leaving them split would make
3522    /// one set compare unequal to another purely by which source produced it.
3523    pub fn normalize_unicode_ranges(mut ranges: Vec<UnicodeRange>) -> Vec<UnicodeRange> {
3524        if ranges.len() < 2 {
3525            return ranges;
3526        }
3527
3528        ranges.sort_unstable();
3529
3530        let mut out: Vec<UnicodeRange> = Vec::with_capacity(ranges.len());
3531        for range in ranges {
3532            match out.last_mut() {
3533                // Overlapping or touching: extend. `saturating_add` so an `end` of
3534                // u32::MAX cannot wrap around into a bogus failure-to-merge.
3535                Some(prev) if range.start <= prev.end.saturating_add(1) => {
3536                    prev.end = prev.end.max(range.end);
3537                }
3538                _ => out.push(range),
3539            }
3540        }
3541        out
3542    }
3543
3544    /// Calculate how well a font's Unicode ranges cover the requested ranges
3545    /// Returns a compatibility score (higher is better, 0 means no overlap)
3546    pub fn calculate_unicode_compatibility(
3547        requested: &[UnicodeRange],
3548        available: &[UnicodeRange],
3549    ) -> i32 {
3550        if requested.is_empty() {
3551            // No specific requirements, return total coverage
3552            return Self::calculate_unicode_coverage(available) as i32;
3553        }
3554        
3555        let mut total_coverage = 0u32;
3556        
3557        for req_range in requested {
3558            for avail_range in available {
3559                // Calculate overlap between requested and available ranges
3560                let overlap_start = req_range.start.max(avail_range.start);
3561                let overlap_end = req_range.end.min(avail_range.end);
3562                
3563                if overlap_start <= overlap_end {
3564                    // There is overlap
3565                    let overlap_size = overlap_end - overlap_start + 1;
3566                    total_coverage += overlap_size;
3567                }
3568            }
3569        }
3570        
3571        total_coverage as i32
3572    }
3573
3574    pub fn calculate_style_score(original: &FcPattern, candidate: &FcPattern) -> i32 {
3575
3576        let mut score = 0_i32;
3577
3578        // Weight calculation with special handling for bold property
3579        if (original.bold == PatternMatch::True && candidate.weight == FcWeight::Bold)
3580            || (original.bold == PatternMatch::False && candidate.weight != FcWeight::Bold)
3581        {
3582            // No weight penalty when bold is requested and font has Bold weight
3583            // No weight penalty when non-bold is requested and font has non-Bold weight
3584        } else {
3585            // Apply normal weight difference penalty
3586            let weight_diff = (original.weight as i32 - candidate.weight as i32).abs();
3587            score += weight_diff as i32;
3588        }
3589
3590        // Exact weight match bonus: reward fonts whose weight matches the request exactly,
3591        // with an extra bonus when both are Normal (the most common case for body text)
3592        if original.weight == candidate.weight {
3593            score -= 15;
3594            if original.weight == FcWeight::Normal {
3595                score -= 10; // Extra bonus for Normal-Normal match
3596            }
3597        }
3598
3599        // Stretch calculation with special handling for condensed property
3600        if (original.condensed == PatternMatch::True && candidate.stretch.is_condensed())
3601            || (original.condensed == PatternMatch::False && !candidate.stretch.is_condensed())
3602        {
3603            // No stretch penalty when condensed is requested and font has condensed stretch
3604            // No stretch penalty when non-condensed is requested and font has non-condensed stretch
3605        } else {
3606            // Apply normal stretch difference penalty
3607            let stretch_diff = (original.stretch as i32 - candidate.stretch as i32).abs();
3608            score += (stretch_diff * 100) as i32;
3609        }
3610
3611        // Handle style properties with standard penalties and bonuses
3612        let style_props = [
3613            (original.italic, candidate.italic, 300, 150),
3614            (original.oblique, candidate.oblique, 200, 100),
3615            (original.bold, candidate.bold, 300, 150),
3616            (original.monospace, candidate.monospace, 100, 50),
3617            (original.condensed, candidate.condensed, 100, 50),
3618        ];
3619
3620        for (orig, cand, mismatch_penalty, dontcare_penalty) in style_props {
3621            if orig.needs_to_match() {
3622                if orig == PatternMatch::False && cand == PatternMatch::DontCare {
3623                    // Requesting non-italic but font doesn't declare: small penalty
3624                    // (less than a full mismatch but more than a perfect match)
3625                    score += dontcare_penalty / 2;
3626                } else if !orig.matches(&cand) {
3627                    if cand == PatternMatch::DontCare {
3628                        score += dontcare_penalty;
3629                    } else {
3630                        score += mismatch_penalty;
3631                    }
3632                } else if orig == PatternMatch::True && cand == PatternMatch::True {
3633                    // Give bonus for exact True match
3634                    score -= 20;
3635                } else if orig == PatternMatch::False && cand == PatternMatch::False {
3636                    // Give bonus for exact False match (prefer explicitly non-italic
3637                    // over fonts with unknown/DontCare italic status)
3638                    score -= 20;
3639                }
3640            } else {
3641                // orig == DontCare: prefer "normal" fonts over styled ones.
3642                // When the caller doesn't specify italic/bold/etc., a font
3643                // that IS italic/bold should score slightly worse than one
3644                // that isn't, so Regular is chosen over Italic by default.
3645                if cand == PatternMatch::True {
3646                    score += dontcare_penalty / 3;
3647                }
3648            }
3649        }
3650
3651        // ── Name-based "base font" detection ──
3652        // The shorter the font name relative to its family, the more "basic" the
3653        // variant.  E.g. "System Font" (the base) should score better than
3654        // "System Font Regular Italic" (a variant) when the user hasn't
3655        // explicitly requested italic.
3656        if let (Some(name), Some(family)) = (&candidate.name, &candidate.family) {
3657            let name_lower = name.to_ascii_lowercase();
3658            let family_lower = family.to_ascii_lowercase();
3659
3660            // Strip the family prefix from the name to get the "extra" part
3661            let extra = if name_lower.starts_with(&family_lower) {
3662                name_lower[family_lower.len()..].to_string()
3663            } else {
3664                String::new()
3665            };
3666
3667            // Strip common neutral descriptors that don't indicate a style variant
3668            let stripped = extra
3669                .replace("regular", "")
3670                .replace("normal", "")
3671                .replace("book", "")
3672                .replace("roman", "");
3673            let stripped = stripped.trim();
3674
3675            if stripped.is_empty() {
3676                // This is a "base font" – name is just the family (± "Regular")
3677                score -= 50;
3678            } else {
3679                // Name has extra style descriptors – add a penalty per extra word
3680                let extra_words = stripped.split_whitespace().count();
3681                score += (extra_words as i32) * 25;
3682            }
3683        }
3684
3685        // ── Subfamily "Regular" bonus ──
3686        // Fonts whose OpenType subfamily is exactly "Regular" are the canonical
3687        // base variant and should be strongly preferred.
3688        if let Some(ref subfamily) = candidate.metadata.font_subfamily {
3689            let sf_lower = subfamily.to_ascii_lowercase();
3690            if sf_lower == "regular" {
3691                score -= 30;
3692            }
3693        }
3694
3695        score
3696    }
3697}
3698
3699#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
3700fn FcScanDirectories() -> Option<(
3701    Vec<(FcPattern, FcFontPath)>,
3702    BTreeMap<String, FcFontRenderConfig>,
3703    BTreeMap<String, Vec<String>>,
3704)> {
3705    use std::fs;
3706    use std::path::Path;
3707
3708    // Real fontconfig honors $FONTCONFIG_FILE as the root config; so do we
3709    // (hermetic test setups and sandboxes depend on it).
3710    let base_path = std::env::var("FONTCONFIG_FILE")
3711        .ok()
3712        .filter(|p| !p.is_empty())
3713        .unwrap_or_else(|| "/etc/fonts/fonts.conf".to_string());
3714
3715    if !Path::new(&base_path).exists() {
3716        return None;
3717    }
3718
3719    let mut font_paths = Vec::with_capacity(32);
3720    let mut paths_to_visit = vec![(None, PathBuf::from(&base_path))];
3721    let mut render_configs: BTreeMap<String, FcFontRenderConfig> = BTreeMap::new();
3722    let mut system_aliases: BTreeMap<String, Vec<String>> = BTreeMap::new();
3723
3724    while let Some((prefix, path_to_visit)) = paths_to_visit.pop() {
3725        let path = match process_path(&prefix, path_to_visit, true) {
3726            Some(path) => path,
3727            None => continue,
3728        };
3729
3730        let metadata = match fs::metadata(&path) {
3731            Ok(metadata) => metadata,
3732            Err(_) => continue,
3733        };
3734
3735        if metadata.is_file() {
3736            let xml_utf8 = match fs::read_to_string(&path) {
3737                Ok(xml_utf8) => xml_utf8,
3738                Err(_) => continue,
3739            };
3740
3741            if ParseFontsConf(&xml_utf8, &mut paths_to_visit, &mut font_paths).is_none() {
3742                continue;
3743            }
3744
3745            // Also parse render config blocks from this file
3746            ParseFontsConfRenderConfig(&xml_utf8, &mut render_configs);
3747
3748            // And <alias>/<prefer> preference blocks (generic families and
3749            // named substitutions alike).
3750            ParseFontsConfAliases(&xml_utf8, &mut system_aliases);
3751        } else if metadata.is_dir() {
3752            let dir_entries = match fs::read_dir(&path) {
3753                Ok(dir_entries) => dir_entries,
3754                Err(_) => continue,
3755            };
3756
3757            for entry_result in dir_entries {
3758                let entry = match entry_result {
3759                    Ok(entry) => entry,
3760                    Err(_) => continue,
3761                };
3762
3763                let entry_path = entry.path();
3764
3765                // `fs::metadata` traverses symbolic links
3766                let entry_metadata = match fs::metadata(&entry_path) {
3767                    Ok(metadata) => metadata,
3768                    Err(_) => continue,
3769                };
3770
3771                if !entry_metadata.is_file() {
3772                    continue;
3773                }
3774
3775                let file_name = match entry_path.file_name() {
3776                    Some(name) => name,
3777                    None => continue,
3778                };
3779
3780                let file_name_str = file_name.to_string_lossy();
3781                if file_name_str.starts_with(|c: char| c.is_ascii_digit())
3782                    && file_name_str.ends_with(".conf")
3783                {
3784                    paths_to_visit.push((None, entry_path));
3785                }
3786            }
3787        }
3788    }
3789
3790    if font_paths.is_empty() {
3791        return None;
3792    }
3793
3794    Some((FcScanDirectoriesInner(&font_paths), render_configs, system_aliases))
3795}
3796
3797/// Parse `<alias><family>NAME</family><prefer><family>...</family>...</prefer></alias>`
3798/// blocks from a fontconfig XML file into `aliases`.
3799///
3800/// Keys are normalized with [`crate::utils::normalize_family_name`];
3801/// preferred families keep their configured order, appended across files
3802/// in include order (fontconfig semantics), deduplicated.
3803#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
3804fn ParseFontsConfAliases(input: &str, aliases: &mut BTreeMap<String, Vec<String>>) {
3805    use xmlparser::Token::*;
3806    use xmlparser::Tokenizer;
3807
3808    #[derive(Clone, Copy, PartialEq)]
3809    enum State {
3810        Idle,
3811        InAlias,
3812        InAliasFamily,
3813        InPrefer,
3814        InPreferFamily,
3815    }
3816
3817    let mut state = State::Idle;
3818    let mut alias_key: Option<String> = None;
3819    let mut preferred: Vec<String> = Vec::new();
3820    let mut text_buf = String::new();
3821
3822    for token_result in Tokenizer::from(input) {
3823        let token = match token_result {
3824            Ok(token) => token,
3825            Err(_) => continue,
3826        };
3827        match token {
3828            ElementStart { local, .. } => match local.as_str() {
3829                "alias" => {
3830                    state = State::InAlias;
3831                    alias_key = None;
3832                    preferred.clear();
3833                }
3834                "family" if state == State::InAlias => {
3835                    state = State::InAliasFamily;
3836                    text_buf.clear();
3837                }
3838                "prefer" if state == State::InAlias => {
3839                    state = State::InPrefer;
3840                }
3841                "family" if state == State::InPrefer => {
3842                    state = State::InPreferFamily;
3843                    text_buf.clear();
3844                }
3845                _ => {}
3846            },
3847            Text { text } => {
3848                if state == State::InAliasFamily || state == State::InPreferFamily {
3849                    text_buf.push_str(text.as_str());
3850                }
3851            }
3852            ElementEnd { end, .. } => {
3853                use xmlparser::ElementEnd;
3854                let closed = match end {
3855                    ElementEnd::Close(_, local) => Some(local.as_str().to_owned()),
3856                    _ => None,
3857                };
3858                let Some(closed) = closed else { continue };
3859                match closed.as_str() {
3860                    "family" => match state {
3861                        State::InAliasFamily => {
3862                            let t = text_buf.trim();
3863                            if !t.is_empty() && alias_key.is_none() {
3864                                alias_key = Some(t.to_owned());
3865                            }
3866                            state = State::InAlias;
3867                        }
3868                        State::InPreferFamily => {
3869                            let t = text_buf.trim();
3870                            if !t.is_empty() {
3871                                preferred.push(t.to_owned());
3872                            }
3873                            state = State::InPrefer;
3874                        }
3875                        _ => {}
3876                    },
3877                    "prefer" if state == State::InPrefer => {
3878                        state = State::InAlias;
3879                    }
3880                    "alias" => {
3881                        if let Some(key) = alias_key.take() {
3882                            if !preferred.is_empty() {
3883                                let norm = crate::utils::normalize_family_name(&key);
3884                                let entry = aliases.entry(norm).or_default();
3885                                for fam in preferred.drain(..) {
3886                                    if !entry.iter().any(|e| e == &fam) {
3887                                        entry.push(fam);
3888                                    }
3889                                }
3890                            }
3891                        }
3892                        state = State::Idle;
3893                    }
3894                    _ => {}
3895                }
3896            }
3897            _ => {}
3898        }
3899    }
3900}
3901
3902// Parses the fonts.conf file
3903#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
3904fn ParseFontsConf(
3905    input: &str,
3906    paths_to_visit: &mut Vec<(Option<String>, PathBuf)>,
3907    font_paths: &mut Vec<(Option<String>, String)>,
3908) -> Option<()> {
3909    use xmlparser::Token::*;
3910    use xmlparser::Tokenizer;
3911
3912    const TAG_INCLUDE: &str = "include";
3913    const TAG_DIR: &str = "dir";
3914    const ATTRIBUTE_PREFIX: &str = "prefix";
3915
3916    let mut current_prefix: Option<&str> = None;
3917    let mut current_path: Option<&str> = None;
3918    let mut is_in_include = false;
3919    let mut is_in_dir = false;
3920
3921    for token_result in Tokenizer::from(input) {
3922        let token = match token_result {
3923            Ok(token) => token,
3924            Err(_) => return None,
3925        };
3926
3927        match token {
3928            ElementStart { local, .. } => {
3929                if is_in_include || is_in_dir {
3930                    return None; /* error: nested tags */
3931                }
3932
3933                match local.as_str() {
3934                    TAG_INCLUDE => {
3935                        is_in_include = true;
3936                    }
3937                    TAG_DIR => {
3938                        is_in_dir = true;
3939                    }
3940                    _ => continue,
3941                }
3942
3943                current_path = None;
3944            }
3945            Text { text, .. } => {
3946                let text = text.as_str().trim();
3947                if text.is_empty() {
3948                    continue;
3949                }
3950                if is_in_include || is_in_dir {
3951                    current_path = Some(text);
3952                }
3953            }
3954            Attribute { local, value, .. } => {
3955                if !is_in_include && !is_in_dir {
3956                    continue;
3957                }
3958                // attribute on <include> or <dir> node
3959                if local.as_str() == ATTRIBUTE_PREFIX {
3960                    current_prefix = Some(value.as_str());
3961                }
3962            }
3963            ElementEnd { end, .. } => {
3964                let end_tag = match end {
3965                    xmlparser::ElementEnd::Close(_, a) => a,
3966                    _ => continue,
3967                };
3968
3969                match end_tag.as_str() {
3970                    TAG_INCLUDE => {
3971                        if !is_in_include {
3972                            continue;
3973                        }
3974
3975                        if let Some(current_path) = current_path.as_ref() {
3976                            paths_to_visit.push((
3977                                current_prefix.map(ToOwned::to_owned),
3978                                PathBuf::from(*current_path),
3979                            ));
3980                        }
3981                    }
3982                    TAG_DIR => {
3983                        if !is_in_dir {
3984                            continue;
3985                        }
3986
3987                        if let Some(current_path) = current_path.as_ref() {
3988                            font_paths.push((
3989                                current_prefix.map(ToOwned::to_owned),
3990                                (*current_path).to_owned(),
3991                            ));
3992                        }
3993                    }
3994                    _ => continue,
3995                }
3996
3997                is_in_include = false;
3998                is_in_dir = false;
3999                current_path = None;
4000                current_prefix = None;
4001            }
4002            _ => {}
4003        }
4004    }
4005
4006    Some(())
4007}
4008
4009/// Parses `<match target="font">` blocks from fonts.conf XML and returns
4010/// a map from family name to per-font rendering configuration.
4011///
4012/// Example fonts.conf snippet that this handles:
4013/// ```xml
4014/// <match target="font">
4015///   <test name="family"><string>Inconsolata</string></test>
4016///   <edit name="antialias" mode="assign"><bool>true</bool></edit>
4017///   <edit name="hintstyle" mode="assign"><const>hintslight</const></edit>
4018/// </match>
4019/// ```
4020#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
4021fn ParseFontsConfRenderConfig(
4022    input: &str,
4023    configs: &mut BTreeMap<String, FcFontRenderConfig>,
4024) {
4025    use xmlparser::Token::*;
4026    use xmlparser::Tokenizer;
4027
4028    // Parser state machine
4029    #[derive(Clone, Copy, PartialEq)]
4030    enum State {
4031        /// Outside any relevant block
4032        Idle,
4033        /// Inside <match target="font">
4034        InMatchFont,
4035        /// Inside <test name="family"> within a match block
4036        InTestFamily,
4037        /// Inside <edit name="..."> within a match block
4038        InEdit,
4039        /// Inside a value element (<bool>, <double>, <const>, <string>) within <edit> or <test>
4040        InValue,
4041    }
4042
4043    let mut state = State::Idle;
4044    let mut match_is_font_target = false;
4045    let mut current_family: Option<String> = None;
4046    let mut current_edit_name: Option<String> = None;
4047    let mut current_value: Option<String> = None;
4048    let mut value_tag: Option<String> = None;
4049    let mut config = FcFontRenderConfig::default();
4050    let mut in_test = false;
4051    let mut test_name: Option<String> = None;
4052
4053    for token_result in Tokenizer::from(input) {
4054        let token = match token_result {
4055            Ok(token) => token,
4056            Err(_) => continue,
4057        };
4058
4059        match token {
4060            ElementStart { local, .. } => {
4061                let tag = local.as_str();
4062                match tag {
4063                    "match" => {
4064                        // Reset state for a new match block
4065                        match_is_font_target = false;
4066                        current_family = None;
4067                        config = FcFontRenderConfig::default();
4068                    }
4069                    "test" if state == State::InMatchFont => {
4070                        in_test = true;
4071                        test_name = None;
4072                    }
4073                    "edit" if state == State::InMatchFont => {
4074                        current_edit_name = None;
4075                    }
4076                    "bool" | "double" | "const" | "string" | "int" => {
4077                        if state == State::InTestFamily || state == State::InEdit {
4078                            value_tag = Some(tag.to_owned());
4079                            current_value = None;
4080                        }
4081                    }
4082                    _ => {}
4083                }
4084            }
4085            Attribute { local, value, .. } => {
4086                let attr_name = local.as_str();
4087                let attr_value = value.as_str();
4088
4089                match attr_name {
4090                    "target" => {
4091                        if attr_value == "font" {
4092                            match_is_font_target = true;
4093                        }
4094                    }
4095                    "name" => {
4096                        if in_test && state == State::InMatchFont {
4097                            test_name = Some(attr_value.to_owned());
4098                        } else if state == State::InMatchFont {
4099                            current_edit_name = Some(attr_value.to_owned());
4100                        }
4101                    }
4102                    _ => {}
4103                }
4104            }
4105            Text { text, .. } => {
4106                let text = text.as_str().trim();
4107                if !text.is_empty() && (state == State::InTestFamily || state == State::InEdit) {
4108                    current_value = Some(text.to_owned());
4109                }
4110            }
4111            ElementEnd { end, .. } => {
4112                match end {
4113                    xmlparser::ElementEnd::Open => {
4114                        // Tag just opened (after attributes processed)
4115                        if match_is_font_target && state == State::Idle {
4116                            state = State::InMatchFont;
4117                            match_is_font_target = false;
4118                        } else if in_test {
4119                            if test_name.as_deref() == Some("family") {
4120                                state = State::InTestFamily;
4121                            }
4122                            in_test = false;
4123                        } else if current_edit_name.is_some() && state == State::InMatchFont {
4124                            state = State::InEdit;
4125                        }
4126                    }
4127                    xmlparser::ElementEnd::Close(_, local) => {
4128                        let tag = local.as_str();
4129                        match tag {
4130                            "match" => {
4131                                // End of match block: store config if we have a family
4132                                if let Some(family) = current_family.take() {
4133                                    let empty = FcFontRenderConfig::default();
4134                                    if config != empty {
4135                                        configs.insert(family, config.clone());
4136                                    }
4137                                }
4138                                state = State::Idle;
4139                                config = FcFontRenderConfig::default();
4140                            }
4141                            "test" => {
4142                                if state == State::InTestFamily {
4143                                    // Extract the family name from the value we collected
4144                                    if let Some(ref val) = current_value {
4145                                        current_family = Some(val.clone());
4146                                    }
4147                                    state = State::InMatchFont;
4148                                }
4149                                current_value = None;
4150                                value_tag = None;
4151                            }
4152                            "edit" => {
4153                                if state == State::InEdit {
4154                                    // Apply the collected value to the config
4155                                    if let (Some(ref name), Some(ref val)) = (&current_edit_name, &current_value) {
4156                                        apply_edit_value(&mut config, name, val, value_tag.as_deref());
4157                                    }
4158                                    state = State::InMatchFont;
4159                                }
4160                                current_edit_name = None;
4161                                current_value = None;
4162                                value_tag = None;
4163                            }
4164                            "bool" | "double" | "const" | "string" | "int" => {
4165                                // value_tag and current_value already set by Text handler
4166                            }
4167                            _ => {}
4168                        }
4169                    }
4170                    xmlparser::ElementEnd::Empty => {
4171                        // Self-closing tags: nothing to do
4172                    }
4173                }
4174            }
4175            _ => {}
4176        }
4177    }
4178}
4179
4180/// Apply a parsed edit value to the render config.
4181#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
4182fn apply_edit_value(
4183    config: &mut FcFontRenderConfig,
4184    edit_name: &str,
4185    value: &str,
4186    value_tag: Option<&str>,
4187) {
4188    match edit_name {
4189        "antialias" => {
4190            config.antialias = parse_bool_value(value);
4191        }
4192        "hinting" => {
4193            config.hinting = parse_bool_value(value);
4194        }
4195        "autohint" => {
4196            config.autohint = parse_bool_value(value);
4197        }
4198        "embeddedbitmap" => {
4199            config.embeddedbitmap = parse_bool_value(value);
4200        }
4201        "embolden" => {
4202            config.embolden = parse_bool_value(value);
4203        }
4204        "minspace" => {
4205            config.minspace = parse_bool_value(value);
4206        }
4207        "hintstyle" => {
4208            config.hintstyle = parse_hintstyle_const(value);
4209        }
4210        "rgba" => {
4211            config.rgba = parse_rgba_const(value);
4212        }
4213        "lcdfilter" => {
4214            config.lcdfilter = parse_lcdfilter_const(value);
4215        }
4216        "dpi" => {
4217            if let Ok(v) = value.parse::<f64>() {
4218                config.dpi = Some(v);
4219            }
4220        }
4221        "scale" => {
4222            if let Ok(v) = value.parse::<f64>() {
4223                config.scale = Some(v);
4224            }
4225        }
4226        _ => {
4227            // Unknown edit property, ignore
4228        }
4229    }
4230}
4231
4232#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
4233fn parse_bool_value(value: &str) -> Option<bool> {
4234    match value {
4235        "true" => Some(true),
4236        "false" => Some(false),
4237        _ => None,
4238    }
4239}
4240
4241#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
4242fn parse_hintstyle_const(value: &str) -> Option<FcHintStyle> {
4243    match value {
4244        "hintnone" => Some(FcHintStyle::None),
4245        "hintslight" => Some(FcHintStyle::Slight),
4246        "hintmedium" => Some(FcHintStyle::Medium),
4247        "hintfull" => Some(FcHintStyle::Full),
4248        _ => None,
4249    }
4250}
4251
4252#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
4253fn parse_rgba_const(value: &str) -> Option<FcRgba> {
4254    match value {
4255        "unknown" => Some(FcRgba::Unknown),
4256        "rgb" => Some(FcRgba::Rgb),
4257        "bgr" => Some(FcRgba::Bgr),
4258        "vrgb" => Some(FcRgba::Vrgb),
4259        "vbgr" => Some(FcRgba::Vbgr),
4260        "none" => Some(FcRgba::None),
4261        _ => None,
4262    }
4263}
4264
4265#[cfg(all(feature = "std", feature = "parsing", target_os = "linux"))]
4266fn parse_lcdfilter_const(value: &str) -> Option<FcLcdFilter> {
4267    match value {
4268        "lcdnone" => Some(FcLcdFilter::None),
4269        "lcddefault" => Some(FcLcdFilter::Default),
4270        "lcdlight" => Some(FcLcdFilter::Light),
4271        "lcdlegacy" => Some(FcLcdFilter::Legacy),
4272        _ => None,
4273    }
4274}
4275
4276// Unicode range bit positions to actual ranges (full table from OpenType spec).
4277// Based on: https://learn.microsoft.com/en-us/typography/opentype/spec/os2#ur
4278#[cfg(all(feature = "std", feature = "parsing"))]
4279const UNICODE_RANGE_MAPPINGS: &[(usize, u32, u32)] = &[
4280    // ulUnicodeRange1 (bits 0-31)
4281    (0, 0x0000, 0x007F), // Basic Latin
4282    (1, 0x0080, 0x00FF), // Latin-1 Supplement
4283    (2, 0x0100, 0x017F), // Latin Extended-A
4284    (3, 0x0180, 0x024F), // Latin Extended-B
4285    (4, 0x0250, 0x02AF), // IPA Extensions
4286    (5, 0x02B0, 0x02FF), // Spacing Modifier Letters
4287    (6, 0x0300, 0x036F), // Combining Diacritical Marks
4288    (7, 0x0370, 0x03FF), // Greek and Coptic
4289    (8, 0x2C80, 0x2CFF), // Coptic
4290    (9, 0x0400, 0x04FF), // Cyrillic
4291    (10, 0x0530, 0x058F), // Armenian
4292    (11, 0x0590, 0x05FF), // Hebrew
4293    (12, 0x0600, 0x06FF), // Arabic
4294    (13, 0x0700, 0x074F), // Syriac
4295    (14, 0x0780, 0x07BF), // Thaana
4296    (15, 0x0900, 0x097F), // Devanagari
4297    (16, 0x0980, 0x09FF), // Bengali
4298    (17, 0x0A00, 0x0A7F), // Gurmukhi
4299    (18, 0x0A80, 0x0AFF), // Gujarati
4300    (19, 0x0B00, 0x0B7F), // Oriya
4301    (20, 0x0B80, 0x0BFF), // Tamil
4302    (21, 0x0C00, 0x0C7F), // Telugu
4303    (22, 0x0C80, 0x0CFF), // Kannada
4304    (23, 0x0D00, 0x0D7F), // Malayalam
4305    (24, 0x0E00, 0x0E7F), // Thai
4306    (25, 0x0E80, 0x0EFF), // Lao
4307    (26, 0x10A0, 0x10FF), // Georgian
4308    (27, 0x1B00, 0x1B7F), // Balinese
4309    (28, 0x1100, 0x11FF), // Hangul Jamo
4310    (29, 0x1E00, 0x1EFF), // Latin Extended Additional
4311    (30, 0x1F00, 0x1FFF), // Greek Extended
4312    (31, 0x2000, 0x206F), // General Punctuation
4313    // ulUnicodeRange2 (bits 32-63)
4314    (32, 0x2070, 0x209F), // Superscripts And Subscripts
4315    (33, 0x20A0, 0x20CF), // Currency Symbols
4316    (34, 0x20D0, 0x20FF), // Combining Diacritical Marks For Symbols
4317    (35, 0x2100, 0x214F), // Letterlike Symbols
4318    (36, 0x2150, 0x218F), // Number Forms
4319    (37, 0x2190, 0x21FF), // Arrows
4320    (38, 0x2200, 0x22FF), // Mathematical Operators
4321    (39, 0x2300, 0x23FF), // Miscellaneous Technical
4322    (40, 0x2400, 0x243F), // Control Pictures
4323    (41, 0x2440, 0x245F), // Optical Character Recognition
4324    (42, 0x2460, 0x24FF), // Enclosed Alphanumerics
4325    (43, 0x2500, 0x257F), // Box Drawing
4326    (44, 0x2580, 0x259F), // Block Elements
4327    (45, 0x25A0, 0x25FF), // Geometric Shapes
4328    (46, 0x2600, 0x26FF), // Miscellaneous Symbols
4329    (47, 0x2700, 0x27BF), // Dingbats
4330    (48, 0x3000, 0x303F), // CJK Symbols And Punctuation
4331    (49, 0x3040, 0x309F), // Hiragana
4332    (50, 0x30A0, 0x30FF), // Katakana
4333    (51, 0x3100, 0x312F), // Bopomofo
4334    (52, 0x3130, 0x318F), // Hangul Compatibility Jamo
4335    (53, 0x3190, 0x319F), // Kanbun
4336    (54, 0x31A0, 0x31BF), // Bopomofo Extended
4337    (55, 0x31C0, 0x31EF), // CJK Strokes
4338    (56, 0x31F0, 0x31FF), // Katakana Phonetic Extensions
4339    (57, 0x3200, 0x32FF), // Enclosed CJK Letters And Months
4340    (58, 0x3300, 0x33FF), // CJK Compatibility
4341    (59, 0x4E00, 0x9FFF), // CJK Unified Ideographs
4342    (60, 0xA000, 0xA48F), // Yi Syllables
4343    (61, 0xA490, 0xA4CF), // Yi Radicals
4344    (62, 0xAC00, 0xD7AF), // Hangul Syllables
4345    (63, 0xD800, 0xDFFF), // Non-Plane 0 (note: surrogates, not directly usable)
4346    // ulUnicodeRange3 (bits 64-95)
4347    (64, 0x10000, 0x10FFFF), // Phoenician and other non-BMP (bit 64 indicates non-BMP support)
4348    (65, 0xF900, 0xFAFF), // CJK Compatibility Ideographs
4349    (66, 0xFB00, 0xFB4F), // Alphabetic Presentation Forms
4350    (67, 0xFB50, 0xFDFF), // Arabic Presentation Forms-A
4351    (68, 0xFE00, 0xFE0F), // Variation Selectors
4352    (69, 0xFE10, 0xFE1F), // Vertical Forms
4353    (70, 0xFE20, 0xFE2F), // Combining Half Marks
4354    (71, 0xFE30, 0xFE4F), // CJK Compatibility Forms
4355    (72, 0xFE50, 0xFE6F), // Small Form Variants
4356    (73, 0xFE70, 0xFEFF), // Arabic Presentation Forms-B
4357    (74, 0xFF00, 0xFFEF), // Halfwidth And Fullwidth Forms
4358    (75, 0xFFF0, 0xFFFF), // Specials
4359    (76, 0x0F00, 0x0FFF), // Tibetan
4360    (77, 0x0700, 0x074F), // Syriac
4361    (78, 0x0780, 0x07BF), // Thaana
4362    (79, 0x0D80, 0x0DFF), // Sinhala
4363    (80, 0x1000, 0x109F), // Myanmar
4364    (81, 0x1200, 0x137F), // Ethiopic
4365    (82, 0x13A0, 0x13FF), // Cherokee
4366    (83, 0x1400, 0x167F), // Unified Canadian Aboriginal Syllabics
4367    (84, 0x1680, 0x169F), // Ogham
4368    (85, 0x16A0, 0x16FF), // Runic
4369    (86, 0x1780, 0x17FF), // Khmer
4370    (87, 0x1800, 0x18AF), // Mongolian
4371    (88, 0x2800, 0x28FF), // Braille Patterns
4372    (89, 0xA000, 0xA48F), // Yi Syllables
4373    (90, 0x1680, 0x169F), // Ogham
4374    (91, 0x16A0, 0x16FF), // Runic
4375    (92, 0x1700, 0x171F), // Tagalog
4376    (93, 0x1720, 0x173F), // Hanunoo
4377    (94, 0x1740, 0x175F), // Buhid
4378    (95, 0x1760, 0x177F), // Tagbanwa
4379    // ulUnicodeRange4 (bits 96-127)
4380    (96, 0x1900, 0x194F), // Limbu
4381    (97, 0x1950, 0x197F), // Tai Le
4382    (98, 0x1980, 0x19DF), // New Tai Lue
4383    (99, 0x1A00, 0x1A1F), // Buginese
4384    (100, 0x2C00, 0x2C5F), // Glagolitic
4385    (101, 0x2D30, 0x2D7F), // Tifinagh
4386    (102, 0x4DC0, 0x4DFF), // Yijing Hexagram Symbols
4387    (103, 0xA800, 0xA82F), // Syloti Nagri
4388    (104, 0x10000, 0x1007F), // Linear B Syllabary
4389    (105, 0x10080, 0x100FF), // Linear B Ideograms
4390    (106, 0x10100, 0x1013F), // Aegean Numbers
4391    (107, 0x10140, 0x1018F), // Ancient Greek Numbers
4392    (108, 0x10300, 0x1032F), // Old Italic
4393    (109, 0x10330, 0x1034F), // Gothic
4394    (110, 0x10380, 0x1039F), // Ugaritic
4395    (111, 0x103A0, 0x103DF), // Old Persian
4396    (112, 0x10400, 0x1044F), // Deseret
4397    (113, 0x10450, 0x1047F), // Shavian
4398    (114, 0x10480, 0x104AF), // Osmanya
4399    (115, 0x10800, 0x1083F), // Cypriot Syllabary
4400    (116, 0x10A00, 0x10A5F), // Kharoshthi
4401    (117, 0x1D000, 0x1D0FF), // Byzantine Musical Symbols
4402    (118, 0x1D100, 0x1D1FF), // Musical Symbols
4403    (119, 0x1D200, 0x1D24F), // Ancient Greek Musical Notation
4404    (120, 0x1D300, 0x1D35F), // Tai Xuan Jing Symbols
4405    (121, 0x1D400, 0x1D7FF), // Mathematical Alphanumeric Symbols
4406    (122, 0x1F000, 0x1F02F), // Mahjong Tiles
4407    (123, 0x1F030, 0x1F09F), // Domino Tiles
4408    (124, 0x1F300, 0x1F9FF), // Miscellaneous Symbols And Pictographs (Emoji)
4409    (125, 0x1F680, 0x1F6FF), // Transport And Map Symbols
4410    (126, 0x1F700, 0x1F77F), // Alchemical Symbols
4411    (127, 0x1F900, 0x1F9FF), // Supplemental Symbols and Pictographs
4412];
4413
4414/// Intermediate parsed data from a single font face within a font file.
4415/// Used to share parsing logic between `FcParseFont` and `FcParseFontBytesInner`.
4416#[cfg(all(feature = "std", feature = "parsing"))]
4417struct ParsedFontFace {
4418    pattern: FcPattern,
4419    font_index: usize,
4420}
4421
4422/// Parse all font table data from a single font face and return the extracted patterns.
4423///
4424/// This is the shared core of `FcParseFont` and `FcParseFontBytesInner`:
4425/// TTC detection, font table parsing, OS/2/head/post reading, unicode range extraction,
4426/// CMAP verification, monospace detection, metadata extraction, and pattern creation.
4427#[cfg(all(feature = "std", feature = "parsing"))]
4428fn parse_font_faces(font_bytes: &[u8]) -> Option<Vec<ParsedFontFace>> {
4429    use allsorts::{
4430        binary::read::ReadScope,
4431        font_data::FontData,
4432        get_name::fontcode_get_name,
4433        post::PostTable,
4434        tables::{
4435            os2::Os2, HeadTable, NameTable,
4436        },
4437        tag,
4438    };
4439    use std::collections::BTreeSet;
4440
4441    const FONT_SPECIFIER_NAME_ID: u16 = 4;
4442    const FONT_SPECIFIER_FAMILY_ID: u16 = 1;
4443
4444    let max_fonts = if font_bytes.len() >= 12 && &font_bytes[0..4] == b"ttcf" {
4445        // Read numFonts from TTC header (offset 8, 4 bytes)
4446        let num_fonts =
4447            u32::from_be_bytes([font_bytes[8], font_bytes[9], font_bytes[10], font_bytes[11]]);
4448        // Cap at a reasonable maximum as a safety measure
4449        std::cmp::min(num_fonts as usize, 100)
4450    } else {
4451        // Not a collection, just one font
4452        1
4453    };
4454
4455    let scope = ReadScope::new(font_bytes);
4456    let font_file = scope.read::<FontData<'_>>().ok()?;
4457
4458    // Handle collections properly by iterating through all fonts
4459    let mut results = Vec::new();
4460
4461    for font_index in 0..max_fonts {
4462        let provider = font_file.table_provider(font_index).ok()?;
4463        let head_data = provider.table_data(tag::HEAD).ok()??.into_owned();
4464        let head_table = ReadScope::new(&head_data).read::<HeadTable>().ok()?;
4465
4466        let is_bold = head_table.is_bold();
4467        let is_italic = head_table.is_italic();
4468        let mut detected_monospace = None;
4469
4470        let post_data = provider.table_data(tag::POST).ok()??;
4471        if let Ok(post_table) = ReadScope::new(&post_data).read::<PostTable>() {
4472            // isFixedPitch here - https://learn.microsoft.com/en-us/typography/opentype/spec/post#header
4473            detected_monospace = Some(post_table.header.is_fixed_pitch != 0);
4474        }
4475
4476        // Get font properties from OS/2 table.
4477        //
4478        // OS/2 is OPTIONAL in TrueType - only OpenType requires it - and plenty
4479        // of real fonts ship without one, including the base-14 PDF font subsets
4480        // printpdf embeds. This used to be `.ok()??`, which turned "no OS/2" into
4481        // "not a font" and made the whole face invisible to the cache even though
4482        // allsorts parses it perfectly well.
4483        //
4484        // Nothing below actually needs OS/2: `head.macStyle` already gave us bold
4485        // and italic, `post`/`hmtx` cover monospace, and coverage has been
4486        // cmap-authoritative since 4.4.8. So treat it as the hint it is.
4487        let os2_data = provider.table_data(tag::OS_2).ok().flatten();
4488        let os2_table = os2_data
4489            .as_deref()
4490            .and_then(|data| ReadScope::new(data).read_dep::<Os2>(data.len()).ok());
4491
4492        // Extract additional style information
4493        let is_oblique = os2_table.as_ref().is_some_and(|os2| {
4494            os2.fs_selection
4495                .contains(allsorts::tables::os2::FsSelectionFlag::OBLIQUE)
4496        });
4497        // Without OS/2 the only weight signal is the `head.macStyle` bold bit, so
4498        // the face lands on Bold or Normal rather than a precise class.
4499        let weight = os2_table.as_ref().map_or(
4500            if is_bold { FcWeight::Bold } else { FcWeight::Normal },
4501            |os2| FcWeight::from_u16(os2.us_weight_class),
4502        );
4503        let stretch = os2_table
4504            .as_ref()
4505            .map_or(FcStretch::Normal, |os2| FcStretch::from_u16(os2.us_width_class));
4506
4507        // Extract unicode ranges from OS/2 table (fast, but may be inaccurate)
4508        // These are hints about what the font *should* support
4509        // For actual glyph coverage verification, query the font file directly
4510        let mut unicode_ranges = Vec::new();
4511
4512        // Process the 4 Unicode range bitfields from OS/2 table. All-zero when
4513        // there is no OS/2 table, which claims nothing and leaves the cmap union
4514        // below to supply the whole coverage set.
4515        let os2_ranges = os2_table.as_ref().map_or([0u32; 4], |os2| {
4516            [
4517                os2.ul_unicode_range1,
4518                os2.ul_unicode_range2,
4519                os2.ul_unicode_range3,
4520                os2.ul_unicode_range4,
4521            ]
4522        });
4523
4524        for &(bit, start, end) in UNICODE_RANGE_MAPPINGS {
4525            let range_idx = bit / 32;
4526            let bit_pos = bit % 32;
4527            if range_idx < 4 && (os2_ranges[range_idx] & (1 << bit_pos)) != 0 {
4528                unicode_ranges.push(UnicodeRange { start, end });
4529            }
4530        }
4531
4532        // OS/2's ulUnicodeRange bits are a HINT, never an upper bound.
4533        //
4534        // Fonts get these bits wrong in BOTH directions. Over-claiming is the
4535        // well-known one: a font advertises a block it has no glyphs for, so
4536        // verify against the cmap and drop what it cannot actually draw.
4537        //
4538        // Under-claiming is the one that used to be invisible here. Noto Sans
4539        // CJK's JP face has Hangul glyphs in its cmap but leaves the Hangul bits
4540        // clear; gating coverage on OS/2 made those codepoints permanently
4541        // unmatchable, so 한국어 resolved to no font at all even with the covering
4542        // face installed. fontconfig does not have this failure mode because it
4543        // builds FcCharSet by walking the cmap itself and never consults
4544        // ulUnicodeRange for coverage.
4545        //
4546        // So: prune what OS/2 over-claims, then union in everything the cmap
4547        // actually covers. Coverage becomes cmap-authoritative, and OS/2 is
4548        // reduced to a hint that can only ever lose an argument with the cmap.
4549        unicode_ranges = verify_unicode_ranges_with_cmap(&provider, unicode_ranges);
4550
4551        if let Some(cmap_ranges) = analyze_cmap_coverage(&provider) {
4552            unicode_ranges.extend(cmap_ranges);
4553        }
4554
4555        // The two sources use different block boundaries, so the union overlaps.
4556        // `calculate_unicode_coverage` sums range widths to rank fallbacks —
4557        // leaving overlaps in would double-count and inflate this font's score.
4558        unicode_ranges = FcFontCache::normalize_unicode_ranges(unicode_ranges);
4559
4560        // Use the shared detect_monospace helper for PANOSE + hmtx fallback
4561        let is_monospace = detect_monospace(&provider, os2_table.as_ref(), detected_monospace)
4562            .unwrap_or(false);
4563
4564        let name_data = provider.table_data(tag::NAME).ok()??.into_owned();
4565        let name_table = ReadScope::new(&name_data).read::<NameTable>().ok()?;
4566
4567        // Extract metadata from name table
4568        let mut metadata = FcFontMetadata::default();
4569
4570        const NAME_ID_COPYRIGHT: u16 = 0;
4571        const NAME_ID_FAMILY: u16 = 1;
4572        const NAME_ID_SUBFAMILY: u16 = 2;
4573        const NAME_ID_UNIQUE_ID: u16 = 3;
4574        const NAME_ID_FULL_NAME: u16 = 4;
4575        const NAME_ID_VERSION: u16 = 5;
4576        const NAME_ID_POSTSCRIPT_NAME: u16 = 6;
4577        const NAME_ID_TRADEMARK: u16 = 7;
4578        const NAME_ID_MANUFACTURER: u16 = 8;
4579        const NAME_ID_DESIGNER: u16 = 9;
4580        const NAME_ID_DESCRIPTION: u16 = 10;
4581        const NAME_ID_VENDOR_URL: u16 = 11;
4582        const NAME_ID_DESIGNER_URL: u16 = 12;
4583        const NAME_ID_LICENSE: u16 = 13;
4584        const NAME_ID_LICENSE_URL: u16 = 14;
4585        const NAME_ID_PREFERRED_FAMILY: u16 = 16;
4586        const NAME_ID_PREFERRED_SUBFAMILY: u16 = 17;
4587
4588        metadata.copyright = get_name_string(&name_data, NAME_ID_COPYRIGHT);
4589        metadata.font_family = get_name_string(&name_data, NAME_ID_FAMILY);
4590        metadata.font_subfamily = get_name_string(&name_data, NAME_ID_SUBFAMILY);
4591        metadata.full_name = get_name_string(&name_data, NAME_ID_FULL_NAME);
4592        metadata.unique_id = get_name_string(&name_data, NAME_ID_UNIQUE_ID);
4593        metadata.version = get_name_string(&name_data, NAME_ID_VERSION);
4594        metadata.postscript_name = get_name_string(&name_data, NAME_ID_POSTSCRIPT_NAME);
4595        metadata.trademark = get_name_string(&name_data, NAME_ID_TRADEMARK);
4596        metadata.manufacturer = get_name_string(&name_data, NAME_ID_MANUFACTURER);
4597        metadata.designer = get_name_string(&name_data, NAME_ID_DESIGNER);
4598        metadata.id_description = get_name_string(&name_data, NAME_ID_DESCRIPTION);
4599        metadata.designer_url = get_name_string(&name_data, NAME_ID_DESIGNER_URL);
4600        metadata.manufacturer_url = get_name_string(&name_data, NAME_ID_VENDOR_URL);
4601        metadata.license = get_name_string(&name_data, NAME_ID_LICENSE);
4602        metadata.license_url = get_name_string(&name_data, NAME_ID_LICENSE_URL);
4603        metadata.preferred_family = get_name_string(&name_data, NAME_ID_PREFERRED_FAMILY);
4604        metadata.preferred_subfamily = get_name_string(&name_data, NAME_ID_PREFERRED_SUBFAMILY);
4605
4606        // One font can support multiple patterns
4607        let mut f_family = None;
4608
4609        let patterns = name_table
4610            .name_records
4611            .iter()
4612            .filter_map(|name_record| {
4613                let name_id = name_record.name_id;
4614                if name_id == FONT_SPECIFIER_FAMILY_ID {
4615                    if let Ok(Some(family)) =
4616                        fontcode_get_name(&name_data, FONT_SPECIFIER_FAMILY_ID)
4617                    {
4618                        f_family = Some(family);
4619                    }
4620                    None
4621                } else if name_id == FONT_SPECIFIER_NAME_ID {
4622                    let family = f_family.as_ref()?;
4623                    let name = fontcode_get_name(&name_data, FONT_SPECIFIER_NAME_ID).ok()??;
4624                    if name.to_bytes().is_empty() {
4625                        None
4626                    } else {
4627                        let mut name_str =
4628                            String::from_utf8_lossy(name.to_bytes()).to_string();
4629                        let mut family_str =
4630                            String::from_utf8_lossy(family.as_bytes()).to_string();
4631                        if name_str.starts_with('.') {
4632                            name_str = name_str[1..].to_string();
4633                        }
4634                        if family_str.starts_with('.') {
4635                            family_str = family_str[1..].to_string();
4636                        }
4637                        Some((
4638                            FcPattern {
4639                                name: Some(name_str),
4640                                family: Some(family_str),
4641                                bold: if is_bold {
4642                                    PatternMatch::True
4643                                } else {
4644                                    PatternMatch::False
4645                                },
4646                                italic: if is_italic {
4647                                    PatternMatch::True
4648                                } else {
4649                                    PatternMatch::False
4650                                },
4651                                oblique: if is_oblique {
4652                                    PatternMatch::True
4653                                } else {
4654                                    PatternMatch::False
4655                                },
4656                                monospace: if is_monospace {
4657                                    PatternMatch::True
4658                                } else {
4659                                    PatternMatch::False
4660                                },
4661                                condensed: if stretch <= FcStretch::Condensed {
4662                                    PatternMatch::True
4663                                } else {
4664                                    PatternMatch::False
4665                                },
4666                                weight,
4667                                stretch,
4668                                unicode_ranges: unicode_ranges.clone(),
4669                                metadata: metadata.clone(),
4670                                render_config: FcFontRenderConfig::default(),
4671                            },
4672                            font_index,
4673                        ))
4674                    }
4675                } else {
4676                    None
4677                }
4678            })
4679            .collect::<BTreeSet<_>>();
4680
4681        results.extend(patterns.into_iter().map(|(pat, idx)| ParsedFontFace {
4682            pattern: pat,
4683            font_index: idx,
4684        }));
4685    }
4686
4687    if results.is_empty() {
4688        None
4689    } else {
4690        Some(results)
4691    }
4692}
4693
4694// Remaining implementation for font scanning, parsing, etc.
4695#[cfg(all(feature = "std", feature = "parsing"))]
4696pub(crate) fn FcParseFont(filepath: &PathBuf) -> Option<Vec<(FcPattern, FcFontPath)>> {
4697    #[cfg(all(not(target_family = "wasm"), feature = "std"))]
4698    use mmapio::MmapOptions;
4699    use std::fs::File;
4700
4701    // Try parsing the font file and see if the postscript name matches
4702    let file = File::open(filepath).ok()?;
4703
4704    #[cfg(all(not(target_family = "wasm"), feature = "std"))]
4705    let font_bytes = unsafe { MmapOptions::new().map(&file).ok()? };
4706
4707    #[cfg(not(all(not(target_family = "wasm"), feature = "std")))]
4708    let font_bytes = std::fs::read(filepath).ok()?;
4709
4710    let faces = parse_font_faces(&font_bytes[..])?;
4711    let path_str = filepath.to_string_lossy().to_string();
4712    // Hash once per file — every face of a .ttc shares this value,
4713    // so the shared-bytes cache can return the same Arc<[u8]> for
4714    // all of them. Use the cheap sampled variant so the scout doesn't
4715    // page-fault the full file into RSS just to produce a dedup key.
4716    let bytes_hash = crate::utils::content_dedup_hash_u64(&font_bytes[..]);
4717
4718    Some(
4719        faces
4720            .into_iter()
4721            .map(|face| {
4722                (
4723                    face.pattern,
4724                    FcFontPath {
4725                        path: path_str.clone(),
4726                        font_index: face.font_index,
4727                        bytes_hash,
4728                    },
4729                )
4730            })
4731            .collect(),
4732    )
4733}
4734
4735/// Coverage info returned by a fast-probe parse.
4736///
4737/// Produced by [`FcParseFontFaceFast`] / [`FcProbeCoverage`] — the
4738/// v4.2 "cheap cmap-only" entry point. Unlike `parse_font_faces`,
4739/// this path does **not** read NAME, OS/2, POST, HHEA, HMTX, HEAD's
4740/// style metadata, or anything else. It only reads the table
4741/// directory, `head.macStyle` (2 bytes), and the cmap subtable that
4742/// matches the codepoints we care about. ~1 ms/face on warm FS
4743/// cache vs ~13 ms for the full parse.
4744///
4745/// The `pattern.unicode_ranges` is populated from the *actual* cmap
4746/// contents (one `UnicodeRange` per covered codepoint in the input
4747/// set) rather than the OS/2 `ulUnicodeRange` bitfield. That's more
4748/// precise (OS/2 bits lie on many fonts — they're hints, not ground
4749/// truth) and means `FontFallbackChain::resolve_char`'s coverage
4750/// check matches what the shaper can actually render.
4751#[cfg(all(feature = "std", feature = "parsing"))]
4752#[derive(Debug, Clone)]
4753pub struct FastCoverage {
4754    /// Metadata pattern with `unicode_ranges` populated from the
4755    /// codepoints this face covered from the request set. `name` /
4756    /// `family` fields are left empty — callers already have the
4757    /// filename-guessed family in [`FcFontRegistry.known_paths`];
4758    /// we avoid the NAME table read entirely.
4759    pub pattern: FcPattern,
4760    /// Subset of the input codepoints that this face covers (maps
4761    /// to a non-zero gid via the best cmap subtable). May be empty
4762    /// if the face covers none, in which case callers should fall
4763    /// through to the next candidate path.
4764    pub covered: alloc::collections::BTreeSet<char>,
4765    /// `head.macStyle.bold` (bit 0).
4766    pub is_bold: bool,
4767    /// `head.macStyle.italic` (bit 1).
4768    pub is_italic: bool,
4769}
4770
4771/// Fast per-face coverage probe.
4772///
4773/// Opens the provided font bytes as a `FontData` (detects TTC
4774/// collections), walks the given face, reads `head.macStyle` for
4775/// bold/italic flags, picks the best cmap subtable, and records
4776/// which of the requested codepoints have a non-zero gid.
4777///
4778/// Cost: table-dir parse + head (54 bytes) + cmap (5-100 KiB,
4779/// faulted in from mmap). No heap allocation besides the
4780/// covered-codepoints set and the returned `FcPattern`.
4781///
4782/// Returns `None` only if the font bytes are structurally bad or
4783/// the face index is out of range — empty coverage returns
4784/// `Some` with `covered.is_empty()`, so the caller can distinguish
4785/// "this face doesn't have the char we want" (try next face) from
4786/// "this file is corrupt" (give up on the whole file).
4787#[cfg(all(feature = "std", feature = "parsing"))]
4788#[allow(non_snake_case)]
4789pub fn FcParseFontFaceFast(
4790    font_bytes: &[u8],
4791    font_index: usize,
4792    codepoints: &alloc::collections::BTreeSet<char>,
4793) -> Option<FastCoverage> {
4794    use allsorts::{
4795        binary::read::ReadScope,
4796        font_data::FontData,
4797        tables::{
4798            cmap::{Cmap, CmapSubtable},
4799            FontTableProvider, HeadTable,
4800        },
4801        tag,
4802    };
4803
4804    let scope = ReadScope::new(font_bytes);
4805    let font_file = scope.read::<FontData<'_>>().ok()?;
4806    let provider = font_file.table_provider(font_index).ok()?;
4807
4808    // head — 54 bytes, macStyle at offset 44. Cheap.
4809    let head_data = provider.table_data(tag::HEAD).ok()??;
4810    let head_table = ReadScope::new(&head_data).read::<HeadTable>().ok()?;
4811    let is_bold = head_table.is_bold();
4812    let is_italic = head_table.is_italic();
4813
4814    // cmap — find the best Unicode subtable, probe each codepoint.
4815    // The mmap page-cache only faults in the bytes we touch.
4816    let cmap_data = provider.table_data(tag::CMAP).ok()??;
4817    let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
4818    let encoding_record = find_best_cmap_subtable(&cmap)?;
4819    let cmap_subtable = ReadScope::new(&cmap_data)
4820        .offset(encoding_record.offset as usize)
4821        .read::<CmapSubtable<'_>>()
4822        .ok()?;
4823
4824    let mut covered: alloc::collections::BTreeSet<char> =
4825        alloc::collections::BTreeSet::new();
4826    let mut covered_ranges: Vec<UnicodeRange> = Vec::new();
4827    for ch in codepoints {
4828        let cp = *ch as u32;
4829        if let Ok(Some(gid)) = cmap_subtable.map_glyph(cp) {
4830            if gid != 0 {
4831                covered.insert(*ch);
4832                // Accumulate into ranges for the FcPattern. Merge
4833                // adjacent codepoints so `unicode_ranges` stays
4834                // compact (common case on Western text: one range).
4835                if let Some(last) = covered_ranges.last_mut() {
4836                    if cp == last.end + 1 {
4837                        last.end = cp;
4838                        continue;
4839                    }
4840                }
4841                covered_ranges.push(UnicodeRange { start: cp, end: cp });
4842            }
4843        }
4844    }
4845
4846    let weight = if is_bold {
4847        FcWeight::Bold
4848    } else {
4849        FcWeight::Normal
4850    };
4851    let italic_match = if is_italic {
4852        PatternMatch::True
4853    } else {
4854        PatternMatch::False
4855    };
4856
4857    let pattern = FcPattern {
4858        name: None,
4859        family: None,
4860        weight,
4861        italic: italic_match,
4862        oblique: PatternMatch::DontCare,
4863        monospace: PatternMatch::DontCare,
4864        unicode_ranges: covered_ranges,
4865        ..Default::default()
4866    };
4867
4868    Some(FastCoverage {
4869        pattern,
4870        covered,
4871        is_bold,
4872        is_italic,
4873    })
4874}
4875
4876/// Count the number of faces inside a TTC, or `1` for a single-face
4877/// font file. Used by [`FcFontRegistry::request_fonts_fast`] to
4878/// iterate every face in a `.ttc` without paying the full-parse
4879/// cost (the TTC header is 12 bytes).
4880#[cfg(all(feature = "std", feature = "parsing"))]
4881#[allow(non_snake_case)]
4882pub fn FcCountFontFaces(font_bytes: &[u8]) -> usize {
4883    if font_bytes.len() >= 12 && &font_bytes[0..4] == b"ttcf" {
4884        let num_fonts = u32::from_be_bytes([
4885            font_bytes[8], font_bytes[9], font_bytes[10], font_bytes[11],
4886        ]);
4887        // Same cap as parse_font_faces, for safety.
4888        std::cmp::min(num_fonts as usize, 100).max(1)
4889    } else {
4890        1
4891    }
4892}
4893
4894/// Parse font bytes and extract font patterns for in-memory fonts.
4895///
4896/// This is the public API for parsing in-memory font data to create
4897/// `(FcPattern, FcFont)` tuples that can be added to an `FcFontCache`
4898/// via `with_memory_fonts()`.
4899///
4900/// # Arguments
4901/// * `font_bytes` - The raw bytes of a TrueType/OpenType font file
4902/// * `font_id` - An identifier string for this font (used internally)
4903///
4904/// # Returns
4905/// A vector of `(FcPattern, FcFont)` tuples, one for each font face in the file.
4906/// Returns `None` if the font could not be parsed.
4907///
4908/// # Example
4909/// ```ignore
4910/// use rust_fontconfig::{FcFontCache, FcParseFontBytes};
4911///
4912/// let font_bytes = include_bytes!("path/to/font.ttf");
4913/// let mut cache = FcFontCache::default();
4914///
4915/// if let Some(fonts) = FcParseFontBytes(font_bytes, "MyFont") {
4916///     cache.with_memory_fonts(fonts);
4917/// }
4918/// ```
4919#[cfg(all(feature = "std", feature = "parsing"))]
4920#[allow(non_snake_case)]
4921pub fn FcParseFontBytes(font_bytes: &[u8], font_id: &str) -> Option<Vec<(FcPattern, FcFont)>> {
4922    FcParseFontBytesInner(font_bytes, font_id)
4923}
4924
4925/// Internal implementation for parsing font bytes.
4926/// Delegates to `parse_font_faces` for shared parsing logic and wraps results as `FcFont`.
4927#[cfg(all(feature = "std", feature = "parsing"))]
4928fn FcParseFontBytesInner(font_bytes: &[u8], font_id: &str) -> Option<Vec<(FcPattern, FcFont)>> {
4929    let faces = parse_font_faces(font_bytes)?;
4930    let id = font_id.to_string();
4931    let bytes = font_bytes.to_vec();
4932
4933    Some(
4934        faces
4935            .into_iter()
4936            .map(|face| {
4937                (
4938                    face.pattern,
4939                    FcFont {
4940                        bytes: bytes.clone(),
4941                        font_index: face.font_index,
4942                        id: id.clone(),
4943                    },
4944                )
4945            })
4946            .collect(),
4947    )
4948}
4949
4950#[cfg(all(feature = "std", feature = "parsing"))]
4951fn FcScanDirectoriesInner(paths: &[(Option<String>, String)]) -> Vec<(FcPattern, FcFontPath)> {
4952    #[cfg(all(feature = "multithreading", not(target_family = "wasm")))]
4953    {
4954        use rayon::prelude::*;
4955
4956        // scan directories in parallel
4957        paths
4958            .par_iter()
4959            .filter_map(|(prefix, p)| {
4960                process_path(prefix, PathBuf::from(p), false).map(FcScanSingleDirectoryRecursive)
4961            })
4962            .flatten()
4963            .collect()
4964    }
4965    // wasm has no rayon (it's target-gated off), so even with `multithreading`
4966    // enabled wasm falls back to the sequential path.
4967    #[cfg(not(all(feature = "multithreading", not(target_family = "wasm"))))]
4968    {
4969        paths
4970            .iter()
4971            .filter_map(|(prefix, p)| {
4972                process_path(prefix, PathBuf::from(p), false).map(FcScanSingleDirectoryRecursive)
4973            })
4974            .flatten()
4975            .collect()
4976    }
4977}
4978
4979/// Recursively collect all files from a directory (no parsing, no allsorts).
4980#[cfg(feature = "std")]
4981fn FcCollectFontFilesRecursive(dir: PathBuf) -> Vec<PathBuf> {
4982    let mut files = Vec::new();
4983    let mut dirs_to_parse = vec![dir];
4984
4985    loop {
4986        let mut new_dirs = Vec::new();
4987        for dir in &dirs_to_parse {
4988            let entries = match std::fs::read_dir(dir) {
4989                Ok(o) => o,
4990                Err(_) => continue,
4991            };
4992            for entry in entries.flatten() {
4993                let path = entry.path();
4994                if path.is_dir() {
4995                    new_dirs.push(path);
4996                } else {
4997                    files.push(path);
4998                }
4999            }
5000        }
5001        if new_dirs.is_empty() {
5002            break;
5003        }
5004        dirs_to_parse = new_dirs;
5005    }
5006
5007    files
5008}
5009
5010#[cfg(all(feature = "std", feature = "parsing"))]
5011fn FcScanSingleDirectoryRecursive(dir: PathBuf) -> Vec<(FcPattern, FcFontPath)> {
5012    let files = FcCollectFontFilesRecursive(dir);
5013    FcParseFontFiles(&files)
5014}
5015
5016#[cfg(all(feature = "std", feature = "parsing"))]
5017fn FcParseFontFiles(files_to_parse: &[PathBuf]) -> Vec<(FcPattern, FcFontPath)> {
5018    let result = {
5019        #[cfg(all(feature = "multithreading", not(target_family = "wasm")))]
5020        {
5021            use rayon::prelude::*;
5022
5023            files_to_parse
5024                .par_iter()
5025                .filter_map(|file| FcParseFont(file))
5026                .collect::<Vec<Vec<_>>>()
5027        }
5028        #[cfg(not(all(feature = "multithreading", not(target_family = "wasm"))))]
5029        {
5030            files_to_parse
5031                .iter()
5032                .filter_map(|file| FcParseFont(file))
5033                .collect::<Vec<Vec<_>>>()
5034        }
5035    };
5036
5037    result.into_iter().flat_map(|f| f.into_iter()).collect()
5038}
5039
5040#[cfg(all(feature = "std", feature = "parsing"))]
5041/// Takes a path & prefix and resolves them to a usable path, or `None` if they're unsupported/unavailable.
5042///
5043/// Behaviour is based on: https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
5044fn process_path(
5045    prefix: &Option<String>,
5046    mut path: PathBuf,
5047    is_include_path: bool,
5048) -> Option<PathBuf> {
5049    use std::env::var;
5050
5051    const HOME_SHORTCUT: &str = "~";
5052    const CWD_PATH: &str = ".";
5053
5054    const HOME_ENV_VAR: &str = "HOME";
5055    const XDG_CONFIG_HOME_ENV_VAR: &str = "XDG_CONFIG_HOME";
5056    const XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX: &str = ".config";
5057    const XDG_DATA_HOME_ENV_VAR: &str = "XDG_DATA_HOME";
5058    const XDG_DATA_HOME_DEFAULT_PATH_SUFFIX: &str = ".local/share";
5059
5060    const PREFIX_CWD: &str = "cwd";
5061    const PREFIX_DEFAULT: &str = "default";
5062    const PREFIX_XDG: &str = "xdg";
5063
5064    // These three could, in theory, be cached, but the work required to do so outweighs the minor benefits
5065    fn get_home_value() -> Option<PathBuf> {
5066        var(HOME_ENV_VAR).ok().map(PathBuf::from)
5067    }
5068    fn get_xdg_config_home_value() -> Option<PathBuf> {
5069        var(XDG_CONFIG_HOME_ENV_VAR)
5070            .ok()
5071            .map(PathBuf::from)
5072            .or_else(|| {
5073                get_home_value()
5074                    .map(|home_path| home_path.join(XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX))
5075            })
5076    }
5077    fn get_xdg_data_home_value() -> Option<PathBuf> {
5078        var(XDG_DATA_HOME_ENV_VAR)
5079            .ok()
5080            .map(PathBuf::from)
5081            .or_else(|| {
5082                get_home_value().map(|home_path| home_path.join(XDG_DATA_HOME_DEFAULT_PATH_SUFFIX))
5083            })
5084    }
5085
5086    // Resolve the tilde character in the path, if present
5087    if path.starts_with(HOME_SHORTCUT) {
5088        if let Some(home_path) = get_home_value() {
5089            path = home_path.join(
5090                path.strip_prefix(HOME_SHORTCUT)
5091                    .expect("already checked that it starts with the prefix"),
5092            );
5093        } else {
5094            return None;
5095        }
5096    }
5097
5098    // Resolve prefix values
5099    match prefix {
5100        Some(prefix) => match prefix.as_str() {
5101            PREFIX_CWD | PREFIX_DEFAULT => {
5102                let mut new_path = PathBuf::from(CWD_PATH);
5103                new_path.push(path);
5104
5105                Some(new_path)
5106            }
5107            PREFIX_XDG => {
5108                if is_include_path {
5109                    get_xdg_config_home_value()
5110                        .map(|xdg_config_home_path| xdg_config_home_path.join(path))
5111                } else {
5112                    get_xdg_data_home_value()
5113                        .map(|xdg_data_home_path| xdg_data_home_path.join(path))
5114                }
5115            }
5116            _ => None, // Unsupported prefix
5117        },
5118        None => Some(path),
5119    }
5120}
5121
5122// Helper function to extract a string from the name table
5123#[cfg(all(feature = "std", feature = "parsing"))]
5124fn get_name_string(name_data: &[u8], name_id: u16) -> Option<String> {
5125    fontcode_get_name(name_data, name_id)
5126        .ok()
5127        .flatten()
5128        .map(|name| String::from_utf8_lossy(name.to_bytes()).to_string())
5129}
5130
5131/// Representative test codepoints for each Unicode block.
5132/// These are carefully chosen to be actual script characters (not punctuation/symbols)
5133/// that a font claiming to support this script should definitely have.
5134#[cfg(all(feature = "std", feature = "parsing"))]
5135fn get_verification_codepoints(start: u32, end: u32) -> Vec<u32> {
5136    match start {
5137        // Basic Latin - test uppercase, lowercase, and digits
5138        0x0000 => vec!['A' as u32, 'M' as u32, 'Z' as u32, 'a' as u32, 'm' as u32, 'z' as u32],
5139        // Latin-1 Supplement - common accented letters
5140        0x0080 => vec![0x00C0, 0x00C9, 0x00D1, 0x00E0, 0x00E9, 0x00F1], // À É Ñ à é ñ
5141        // Latin Extended-A
5142        0x0100 => vec![0x0100, 0x0110, 0x0141, 0x0152, 0x0160], // Ā Đ Ł Œ Š
5143        // Latin Extended-B
5144        0x0180 => vec![0x0180, 0x01A0, 0x01B0, 0x01CD], // ƀ Ơ ư Ǎ
5145        // IPA Extensions
5146        0x0250 => vec![0x0250, 0x0259, 0x026A, 0x0279], // ɐ ə ɪ ɹ
5147        // Greek and Coptic
5148        0x0370 => vec![0x0391, 0x0392, 0x0393, 0x03B1, 0x03B2, 0x03C9], // Α Β Γ α β ω
5149        // Cyrillic
5150        0x0400 => vec![0x0410, 0x0411, 0x0412, 0x0430, 0x0431, 0x042F], // А Б В а б Я
5151        // Armenian
5152        0x0530 => vec![0x0531, 0x0532, 0x0533, 0x0561, 0x0562], // Ա Բ Գ ա բ
5153        // Hebrew
5154        0x0590 => vec![0x05D0, 0x05D1, 0x05D2, 0x05E9, 0x05EA], // א ב ג ש ת
5155        // Arabic
5156        0x0600 => vec![0x0627, 0x0628, 0x062A, 0x062C, 0x0645], // ا ب ت ج م
5157        // Syriac
5158        0x0700 => vec![0x0710, 0x0712, 0x0713, 0x0715], // ܐ ܒ ܓ ܕ
5159        // Devanagari
5160        0x0900 => vec![0x0905, 0x0906, 0x0915, 0x0916, 0x0939], // अ आ क ख ह
5161        // Bengali
5162        0x0980 => vec![0x0985, 0x0986, 0x0995, 0x0996], // অ আ ক খ
5163        // Gurmukhi
5164        0x0A00 => vec![0x0A05, 0x0A06, 0x0A15, 0x0A16], // ਅ ਆ ਕ ਖ
5165        // Gujarati
5166        0x0A80 => vec![0x0A85, 0x0A86, 0x0A95, 0x0A96], // અ આ ક ખ
5167        // Oriya
5168        0x0B00 => vec![0x0B05, 0x0B06, 0x0B15, 0x0B16], // ଅ ଆ କ ଖ
5169        // Tamil
5170        0x0B80 => vec![0x0B85, 0x0B86, 0x0B95, 0x0BA4], // அ ஆ க த
5171        // Telugu
5172        0x0C00 => vec![0x0C05, 0x0C06, 0x0C15, 0x0C16], // అ ఆ క ఖ
5173        // Kannada
5174        0x0C80 => vec![0x0C85, 0x0C86, 0x0C95, 0x0C96], // ಅ ಆ ಕ ಖ
5175        // Malayalam
5176        0x0D00 => vec![0x0D05, 0x0D06, 0x0D15, 0x0D16], // അ ആ ക ഖ
5177        // Thai
5178        0x0E00 => vec![0x0E01, 0x0E02, 0x0E04, 0x0E07, 0x0E40], // ก ข ค ง เ
5179        // Lao
5180        0x0E80 => vec![0x0E81, 0x0E82, 0x0E84, 0x0E87], // ກ ຂ ຄ ງ
5181        // Myanmar
5182        0x1000 => vec![0x1000, 0x1001, 0x1002, 0x1010, 0x1019], // က ခ ဂ တ မ
5183        // Georgian
5184        0x10A0 => vec![0x10D0, 0x10D1, 0x10D2, 0x10D3], // ა ბ გ დ
5185        // Hangul Jamo
5186        0x1100 => vec![0x1100, 0x1102, 0x1103, 0x1161, 0x1162], // ᄀ ᄂ ᄃ ᅡ ᅢ
5187        // Ethiopic
5188        0x1200 => vec![0x1200, 0x1208, 0x1210, 0x1218], // ሀ ለ ሐ መ
5189        // Cherokee
5190        0x13A0 => vec![0x13A0, 0x13A1, 0x13A2, 0x13A3], // Ꭰ Ꭱ Ꭲ Ꭳ
5191        // Khmer
5192        0x1780 => vec![0x1780, 0x1781, 0x1782, 0x1783], // ក ខ គ ឃ
5193        // Mongolian
5194        0x1800 => vec![0x1820, 0x1821, 0x1822, 0x1823], // ᠠ ᠡ ᠢ ᠣ
5195        // Hiragana
5196        0x3040 => vec![0x3042, 0x3044, 0x3046, 0x304B, 0x304D, 0x3093], // あ い う か き ん
5197        // Katakana
5198        0x30A0 => vec![0x30A2, 0x30A4, 0x30A6, 0x30AB, 0x30AD, 0x30F3], // ア イ ウ カ キ ン
5199        // Bopomofo
5200        0x3100 => vec![0x3105, 0x3106, 0x3107, 0x3108], // ㄅ ㄆ ㄇ ㄈ
5201        // CJK Unified Ideographs - common characters
5202        0x4E00 => vec![0x4E00, 0x4E2D, 0x4EBA, 0x5927, 0x65E5, 0x6708], // 一 中 人 大 日 月
5203        // Hangul Syllables
5204        0xAC00 => vec![0xAC00, 0xAC01, 0xAC04, 0xB098, 0xB2E4], // 가 각 간 나 다
5205        // CJK Compatibility Ideographs
5206        0xF900 => vec![0xF900, 0xF901, 0xF902], // 豈 更 車
5207        // Arabic Presentation Forms-A
5208        0xFB50 => vec![0xFB50, 0xFB51, 0xFB52, 0xFB56], // ﭐ ﭑ ﭒ ﭖ
5209        // Arabic Presentation Forms-B
5210        0xFE70 => vec![0xFE70, 0xFE72, 0xFE74, 0xFE76], // ﹰ ﹲ ﹴ ﹶ
5211        // Halfwidth and Fullwidth Forms
5212        0xFF00 => vec![0xFF01, 0xFF21, 0xFF41, 0xFF61], // ! A a 。
5213        // Default: sample at regular intervals
5214        _ => {
5215            let range_size = end - start;
5216            if range_size > 20 {
5217                vec![
5218                    start + range_size / 5,
5219                    start + 2 * range_size / 5,
5220                    start + 3 * range_size / 5,
5221                    start + 4 * range_size / 5,
5222                ]
5223            } else {
5224                vec![start, start + range_size / 2]
5225            }
5226        }
5227    }
5228}
5229
5230/// Find the best Unicode CMAP subtable from a font provider.
5231/// Tries multiple platform/encoding combinations in priority order.
5232#[cfg(all(feature = "std", feature = "parsing"))]
5233fn find_best_cmap_subtable<'a>(
5234    cmap: &allsorts::tables::cmap::Cmap<'a>,
5235) -> Option<allsorts::tables::cmap::EncodingRecord> {
5236    use allsorts::tables::cmap::{PlatformId, EncodingId};
5237
5238    cmap.find_subtable(PlatformId::UNICODE, EncodingId(3))
5239        .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(4)))
5240        .or_else(|| cmap.find_subtable(PlatformId::WINDOWS, EncodingId(1)))
5241        .or_else(|| cmap.find_subtable(PlatformId::WINDOWS, EncodingId(10)))
5242        .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(0)))
5243        .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(1)))
5244}
5245
5246/// Verify OS/2 reported Unicode ranges against actual CMAP support.
5247/// Returns only ranges that are actually supported by the font's CMAP table.
5248#[cfg(all(feature = "std", feature = "parsing"))]
5249fn verify_unicode_ranges_with_cmap(
5250    provider: &impl FontTableProvider,
5251    os2_ranges: Vec<UnicodeRange>
5252) -> Vec<UnicodeRange> {
5253    use allsorts::tables::cmap::{Cmap, CmapSubtable};
5254
5255    if os2_ranges.is_empty() {
5256        return Vec::new();
5257    }
5258
5259    // Try to get CMAP subtable
5260    let cmap_data = match provider.table_data(tag::CMAP) {
5261        Ok(Some(data)) => data,
5262        _ => return os2_ranges, // Can't verify, trust OS/2
5263    };
5264
5265    let cmap = match ReadScope::new(&cmap_data).read::<Cmap<'_>>() {
5266        Ok(c) => c,
5267        Err(_) => return os2_ranges,
5268    };
5269
5270    let encoding_record = match find_best_cmap_subtable(&cmap) {
5271        Some(r) => r,
5272        None => return os2_ranges, // No suitable subtable, trust OS/2
5273    };
5274
5275    let cmap_subtable = match ReadScope::new(&cmap_data)
5276        .offset(encoding_record.offset as usize)
5277        .read::<CmapSubtable<'_>>()
5278    {
5279        Ok(st) => st,
5280        Err(_) => return os2_ranges,
5281    };
5282
5283    // Verify each range
5284    let mut verified_ranges = Vec::new();
5285
5286    for range in os2_ranges {
5287        let test_codepoints = get_verification_codepoints(range.start, range.end);
5288
5289        // Require at least 50% of test codepoints to have valid glyphs
5290        // This is stricter than before to avoid false positives
5291        let required_hits = (test_codepoints.len() + 1) / 2; // ceil(len/2)
5292        let mut hits = 0;
5293
5294        for cp in test_codepoints {
5295            if cp >= range.start && cp <= range.end {
5296                if let Ok(Some(gid)) = cmap_subtable.map_glyph(cp) {
5297                    if gid != 0 {
5298                        hits += 1;
5299                        if hits >= required_hits {
5300                            break;
5301                        }
5302                    }
5303                }
5304            }
5305        }
5306
5307        if hits >= required_hits {
5308            verified_ranges.push(range);
5309        }
5310    }
5311
5312    verified_ranges
5313}
5314
5315/// Analyze CMAP table to discover font coverage when OS/2 provides no info.
5316/// This is the fallback when OS/2 ulUnicodeRange bits are all zero.
5317#[cfg(all(feature = "std", feature = "parsing"))]
5318fn analyze_cmap_coverage(provider: &impl FontTableProvider) -> Option<Vec<UnicodeRange>> {
5319    use allsorts::tables::cmap::{Cmap, CmapSubtable};
5320
5321    let cmap_data = provider.table_data(tag::CMAP).ok()??;
5322    let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
5323
5324    let encoding_record = find_best_cmap_subtable(&cmap)?;
5325
5326    let cmap_subtable = ReadScope::new(&cmap_data)
5327        .offset(encoding_record.offset as usize)
5328        .read::<CmapSubtable<'_>>()
5329        .ok()?;
5330
5331    // Standard Unicode blocks to probe
5332    let blocks_to_check: &[(u32, u32)] = &[
5333        (0x0000, 0x007F), // Basic Latin
5334        (0x0080, 0x00FF), // Latin-1 Supplement
5335        (0x0100, 0x017F), // Latin Extended-A
5336        (0x0180, 0x024F), // Latin Extended-B
5337        (0x0250, 0x02AF), // IPA Extensions
5338        (0x0300, 0x036F), // Combining Diacritical Marks
5339        (0x0370, 0x03FF), // Greek and Coptic
5340        (0x0400, 0x04FF), // Cyrillic
5341        (0x0500, 0x052F), // Cyrillic Supplement
5342        (0x0530, 0x058F), // Armenian
5343        (0x0590, 0x05FF), // Hebrew
5344        (0x0600, 0x06FF), // Arabic
5345        (0x0700, 0x074F), // Syriac
5346        (0x0900, 0x097F), // Devanagari
5347        (0x0980, 0x09FF), // Bengali
5348        (0x0A00, 0x0A7F), // Gurmukhi
5349        (0x0A80, 0x0AFF), // Gujarati
5350        (0x0B00, 0x0B7F), // Oriya
5351        (0x0B80, 0x0BFF), // Tamil
5352        (0x0C00, 0x0C7F), // Telugu
5353        (0x0C80, 0x0CFF), // Kannada
5354        (0x0D00, 0x0D7F), // Malayalam
5355        (0x0E00, 0x0E7F), // Thai
5356        (0x0E80, 0x0EFF), // Lao
5357        (0x1000, 0x109F), // Myanmar
5358        (0x10A0, 0x10FF), // Georgian
5359        (0x1100, 0x11FF), // Hangul Jamo
5360        (0x1200, 0x137F), // Ethiopic
5361        (0x13A0, 0x13FF), // Cherokee
5362        (0x1780, 0x17FF), // Khmer
5363        (0x1800, 0x18AF), // Mongolian
5364        (0x2000, 0x206F), // General Punctuation
5365        (0x20A0, 0x20CF), // Currency Symbols
5366        (0x2100, 0x214F), // Letterlike Symbols
5367        (0x2190, 0x21FF), // Arrows
5368        (0x2200, 0x22FF), // Mathematical Operators
5369        (0x2500, 0x257F), // Box Drawing
5370        (0x25A0, 0x25FF), // Geometric Shapes
5371        (0x2600, 0x26FF), // Miscellaneous Symbols
5372        (0x3000, 0x303F), // CJK Symbols and Punctuation
5373        (0x3040, 0x309F), // Hiragana
5374        (0x30A0, 0x30FF), // Katakana
5375        (0x3100, 0x312F), // Bopomofo
5376        (0x3130, 0x318F), // Hangul Compatibility Jamo
5377        (0x4E00, 0x9FFF), // CJK Unified Ideographs
5378        (0xAC00, 0xD7AF), // Hangul Syllables
5379        (0xF900, 0xFAFF), // CJK Compatibility Ideographs
5380        (0xFB50, 0xFDFF), // Arabic Presentation Forms-A
5381        (0xFE70, 0xFEFF), // Arabic Presentation Forms-B
5382        (0xFF00, 0xFFEF), // Halfwidth and Fullwidth Forms
5383    ];
5384
5385    let mut ranges = Vec::new();
5386
5387    for &(start, end) in blocks_to_check {
5388        let test_codepoints = get_verification_codepoints(start, end);
5389        let required_hits = (test_codepoints.len() + 1) / 2;
5390        // Blocks the font does NOT have are the common case: a Latin face covers a
5391        // handful of the ~50 probed here. Stop as soon as the remaining probes
5392        // cannot reach `required_hits` rather than testing every codepoint to
5393        // confirm a foregone conclusion. Same verdict, fewer cmap lookups.
5394        let allowed_misses = test_codepoints.len() - required_hits;
5395        let mut hits = 0;
5396        let mut misses = 0;
5397
5398        for cp in test_codepoints {
5399            if matches!(cmap_subtable.map_glyph(cp), Ok(Some(gid)) if gid != 0) {
5400                hits += 1;
5401                if hits >= required_hits {
5402                    break;
5403                }
5404            } else {
5405                misses += 1;
5406                if misses > allowed_misses {
5407                    break;
5408                }
5409            }
5410        }
5411
5412        if hits >= required_hits {
5413            ranges.push(UnicodeRange { start, end });
5414        }
5415    }
5416
5417    if ranges.is_empty() {
5418        None
5419    } else {
5420        Some(ranges)
5421    }
5422}
5423
5424// Helper function to extract unicode ranges (unused, kept for reference)
5425#[cfg(all(feature = "std", feature = "parsing"))]
5426#[allow(dead_code)]
5427fn extract_unicode_ranges(os2_table: &Os2) -> Vec<UnicodeRange> {
5428    let mut unicode_ranges = Vec::new();
5429
5430    let ranges = [
5431        os2_table.ul_unicode_range1,
5432        os2_table.ul_unicode_range2,
5433        os2_table.ul_unicode_range3,
5434        os2_table.ul_unicode_range4,
5435    ];
5436
5437    for &(bit, start, end) in UNICODE_RANGE_MAPPINGS {
5438        let range_idx = bit / 32;
5439        let bit_pos = bit % 32;
5440        if range_idx < 4 && (ranges[range_idx] & (1 << bit_pos)) != 0 {
5441            unicode_ranges.push(UnicodeRange { start, end });
5442        }
5443    }
5444
5445    unicode_ranges
5446}
5447
5448// Helper function to detect if a font is monospace
5449#[cfg(all(feature = "std", feature = "parsing"))]
5450fn detect_monospace(
5451    provider: &impl FontTableProvider,
5452    os2_table: Option<&Os2>,
5453    detected_monospace: Option<bool>,
5454) -> Option<bool> {
5455    if let Some(is_monospace) = detected_monospace {
5456        return Some(is_monospace);
5457    }
5458
5459    // Try using PANOSE classification, when there is an OS/2 table to read it
5460    // from; otherwise fall straight through to the hmtx width check.
5461    if let Some(os2_table) = os2_table {
5462        if os2_table.panose[0] == 2 {
5463            // 2 = Latin Text
5464            return Some(os2_table.panose[3] == 9); // 9 = Monospaced
5465        }
5466    }
5467
5468    // Check glyph widths in hmtx table
5469    let hhea_data = provider.table_data(tag::HHEA).ok()??;
5470    let hhea_table = ReadScope::new(&hhea_data).read::<HheaTable>().ok()?;
5471    let maxp_data = provider.table_data(tag::MAXP).ok()??;
5472    let maxp_table = ReadScope::new(&maxp_data).read::<MaxpTable>().ok()?;
5473    let hmtx_data = provider.table_data(tag::HMTX).ok()??;
5474    let hmtx_table = ReadScope::new(&hmtx_data)
5475        .read_dep::<HmtxTable<'_>>((
5476            usize::from(maxp_table.num_glyphs),
5477            usize::from(hhea_table.num_h_metrics),
5478        ))
5479        .ok()?;
5480
5481    let mut monospace = true;
5482    let mut last_advance = 0;
5483
5484    // Check if all advance widths are the same
5485    for i in 0..hhea_table.num_h_metrics as usize {
5486        let advance = hmtx_table.h_metrics.read_item(i).ok()?.advance_width;
5487        if i > 0 && advance != last_advance {
5488            monospace = false;
5489            break;
5490        }
5491        last_advance = advance;
5492    }
5493
5494    Some(monospace)
5495}
5496
5497/// Guess font metadata from a filename using the existing tokenizer.
5498///
5499/// Uses [`config::tokenize_font_stem`] and [`config::FONT_STYLE_TOKENS`]
5500/// to extract the family name and detect style hints from the filename.
5501///
5502/// Only compiled for the filename-only (`not(parsing)`) scan path — its
5503/// sole caller is [`FcFontCache::build_from_filenames`]. With `parsing`
5504/// on, allsorts reads real metadata and this fallback is unused.
5505#[cfg(all(feature = "std", not(feature = "parsing")))]
5506fn pattern_from_filename(path: &std::path::Path) -> Option<FcPattern> {
5507    let ext = path.extension()?.to_str()?.to_ascii_lowercase();
5508    match ext.as_str() {
5509        "ttf" | "otf" | "ttc" | "woff" | "woff2" => {}
5510        _ => return None,
5511    }
5512
5513    let stem = path.file_stem()?.to_str()?;
5514    let all_tokens = crate::config::tokenize_lowercase(stem);
5515
5516    // Style detection: check if any token matches a known style keyword
5517    let has_token = |kw: &str| all_tokens.iter().any(|t| t == kw);
5518    let is_bold = has_token("bold") || has_token("heavy");
5519    let is_italic = has_token("italic");
5520    let is_oblique = has_token("oblique");
5521    let is_mono = has_token("mono") || has_token("monospace");
5522    let is_condensed = has_token("condensed");
5523
5524    // Family = non-style tokens joined
5525    let family_tokens = crate::config::tokenize_font_stem(stem);
5526    if family_tokens.is_empty() { return None; }
5527    let family = family_tokens.join(" ");
5528
5529    Some(FcPattern {
5530        name: Some(stem.to_string()),
5531        family: Some(family),
5532        bold: if is_bold { PatternMatch::True } else { PatternMatch::False },
5533        italic: if is_italic { PatternMatch::True } else { PatternMatch::False },
5534        oblique: if is_oblique { PatternMatch::True } else { PatternMatch::DontCare },
5535        monospace: if is_mono { PatternMatch::True } else { PatternMatch::DontCare },
5536        condensed: if is_condensed { PatternMatch::True } else { PatternMatch::DontCare },
5537        weight: if is_bold { FcWeight::Bold } else { FcWeight::Normal },
5538        stretch: if is_condensed { FcStretch::Condensed } else { FcStretch::Normal },
5539        unicode_ranges: Vec::new(),
5540        metadata: FcFontMetadata::default(),
5541        render_config: FcFontRenderConfig::default(),
5542    })
5543}
5544
5545#[cfg(all(test, feature = "std", feature = "parsing", target_os = "linux"))]
5546mod system_alias_tests {
5547    use super::*;
5548
5549    const SAMPLE: &str = r#"<?xml version="1.0"?>
5550<fontconfig>
5551  <alias>
5552    <family>sans-serif</family>
5553    <prefer>
5554      <family>Noto Sans</family>
5555      <family>DejaVu Sans</family>
5556    </prefer>
5557  </alias>
5558  <alias>
5559    <family>Arial</family>
5560    <prefer><family>Liberation Sans</family></prefer>
5561  </alias>
5562  <alias binding="same">
5563    <family>monospace</family>
5564    <prefer><family>Noto Sans Mono</family></prefer>
5565  </alias>
5566</fontconfig>"#;
5567
5568    const SECOND_FILE: &str = r#"<fontconfig>
5569  <alias>
5570    <family>sans-serif</family>
5571    <prefer>
5572      <family>Ubuntu</family>
5573      <family>Noto Sans</family>
5574    </prefer>
5575  </alias>
5576</fontconfig>"#;
5577
5578    #[test]
5579    fn alias_blocks_parse_with_order_and_dedup_across_files() {
5580        let mut aliases = BTreeMap::new();
5581        ParseFontsConfAliases(SAMPLE, &mut aliases);
5582        ParseFontsConfAliases(SECOND_FILE, &mut aliases);
5583        let key = crate::utils::normalize_family_name("sans-serif");
5584        assert_eq!(
5585            aliases.get(&key).map(Vec::as_slice),
5586            Some(&["Noto Sans".to_string(), "DejaVu Sans".to_string(), "Ubuntu".to_string()][..]),
5587            "prefer entries append across files in include order, deduplicated"
5588        );
5589        assert_eq!(
5590            aliases.get("arial").map(Vec::as_slice),
5591            Some(&["Liberation Sans".to_string()][..]),
5592            "named-family aliases parse too (key normalized)"
5593        );
5594        assert_eq!(
5595            aliases.get("monospace").map(Vec::as_slice),
5596            Some(&["Noto Sans Mono".to_string()][..]),
5597            "alias attributes (binding=...) do not confuse the parser"
5598        );
5599    }
5600
5601    #[test]
5602    fn config_first_expansion_beats_the_builtin_lists() {
5603        let cache = FcFontCache::default();
5604        {
5605            let mut state = cache.state_write();
5606            let mut aliases = BTreeMap::new();
5607            ParseFontsConfAliases(SAMPLE, &mut aliases);
5608            state.system_aliases = aliases;
5609        }
5610        let out = cache.expand_font_families_config_first(
5611            &["Arial".to_string(), "sans-serif".to_string()],
5612            OperatingSystem::Linux,
5613            &[],
5614        );
5615        assert_eq!(
5616            out,
5617            vec![
5618                "Arial".to_string(),            // named family keeps itself first
5619                "Liberation Sans".to_string(),  // its configured substitution
5620                "Noto Sans".to_string(),        // sans-serif configured prefer list
5621                "DejaVu Sans".to_string(),
5622            ],
5623            "configured preferences resolve the stack; no built-in list entries leak in"
5624        );
5625    }
5626
5627    #[test]
5628    fn generic_family_without_config_falls_back_to_builtin_lists() {
5629        let cache = FcFontCache::default();
5630        let out = cache.expand_font_families_config_first(
5631            &["sans-serif".to_string()],
5632            OperatingSystem::Linux,
5633            &[],
5634        );
5635        assert!(
5636            !out.is_empty() && out.iter().any(|f| f == "DejaVu Sans"),
5637            "no configuration parsed -> the built-in candidates are the last resort: {out:?}"
5638        );
5639    }
5640}