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 /// The faces answering to each name, keyed as [`name_key`] spells it, in
388 /// registration order. Resolution asks per character, so a name is a
389 /// lookup rather than a walk over every face comparing strings.
390 by_name: HashMap<String, Vec<FontId>>,
391}
392
393/// `StyleFaces` is one text style's font resolution, prepared by
394/// [`FaceSet::style_faces`].
395///
396/// It holds face identifiers only, ordered for [`FaceSet::resolve_prepared`];
397/// it is stale once a face is added to the set it came from.
398#[derive(Clone, Debug)]
399pub struct StyleFaces {
400 /// The requested families' faces, in request order, nearest-first.
401 families: Vec<Vec<FontId>>,
402 /// The fallback chain, nearest-first.
403 fallbacks: Vec<FontId>,
404 /// Where tofu renders when nothing covers: the style's best variant,
405 /// else the first fallback, else the first face; `None` on an empty set.
406 tofu: Option<FontId>,
407}
408
409impl FaceSet {
410 /// `new` creates an empty face set.
411 pub fn new() -> Self {
412 Self::default()
413 }
414
415 /// `register` adds font bytes under a family using default attributes.
416 ///
417 /// It returns `None` when face zero cannot be parsed.
418 pub fn register(&mut self, family: &str, bytes: Vec<u8>) -> Option<FontId> {
419 self.register_with(family, FontAttrs::default(), bytes)
420 }
421
422 /// `register_with` adds font bytes under a family with explicit attributes.
423 ///
424 /// It returns `None` when face zero cannot be parsed.
425 pub fn register_with(
426 &mut self,
427 family: &str,
428 attrs: FontAttrs,
429 bytes: Vec<u8>,
430 ) -> Option<FontId> {
431 let (data, face_index) = unwrapped(Arc::new(bytes), 0)?;
432 let font = Font::parse(family, attrs, data, face_index, Vec::new())?;
433 Some(self.add(font))
434 }
435
436 /// `add` registers an already parsed font under its embedded names and attributes.
437 pub fn add(&mut self, font: Font) -> FontId {
438 let id = FontId(self.fonts.len() as u32);
439 let aliases = font.aliases().iter().map(String::as_str);
440 for name in std::iter::once(font.family()).chain(aliases) {
441 let ids = self.by_name.entry(name_key(name)).or_default();
442 if ids.last() != Some(&id) {
443 ids.push(id);
444 }
445 }
446 self.fonts.push(Arc::new(font));
447 id
448 }
449
450 /// `with_font` returns a cloned face set containing one additional font.
451 ///
452 /// Existing fonts remain shared and are not reparsed.
453 pub fn with_font(&self, font: Font) -> (FaceSet, FontId) {
454 let mut next = self.clone();
455 let id = next.add(font);
456 (next, id)
457 }
458
459 /// `with_fallbacks` returns a clone with its global fallback order replaced.
460 ///
461 /// Every identifier must belong to this face set.
462 pub fn with_fallbacks(&self, fallbacks: Vec<FontId>) -> FaceSet {
463 let mut next = self.clone();
464 next.fallbacks = fallbacks;
465 next
466 }
467
468 /// `add_fallback` appends a registered font to the global fallback order.
469 ///
470 /// Requested families are searched before this chain. `id` must belong to
471 /// this face set.
472 pub fn add_fallback(&mut self, id: FontId) {
473 self.fallbacks.push(id);
474 }
475
476 /// `grown_by` returns a clone extended with answers from a font source.
477 ///
478 /// Requested families are also registered under the requested name.
479 /// Uncovered codepoints add matching faces to the fallback chain. It returns
480 /// `None` when the source supplies nothing new.
481 pub fn grown_by(&self, source: &mut dyn FontSource, demand: &FontDemand) -> Option<FaceSet> {
482 let mut next = self.clone();
483 let mut grew = false;
484 for name in &demand.families {
485 if next.family(name).is_some() {
486 // Answered already (a stale demand) — resolution matches
487 // it now; asking the source again would duplicate faces.
488 continue;
489 }
490 grew |= next.register_answers(source.family(name), name);
491 }
492 for &(codepoint, attrs) in &demand.codepoints {
493 grew |= next.register_fallback_answer(source, codepoint, attrs);
494 }
495 grew.then_some(next)
496 }
497
498 fn register_answers(&mut self, faces: Vec<Font>, requested_name: &str) -> bool {
499 let mut added = false;
500 for mut font in faces {
501 font.add_alias(requested_name);
502 self.add(font);
503 added = true;
504 }
505 added
506 }
507
508 fn register_fallback_answer(
509 &mut self,
510 source: &mut dyn FontSource,
511 codepoint: char,
512 attrs: FontAttrs,
513 ) -> bool {
514 if is_private_use(codepoint) {
515 // Icon fonts resolve by their REGISTERED family name; a
516 // generic source "covering" the Private Use Area paints some
517 // vendor's glyphs where tofu is the honest render.
518 return false;
519 }
520 if self.fallbacks_cover(codepoint) {
521 // Already in the chain — resolution will find that face without
522 // a second copy of it.
523 return false;
524 }
525 let Some(font) = source.face_for_codepoint(codepoint, attrs) else {
526 return false;
527 };
528 let id = self.add(font);
529 self.add_fallback(id);
530 true
531 }
532
533 /// Whether the global fallback chain covers `codepoint`.
534 ///
535 /// A fallback answer is only ever reached through this chain, so this —
536 /// not "some registered face" — decides whether another one is needed.
537 fn fallbacks_cover(&self, codepoint: char) -> bool {
538 self.fallbacks
539 .iter()
540 .any(|id| self.get(*id).covers(codepoint))
541 }
542
543 /// Whether resolution finds a face covering `ch`.
544 ///
545 /// This is the walk [`Self::resolve_covered`] makes, so a face resolution
546 /// cannot reach — registered under a name nobody asked for and absent from
547 /// the fallback chain, as an icon font is — does not pass for coverage it
548 /// will never supply. Asking "does some registered face cover it" instead
549 /// lets such a font suppress the fallback the text actually needs.
550 pub(crate) fn covered_by_resolution(
551 &self,
552 families: &[String],
553 attrs: FontAttrs,
554 ch: char,
555 ) -> bool {
556 matches!(
557 self.resolve_covered_opt(families, attrs, ch),
558 Some((_, true))
559 )
560 }
561
562 /// `is_empty` reports whether no fonts are registered.
563 pub fn is_empty(&self) -> bool {
564 self.fonts.is_empty()
565 }
566
567 /// `len` returns the number of registered fonts.
568 pub fn len(&self) -> usize {
569 self.fonts.len()
570 }
571
572 /// `get_arc` returns a shared handle to a registered font.
573 ///
574 /// # Panics
575 ///
576 /// Panics if `id` does not belong to this face set.
577 pub fn get_arc(&self, id: FontId) -> Arc<Font> {
578 self.fonts[id.0 as usize].clone()
579 }
580
581 /// `get` returns a registered font by identifier.
582 ///
583 /// # Panics
584 ///
585 /// Panics if `id` does not belong to this face set.
586 pub fn get(&self, id: FontId) -> &Font {
587 &self.fonts[id.0 as usize]
588 }
589
590 /// `family` returns the first registered font matching a family name or alias.
591 pub fn family(&self, name: &str) -> Option<FontId> {
592 self.by_name.get(&name_key(name))?.first().copied()
593 }
594
595 /// `faces` iterates over every font matching a family name or alias.
596 ///
597 /// Results follow registration order and include separate subset faces.
598 pub fn faces<'a>(&'a self, name: &'a str) -> impl Iterator<Item = FontId> + 'a {
599 self.variants(name).map(|(id, _)| id)
600 }
601
602 /// `family_variant` returns the family face nearest to requested attributes.
603 ///
604 /// Width is matched first, then italic style and weight. Registration order
605 /// breaks ties.
606 pub fn family_variant(&self, name: &str, attrs: FontAttrs) -> Option<FontId> {
607 self.nearest(self.variants(name), attrs)
608 }
609
610 /// `resolve` selects a font for a character and requested style.
611 ///
612 /// It searches requested families in order, then global fallbacks. If no
613 /// font covers the character, it returns a font suitable for rendering
614 /// `.notdef`.
615 ///
616 /// # Panics
617 ///
618 /// Panics when the face set is empty.
619 pub fn resolve(&self, families: &[String], attrs: FontAttrs, ch: char) -> FontId {
620 self.resolve_covered(families, attrs, ch).0
621 }
622
623 /// `resolve_covered` selects a font and reports whether it covers the character.
624 ///
625 /// A `false` flag means the returned font will render `.notdef`.
626 ///
627 /// # Panics
628 ///
629 /// Panics when the face set is empty.
630 pub fn resolve_covered(
631 &self,
632 families: &[String],
633 attrs: FontAttrs,
634 ch: char,
635 ) -> (FontId, bool) {
636 let faces = self.style_faces(families, attrs);
637 self.resolve_prepared(&faces, ch).unwrap_or_else(|| {
638 panic!(
639 "FontCollection has no fonts registered — register() one before building paragraphs"
640 );
641 })
642 }
643
644 /// `style_faces` prepares one style's resolution: the faces of each
645 /// requested family and of the fallback chain, nearest-first to `attrs`.
646 ///
647 /// A run of text asks for a font per character; prepared once per style,
648 /// each character is a coverage walk over these lists and no name lookup
649 /// (Skia's `SkFontCollection::findTypefaces`, done once per text style).
650 pub fn style_faces(&self, families: &[String], attrs: FontAttrs) -> StyleFaces {
651 let fallbacks = self.fallbacks.iter().map(|id| (*id, self.get(*id)));
652 StyleFaces {
653 families: families
654 .iter()
655 .map(|name| self.nearest_first(self.variants(name), attrs))
656 .collect(),
657 fallbacks: self.nearest_first(fallbacks, attrs),
658 tofu: self.tofu_face_opt(families, attrs),
659 }
660 }
661
662 /// `resolve_prepared` selects a font for `ch` from a prepared style.
663 ///
664 /// The first covering face of the first requested family that has one
665 /// wins, then the fallback chain's; the flag is `false` when nothing
666 /// covers and the returned font renders `.notdef`. `None` when the face
667 /// set is empty.
668 pub fn resolve_prepared(&self, faces: &StyleFaces, ch: char) -> Option<(FontId, bool)> {
669 let covering = faces
670 .families
671 .iter()
672 .chain(std::iter::once(&faces.fallbacks))
673 .flatten()
674 .find(|id| self.get(**id).covers(ch));
675 match covering {
676 Some(id) => Some((*id, true)),
677 None => faces.tofu.map(|id| (id, false)),
678 }
679 }
680
681 /// Every face answering to `name`, with its id, in registration order.
682 fn variants<'a>(&'a self, name: &str) -> impl Iterator<Item = (FontId, &'a Font)> + 'a {
683 self.by_name
684 .get(&name_key(name))
685 .into_iter()
686 .flatten()
687 .map(|id| (*id, self.get(*id)))
688 }
689
690 /// CSS-style nearest among `faces`: width first, then matching style,
691 /// then smallest weight distance, ties to the first registered. That is
692 /// the CSS font-matching precedence; the distances themselves stay plain
693 /// absolute differences rather than CSS's directional walk.
694 fn nearest<'a>(
695 &self,
696 faces: impl Iterator<Item = (FontId, &'a Font)>,
697 attrs: FontAttrs,
698 ) -> Option<FontId> {
699 faces
700 .min_by_key(|(_, f)| {
701 (
702 stretch_distance(f.attrs.stretch, attrs.stretch),
703 f.attrs.italic != attrs.italic,
704 f.attrs.weight.abs_diff(attrs.weight),
705 )
706 })
707 .map(|(id, _)| id)
708 }
709
710 /// `faces` in [`Self::nearest`]'s order: the face `nearest` would pick
711 /// first, and so on, with ties in registration order.
712 fn nearest_first<'a>(
713 &self,
714 faces: impl Iterator<Item = (FontId, &'a Font)>,
715 attrs: FontAttrs,
716 ) -> Vec<FontId> {
717 let mut faces: Vec<(FontId, &Font)> = faces.collect();
718 faces.sort_by_key(|(_, f)| {
719 (
720 stretch_distance(f.attrs.stretch, attrs.stretch),
721 f.attrs.italic != attrs.italic,
722 f.attrs.weight.abs_diff(attrs.weight),
723 )
724 });
725 faces.into_iter().map(|(id, _)| id).collect()
726 }
727
728 /// [`Self::tofu_face`] that reports an EMPTY collection instead of
729 /// panicking — the `build_with` path, where an empty start is valid
730 /// (the chain is asked first; a char no source can render is skipped
731 /// and reported through the demand).
732 pub(crate) fn tofu_face_opt(&self, families: &[String], attrs: FontAttrs) -> Option<FontId> {
733 families
734 .iter()
735 .find_map(|name| self.family_variant(name, attrs))
736 .or_else(|| self.fallbacks.first().copied())
737 .or_else(|| (!self.fonts.is_empty()).then_some(FontId(0)))
738 }
739
740 /// [`Self::resolve_covered`] that can also report "no face exists at
741 /// all" (empty collection on the `build_with` path).
742 pub(crate) fn resolve_covered_opt(
743 &self,
744 families: &[String],
745 attrs: FontAttrs,
746 ch: char,
747 ) -> Option<(FontId, bool)> {
748 if self.fonts.is_empty() {
749 return None;
750 }
751 Some(self.resolve_covered(families, attrs, ch))
752 }
753}
754
755/// The spelling a family name or alias is indexed under: names match
756/// ASCII-case-insensitively (as [`Font::matches`] compares them).
757fn name_key(name: &str) -> String {
758 name.to_ascii_lowercase()
759}
760
761impl Font {
762 /// `from_bytes` parses face zero from owned font-file bytes.
763 ///
764 /// Family names and attributes come from the font. Localized family names
765 /// become aliases. It returns `None` when the bytes cannot be parsed.
766 pub fn from_bytes(bytes: Vec<u8>) -> Option<Font> {
767 Self::from_data(Arc::new(bytes), 0)
768 }
769
770 /// `from_data` parses one face from shared font-file storage.
771 ///
772 /// Use face index zero for a single-face file. It returns `None` when the
773 /// index or font data is invalid.
774 pub fn from_data(data: FontData, face_index: u32) -> Option<Font> {
775 let (data, face_index) = unwrapped(data, face_index)?;
776 Self::instance(data, face_index, Vec::new())
777 }
778
779 /// `instances_from_data` parses the registrable instances of one face.
780 ///
781 /// Static fonts produce one item. Variable fonts with named instances
782 /// produce one [`Font`] per instance. Invalid data returns an empty vector.
783 pub fn instances_from_data(data: FontData, face_index: u32) -> Vec<Font> {
784 let Some((data, face_index)) = unwrapped(data, face_index) else {
785 return Vec::new();
786 };
787 let instances = named_instance_coordinates((*data).as_ref(), face_index);
788 if instances.is_empty() {
789 return Self::instance(data, face_index, Vec::new())
790 .into_iter()
791 .collect();
792 }
793 // The expensive halves (cmap, shaping caches) parse ONCE; each
794 // instance adds only its metrics and axis data.
795 let Some(shared) = SharedFace::parse(data, face_index) else {
796 return Vec::new();
797 };
798 instances
799 .into_iter()
800 .filter_map(|coordinates| Self::shared_instance(shared.clone(), coordinates))
801 .collect()
802 }
803
804 /// One face at one set of coordinates, self-described from its tables
805 /// (attrs overridden by the coordinates' weight/italic axes).
806 fn instance(data: FontData, face_index: u32, coordinates: Vec<([u8; 4], f32)>) -> Option<Font> {
807 let shared = SharedFace::parse(data, face_index)?;
808 Self::shared_instance(shared, coordinates)
809 }
810
811 /// [`Self::instance`] over already-shared file state.
812 fn shared_instance(shared: Arc<SharedFace>, coordinates: Vec<([u8; 4], f32)>) -> Option<Font> {
813 let bytes: &[u8] = (*shared.data).as_ref();
814 let (family, aliases) = embedded_names(bytes, shared.face_index)?;
815 let attrs = instance_attrs(embedded_attrs(bytes, shared.face_index), &coordinates);
816 let mut font = Font::at_coordinates(shared, &family, attrs, coordinates)?;
817 for name in &aliases {
818 font.add_alias(name);
819 }
820 Some(font)
821 }
822}
823
824impl SharedFace {
825 fn parse(data: FontData, face_index: u32) -> Option<Arc<Self>> {
826 let bytes: &[u8] = (*data).as_ref();
827 let font = skrifa::FontRef::from_index(bytes, face_index).ok()?;
828 let charmap = font
829 .charmap()
830 .mappings()
831 .map(|(code, glyph)| (code, glyph.to_u32()))
832 .collect();
833 let harf = harfrust::FontRef::from_index(bytes, face_index).ok()?;
834 let shaper_data = harfrust::ShaperData::new(&harf);
835 Some(Arc::new(Self {
836 data,
837 face_index,
838 charmap,
839 shaper_data,
840 }))
841 }
842}
843
844/// The (tag, value) coordinate rows of every fvar named instance.
845fn named_instance_coordinates(bytes: &[u8], face_index: u32) -> Vec<Vec<([u8; 4], f32)>> {
846 let Ok(font) = skrifa::FontRef::from_index(bytes, face_index) else {
847 return Vec::new();
848 };
849 let axis_tags: Vec<[u8; 4]> = font
850 .axes()
851 .iter()
852 .map(|axis| axis.tag().to_be_bytes())
853 .collect();
854 font.named_instances()
855 .iter()
856 .map(|instance| {
857 axis_tags
858 .iter()
859 .copied()
860 .zip(instance.user_coords())
861 .collect()
862 })
863 .collect()
864}
865
866/// A named instance's place in its family: the weight/italic/width axes
867/// override what the file's default-instance OS/2 table says.
868fn instance_attrs(base: FontAttrs, coordinates: &[([u8; 4], f32)]) -> FontAttrs {
869 let mut attrs = base;
870 for (tag, value) in coordinates {
871 match tag {
872 b"wght" => attrs.weight = value.clamp(1.0, 1000.0) as u16,
873 b"ital" => attrs.italic = *value >= 0.5,
874 b"slnt" => attrs.italic = attrs.italic || *value < 0.0,
875 // `wdth` is already a percentage, which is what CSS asks for.
876 b"wdth" => attrs.stretch = value.clamp(1.0, 1000.0),
877 _ => {}
878 }
879 }
880 attrs
881}
882
883/// Ordered width distance for variant matching. Quantized to 1/16 of a
884/// percent so it can be an integer sort key without collapsing the named
885/// widths (they are 12.5 apart at the closest).
886fn stretch_distance(candidate: f32, wanted: f32) -> u32 {
887 ((candidate - wanted).abs() * 16.0) as u32
888}
889
890/// WOFF2 arrives brotli-wrapped; faces parse the unwrapped TrueType bytes
891/// (icon and web fonts ship compressed — the CoreText managers accept
892/// them, so registration here does too). Identity for everything else.
893/// A collection face is emitted as its own single-face SFNT, so the
894/// returned index is always zero after unwrap.
895#[cfg(feature = "woff2")]
896fn unwrapped(data: FontData, face_index: u32) -> Option<(FontData, u32)> {
897 let bytes: &[u8] = (*data).as_ref();
898 if !bytes.starts_with(b"wOF2") {
899 return Some((data, face_index));
900 }
901 let unpacked = crate::woff2::to_sfnt(bytes, face_index)?;
902 Some((Arc::new(unpacked), 0))
903}
904
905#[cfg(not(feature = "woff2"))]
906fn unwrapped(data: FontData, face_index: u32) -> Option<(FontData, u32)> {
907 Some((data, face_index))
908}
909
910/// name table: the primary is the en typographic family (then en family,
911/// then any-language); every other family/typographic-family string rides
912/// along as an alias.
913fn embedded_names(data: &[u8], face_index: u32) -> Option<(String, Vec<String>)> {
914 use swash::StringId;
915 let font = swash::FontRef::from_index(data, face_index as usize)?;
916 let strings = font.localized_strings();
917 let pick = |id: StringId| {
918 strings
919 .find_by_id(id, Some("en"))
920 .or_else(|| strings.find_by_id(id, None))
921 .map(|s| s.to_string())
922 };
923 let primary = pick(StringId::TypographicFamily).or_else(|| pick(StringId::Family))?;
924 let aliases = strings
925 .filter(|s| matches!(s.id(), StringId::Family | StringId::TypographicFamily))
926 .map(|s| s.to_string())
927 .filter(|name| *name != primary)
928 .collect();
929 Some((primary, aliases))
930}
931
932/// OS/2 weight + width + style flags via swash attributes.
933fn embedded_attrs(data: &[u8], face_index: u32) -> FontAttrs {
934 let Some(font) = swash::FontRef::from_index(data, face_index as usize) else {
935 return FontAttrs::default();
936 };
937 let attrs = font.attributes();
938 FontAttrs {
939 weight: attrs.weight().0,
940 italic: attrs.style() != swash::Style::Normal,
941 stretch: attrs.stretch().to_percentage(),
942 }
943}
944
945/// Unicode Private Use Areas — codepoints whose meaning belongs to a
946/// specific registered font, never to a generic fallback source.
947fn is_private_use(codepoint: char) -> bool {
948 matches!(
949 codepoint,
950 '\u{E000}'..='\u{F8FF}' | '\u{F0000}'..='\u{FFFFD}' | '\u{100000}'..='\u{10FFFD}'
951 )
952}
953
954/// `FontCollection` owns registered faces and sources for resolving missing fonts.
955///
956/// Paragraph building consults sources in order and registers their answers.
957/// Unanswered requests accumulate until [`Self::take_unanswered`] is called.
958#[derive(Default)]
959pub struct FontCollection {
960 faces: FaceSet,
961 /// Consulted in order, first answer wins (Skia's manager priority:
962 /// registered bytes and downloaders before the platform database).
963 sources: Vec<Box<dyn FontSource>>,
964 /// Misses no source answered, since the last drain.
965 unanswered: FontDemand,
966}
967
968impl FontCollection {
969 /// `new` creates an empty collection with no font sources.
970 pub fn new() -> FontCollection {
971 FontCollection::default()
972 }
973
974 /// `faces` returns the faces currently registered in the collection.
975 ///
976 /// A built paragraph clones this set and remains independent of later changes.
977 pub fn faces(&self) -> &FaceSet {
978 &self.faces
979 }
980
981 /// `register` adds font bytes under a family using default attributes.
982 ///
983 /// It returns `None` when face zero cannot be parsed.
984 pub fn register(&mut self, family: &str, bytes: Vec<u8>) -> Option<FontId> {
985 self.faces.register(family, bytes)
986 }
987
988 /// `add` registers an already parsed font.
989 pub fn add(&mut self, font: Font) -> FontId {
990 self.faces.add(font)
991 }
992
993 /// `add_fallback` appends a registered font to the global fallback order.
994 ///
995 /// `id` must belong to this collection.
996 pub fn add_fallback(&mut self, id: FontId) {
997 self.faces.add_fallback(id);
998 }
999
1000 /// `get` returns a registered font by identifier.
1001 ///
1002 /// # Panics
1003 ///
1004 /// Panics if `id` does not belong to this collection.
1005 pub fn get(&self, id: FontId) -> &Font {
1006 self.faces.get(id)
1007 }
1008
1009 /// `len` returns the number of registered fonts.
1010 pub fn len(&self) -> usize {
1011 self.faces.len()
1012 }
1013
1014 /// `family` returns the first font matching a family name or alias.
1015 pub fn family(&self, name: &str) -> Option<FontId> {
1016 self.faces.family(name)
1017 }
1018
1019 /// `add_source` appends a source consulted when registered fonts cannot satisfy a request.
1020 pub fn add_source(&mut self, source: impl FontSource + 'static) {
1021 self.sources.push(Box::new(source));
1022 }
1023
1024 /// `add_boxed_source` appends an already boxed font source.
1025 pub fn add_boxed_source(&mut self, source: Box<dyn FontSource>) {
1026 self.sources.push(source);
1027 }
1028
1029 /// `is_empty` reports whether no fonts are currently registered.
1030 pub fn is_empty(&self) -> bool {
1031 self.faces.is_empty()
1032 }
1033
1034 /// `adopt_faces` replaces the registered faces with a prepared set.
1035 ///
1036 /// This supports hosts that answer [`FontDemand`] using [`FaceSet::grown_by`].
1037 pub fn adopt_faces(&mut self, faces: FaceSet) {
1038 self.faces = faces;
1039 }
1040
1041 /// `take_unanswered` drains font requests that no source could satisfy.
1042 ///
1043 /// Hosts may load and [`Self::register`] matching fonts before rebuilding
1044 /// affected paragraphs.
1045 pub fn take_unanswered(&mut self) -> FontDemand {
1046 std::mem::take(&mut self.unanswered)
1047 }
1048
1049 /// Resolution with growth: look up, else ask the sources in order,
1050 /// registering what they answer (Skia's `findTypefaces` walking its
1051 /// managers). `false` = nobody had it; the miss is recorded.
1052 pub(crate) fn require_family(&mut self, name: &str) -> bool {
1053 if self.faces.family(name).is_some() {
1054 return true;
1055 }
1056 for source in &mut self.sources {
1057 let faces = source.family(name);
1058 if self.faces.register_answers(faces, name) {
1059 return true;
1060 }
1061 }
1062 self.unanswered.add_family(name);
1063 false
1064 }
1065
1066 /// The per-codepoint half (Skia's `defaultFallback(unicode, ..)`).
1067 pub(crate) fn require_codepoint(
1068 &mut self,
1069 families: &[String],
1070 codepoint: char,
1071 attrs: FontAttrs,
1072 ) -> bool {
1073 if self.faces.covered_by_resolution(families, attrs, codepoint) {
1074 return true;
1075 }
1076 for index in 0..self.sources.len() {
1077 let (head, tail) = self.sources.split_at_mut(index);
1078 let _ = head;
1079 let source = &mut tail[0];
1080 if self
1081 .faces
1082 .register_fallback_answer(source.as_mut(), codepoint, attrs)
1083 {
1084 return true;
1085 }
1086 }
1087 self.unanswered.add_codepoint(codepoint, attrs);
1088 false
1089 }
1090}