Skip to main content

ferroui_base/media/fonts/
font_collection_base.rs

1use std::any::Any;
2use std::cell::RefCell;
3use std::cmp::Ordering;
4use std::collections::{BTreeMap, HashMap};
5use std::io::Read;
6use std::rc::Rc;
7
8use crate::media::fonts::font_family_loader::FontFamilyLoader;
9use crate::media::fonts::{FontCollectionKey, FontFallbackScriptHints, IFontCollection};
10use crate::media::text_formatting::unicode::{Codepoint, Script};
11use crate::media::{FontFamily, FontSimulations, FontStretch, FontStyle, FontWeight, GlyphTypeface, Typeface};
12use crate::platform::{IAssetLoader, IFontManagerImpl, ASSET_SCHEME};
13use crate::utilities::{CultureInfo, Uri};
14use crate::{FerroLocator, LocatorExtensions};
15
16/// The typefaces of one font family: glyph typefaces by style, weight and
17/// stretch. A `None` value records that the platform has no typeface for the
18/// key, so it is not asked again.
19pub type GlyphTypefaceMap = BTreeMap<FontCollectionKey, Option<Rc<GlyphTypeface>>>;
20
21/// Maps a character to the one used for ordinal case-insensitive comparison
22/// (the simple upper case mapping).
23fn to_upper_ordinal(c: char) -> char {
24    if c.is_ascii() {
25        return c.to_ascii_uppercase();
26    }
27
28    let mut upper = c.to_uppercase();
29
30    match (upper.next(), upper.next()) {
31        (Some(single), None) => single,
32        _ => c,
33    }
34}
35
36/// Orders characters as their UTF-16 encodings order.
37fn utf16_order_key(c: char) -> (u32, u32) {
38    let value = c as u32;
39
40    if value >= 0x10000 {
41        (0xD800 + ((value - 0x10000) >> 10), value)
42    } else {
43        (value, 0)
44    }
45}
46
47/// Compares two strings ignoring case (C# `StringComparison.OrdinalIgnoreCase`).
48pub(crate) fn compare_ordinal_ignore_case(a: &str, b: &str) -> Ordering {
49    let mut a = a.chars();
50    let mut b = b.chars();
51
52    loop {
53        match (a.next(), b.next()) {
54            (None, None) => return Ordering::Equal,
55            (None, Some(_)) => return Ordering::Less,
56            (Some(_), None) => return Ordering::Greater,
57            (Some(x), Some(y)) => {
58                if x == y {
59                    continue;
60                }
61
62                let (x, y) = (to_upper_ordinal(x), to_upper_ordinal(y));
63
64                if x != y {
65                    return utf16_order_key(x).cmp(&utf16_order_key(y));
66                }
67            }
68        }
69    }
70}
71
72/// Whether two strings are equal ignoring case (C# `StringComparison.OrdinalIgnoreCase`).
73pub(crate) fn equals_ordinal_ignore_case(a: &str, b: &str) -> bool {
74    compare_ordinal_ignore_case(a, b) == Ordering::Equal
75}
76
77/// Whether `value` starts with `prefix` ignoring case (C# `StringComparison.OrdinalIgnoreCase`).
78pub(crate) fn starts_with_ordinal_ignore_case(value: &str, prefix: &str) -> bool {
79    let mut value = value.chars();
80
81    for expected in prefix.chars() {
82        match value.next() {
83            Some(actual) if actual == expected || to_upper_ordinal(actual) == to_upper_ordinal(expected) => {}
84            _ => return false,
85        }
86    }
87
88    true
89}
90
91/// The glyph typefaces of a font collection by family name. Family names are
92/// compared ignoring case.
93///
94/// The entries are kept sorted by name, so lookups neither allocate nor hash.
95#[derive(Default)]
96pub struct GlyphTypefaceCache {
97    entries: RefCell<Vec<(String, Rc<RefCell<GlyphTypefaceMap>>)>>,
98}
99
100impl GlyphTypefaceCache {
101    fn search(entries: &[(String, Rc<RefCell<GlyphTypefaceMap>>)], family_name: &str) -> Result<usize, usize> {
102        entries.binary_search_by(|(name, _)| compare_ordinal_ignore_case(name, family_name))
103    }
104
105    /// The typefaces cached for the family.
106    pub fn try_get_value(&self, family_name: &str) -> Option<Rc<RefCell<GlyphTypefaceMap>>> {
107        let entries = self.entries.borrow();
108
109        Self::search(&entries, family_name).ok().map(|index| entries[index].1.clone())
110    }
111
112    /// The typefaces cached for the family, adding an empty entry when the
113    /// family is not cached yet. The flag tells whether the entry was added.
114    fn get_or_add(&self, family_name: &str) -> (Rc<RefCell<GlyphTypefaceMap>>, bool) {
115        let mut entries = self.entries.borrow_mut();
116
117        match Self::search(&entries, family_name) {
118            Ok(index) => (entries[index].1.clone(), false),
119            Err(index) => {
120                let map = Rc::new(RefCell::new(GlyphTypefaceMap::new()));
121                entries.insert(index, (family_name.to_owned(), map.clone()));
122                (map, true)
123            }
124        }
125    }
126
127    /// Whether typefaces are cached for the family.
128    pub fn contains_key(&self, family_name: &str) -> bool {
129        Self::search(&self.entries.borrow(), family_name).is_ok()
130    }
131
132    /// The number of cached families.
133    pub fn count(&self) -> usize {
134        self.entries.borrow().len()
135    }
136
137    /// The names of the cached families, sorted ignoring case.
138    pub fn keys(&self) -> Vec<String> {
139        self.entries.borrow().iter().map(|(name, _)| name.clone()).collect()
140    }
141
142    fn values(&self) -> Vec<Rc<RefCell<GlyphTypefaceMap>>> {
143        self.entries.borrow().iter().map(|(_, map)| map.clone()).collect()
144    }
145}
146
147/// Identifies a script/culture bucket of the fallback cache.
148type ScriptFallbackKey = (Script, Option<CultureInfo>);
149
150/// The state and the shared matching, fallback and caching logic of font
151/// collections.
152///
153/// Upstream this is an abstract class. Here a collection type holds a
154/// `FontCollectionBase` and implements [`FontCollectionBaseImpl`] (the
155/// abstract and virtual members); [`IFontCollection`] is then implemented for
156/// it automatically. Members of the base class that reach overridable members
157/// are associated functions taking the collection as `this`
158/// (`FontCollectionBase::try_add_glyph_typeface(this, ..)`); the others are
159/// methods of the state (`collection.base().count()`).
160pub struct FontCollectionBase {
161    glyph_typeface_cache: GlyphTypefaceCache,
162
163    // Cache of resolved script/culture fallback family names. A `Some` value is the preferred
164    // fallback family for that script bucket: a Tier B hint that is still re-checked for coverage
165    // per codepoint, and which does NOT by itself suppress a platform call. A `None` value is a
166    // *negative* entry that prevents repeated platform-fallback calls for the same script bucket.
167    script_fallback_cache: RefCell<HashMap<ScriptFallbackKey, Option<String>>>,
168
169    font_families: RefCell<Vec<FontFamily>>,
170    font_manager_impl: Rc<dyn IFontManagerImpl>,
171    asset_loader: Option<Rc<dyn IAssetLoader>>,
172}
173
174/// The abstract and virtual members of a font collection deriving from
175/// [`FontCollectionBase`].
176///
177/// `this` is the collection itself. A default implementation is the base
178/// class behaviour; an override calls it with
179/// `FontCollectionBase::<member>(this, ..)`.
180pub trait FontCollectionBaseImpl: Sized + 'static {
181    /// The base class state of the collection.
182    fn base(this: &Self) -> &FontCollectionBase;
183
184    /// Gets the unique identifier for the font collection.
185    fn key(this: &Self) -> Uri;
186
187    /// Tries to match a specified character to a [`Typeface`] that supports
188    /// specified font properties.
189    fn try_match_character(
190        this: &Self,
191        codepoint: i32,
192        style: FontStyle,
193        weight: FontWeight,
194        stretch: FontStretch,
195        family_name: Option<&str>,
196        culture: Option<&CultureInfo>,
197    ) -> Option<Typeface> {
198        FontCollectionBase::try_match_character(this, codepoint, style, weight, stretch, family_name, culture)
199    }
200
201    /// Hook for platform-backed collections (e.g. the system font collection)
202    /// to consult the underlying font manager for a fallback typeface.
203    /// Invoked at most once per (script-bucket, culture) pair from
204    /// [`FontCollectionBase::try_match_character`].
205    fn try_match_character_from_platform(
206        _this: &Self,
207        _codepoint: i32,
208        _key: FontCollectionKey,
209        _family_name: Option<&str>,
210        _culture: Option<&CultureInfo>,
211    ) -> Option<Rc<GlyphTypeface>> {
212        None
213    }
214
215    /// Try to get a synthetic glyph typeface for given parameters.
216    fn try_create_synthetic_glyph_typeface(
217        this: &Self,
218        glyph_typeface: &Rc<GlyphTypeface>,
219        style: FontStyle,
220        weight: FontWeight,
221        stretch: FontStretch,
222    ) -> Option<Rc<GlyphTypeface>> {
223        FontCollectionBase::try_create_synthetic_glyph_typeface(this, glyph_typeface, style, weight, stretch)
224    }
225
226    /// Try to get a glyph typeface for given parameters.
227    fn try_get_glyph_typeface(
228        this: &Self,
229        family_name: &str,
230        style: FontStyle,
231        weight: FontWeight,
232        stretch: FontStretch,
233    ) -> Option<Rc<GlyphTypeface>> {
234        FontCollectionBase::try_get_glyph_typeface(this, family_name, style, weight, stretch)
235    }
236
237    /// Tries to get a list of typefaces for the specified family name.
238    fn try_get_family_typefaces(this: &Self, family_name: &str) -> Option<Vec<Typeface>> {
239        FontCollectionBase::try_get_family_typefaces(this, family_name)
240    }
241}
242
243/// A font collection deriving from [`FontCollectionBase`], as a trait object:
244/// the `this` of the base class members. Implemented automatically for every
245/// [`FontCollectionBaseImpl`].
246pub trait IFontCollectionBase: IFontCollection {
247    /// The base class state of the collection.
248    fn base(&self) -> &FontCollectionBase;
249
250    /// Calls the collection's
251    /// [`try_match_character_from_platform`](FontCollectionBaseImpl::try_match_character_from_platform).
252    fn try_match_character_from_platform(
253        &self,
254        codepoint: i32,
255        key: FontCollectionKey,
256        family_name: Option<&str>,
257        culture: Option<&CultureInfo>,
258    ) -> Option<Rc<GlyphTypeface>>;
259}
260
261impl<T: FontCollectionBaseImpl> IFontCollectionBase for T {
262    fn base(&self) -> &FontCollectionBase {
263        T::base(self)
264    }
265
266    fn try_match_character_from_platform(
267        &self,
268        codepoint: i32,
269        key: FontCollectionKey,
270        family_name: Option<&str>,
271        culture: Option<&CultureInfo>,
272    ) -> Option<Rc<GlyphTypeface>> {
273        T::try_match_character_from_platform(self, codepoint, key, family_name, culture)
274    }
275}
276
277impl<T: FontCollectionBaseImpl> IFontCollection for T {
278    fn key(&self) -> Uri {
279        T::key(self)
280    }
281
282    fn count(&self) -> usize {
283        T::base(self).count()
284    }
285
286    fn get(&self, index: usize) -> FontFamily {
287        T::base(self).get(index)
288    }
289
290    fn font_families(&self) -> Vec<FontFamily> {
291        T::base(self).font_families()
292    }
293
294    fn try_get_glyph_typeface(
295        &self,
296        family_name: &str,
297        style: FontStyle,
298        weight: FontWeight,
299        stretch: FontStretch,
300    ) -> Option<Rc<GlyphTypeface>> {
301        T::try_get_glyph_typeface(self, family_name, style, weight, stretch)
302    }
303
304    fn try_match_character(
305        &self,
306        codepoint: i32,
307        font_style: FontStyle,
308        font_weight: FontWeight,
309        font_stretch: FontStretch,
310        family_name: Option<&str>,
311        culture: Option<&CultureInfo>,
312    ) -> Option<Typeface> {
313        T::try_match_character(self, codepoint, font_style, font_weight, font_stretch, family_name, culture)
314    }
315
316    fn try_get_family_typefaces(&self, family_name: &str) -> Option<Vec<Typeface>> {
317        T::try_get_family_typefaces(self, family_name)
318    }
319
320    fn try_create_synthetic_glyph_typeface(
321        &self,
322        glyph_typeface: &Rc<GlyphTypeface>,
323        style: FontStyle,
324        weight: FontWeight,
325        stretch: FontStretch,
326    ) -> Option<Rc<GlyphTypeface>> {
327        T::try_create_synthetic_glyph_typeface(self, glyph_typeface, style, weight, stretch)
328    }
329
330    fn try_get_nearest_match(
331        &self,
332        family_name: &str,
333        style: FontStyle,
334        weight: FontWeight,
335        stretch: FontStretch,
336    ) -> Option<Rc<GlyphTypeface>> {
337        T::base(self).try_get_nearest_match(family_name, style, weight, stretch)
338    }
339
340    fn dispose(&self) {
341        T::base(self).dispose();
342    }
343
344    fn as_font_collection_base(&self) -> Option<&dyn IFontCollectionBase> {
345        Some(self)
346    }
347
348    fn as_any(&self) -> &dyn Any {
349        self
350    }
351}
352
353impl Default for FontCollectionBase {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359impl FontCollectionBase {
360    /// Creates the state of a font collection.
361    ///
362    /// Panics when no font manager backend (`IFontManagerImpl`) is registered
363    /// with the locator.
364    pub fn new() -> Self {
365        Self {
366            glyph_typeface_cache: GlyphTypefaceCache::default(),
367            script_fallback_cache: RefCell::new(HashMap::new()),
368            font_families: RefCell::new(Vec::new()),
369            font_manager_impl: FerroLocator::current().get_required_service::<dyn IFontManagerImpl>(),
370            asset_loader: FerroLocator::current().get_service::<dyn IAssetLoader>(),
371        }
372    }
373
374    /// The cached glyph typefaces by family name.
375    pub fn glyph_typeface_cache(&self) -> &GlyphTypefaceCache {
376        &self.glyph_typeface_cache
377    }
378
379    /// The number of font families in the collection.
380    pub fn count(&self) -> usize {
381        self.font_families.borrow().len()
382    }
383
384    /// The font family at `index`.
385    ///
386    /// Panics when `index` is out of range.
387    pub fn get(&self, index: usize) -> FontFamily {
388        self.font_families.borrow()[index].clone()
389    }
390
391    /// A snapshot of the font families of the collection, sorted by name.
392    pub fn font_families(&self) -> Vec<FontFamily> {
393        self.font_families.borrow().clone()
394    }
395
396    /// The base implementation of
397    /// [`FontCollectionBaseImpl::try_match_character`].
398    pub fn try_match_character(
399        this: &dyn IFontCollectionBase,
400        codepoint: i32,
401        style: FontStyle,
402        weight: FontWeight,
403        stretch: FontStretch,
404        family_name: Option<&str>,
405        culture: Option<&CultureInfo>,
406    ) -> Option<Typeface> {
407        Self::try_match_character_with_script(
408            this,
409            codepoint,
410            style,
411            weight,
412            stretch,
413            family_name,
414            culture,
415            Script::Unknown,
416        )
417    }
418
419    /// Character-to-typeface match with an optional shaping-capability
420    /// constraint. When `shaping_script` is a complex script, only candidates
421    /// that can shape it ([`GlyphTypeface::can_shape_script`]) are considered;
422    /// [`Script::Unknown`] imposes no constraint and is identical to the
423    /// public overload.
424    #[allow(clippy::too_many_arguments)]
425    pub(crate) fn try_match_character_with_script(
426        this: &dyn IFontCollectionBase,
427        codepoint: i32,
428        style: FontStyle,
429        weight: FontWeight,
430        stretch: FontStretch,
431        family_name: Option<&str>,
432        culture: Option<&CultureInfo>,
433        shaping_script: Script,
434    ) -> Option<Typeface> {
435        let base = this.base();
436
437        let key = FontCollectionKey::new(style, weight, stretch);
438        let cp = Codepoint::new(codepoint as u32);
439        let script = cp.script();
440        let refined_script = FontFallbackScriptHints::refine_with_culture(cp, culture);
441        let script_key: ScriptFallbackKey = (refined_script, culture.cloned());
442
443        // --- Tier A: requested family, coverage-checked, culture-compatible ---
444        if let Some(family_name) = family_name {
445            if let Some(requested_family) = base.glyph_typeface_cache.try_get_value(family_name) {
446                if let Some(requested_glyph_typeface) = Self::try_get_covering_match_for_family(
447                    this,
448                    &requested_family,
449                    key,
450                    codepoint,
451                    culture,
452                    shaping_script,
453                ) {
454                    if Self::is_culture_compatible(&requested_glyph_typeface, culture, script) {
455                        return Some(Self::build_typeface_with_synthesis(this, &requested_glyph_typeface, key));
456                    }
457                }
458            }
459        }
460
461        // --- Tier B: cached script/culture resolution ---
462        if FontFallbackScriptHints::is_locale_sensitive(refined_script) || culture.is_some() {
463            let cached_family = base.script_fallback_cache.borrow().get(&script_key).cloned().flatten();
464
465            if let Some(cached_family) = cached_family {
466                if !family_name.is_some_and(|family_name| equals_ordinal_ignore_case(&cached_family, family_name)) {
467                    if let Some(cached_typefaces) = base.glyph_typeface_cache.try_get_value(&cached_family) {
468                        if let Some(cached_glyph_typeface) = Self::try_get_covering_match_for_family(
469                            this,
470                            &cached_typefaces,
471                            key,
472                            codepoint,
473                            culture,
474                            shaping_script,
475                        ) {
476                            return Some(Self::build_typeface_with_synthesis(this, &cached_glyph_typeface, key));
477                        }
478                    }
479                }
480            }
481        }
482
483        // --- Tier C: deterministic cache sweep (non last-resort), culture-scored ---
484        if let Some((best_gt, best_family_name)) = base.try_match_in_cache(
485            codepoint,
486            key,
487            family_name,
488            culture,
489            script,
490            refined_script,
491            shaping_script,
492            false,
493        ) {
494            // The sweep returns the nearest cached key for the winning family. A codepoint the
495            // bucket family can't cover (e.g. a Simplified-only ideograph when the bucket is a JP
496            // font) lands here, so the exact-key upgrade is needed here too: otherwise a Bold
497            // face cached for an earlier run is rendered for a Normal request.
498            let best_gt = Self::prefer_exact_key(this, best_gt, key, codepoint, culture, shaping_script);
499
500            if FontFallbackScriptHints::is_locale_sensitive(refined_script) || culture.is_some() {
501                base.script_fallback_cache.borrow_mut().entry(script_key).or_insert(Some(best_family_name));
502            }
503
504            return Some(Self::build_typeface_with_synthesis(this, &best_gt, key));
505        }
506
507        // --- Tier D: platform fallback ---
508        // Only a *negative* cache entry (the platform had no font for this script bucket)
509        // suppresses a retry. A *positive* entry must not: it records a preferred family for the
510        // bucket, but that family may not cover THIS codepoint - e.g. U+4E2D resolves to a CJK
511        // font that lacks the Simplified-only U+534E - and the platform resolves per codepoint,
512        // so it may still place a codepoint the bucket's font cannot. Reaching Tier D already
513        // means no cached family covered this codepoint.
514        // Skipped for shaping-constrained queries: the platform match is not capability-checked,
515        // and the caller's unconstrained pass already covers the platform path (so the negative
516        // cache stays consistent with unconstrained lookups).
517        if shaping_script == Script::Unknown {
518            let cache_entry = base.script_fallback_cache.borrow().get(&script_key).cloned();
519            let has_cache_entry = cache_entry.is_some();
520            let platform_already_attempted = matches!(cache_entry, Some(None));
521
522            if !platform_already_attempted {
523                if let Some(platform_gt) =
524                    this.try_match_character_from_platform(codepoint, key, family_name, culture)
525                {
526                    // Keep any existing positive hint (the entry is not overwritten); registering
527                    // the match lets later lookups for this codepoint be served by Tier C without
528                    // the platform.
529                    base.script_fallback_cache
530                        .borrow_mut()
531                        .entry(script_key)
532                        .or_insert_with(|| Some(platform_gt.family_name().to_owned()));
533
534                    return Some(Self::build_typeface_with_synthesis(this, &platform_gt, key));
535                }
536            }
537
538            // Record a negative only when nothing was cached for the bucket yet, so a positive hint
539            // for one codepoint isn't downgraded to a negative by another the platform can't place.
540            if !has_cache_entry {
541                base.script_fallback_cache.borrow_mut().entry(script_key).or_insert(None);
542            }
543        }
544
545        // --- Tier E: last-resort cache sweep ---
546        if let Some((lr_gt, _)) = base.try_match_in_cache(
547            codepoint,
548            key,
549            family_name,
550            culture,
551            script,
552            refined_script,
553            shaping_script,
554            true,
555        ) {
556            return Some(Self::build_typeface_with_synthesis(this, &lr_gt, key));
557        }
558
559        None
560    }
561
562    #[allow(clippy::too_many_arguments)]
563    fn try_match_in_cache(
564        &self,
565        codepoint: i32,
566        key: FontCollectionKey,
567        skip_family_name: Option<&str>,
568        culture: Option<&CultureInfo>,
569        script: Script,
570        refined_script: Script,
571        shaping_script: Script,
572        is_last_resort: bool,
573    ) -> Option<(Rc<GlyphTypeface>, String)> {
574        let mut best: Option<(Rc<GlyphTypeface>, String)> = None;
575
576        // Iterate the sorted family list for deterministic order.
577        let snapshot = self.font_families.borrow();
578        let mut best_score = i32::MIN;
579
580        for font_family in snapshot.iter() {
581            let family_name = font_family.name();
582
583            if skip_family_name.is_some_and(|skip| equals_ordinal_ignore_case(family_name, skip)) {
584                continue;
585            }
586
587            let Some(glyph_typefaces) = self.glyph_typeface_cache.try_get_value(family_name) else {
588                continue;
589            };
590
591            let Some(candidate) = Self::try_get_covering_match(
592                &glyph_typefaces.borrow(),
593                key,
594                codepoint,
595                is_last_resort,
596                shaping_script,
597            ) else {
598                continue;
599            };
600
601            let score = Self::score_candidate(&candidate, key, culture, script, refined_script);
602
603            if score > best_score {
604                best_score = score;
605                best = Some((candidate, family_name.to_owned()));
606            }
607        }
608
609        best
610    }
611
612    fn score_candidate(
613        candidate: &GlyphTypeface,
614        requested_key: FontCollectionKey,
615        culture: Option<&CultureInfo>,
616        script: Script,
617        refined_script: Script,
618    ) -> i32 {
619        let mut score = 0;
620
621        if let Some(culture) = culture {
622            // Exact culture match in the font's name table.
623            if candidate.family_names().contains_key(culture) {
624                score += 8;
625            } else {
626                let parent = culture.parent();
627
628                if !parent.is_invariant() && candidate.family_names().contains_key(&parent) {
629                    score += 4;
630                }
631            }
632
633            // Self-declared culture coverage via OS/2 codepage bits or meta dlng/slng.
634            if FontFallbackScriptHints::is_font_compatible_with_culture(candidate, Some(culture)) {
635                score += 4;
636            }
637        }
638
639        // Font's primary script aligns with the requested script - ask the font directly.
640        if FontFallbackScriptHints::is_locale_sensitive(refined_script) {
641            if candidate.supports_script(refined_script) {
642                score += 2;
643            } else if refined_script != script && candidate.supports_script(script) {
644                score += 1;
645            }
646        }
647
648        if FontCollectionKey::from(candidate) == requested_key {
649            score += 1;
650        }
651
652        score
653    }
654
655    fn is_culture_compatible(candidate: &GlyphTypeface, culture: Option<&CultureInfo>, script: Script) -> bool {
656        // If no culture or codepoint is locale-insensitive, the candidate is fine.
657        let Some(culture) = culture else {
658            return true;
659        };
660
661        if !FontFallbackScriptHints::is_locale_sensitive(script) {
662            return true;
663        }
664
665        // Positive signal from the font's own self-declaration (OS/2 codepage bits or meta dlng/slng).
666        // When the font declares coverage we accept it as compatible regardless of localized names.
667        if FontFallbackScriptHints::is_font_compatible_with_culture(candidate, Some(culture)) {
668            return true;
669        }
670
671        // If the font has no localized family names at all, treat as compatible (no negative signal).
672        if candidate.family_names().is_empty() {
673            return true;
674        }
675
676        if candidate.family_names().contains_key(culture) {
677            return true;
678        }
679
680        let parent = culture.parent();
681
682        if !parent.is_invariant() && candidate.family_names().contains_key(&parent) {
683            return true;
684        }
685
686        // The font advertises localized names but not for this culture - reject so Tier C can score.
687        false
688    }
689
690    fn build_typeface_with_synthesis(
691        this: &dyn IFontCollectionBase,
692        glyph_typeface: &Rc<GlyphTypeface>,
693        requested_key: FontCollectionKey,
694    ) -> Typeface {
695        let matched_key = FontCollectionKey::from(&**glyph_typeface);
696
697        // The fallback search already found a font face that maps the codepoint. Synthesis
698        // (re-loading the stream with font simulations) is unsafe here: with .ttc collections
699        // the platform may resolve a different face from the same stream (e.g. asking for an
700        // oblique simulation of one face returns a sibling face). Accept the matched
701        // glyph typeface as-is and pre-cache it under the requested key so later
702        // glyph typeface lookups via the returned typeface short-circuit through the cache.
703        if matched_key != requested_key {
704            Self::try_add_glyph_typeface_by_name(
705                this,
706                glyph_typeface.family_name(),
707                requested_key,
708                Some(glyph_typeface.clone()),
709            );
710        }
711
712        Typeface::with_style(
713            FontFamily::with_base_uri(
714                None,
715                &format!("{}#{}", this.key().absolute_uri(), glyph_typeface.family_name()),
716            ),
717            requested_key.style,
718            requested_key.weight,
719            requested_key.stretch,
720        )
721    }
722
723    /// Resolves a covering face for a single family at the requested key.
724    /// Takes the cheap cached covering match first (an exact-key hit needs
725    /// nothing more) and only escalates to the exact key when that match
726    /// differs in any axis, so a Bold (or Italic, or Condensed) face cached
727    /// for one run is not reused for a differently-keyed request of the same
728    /// family.
729    fn try_get_covering_match_for_family(
730        this: &dyn IFontCollectionBase,
731        glyph_typefaces: &Rc<RefCell<GlyphTypefaceMap>>,
732        key: FontCollectionKey,
733        codepoint: i32,
734        culture: Option<&CultureInfo>,
735        shaping_script: Script,
736    ) -> Option<Rc<GlyphTypeface>> {
737        // The borrow of the family's typefaces ends before the platform is consulted.
738        let glyph_typeface =
739            Self::try_get_covering_match(&glyph_typefaces.borrow(), key, codepoint, false, shaping_script)?;
740
741        Some(Self::prefer_exact_key(this, glyph_typeface, key, codepoint, culture, shaping_script))
742    }
743
744    /// When `glyph_typeface` differs from the requested `key` in any axis
745    /// (style, weight or stretch), asks the platform for the exact-key face
746    /// of the same family - the only source of a key the cache lacks - via
747    /// `try_match_character_from_platform`, and returns it when it is an
748    /// exact, shapeable match; otherwise returns the input unchanged. The
749    /// platform is consulted only on a mismatch, and a collection without one
750    /// keeps the neighbouring match. This guards every key axis, not just
751    /// weight.
752    fn prefer_exact_key(
753        this: &dyn IFontCollectionBase,
754        glyph_typeface: Rc<GlyphTypeface>,
755        key: FontCollectionKey,
756        codepoint: i32,
757        culture: Option<&CultureInfo>,
758        shaping_script: Script,
759    ) -> Rc<GlyphTypeface> {
760        // The platform's character match, biased by the family already resolved, yields that family
761        // at the requested key when it has that face (the match covers the codepoint, so no
762        // extra coverage check is needed). Accept it only when it is the exact key and can shape.
763        if FontCollectionKey::from(&*glyph_typeface) != key {
764            if let Some(exact) = this.try_match_character_from_platform(
765                codepoint,
766                key,
767                Some(glyph_typeface.family_name()),
768                culture,
769            ) {
770                if FontCollectionKey::from(&*exact) == key && Self::can_shape(&exact, shaping_script) {
771                    return exact;
772                }
773            }
774        }
775
776        glyph_typeface
777    }
778
779    // A candidate satisfies a shaping-capability constraint when it can shape the requested
780    // script; `Script::Unknown` means no constraint (the historical, unconstrained behaviour).
781    fn can_shape(glyph_typeface: &GlyphTypeface, shaping_script: Script) -> bool {
782        shaping_script == Script::Unknown || glyph_typeface.can_shape_script(shaping_script)
783    }
784
785    /// Picks a variant of the family that both is close to the requested key
786    /// and actually maps the requested codepoint. Falls back through the
787    /// existing weight/stretch search but filters every candidate through the
788    /// font's character-to-glyph map.
789    fn try_get_covering_match(
790        glyph_typefaces: &GlyphTypefaceMap,
791        key: FontCollectionKey,
792        codepoint: i32,
793        is_last_resort: bool,
794        shaping_script: Script,
795    ) -> Option<Rc<GlyphTypeface>> {
796        let covers = |glyph_typeface: &GlyphTypeface| {
797            glyph_typeface.is_last_resort() == is_last_resort
798                && glyph_typeface.character_to_glyph_map().try_get_glyph(codepoint).is_some()
799                && Self::can_shape(glyph_typeface, shaping_script)
800        };
801
802        // Exact key first.
803        if let Some(Some(glyph_typeface)) = glyph_typefaces.get(&key) {
804            if covers(glyph_typeface) {
805                return Some(glyph_typeface.clone());
806            }
807        }
808
809        let mut covering_nearest: Option<&Rc<GlyphTypeface>> = None;
810        let mut covering_distance = i32::MAX;
811
812        // The map iterates its keys in sorted order.
813        for (candidate_key, candidate) in glyph_typefaces {
814            let Some(candidate) = candidate else {
815                continue;
816            };
817
818            if !covers(candidate) {
819                continue;
820            }
821
822            let distance = Self::key_distance(*candidate_key, key);
823
824            if distance < covering_distance {
825                covering_distance = distance;
826                covering_nearest = Some(candidate);
827            }
828        }
829
830        covering_nearest.cloned()
831    }
832
833    fn key_distance(a: FontCollectionKey, b: FontCollectionKey) -> i32 {
834        let weight_delta = (a.weight.value() - b.weight.value()).abs();
835
836        let stretch_delta = (a.stretch as i32 - b.stretch as i32).abs();
837
838        let style_delta = if a.style == b.style { 0 } else { 1 };
839
840        weight_delta + stretch_delta * 100 + style_delta * 10_000
841    }
842
843    /// The base implementation of
844    /// [`FontCollectionBaseImpl::try_create_synthetic_glyph_typeface`].
845    pub fn try_create_synthetic_glyph_typeface(
846        this: &dyn IFontCollectionBase,
847        glyph_typeface: &Rc<GlyphTypeface>,
848        style: FontStyle,
849        weight: FontWeight,
850        stretch: FontStretch,
851    ) -> Option<Rc<GlyphTypeface>> {
852        let base = this.base();
853
854        // Source family should be present in the cache.
855        let glyph_typefaces = base.glyph_typeface_cache.try_get_value(glyph_typeface.family_name())?;
856
857        let key = FontCollectionKey::new(style, weight, stretch);
858
859        let current_key = FontCollectionKey::from(&**glyph_typeface);
860
861        if current_key == key {
862            return None;
863        }
864
865        let mut font_simulations = FontSimulations::None;
866
867        if style != FontStyle::Normal && glyph_typeface.style() != style {
868            font_simulations |= FontSimulations::Oblique;
869        }
870
871        if weight.value() >= 600 && glyph_typeface.weight() < weight {
872            font_simulations |= FontSimulations::Bold;
873        }
874
875        if font_simulations == FontSimulations::None {
876            return None;
877        }
878
879        // A synthetic for this key may already be cached under the source family, reached
880        // through another of its names. Building a second one copies the whole font file
881        // through the platform stream, then loses the slot below to the instance already
882        // there, so nothing caches it and nothing disposes it.
883        if let Some(Some(cached_glyph_typeface)) = glyph_typefaces.borrow().get(&key) {
884            if cached_glyph_typeface.font_simulations() == font_simulations {
885                return Some(cached_glyph_typeface.clone());
886            }
887        }
888
889        let mut stream = glyph_typeface.platform_typeface().try_get_stream()?;
890
891        let platform_typeface =
892            base.font_manager_impl.try_create_glyph_typeface_from_stream(&mut *stream, font_simulations)?;
893
894        let synthetic_glyph_typeface = GlyphTypeface::try_create(platform_typeface, font_simulations)?;
895
896        // Add the typographic family name to the cache
897        if !glyph_typeface.typographic_family_name().is_empty() {
898            Self::try_add_glyph_typeface_by_name(
899                this,
900                glyph_typeface.typographic_family_name(),
901                key,
902                Some(synthetic_glyph_typeface.clone()),
903            );
904        }
905
906        for family_name in glyph_typeface.family_names().values() {
907            Self::try_add_glyph_typeface_by_name(this, family_name, key, Some(synthetic_glyph_typeface.clone()));
908        }
909
910        Some(synthetic_glyph_typeface)
911    }
912
913    /// The base implementation of
914    /// [`FontCollectionBaseImpl::try_get_glyph_typeface`].
915    pub fn try_get_glyph_typeface(
916        this: &dyn IFontCollectionBase,
917        family_name: &str,
918        style: FontStyle,
919        weight: FontWeight,
920        stretch: FontStretch,
921    ) -> Option<Rc<GlyphTypeface>> {
922        let (typeface, family_name) =
923            Typeface::from_name_with_style(family_name, style, weight, stretch).normalize();
924
925        let key = FontCollectionKey::from(&typeface);
926
927        Self::try_get_glyph_typeface_by_key(this, &family_name, key, true)
928    }
929
930    /// The base implementation of
931    /// [`FontCollectionBaseImpl::try_get_family_typefaces`].
932    pub fn try_get_family_typefaces(this: &dyn IFontCollectionBase, family_name: &str) -> Option<Vec<Typeface>> {
933        let glyph_typefaces = this.base().glyph_typeface_cache.try_get_value(family_name)?;
934
935        // Take a snapshot of the keys: creating the font families must not hold the borrow.
936        let keys: Vec<FontCollectionKey> = glyph_typefaces.borrow().keys().copied().collect();
937
938        let collection_key = this.key();
939
940        Some(
941            keys.into_iter()
942                .map(|key| {
943                    Typeface::with_style(
944                        FontFamily::new(&format!("{collection_key}#{family_name}")),
945                        key.style,
946                        key.weight,
947                        key.stretch,
948                    )
949                })
950                .collect(),
951        )
952    }
953
954    /// Attempts to retrieve the glyph typeface that most closely matches the
955    /// specified font family name, style, weight, and stretch.
956    pub fn try_get_nearest_match(
957        &self,
958        family_name: &str,
959        style: FontStyle,
960        weight: FontWeight,
961        stretch: FontStretch,
962    ) -> Option<Rc<GlyphTypeface>> {
963        let glyph_typefaces = self.glyph_typeface_cache.try_get_value(family_name)?;
964
965        let key = FontCollectionKey::new(style, weight, stretch);
966
967        let glyph_typefaces = glyph_typefaces.borrow();
968
969        Self::try_get_nearest_match_in(&glyph_typefaces, key)
970    }
971
972    /// Attempts to add the specified [`GlyphTypeface`] to the font collection.
973    ///
974    /// This method checks the family name and, if applicable, the typographic
975    /// family name and other family names provided by the glyph typeface. If
976    /// any of these names can be associated with the glyph typeface, the
977    /// typeface is added to the collection. The method ensures that duplicate
978    /// entries are not added.
979    ///
980    /// Returns `true` if the glyph typeface was successfully added to the
981    /// collection; otherwise, `false`.
982    pub fn try_add_glyph_typeface(this: &dyn IFontCollectionBase, glyph_typeface: &Rc<GlyphTypeface>) -> bool {
983        let key = FontCollectionKey::from(&**glyph_typeface);
984
985        Self::try_add_glyph_typeface_with_key(this, glyph_typeface, key)
986    }
987
988    /// Attempts to add the specified glyph typeface to the collection using
989    /// the provided key.
990    ///
991    /// The method adds the glyph typeface using both its typographic family
992    /// name and all available family names. If the glyph typeface's family
993    /// name is empty, the method returns `false` and does not add the
994    /// typeface.
995    pub fn try_add_glyph_typeface_with_key(
996        this: &dyn IFontCollectionBase,
997        glyph_typeface: &Rc<GlyphTypeface>,
998        key: FontCollectionKey,
999    ) -> bool {
1000        if glyph_typeface.family_name().is_empty() {
1001            return false;
1002        }
1003
1004        let mut result = false;
1005
1006        // Add the typographic family name to the cache
1007        if !glyph_typeface.typographic_family_name().is_empty()
1008            && Self::try_add_glyph_typeface_by_name(
1009                this,
1010                glyph_typeface.typographic_family_name(),
1011                key,
1012                Some(glyph_typeface.clone()),
1013            )
1014        {
1015            result = true;
1016        }
1017
1018        for family_name in glyph_typeface.family_names().values() {
1019            if Self::try_add_glyph_typeface_by_name(this, family_name, key, Some(glyph_typeface.clone())) {
1020                result = true;
1021            }
1022        }
1023
1024        result
1025    }
1026
1027    /// Attempts to add a glyph typeface from the specified font stream.
1028    ///
1029    /// The method first attempts to create a glyph typeface from the provided
1030    /// font stream. If successful, it adds the created glyph typeface to the
1031    /// collection.
1032    ///
1033    /// Returns the created glyph typeface if it was created and added;
1034    /// otherwise, `None`.
1035    pub fn try_add_glyph_typeface_from_stream(
1036        this: &dyn IFontCollectionBase,
1037        stream: &mut dyn Read,
1038    ) -> Option<Rc<GlyphTypeface>> {
1039        let platform_typeface =
1040            this.base().font_manager_impl.try_create_glyph_typeface_from_stream(stream, FontSimulations::None)?;
1041
1042        let glyph_typeface = GlyphTypeface::try_create(platform_typeface, FontSimulations::None)?;
1043
1044        Self::try_add_glyph_typeface(this, &glyph_typeface).then_some(glyph_typeface)
1045    }
1046
1047    /// Attempts to add a font source to the font collection.
1048    ///
1049    /// This method processes the specified font source and attempts to load
1050    /// all available fonts from it. Fonts are added to the collection based
1051    /// on their family name and typographic family name (if available).
1052    ///
1053    /// `source` can be a `file:` path (a font file or a directory of font
1054    /// files) or a resource URI (`ferres:`, `resm:`).
1055    ///
1056    /// Returns `true` if at least one font from the specified source was
1057    /// successfully added to the font collection; otherwise, `false`.
1058    pub fn try_add_font_source(this: &dyn IFontCollectionBase, source: &Uri) -> bool {
1059        if !source.is_absolute_uri() {
1060            return false;
1061        }
1062
1063        let base = this.base();
1064
1065        let mut result = false;
1066
1067        match source.scheme() {
1068            ASSET_SCHEME | "resm" => {
1069                let font_assets = FontFamilyLoader::load_font_assets(source);
1070
1071                if font_assets.is_empty() {
1072                    return false;
1073                }
1074
1075                let asset_loader = base
1076                    .asset_loader
1077                    .clone()
1078                    .unwrap_or_else(|| FerroLocator::current().get_required_service::<dyn IAssetLoader>());
1079
1080                for font_asset in &font_assets {
1081                    let Ok(mut stream) = asset_loader.open(font_asset, None) else {
1082                        continue;
1083                    };
1084
1085                    let Some(glyph_typeface) = base
1086                        .font_manager_impl
1087                        .try_create_glyph_typeface_from_stream(&mut *stream, FontSimulations::None)
1088                        .and_then(|platform_typeface| {
1089                            GlyphTypeface::try_create(platform_typeface, FontSimulations::None)
1090                        })
1091                    else {
1092                        continue;
1093                    };
1094
1095                    let key = FontCollectionKey::from(&*glyph_typeface);
1096
1097                    // Add the typographic family name to the cache
1098                    if !glyph_typeface.typographic_family_name().is_empty()
1099                        && Self::try_add_glyph_typeface_by_name(
1100                            this,
1101                            glyph_typeface.typographic_family_name(),
1102                            key,
1103                            Some(glyph_typeface.clone()),
1104                        )
1105                    {
1106                        result = true;
1107                    }
1108
1109                    if Self::try_add_glyph_typeface_by_name(
1110                        this,
1111                        glyph_typeface.family_name(),
1112                        key,
1113                        Some(glyph_typeface.clone()),
1114                    ) {
1115                        result = true;
1116                    }
1117                }
1118            }
1119            "file" => {
1120                result = Self::try_add_file_font_source(this, source);
1121            }
1122            _ => {
1123                // Unsupported scheme
1124                return false;
1125            }
1126        }
1127
1128        result
1129    }
1130
1131    #[cfg(not(target_family = "wasm"))]
1132    fn try_add_file_font_source(this: &dyn IFontCollectionBase, source: &Uri) -> bool {
1133        use std::fs::File;
1134        use std::path::{Path, PathBuf};
1135
1136        let add_file = |path: &Path| -> bool {
1137            let Ok(mut stream) = File::open(path) else {
1138                return false;
1139            };
1140
1141            this.base()
1142                .font_manager_impl
1143                .try_create_glyph_typeface_from_stream(&mut stream, FontSimulations::None)
1144                .and_then(|platform_typeface| GlyphTypeface::try_create(platform_typeface, FontSimulations::None))
1145                .is_some_and(|glyph_typeface| Self::try_add_glyph_typeface(this, &glyph_typeface))
1146        };
1147
1148        let local_path = PathBuf::from(local_path(source));
1149
1150        // If the path is a file, load the font file directly
1151        if FontFamilyLoader::is_font_source(source) {
1152            if !local_path.is_file() {
1153                return false;
1154            }
1155
1156            return add_file(&local_path);
1157        }
1158
1159        // If the path is a directory, load all font files from that directory
1160        let Ok(entries) = std::fs::read_dir(&local_path) else {
1161            return false;
1162        };
1163
1164        // The directory is read in name order so that the result does not
1165        // depend on the file system's enumeration order.
1166        let mut files: Vec<PathBuf> =
1167            entries.filter_map(Result::ok).map(|entry| entry.path()).filter(|path| path.is_file()).collect();
1168
1169        files.sort();
1170
1171        let mut result = false;
1172
1173        for file in &files {
1174            if file.to_str().is_some_and(FontFamilyLoader::is_font_file) && add_file(file) {
1175                result = true;
1176            }
1177        }
1178
1179        result
1180    }
1181
1182    /// There is no file system on this target.
1183    #[cfg(target_family = "wasm")]
1184    fn try_add_file_font_source(_this: &dyn IFontCollectionBase, _source: &Uri) -> bool {
1185        false
1186    }
1187
1188    /// Inserts the specified font family into the collection, maintaining the
1189    /// collection in sorted order by font family name.
1190    ///
1191    /// If a font family with the same name (ignoring case) already exists in
1192    /// the collection, nothing is added.
1193    pub fn add_font_family(&self, font_family: FontFamily) {
1194        let mut font_families = self.font_families.borrow_mut();
1195
1196        let index = font_families
1197            .binary_search_by(|existing| compare_ordinal_ignore_case(existing.name(), font_family.name()));
1198
1199        // If an existing family with the same name is present, do nothing
1200        if let Err(index) = index {
1201            font_families.insert(index, font_family);
1202        }
1203    }
1204
1205    /// Attempts to retrieve a glyph typeface that matches the specified font
1206    /// family name and font collection key.
1207    ///
1208    /// This method performs a binary search to locate font families with
1209    /// names that match the specified `family_name` (ignoring case). If
1210    /// multiple matches are found, the method iterates over them to find the
1211    /// best match based on the provided `key`.
1212    ///
1213    /// `allow_nearest_match` tells whether to allow a nearest match (as
1214    /// opposed to only an exact match).
1215    pub fn try_get_glyph_typeface_by_key(
1216        this: &dyn IFontCollectionBase,
1217        family_name: &str,
1218        key: FontCollectionKey,
1219        allow_nearest_match: bool,
1220    ) -> Option<Rc<GlyphTypeface>> {
1221        let base = this.base();
1222
1223        if let Some(glyph_typefaces) = base.glyph_typeface_cache.try_get_value(family_name) {
1224            // The borrow of the family's typefaces ends before the synthetic typeface is created.
1225            let matched = Self::try_get_match(&glyph_typefaces.borrow(), key, allow_nearest_match);
1226
1227            if let Some((mut glyph_typeface, is_nearest_match)) = matched {
1228                let matched_key = FontCollectionKey::from(&*glyph_typeface);
1229
1230                if is_nearest_match && matched_key != key {
1231                    if let Some(synthetic_glyph_typeface) = this.try_create_synthetic_glyph_typeface(
1232                        &glyph_typeface,
1233                        key.style,
1234                        key.weight,
1235                        key.stretch,
1236                    ) {
1237                        glyph_typeface = synthetic_glyph_typeface;
1238                    }
1239
1240                    // The synthetic typeface is registered only under the source font's own
1241                    // family names, so a request arriving through a different name would
1242                    // otherwise miss the cache and re-synthesise on every call, copying the
1243                    // whole font file each time.
1244                    Self::try_add_glyph_typeface_by_name(this, family_name, key, Some(glyph_typeface.clone()));
1245                }
1246
1247                return Some(glyph_typeface);
1248            }
1249        }
1250
1251        // Binary search for the first possible prefix match
1252        let snapshot = base.font_families.borrow();
1253        let mut left = 0isize;
1254        let mut right = snapshot.len() as isize - 1;
1255        let mut first_match: Option<usize> = None;
1256
1257        while left <= right {
1258            let mid = ((left + right) / 2) as usize;
1259            let name = snapshot[mid].name();
1260
1261            match compare_ordinal_ignore_case(name, family_name) {
1262                // If the current name is lexicographically less than the search name, move right
1263                Ordering::Less => left = mid as isize + 1,
1264                Ordering::Equal => {
1265                    // Exact match found. Use the exact family name for lookup.
1266                    // Exact family present but no matching typeface found gives `None`.
1267                    let exact_glyph_typefaces = base.glyph_typeface_cache.try_get_value(name)?;
1268                    let exact_glyph_typefaces = exact_glyph_typefaces.borrow();
1269
1270                    return Self::try_get_match(&exact_glyph_typefaces, key, allow_nearest_match)
1271                        .map(|(glyph_typeface, _)| glyph_typeface);
1272                }
1273                Ordering::Greater => {
1274                    // Only check for prefix when the name is greater than the family name. This
1275                    // avoids the more expensive prefix check for names that are definitely
1276                    // ordered before the search term.
1277                    if starts_with_ordinal_ignore_case(name, family_name) {
1278                        first_match = Some(mid);
1279                    }
1280
1281                    // Continue searching to the left for the first match
1282                    right = mid as isize - 1;
1283                }
1284            }
1285        }
1286
1287        if let Some(first_match) = first_match {
1288            // Iterate over all consecutive prefix matches
1289            for font_family in &snapshot[first_match..] {
1290                if !starts_with_ordinal_ignore_case(font_family.name(), family_name) {
1291                    break;
1292                }
1293
1294                if let Some(glyph_typefaces) = base.glyph_typeface_cache.try_get_value(font_family.name()) {
1295                    if let Some((glyph_typeface, _)) =
1296                        Self::try_get_match(&glyph_typefaces.borrow(), key, allow_nearest_match)
1297                    {
1298                        return Some(glyph_typeface);
1299                    }
1300                }
1301            }
1302        }
1303
1304        None
1305    }
1306
1307    /// The match and whether it is a nearest (not an exact) match.
1308    fn try_get_match(
1309        glyph_typefaces: &GlyphTypefaceMap,
1310        key: FontCollectionKey,
1311        allow_nearest_match: bool,
1312    ) -> Option<(Rc<GlyphTypeface>, bool)> {
1313        if let Some(Some(glyph_typeface)) = glyph_typefaces.get(&key) {
1314            return Some((glyph_typeface.clone(), false));
1315        }
1316
1317        if allow_nearest_match {
1318            if let Some(glyph_typeface) = Self::try_get_nearest_match_in(glyph_typefaces, key) {
1319                return Some((glyph_typeface, true));
1320            }
1321        }
1322
1323        None
1324    }
1325
1326    /// Attempts to retrieve the nearest matching [`GlyphTypeface`] for the
1327    /// specified font key from the provided collection of glyph typefaces.
1328    ///
1329    /// This method attempts to find the best match for the specified font key
1330    /// by considering various fallback strategies, such as normalizing the
1331    /// font style, stretch, and weight. If no suitable match is found, the
1332    /// method will return the first available glyph typeface from the
1333    /// collection, if any.
1334    pub fn try_get_nearest_match_in(
1335        glyph_typefaces: &GlyphTypefaceMap,
1336        key: FontCollectionKey,
1337    ) -> Option<Rc<GlyphTypeface>> {
1338        Self::try_get_nearest_match_core(glyph_typefaces, key, false)
1339            .or_else(|| Self::try_get_nearest_match_core(glyph_typefaces, key, true))
1340    }
1341
1342    fn try_get_nearest_match_core(
1343        glyph_typefaces: &GlyphTypefaceMap,
1344        mut key: FontCollectionKey,
1345        is_last_resort: bool,
1346    ) -> Option<Rc<GlyphTypeface>> {
1347        if let Some(Some(glyph_typeface)) = glyph_typefaces.get(&key) {
1348            if glyph_typeface.is_last_resort() == is_last_resort {
1349                return Some(glyph_typeface.clone());
1350            }
1351        }
1352
1353        if key.style != FontStyle::Normal {
1354            key.style = FontStyle::Normal;
1355        }
1356
1357        if key.stretch != FontStretch::Normal {
1358            if let Some(glyph_typeface) = Self::try_find_stretch_fallback(glyph_typefaces, key, is_last_resort) {
1359                return Some(glyph_typeface);
1360            }
1361
1362            if key.weight != FontWeight::Normal {
1363                if let Some(glyph_typeface) = Self::try_find_stretch_fallback(
1364                    glyph_typefaces,
1365                    FontCollectionKey { weight: FontWeight::Normal, ..key },
1366                    is_last_resort,
1367                ) {
1368                    return Some(glyph_typeface);
1369                }
1370            }
1371
1372            key.stretch = FontStretch::Normal;
1373        }
1374
1375        if let Some(glyph_typeface) = Self::try_find_weight_fallback(glyph_typefaces, key, is_last_resort) {
1376            return Some(glyph_typeface);
1377        }
1378
1379        if let Some(glyph_typeface) = Self::try_find_stretch_fallback(glyph_typefaces, key, is_last_resort) {
1380            return Some(glyph_typeface);
1381        }
1382
1383        // Take the first glyph typeface we can find.
1384        glyph_typefaces
1385            .values()
1386            .flatten()
1387            .find(|typeface| is_last_resort == typeface.is_last_resort())
1388            .cloned()
1389    }
1390
1391    /// Attempts to add a glyph typeface to the cache for the specified font
1392    /// family and key.
1393    ///
1394    /// The font family is added to the collection, in sorted order, with the
1395    /// first glyph typeface cached for it.
1396    ///
1397    /// `glyph_typeface` can be `None` (the platform has no typeface for the
1398    /// key): the answer is cached so that the platform is not asked again,
1399    /// and the family is not added to the collection, which lists only
1400    /// families it has a typeface of.
1401    ///
1402    /// Returns `true` if the glyph typeface was successfully added to the
1403    /// cache or the same typeface is already cached for the key; otherwise,
1404    /// `false`.
1405    pub fn try_add_glyph_typeface_by_name(
1406        this: &dyn IFontCollectionBase,
1407        family_name: &str,
1408        key: FontCollectionKey,
1409        glyph_typeface: Option<Rc<GlyphTypeface>>,
1410    ) -> bool {
1411        if family_name.is_empty() {
1412            return false;
1413        }
1414
1415        let base = this.base();
1416
1417        let (glyph_typefaces, _) = base.glyph_typeface_cache.get_or_add(family_name);
1418
1419        let mut glyph_typefaces = glyph_typefaces.borrow_mut();
1420
1421        // Add or compare the glyph typeface.
1422        if let Some(existing) = glyph_typefaces.get(&key) {
1423            return match (existing, &glyph_typeface) {
1424                (Some(existing), Some(glyph_typeface)) => Rc::ptr_eq(existing, glyph_typeface),
1425                (None, None) => true,
1426                _ => false,
1427            };
1428        }
1429
1430        // Deviation (DEVIATIONS.md, Fonts): upstream's `TryAddGlyphTypeface` publishes the family
1431        // when it creates the cache entry, also for a miss, so a family the platform cannot
1432        // create becomes a family of the system fonts that has no glyph typeface. The family is
1433        // published once, with its first glyph typeface.
1434        let publish = glyph_typeface.is_some() && glyph_typefaces.values().all(Option::is_none);
1435
1436        glyph_typefaces.insert(key, glyph_typeface);
1437
1438        if publish {
1439            let font_family = FontFamily::new(&format!("{}#{}", this.key(), family_name));
1440
1441            // Add the font family to the sorted list
1442            base.add_font_family(font_family);
1443        }
1444
1445        true
1446    }
1447
1448    /// Attempts to locate a fallback glyph typeface with a similar font
1449    /// stretch to the specified key within the provided collection.
1450    ///
1451    /// The search prioritizes font stretches closest to the requested value,
1452    /// expanding outward until a match is found or all options are exhausted.
1453    fn try_find_stretch_fallback(
1454        glyph_typefaces: &GlyphTypefaceMap,
1455        key: FontCollectionKey,
1456        is_last_resort: bool,
1457    ) -> Option<Rc<GlyphTypeface>> {
1458        let stretch = key.stretch as i32;
1459
1460        let try_get_with_stretch = |effective_stretch: i32| -> Option<Rc<GlyphTypeface>> {
1461            let effective_stretch = FontStretch::from_i32(effective_stretch)?;
1462
1463            match glyph_typefaces.get(&FontCollectionKey { stretch: effective_stretch, ..key }) {
1464                Some(Some(glyph_typeface)) if glyph_typeface.is_last_resort() == is_last_resort => {
1465                    Some(glyph_typeface.clone())
1466                }
1467                _ => None,
1468            }
1469        };
1470
1471        if stretch < 5 {
1472            let mut i = 0;
1473
1474            while stretch + i < 9 {
1475                if let Some(glyph_typeface) = try_get_with_stretch(stretch + i) {
1476                    return Some(glyph_typeface);
1477                }
1478
1479                i += 1;
1480            }
1481        } else {
1482            let mut i = 0;
1483
1484            while stretch - i > 1 {
1485                if let Some(glyph_typeface) = try_get_with_stretch(stretch - i) {
1486                    return Some(glyph_typeface);
1487                }
1488
1489                i += 1;
1490            }
1491        }
1492
1493        None
1494    }
1495
1496    /// Attempts to locate a fallback glyph typeface in the specified
1497    /// collection that closely matches the weight of the provided key.
1498    ///
1499    /// The method searches for the closest available weight to the requested
1500    /// value, considering both lighter and heavier alternatives within the
1501    /// collection. If no exact match is found, it progressively searches for
1502    /// the nearest available weight in both directions.
1503    fn try_find_weight_fallback(
1504        glyph_typefaces: &GlyphTypefaceMap,
1505        key: FontCollectionKey,
1506        is_last_resort: bool,
1507    ) -> Option<Rc<GlyphTypeface>> {
1508        let weight = key.weight.value();
1509
1510        let try_get_with_weight = |effective_weight: i32| -> Option<Rc<GlyphTypeface>> {
1511            match glyph_typefaces.get(&FontCollectionKey { weight: FontWeight(effective_weight), ..key }) {
1512                Some(Some(glyph_typeface)) if glyph_typeface.is_last_resort() == is_last_resort => {
1513                    Some(glyph_typeface.clone())
1514                }
1515                _ => None,
1516            }
1517        };
1518
1519        // Looks for available weights from the target up to `limit`, in ascending order.
1520        let ascending = |limit: i32| -> Option<Rc<GlyphTypeface>> {
1521            let mut i = 0;
1522
1523            while weight + i <= limit {
1524                if let Some(glyph_typeface) = try_get_with_weight(weight + i) {
1525                    return Some(glyph_typeface);
1526                }
1527
1528                i += 50;
1529            }
1530
1531            None
1532        };
1533
1534        // Looks for available weights from the target down to 100, in descending order.
1535        let descending = || -> Option<Rc<GlyphTypeface>> {
1536            let mut i = 0;
1537
1538            while weight - i >= 100 {
1539                if let Some(glyph_typeface) = try_get_with_weight(weight - i) {
1540                    return Some(glyph_typeface);
1541                }
1542
1543                i += 50;
1544            }
1545
1546            None
1547        };
1548
1549        // If the target weight given is between 400 and 500 inclusive
1550        if (400..=500).contains(&weight) {
1551            // Look for available weights between the target and 500, in ascending order.
1552            // If no match is found, look for available weights less than the target, in descending order.
1553            // If no match is found, look for available weights greater than 500, in ascending order.
1554            if let Some(glyph_typeface) = ascending(500).or_else(descending).or_else(|| ascending(900)) {
1555                return Some(glyph_typeface);
1556            }
1557        }
1558
1559        // If a weight less than 400 is given, look for available weights less than the target, in descending order.
1560        if weight < 400 {
1561            // If no match is found, look for available weights greater than the target, in ascending order.
1562            if let Some(glyph_typeface) = descending().or_else(|| ascending(900)) {
1563                return Some(glyph_typeface);
1564            }
1565        }
1566
1567        // If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
1568        if weight > 500 {
1569            // If no match is found, look for available weights less than the target, in descending order.
1570            if let Some(glyph_typeface) = ascending(900).or_else(descending) {
1571                return Some(glyph_typeface);
1572            }
1573        }
1574
1575        None
1576    }
1577
1578    /// Disposes every cached glyph typeface.
1579    pub fn dispose(&self) {
1580        for glyph_typefaces in self.glyph_typeface_cache.values() {
1581            let glyph_typefaces: Vec<Rc<GlyphTypeface>> =
1582                glyph_typefaces.borrow().values().flatten().cloned().collect();
1583
1584            for glyph_typeface in glyph_typefaces {
1585                glyph_typeface.dispose();
1586            }
1587        }
1588    }
1589}
1590
1591/// The local operating-system representation of the path of a `file:` URI
1592/// (C# `Uri.LocalPath`): the unescaped path, with Windows drive paths in
1593/// their native form.
1594fn local_path(uri: &Uri) -> String {
1595    let path = crate::utilities::UriExtensions::get_unescape_absolute_path(uri);
1596
1597    if cfg!(windows) && uri.scheme() == "file" {
1598        let bytes = path.as_bytes();
1599
1600        // "/C:/dir/file" -> "C:\dir\file"
1601        if bytes.len() >= 3 && bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b':' {
1602            return path[1..].replace('/', "\\");
1603        }
1604    }
1605
1606    path
1607}