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