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