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/// Index into a [`FontCollection`] — stable for the collection's lifetime.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub struct FontId(pub u32);
11
12/// Font file bytes however the host owns them: an owned buffer, or a
13/// memory-mapped file from a system-font source — faces only ever read.
14pub type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
15
16/// One registered font: immutable bytes + the metrics every layout needs.
17/// Shaping (harfrust), outlines (skrifa), and raster (swash) all re-read
18/// the same bytes — no parsed state is shared across those seams.
19/// A variant's place in its family — CSS-style matching picks by these.
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct FontAttrs {
22    /// CSS weight, 100–900.
23    pub weight: u16,
24    pub italic: bool,
25    /// CSS `font-width` (legacy `font-stretch`) as a PERCENTAGE: 100 is
26    /// normal, 75 condensed, 125 expanded. A variable font's `wdth` named
27    /// instances register as separate variants and are matched on this, the
28    /// same way weight already picks among a family's faces.
29    pub stretch: f32,
30}
31
32/// Neither condensed nor expanded — CSS `font-width: normal`.
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/// Stable raster identity of one font INSTANCE (glyph caches key on it:
64/// same uid = same outlines). Assigned at instance construction from a
65/// process counter — Skia's typeface uniqueID role.
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
67pub struct FontUid(pub u64);
68
69impl FontUid {
70    fn next() -> FontUid {
71        use std::sync::atomic::{AtomicU64, Ordering};
72        static COUNTER: AtomicU64 = AtomicU64::new(1);
73        FontUid(COUNTER.fetch_add(1, Ordering::Relaxed))
74    }
75}
76
77pub struct Font {
78    uid: FontUid,
79    shared: Arc<SharedFace>,
80    /// User-space variation coordinates when this font is a named instance
81    /// of a variable font ((axis tag, value) per axis); empty = the
82    /// file's default instance.
83    variation_coordinates: Vec<([u8; 4], f32)>,
84    /// The same coordinates in normalized variation space, for the skrifa
85    /// views (metrics, COLR paint graphs).
86    variation_location: skrifa::instance::Location,
87    /// HarfBuzz's compiled per-instance data (None = default instance).
88    shaper_instance: Option<harfrust::ShaperInstance>,
89    family: String,
90    /// Other names this face answers to: the file's localized family names
91    /// (fontdb keeps all of them) plus any host-registered alias (Skia's
92    /// `registerTypeface(typeface, familyName)`).
93    aliases: Vec<String>,
94    attrs: FontAttrs,
95    units_per_em: f32,
96    /// Font-unit metrics, y-up (ascent positive, descent negative).
97    ascent: f32,
98    descent: f32,
99    line_gap: f32,
100    /// Union of all glyph ink, font units y-up — Skia's fXMin/fXMax family.
101    bounds: Option<(f32, f32, f32, f32)>,
102    /// (offset from baseline, thickness) in font units, when the font says.
103    underline: Option<(f32, f32)>,
104    strikeout: Option<(f32, f32)>,
105}
106
107impl std::fmt::Debug for Font {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.debug_struct("Font")
110            .field("uid", &self.uid)
111            .field("family", &self.family)
112            .field("attrs", &self.attrs)
113            .finish_non_exhaustive()
114    }
115}
116
117impl Font {
118    fn parse(
119        family: &str,
120        attrs: FontAttrs,
121        data: FontData,
122        face_index: u32,
123        variation_coordinates: Vec<([u8; 4], f32)>,
124    ) -> Option<Self> {
125        let shared = SharedFace::parse(data, face_index)?;
126        Self::at_coordinates(shared, family, attrs, variation_coordinates)
127    }
128
129    /// One instance over already-parsed shared state — metrics and shaping
130    /// instance data are the only per-instance work.
131    fn at_coordinates(
132        shared: Arc<SharedFace>,
133        family: &str,
134        attrs: FontAttrs,
135        variation_coordinates: Vec<([u8; 4], f32)>,
136    ) -> Option<Self> {
137        let bytes: &[u8] = (*shared.data).as_ref();
138        let font = skrifa::FontRef::from_index(bytes, shared.face_index).ok()?;
139        let variation_location = font.axes().location(
140            variation_coordinates
141                .iter()
142                .map(|(tag, value)| (skrifa::Tag::new(tag), *value)),
143        );
144        let metrics = Metrics::new(&font, Size::unscaled(), &variation_location);
145        let shaper_instance = (!variation_coordinates.is_empty()).then(|| {
146            let harf = harfrust::FontRef::from_index(bytes, shared.face_index).ok();
147            harf.map(|harf| {
148                harfrust::ShaperInstance::from_variations(
149                    &harf,
150                    variation_coordinates
151                        .iter()
152                        .map(|(tag, value)| harfrust::Variation {
153                            tag: harfrust::Tag::new(tag),
154                            value: *value,
155                        }),
156                )
157            })
158        });
159        Some(Self {
160            uid: FontUid::next(),
161            family: family.to_owned(),
162            aliases: Vec::new(),
163            attrs,
164            units_per_em: metrics.units_per_em as f32,
165            ascent: metrics.ascent,
166            descent: metrics.descent,
167            line_gap: metrics.leading,
168            bounds: metrics.bounds.map(|b| (b.x_min, b.y_min, b.x_max, b.y_max)),
169            underline: metrics.underline.map(|d| (d.offset, d.thickness)),
170            strikeout: metrics.strikeout.map(|d| (d.offset, d.thickness)),
171            shared,
172            variation_coordinates,
173            variation_location,
174            shaper_instance: shaper_instance.flatten(),
175        })
176    }
177
178    /// The raw file bytes — possibly a whole .ttc collection; pair every
179    /// face view with [`Self::face_index`].
180    pub fn data(&self) -> &[u8] {
181        (*self.shared.data).as_ref()
182    }
183
184    /// Which face of [`Self::data`] this font is.
185    pub fn face_index(&self) -> u32 {
186        self.shared.face_index
187    }
188
189    /// User-space variation coordinates ((axis tag, value)); empty for the
190    /// default instance and for static fonts.
191    pub fn variation_coordinates(&self) -> &[([u8; 4], f32)] {
192        &self.variation_coordinates
193    }
194
195    pub(crate) fn variation_location(&self) -> &skrifa::instance::Location {
196        &self.variation_location
197    }
198
199    pub(crate) fn shaper_instance(&self) -> Option<&harfrust::ShaperInstance> {
200        self.shaper_instance.as_ref()
201    }
202
203    /// The instance's stable raster identity.
204    pub fn uid(&self) -> FontUid {
205        self.uid
206    }
207
208    pub fn family(&self) -> &str {
209        &self.family
210    }
211
212    pub fn aliases(&self) -> &[String] {
213        &self.aliases
214    }
215
216    /// Does this face answer to `name`? (its family or any alias —
217    /// ASCII-case-insensitively, the CSS and platform-manager behavior)
218    pub fn matches(&self, name: &str) -> bool {
219        self.family.eq_ignore_ascii_case(name)
220            || self.aliases.iter().any(|a| a.eq_ignore_ascii_case(name))
221    }
222
223    /// Register one more name for this face (no-op if already answered).
224    pub fn add_alias(&mut self, name: &str) {
225        if !self.matches(name) {
226            self.aliases.push(name.to_owned());
227        }
228    }
229
230    pub fn attrs(&self) -> FontAttrs {
231        self.attrs
232    }
233
234    /// Ascent in px at `size` (positive, above the baseline).
235    pub fn ascent_px(&self, size: f32) -> f32 {
236        self.ascent * size / self.units_per_em
237    }
238
239    /// Descent in px at `size` (positive, below the baseline).
240    pub fn descent_px(&self, size: f32) -> f32 {
241        -self.descent * size / self.units_per_em
242    }
243
244    /// Default line height in px at `size`.
245    pub fn line_height_px(&self, size: f32) -> f32 {
246        (self.ascent - self.descent + self.line_gap) * size / self.units_per_em
247    }
248
249    pub fn units_per_em(&self) -> f32 {
250        self.units_per_em
251    }
252
253    /// The font-wide ink box at `size`, y-UP around the glyph origin:
254    /// (x_min, y_min, x_max, y_max). Any glyph's ink fits inside — the
255    /// cheap way to bound italic overhang and mark excursions per run.
256    pub fn ink_box_px(&self, size: f32) -> Option<(f32, f32, f32, f32)> {
257        let k = size / self.units_per_em;
258        self.bounds
259            .map(|(x0, y0, x1, y1)| (x0 * k, y0 * k, x1 * k, y1 * k))
260    }
261
262    pub fn covers(&self, ch: char) -> bool {
263        self.shared.charmap.contains_key(&(ch as u32))
264    }
265
266    pub fn glyph_for(&self, ch: char) -> Option<u32> {
267        self.shared.charmap.get(&(ch as u32)).copied()
268    }
269
270    pub(crate) fn shaper_data(&self) -> &harfrust::ShaperData {
271        &self.shared.shaper_data
272    }
273
274    /// Underline (offset below baseline, thickness) in px at `size`, with
275    /// conventional fallbacks when the font omits the post-table values.
276    pub fn underline_px(&self, size: f32) -> (f32, f32) {
277        self.decoration_px(self.underline, size, -0.1, 0.05)
278    }
279
280    /// Strikeout (offset ABOVE baseline, thickness) in px at `size`.
281    pub fn strikeout_px(&self, size: f32) -> (f32, f32) {
282        self.decoration_px(self.strikeout, size, 0.3, 0.05)
283    }
284
285    fn decoration_px(
286        &self,
287        metric: Option<(f32, f32)>,
288        size: f32,
289        default_offset: f32,
290        default_thickness: f32,
291    ) -> (f32, f32) {
292        match metric {
293            // Font units are y-up: positive offsets sit above the baseline.
294            Some((offset, thickness)) => (
295                offset * size / self.units_per_em,
296                (thickness * size / self.units_per_em).max(0.5),
297            ),
298            None => (size * default_offset, (size * default_thickness).max(0.5)),
299        }
300    }
301}
302
303/// What shaping could not resolve — the host's font-loading demand signal
304/// (Flutter web's missing-font detection reshaped as an API).
305/// `families`: requested names the collection has NO face for at all.
306/// `codepoints`: chars NO present face covers, each with the attrs of the
307/// span that wanted it — a bold span's missing glyph should be answered
308/// with a bold face (for subset-chunk families this doubles as "which
309/// chunk is missing"). valo detects; the host owns the loading policy —
310/// Google, a mirror, bundled files, the OS, anything.
311#[derive(Clone, Debug, Default, PartialEq)]
312pub struct FontDemand {
313    pub families: Vec<String>,
314    pub codepoints: Vec<(char, FontAttrs)>,
315}
316
317impl FontDemand {
318    pub fn is_empty(&self) -> bool {
319        self.families.is_empty() && self.codepoints.is_empty()
320    }
321
322    pub(crate) fn add_family(&mut self, name: &str) {
323        if !self.families.iter().any(|f| f == name) {
324            self.families.push(name.to_owned());
325        }
326    }
327
328    pub(crate) fn add_codepoint(&mut self, ch: char, attrs: FontAttrs) {
329        if !self.codepoints.contains(&(ch, attrs)) {
330            self.codepoints.push((ch, attrs));
331        }
332    }
333}
334
335/// Somewhere fonts can come FROM when the collection lacks them — the
336/// pluggable half of the demand loop. Implementations only
337/// locate and parse (an installed-fonts scan today; a platform-native
338/// CoreText/DirectWrite lookup can replace it without touching policy);
339/// the growth policy itself lives in [`FaceSet::grown_by`].
340pub trait FontSource {
341    /// Every face answering to `name` (all weights and styles — the
342    /// collection's nearest-variant matching picks per span).
343    fn family(&mut self, name: &str) -> Vec<Font>;
344
345    /// One face covering `codepoint`, nearest to `attrs`.
346    fn face_for_codepoint(&mut self, codepoint: char, attrs: FontAttrs) -> Option<Font>;
347}
348
349/// The host's registered fonts: families for styles to name, plus a global
350/// fallback chain consulted per character. Immutable once built —
351/// register everything, then `Arc` it for builders and the renderer.
352#[derive(Default, Clone)]
353pub struct FaceSet {
354    /// `Arc` per face: adding a font clones N pointers, never re-parses
355    /// (Skia registers typefaces incrementally).
356    fonts: Vec<Arc<Font>>,
357    fallbacks: Vec<FontId>,
358}
359
360impl FaceSet {
361    pub fn new() -> Self {
362        Self::default()
363    }
364
365    /// Register font bytes under a family name (regular weight/style).
366    /// Returns `None` when the bytes don't parse as a font.
367    pub fn register(&mut self, family: &str, bytes: Vec<u8>) -> Option<FontId> {
368        self.register_with(family, FontAttrs::default(), bytes)
369    }
370
371    /// Register one variant of a family — `resolve` picks the nearest
372    /// weight with a matching style (CSS §5.2, simplified: style first,
373    /// then minimal weight distance, ties to the first registered).
374    pub fn register_with(
375        &mut self,
376        family: &str,
377        attrs: FontAttrs,
378        bytes: Vec<u8>,
379    ) -> Option<FontId> {
380        let data = unwrapped(Arc::new(bytes))?;
381        let font = Font::parse(family, attrs, data, 0, Vec::new())?;
382        Some(self.add(font))
383    }
384
385    /// Add an already-parsed [`Font`] under ITS OWN name/attrs — the
386    /// `SkTypeface` → `registerTypeface` shape.
387    pub fn add(&mut self, font: Font) -> FontId {
388        self.fonts.push(Arc::new(font));
389        FontId(self.fonts.len() as u32 - 1)
390    }
391
392    /// A new collection = this one + `font`. Faces are shared by `Arc`, so
393    /// this is O(faces) pointer clones — no re-parsing, and every existing
394    /// holder of the old collection is untouched.
395    pub fn with_font(&self, font: Font) -> (FaceSet, FontId) {
396        let mut next = self.clone();
397        let id = next.add(font);
398        (next, id)
399    }
400
401    /// A new collection with the fallback chain REPLACED (order matters).
402    pub fn with_fallbacks(&self, fallbacks: Vec<FontId>) -> FaceSet {
403        let mut next = self.clone();
404        next.fallbacks = fallbacks;
405        next
406    }
407
408    /// Append to the global fallback chain (consulted after a style's own
409    /// families: nearest attrs among the faces covering the character,
410    /// ties in chain order).
411    pub fn add_fallback(&mut self, id: FontId) {
412        self.fallbacks.push(id);
413    }
414
415    /// Grow this collection to answer `demand` from `source`: demanded
416    /// families register under their own names PLUS the demanded name as
417    /// an alias (a localized or differently-spelled request must match on
418    /// the next layout, or a loop around this call could demand forever);
419    /// codepoints still uncovered afterwards extend the fallback chain
420    /// with a face matching the demanding span's attrs. `Some(grown)`
421    /// only when something new was found — the caller's signal to
422    /// re-register the collection and lay out again.
423    /// Grow a COPY of this face set to answer `demand` from `source` — the
424    /// out-of-band path (a host that already knows what it wants). Live
425    /// resolution goes through [`FontCollection`], which owns its sources.
426    pub fn grown_by(&self, source: &mut dyn FontSource, demand: &FontDemand) -> Option<FaceSet> {
427        let mut next = self.clone();
428        let mut grew = false;
429        for name in &demand.families {
430            if next.family(name).is_some() {
431                // Answered already (a stale demand) — resolution matches
432                // it now; asking the source again would duplicate faces.
433                continue;
434            }
435            grew |= next.register_answers(source.family(name), name);
436        }
437        for &(codepoint, attrs) in &demand.codepoints {
438            grew |= next.register_fallback_answer(source, codepoint, attrs);
439        }
440        grew.then_some(next)
441    }
442
443    fn register_answers(&mut self, faces: Vec<Font>, requested_name: &str) -> bool {
444        let mut added = false;
445        for mut font in faces {
446            font.add_alias(requested_name);
447            self.add(font);
448            added = true;
449        }
450        added
451    }
452
453    fn register_fallback_answer(
454        &mut self,
455        source: &mut dyn FontSource,
456        codepoint: char,
457        attrs: FontAttrs,
458    ) -> bool {
459        if is_private_use(codepoint) {
460            // Icon fonts resolve by their REGISTERED family name; a
461            // generic source "covering" the Private Use Area paints some
462            // vendor's glyphs where tofu is the honest render.
463            return false;
464        }
465        if self.covers_anywhere(codepoint) {
466            // A family registered moments ago (or the host) already covers
467            // it — resolution will find that face without a new fallback.
468            return false;
469        }
470        let Some(font) = source.face_for_codepoint(codepoint, attrs) else {
471            return false;
472        };
473        let id = self.add(font);
474        self.add_fallback(id);
475        true
476    }
477
478    fn covers_anywhere(&self, codepoint: char) -> bool {
479        self.fonts.iter().any(|font| font.covers(codepoint))
480    }
481
482    /// True until the first `add`/`register` — building paragraphs against
483    /// an empty collection is a contract violation (`resolve` asserts).
484    pub fn is_empty(&self) -> bool {
485        self.fonts.is_empty()
486    }
487
488    /// Faces registered so far. Ids are append-only, so a holder of an older
489    /// collection can name the faces added since: `old.len()..new.len()`.
490    pub fn len(&self) -> usize {
491        self.fonts.len()
492    }
493
494    /// The shared instance behind `id` — what glyph runs carry to the
495    /// renderer (Skia: blobs hold `sk_sp<SkTypeface>`).
496    pub fn get_arc(&self, id: FontId) -> Arc<Font> {
497        self.fonts[id.0 as usize].clone()
498    }
499
500    pub fn get(&self, id: FontId) -> &Font {
501        &self.fonts[id.0 as usize]
502    }
503
504    pub fn family(&self, name: &str) -> Option<FontId> {
505        let at = self.fonts.iter().position(|f| f.matches(name))?;
506        Some(FontId(at as u32))
507    }
508
509    /// Ids of EVERY face answering to `name`, in registration order. Subset
510    /// families (css2/cn-font-split unicode-range chunks) register many
511    /// faces under one name with disjoint coverage — a fallback chain built
512    /// from [`Self::family`] alone reaches only the first-loaded chunk, so
513    /// hosts expand fallback names with this.
514    pub fn faces<'a>(&'a self, name: &'a str) -> impl Iterator<Item = FontId> + 'a {
515        self.variants(name).map(|(id, _)| id)
516    }
517
518    /// The family variant nearest `attrs`: matching style wins, then the
519    /// smallest weight distance (ties to the first registered).
520    pub fn family_variant(&self, name: &str, attrs: FontAttrs) -> Option<FontId> {
521        self.nearest(self.variants(name), attrs)
522    }
523
524    /// The font that renders `ch` for a style: per requested family, the
525    /// nearest variant that COVERS `ch` — subset families (cn-font-split
526    /// chunks) carry one unicode range per face, so coverage must look past
527    /// the best-attrs face. Then the fallback chain, else the first
528    /// candidate.
529    pub fn resolve(&self, families: &[String], attrs: FontAttrs, ch: char) -> FontId {
530        self.resolve_covered(families, attrs, ch).0
531    }
532
533    /// [`Self::resolve`] plus whether ANYTHING actually covers `ch` — false
534    /// means the returned face will shape `.notdef`. The demand signal:
535    /// callers report uncovered chars to the host, which
536    /// decides where fonts come from — valo only detects.
537    pub fn resolve_covered(
538        &self,
539        families: &[String],
540        attrs: FontAttrs,
541        ch: char,
542    ) -> (FontId, bool) {
543        for name in families {
544            let covering = self.variants(name).filter(|(_, f)| f.covers(ch));
545            if let Some(id) = self.nearest(covering, attrs) {
546                return (id, true);
547            }
548        }
549        let covering_fallbacks = self
550            .fallbacks
551            .iter()
552            .map(|id| (*id, self.get(*id)))
553            .filter(|(_, f)| f.covers(ch));
554        if let Some(id) = self.nearest(covering_fallbacks, attrs) {
555            return (id, true);
556        }
557        (self.tofu_face(families, attrs), false)
558    }
559
560    /// Every face answering to `name`, with its id.
561    fn variants<'a>(&'a self, name: &'a str) -> impl Iterator<Item = (FontId, &'a Font)> {
562        self.fonts
563            .iter()
564            .enumerate()
565            .filter(move |(_, f)| f.matches(name))
566            .map(|(at, f)| (FontId(at as u32), f.as_ref()))
567    }
568
569    /// CSS-style nearest among `faces`: width first, then matching style,
570    /// then smallest weight distance, ties to the first registered. That is
571    /// the CSS font-matching precedence; the distances themselves stay plain
572    /// absolute differences rather than CSS's directional walk.
573    fn nearest<'a>(
574        &self,
575        faces: impl Iterator<Item = (FontId, &'a Font)>,
576        attrs: FontAttrs,
577    ) -> Option<FontId> {
578        faces
579            .min_by_key(|(_, f)| {
580                (
581                    stretch_distance(f.attrs.stretch, attrs.stretch),
582                    f.attrs.italic != attrs.italic,
583                    f.attrs.weight.abs_diff(attrs.weight),
584                )
585            })
586            .map(|(id, _)| id)
587    }
588
589    /// Nothing covers `ch`: the style's best variant, else the first
590    /// fallback, else the first face — the tofu renders in SOMETHING.
591    fn tofu_face(&self, families: &[String], attrs: FontAttrs) -> FontId {
592        self.tofu_face_opt(families, attrs).unwrap_or_else(|| {
593            panic!(
594                "FontCollection has no fonts registered — register() one before building paragraphs"
595            );
596        })
597    }
598
599    /// [`Self::tofu_face`] that reports an EMPTY collection instead of
600    /// panicking — the `build_with` path, where an empty start is valid
601    /// (the chain is asked first; a char no source can render is skipped
602    /// and reported through the demand).
603    pub(crate) fn tofu_face_opt(&self, families: &[String], attrs: FontAttrs) -> Option<FontId> {
604        families
605            .iter()
606            .find_map(|name| self.family_variant(name, attrs))
607            .or_else(|| self.fallbacks.first().copied())
608            .or_else(|| (!self.fonts.is_empty()).then_some(FontId(0)))
609    }
610
611    /// [`Self::resolve_covered`] that can also report "no face exists at
612    /// all" (empty collection on the `build_with` path).
613    pub(crate) fn resolve_covered_opt(
614        &self,
615        families: &[String],
616        attrs: FontAttrs,
617        ch: char,
618    ) -> Option<(FontId, bool)> {
619        if self.fonts.is_empty() {
620            return None;
621        }
622        Some(self.resolve_covered(families, attrs, ch))
623    }
624}
625
626impl Font {
627    /// Parse a font file into a queryable object: family and weight/style
628    /// come from the file's own tables (Skia's `SkTypeface::MakeFromData`
629    /// shape — parse once, inspect, then `FontCollection::add`). Every
630    /// localized family name becomes an alias (fontdb keeps them all —
631    /// documents reference `优设标题黑` as readily as `YouSheBiaoTiHei`).
632    pub fn from_bytes(bytes: Vec<u8>) -> Option<Font> {
633        Self::from_data(Arc::new(bytes), 0)
634    }
635
636    /// [`Self::from_bytes`] for shared or memory-mapped bytes and for
637    /// collection files: `face_index` picks the face inside a .ttc (0 for
638    /// single-face files).
639    pub fn from_data(data: FontData, face_index: u32) -> Option<Font> {
640        let data = unwrapped(data)?;
641        Self::instance(data, face_index, Vec::new())
642    }
643
644    /// Every face a file offers for registration: a static font is itself;
645    /// a variable font is its NAMED INSTANCES (fvar), each a [`Font`] with
646    /// the instance's attrs and coordinates — nearest-variant matching
647    /// then picks weights exactly like a static multi-weight family.
648    pub fn instances_from_data(data: FontData, face_index: u32) -> Vec<Font> {
649        let Some(data) = unwrapped(data) else {
650            return Vec::new();
651        };
652        let instances = named_instance_coordinates((*data).as_ref(), face_index);
653        if instances.is_empty() {
654            return Self::instance(data, face_index, Vec::new())
655                .into_iter()
656                .collect();
657        }
658        // The expensive halves (cmap, shaping caches) parse ONCE; each
659        // instance adds only its metrics and axis data.
660        let Some(shared) = SharedFace::parse(data, face_index) else {
661            return Vec::new();
662        };
663        instances
664            .into_iter()
665            .filter_map(|coordinates| Self::shared_instance(shared.clone(), coordinates))
666            .collect()
667    }
668
669    /// One face at one set of coordinates, self-described from its tables
670    /// (attrs overridden by the coordinates' weight/italic axes).
671    fn instance(data: FontData, face_index: u32, coordinates: Vec<([u8; 4], f32)>) -> Option<Font> {
672        let shared = SharedFace::parse(data, face_index)?;
673        Self::shared_instance(shared, coordinates)
674    }
675
676    /// [`Self::instance`] over already-shared file state.
677    fn shared_instance(shared: Arc<SharedFace>, coordinates: Vec<([u8; 4], f32)>) -> Option<Font> {
678        let bytes: &[u8] = (*shared.data).as_ref();
679        let (family, aliases) = embedded_names(bytes, shared.face_index)?;
680        let attrs = instance_attrs(embedded_attrs(bytes, shared.face_index), &coordinates);
681        let mut font = Font::at_coordinates(shared, &family, attrs, coordinates)?;
682        for name in &aliases {
683            font.add_alias(name);
684        }
685        Some(font)
686    }
687}
688
689impl SharedFace {
690    fn parse(data: FontData, face_index: u32) -> Option<Arc<Self>> {
691        let bytes: &[u8] = (*data).as_ref();
692        let font = skrifa::FontRef::from_index(bytes, face_index).ok()?;
693        let charmap = font
694            .charmap()
695            .mappings()
696            .map(|(code, glyph)| (code, glyph.to_u32()))
697            .collect();
698        let harf = harfrust::FontRef::from_index(bytes, face_index).ok()?;
699        let shaper_data = harfrust::ShaperData::new(&harf);
700        Some(Arc::new(Self {
701            data,
702            face_index,
703            charmap,
704            shaper_data,
705        }))
706    }
707}
708
709/// The (tag, value) coordinate rows of every fvar named instance.
710fn named_instance_coordinates(bytes: &[u8], face_index: u32) -> Vec<Vec<([u8; 4], f32)>> {
711    let Ok(font) = skrifa::FontRef::from_index(bytes, face_index) else {
712        return Vec::new();
713    };
714    let axis_tags: Vec<[u8; 4]> = font
715        .axes()
716        .iter()
717        .map(|axis| axis.tag().to_be_bytes())
718        .collect();
719    font.named_instances()
720        .iter()
721        .map(|instance| {
722            axis_tags
723                .iter()
724                .copied()
725                .zip(instance.user_coords())
726                .collect()
727        })
728        .collect()
729}
730
731/// A named instance's place in its family: the weight/italic/width axes
732/// override what the file's default-instance OS/2 table says.
733fn instance_attrs(base: FontAttrs, coordinates: &[([u8; 4], f32)]) -> FontAttrs {
734    let mut attrs = base;
735    for (tag, value) in coordinates {
736        match tag {
737            b"wght" => attrs.weight = value.clamp(1.0, 1000.0) as u16,
738            b"ital" => attrs.italic = *value >= 0.5,
739            b"slnt" => attrs.italic = attrs.italic || *value < 0.0,
740            // `wdth` is already a percentage, which is what CSS asks for.
741            b"wdth" => attrs.stretch = value.clamp(1.0, 1000.0),
742            _ => {}
743        }
744    }
745    attrs
746}
747
748/// Ordered width distance for variant matching. Quantized to 1/16 of a
749/// percent so it can be an integer sort key without collapsing the named
750/// widths (they are 12.5 apart at the closest).
751fn stretch_distance(candidate: f32, wanted: f32) -> u32 {
752    ((candidate - wanted).abs() * 16.0) as u32
753}
754
755/// WOFF2 arrives brotli-wrapped; faces parse the unwrapped TrueType bytes
756/// (icon and web fonts ship compressed — the CoreText managers accept
757/// them, so registration here does too). Identity for everything else.
758#[cfg(feature = "woff2")]
759fn unwrapped(data: FontData) -> Option<FontData> {
760    let bytes: &[u8] = (*data).as_ref();
761    if !woff2_patched::decode::is_woff2(bytes) {
762        return Some(data);
763    }
764    let unpacked = woff2_patched::decode::convert_woff2_to_ttf(&mut &bytes[..]).ok()?;
765    Some(Arc::new(unpacked))
766}
767
768#[cfg(not(feature = "woff2"))]
769fn unwrapped(data: FontData) -> Option<FontData> {
770    Some(data)
771}
772
773/// name table: the primary is the en typographic family (then en family,
774/// then any-language); every other family/typographic-family string rides
775/// along as an alias.
776fn embedded_names(data: &[u8], face_index: u32) -> Option<(String, Vec<String>)> {
777    use swash::StringId;
778    let font = swash::FontRef::from_index(data, face_index as usize)?;
779    let strings = font.localized_strings();
780    let pick = |id: StringId| {
781        strings
782            .find_by_id(id, Some("en"))
783            .or_else(|| strings.find_by_id(id, None))
784            .map(|s| s.to_string())
785    };
786    let primary = pick(StringId::TypographicFamily).or_else(|| pick(StringId::Family))?;
787    let aliases = strings
788        .filter(|s| matches!(s.id(), StringId::Family | StringId::TypographicFamily))
789        .map(|s| s.to_string())
790        .filter(|name| *name != primary)
791        .collect();
792    Some((primary, aliases))
793}
794
795/// OS/2 weight + width + style flags via swash attributes.
796fn embedded_attrs(data: &[u8], face_index: u32) -> FontAttrs {
797    let Some(font) = swash::FontRef::from_index(data, face_index as usize) else {
798        return FontAttrs::default();
799    };
800    let attrs = font.attributes();
801    FontAttrs {
802        weight: attrs.weight().0,
803        italic: attrs.style() != swash::Style::Normal,
804        stretch: attrs.stretch().to_percentage(),
805    }
806}
807
808/// Unicode Private Use Areas — codepoints whose meaning belongs to a
809/// specific registered font, never to a generic fallback source.
810fn is_private_use(codepoint: char) -> bool {
811    matches!(
812        codepoint,
813        '\u{E000}'..='\u{F8FF}' | '\u{F0000}'..='\u{FFFFD}' | '\u{100000}'..='\u{10FFFD}'
814    )
815}
816
817/// Faces plus the sources that can find more — Skia's `FontCollection`
818/// (skparagraph FontCollection.h: the asset/dynamic/default `SkFontMgr`s
819/// live INSIDE the collection, and `findTypefaces`/`defaultFallback` are
820/// its methods). Shaping consults this at every miss; what no source can
821/// answer accumulates as the [`demand`](Self::take_unanswered) a host
822/// fetches asynchronously (Flutter web's `_unprocessedCodePoints`).
823#[derive(Default)]
824pub struct FontCollection {
825    faces: FaceSet,
826    /// Consulted in order, first answer wins (Skia's manager priority:
827    /// registered bytes and downloaders before the platform database).
828    sources: Vec<Box<dyn FontSource>>,
829    /// Misses no source answered, since the last drain.
830    unanswered: FontDemand,
831}
832
833impl FontCollection {
834    pub fn new() -> FontCollection {
835        FontCollection::default()
836    }
837
838    /// The faces resolved so far — what a built paragraph snapshots.
839    pub fn faces(&self) -> &FaceSet {
840        &self.faces
841    }
842
843    /// Registers bytes under a family (Skia `registerTypeface`; Flutter
844    /// `FontLoader.load`). Host-facing: the other half of the async loop.
845    pub fn register(&mut self, family: &str, bytes: Vec<u8>) -> Option<FontId> {
846        self.faces.register(family, bytes)
847    }
848
849    pub fn add(&mut self, font: Font) -> FontId {
850        self.faces.add(font)
851    }
852
853    pub fn add_fallback(&mut self, id: FontId) {
854        self.faces.add_fallback(id);
855    }
856
857    pub fn get(&self, id: FontId) -> &Font {
858        self.faces.get(id)
859    }
860
861    pub fn len(&self) -> usize {
862        self.faces.len()
863    }
864
865    pub fn family(&self, name: &str) -> Option<FontId> {
866        self.faces.family(name)
867    }
868
869    /// Adds a source consulted on a miss: the OS database, a downloader's
870    /// already-fetched cache, anything.
871    pub fn add_source(&mut self, source: impl FontSource + 'static) {
872        self.sources.push(Box::new(source));
873    }
874
875    /// [`add_source`](Self::add_source) for an already-boxed source — what
876    /// a platform hands the framework through a trait object.
877    pub fn add_boxed_source(&mut self, source: Box<dyn FontSource>) {
878        self.sources.push(source);
879    }
880
881    pub fn is_empty(&self) -> bool {
882        self.faces.is_empty()
883    }
884
885    /// Replaces the faces with a set grown out of band (a host that
886    /// answered a demand itself — [`FaceSet::grown_by`]).
887    pub fn adopt_faces(&mut self, faces: FaceSet) {
888        self.faces = faces;
889    }
890
891    /// Takes the misses no source could answer — the host's cue to fetch
892    /// (and later [`register`](Self::register), which invalidates the text
893    /// that wanted them). Draining is the caller's; nothing here is async.
894    pub fn take_unanswered(&mut self) -> FontDemand {
895        std::mem::take(&mut self.unanswered)
896    }
897
898    /// Resolution with growth: look up, else ask the sources in order,
899    /// registering what they answer (Skia's `findTypefaces` walking its
900    /// managers). `false` = nobody had it; the miss is recorded.
901    pub(crate) fn require_family(&mut self, name: &str) -> bool {
902        if self.faces.family(name).is_some() {
903            return true;
904        }
905        for source in &mut self.sources {
906            let faces = source.family(name);
907            if self.faces.register_answers(faces, name) {
908                return true;
909            }
910        }
911        self.unanswered.add_family(name);
912        false
913    }
914
915    /// The per-codepoint half (Skia's `defaultFallback(unicode, ..)`).
916    pub(crate) fn require_codepoint(&mut self, codepoint: char, attrs: FontAttrs) -> bool {
917        if self.faces.covers_anywhere(codepoint) {
918            return true;
919        }
920        for index in 0..self.sources.len() {
921            let (head, tail) = self.sources.split_at_mut(index);
922            let _ = head;
923            let source = &mut tail[0];
924            if self
925                .faces
926                .register_fallback_answer(source.as_mut(), codepoint, attrs)
927            {
928                return true;
929            }
930        }
931        self.unanswered.add_codepoint(codepoint, attrs);
932        false
933    }
934}