Skip to main content

cranpose_text/
typeface.rs

1use std::{
2    hash::{BuildHasher, Hash, Hasher},
3    sync::{Arc, OnceLock},
4};
5
6use foldhash::fast::FixedState;
7use read_fonts::{
8    FontRef, TableProvider,
9    model::{Blob, Font, Source, Variation, attribute::Style, metrics::GlyphExtents, name::NameId},
10    types::{Fixed, GlyphId as FontGlyphId, Tag},
11};
12use smallvec::SmallVec;
13
14use crate::{
15    coverage::Coverage,
16    font::{FontFamily, FontStyle, FontSynthesis, FontWeight},
17    glyph_id::GlyphId,
18    glyph_ink::GlyphInks,
19    simple_glyf::{self, GlyfTables},
20};
21
22/// The bytes a typeface is read from.
23pub enum FontBytes {
24    /// Bytes that live as long as the process, such as an embedded font or a
25    /// font file read once: the typeface reads them in place.
26    Static(&'static [u8]),
27    /// Bytes the typeface takes over.
28    Owned(Vec<u8>),
29}
30
31impl FontBytes {
32    /// The font file's bytes.
33    pub fn as_slice(&self) -> &[u8] {
34        match self {
35            Self::Static(bytes) => bytes,
36            Self::Owned(bytes) => bytes,
37        }
38    }
39}
40
41impl From<&'static [u8]> for FontBytes {
42    fn from(bytes: &'static [u8]) -> Self {
43        Self::Static(bytes)
44    }
45}
46
47impl<const N: usize> From<&'static [u8; N]> for FontBytes {
48    fn from(bytes: &'static [u8; N]) -> Self {
49        Self::Static(bytes)
50    }
51}
52
53impl From<Vec<u8>> for FontBytes {
54    fn from(bytes: Vec<u8>) -> Self {
55        Self::Owned(bytes)
56    }
57}
58
59/// The identity an app-supplied typeface was registered under.
60///
61/// `FontFamily::FileBacked` and `FontFamily::LoadedTypeface` name a typeface by
62/// its files rather than by a name inside the font, so resolution cannot
63/// compare strings from the `name` table. Hashing the `FontFamily` value once
64/// at registration and once per resolve keeps the two sides in step without
65/// walking path lists on every frame.
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
67pub struct FontFamilyKey(u64);
68
69impl FontFamilyKey {
70    /// Returns the key of `family`, the same for equal families.
71    pub fn of(family: &FontFamily) -> Self {
72        Self(HASH_STATE.hash_one(family))
73    }
74}
75
76/// Why bytes could not become a typeface.
77#[derive(Debug, PartialEq, thiserror::Error)]
78pub enum TypefaceError {
79    /// The bytes are not a font this crate reads.
80    #[error("the bytes are not a readable font")]
81    InvalidFont,
82    /// A variation names an axis the font lacks, or a value outside its range.
83    #[error("variation {tag:?} is not an axis of the font, or lies outside its range")]
84    InvalidVariation {
85        /// The axis tag of the variation.
86        tag: [u8; 4],
87    },
88}
89
90/// A font loaded for text at one location in its design space, with the
91/// family names, weight and style text chooses it by: Compose's `Typeface`.
92/// Clones share one load; two typefaces are equal when they draw the same
93/// glyphs, from the same bytes at the same axis positions.
94#[derive(Clone)]
95pub struct Typeface(Arc<Loaded>);
96
97impl PartialEq for Typeface {
98    fn eq(&self, other: &Self) -> bool {
99        self.same(other) || self.0.content_hash == other.0.content_hash
100    }
101}
102
103struct Loaded {
104    font: Font,
105    families: Box<[String]>,
106    registered_family: Option<FontFamilyKey>,
107    weight: FontWeight,
108    style: FontStyle,
109    // The weights the typeface draws: its `wght` axis range when it was read
110    // without a declaration, otherwise its one weight.
111    weights: (f32, f32),
112    // Whether an `ital` or `slnt` axis lets the typeface draw italic as well.
113    italic_axis: bool,
114    // The `wght`, `ital` and `slnt` axes a font read without a declaration
115    // draws a weight and style on; none for a registered one, which its
116    // declaration instanced.
117    style_axes: SmallVec<[StyleAxis; 3]>,
118    is_color: bool,
119    // The axis positions the typeface is instanced at, sorted by tag.
120    variations: Box<[([u8; 4], f32)]>,
121    content_hash: u64,
122    found: Found,
123    // The normal track of the font's `trak` table: the tracking in ems at
124    // each point size it lists, ascending; empty without one.
125    tracking: Box<[(f32, f32)]>,
126    // The glyph of the space, whose advance justification stretches.
127    space: Option<GlyphId>,
128}
129
130// What a typeface reads of its font on first use.
131#[derive(Default)]
132struct Found {
133    coverage: OnceLock<Coverage>,
134    glyf: OnceLock<Option<GlyfTables>>,
135    // Which glyphs put ink past their advance or the font's lines, read as
136    // layouts meet them.
137    inks: OnceLock<GlyphInks>,
138}
139
140impl Typeface {
141    /// Reads `bytes` as a typeface no family was declared for: its weight,
142    /// style and family names come from the font.
143    pub fn from_bytes(bytes: impl Into<FontBytes>) -> Result<Self, TypefaceError> {
144        let font = Font::new(blob(bytes.into()), 0).ok_or(TypefaceError::InvalidFont)?;
145        let content_hash = content_hash(&font, &[]);
146        let weight = (&font.tables())
147            .os2()
148            .ok()
149            .and_then(|os2| FontWeight::try_new(os2.us_weight_class()))
150            .unwrap_or_default();
151        let style = match font.default_attributes().style {
152            Style::Normal => FontStyle::Normal,
153            Style::Italic | Style::Oblique(_) => FontStyle::Italic,
154        };
155        let style_axes = style_axes(&font);
156        let axis = |tag: &[u8; 4]| style_axes.iter().find(|axis| axis.tag == *tag);
157        let default_weight = f32::from(weight.value());
158        let weights = axis(b"wght").map_or((default_weight, default_weight), |axis| {
159            (axis.min.max(1.0), axis.max.min(1000.0))
160        });
161        let italic_axis = axis(b"ital").is_some_and(|axis| axis.max >= 1.0)
162            || axis(b"slnt").is_some_and(|axis| axis.min < 0.0);
163        Ok(Self(Arc::new(Loaded {
164            families: family_names(&font),
165            registered_family: None,
166            weight,
167            style,
168            weights,
169            italic_axis,
170            style_axes,
171            is_color: is_color(&font),
172            variations: Box::default(),
173            content_hash,
174            found: Found::default(),
175            tracking: normal_track(&font),
176            space: space_glyph(&font),
177            font,
178        })))
179    }
180
181    /// Reads `bytes` as a typeface an app registered under `family`,
182    /// declaring `weight` and `style` for it, as a Compose `Font(resId,
183    /// weight, style)` entry does: the declaration wins over the font's own
184    /// values, and a variable font is instanced on its `wght`, `ital` and
185    /// `slnt` axes, so one file backs a whole family. The explicit
186    /// `variations` then win over the declared positions; a variation of an
187    /// axis the font lacks, or outside that axis's range, fails.
188    pub fn from_registered(
189        family: &FontFamily,
190        weight: FontWeight,
191        style: FontStyle,
192        bytes: impl Into<FontBytes>,
193        variations: &[([u8; 4], f32)],
194    ) -> Result<Self, TypefaceError> {
195        let font = Font::new(blob(bytes.into()), 0).ok_or(TypefaceError::InvalidFont)?;
196        let mut applied = declared_variations(&style_axes(&font), weight, style);
197        for &(tag, value) in variations {
198            let valid = value.is_finite()
199                && font.axes().iter().any(|axis| {
200                    axis.tag() == Tag::new(&tag)
201                        && (axis.min_value()..=axis.max_value()).contains(&value)
202                });
203            if !valid {
204                return Err(TypefaceError::InvalidVariation { tag });
205            }
206            applied.retain(|(existing, _)| *existing != tag);
207            applied.push((tag, value));
208        }
209        applied.sort_unstable_by_key(|(tag, _)| *tag);
210        let content_hash = content_hash(&font, &applied);
211        let font = if applied.is_empty() {
212            font
213        } else {
214            font.instance_builder()
215                .variations(
216                    applied
217                        .iter()
218                        .map(|(tag, value)| Variation::new(Tag::new(tag), *value)),
219                )
220                .build()
221        };
222        let declared_weight = f32::from(weight.value());
223        Ok(Self(Arc::new(Loaded {
224            families: family_names(&font),
225            registered_family: Some(FontFamilyKey::of(family)),
226            weight,
227            style,
228            weights: (declared_weight, declared_weight),
229            italic_axis: false,
230            style_axes: SmallVec::new(),
231            is_color: is_color(&font),
232            variations: applied.into_boxed_slice(),
233            content_hash,
234            found: Found::default(),
235            tracking: normal_track(&font),
236            space: space_glyph(&font),
237            font,
238        })))
239    }
240
241    /// The family names the font's `name` table gives it, typographic family
242    /// first.
243    pub fn family_names(&self) -> &[String] {
244        &self.0.families
245    }
246
247    /// The family the typeface was registered under, `None` for one read
248    /// with [`Typeface::from_bytes`].
249    pub fn registered_family(&self) -> Option<FontFamilyKey> {
250        self.0.registered_family
251    }
252
253    /// The weight text chooses the typeface by.
254    pub fn weight(&self) -> FontWeight {
255        self.0.weight
256    }
257
258    /// The style text chooses the typeface by.
259    pub fn style(&self) -> FontStyle {
260        self.0.style
261    }
262
263    /// Whether the font draws glyphs in color (a `COLR`, `CBDT`, `sbix` or
264    /// `SVG` table), as emoji fonts do.
265    pub fn is_color(&self) -> bool {
266        self.0.is_color
267    }
268
269    /// What Compose synthesizes for text in `weight` and `style` drawn with
270    /// this typeface, as `synthesis` allows: a bold for a weight of 600 or
271    /// more when the typeface draws none that heavy, and an italic when it
272    /// draws none.
273    pub fn synthesis(
274        &self,
275        weight: FontWeight,
276        style: FontStyle,
277        synthesis: FontSynthesis,
278    ) -> Synthesis {
279        const BOLD: u16 = 600;
280        Synthesis {
281            bold: matches!(synthesis, FontSynthesis::All | FontSynthesis::Weight)
282                && weight.value() >= BOLD
283                && self.0.weights.1 < f32::from(BOLD),
284            italic: matches!(synthesis, FontSynthesis::All | FontSynthesis::Style)
285                && style == FontStyle::Italic
286                && !self.draws(FontStyle::Italic),
287        }
288    }
289
290    // The weights the typeface draws, lowest and highest.
291    pub(crate) fn weights(&self) -> (f32, f32) {
292        self.0.weights
293    }
294
295    // Whether the typeface draws `style`.
296    pub(crate) fn draws(&self, style: FontStyle) -> bool {
297        self.0.style == style || self.0.italic_axis
298    }
299
300    // The characters the font maps to glyphs, read from the `cmap` on first use.
301    pub(crate) fn coverage(&self) -> &Coverage {
302        self.0.found.coverage.get_or_init(|| {
303            Coverage::from_codepoints(
304                self.0
305                    .font
306                    .charmap()
307                    .unicodes()
308                    .filter(|(_, glyph)| *glyph != FontGlyphId::NOTDEF)
309                    .map(|(codepoint, _)| codepoint),
310            )
311        })
312    }
313
314    /// The axis positions the typeface is instanced at, sorted by tag.
315    pub fn variations(&self) -> &[([u8; 4], f32)] {
316        &self.0.variations
317    }
318
319    /// A hash of the font's bytes and its axis positions: equal for two
320    /// loads of one instance.
321    pub fn content_hash(&self) -> u64 {
322        self.0.content_hash
323    }
324
325    /// The font's design units in an em.
326    pub fn units_per_em(&self) -> u16 {
327        self.0.font.units_per_em()
328    }
329
330    /// The font's line metrics at the instance, in design units: the
331    /// typographic line when the font asks for it with `USE_TYPO_METRICS`,
332    /// otherwise the `hhea` line, as Skia and FreeType read them.
333    #[inline(never)]
334    pub fn line_metrics(&self) -> LineMetrics {
335        let metrics = self.0.font.metrics();
336        metrics
337            .h_line()
338            .map_or_else(LineMetrics::default, |line| LineMetrics {
339                ascent: line.ascender.to_f32(),
340                descent: line.descender.to_f32(),
341                line_gap: line.line_gap.to_f32(),
342            })
343    }
344
345    /// Where the font draws an underline and a line through its text, in
346    /// design units: its `post` underline and `OS/2` strikeout, or Skia's
347    /// defaults for a font without them.
348    pub fn decoration_metrics(&self) -> DecorationMetrics {
349        let metrics = self.0.font.style_metrics();
350        let em = f32::from(self.units_per_em());
351        let top = |decoration: Option<read_fonts::model::metrics::Decoration>,
352                   default: (f32, f32)| {
353            decoration.map_or(default, |decoration| {
354                (-decoration.position.to_f32(), decoration.thickness.to_f32())
355            })
356        };
357        let (underline_top, underline_thickness) = top(metrics.underline, (em / 9.0, em / 18.0));
358        let (strike_through_top, strike_through_thickness) =
359            top(metrics.strike_through, (-em * 6.0 / 21.0, em / 18.0));
360        DecorationMetrics {
361            underline_top,
362            underline_thickness,
363            strike_through_top,
364            strike_through_thickness,
365        }
366    }
367
368    // The font's default tracking at `size` pixels per em, in pixels: its
369    // normal track interpolated between the point sizes it lists, held at
370    // either end; zero for a font without a `trak` table.
371    pub(crate) fn tracking(&self, size: f32) -> f32 {
372        let track = &self.0.tracking;
373        if track.is_empty() || !size.is_finite() || size <= 0.0 {
374            return 0.0;
375        }
376        self.tracked(size)
377    }
378
379    // `tracking` of a font with a `trak` table. Out of line, as most fonts
380    // have none.
381    #[inline(never)]
382    fn tracked(&self, size: f32) -> f32 {
383        let track = &self.0.tracking;
384        let above = track.partition_point(|&(point_size, _)| point_size < size);
385        let em = match (
386            above.checked_sub(1).map(|below| track[below]),
387            track.get(above),
388        ) {
389            (Some((low_size, low)), Some(&(high_size, high))) => {
390                low + (high - low) * (size - low_size) / (high_size - low_size)
391            }
392            (Some((_, em)), None) | (None, Some(&(_, em))) => em,
393            (None, None) => 0.0,
394        };
395        em * size
396    }
397
398    /// The glyph the font maps `ch` to, `None` when it has none.
399    pub fn glyph(&self, ch: char) -> Option<GlyphId> {
400        self.0
401            .font
402            .charmap()
403            .map_unicode(ch)
404            .map(|glyph| GlyphId(glyph.to_u32()))
405    }
406
407    // The glyph of the space.
408    pub(crate) fn space(&self) -> Option<GlyphId> {
409        self.0.space
410    }
411
412    /// Whether the font maps `ch` to a glyph.
413    pub fn covers(&self, ch: char) -> bool {
414        self.coverage().contains(ch)
415    }
416
417    /// The horizontal advance of `glyph` at the instance, in design units.
418    pub fn advance(&self, glyph: GlyphId) -> f32 {
419        self.0
420            .font
421            .glyph_metrics()
422            .h_advance(FontGlyphId::new(glyph.0))
423    }
424
425    // Whether `other` is a clone of this typeface.
426    pub(crate) fn same(&self, other: &Self) -> bool {
427        Arc::ptr_eq(&self.0, &other.0)
428    }
429
430    // The font model at the typeface's instance.
431    pub(crate) fn font(&self) -> &Font {
432        &self.0.font
433    }
434
435    // Where `glyph` puts ink, in design units: from its `glyf` header for a
436    // simple TrueType font, otherwise as the general loader measures it.
437    pub(crate) fn glyph_extents(&self, glyph: GlyphId) -> Option<GlyphExtents<f32>> {
438        let id = FontGlyphId::new(glyph.0);
439        match self.glyf_tables() {
440            Some(tables) => simple_glyf::extents(&self.0.font, tables, id),
441            None => self.0.font.glyph_metrics().extents(id),
442        }
443    }
444
445    // Which glyphs put ink past their advance or the font's lines.
446    pub(crate) fn glyph_inks(&self) -> &GlyphInks {
447        self.0.found.inks.get_or_init(|| GlyphInks::of(self))
448    }
449
450    // Where the font keeps its simple glyphs, found on first use; `None`
451    // for a font the general loader draws.
452    pub(crate) fn glyf_tables(&self) -> Option<&GlyfTables> {
453        self.0
454            .found
455            .glyf
456            .get_or_init(|| GlyfTables::of(&self.0.font))
457            .as_ref()
458    }
459
460    // The axis positions that draw the typeface at `weight` and `style`: for
461    // a variable font read without a declaration, its `wght`, `ital` and
462    // `slnt` positions; none for a registered typeface, which its declaration
463    // already instanced.
464    pub(crate) fn axis_positions(
465        &self,
466        weight: FontWeight,
467        style: FontStyle,
468    ) -> SmallVec<[([u8; 4], f32); 3]> {
469        declared_variations(&self.0.style_axes, weight, style)
470    }
471}
472
473impl std::fmt::Debug for Typeface {
474    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        formatter
476            .debug_struct("Typeface")
477            .field("family_names", &self.0.families)
478            .field("weight", &self.0.weight)
479            .field("style", &self.0.style)
480            .field("variations", &self.0.variations)
481            .finish_non_exhaustive()
482    }
483}
484
485/// What a typeface lacks for a text's weight and style that is drawn in its
486/// place.
487#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
488pub struct Synthesis {
489    /// A bold the typeface lacks.
490    pub bold: bool,
491    /// An italic the typeface lacks.
492    pub italic: bool,
493}
494
495/// Where a font draws an underline and a line through its text, in design
496/// units: Android's underline and strike-through metrics.
497#[derive(Clone, Copy, Debug, Default, PartialEq)]
498pub struct DecorationMetrics {
499    /// How far below the baseline the top of an underline sits.
500    pub underline_top: f32,
501    /// How thick an underline is.
502    pub underline_thickness: f32,
503    /// How far below the baseline the top of a line through the text sits;
504    /// negative above it.
505    pub strike_through_top: f32,
506    /// How thick a line through the text is.
507    pub strike_through_thickness: f32,
508}
509
510/// A font's vertical line metrics, in design units.
511#[derive(Clone, Copy, Debug, Default, PartialEq)]
512pub struct LineMetrics {
513    /// Height above the baseline.
514    pub ascent: f32,
515    /// Depth below the baseline, negative.
516    pub descent: f32,
517    /// Space between one line's bottom and the next line's top.
518    pub line_gap: f32,
519}
520
521fn blob(bytes: FontBytes) -> Blob {
522    match bytes {
523        FontBytes::Static(bytes) => Blob::from(bytes),
524        FontBytes::Owned(bytes) => Blob::from(bytes),
525    }
526}
527
528// The normal track of `font`'s `trak` table, the tracking in ems at each
529// point size it lists; empty for a font without one or with sizes out of
530// order.
531fn normal_track(font: &Font) -> Box<[(f32, f32)]> {
532    let read = || {
533        let trak = font.tables().trak().ok()?;
534        let data = trak.horiz()?.ok()?;
535        let offset_data = trak.offset_data();
536        let normal = data
537            .track_table()
538            .iter()
539            .find(|track| track.track() == Fixed::ZERO)?;
540        let values = normal.per_size_values(offset_data, data.n_sizes())?;
541        let sizes = data.size_table(offset_data)?;
542        let em = f32::from(font.units_per_em());
543        let mut points = Vec::with_capacity(sizes.len());
544        for (size, value) in sizes.iter().zip(values) {
545            let size = size.get().to_f32();
546            if size <= 0.0 || points.last().is_some_and(|&(previous, _)| size <= previous) {
547                return None;
548            }
549            points.push((size, f32::from(value.get()) / em));
550        }
551        Some(points.into_boxed_slice())
552    };
553    read().unwrap_or_default()
554}
555
556// The model's tables expose no color tables, so this reads the table
557// directory of the bytes.
558fn is_color(font: &Font) -> bool {
559    let Source::Blob(bytes) = font.source() else {
560        return false;
561    };
562    FontRef::from_index(bytes.as_ref(), 0).is_ok_and(|font| {
563        [*b"COLR", *b"CBDT", *b"sbix", *b"SVG "]
564            .iter()
565            .any(|tag| font.table_data(Tag::new(tag)).is_some())
566    })
567}
568
569// The state every hash of this module starts from, the same each run, so a
570// family key and a content hash stay put for the process's life.
571const HASH_STATE: FixedState = FixedState::with_seed(0);
572
573// A hash of the font's bytes and the axis positions it is instanced at.
574fn content_hash(font: &Font, variations: &[([u8; 4], f32)]) -> u64 {
575    let mut state = HASH_STATE.build_hasher();
576    if let Source::Blob(bytes) = font.source() {
577        bytes.as_ref().hash(&mut state);
578    }
579    for (tag, value) in variations {
580        tag.hash(&mut state);
581        value.to_bits().hash(&mut state);
582    }
583    state.finish()
584}
585
586fn family_names(font: &Font) -> Box<[String]> {
587    let names = font.names();
588    let mut families: Vec<String> = Vec::new();
589    for id in [NameId::TYPOGRAPHIC_FAMILY_NAME, NameId::FAMILY_NAME] {
590        let Some(name) = names.get(id) else {
591            continue;
592        };
593        for localized in name.iter() {
594            let value: String = localized.name().chars().collect();
595            if !value.is_empty()
596                && !families
597                    .iter()
598                    .any(|existing| existing.eq_ignore_ascii_case(&value))
599            {
600                families.push(value);
601            }
602        }
603    }
604    families.into_boxed_slice()
605}
606
607// The positions a declared weight and style ask of the font's axes, clamped
608// into each axis's range.
609fn declared_variations(
610    axes: &[StyleAxis],
611    weight: FontWeight,
612    style: FontStyle,
613) -> SmallVec<[([u8; 4], f32); 3]> {
614    const OBLIQUE_DEGREES: f32 = -12.0;
615    axes.iter()
616        .filter_map(|axis| {
617            let requested = match &axis.tag {
618                b"wght" => f32::from(weight.value()),
619                b"ital" if style == FontStyle::Italic => 1.0,
620                b"slnt" if style == FontStyle::Italic => OBLIQUE_DEGREES,
621                _ => return None,
622            };
623            Some((axis.tag, requested.clamp(axis.min, axis.max)))
624        })
625        .collect()
626}
627
628// An axis a weight or style sets, with its range.
629#[derive(Clone, Copy, Debug)]
630struct StyleAxis {
631    tag: [u8; 4],
632    min: f32,
633    max: f32,
634}
635
636// The `wght`, `ital` and `slnt` axes of `font`.
637fn style_axes(font: &Font) -> SmallVec<[StyleAxis; 3]> {
638    font.axes()
639        .iter()
640        .map(|axis| StyleAxis {
641            tag: axis.tag().to_be_bytes(),
642            min: axis.min_value(),
643            max: axis.max_value(),
644        })
645        .filter(|axis| matches!(&axis.tag, b"wght" | b"ital" | b"slnt"))
646        .collect()
647}
648
649fn space_glyph(font: &Font) -> Option<GlyphId> {
650    font.charmap()
651        .map_unicode(' ')
652        .map(|glyph| GlyphId(glyph.to_u32()))
653}