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