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