Skip to main content

valo_text/
font.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use skrifa::metrics::Metrics;
5use skrifa::prelude::Size;
6use skrifa::MetadataProvider;
7
8/// `FontId` identifies a registered font within a [`FaceSet`] or [`FontCollection`].
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub struct FontId(
11    /// The zero-based registration index.
12    pub u32,
13);
14
15/// `FontData` is shared, immutable font-file storage supplied by the host.
16///
17/// It may wrap owned bytes or memory-mapped data and must remain readable for
18/// the lifetime of every [`Font`] created from it.
19pub type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
20
21/// `FontAttrs` describes a face's position within a font family.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct FontAttrs {
24    /// `weight` is the CSS font weight, conventionally from 100 to 900.
25    pub weight: u16,
26    /// `italic` indicates whether the face uses an italic or oblique style.
27    pub italic: bool,
28    /// `stretch` is the CSS font-width percentage, where 100 is normal.
29    pub stretch: f32,
30}
31
32/// `NORMAL_STRETCH` is the CSS normal font-width percentage.
33pub const NORMAL_STRETCH: f32 = 100.0;
34
35impl Default for FontAttrs {
36    fn default() -> Self {
37        Self {
38            weight: 400,
39            italic: false,
40            stretch: NORMAL_STRETCH,
41        }
42    }
43}
44
45/// The instance-independent half of a parsed face: bytes, coverage, and
46/// the compiled shaping caches are IDENTICAL across a variable font's
47/// named instances, so every instance shares one of these (DirectWrite's
48/// model: instances enumerate separately, file state is shared).
49struct SharedFace {
50    data: FontData,
51    /// Which face inside `data` — TrueType collections (.ttc) pack
52    /// several; 0 for single-face files.
53    face_index: u32,
54    /// cmap materialized ONCE — fallback resolution is per-character and
55    /// must never re-parse the font (Skia caches per-typeface the same way).
56    charmap: HashMap<u32, u32>,
57    /// HarfBuzz's compiled shaping caches (GSUB/GDEF/cmap), built once —
58    /// the FontRef itself is cheap to reconstruct per shape (Skia's analog:
59    /// HB faces cached per typeface).
60    shaper_data: harfrust::ShaperData,
61}
62
63/// `FontUid` is the process-unique identity of one font instance.
64///
65/// Glyph caches may use it as a stable key because equal identifiers imply
66/// equal outlines and variation coordinates.
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub struct FontUid(
69    /// The process-unique numeric identity.
70    pub u64,
71);
72
73impl FontUid {
74    fn next() -> FontUid {
75        use std::sync::atomic::{AtomicU64, Ordering};
76        static COUNTER: AtomicU64 = AtomicU64::new(1);
77        FontUid(COUNTER.fetch_add(1, Ordering::Relaxed))
78    }
79}
80
81/// `Font` is one parsed font face or named variable-font instance.
82///
83/// It retains immutable source bytes and exposes the names, attributes,
84/// coverage, and metrics needed for shaping and rendering.
85pub struct Font {
86    uid: FontUid,
87    shared: Arc<SharedFace>,
88    /// User-space variation coordinates when this font is a named instance
89    /// of a variable font ((axis tag, value) per axis); empty = the
90    /// file's default instance.
91    variation_coordinates: Vec<([u8; 4], f32)>,
92    /// The same coordinates in normalized variation space, for the skrifa
93    /// views (metrics, COLR paint graphs).
94    variation_location: skrifa::instance::Location,
95    /// HarfBuzz's compiled per-instance data (None = default instance).
96    shaper_instance: Option<harfrust::ShaperInstance>,
97    family: String,
98    /// Other names this face answers to: the file's localized family names
99    /// (fontdb keeps all of them) plus any host-registered alias (Skia's
100    /// `registerTypeface(typeface, familyName)`).
101    aliases: Vec<String>,
102    attrs: FontAttrs,
103    units_per_em: f32,
104    /// Font-unit metrics, y-up (ascent positive, descent negative).
105    ascent: f32,
106    descent: f32,
107    line_gap: f32,
108    /// Union of all glyph ink, font units y-up — Skia's fXMin/fXMax family.
109    bounds: Option<(f32, f32, f32, f32)>,
110    /// (offset from baseline, thickness) in font units, when the font says.
111    underline: Option<(f32, f32)>,
112    strikeout: Option<(f32, f32)>,
113}
114
115impl std::fmt::Debug for Font {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("Font")
118            .field("uid", &self.uid)
119            .field("family", &self.family)
120            .field("attrs", &self.attrs)
121            .finish_non_exhaustive()
122    }
123}
124
125/// Two fonts are equal when they are the same face at the same instance, which is what
126/// their identifiers say; equal identifiers imply equal outlines and coordinates.
127impl PartialEq for Font {
128    fn eq(&self, other: &Font) -> bool {
129        self.uid == other.uid
130    }
131}
132
133impl Font {
134    fn parse(
135        family: &str,
136        attrs: FontAttrs,
137        data: FontData,
138        face_index: u32,
139        variation_coordinates: Vec<([u8; 4], f32)>,
140    ) -> Option<Self> {
141        let shared = SharedFace::parse(data, face_index)?;
142        Self::at_coordinates(shared, family, attrs, variation_coordinates)
143    }
144
145    /// One instance over already-parsed shared state — metrics and shaping
146    /// instance data are the only per-instance work.
147    fn at_coordinates(
148        shared: Arc<SharedFace>,
149        family: &str,
150        attrs: FontAttrs,
151        variation_coordinates: Vec<([u8; 4], f32)>,
152    ) -> Option<Self> {
153        let bytes: &[u8] = (*shared.data).as_ref();
154        let font = skrifa::FontRef::from_index(bytes, shared.face_index).ok()?;
155        let variation_location = font.axes().location(
156            variation_coordinates
157                .iter()
158                .map(|(tag, value)| (skrifa::Tag::new(tag), *value)),
159        );
160        let metrics = Metrics::new(&font, Size::unscaled(), &variation_location);
161        let shaper_instance = (!variation_coordinates.is_empty()).then(|| {
162            let harf = harfrust::FontRef::from_index(bytes, shared.face_index).ok();
163            harf.map(|harf| {
164                harfrust::ShaperInstance::from_variations(
165                    &harf,
166                    variation_coordinates
167                        .iter()
168                        .map(|(tag, value)| harfrust::Variation {
169                            tag: harfrust::Tag::new(tag),
170                            value: *value,
171                        }),
172                )
173            })
174        });
175        Some(Self {
176            uid: FontUid::next(),
177            family: family.to_owned(),
178            aliases: Vec::new(),
179            attrs,
180            units_per_em: metrics.units_per_em as f32,
181            ascent: metrics.ascent,
182            descent: metrics.descent,
183            line_gap: metrics.leading,
184            bounds: metrics.bounds.map(|b| (b.x_min, b.y_min, b.x_max, b.y_max)),
185            underline: metrics.underline.map(|d| (d.offset, d.thickness)),
186            strikeout: metrics.strikeout.map(|d| (d.offset, d.thickness)),
187            shared,
188            variation_coordinates,
189            variation_location,
190            shaper_instance: shaper_instance.flatten(),
191        })
192    }
193
194    /// `data` returns the source font-file bytes.
195    ///
196    /// For a font collection file, use [`Self::face_index`] to select this face.
197    pub fn data(&self) -> &[u8] {
198        (*self.shared.data).as_ref()
199    }
200
201    /// `face_index` returns this face's index within [`Self::data`].
202    pub fn face_index(&self) -> u32 {
203        self.shared.face_index
204    }
205
206    /// `variation_coordinates` returns user-space axis tags and values.
207    ///
208    /// It is empty for static fonts and default variable-font instances.
209    pub fn variation_coordinates(&self) -> &[([u8; 4], f32)] {
210        &self.variation_coordinates
211    }
212
213    pub(crate) fn variation_location(&self) -> &skrifa::instance::Location {
214        &self.variation_location
215    }
216
217    pub(crate) fn shaper_instance(&self) -> Option<&harfrust::ShaperInstance> {
218        self.shaper_instance.as_ref()
219    }
220
221    /// `uid` returns the process-unique identity of this font instance.
222    pub fn uid(&self) -> FontUid {
223        self.uid
224    }
225
226    /// `family` returns the font's primary family name.
227    pub fn family(&self) -> &str {
228        &self.family
229    }
230
231    /// `aliases` returns additional family names recognized for this font.
232    pub fn aliases(&self) -> &[String] {
233        &self.aliases
234    }
235
236    /// `matches` reports whether `name` matches the family or an alias.
237    ///
238    /// Matching is ASCII case-insensitive.
239    pub fn matches(&self, name: &str) -> bool {
240        self.family.eq_ignore_ascii_case(name)
241            || self.aliases.iter().any(|a| a.eq_ignore_ascii_case(name))
242    }
243
244    /// `add_alias` registers another family name if it is not already recognized.
245    pub fn add_alias(&mut self, name: &str) {
246        if !self.matches(name) {
247            self.aliases.push(name.to_owned());
248        }
249    }
250
251    /// `attrs` returns the face-selection attributes of this font.
252    pub fn attrs(&self) -> FontAttrs {
253        self.attrs
254    }
255
256    /// `ascent_px` returns the positive distance above the baseline at `size`.
257    pub fn ascent_px(&self, size: f32) -> f32 {
258        self.ascent * size / self.units_per_em
259    }
260
261    /// `descent_px` returns the positive distance below the baseline at `size`.
262    pub fn descent_px(&self, size: f32) -> f32 {
263        -self.descent * size / self.units_per_em
264    }
265
266    /// `line_height_px` returns the font's default line height at `size`.
267    pub fn line_height_px(&self, size: f32) -> f32 {
268        (self.ascent - self.descent + self.line_gap) * size / self.units_per_em
269    }
270
271    /// `units_per_em` returns the font's design-space units per em.
272    pub fn units_per_em(&self) -> f32 {
273        self.units_per_em
274    }
275
276    /// `ink_box_px` returns the font-wide ink bounds at `size`.
277    ///
278    /// The tuple is `(x_min, y_min, x_max, y_max)` in y-up coordinates around
279    /// the glyph origin. It returns `None` when the font provides no bounds.
280    pub fn ink_box_px(&self, size: f32) -> Option<(f32, f32, f32, f32)> {
281        let k = size / self.units_per_em;
282        self.bounds
283            .map(|(x0, y0, x1, y1)| (x0 * k, y0 * k, x1 * k, y1 * k))
284    }
285
286    /// `covers` reports whether the font maps a character to a glyph.
287    pub fn covers(&self, ch: char) -> bool {
288        self.shared.charmap.contains_key(&(ch as u32))
289    }
290
291    /// `glyph_for` returns the glyph identifier mapped from a character.
292    pub fn glyph_for(&self, ch: char) -> Option<u32> {
293        self.shared.charmap.get(&(ch as u32)).copied()
294    }
295
296    pub(crate) fn shaper_data(&self) -> &harfrust::ShaperData {
297        &self.shared.shaper_data
298    }
299
300    /// `underline_px` returns underline offset and thickness at `size`.
301    ///
302    /// The offset is positive below the baseline. Conventional values are used
303    /// when the font omits underline metrics.
304    pub fn underline_px(&self, size: f32) -> (f32, f32) {
305        self.decoration_px(self.underline, size, -0.1, 0.05)
306    }
307
308    /// `strikeout_px` returns strikeout offset and thickness at `size`.
309    ///
310    /// The offset is positive above the baseline. Conventional values are used
311    /// when the font omits strikeout metrics.
312    pub fn strikeout_px(&self, size: f32) -> (f32, f32) {
313        self.decoration_px(self.strikeout, size, 0.3, 0.05)
314    }
315
316    fn decoration_px(
317        &self,
318        metric: Option<(f32, f32)>,
319        size: f32,
320        default_offset: f32,
321        default_thickness: f32,
322    ) -> (f32, f32) {
323        match metric {
324            // Font units are y-up: positive offsets sit above the baseline.
325            Some((offset, thickness)) => (
326                offset * size / self.units_per_em,
327                (thickness * size / self.units_per_em).max(0.5),
328            ),
329            None => (size * default_offset, (size * default_thickness).max(0.5)),
330        }
331    }
332}
333
334/// `FontDemand` describes font requests that no registered face could satisfy.
335///
336/// Hosts can use it to load additional fonts and lay out affected text again.
337#[derive(Clone, Debug, Default, PartialEq)]
338pub struct FontDemand {
339    /// `families` lists requested family names with no registered match.
340    pub families: Vec<String>,
341    /// `codepoints` lists uncovered characters and their requested attributes.
342    pub codepoints: Vec<(char, FontAttrs)>,
343}
344
345impl FontDemand {
346    /// `is_empty` reports whether every font request was satisfied.
347    pub fn is_empty(&self) -> bool {
348        self.families.is_empty() && self.codepoints.is_empty()
349    }
350
351    pub(crate) fn add_family(&mut self, name: &str) {
352        if !self.families.iter().any(|f| f == name) {
353            self.families.push(name.to_owned());
354        }
355    }
356
357    pub(crate) fn add_codepoint(&mut self, ch: char, attrs: FontAttrs) {
358        if !self.codepoints.contains(&(ch, attrs)) {
359            self.codepoints.push((ch, attrs));
360        }
361    }
362}
363
364/// `FontSource` locates fonts that are not already registered.
365///
366/// A source may consult installed fonts, downloaded assets, or another
367/// host-owned repository. [`FontCollection`] consults sources in registration
368/// order.
369pub trait FontSource {
370    /// `family` returns every available face matching a family name.
371    fn family(&mut self, name: &str) -> Vec<Font>;
372
373    /// `face_for_codepoint` returns a covering face nearest to `attrs`.
374    fn face_for_codepoint(&mut self, codepoint: char, attrs: FontAttrs) -> Option<Font>;
375}
376
377/// `FaceSet` stores registered fonts and their global fallback order.
378///
379/// Cloning a face set shares parsed fonts, allowing callers to grow a snapshot
380/// without modifying existing holders.
381#[derive(Default, Clone)]
382pub struct FaceSet {
383    /// `Arc` per face: adding a font clones N pointers, never re-parses
384    /// (Skia registers typefaces incrementally).
385    fonts: Vec<Arc<Font>>,
386    fallbacks: Vec<FontId>,
387    /// The faces answering to each name, keyed as [`name_key`] spells it, in
388    /// registration order. Resolution asks per character, so a name is a
389    /// lookup rather than a walk over every face comparing strings.
390    by_name: HashMap<String, Vec<FontId>>,
391}
392
393/// `StyleFaces` is one text style's font resolution, prepared by
394/// [`FaceSet::style_faces`].
395///
396/// It holds face identifiers only, ordered for [`FaceSet::resolve_prepared`];
397/// it is stale once a face is added to the set it came from.
398#[derive(Clone, Debug)]
399pub struct StyleFaces {
400    /// The requested families' faces, in request order, nearest-first.
401    families: Vec<Vec<FontId>>,
402    /// The fallback chain, nearest-first.
403    fallbacks: Vec<FontId>,
404    /// Where tofu renders when nothing covers: the style's best variant,
405    /// else the first fallback, else the first face; `None` on an empty set.
406    tofu: Option<FontId>,
407}
408
409impl FaceSet {
410    /// `new` creates an empty face set.
411    pub fn new() -> Self {
412        Self::default()
413    }
414
415    /// `register` adds font bytes under a family using default attributes.
416    ///
417    /// It returns `None` when face zero cannot be parsed.
418    pub fn register(&mut self, family: &str, bytes: Vec<u8>) -> Option<FontId> {
419        self.register_with(family, FontAttrs::default(), bytes)
420    }
421
422    /// `register_with` adds font bytes under a family with explicit attributes.
423    ///
424    /// It returns `None` when face zero cannot be parsed.
425    pub fn register_with(
426        &mut self,
427        family: &str,
428        attrs: FontAttrs,
429        bytes: Vec<u8>,
430    ) -> Option<FontId> {
431        let (data, face_index) = unwrapped(Arc::new(bytes), 0)?;
432        let font = Font::parse(family, attrs, data, face_index, Vec::new())?;
433        Some(self.add(font))
434    }
435
436    /// `add` registers an already parsed font under its embedded names and attributes.
437    pub fn add(&mut self, font: Font) -> FontId {
438        let id = FontId(self.fonts.len() as u32);
439        let aliases = font.aliases().iter().map(String::as_str);
440        for name in std::iter::once(font.family()).chain(aliases) {
441            let ids = self.by_name.entry(name_key(name)).or_default();
442            if ids.last() != Some(&id) {
443                ids.push(id);
444            }
445        }
446        self.fonts.push(Arc::new(font));
447        id
448    }
449
450    /// `with_font` returns a cloned face set containing one additional font.
451    ///
452    /// Existing fonts remain shared and are not reparsed.
453    pub fn with_font(&self, font: Font) -> (FaceSet, FontId) {
454        let mut next = self.clone();
455        let id = next.add(font);
456        (next, id)
457    }
458
459    /// `with_fallbacks` returns a clone with its global fallback order replaced.
460    ///
461    /// Every identifier must belong to this face set.
462    pub fn with_fallbacks(&self, fallbacks: Vec<FontId>) -> FaceSet {
463        let mut next = self.clone();
464        next.fallbacks = fallbacks;
465        next
466    }
467
468    /// `add_fallback` appends a registered font to the global fallback order.
469    ///
470    /// Requested families are searched before this chain. `id` must belong to
471    /// this face set.
472    pub fn add_fallback(&mut self, id: FontId) {
473        self.fallbacks.push(id);
474    }
475
476    /// `grown_by` returns a clone extended with answers from a font source.
477    ///
478    /// Requested families are also registered under the requested name.
479    /// Uncovered codepoints add matching faces to the fallback chain. It returns
480    /// `None` when the source supplies nothing new.
481    pub fn grown_by(&self, source: &mut dyn FontSource, demand: &FontDemand) -> Option<FaceSet> {
482        let mut next = self.clone();
483        let mut grew = false;
484        for name in &demand.families {
485            if next.family(name).is_some() {
486                // Answered already (a stale demand) — resolution matches
487                // it now; asking the source again would duplicate faces.
488                continue;
489            }
490            grew |= next.register_answers(source.family(name), name);
491        }
492        for &(codepoint, attrs) in &demand.codepoints {
493            grew |= next.register_fallback_answer(source, codepoint, attrs);
494        }
495        grew.then_some(next)
496    }
497
498    fn register_answers(&mut self, faces: Vec<Font>, requested_name: &str) -> bool {
499        let mut added = false;
500        for mut font in faces {
501            font.add_alias(requested_name);
502            self.add(font);
503            added = true;
504        }
505        added
506    }
507
508    fn register_fallback_answer(
509        &mut self,
510        source: &mut dyn FontSource,
511        codepoint: char,
512        attrs: FontAttrs,
513    ) -> bool {
514        if is_private_use(codepoint) {
515            // Icon fonts resolve by their REGISTERED family name; a
516            // generic source "covering" the Private Use Area paints some
517            // vendor's glyphs where tofu is the honest render.
518            return false;
519        }
520        if self.fallbacks_cover(codepoint) {
521            // Already in the chain — resolution will find that face without
522            // a second copy of it.
523            return false;
524        }
525        let Some(font) = source.face_for_codepoint(codepoint, attrs) else {
526            return false;
527        };
528        let id = self.add(font);
529        self.add_fallback(id);
530        true
531    }
532
533    /// Whether the global fallback chain covers `codepoint`.
534    ///
535    /// A fallback answer is only ever reached through this chain, so this —
536    /// not "some registered face" — decides whether another one is needed.
537    fn fallbacks_cover(&self, codepoint: char) -> bool {
538        self.fallbacks
539            .iter()
540            .any(|id| self.get(*id).covers(codepoint))
541    }
542
543    /// Whether resolution finds a face covering `ch`.
544    ///
545    /// This is the walk [`Self::resolve_covered`] makes, so a face resolution
546    /// cannot reach — registered under a name nobody asked for and absent from
547    /// the fallback chain, as an icon font is — does not pass for coverage it
548    /// will never supply. Asking "does some registered face cover it" instead
549    /// lets such a font suppress the fallback the text actually needs.
550    pub(crate) fn covered_by_resolution(
551        &self,
552        families: &[String],
553        attrs: FontAttrs,
554        ch: char,
555    ) -> bool {
556        matches!(
557            self.resolve_covered_opt(families, attrs, ch),
558            Some((_, true))
559        )
560    }
561
562    /// `is_empty` reports whether no fonts are registered.
563    pub fn is_empty(&self) -> bool {
564        self.fonts.is_empty()
565    }
566
567    /// `len` returns the number of registered fonts.
568    pub fn len(&self) -> usize {
569        self.fonts.len()
570    }
571
572    /// `get_arc` returns a shared handle to a registered font.
573    ///
574    /// # Panics
575    ///
576    /// Panics if `id` does not belong to this face set.
577    pub fn get_arc(&self, id: FontId) -> Arc<Font> {
578        self.fonts[id.0 as usize].clone()
579    }
580
581    /// `get` returns a registered font by identifier.
582    ///
583    /// # Panics
584    ///
585    /// Panics if `id` does not belong to this face set.
586    pub fn get(&self, id: FontId) -> &Font {
587        &self.fonts[id.0 as usize]
588    }
589
590    /// `family` returns the first registered font matching a family name or alias.
591    pub fn family(&self, name: &str) -> Option<FontId> {
592        self.by_name.get(&name_key(name))?.first().copied()
593    }
594
595    /// `faces` iterates over every font matching a family name or alias.
596    ///
597    /// Results follow registration order and include separate subset faces.
598    pub fn faces<'a>(&'a self, name: &'a str) -> impl Iterator<Item = FontId> + 'a {
599        self.variants(name).map(|(id, _)| id)
600    }
601
602    /// `family_variant` returns the family face nearest to requested attributes.
603    ///
604    /// Width is matched first, then italic style and weight. Registration order
605    /// breaks ties.
606    pub fn family_variant(&self, name: &str, attrs: FontAttrs) -> Option<FontId> {
607        self.nearest(self.variants(name), attrs)
608    }
609
610    /// `resolve` selects a font for a character and requested style.
611    ///
612    /// It searches requested families in order, then global fallbacks. If no
613    /// font covers the character, it returns a font suitable for rendering
614    /// `.notdef`.
615    ///
616    /// # Panics
617    ///
618    /// Panics when the face set is empty.
619    pub fn resolve(&self, families: &[String], attrs: FontAttrs, ch: char) -> FontId {
620        self.resolve_covered(families, attrs, ch).0
621    }
622
623    /// `resolve_covered` selects a font and reports whether it covers the character.
624    ///
625    /// A `false` flag means the returned font will render `.notdef`.
626    ///
627    /// # Panics
628    ///
629    /// Panics when the face set is empty.
630    pub fn resolve_covered(
631        &self,
632        families: &[String],
633        attrs: FontAttrs,
634        ch: char,
635    ) -> (FontId, bool) {
636        let faces = self.style_faces(families, attrs);
637        self.resolve_prepared(&faces, ch).unwrap_or_else(|| {
638            panic!(
639                "FontCollection has no fonts registered — register() one before building paragraphs"
640            );
641        })
642    }
643
644    /// `style_faces` prepares one style's resolution: the faces of each
645    /// requested family and of the fallback chain, nearest-first to `attrs`.
646    ///
647    /// A run of text asks for a font per character; prepared once per style,
648    /// each character is a coverage walk over these lists and no name lookup
649    /// (Skia's `SkFontCollection::findTypefaces`, done once per text style).
650    pub fn style_faces(&self, families: &[String], attrs: FontAttrs) -> StyleFaces {
651        let fallbacks = self.fallbacks.iter().map(|id| (*id, self.get(*id)));
652        StyleFaces {
653            families: families
654                .iter()
655                .map(|name| self.nearest_first(self.variants(name), attrs))
656                .collect(),
657            fallbacks: self.nearest_first(fallbacks, attrs),
658            tofu: self.tofu_face_opt(families, attrs),
659        }
660    }
661
662    /// `resolve_prepared` selects a font for `ch` from a prepared style.
663    ///
664    /// The first covering face of the first requested family that has one
665    /// wins, then the fallback chain's; the flag is `false` when nothing
666    /// covers and the returned font renders `.notdef`. `None` when the face
667    /// set is empty.
668    pub fn resolve_prepared(&self, faces: &StyleFaces, ch: char) -> Option<(FontId, bool)> {
669        let covering = faces
670            .families
671            .iter()
672            .chain(std::iter::once(&faces.fallbacks))
673            .flatten()
674            .find(|id| self.get(**id).covers(ch));
675        match covering {
676            Some(id) => Some((*id, true)),
677            None => faces.tofu.map(|id| (id, false)),
678        }
679    }
680
681    /// Every face answering to `name`, with its id, in registration order.
682    fn variants<'a>(&'a self, name: &str) -> impl Iterator<Item = (FontId, &'a Font)> + 'a {
683        self.by_name
684            .get(&name_key(name))
685            .into_iter()
686            .flatten()
687            .map(|id| (*id, self.get(*id)))
688    }
689
690    /// CSS-style nearest among `faces`: width first, then matching style,
691    /// then smallest weight distance, ties to the first registered. That is
692    /// the CSS font-matching precedence; the distances themselves stay plain
693    /// absolute differences rather than CSS's directional walk.
694    fn nearest<'a>(
695        &self,
696        faces: impl Iterator<Item = (FontId, &'a Font)>,
697        attrs: FontAttrs,
698    ) -> Option<FontId> {
699        faces
700            .min_by_key(|(_, f)| {
701                (
702                    stretch_distance(f.attrs.stretch, attrs.stretch),
703                    f.attrs.italic != attrs.italic,
704                    f.attrs.weight.abs_diff(attrs.weight),
705                )
706            })
707            .map(|(id, _)| id)
708    }
709
710    /// `faces` in [`Self::nearest`]'s order: the face `nearest` would pick
711    /// first, and so on, with ties in registration order.
712    fn nearest_first<'a>(
713        &self,
714        faces: impl Iterator<Item = (FontId, &'a Font)>,
715        attrs: FontAttrs,
716    ) -> Vec<FontId> {
717        let mut faces: Vec<(FontId, &Font)> = faces.collect();
718        faces.sort_by_key(|(_, f)| {
719            (
720                stretch_distance(f.attrs.stretch, attrs.stretch),
721                f.attrs.italic != attrs.italic,
722                f.attrs.weight.abs_diff(attrs.weight),
723            )
724        });
725        faces.into_iter().map(|(id, _)| id).collect()
726    }
727
728    /// [`Self::tofu_face`] that reports an EMPTY collection instead of
729    /// panicking — the `build_with` path, where an empty start is valid
730    /// (the chain is asked first; a char no source can render is skipped
731    /// and reported through the demand).
732    pub(crate) fn tofu_face_opt(&self, families: &[String], attrs: FontAttrs) -> Option<FontId> {
733        families
734            .iter()
735            .find_map(|name| self.family_variant(name, attrs))
736            .or_else(|| self.fallbacks.first().copied())
737            .or_else(|| (!self.fonts.is_empty()).then_some(FontId(0)))
738    }
739
740    /// [`Self::resolve_covered`] that can also report "no face exists at
741    /// all" (empty collection on the `build_with` path).
742    pub(crate) fn resolve_covered_opt(
743        &self,
744        families: &[String],
745        attrs: FontAttrs,
746        ch: char,
747    ) -> Option<(FontId, bool)> {
748        if self.fonts.is_empty() {
749            return None;
750        }
751        Some(self.resolve_covered(families, attrs, ch))
752    }
753}
754
755/// The spelling a family name or alias is indexed under: names match
756/// ASCII-case-insensitively (as [`Font::matches`] compares them).
757fn name_key(name: &str) -> String {
758    name.to_ascii_lowercase()
759}
760
761impl Font {
762    /// `from_bytes` parses face zero from owned font-file bytes.
763    ///
764    /// Family names and attributes come from the font. Localized family names
765    /// become aliases. It returns `None` when the bytes cannot be parsed.
766    pub fn from_bytes(bytes: Vec<u8>) -> Option<Font> {
767        Self::from_data(Arc::new(bytes), 0)
768    }
769
770    /// `from_data` parses one face from shared font-file storage.
771    ///
772    /// Use face index zero for a single-face file. It returns `None` when the
773    /// index or font data is invalid.
774    pub fn from_data(data: FontData, face_index: u32) -> Option<Font> {
775        let (data, face_index) = unwrapped(data, face_index)?;
776        Self::instance(data, face_index, Vec::new())
777    }
778
779    /// `instances_from_data` parses the registrable instances of one face.
780    ///
781    /// Static fonts produce one item. Variable fonts with named instances
782    /// produce one [`Font`] per instance. Invalid data returns an empty vector.
783    pub fn instances_from_data(data: FontData, face_index: u32) -> Vec<Font> {
784        let Some((data, face_index)) = unwrapped(data, face_index) else {
785            return Vec::new();
786        };
787        let instances = named_instance_coordinates((*data).as_ref(), face_index);
788        if instances.is_empty() {
789            return Self::instance(data, face_index, Vec::new())
790                .into_iter()
791                .collect();
792        }
793        // The expensive halves (cmap, shaping caches) parse ONCE; each
794        // instance adds only its metrics and axis data.
795        let Some(shared) = SharedFace::parse(data, face_index) else {
796            return Vec::new();
797        };
798        instances
799            .into_iter()
800            .filter_map(|coordinates| Self::shared_instance(shared.clone(), coordinates))
801            .collect()
802    }
803
804    /// One face at one set of coordinates, self-described from its tables
805    /// (attrs overridden by the coordinates' weight/italic axes).
806    fn instance(data: FontData, face_index: u32, coordinates: Vec<([u8; 4], f32)>) -> Option<Font> {
807        let shared = SharedFace::parse(data, face_index)?;
808        Self::shared_instance(shared, coordinates)
809    }
810
811    /// [`Self::instance`] over already-shared file state.
812    fn shared_instance(shared: Arc<SharedFace>, coordinates: Vec<([u8; 4], f32)>) -> Option<Font> {
813        let bytes: &[u8] = (*shared.data).as_ref();
814        let (family, aliases) = embedded_names(bytes, shared.face_index)?;
815        let attrs = instance_attrs(embedded_attrs(bytes, shared.face_index), &coordinates);
816        let mut font = Font::at_coordinates(shared, &family, attrs, coordinates)?;
817        for name in &aliases {
818            font.add_alias(name);
819        }
820        Some(font)
821    }
822}
823
824impl SharedFace {
825    fn parse(data: FontData, face_index: u32) -> Option<Arc<Self>> {
826        let bytes: &[u8] = (*data).as_ref();
827        let font = skrifa::FontRef::from_index(bytes, face_index).ok()?;
828        let charmap = font
829            .charmap()
830            .mappings()
831            .map(|(code, glyph)| (code, glyph.to_u32()))
832            .collect();
833        let harf = harfrust::FontRef::from_index(bytes, face_index).ok()?;
834        let shaper_data = harfrust::ShaperData::new(&harf);
835        Some(Arc::new(Self {
836            data,
837            face_index,
838            charmap,
839            shaper_data,
840        }))
841    }
842}
843
844/// The (tag, value) coordinate rows of every fvar named instance.
845fn named_instance_coordinates(bytes: &[u8], face_index: u32) -> Vec<Vec<([u8; 4], f32)>> {
846    let Ok(font) = skrifa::FontRef::from_index(bytes, face_index) else {
847        return Vec::new();
848    };
849    let axis_tags: Vec<[u8; 4]> = font
850        .axes()
851        .iter()
852        .map(|axis| axis.tag().to_be_bytes())
853        .collect();
854    font.named_instances()
855        .iter()
856        .map(|instance| {
857            axis_tags
858                .iter()
859                .copied()
860                .zip(instance.user_coords())
861                .collect()
862        })
863        .collect()
864}
865
866/// A named instance's place in its family: the weight/italic/width axes
867/// override what the file's default-instance OS/2 table says.
868fn instance_attrs(base: FontAttrs, coordinates: &[([u8; 4], f32)]) -> FontAttrs {
869    let mut attrs = base;
870    for (tag, value) in coordinates {
871        match tag {
872            b"wght" => attrs.weight = value.clamp(1.0, 1000.0) as u16,
873            b"ital" => attrs.italic = *value >= 0.5,
874            b"slnt" => attrs.italic = attrs.italic || *value < 0.0,
875            // `wdth` is already a percentage, which is what CSS asks for.
876            b"wdth" => attrs.stretch = value.clamp(1.0, 1000.0),
877            _ => {}
878        }
879    }
880    attrs
881}
882
883/// Ordered width distance for variant matching. Quantized to 1/16 of a
884/// percent so it can be an integer sort key without collapsing the named
885/// widths (they are 12.5 apart at the closest).
886fn stretch_distance(candidate: f32, wanted: f32) -> u32 {
887    ((candidate - wanted).abs() * 16.0) as u32
888}
889
890/// WOFF2 arrives brotli-wrapped; faces parse the unwrapped TrueType bytes
891/// (icon and web fonts ship compressed — the CoreText managers accept
892/// them, so registration here does too). Identity for everything else.
893/// A collection face is emitted as its own single-face SFNT, so the
894/// returned index is always zero after unwrap.
895#[cfg(feature = "woff2")]
896fn unwrapped(data: FontData, face_index: u32) -> Option<(FontData, u32)> {
897    let bytes: &[u8] = (*data).as_ref();
898    if !bytes.starts_with(b"wOF2") {
899        return Some((data, face_index));
900    }
901    let unpacked = crate::woff2::to_sfnt(bytes, face_index)?;
902    Some((Arc::new(unpacked), 0))
903}
904
905#[cfg(not(feature = "woff2"))]
906fn unwrapped(data: FontData, face_index: u32) -> Option<(FontData, u32)> {
907    Some((data, face_index))
908}
909
910/// name table: the primary is the en typographic family (then en family,
911/// then any-language); every other family/typographic-family string rides
912/// along as an alias.
913fn embedded_names(data: &[u8], face_index: u32) -> Option<(String, Vec<String>)> {
914    use swash::StringId;
915    let font = swash::FontRef::from_index(data, face_index as usize)?;
916    let strings = font.localized_strings();
917    let pick = |id: StringId| {
918        strings
919            .find_by_id(id, Some("en"))
920            .or_else(|| strings.find_by_id(id, None))
921            .map(|s| s.to_string())
922    };
923    let primary = pick(StringId::TypographicFamily).or_else(|| pick(StringId::Family))?;
924    let aliases = strings
925        .filter(|s| matches!(s.id(), StringId::Family | StringId::TypographicFamily))
926        .map(|s| s.to_string())
927        .filter(|name| *name != primary)
928        .collect();
929    Some((primary, aliases))
930}
931
932/// OS/2 weight + width + style flags via swash attributes.
933fn embedded_attrs(data: &[u8], face_index: u32) -> FontAttrs {
934    let Some(font) = swash::FontRef::from_index(data, face_index as usize) else {
935        return FontAttrs::default();
936    };
937    let attrs = font.attributes();
938    FontAttrs {
939        weight: attrs.weight().0,
940        italic: attrs.style() != swash::Style::Normal,
941        stretch: attrs.stretch().to_percentage(),
942    }
943}
944
945/// Unicode Private Use Areas — codepoints whose meaning belongs to a
946/// specific registered font, never to a generic fallback source.
947fn is_private_use(codepoint: char) -> bool {
948    matches!(
949        codepoint,
950        '\u{E000}'..='\u{F8FF}' | '\u{F0000}'..='\u{FFFFD}' | '\u{100000}'..='\u{10FFFD}'
951    )
952}
953
954/// `FontCollection` owns registered faces and sources for resolving missing fonts.
955///
956/// Paragraph building consults sources in order and registers their answers.
957/// Unanswered requests accumulate until [`Self::take_unanswered`] is called.
958#[derive(Default)]
959pub struct FontCollection {
960    faces: FaceSet,
961    /// Consulted in order, first answer wins (Skia's manager priority:
962    /// registered bytes and downloaders before the platform database).
963    sources: Vec<Box<dyn FontSource>>,
964    /// Misses no source answered, since the last drain.
965    unanswered: FontDemand,
966}
967
968impl FontCollection {
969    /// `new` creates an empty collection with no font sources.
970    pub fn new() -> FontCollection {
971        FontCollection::default()
972    }
973
974    /// `faces` returns the faces currently registered in the collection.
975    ///
976    /// A built paragraph clones this set and remains independent of later changes.
977    pub fn faces(&self) -> &FaceSet {
978        &self.faces
979    }
980
981    /// `register` adds font bytes under a family using default attributes.
982    ///
983    /// It returns `None` when face zero cannot be parsed.
984    pub fn register(&mut self, family: &str, bytes: Vec<u8>) -> Option<FontId> {
985        self.faces.register(family, bytes)
986    }
987
988    /// `add` registers an already parsed font.
989    pub fn add(&mut self, font: Font) -> FontId {
990        self.faces.add(font)
991    }
992
993    /// `add_fallback` appends a registered font to the global fallback order.
994    ///
995    /// `id` must belong to this collection.
996    pub fn add_fallback(&mut self, id: FontId) {
997        self.faces.add_fallback(id);
998    }
999
1000    /// `get` returns a registered font by identifier.
1001    ///
1002    /// # Panics
1003    ///
1004    /// Panics if `id` does not belong to this collection.
1005    pub fn get(&self, id: FontId) -> &Font {
1006        self.faces.get(id)
1007    }
1008
1009    /// `len` returns the number of registered fonts.
1010    pub fn len(&self) -> usize {
1011        self.faces.len()
1012    }
1013
1014    /// `family` returns the first font matching a family name or alias.
1015    pub fn family(&self, name: &str) -> Option<FontId> {
1016        self.faces.family(name)
1017    }
1018
1019    /// `add_source` appends a source consulted when registered fonts cannot satisfy a request.
1020    pub fn add_source(&mut self, source: impl FontSource + 'static) {
1021        self.sources.push(Box::new(source));
1022    }
1023
1024    /// `add_boxed_source` appends an already boxed font source.
1025    pub fn add_boxed_source(&mut self, source: Box<dyn FontSource>) {
1026        self.sources.push(source);
1027    }
1028
1029    /// `is_empty` reports whether no fonts are currently registered.
1030    pub fn is_empty(&self) -> bool {
1031        self.faces.is_empty()
1032    }
1033
1034    /// `adopt_faces` replaces the registered faces with a prepared set.
1035    ///
1036    /// This supports hosts that answer [`FontDemand`] using [`FaceSet::grown_by`].
1037    pub fn adopt_faces(&mut self, faces: FaceSet) {
1038        self.faces = faces;
1039    }
1040
1041    /// `take_unanswered` drains font requests that no source could satisfy.
1042    ///
1043    /// Hosts may load and [`Self::register`] matching fonts before rebuilding
1044    /// affected paragraphs.
1045    pub fn take_unanswered(&mut self) -> FontDemand {
1046        std::mem::take(&mut self.unanswered)
1047    }
1048
1049    /// Resolution with growth: look up, else ask the sources in order,
1050    /// registering what they answer (Skia's `findTypefaces` walking its
1051    /// managers). `false` = nobody had it; the miss is recorded.
1052    pub(crate) fn require_family(&mut self, name: &str) -> bool {
1053        if self.faces.family(name).is_some() {
1054            return true;
1055        }
1056        for source in &mut self.sources {
1057            let faces = source.family(name);
1058            if self.faces.register_answers(faces, name) {
1059                return true;
1060            }
1061        }
1062        self.unanswered.add_family(name);
1063        false
1064    }
1065
1066    /// The per-codepoint half (Skia's `defaultFallback(unicode, ..)`).
1067    pub(crate) fn require_codepoint(
1068        &mut self,
1069        families: &[String],
1070        codepoint: char,
1071        attrs: FontAttrs,
1072    ) -> bool {
1073        if self.faces.covered_by_resolution(families, attrs, codepoint) {
1074            return true;
1075        }
1076        for index in 0..self.sources.len() {
1077            let (head, tail) = self.sources.split_at_mut(index);
1078            let _ = head;
1079            let source = &mut tail[0];
1080            if self
1081                .faces
1082                .register_fallback_answer(source.as_mut(), codepoint, attrs)
1083            {
1084                return true;
1085            }
1086        }
1087        self.unanswered.add_codepoint(codepoint, attrs);
1088        false
1089    }
1090}