Skip to main content

valo_text/
font.rs

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